From 956e9f26b1c0740e70d5f28f86d1d96c76f287eb Mon Sep 17 00:00:00 2001 From: demetrius albuquerque Date: Sat, 25 Jul 2026 12:51:20 +0200 Subject: [PATCH 1/6] fix(ArrowReader): add guard checks for nil schema and RecordBatch Replace force unwrapping with proper guard statements to prevent crashes when parsing malformed Arrow files without a schema or RecordBatch header. --- Sources/Arrow/ArrowReader.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/Arrow/ArrowReader.swift b/Sources/Arrow/ArrowReader.swift index d1c0853..d50123e 100644 --- a/Sources/Arrow/ArrowReader.swift +++ b/Sources/Arrow/ArrowReader.swift @@ -282,11 +282,19 @@ public class ArrowReader { // swiftlint:disable:this type_body_length let message: org_apache_arrow_flatbuf_Message = getRoot(byteBuffer: &dataBuffer) switch message.headerType { case .recordbatch: - let rbMessage = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self)! + guard let rbMessage = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self) else { + return .failure(.invalid("RecordBatch header not found")) + } + guard let schemaMsg = schemaMessage else { + return .failure(.invalid("Schema must be defined before RecordBatch")) + } + guard let schema = result.schema else { + return .failure(.invalid("Schema not loaded")) + } let recordBatchResult = loadRecordBatch( rbMessage, - schema: schemaMessage!, - arrowSchema: result.schema!, + schema: schemaMsg, + arrowSchema: schema, data: input, messageEndOffset: (Int64(offset) + Int64(length))) switch recordBatchResult { From 699bc40efcda287db4a71d93c6b73acac5e5ffbc Mon Sep 17 00:00:00 2001 From: demetrius albuquerque Date: Sat, 25 Jul 2026 12:56:00 +0200 Subject: [PATCH 2/6] fix(ArrowReader): add safe unwrapping for footer schema and record batch Replace force unwraps with guard statements when accessing footer schema and RecordBatch messages to prevent crashes on malformed Arrow files. --- Sources/Arrow/ArrowReader.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/Arrow/ArrowReader.swift b/Sources/Arrow/ArrowReader.swift index d50123e..b4b62e9 100644 --- a/Sources/Arrow/ArrowReader.swift +++ b/Sources/Arrow/ArrowReader.swift @@ -343,7 +343,10 @@ public class ArrowReader { // swiftlint:disable:this type_body_length data: footerData, allowReadingUnalignedBuffers: useUnalignedBuffers) let footer: org_apache_arrow_flatbuf_Footer = getRoot(byteBuffer: &footerBuffer) - let schemaResult = loadSchema(footer.schema!) + guard let footerSchema = footer.schema else { + return .failure(.invalid("Footer schema not found")) + } + let schemaResult = loadSchema(footerSchema) switch schemaResult { case .success(let schema): result.schema = schema @@ -376,11 +379,16 @@ public class ArrowReader { // swiftlint:disable:this type_body_length let message: org_apache_arrow_flatbuf_Message = getRoot(byteBuffer: &mbb) switch message.headerType { case .recordbatch: - let rbMessage = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self)! + guard let rbMessage = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self) else { + return .failure(.invalid("RecordBatch header not found")) + } + guard let schema = result.schema else { + return .failure(.invalid("Schema not loaded")) + } let recordBatchResult = loadRecordBatch( rbMessage, - schema: footer.schema!, - arrowSchema: result.schema!, + schema: footerSchema, + arrowSchema: schema, data: fileData, messageEndOffset: messageEndOffset) switch recordBatchResult { From 64dc89f5f22f586f73fc46c0ca25827772cd2e0a Mon Sep 17 00:00:00 2001 From: demetrius albuquerque Date: Sat, 25 Jul 2026 12:58:20 +0200 Subject: [PATCH 3/6] fix(ArrowReader): add safe unwrapping for schema and record batch messages Replace force unwraps with guard statements when parsing Message headers to prevent crashes on malformed Arrow streams. --- Sources/Arrow/ArrowReader.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/Arrow/ArrowReader.swift b/Sources/Arrow/ArrowReader.swift index b4b62e9..3e0e620 100644 --- a/Sources/Arrow/ArrowReader.swift +++ b/Sources/Arrow/ArrowReader.swift @@ -437,7 +437,9 @@ public class ArrowReader { // swiftlint:disable:this type_body_length let message: org_apache_arrow_flatbuf_Message = getRoot(byteBuffer: &mbb) switch message.headerType { case .schema: - let sMessage = message.header(type: org_apache_arrow_flatbuf_Schema.self)! + guard let sMessage = message.header(type: org_apache_arrow_flatbuf_Schema.self) else { + return .failure(.invalid("Schema header not found")) + } switch loadSchema(sMessage) { case .success(let schema): result.schema = schema @@ -447,9 +449,17 @@ public class ArrowReader { // swiftlint:disable:this type_body_length return .failure(error) } case .recordbatch: - let rbMessage = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self)! + guard let rbMessage = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self) else { + return .failure(.invalid("RecordBatch header not found")) + } + guard let messageSchema = result.messageSchema else { + return .failure(.invalid("Schema must be defined before RecordBatch")) + } + guard let schema = result.schema else { + return .failure(.invalid("Schema not loaded")) + } let recordBatchResult = loadRecordBatch( - rbMessage, schema: result.messageSchema!, arrowSchema: result.schema!, + rbMessage, schema: messageSchema, arrowSchema: schema, data: dataBody, messageEndOffset: 0) switch recordBatchResult { case .success(let recordBatch): From 8b8e0bade4224cf84a99a408da6242766b929340 Mon Sep 17 00:00:00 2001 From: demetrius albuquerque Date: Sat, 25 Jul 2026 13:06:09 +0200 Subject: [PATCH 4/6] test(IPCStreamReader): add test for RecordBatch before Schema Adds a test that verifies readStreaming fails gracefully when a RecordBatch message appears without a preceding Schema message, instead of crashing. --- Tests/ArrowTests/IPCTests.swift | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Tests/ArrowTests/IPCTests.swift b/Tests/ArrowTests/IPCTests.swift index 58f973e..fcc2172 100644 --- a/Tests/ArrowTests/IPCTests.swift +++ b/Tests/ArrowTests/IPCTests.swift @@ -262,6 +262,50 @@ final class IPCStreamReaderTests: XCTestCase { throw error } } + + func testReadStreamingRecordBatchBeforeSchema() throws { + // Build a minimal streaming message: a RecordBatch header with no + // preceding Schema message. This should fail gracefully instead + // of crashing on a force unwrap. + let schema = makeSchema() + let recordBatch = try makeRecordBatch() + let arrowWriter = ArrowWriter() + let writerInfo = ArrowWriter.Info(.recordbatch, schema: schema, batches: [recordBatch]) + + switch arrowWriter.writeStreaming(writerInfo) { + case .success(let writeData): + // Mirror the parsing logic in ArrowReader.readStreaming to advance + // past exactly one message (the Schema message), leaving the + // RecordBatch message intact and correctly positioned for + // readStreaming to parse on its own. + var offset = 0 + var length = getUInt32(writeData, offset: offset) + if length == CONTINUATIONMARKER { + offset += Int(MemoryLayout.size) + length = getUInt32(writeData, offset: offset) + } + offset += Int(MemoryLayout.size) + + var dataBuffer = ByteBuffer( + data: writeData[offset...], + allowReadingUnalignedBuffers: false) + let message: org_apache_arrow_flatbuf_Message = getRoot(byteBuffer: &dataBuffer) + XCTAssertEqual(message.headerType, .schema) + + offset += Int(message.bodyLength + Int64(length)) + let truncatedData = Data(writeData[offset...]) + + let arrowReader = ArrowReader() + switch arrowReader.readStreaming(truncatedData) { + case .success: + XCTFail("Expected failure when RecordBatch precedes Schema") + case .failure: + break // Correct: should fail gracefully, not crash + } + case .failure(let error): + throw error + } + } } final class IPCFileReaderTests: XCTestCase { // swiftlint:disable:this type_body_length @@ -671,5 +715,6 @@ final class IPCFileReaderTests: XCTestCase { // swiftlint:disable:this type_body throw error } } + } // swiftlint:disable:this file_length From 2d375929456359d85f2bdf27215643cfe8419675 Mon Sep 17 00:00:00 2001 From: demetrius albuquerque Date: Sat, 25 Jul 2026 13:28:22 +0200 Subject: [PATCH 5/6] fix(ArrowReader): use absolute indices for stream data slicing Replace relative offset slicing with startIndex-based indexing to ensure correct data boundaries when parsing streaming Arrow messages. --- Sources/Arrow/ArrowReader.swift | 2 +- Tests/ArrowTests/IPCTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Arrow/ArrowReader.swift b/Sources/Arrow/ArrowReader.swift index 3e0e620..ee91eba 100644 --- a/Sources/Arrow/ArrowReader.swift +++ b/Sources/Arrow/ArrowReader.swift @@ -274,7 +274,7 @@ public class ArrowReader { // swiftlint:disable:this type_body_length } offset += Int(MemoryLayout.size) - streamData = input[offset...] + streamData = input[(input.startIndex + offset)...] var dataBuffer = ByteBuffer( data: streamData, allowReadingUnalignedBuffers: useUnalignedBuffers diff --git a/Tests/ArrowTests/IPCTests.swift b/Tests/ArrowTests/IPCTests.swift index fcc2172..2aa9a3d 100644 --- a/Tests/ArrowTests/IPCTests.swift +++ b/Tests/ArrowTests/IPCTests.swift @@ -293,7 +293,7 @@ final class IPCStreamReaderTests: XCTestCase { XCTAssertEqual(message.headerType, .schema) offset += Int(message.bodyLength + Int64(length)) - let truncatedData = Data(writeData[offset...]) + let truncatedData = writeData[offset...] let arrowReader = ArrowReader() switch arrowReader.readStreaming(truncatedData) { From 10a2c908988c85961cdf610703d4573c64179886 Mon Sep 17 00:00:00 2001 From: demetrius albuquerque Date: Sat, 25 Jul 2026 15:58:20 +0200 Subject: [PATCH 6/6] style: Fix comment indentation flagged by SwiftLint --- Tests/ArrowTests/IPCTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ArrowTests/IPCTests.swift b/Tests/ArrowTests/IPCTests.swift index 2aa9a3d..7d7e972 100644 --- a/Tests/ArrowTests/IPCTests.swift +++ b/Tests/ArrowTests/IPCTests.swift @@ -264,9 +264,9 @@ final class IPCStreamReaderTests: XCTestCase { } func testReadStreamingRecordBatchBeforeSchema() throws { - // Build a minimal streaming message: a RecordBatch header with no - // preceding Schema message. This should fail gracefully instead - // of crashing on a force unwrap. + // Build a minimal streaming message: a RecordBatch header with no + // preceding Schema message. This should fail gracefully instead + // of crashing on a force unwrap. let schema = makeSchema() let recordBatch = try makeRecordBatch() let arrowWriter = ArrowWriter()