diff --git a/Scripts/format.sh b/Scripts/format.sh index b05c68b..86da148 100755 --- a/Scripts/format.sh +++ b/Scripts/format.sh @@ -18,3 +18,5 @@ echo "Formatting Swift sources in $(pwd)" # Run the format / lint commands git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel + +sed -i '' 's/borrowbuffer/borrow buffer/g' "Sources/BinaryParsing/Parser Types/ParserSpan.swift" diff --git a/Sources/BinaryParsing/Parser Types/ParserRange.swift b/Sources/BinaryParsing/Parser Types/ParserRange.swift index a9e4571..85f77b5 100644 --- a/Sources/BinaryParsing/Parser Types/ParserRange.swift +++ b/Sources/BinaryParsing/Parser Types/ParserRange.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -public struct ParserRange: Hashable { +public struct ParserRange: Hashable, Sendable { @usableFromInline internal var range: Range diff --git a/Sources/BinaryParsing/Parser Types/ParserSource.swift b/Sources/BinaryParsing/Parser Types/ParserSource.swift index 6f55dec..9ad8c6c 100644 --- a/Sources/BinaryParsing/Parser Types/ParserSource.swift +++ b/Sources/BinaryParsing/Parser Types/ParserSource.swift @@ -122,6 +122,7 @@ extension Data: ParserSpanProvider { #endif extension ParserSpanProvider where Self: RandomAccessCollection { + @discardableResult @inlinable public func withParserSpan( _ body: (inout ParserSpan) throws(ThrownParsingError) -> T diff --git a/Sources/BinaryParsing/Parser Types/ParsingError.swift b/Sources/BinaryParsing/Parser Types/ParsingError.swift index 99370ef..a20b89c 100644 --- a/Sources/BinaryParsing/Parser Types/ParsingError.swift +++ b/Sources/BinaryParsing/Parser Types/ParsingError.swift @@ -94,6 +94,19 @@ extension ParsingError: CustomStringConvertible { } } } + +extension ParsingError.Status: CustomStringConvertible { + public var description: String { + switch self.rawValue { + case .insufficientData: + "insufficient data" + case .invalidValue: + "invalid value" + case .userError: + "user error" + } + } +} #endif #if !$Embedded diff --git a/Sources/BinaryParsing/Parser Types/Seeking.swift b/Sources/BinaryParsing/Parser Types/Seeking.swift index b98470c..c757b5f 100644 --- a/Sources/BinaryParsing/Parser Types/Seeking.swift +++ b/Sources/BinaryParsing/Parser Types/Seeking.swift @@ -97,11 +97,10 @@ extension ParserSpan { throws(ParsingError) { guard let offset = Int(exactly: offset), - (0..._bytes.byteCount).contains(offset) + (0...endPosition).contains(offset) else { throw ParsingError(status: .invalidValue, location: startPosition) } - self._lowerBound = _bytes.byteCount &- offset - self._upperBound = _bytes.byteCount + self._lowerBound = endPosition &- offset } } diff --git a/Sources/BinaryParsing/Parser Types/Slicing.swift b/Sources/BinaryParsing/Parser Types/Slicing.swift index 83d6155..00bdd10 100644 --- a/Sources/BinaryParsing/Parser Types/Slicing.swift +++ b/Sources/BinaryParsing/Parser Types/Slicing.swift @@ -15,7 +15,7 @@ extension ParserSpan { public mutating func sliceSpan(byteCount: some FixedWidthInteger) throws(ParsingError) -> ParserSpan { - guard let byteCount = Int(exactly: byteCount), count >= 0 else { + guard let byteCount = Int(exactly: byteCount), byteCount >= 0 else { throw ParsingError(status: .invalidValue, location: startPosition) } guard count >= byteCount else { @@ -60,18 +60,25 @@ extension ParserSpan { try sliceSpan(objectStride: objectStride, objectCount: objectCount) .parserRange } + + @inlinable + @lifetime(&self) + public mutating func sliceRemainingRange() -> ParserRange { + divide(atOffset: self.count).parserRange + } } extension ParserSpan { @inlinable @lifetime(copy self) - @available(macOS 9999, *) + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) public mutating func sliceUTF8Span(byteCount: some FixedWidthInteger) throws(ParsingError) -> UTF8Span { let rawSpan = try sliceSpan(byteCount: byteCount).bytes do { - return try UTF8Span(validating: Span(_bytes: rawSpan)) + let span = Span(_bytes: rawSpan) + return try UTF8Span(validating: span) } catch { throw ParsingError(status: .userError, location: startPosition) } diff --git a/Tests/BinaryParsingTests/ArrayParsingTests.swift b/Tests/BinaryParsingTests/ArrayParsingTests.swift new file mode 100644 index 0000000..ddb4927 --- /dev/null +++ b/Tests/BinaryParsingTests/ArrayParsingTests.swift @@ -0,0 +1,244 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsing +import Testing + +struct ArrayParsingTests { + private let testBuffer: [UInt8] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + ] + + private let emptyBuffer: [UInt8] = [] + + @Test + func parseRemainingBytes() throws { + try testBuffer.withParserSpan { span in + let parsedArray = try Array(parsingRemainingBytes: &span) + #expect(parsedArray == testBuffer) + #expect(span.count == 0) + } + + // Test parsing after consuming part of the buffer + try testBuffer.withParserSpan { span in + try span.seek(toRelativeOffset: 3) + let parsedArray = try Array(parsingRemainingBytes: &span) + #expect(parsedArray[...] == testBuffer.dropFirst(3)) + #expect(span.count == 0) + } + + // Test with an empty span + try emptyBuffer.withParserSpan { span in + let parsedArray = try [UInt8](parsingRemainingBytes: &span) + #expect(parsedArray.isEmpty) + } + } + + @Test + func parseByteCount() throws { + try testBuffer.withParserSpan { span in + let parsedArray = try [UInt8](parsing: &span, byteCount: 5) + #expect(parsedArray[...] == testBuffer.prefix(5)) + #expect(span.count == 5) + + let parsedArray2 = try [UInt8](parsing: &span, byteCount: 3) + #expect(parsedArray2[...] == testBuffer.dropFirst(5).prefix(3)) + #expect(span.count == 2) + } + + // 'byteCount' == 0 + try testBuffer.withParserSpan { span in + let parsedArray = try [UInt8](parsing: &span, byteCount: 0) + #expect(parsedArray.isEmpty) + #expect(span.count == testBuffer.count) + } + + // 'byteCount' greater than available bytes + try testBuffer.withParserSpan { span in + #expect(throws: ParsingError.self) { + _ = try [UInt8](parsing: &span, byteCount: testBuffer.count + 1) + } + #expect(span.count == testBuffer.count) + } + } + + @Test + func parseArrayOfFixedSize() throws { + // Arrays of fixed-size integers + try testBuffer.withParserSpan { span in + let parsedArray = try Array(parsing: &span, count: 5) { input in + try UInt8(parsing: &input) + } + #expect(parsedArray[...] == testBuffer.prefix(5)) + #expect(span.count == 5) + + // Parse two UInt16 values + let parsedArray2 = try Array(parsing: &span, count: 2) { input in + try UInt16(parsingBigEndian: &input) + } + #expect(parsedArray2 == [0x0607, 0x0809]) + #expect(span.count == 1) + + // Fail to parse one UInt16 + #expect(throws: ParsingError.self) { + _ = try Array(parsing: &span, count: 1) { input in + try UInt16(parsingBigEndian: &input) + } + } + + let lastByte = try Array( + parsing: &span, + count: 1, + parser: UInt8.init(parsing:)) + #expect(lastByte == [0x0A]) + #expect(span.count == 0) + } + + // Parsing count = 0 always succeeds + try testBuffer.withParserSpan { span in + let parsedArray1 = try Array(parsing: &span, count: 0) { input in + try UInt64(parsingBigEndian: &input) + } + #expect(parsedArray1.isEmpty) + #expect(span.count == testBuffer.count) + + try span.seek(toOffsetFromEnd: 0) + let parsedArray2 = try Array(parsing: &span, count: 0) { input in + try UInt64(parsingBigEndian: &input) + } + #expect(parsedArray2.isEmpty) + #expect(span.count == 0) + } + + // Non-'Int' count that would overflow + _ = try testBuffer.withParserSpan { span in + #expect(throws: ParsingError.self) { + _ = try [UInt8](parsing: &span, count: UInt.max) { input in + try UInt8(parsing: &input) + } + } + } + } + + @Test + func parseArrayOfCustomTypes() throws { + // Define a custom struct to test with + struct CustomType { + var value: UInt8 + var doubled: UInt8 + + init(parsing input: inout ParserSpan) throws { + self.value = try UInt8(parsing: &input) + guard let d = self.value *? 2 else { + throw TestError("Doubled value too large for UInt8") + } + self.doubled = d + } + } + + try testBuffer.withParserSpan { span in + let parsedArray = try Array(parsing: &span, count: 5) { input in + try CustomType(parsing: &input) + } + + #expect(parsedArray.map(\.value) == [0x01, 0x02, 0x03, 0x04, 0x05]) + #expect(parsedArray.map(\.doubled) == [0x02, 0x04, 0x06, 0x08, 0x0A]) + #expect(span.count == 5) + } + + _ = try [0x0f, 0xf0].withParserSpan { span in + #expect(throws: TestError.self) { + try Array(parsingAll: &span, parser: CustomType.init(parsing:)) + } + } + } + + @Test + func parseAllAvailableElements() throws { + // Parse as UInt8 + try testBuffer.withParserSpan { span in + let parsedArray = try Array(parsingAll: &span) { input in + try UInt8(parsing: &input) + } + #expect(parsedArray == testBuffer) + #expect(span.count == 0) + } + + // Parse as UInt16 + try testBuffer.withParserSpan { span in + let parsedArray = try Array(parsingAll: &span) { input in + try UInt16(parsingBigEndian: &input) + } + #expect(parsedArray == [0x0102, 0x0304, 0x0506, 0x0708, 0x090A]) + #expect(span.count == 0) + } + + // Parse as UInt16 with recovery + let oddBuffer: [UInt8] = [0x01, 0x02, 0x03, 0x04, 0x05] + try oddBuffer.withParserSpan { span in + let parsedArray = try Array(parsingAll: &span) { input in + do { + return try UInt16(parsingBigEndian: &input) + } catch { + if input.count == 1 { + return try UInt16(parsing: &input, storedAs: UInt8.self) + } + throw error + } + } + + // Two complete 'UInt16' values plus one 'UInt16' from the last byte + #expect(parsedArray == [0x0102, 0x0304, 0x0005]) + #expect(span.count == 0) + } + + // Test with empty buffer + try emptyBuffer.withParserSpan { span in + let parsedArray = try Array(parsingAll: &span) { input in + try UInt8(parsing: &input) + } + #expect(parsedArray.isEmpty) + #expect(span.count == 0) + } + } + + @Test + func parseArrayWithErrorHandling() throws { + struct ValidatedUInt8 { + var value: UInt8 + + init(parsing input: inout ParserSpan) throws { + self.value = try UInt8(parsing: &input) + if value > 5 { + throw TestError("Value \(value) exceeds maximum allowed value of 5") + } + } + } + + try testBuffer.withParserSpan { span in + // This should fail because values in the buffer exceed 5 + #expect(throws: TestError.self) { + _ = try Array(parsing: &span, count: testBuffer.count) { input in + try ValidatedUInt8(parsing: &input) + } + } + // Even though the parsing failed, it should have consumed some elements + #expect(span.count < testBuffer.count) + + // Reset and try just parsing the valid values + try span.seek(toAbsoluteOffset: 0) + let parsedArray = try Array(parsing: &span, count: 5) { input in + try ValidatedUInt8(parsing: &input) + } + #expect(parsedArray.map(\.value) == [0x01, 0x02, 0x03, 0x04, 0x05]) + } + } +} diff --git a/Tests/BinaryParsingTests/IntegerParsingTests.swift b/Tests/BinaryParsingTests/IntegerParsingTests.swift index c709ff6..e85fb20 100644 --- a/Tests/BinaryParsingTests/IntegerParsingTests.swift +++ b/Tests/BinaryParsingTests/IntegerParsingTests.swift @@ -144,12 +144,12 @@ struct IntegerParsingTests { let size = T.bitWidth * 2 let beBadPadding = [UInt8]( bigEndian: number, paddingTo: size, withPadding: 0xb0) - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try beBadPadding.withParserSpan { try T(parsingBigEndian: &$0, byteCount: size) } } - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try beBadPadding.withParserSpan { try T(parsing: &$0, endianness: .big, byteCount: size) } @@ -160,12 +160,12 @@ struct IntegerParsingTests { let size = T.bitWidth * 2 let leBadPadding = [UInt8]( littleEndian: number, paddingTo: size, withPadding: 0xb0) - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try leBadPadding.withParserSpan { try T(parsingLittleEndian: &$0, byteCount: size) } } - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try leBadPadding.withParserSpan { try T(parsing: &$0, endianness: .little, byteCount: size) } @@ -340,12 +340,12 @@ struct IntegerParsingTests { do { let beBadPadding = [UInt8]( bigEndian: number, paddingTo: paddedSize, withPadding: 0xb0) - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try beBadPadding.withParserSpan { try T(parsingBigEndian: &$0, byteCount: paddedSize) } } - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try beBadPadding.withParserSpan { try T(parsing: &$0, endianness: .big, byteCount: paddedSize) } @@ -355,12 +355,12 @@ struct IntegerParsingTests { do { let leBadPadding = [UInt8]( littleEndian: number, paddingTo: paddedSize, withPadding: 0xb0) - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try leBadPadding.withParserSpan { try T(parsingLittleEndian: &$0, byteCount: paddedSize) } } - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try leBadPadding.withParserSpan { try T(parsing: &$0, endianness: .little, byteCount: paddedSize) } @@ -476,12 +476,12 @@ struct IntegerParsingTests { let size = T.bitWidth * 2 let beBadPadding = [UInt8]( bigEndian: number, paddingTo: size, withPadding: 0xb0) - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try beBadPadding.withParserSpan { try T(parsingBigEndian: &$0, byteCount: size) } } - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try beBadPadding.withParserSpan { try T(parsing: &$0, endianness: .big, byteCount: size) } @@ -492,12 +492,12 @@ struct IntegerParsingTests { let size = T.bitWidth * 2 let leBadPadding = [UInt8]( littleEndian: number, paddingTo: size, withPadding: 0xb0) - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try leBadPadding.withParserSpan { try T(parsingLittleEndian: &$0, byteCount: size) } } - #expect(throws: Error.self) { + #expect(throws: ParsingError.self) { try leBadPadding.withParserSpan { try T(parsing: &$0, endianness: .little, byteCount: size) } diff --git a/Tests/BinaryParsingTests/OptionatorTests.swift b/Tests/BinaryParsingTests/OptionalOperationsTests.swift similarity index 80% rename from Tests/BinaryParsingTests/OptionatorTests.swift rename to Tests/BinaryParsingTests/OptionalOperationsTests.swift index 835a480..7291782 100644 --- a/Tests/BinaryParsingTests/OptionatorTests.swift +++ b/Tests/BinaryParsingTests/OptionalOperationsTests.swift @@ -12,9 +12,9 @@ import BinaryParsing import Testing -let numbers = [nil, .min, -100, 0, 100, .max] - struct OptionatorTests { + static let numbers = [nil, .min, -100, 0, 100, .max] + @Test(arguments: numbers, numbers) func addition(_ a: Int?, _ b: Int?) { let expected = b.flatMap { a?.addingReportingOverflow($0) } @@ -135,3 +135,34 @@ struct OptionatorTests { } } } + +struct OptionalOperationsTests { + @Test func collectionIfInBounds() throws { + let str = "Hello, world!" + let substr = str.dropFirst(5).dropLast() + + var i = str.startIndex + while true { + if substr.indices.contains(i) { + #expect(substr[ifInBounds: i] == substr[i]) + } else { + #expect(substr[ifInBounds: i] == nil) + } + if i == str.endIndex { break } + str.formIndex(after: &i) + } + } + + @Test func collectionRACIfInBounds() throws { + let numbers = Array(1...100) + let slice = numbers.dropFirst(14).dropLast(20) + + for i in 0...UInt8.max { + if slice.indices.contains(Int(i)) { + #expect(slice[ifInBounds: i] == slice[Int(i)]) + } else { + #expect(slice[ifInBounds: i] == nil) + } + } + } +} diff --git a/Tests/BinaryParsingTests/RangeParsingTests.swift b/Tests/BinaryParsingTests/RangeParsingTests.swift new file mode 100644 index 0000000..7d2e335 --- /dev/null +++ b/Tests/BinaryParsingTests/RangeParsingTests.swift @@ -0,0 +1,171 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsing +import Testing + +private let buffer: [UInt8] = [ + 0x00, 0x01, 0x00, 0x02, + 0x00, 0x03, 0x00, 0x04, +] + +struct RangeParsingTests { + @Test + func startAndCount() throws { + try buffer.withParserSpan { span in + let range1 = try Range(parsingStartAndCount: &span) { span in + try Int16(parsingBigEndian: &span) + } + let range2 = try Range( + parsingStartAndCount: &span, + parser: Int16.init(parsingBigEndian:)) + #expect(range1 == 1..<3) + #expect(range2 == 3..<7) + } + + _ = try buffer.withParserSpan { span in + // Negative count + #expect(throws: ParsingError.self) { + try Range(parsingStartAndCount: &span) { span in + try -(Int16(parsingBigEndian: &span)) + } + } + + // Insufficient data + #expect(throws: ParsingError.self) { + try Range( + parsingStartAndCount: &span, + parser: Int64.init(parsingBigEndian:)) + } + + // Custom error + #expect(throws: TestError.self) { + try Range(parsingStartAndCount: &span) { _ -> Int in + throw TestError() + } + } + } + } + + @Test + func startAndEnd() throws { + try buffer.withParserSpan { span in + let range1 = try Range(parsingStartAndEnd: &span) { span in + try Int16(parsingBigEndian: &span) + } + let range2 = try Range( + parsingStartAndEnd: &span, + boundsParser: Int16.init(parsingBigEndian:)) + #expect(range1 == 1..<2) + #expect(range2 == 3..<4) + } + + _ = try buffer.withParserSpan { span in + // Reversed start and end + #expect(throws: ParsingError.self) { + try Range(parsingStartAndEnd: &span) { span in + try -(Int16(parsingBigEndian: &span)) + } + } + + // Insufficient data + #expect(throws: ParsingError.self) { + try Range( + parsingStartAndEnd: &span, + boundsParser: Int64.init(parsingBigEndian:)) + } + + // Custom error + #expect(throws: TestError.self) { + try Range(parsingStartAndEnd: &span) { _ -> Int in + throw TestError() + } + } + } + } + + @available( + *, deprecated, message: "Deprecated to avoid deprecations warnings within" + ) + @Test + func closedStartAndCount() throws { + try buffer.withParserSpan { span in + let range1 = try ClosedRange(parsingStartAndCount: &span) { span in + try Int16(parsingBigEndian: &span) + } + let range2 = try ClosedRange( + parsingStartAndCount: &span, + parser: Int16.init(parsingBigEndian:)) + #expect(range1 == 1...2) + #expect(range2 == 3...6) + } + + _ = try buffer.withParserSpan { span in + // Reversed start and end + #expect(throws: ParsingError.self) { + try ClosedRange(parsingStartAndCount: &span) { span in + try -(Int16(parsingBigEndian: &span)) + } + } + + // Insufficient data + #expect(throws: ParsingError.self) { + try ClosedRange( + parsingStartAndCount: &span, + parser: Int64.init(parsingBigEndian:)) + } + + // Custom error + #expect(throws: TestError.self) { + try ClosedRange(parsingStartAndCount: &span) { _ -> Int in + throw TestError() + } + } + } + } + + @Test + func closedStartAndEnd() throws { + try buffer.withParserSpan { span in + let range1 = try ClosedRange(parsingStartAndEnd: &span) { span in + try Int16(parsingBigEndian: &span) + } + let range2 = try ClosedRange( + parsingStartAndEnd: &span, + boundsParser: Int16.init(parsingBigEndian:)) + #expect(range1 == 1...2) + #expect(range2 == 3...4) + } + + _ = try buffer.withParserSpan { span in + // Reversed start and end + #expect(throws: ParsingError.self) { + try ClosedRange(parsingStartAndEnd: &span) { span in + try -(Int16(parsingBigEndian: &span)) + } + } + + // Insufficient data + #expect(throws: ParsingError.self) { + try ClosedRange( + parsingStartAndEnd: &span, + boundsParser: Int64.init(parsingBigEndian:)) + } + + // Custom error + #expect(throws: TestError.self) { + try ClosedRange(parsingStartAndEnd: &span) { _ -> Int in + throw TestError() + } + } + } + } +} diff --git a/Tests/BinaryParsingTests/SeekingTests.swift b/Tests/BinaryParsingTests/SeekingTests.swift index aba10bd..b39f19a 100644 --- a/Tests/BinaryParsingTests/SeekingTests.swift +++ b/Tests/BinaryParsingTests/SeekingTests.swift @@ -13,36 +13,218 @@ import BinaryParsing import Testing private let buffer: [UInt8] = [ - 0x00, 0x01, 0x00, 0x02, - 0x00, 0x03, 0x00, 0x04, + 0, 1, 0, 2, 0, 3, 0, 4, + 0, 5, 0, 6, 0, 7, 0, 0, ] +private let bigBuffer: [UInt8] = Array(repeating: 0, count: 1000) + +// Can't throw at top level + statically known +// swift-format-ignore: NeverUseForceTry +private let (firstHalf, secondHalf) = try! buffer.withParserSpan { input in + try (input.sliceRange(byteCount: 8), input.sliceRemainingRange()) +} + struct SeekingTests { - @Test - func currentPosition1() throws { - let (x, y) = try buffer.withParserSpan { input in + @Test func currentParserRange() throws { + try buffer.withParserSpan { input in + // Get the current range let range = input.parserRange + + // Jump ahead and parse _ = try input.sliceRange(byteCount: 2) - let y = try UInt16(parsingBigEndian: &input) + let second = try UInt16(parsingBigEndian: &input) + #expect(second == 2) + // Reset to original range and validate try input.seek(toRange: range) - let x = try UInt16(parsingBigEndian: &input) - return (x, y) + let allNumbers = try Array( + parsingAll: &input, parser: UInt16.init(parsingBigEndian:)) + #expect(allNumbers == [1, 2, 3, 4, 5, 6, 7, 0]) } - #expect((x, y) == (1, 2)) } - @Test - func currentPosition2() throws { - let (x, y) = try buffer.withParserSpan { input in - let range = input.parserRange - try input.seek(toRelativeOffset: 2) - let y = try UInt16(parsingBigEndian: &input) + @Test func seekAbsoluteOffset() throws { + try buffer.withParserSpan { input in + let otherInput = try input.seeking(toAbsoluteOffset: 4) + try input.seek(toAbsoluteOffset: 4) + #expect(input.count == 12) + let identical = input === otherInput + #expect(identical) + let thirdValue = try Int16(parsingBigEndian: &input) + #expect(thirdValue == 3) - try input.seek(toRange: range) - let x = try UInt16(parsingBigEndian: &input) - return (x, y) + try input.seek(toAbsoluteOffset: 0) + #expect(input.count == 16) + let firstValue = try Int16(parsingBigEndian: &input) + #expect(firstValue == 1) + + // Absolute offset is always referent to original bounds + var slice1 = try input.sliceSpan(byteCount: 4) + var slice2 = slice1 + #expect(slice1.startPosition == 2) + #expect(slice1.count == 4) + + try slice1.seek(toAbsoluteOffset: 8) + #expect(slice1.startPosition == 8) + #expect(slice1.count == 8) + + try slice2.seek(toAbsoluteOffset: 0) + #expect(slice2.startPosition == 0) + #expect(slice2.count == 16) + + // Can't seek past endpoints + #expect(throws: ParsingError.self) { + try input.seek(toAbsoluteOffset: -1) + } + #expect(throws: ParsingError.self) { + try input.seek(toAbsoluteOffset: 17) + } + #expect(throws: ParsingError.self) { + try input.seek(toAbsoluteOffset: UInt.max) + } + #expect(throws: ParsingError.self) { + _ = try input.seeking(toAbsoluteOffset: -1) + } + } + } + + @Test func seekRelativeOffset() throws { + try buffer.withParserSpan { input in + let firstOffset = try Int16(parsingBigEndian: &input) + #expect(firstOffset == 1) + let otherInput = try input.seeking(toRelativeOffset: firstOffset) + try input.seek(toRelativeOffset: firstOffset) + let identical = input === otherInput + #expect(identical) + + let secondOffset = try UInt8(parsing: &input) + #expect(secondOffset == 2) + try input.seek(toRelativeOffset: secondOffset) + + let doubleOffsetValue = try Int16(parsingBigEndian: &input) + #expect(doubleOffsetValue == 4) + #expect(input.startPosition == 8) + + // Start over + try input.seek(toRelativeOffset: -8) + let fourValues = try Array( + parsing: &input, + count: 4, + parser: Int16.init(parsingBigEndian:)) + #expect(fourValues == [1, 2, 3, 4]) + #expect(input.startPosition == 8) + + // Seek to end + try input.seek(toRelativeOffset: 8) + #expect(input.count == 0) + try input.seek(toRelativeOffset: -8) + + // Can't seek past endpoints + #expect(throws: ParsingError.self) { + try input.seek(toRelativeOffset: -9) + } + #expect(input.startPosition == 8) + #expect(throws: ParsingError.self) { + try input.seek(toRelativeOffset: 9) + } + #expect(throws: ParsingError.self) { + _ = try input.seeking(toRelativeOffset: 9) + } + + // Relative seek obeys end boundary + var chunk = try input.sliceSpan(byteCount: 4) + #expect(chunk.count == 4) + #expect(chunk.startPosition == 8) + #expect(throws: ParsingError.self) { + try chunk.seek(toRelativeOffset: 5) + } + try chunk.seek(toRelativeOffset: 4) + #expect(chunk.count == 0) + } + } + + @Test func seekOffsetFromEnd() throws { + try buffer.withParserSpan { input in + let otherInput = try input.seeking(toOffsetFromEnd: 4) + try input.seek(toOffsetFromEnd: 4) + let identical = input === otherInput + #expect(identical) + + let value1 = try Int16(parsingBigEndian: &input) + #expect(value1 == 7) + #expect(input.count == 2) + + try input.seek(toOffsetFromEnd: 16) + let value2 = try Int16(parsingBigEndian: &input) + #expect(value2 == 1) + #expect(input.count == 14) + + // Can't seek past endpoints + #expect(throws: ParsingError.self) { + try input.seek(toOffsetFromEnd: -1) + } + #expect(throws: ParsingError.self) { + try input.seek(toOffsetFromEnd: 17) + } + #expect(throws: ParsingError.self) { + _ = try input.seeking(toOffsetFromEnd: 17) + } + try input.seek(toOffsetFromEnd: 0) + #expect(input.count == 0) + + // Relative seek obeys end boundary + try input.seek(toAbsoluteOffset: 8) + var chunk = try input.sliceSpan(byteCount: 4) + #expect(chunk.count == 4) + #expect(chunk.startPosition == 8) + #expect(throws: ParsingError.self) { + try chunk.seek(toOffsetFromEnd: -1) + } + #expect(chunk.startPosition == 8) + #expect(chunk.endPosition == 12) + #expect(throws: ParsingError.self) { + try chunk.seek(toOffsetFromEnd: 13) + } + try chunk.seek(toOffsetFromEnd: 12) + #expect(chunk.count == 12) + } + } + + @Test func seekRange() throws { + try buffer.withParserSpan { input in + let otherInput = try input.seeking(toRange: secondHalf) + try input.seek(toRange: secondHalf) + let identical = input === otherInput + #expect(identical) + + let fourValues = try Array( + parsingAll: &input, + parser: Int16.init(parsingBigEndian:)) + #expect(fourValues == [5, 6, 7, 0]) + #expect(input.count == 0) + + try input.seek(toRange: firstHalf) + let firstFourValues = try Array( + parsingAll: &input, + parser: Int16.init(parsingBigEndian:)) + #expect(firstFourValues == [1, 2, 3, 4]) + #expect(input.count == 0) + + let (badRange1, badRange2) = try bigBuffer.withParserSpan { bigInput in + try ( + bigInput.sliceRange(byteCount: 100), bigInput.sliceRange(byteCount: 4) + ) + } + #expect(throws: ParsingError.self) { + try input.seek(toRange: badRange1) + } + #expect(throws: ParsingError.self) { + try input.seek(toRange: badRange2) + } + #expect(throws: ParsingError.self) { + _ = try input.seeking(toRange: badRange2) + } } - #expect((x, y) == (1, 2)) } } diff --git a/Tests/BinaryParsingTests/SlicingTests.swift b/Tests/BinaryParsingTests/SlicingTests.swift new file mode 100644 index 0000000..da42818 --- /dev/null +++ b/Tests/BinaryParsingTests/SlicingTests.swift @@ -0,0 +1,306 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsing +import Testing + +private let buffer: [UInt8] = [ + 0, 1, 0, 2, 0, 3, 0, 4, + 0, 5, 0, 6, 0, 7, 0, 0, +] + +private let emptyBuffer: [UInt8] = [] + +private let testString = "Hello, world!" + +struct SlicingTests { + @Test func rangeByteCount() throws { + try buffer.withParserSpan { input in + let firstRange = try input.sliceRange(byteCount: 4) + #expect(firstRange.lowerBound == 0) + #expect(firstRange.upperBound == 4) + + let secondRange = try input.sliceRange(byteCount: 4) + #expect(secondRange.lowerBound == 4) + #expect(secondRange.upperBound == 8) + + // Input position should advance + #expect(input.startPosition == 8) + #expect(input.count == 8) + + let emptyRange = try input.sliceRange(byteCount: 0) + #expect(emptyRange.isEmpty) + #expect(emptyRange.lowerBound == 8) + #expect(emptyRange.upperBound == 8) + #expect(input.startPosition == 8) + + // byteCount > available + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(byteCount: 9) + } + #expect(input.startPosition == 8) + + // negative byteCount + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(byteCount: -1) + } + } + + // empty buffer + try emptyBuffer.withParserSpan { input in + let emptyRange = try input.sliceRange(byteCount: 0) + #expect(emptyRange.isEmpty) + + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(byteCount: 1) + } + } + } + + @Test func remainingRange() throws { + try buffer.withParserSpan { input in + try input.seek(toRelativeOffset: 6) + + let remainingRange = input.sliceRemainingRange() + #expect(remainingRange.lowerBound == 6) + #expect(remainingRange.upperBound == 16) + + // Verify that all bytes are consumed & reset + #expect(input.count == 0) + try input.seek(toAbsoluteOffset: 0) + + // Get the full range + let range = input.parserRange + let fullRange = input.sliceRemainingRange() + #expect(fullRange == range) + #expect(input.count == 0) + } + + // Test with empty buffer + try emptyBuffer.withParserSpan { input in + let emptyRange = input.sliceRemainingRange() + #expect(emptyRange.isEmpty) + #expect(input.count == 0) + } + } + + @Test func rangeObjectCount() throws { + try buffer.withParserSpan { input in + // 2 objects of 2 bytes each + let firstRange = try input.sliceRange(objectStride: 2, objectCount: 2) + #expect(firstRange.lowerBound == 0) + #expect(firstRange.upperBound == 4) + + // 1 object of 4 bytes + let secondRange = try input.sliceRange(objectStride: 4, objectCount: 1) + #expect(secondRange.lowerBound == 4) + #expect(secondRange.upperBound == 8) + + // Input position should advance + #expect(input.startPosition == 8) + #expect(input.count == 8) + + // objectCount == 0 (should create an empty range) + let emptyRange = try input.sliceRange(objectStride: 2, objectCount: 0) + #expect(emptyRange.isEmpty) + #expect(emptyRange.lowerBound == 8) + #expect(emptyRange.upperBound == 8) + #expect(input.startPosition == 8) + + // objectStride == 0 (should create an empty range) + let emptyRange2 = try input.sliceRange(objectStride: 0, objectCount: 5) + #expect(emptyRange2.isEmpty) + #expect(emptyRange2.lowerBound == 8) + #expect(emptyRange2.upperBound == 8) + #expect(input.startPosition == 8) + + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(objectStride: 3, objectCount: 3) + } + #expect(input.startPosition == 8) + + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(objectStride: -1, objectCount: 2) + } + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(objectStride: 2, objectCount: -1) + } + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(objectStride: Int.max, objectCount: 2) + } + } + + // Test with empty buffer + try emptyBuffer.withParserSpan { input in + // Zero objectCount should succeed even with empty buffer + let emptyRange = try input.sliceRange(objectStride: 4, objectCount: 0) + #expect(emptyRange.isEmpty) + + // Any positive objectCount should fail with empty buffer + #expect(throws: ParsingError.self) { + _ = try input.sliceRange(objectStride: 1, objectCount: 1) + } + } + } + + @Test func spanByteCount() throws { + try buffer.withParserSpan { input in + var firstSpan = try input.sliceSpan(byteCount: 4) + #expect(firstSpan.startPosition == 0) + #expect(firstSpan.count == 4) + + // Verify contents of the sliced span + let firstValue = try UInt16(parsingBigEndian: &firstSpan) + let secondValue = try UInt16(parsingBigEndian: &firstSpan) + #expect(firstValue == 1) + #expect(secondValue == 2) + #expect(firstSpan.count == 0) + + // Input position should advance + #expect(input.startPosition == 4) + #expect(input.count == 12) + + // Slice another span after advancing the input + _ = try input.seek(toRelativeOffset: 2) + var secondSpan = try input.sliceSpan(byteCount: 4) + #expect(secondSpan.startPosition == 6) + #expect(secondSpan.count == 4) + + // Verify the content of the second sliced span + let thirdValue = try UInt16(parsingBigEndian: &secondSpan) + let fourthValue = try UInt16(parsingBigEndian: &secondSpan) + #expect(thirdValue == 4) + #expect(fourthValue == 5) + + // Try slicing with zero byteCount + let emptySpan = try input.sliceSpan(byteCount: 0) + #expect(emptySpan.count == 0) + #expect(emptySpan.startPosition == 10) + + // Attempt to slice more than available + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(byteCount: 7) + } + + // Try with negative byteCount + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(byteCount: -1) + } + } + + // Test with empty buffer + try emptyBuffer.withParserSpan { input in + // Zero byteCount should succeed + let emptySpan = try input.sliceSpan(byteCount: 0) + #expect(emptySpan.count == 0) + + // Any positive byteCount should fail + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(byteCount: 1) + } + } + } + + @Test func spanObjectCount() throws { + try buffer.withParserSpan { input in + // 2 objects of 2 bytes each + var firstSpan = try input.sliceSpan(objectStride: 2, objectCount: 2) + #expect(firstSpan.startPosition == 0) + #expect(firstSpan.count == 4) + + // Verify contents of the sliced span + let firstValue = try UInt16(parsingBigEndian: &firstSpan) + let secondValue = try UInt16(parsingBigEndian: &firstSpan) + #expect(firstValue == 1) + #expect(secondValue == 2) + #expect(firstSpan.count == 0) + + // 1 object of 4 bytes + var secondSpan = try input.sliceSpan(objectStride: 4, objectCount: 1) + #expect(secondSpan.startPosition == 4) + #expect(secondSpan.count == 4) + + // Verify contents of the second slice + let thirdValue = try UInt32(parsingBigEndian: &secondSpan) + #expect(thirdValue == 0x0003_0004) + #expect(secondSpan.count == 0) + + // Input position should advance + #expect(input.startPosition == 8) + #expect(input.count == 8) + + // objectCount == 0 (should create an empty span) + let emptySpan = try input.sliceSpan(objectStride: 2, objectCount: 0) + #expect(emptySpan.count == 0) + #expect(emptySpan.startPosition == 8) + + // objectStride == 0 (should create an empty span) + let emptySpan2 = try input.sliceSpan(objectStride: 0, objectCount: 5) + #expect(emptySpan2.count == 0) + #expect(emptySpan2.startPosition == 8) + + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(objectStride: 3, objectCount: 3) + } + #expect(input.startPosition == 8) + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(objectStride: -1, objectCount: 2) + } + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(objectStride: 2, objectCount: -1) + } + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(objectStride: Int.max, objectCount: 2) + } + } + + // Test with empty buffer + try emptyBuffer.withParserSpan { input in + let emptySpan = try input.sliceSpan(objectStride: 4, objectCount: 0) + #expect(emptySpan.count == 0) + + #expect(throws: ParsingError.self) { + _ = try input.sliceSpan(objectStride: 1, objectCount: 1) + } + } + } + + @Test func nestedSlices() throws { + try buffer.withParserSpan { input in + var firstSlice = try input.sliceSpan(byteCount: 8) + + // Create nested slices from the first slice + var nestedSlice1 = try firstSlice.sliceSpan(byteCount: 4) + var nestedSlice2 = try firstSlice.sliceSpan(byteCount: 4) + + #expect(nestedSlice1.startPosition == 0) + #expect(nestedSlice1.count == 4) + + #expect(nestedSlice2.startPosition == 4) + #expect(nestedSlice2.count == 4) + + // First slice should advance + #expect(firstSlice.startPosition == 8) + #expect(firstSlice.count == 0) + + // Parse from nested slices + let value1 = try UInt16(parsingBigEndian: &nestedSlice1) + let value2 = try UInt16(parsingBigEndian: &nestedSlice1) + #expect(value1 == 1) + #expect(value2 == 2) + + let value3 = try UInt16(parsingBigEndian: &nestedSlice2) + let value4 = try UInt16(parsingBigEndian: &nestedSlice2) + #expect(value3 == 3) + #expect(value4 == 4) + } + } +} diff --git a/Tests/BinaryParsingTests/StringParsingTests.swift b/Tests/BinaryParsingTests/StringParsingTests.swift new file mode 100644 index 0000000..9f3e392 --- /dev/null +++ b/Tests/BinaryParsingTests/StringParsingTests.swift @@ -0,0 +1,275 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsing +import Testing + +struct StringParsingTests { + // Test data + private let testString = "Hello, world!" + private let testStringWithNul = "Hello\0World" + private let testStringNonASCII = "μ•ˆλ…• 세계!" + + private let invalidBuffer: [UInt8] = [0xD8, 0x00] + private let emptyBuffer: [UInt8] = [] + private let nulOnlyBuffer: [UInt8] = [0] + + @Test + func parseNulTerminated() throws { + // Valid nul-terminated string + let nulTerminated = Array(testString.utf8) + [0] + try nulTerminated.withParserSpan { span in + let str = try String(parsingNulTerminated: &span) + #expect(str == testString) + #expect(span.count == 0) + } + + // String without nul terminator should throw + _ = try testString.withParserSpan { span in + #expect(throws: ParsingError.self) { + _ = try String(parsingNulTerminated: &span) + } + } + + // String with nul in the middle + try testStringWithNul.withParserSpan { span in + let str = try String(parsingNulTerminated: &span) + #expect(str == "Hello") + #expect(span.count == 5) // Remaining: "World" + } + + // Empty string with just nul + try nulOnlyBuffer.withParserSpan { span in + let str = try String(parsingNulTerminated: &span) + #expect(str.isEmpty) + #expect(span.count == 0) + } + + // Invalid UTF-8 sequence + try invalidBuffer.withParserSpan { span in + let str = try String(parsingNulTerminated: &span) + #expect(str == "\u{FFFD}") + #expect(span.count == 0) + } + } + + @Test + func parseUTF8Full() throws { + // Parse entire UTF-8 buffer + try testString.withParserSpan { span in + let str = try String(parsingUTF8: &span) + #expect(str == testString) + #expect(span.count == 0) + } + try testStringWithNul.withParserSpan { span in + let str = try String(parsingUTF8: &span) + #expect(str == testStringWithNul) + #expect(span.count == 0) + } + try testStringNonASCII.withParserSpan { span in + let str = try String(parsingUTF8: &span) + #expect(str == testStringNonASCII) + #expect(span.count == 0) + } + + // Empty string + try emptyBuffer.withParserSpan { span in + let str = try String(parsingUTF8: &span) + #expect(str.isEmpty) + #expect(span.count == 0) + } + + // Invalid UTF-8 sequence + try invalidBuffer.withParserSpan { span in + let str = try String(parsingUTF8: &span) + #expect(str == "\u{FFFD}\0") + #expect(span.count == 0) + } + } + + @Test + func parseUTF8WithCount() throws { + // Parse partial UTF-8 buffer with count + try testString.withParserSpan { span in + let str = try String(parsingUTF8: &span, count: 5) + #expect(str == "Hello") + #expect(span.count == testString.utf8.count - 5) + } + + // Parse with count = 0 + try testString.withParserSpan { span in + let str = try String(parsingUTF8: &span, count: 0) + #expect(str.isEmpty) + #expect(span.count == testString.utf8.count) + } + + // Parse with count equal to buffer size + try testString.withParserSpan { span in + let str = try String(parsingUTF8: &span, count: testString.utf8.count) + #expect(str == testString) + #expect(span.count == 0) + } + + // Parse with count exceeding buffer size + try testString.withParserSpan { span in + #expect(throws: ParsingError.self) { + _ = try String(parsingUTF8: &span, count: testString.utf8.count + 1) + } + } + + // Parse with invalid UTF-8 sequence + let invalidUTF8: [UInt8] = testString.utf8.prefix(5) + invalidBuffer + try invalidUTF8.withParserSpan { span in + let str = try String(parsingUTF8: &span, count: 5) + #expect(str == "Hello") + #expect(span.count == 2) + } + + try invalidUTF8.withParserSpan { span in + let str = try String(parsingUTF8: &span, count: 7) + #expect(str == "Hello\u{FFFD}\0") + #expect(span.count == 0) + } + } + + @Test + func parseUTF16Full() throws { + try testString.utf16Buffer.withParserSpan { span in + let str = try String(parsingUTF16: &span) + #expect(str == testString) + #expect(span.count == 0) + } + + try testStringNonASCII.utf16Buffer.withParserSpan { span in + let str = try String(parsingUTF16: &span) + #expect(str == testStringNonASCII) + #expect(span.count == 0) + } + + // Empty string + try emptyBuffer.withParserSpan { span in + let str = try String(parsingUTF16: &span) + #expect(str.isEmpty) + #expect(span.count == 0) + } + + // Buffer with odd number of bytes should throw + try [0x48, 0x00, 0x65].withParserSpan { span in + #expect(throws: ParsingError.self) { + _ = try String(parsingUTF16: &span) + } + } + + // Invalid UTF-16 sequence (unpaired surrogate) + let unpaired: [UInt8] = [0x00, 0xD8, 0x00, 0x00] // Unpaired high surrogate + try unpaired.withParserSpan { span in + let str = try String(parsingUTF16: &span) + #expect(!str.isEmpty) // Contains replacement character + #expect(span.count == 0) + } + } + + @Test + func parseUTF16WithCount() throws { + let buffer = testString.utf16Buffer + + // Parse partial UTF-16 buffer with count + try buffer.withParserSpan { span in + let str = try String(parsingUTF16: &span, codeUnitCount: 2) + #expect(str == "He") + #expect(span.count == buffer.count - 4) + } + + // Parse with count = 0 + try buffer.withParserSpan { span in + let str = try String(parsingUTF16: &span, codeUnitCount: 0) + #expect(str.isEmpty) + #expect(span.count == buffer.count) + } + + // Parse with count exactly matching the number of code units + try buffer.withParserSpan { span in + let codeUnitCount = testString.utf16.count + let str = try String(parsingUTF16: &span, codeUnitCount: codeUnitCount) + #expect(str == testString) + #expect(span.count == 0) + } + + // Parse with count larger than the buffer can provide + try buffer.withParserSpan { span in + let codeUnitCount = testString.utf16.count + 1 + #expect(throws: ParsingError.self) { + _ = try String(parsingUTF16: &span, codeUnitCount: codeUnitCount) + } + } + } + + @Test + func testMultipleOperationsOnSameBuffer() throws { + let combinedString = "\(testString)\0\(testStringNonASCII)" + + try combinedString.withParserSpan { span in + // First parse a nul-terminated UTF-8 string + let str1 = try String(parsingNulTerminated: &span) + #expect(str1 == testString) + + // Then parse the test as a UTF-8 string + let str2 = try String(parsingUTF8: &span) + #expect(str2 == testStringNonASCII) + + #expect(span.count == 0) + } + } + + @Test + func testWithComplicatedUnicodeStrings() throws { + let complexString = "Hello πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ world! πŸ‡ΊπŸ‡Έ" + + // Test UTF-8 parsing + try complexString.withParserSpan { span in + let parsedString = try String(parsingUTF8: &span) + #expect(parsedString == complexString) + #expect(span.count == 0) + } + + // Test UTF-16 parsing + try complexString.utf16Buffer.withParserSpan { span in + let parsedString = try String(parsingUTF16: &span) + #expect(parsedString == complexString) + #expect(span.count == 0) + } + + // Test partial parsing with emoji boundary + let emojiIndex = complexString.firstIndex( + where: \.unicodeScalars.first!.properties.isEmoji)! + let beforeEmoji = complexString[.. Bool { + guard lhs.startPosition == rhs.startPosition, + lhs.count == rhs.count + else { return false } + + return lhs.withUnsafeBytes { lhs in + rhs.withUnsafeBytes { rhs in + (lhs.baseAddress, lhs.count) == (rhs.baseAddress, rhs.count) + } + } +} /// A basic error type for testing user-thrown errors. -struct TestError: Error {} +struct TestError: Error, Equatable { + var description: String + init(_ description: String = "") { + self.description = description + } +} + +extension String { + /// Run the provided closure on a parser span over the UTF8 contents of this + /// string. + @discardableResult + func withParserSpan( + _ body: (inout ParserSpan) throws -> T + ) throws -> T { + try Array(self.utf8).withParserSpan(body) + } + + var utf16Buffer: [UInt8] { + var result: [UInt8] = [] + for codeUnit in self.utf16 { + result.append(UInt8(codeUnit & 0xFF)) + result.append(UInt8(codeUnit >> 8)) + } + return result + } +} /// The random seed to use for the RNG when "fuzzing", calculated once per /// testing session. diff --git a/Tests/BinaryParsingTests/ThrowingOperationsTests.swift b/Tests/BinaryParsingTests/ThrowingOperationsTests.swift new file mode 100644 index 0000000..af9330d --- /dev/null +++ b/Tests/BinaryParsingTests/ThrowingOperationsTests.swift @@ -0,0 +1,163 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsing +import Testing + +struct ThrowingOperationsTests { + static let numbers = [.min, -100, 0, 100, .max] + + @Test(arguments: numbers, numbers) + func addition(_ a: Int, _ b: Int) throws { + let expected = a.addingReportingOverflow(b) + switch expected { + case (let result, false): + let actual = try a.addingThrowingOnOverflow(b) + var actualMutating = a + try actualMutating.addThrowingOnOverflow(b) + + #expect(actual == result) + #expect(actualMutating == result) + default: + #expect(throws: ParsingError.self) { + try a.addingThrowingOnOverflow(b) + } + #expect(throws: ParsingError.self) { + var a = a + try a.addThrowingOnOverflow(b) + } + } + } + + @Test(arguments: numbers, numbers) + func subtraction(_ a: Int, _ b: Int) throws { + let expected = a.subtractingReportingOverflow(b) + switch expected { + case (let result, false): + let actual = try a.subtractingThrowingOnOverflow(b) + var actualMutating = a + try actualMutating.subtractThrowingOnOverflow(b) + + #expect(actual == result) + #expect(actualMutating == result) + default: + #expect(throws: ParsingError.self) { + try a.subtractingThrowingOnOverflow(b) + } + #expect(throws: ParsingError.self) { + var a = a + try a.subtractThrowingOnOverflow(b) + } + } + } + + @Test(arguments: numbers, numbers) + func multiplication(_ a: Int, _ b: Int) throws { + let expected = a.multipliedReportingOverflow(by: b) + switch expected { + case (let result, false): + let actual = try a.multipliedThrowingOnOverflow(by: b) + var actualMutating = a + try actualMutating.multiplyThrowingOnOverflow(by: b) + + #expect(actual == result) + #expect(actualMutating == result) + default: + #expect(throws: ParsingError.self) { + try a.multipliedThrowingOnOverflow(by: b) + } + #expect(throws: ParsingError.self) { + var a = a + try a.multiplyThrowingOnOverflow(by: b) + } + } + } + + @Test(arguments: numbers, numbers) + func division(_ a: Int, _ b: Int) throws { + let expected = a.dividedReportingOverflow(by: b) + switch expected { + case (let result, false): + let actual = try a.dividedThrowingOnOverflow(by: b) + var actualMutating = a + try actualMutating.divideThrowingOnOverflow(by: b) + + #expect(actual == result) + #expect(actualMutating == result) + default: + #expect(throws: ParsingError.self) { + try a.dividedThrowingOnOverflow(by: b) + } + #expect(throws: ParsingError.self) { + var a = a + try a.divideThrowingOnOverflow(by: b) + } + } + } + + @Test(arguments: numbers, numbers) + func modulo(_ a: Int, _ b: Int) throws { + let expected = a.remainderReportingOverflow(dividingBy: b) + switch expected { + case (let result, false): + let actual = try a.remainderThrowingOnOverflow(dividingBy: b) + var actualMutating = a + try actualMutating.formRemainderThrowingOnOverflow(dividingBy: b) + + #expect(actual == result) + #expect(actualMutating == result) + default: + #expect(throws: ParsingError.self) { + try a.remainderThrowingOnOverflow(dividingBy: b) + } + #expect(throws: ParsingError.self) { + var a = a + try a.formRemainderThrowingOnOverflow(dividingBy: b) + } + } + } + + @Test(arguments: numbers) + func conversion(_ value: Int) throws { + if let expected = Int16(exactly: value) { + let result = try Int16(throwingOnOverflow: value) + #expect(expected == result) + } else { + #expect(throws: ParsingError.self) { + try Int16(throwingOnOverflow: value) + } + } + } + + @Test(arguments: [1, nil]) + func optional(_ value: Int?) throws { + if let v = value { + let result = try value.unwrapped + #expect(v == result) + } else { + #expect(throws: ParsingError.self) { + try value.unwrapped + } + } + } + + @Test(arguments: (-1)...10) + func collectionSubscript(_ i: Int) throws { + if Self.numbers.indices.contains(i) { + let result = try Self.numbers[throwing: i] + #expect(Self.numbers[i] == result) + } else { + #expect(throws: ParsingError.self) { + try Self.numbers[throwing: i] + } + } + } +} diff --git a/Tests/ExampleParserTests/TestQOI.swift b/Tests/ExampleParserTests/TestQOI.swift index 00bb257..fdda7d5 100644 --- a/Tests/ExampleParserTests/TestQOI.swift +++ b/Tests/ExampleParserTests/TestQOI.swift @@ -23,10 +23,10 @@ struct QOITests { @Test(arguments: ["tricolor", "antelope"]) func parseImage(fileName: String) throws { let qoi = try #require(Self.getQOIPixels(testFileName: fileName)) -#if canImport(AppKit) + #if canImport(AppKit) let png = try #require(Self.getPNGPixels(testFileName: fileName)) #expect(png == qoi) -#endif + #endif } static func getQOIPixels(testFileName: String) -> Data? { @@ -38,7 +38,7 @@ struct QOITests { }.pixels } -#if canImport(AppKit) + #if canImport(AppKit) static func getPNGPixels(testFileName: String) -> Data? { guard let imageData = testData(named: "PNG/\(testFileName).png"), let image = NSImage(data: imageData) @@ -55,5 +55,5 @@ struct QOITests { } return nil } -#endif + #endif }