0

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)
Bram
  • 2,718
  • 1
  • 22
  • 43
  • `Data` conforms to `RangeReplaceableCollection`. Why don't you use `removeSubrange`? Btw `Data.Index` is just a typeAlias to `Int` – Leo Dabus Aug 11 '20 at 22:39
  • Note that `removeSubrange` works for any `RangeExpression` – Leo Dabus Aug 11 '20 at 22:43
  • `removeSubrange` doesn't return the removed data. I need the removed data similar to `data.removeFirst()` which returns the `UInt8` that was removed. `removeSubrange` performs the operation correctly, but returns `Void`. – Bram Aug 11 '20 at 22:46
  • Do you want me to show how this should be implemented? I mean a generic method that would be like removeSubrange but returns what is being removed. I would also remove the preconditions – Leo Dabus Aug 11 '20 at 22:50
  • @LeoDabus Yes please! – Bram Aug 11 '20 at 22:58
  • Btw your pre condition is wrong – Leo Dabus Aug 11 '20 at 23:03
  • Why? Range can go below zero, while the index of a collection cannot and the upperbound can never be greater or equal than the amount of bytes in the collection. If the collection is 100 bytes long, the max index is 99. I believe my preconditions are correct... – Bram Aug 11 '20 at 23:14
  • 1
    try `var data = Data([0,1,2,3,4,5])` `let subdata = data.remove(at: 0..<6)` it wiill throw **Precondition failed: Range invalid, upper bound exceeds data size** Range upper bound can be equal to count – Leo Dabus Aug 11 '20 at 23:16
  • Ah, right. So the `count >= 0` is correct, the other one should be `count <= upperBound`, of course! – Bram Aug 11 '20 at 23:25
  • 1
    No. it should be the opposite `upperbound <= count` – Leo Dabus Aug 11 '20 at 23:34

2 Answers2

1

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")
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • This is similar to an earlier implementation I had. This does still give me an error. I have reasons to believe that this may be due some lazy loading and/or properties, which means some of the data is not yet present at the time of executing this code. Kind of explains the bad instruction error, as by the time the debugger is attached, the memory is loaded – Bram Aug 11 '20 at 23:16
  • You should always check the data indices before removing any content – Leo Dabus Aug 11 '20 at 23:20
  • Can you elaborate on _check indices_? – Bram Aug 11 '20 at 23:23
  • I've updated my question with the calling functions. The error persists. – Bram Aug 12 '20 at 11:50
0

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.

Bram
  • 2,718
  • 1
  • 22
  • 43