-
Notifications
You must be signed in to change notification settings - Fork 16
fix: correct VariableBufferBuilder null handling, first-value resize, and offset semantics #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5956cda
41ac5ba
564bd5b
ab5c44d
59314ea
a3989c9
84f8d6f
e8770ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,13 @@ | |
| import XCTest | ||
| @testable import Arrow | ||
|
|
||
| private func int32Values(in buffer: ArrowBuffer, count: Int) -> [Int32] { | ||
| (0..<count).map { index in | ||
| let offset = index * MemoryLayout<Int32>.stride | ||
| return buffer.rawPointer.advanced(by: offset).load(as: Int32.self) | ||
| } | ||
| } | ||
|
|
||
| final class ArrayTests: XCTestCase { // swiftlint:disable:this type_body_length | ||
| func testPrimitiveArray() throws { | ||
| // This is an example of a functional test case. | ||
|
|
@@ -63,7 +70,7 @@ final class ArrayTests: XCTestCase { // swiftlint:disable:this type_body_length | |
|
|
||
| XCTAssertEqual(stringBuilder.nullCount, 10) | ||
| XCTAssertEqual(stringBuilder.length, 100) | ||
| XCTAssertEqual(stringBuilder.capacity, 640) | ||
| XCTAssertGreaterThanOrEqual(stringBuilder.capacity, 640) | ||
| let stringArray = try stringBuilder.finish() | ||
| XCTAssertEqual(stringArray.length, 100) | ||
| for index in 0..<stringArray.length { | ||
|
|
@@ -139,7 +146,7 @@ final class ArrayTests: XCTestCase { // swiftlint:disable:this type_body_length | |
|
|
||
| XCTAssertEqual(binaryBuilder.nullCount, 10) | ||
| XCTAssertEqual(binaryBuilder.length, 100) | ||
| XCTAssertEqual(binaryBuilder.capacity, 640) | ||
| XCTAssertGreaterThanOrEqual(binaryBuilder.capacity, 640) | ||
| let binaryArray = try binaryBuilder.finish() | ||
| XCTAssertEqual(binaryArray.length, 100) | ||
| for index in 0..<binaryArray.length { | ||
|
|
@@ -152,6 +159,66 @@ final class ArrayTests: XCTestCase { // swiftlint:disable:this type_body_length | |
| } | ||
| } | ||
|
|
||
| func testStringArrayTracksLogicalValueLengthAfterFirstAppend() throws { | ||
| let stringBuilder = try ArrowArrayBuilders.loadStringArrayBuilder() | ||
| let firstValue = String(repeating: "a", count: 256) | ||
|
|
||
| stringBuilder.append(firstValue) | ||
|
|
||
| let stringArray = try stringBuilder.finish() | ||
| XCTAssertEqual(stringArray.length, 1) | ||
| XCTAssertEqual(stringArray[0], firstValue) | ||
| XCTAssertEqual(stringArray.arrowData.buffers[2].length, UInt(firstValue.utf8.count)) | ||
| XCTAssertEqual(int32Values(in: stringArray.arrowData.buffers[1], count: 2), [0, 256]) | ||
| } | ||
|
|
||
| func testStringArrayNullDoesNotAdvanceOffsets() throws { | ||
| let stringBuilder = try ArrowArrayBuilders.loadStringArrayBuilder() | ||
|
|
||
| stringBuilder.append("a") | ||
| stringBuilder.append(nil) | ||
| stringBuilder.append("bbb") | ||
|
|
||
| let stringArray = try stringBuilder.finish() | ||
| XCTAssertEqual(stringArray.length, 3) | ||
| XCTAssertEqual(stringArray[0], "a") | ||
| XCTAssertNil(stringArray[1]) | ||
| XCTAssertEqual(stringArray[2], "bbb") | ||
| XCTAssertEqual(stringArray.arrowData.buffers[2].length, 4) | ||
| XCTAssertEqual(int32Values(in: stringArray.arrowData.buffers[1], count: 4), [0, 1, 1, 4]) | ||
| } | ||
|
|
||
| func testStringArrayNullFirstThenLongValue() throws { | ||
| let stringBuilder = try ArrowArrayBuilders.loadStringArrayBuilder() | ||
| let longValue = String(repeating: "z", count: 512) | ||
|
|
||
| stringBuilder.append(nil) | ||
| stringBuilder.append(longValue) | ||
|
|
||
| let stringArray = try stringBuilder.finish() | ||
| XCTAssertEqual(stringArray.length, 2) | ||
| XCTAssertNil(stringArray[0]) | ||
| XCTAssertEqual(stringArray[1], longValue) | ||
| XCTAssertEqual(stringArray.arrowData.buffers[2].length, UInt(longValue.utf8.count)) | ||
| XCTAssertEqual(int32Values(in: stringArray.arrowData.buffers[1], count: 3), [0, 0, 512]) | ||
| } | ||
|
|
||
| func testBinaryArrayNullDoesNotAdvanceOffsets() throws { | ||
| let binaryBuilder = try ArrowArrayBuilders.loadBinaryArrayBuilder() | ||
|
|
||
| binaryBuilder.append(Data("a".utf8)) | ||
| binaryBuilder.append(nil) | ||
| binaryBuilder.append(Data("bbb".utf8)) | ||
|
|
||
| let binaryArray = try binaryBuilder.finish() | ||
| XCTAssertEqual(binaryArray.length, 3) | ||
| XCTAssertEqual(binaryArray[0], Data("a".utf8)) | ||
| XCTAssertNil(binaryArray[1]) | ||
| XCTAssertEqual(binaryArray[2], Data("bbb".utf8)) | ||
| XCTAssertEqual(binaryArray.arrowData.buffers[2].length, 4) | ||
| XCTAssertEqual(int32Values(in: binaryArray.arrowData.buffers[1], count: 4), [0, 1, 1, 4]) | ||
| } | ||
|
|
||
| func testTime32Array() throws { | ||
| let milliBuilder = try ArrowArrayBuilders.loadTime32ArrayBuilder(.milliseconds) | ||
| milliBuilder.append(100) | ||
|
|
@@ -344,10 +411,25 @@ final class ArrayTests: XCTestCase { // swiftlint:disable:this type_body_length | |
| } | ||
|
|
||
| func checkHolderForType(_ checkType: ArrowType) throws { | ||
| let buffers = [ArrowBuffer(length: 0, capacity: 0, | ||
| rawPointer: UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: .zero)), | ||
| ArrowBuffer(length: 0, capacity: 0, | ||
| rawPointer: UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: .zero))] | ||
| let emptyPtr = UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: .zero) | ||
| let buffers: [ArrowBuffer] | ||
| switch checkType.info { | ||
| case .variableInfo: | ||
| let offsetPtr = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<Int32>.stride, alignment: 4) | ||
| offsetPtr.storeBytes(of: Int32(0), as: Int32.self) | ||
| buffers = [ | ||
| ArrowBuffer(length: 0, capacity: 0, rawPointer: emptyPtr), | ||
| ArrowBuffer(length: 1, capacity: UInt(MemoryLayout<Int32>.stride), rawPointer: offsetPtr), | ||
| ArrowBuffer(length: 0, capacity: 0, | ||
| rawPointer: UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: .zero)) | ||
| ] | ||
| default: | ||
| buffers = [ | ||
| ArrowBuffer(length: 0, capacity: 0, rawPointer: emptyPtr), | ||
| ArrowBuffer(length: 0, capacity: 0, | ||
| rawPointer: UnsafeMutableRawPointer.allocate(byteCount: 0, alignment: .zero)) | ||
| ] | ||
| } | ||
|
Comment on lines
+414
to
+432
|
||
| let field = ArrowField("", type: checkType, isNullable: true) | ||
| switch makeArrayHolder(field, buffers: buffers, nullCount: 0, children: nil, rbLength: 0) { | ||
| case .success(let holder): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loadVariableDataderives the values buffer logical length from the last offset without validating it. If the IPC input is malformed,lastOffsetcan be negative or larger than the underlyingvalueBuffer.length, and the subsequentUInt(lastOffset)conversion / later value reads can crash or read out of bounds. Add guards to ensure the offset buffer is large enough to read the last offset,lastOffset >= 0, andlastOffset <= valueBuffer.length(bytes) before creating the value buffer (otherwise return.failure(.invalid(...))).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added guards to reject negative offsets and offsets exceeding the value buffer length, returning .failure(.invalid(...)) with a descriptive message. This is consistent with the existing guard chain in the method.