Data.subdata(in:) results in EXC_BAD_INSTRUCTION

747 Views Asked by At

While trying to retrieve subdata of a Data object, the application crashes issuing the following error:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

Below you can see the code. It's a Data extension. Hope someone can explain why this crashes.

public extension Data {
    /// Removes and returns the range of data at the specified position.
    /// - Parameter range: The range to remove. `range` must be valid
    /// for the collection and should not exceed the collection's end index.
    /// - Returns: The removed data.
    mutating func remove(at range: Range<Data.Index>) -> Self {
        precondition(range.lowerBound >= 0, "Range invalid, lower bound cannot be below 0")
        precondition(range.upperBound < self.count, "Range invalid, upper bound exceeds data size")
        
        let removal = subdata(in: range) // <- Error occurs here
        removeSubrange(range)
        return removal
    }
}

EDIT - added the caller functions:

This extension is called from the following function:

func temporary(data: inout Data) -> Data {
    let _ = data.removeFirst()
    return data.remove(range: 0 ..< 3)
}

Which in turn is called like this:

var data = Data([0,1,2,3,4,5])
let subdata = temporary(data: &data)
2

There are 2 best solutions below

1
Bram On BEST ANSWER

The error is caused by the removeFirst function. The documentation clearly states:

Calling this method may invalidate all saved indices of this collection. Do not rely on a previously stored index value after altering a collection with any operation that can change its length.

It appears that is exactly what is causing my error. I have replaced removeFirst with remove(at:) and it now works.

4
Leo Dabus On

You haven't provided enough information for us to know the reason of your crash. One thing that I know that is wrong in your method is your precondition. You wont be able to pass a range to remove all elements of your collection. Besides that you should implement a generic method that would take a RangeExpression instead of a Range. This is how I would implement such method:

extension Data {
    /// Removes and returns the range of data at the specified position.
    /// - Parameter range: The range to remove. `range` must be valid
    /// for the collection and should not exceed the collection's end index.
    /// - Returns: The removed data.
    mutating func remove<R>(_ range: R) -> Data where R: RangeExpression, Index == R.Bound {
        defer { removeSubrange(range) }
        return subdata(in: range.relative(to: self))
    }
}

Usage:

var data = Data([0,1,2,3,4,5])
let subdata = data.remove(0..<6)
print(Array(data), Array(subdata))  // "[] [0, 1, 2, 3, 4, 5]\n"

To check if your data indices contains a specific range before attempting to remove you can use pattern-matching operator:

var data = Data([0,1,2,3,4,5])
let range = 0..<7
if data.indices ~= range {
    let subdata = data.remove(range)
    print(Array(data), Array(subdata))
} else {
    print("invalid subrange")  // "invalid subrange\n"
}

If you would like to do the same with a ClosedRange you would need to implement your own pattern-matching operator on Range:

extension Range {
    static func ~=(lhs: Self, rhs: ClosedRange<Bound>) -> Bool {
        lhs.contains(rhs.lowerBound) && lhs.contains(rhs.upperBound)
    }
}

Usage:

var data = Data([0,1,2,3,4,5])
let range = 0...5
if data.indices ~= range {
    let subdata = data.remove(range)
    print(Array(data), Array(subdata))  // "[] [0, 1, 2, 3, 4, 5]\n"
} else {
    print("invalid subrange")
}