diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 960fdcb..4e34055 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -16,18 +16,31 @@ jobs: name: Run Benchmarks if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} runs-on: macOS-26 - env: - DEVELOPER_DIR: "/Applications/Xcode_26.1.app/Contents/Developer" steps: - uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Install Swiftly And Swift + run: | + curl -L https://download.swift.org/swiftly/darwin/swiftly.pkg > swiftly.pkg + installer -pkg swiftly.pkg -target CurrentUserHomeDirectory + + ~/.swiftly/bin/swiftly init + + . "$HOME/.swiftly/env.sh" + hash -r + + swiftly install --use --assume-yes + swiftly link --assume-yes + + echo "$HOME/.swiftly/bin" >> $GITHUB_PATH - name: Swift Version - run: xcrun swift --version + run: which swift && swift --version - name: Install jemalloc run: | echo "/opt/homebrew/bin:/usr/local/bin" >> $GITHUB_PATH brew install jemalloc - name: Run Parsing Benchmarks run: | - xcrun swift package benchmark --no-progress --format markdown >> $GITHUB_STEP_SUMMARY + export ENABLE_BENCHMARK=1 + swift package benchmark --no-progress --format markdown >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a649d1b..d9f78df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,46 +23,55 @@ jobs: lint: name: Lint & Format Check runs-on: macOS-26 - env: - DEVELOPER_DIR: "/Applications/Xcode_26.1.app/Contents/Developer" steps: - name: Checkout uses: actions/checkout@v6 - - name: Swift Version - run: xcrun swift --version - name: Install SwiftLint run: brew install swiftlint --quiet - name: Install swift-format run: brew install swiftformat --quiet - name: SwiftLint - run: swiftlint --strict --config .swiftlint.yml . + run: swiftlint --strict --config .swiftlint.yml . --reporter github-actions-logging - name: Swift Format Check - run: swiftformat --lint-only --config .swiftformat.conf . --strict + run: swiftformat --lint-only --config .swiftformat.conf . --strict --reporter github-actions-log --verbose test: name: ${{ matrix.name }} runs-on: ${{ matrix.runsOn }} needs: lint - env: - DEVELOPER_DIR: "/Applications/${{ matrix.xcode }}.app/Contents/Developer" strategy: matrix: include: - - xcode: "Xcode_26.0" - runsOn: macOS-26 + - runsOn: macOS-26 name: "macOS 26, SPM 6.2.0 Test" steps: - uses: actions/checkout@v4 + - name: Install Swiftly And Swift + run: | + curl -L https://download.swift.org/swiftly/darwin/swiftly.pkg > swiftly.pkg + installer -pkg swiftly.pkg -target CurrentUserHomeDirectory + + ~/.swiftly/bin/swiftly init + + . "$HOME/.swiftly/env.sh" + hash -r + + swiftly install --use --assume-yes + swiftly link --assume-yes + + echo "$HOME/.swiftly/bin" >> $GITHUB_PATH - name: Swift Version - run: xcrun swift --version - - name: Install xcbeautify + run: which swift && swift --version + - name: Brew install tools run: brew install xcbeautify --quiet - name: Run Tests run: | - set -o pipefail && \ - xcodebuild test \ - -scheme BinaryParseKit \ - -destination 'platform=macOS' \ - -skipMacroValidation | xcbeautify --renderer github-actions + defaults write com.apple.dt.Xcode IDEPackageEnablePrebuilts YES + + # set -o pipefail && \ + # xcodebuild test \ + # -scheme BinaryParseKit \ + # -destination 'platform=macOS' \ + # -skipMacroValidation | xcbeautify --renderer github-actions # https://github.com/swiftlang/swift-package-manager/issues/9163 - # set -o pipefail && xcrun swift test -c debug --enable-code-coverage --xunit-output $TEST_RESULTS_DIR/xunit.xml 2>&1 | xcbeautify --renderer github-actions + set -o pipefail && swift test | xcbeautify --renderer github-actions diff --git a/.github/workflows/deploy-docc.yml b/.github/workflows/deploy-docc.yml index 7365f13..5fe9dbe 100644 --- a/.github/workflows/deploy-docc.yml +++ b/.github/workflows/deploy-docc.yml @@ -14,17 +14,29 @@ jobs: build: name: Build DocC Documentation runs-on: macOS-26 - env: - DEVELOPER_DIR: "/Applications/Xcode_26.1.app/Contents/Developer" steps: - name: Checkout uses: actions/checkout@v6 + - name: Install Swiftly And Swift + run: | + curl -L https://download.swift.org/swiftly/darwin/swiftly.pkg > swiftly.pkg + installer -pkg swiftly.pkg -target CurrentUserHomeDirectory + + ~/.swiftly/bin/swiftly init + + . "$HOME/.swiftly/env.sh" + hash -r + + swiftly install --use --assume-yes + swiftly link --assume-yes + + echo "$HOME/.swiftly/bin" >> $GITHUB_PATH - name: Swift Version - run: xcrun swift --version + run: which swift && swift --version - name: Build DocC run: | - xcrun swift build - xcrun swift package --allow-writing-to-directory ./docs \ + swift build + swift package --allow-writing-to-directory ./docs \ generate-documentation \ --enable-experimental-combined-documentation \ --target BinaryParseKit \ diff --git a/.swift-version b/.swift-version index 0cda48a..bee9433 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -6.2 +6.2.3 diff --git a/.swiftlint.yml b/.swiftlint.yml index 5855415..f7d4348 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -20,7 +20,7 @@ function_body_length: warning: 300 error: 500 file_length: - warning: 800 + warning: 1000 error: 1200 ignore_comment_only_lines: true large_tuple: @@ -33,6 +33,7 @@ identifier_name: - j - x - y + - "b[0-9]" allowed_symbols: ["_", " "] cyclomatic_complexity: warning: 20 diff --git a/Benchmarks/BenchmarkTypes/BenchmarkBitmaskComplex.swift b/Benchmarks/BenchmarkTypes/BenchmarkBitmaskComplex.swift index d4681dc..d0d0803 100644 --- a/Benchmarks/BenchmarkTypes/BenchmarkBitmaskComplex.swift +++ b/Benchmarks/BenchmarkTypes/BenchmarkBitmaskComplex.swift @@ -11,8 +11,6 @@ import Foundation @ParseBitmask public struct BenchmarkBitmaskComplex: Equatable, Sendable, BaselineParsable { - public typealias RawBitsInteger = UInt32 - @mask(bitCount: 1) public var flag1: UInt8 diff --git a/Benchmarks/BenchmarkTypes/BenchmarkBitmaskSimple.swift b/Benchmarks/BenchmarkTypes/BenchmarkBitmaskSimple.swift index f264935..aeec916 100644 --- a/Benchmarks/BenchmarkTypes/BenchmarkBitmaskSimple.swift +++ b/Benchmarks/BenchmarkTypes/BenchmarkBitmaskSimple.swift @@ -11,8 +11,6 @@ import Foundation @ParseBitmask public struct BenchmarkBitmaskSimple: Equatable, Sendable, BaselineParsable { - public typealias RawBitsInteger = UInt8 - @mask(bitCount: 1) public var flag: UInt8 diff --git a/Benchmarks/BenchmarkTypes/NonByteAlignedBitmask.swift b/Benchmarks/BenchmarkTypes/NonByteAlignedBitmask.swift index 31db372..f0c73f0 100644 --- a/Benchmarks/BenchmarkTypes/NonByteAlignedBitmask.swift +++ b/Benchmarks/BenchmarkTypes/NonByteAlignedBitmask.swift @@ -11,8 +11,6 @@ import Foundation @ParseBitmask public struct NonByteAlignedBitmask: Equatable, Sendable, BaselineParsable { - public typealias RawBitsInteger = UInt16 - @mask(bitCount: 3) public var first: UInt8 diff --git a/Benchmarks/BenchmarkTypes/UInt16+.swift b/Benchmarks/BenchmarkTypes/UInt16+.swift index 1f9cb92..c4f63bf 100644 --- a/Benchmarks/BenchmarkTypes/UInt16+.swift +++ b/Benchmarks/BenchmarkTypes/UInt16+.swift @@ -8,10 +8,8 @@ import BinaryParseKit import Foundation extension UInt16: ExpressibleByRawBits { - public typealias RawBitsInteger = UInt16 - - public init(bits: RawBitsInteger) throws { - self = bits + public init(bits: borrowing RawBitsSpan) throws { + self = try bits.load(as: Self.self) } } diff --git a/Benchmarks/BenchmarkTypes/Utils.swift b/Benchmarks/BenchmarkTypes/Utils.swift new file mode 100644 index 0000000..7db9221 --- /dev/null +++ b/Benchmarks/BenchmarkTypes/Utils.swift @@ -0,0 +1,10 @@ +import Benchmark + +public extension Benchmark { + @inlinable + func context(_ body: () -> Void) { + startMeasurement() + body() + stopMeasurement() + } +} diff --git a/Benchmarks/BenchmarkTypesTests/BenchmarkTypesTests.swift b/Benchmarks/BenchmarkTypesTests/BenchmarkTypesTests.swift index 656fe50..190a5dd 100644 --- a/Benchmarks/BenchmarkTypesTests/BenchmarkTypesTests.swift +++ b/Benchmarks/BenchmarkTypesTests/BenchmarkTypesTests.swift @@ -6,6 +6,7 @@ // import BenchmarkTypes +import BinaryParseKit import Foundation import Testing @@ -113,12 +114,15 @@ struct BenchmarkTypesTests { @Test("Parse simple bitmask") func parseSimpleBitmask() throws { - let bits: UInt8 = 0b1010_0011 - let parsed = try BenchmarkBitmaskSimple(bits: bits) - let baseline = BenchmarkBitmaskSimple.parseBaseline(Data([bits])) - #expect(parsed.flag == 1) - #expect(parsed.value == 0x23) - #expect(parsed == baseline) + let data = Data([0b1010_0011]) + try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + let parsed = try BenchmarkBitmaskSimple(bits: rawBits) + let baseline = BenchmarkBitmaskSimple.parseBaseline(data) + #expect(parsed.flag == 1) + #expect(parsed.value == 0x23) + #expect(parsed == baseline) + } } // MARK: - Complex Bitmask Tests @@ -126,15 +130,17 @@ struct BenchmarkTypesTests { @Test("Parse complex bitmask") func parseComplexBitmask() throws { let data = Data([0xAB, 0xCD, 0xEF, 0x12]) - let bits: UInt32 = 0xABCD_EF12 - let parsed = try BenchmarkBitmaskComplex(bits: bits) - let baseline = BenchmarkBitmaskComplex.parseBaseline(data) - #expect(parsed.flag1 == 1) - #expect(parsed.priority == 2) - #expect(parsed.nibble == 11) - #expect(parsed.byte == 0xCD) - #expect(parsed.word == 0xEF12) - #expect(parsed == baseline) + try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + let parsed = try BenchmarkBitmaskComplex(bits: rawBits) + let baseline = BenchmarkBitmaskComplex.parseBaseline(data) + #expect(parsed.flag1 == 1) + #expect(parsed.priority == 2) + #expect(parsed.nibble == 11) + #expect(parsed.byte == 0xCD) + #expect(parsed.word == 0xEF12) + #expect(parsed == baseline) + } } // MARK: - Endianness Tests @@ -164,13 +170,15 @@ struct BenchmarkTypesTests { @Test("Parse non-byte-aligned bitmask") func parseNonByteAlignedBitmask() throws { let data = Data([0xAC, 0xC0]) - let bits: UInt16 = 0b1010_1100_1100_0000 - let parsed = try NonByteAlignedBitmask(bits: bits) - let baseline = NonByteAlignedBitmask.parseBaseline(data) - #expect(parsed.first == 5) // 101 - #expect(parsed.second == 12) // 01100 - #expect(parsed.third == 3) // 11 - #expect(parsed == baseline) + try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10) + let parsed = try NonByteAlignedBitmask(bits: rawBits) + let baseline = NonByteAlignedBitmask.parseBaseline(data) + #expect(parsed.first == 5) // 101 + #expect(parsed.second == 12) // 01100 + #expect(parsed.third == 3) // 11 + #expect(parsed == baseline) + } } // MARK: - Round-Trip Tests @@ -187,8 +195,10 @@ struct BenchmarkTypesTests { func roundTripSimpleBitmask() throws { let original = BenchmarkBitmaskSimple(flag: 1, value: 0x23) let printed = try original.printParsed(printer: .data) - let bits = printed[0] - let reparsed = try BenchmarkBitmaskSimple(bits: bits) - #expect(original == reparsed) + try printed.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + let reparsed = try BenchmarkBitmaskSimple(bits: rawBits) + #expect(original == reparsed) + } } } diff --git a/Benchmarks/ParsingBenchmarks/ParsingBenchmarks.swift b/Benchmarks/ParsingBenchmarks/ParsingBenchmarks.swift index 8dbc142..e664fb0 100644 --- a/Benchmarks/ParsingBenchmarks/ParsingBenchmarks.swift +++ b/Benchmarks/ParsingBenchmarks/ParsingBenchmarks.swift @@ -88,13 +88,16 @@ let benchmarks: @Sendable () -> Void = { // MARK: - Bitmask Parsing Benchmarks let simpleBitmaskData = Data([0xA3]) - let simpleBitmaskBits: UInt8 = 0b1010_0011 let complexBitmaskData = Data([0xAB, 0xCD, 0xEF, 0x12]) - let complexBitmaskBits: UInt32 = 0xABCD_EF12 Benchmark("Parse Simple Bitmask") { benchmark in - for _ in benchmark.scaledIterations { - blackHole(try! BenchmarkBitmaskSimple(bits: simpleBitmaskBits)) + simpleBitmaskData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + benchmark.context { + for _ in benchmark.scaledIterations { + blackHole(try! BenchmarkBitmaskSimple(bits: rawBits)) + } + } } } @@ -105,8 +108,13 @@ let benchmarks: @Sendable () -> Void = { } Benchmark("Parse Complex Bitmask") { benchmark in - for _ in benchmark.scaledIterations { - blackHole(try! BenchmarkBitmaskComplex(bits: complexBitmaskBits)) + complexBitmaskData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + benchmark.context { + for _ in benchmark.scaledIterations { + blackHole(try! BenchmarkBitmaskComplex(bits: rawBits)) + } + } } } @@ -165,11 +173,15 @@ let benchmarks: @Sendable () -> Void = { // MARK: - Non-Byte-Aligned Bitmask Benchmarks let nonAlignedData = Data([0xAC, 0xC0]) - let nonAlignedBits: UInt16 = 0b1010_1100_1100_0000 Benchmark("Parse Non-Byte-Aligned Bitmask (10 bits)") { benchmark in - for _ in benchmark.scaledIterations { - blackHole(try! NonByteAlignedBitmask(bits: nonAlignedBits)) + nonAlignedData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10) + benchmark.context { + for _ in benchmark.scaledIterations { + blackHole(try! NonByteAlignedBitmask(bits: rawBits)) + } + } } } diff --git a/Benchmarks/PrintingBenchmarks/PrintingBenchmarks.swift b/Benchmarks/PrintingBenchmarks/PrintingBenchmarks.swift index 958a0d6..58d2a61 100644 --- a/Benchmarks/PrintingBenchmarks/PrintingBenchmarks.swift +++ b/Benchmarks/PrintingBenchmarks/PrintingBenchmarks.swift @@ -137,7 +137,7 @@ let benchmarks: @Sendable () -> Void = { let roundTripEnumData = Data([0x03, 0x12, 0x34, 0x56, 0x78]) let roundTripStructData = Data([0x12, 0x34, 0x56, 0x78]) - let roundTripBitmaskBits: UInt8 = 0b1010_0011 + let roundTripBitmaskData = Data([0b1010_0011]) Benchmark("Round-Trip Enum (Parse + Print)") { benchmark in for _ in benchmark.scaledIterations { @@ -154,9 +154,14 @@ let benchmarks: @Sendable () -> Void = { } Benchmark("Round-Trip Bitmask (Parse + Print)") { benchmark in - for _ in benchmark.scaledIterations { - let parsed = try! BenchmarkBitmaskSimple(bits: roundTripBitmaskBits) - blackHole(try! parsed.printParsed(printer: .data)) + roundTripBitmaskData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + benchmark.context { + for _ in benchmark.scaledIterations { + let parsed = try! BenchmarkBitmaskSimple(bits: rawBits) + blackHole(try! parsed.printParsed(printer: .data)) + } + } } } diff --git a/Package.resolved b/Package.resolved index 0f55cf0..1c4c07f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,51 +1,6 @@ { - "originHash" : "2baa1566b6b5f040e5369e52c3b1d6ee79a9bb7348a57aa8f333cf7e4e1f8feb", + "originHash" : "491e0a0954c96bc9c49392122d5be36e770264882625a888e40a2f47bfb31c46", "pins" : [ - { - "identity" : "hdrhistogram-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/HdrHistogram/hdrhistogram-swift.git", - "state" : { - "revision" : "93a1618c8aa20f6a521a9da656a3e0591889e9dc", - "version" : "0.1.3" - } - }, - { - "identity" : "package-benchmark", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ordo-one/package-benchmark", - "state" : { - "revision" : "acfd97b98f5a40d963c89437e1cfaeab8ef10bf9", - "version" : "1.29.7" - } - }, - { - "identity" : "package-jemalloc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ordo-one/package-jemalloc.git", - "state" : { - "revision" : "e8a5db026963f5bfeac842d9d3f2cc8cde323b49", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", - "version" : "1.7.0" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", - "version" : "1.3.0" - } - }, { "identity" : "swift-binary-parsing", "kind" : "remoteSourceControl", @@ -109,15 +64,6 @@ "version" : "0.8.0" } }, - { - "identity" : "swift-numerics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics", - "state" : { - "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", - "version" : "1.1.1" - } - }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", @@ -136,24 +82,6 @@ "version" : "602.0.0" } }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", - "version" : "1.6.3" - } - }, - { - "identity" : "texttable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ordo-one/TextTable.git", - "state" : { - "revision" : "a27a07300cf4ae322e0079ca0a475c5583dd575f", - "version" : "0.0.2" - } - }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 43fd8f2..7c03879 100644 --- a/Package.swift +++ b/Package.swift @@ -2,8 +2,11 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import CompilerPluginSupport +import class Foundation.ProcessInfo import PackageDescription +private let enableBenchmark = ProcessInfo.processInfo.environment["ENABLE_BENCHMARK"] + let package = Package( name: "BinaryParseKit", platforms: [.macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16), .visionOS(.v1)], @@ -25,7 +28,6 @@ let package = Package( .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.5"), .package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.6.4"), .package(url: "https://github.com/stackotter/swift-macro-toolkit.git", from: "0.8.0"), - .package(url: "https://github.com/ordo-one/package-benchmark", from: "1.29.7"), ], targets: [ .macro( @@ -86,10 +88,21 @@ let package = Package( "BinaryParseKit", ], ), + + ], + swiftLanguageModes: [.v6], +) + +if enableBenchmark == "1" || enableBenchmark == "true" { + package.dependencies.append( + .package(url: "https://github.com/ordo-one/package-benchmark", from: "1.29.7"), + ) + package.targets.append(contentsOf: [ .target( name: "BenchmarkTypes", dependencies: [ "BinaryParseKit", + .product(name: "Benchmark", package: "package-benchmark"), ], path: "Benchmarks/BenchmarkTypes", ), @@ -120,6 +133,5 @@ let package = Package( ], path: "Benchmarks/PrintingBenchmarks", ), - ], - swiftLanguageModes: [.v6], -) + ]) +} diff --git a/Sources/BinaryParseKit/BinaryParseKit.swift b/Sources/BinaryParseKit/BinaryParseKit.swift index 0da3fcc..e7f3a95 100644 --- a/Sources/BinaryParseKit/BinaryParseKit.swift +++ b/Sources/BinaryParseKit/BinaryParseKit.swift @@ -256,6 +256,7 @@ public macro parseRest(endianness: Endianness) = #externalMacro( /// - A ``Printable`` conformance /// /// - Parameters: +/// - bitEndian: The bit ordering for bitmask parsing. Use `.big` for MSB-first (default) or `.little` for LSB-first. /// - parsingAccessor: The accessor level for the generated `Parsable` conformance (default is `.follow`) /// - printingAccessor: The accessor level for the generated `Printable` conformance (default is `.follow`) /// @@ -282,6 +283,7 @@ public macro parseRest(endianness: Endianness) = #externalMacro( /// ``` @attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary) public macro ParseStruct( + bitEndian: Endianness = .big, parsingAccessor: ExtensionAccessor = .follow, printingAccessor: ExtensionAccessor = .follow, ) = #externalMacro( @@ -302,6 +304,7 @@ public macro ParseStruct( /// - A ``Printable`` conformance /// /// - Parameters: +/// - bitEndian: The bit ordering for bitmask parsing. Use `.big` for MSB-first (default) or `.little` for LSB-first. /// - parsingAccessor: The accessor level for the generated `Parsable` conformance (default is `.follow`) /// - printingAccessor: The accessor level for the generated `Printable` conformance (default is `.follow`) /// @@ -311,6 +314,7 @@ public macro ParseStruct( /// - Note: any `match` macro has to proceed `parse` and `skip` macros. @attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary) public macro ParseEnum( + bitEndian: Endianness = .big, parsingAccessor: ExtensionAccessor = .follow, printingAccessor: ExtensionAccessor = .follow, ) = #externalMacro( @@ -582,7 +586,7 @@ public macro matchDefault() = #externalMacro( /// Parses a field at the bit level with an explicit bit count. /// /// Use this macro to parse a field using a specific number of bits from a bitmask. -/// The field type must conform to `ExpressibleByRawBits`. +/// The field type must conform to ``ExpressibleByRawBits``. /// /// Consecutive `@mask` fields are grouped together and read from a shared byte buffer. /// Gaps or interleaved non-mask fields start a new bitmask region. @@ -611,7 +615,7 @@ public macro mask(bitCount: Int) = #externalMacro( /// Parses a field at the bit level with inferred bit count. /// /// Use this macro to parse a field using the type's `bitCount` property. -/// The field type must conform to `BitmaskParsable` (which includes `BitCountProviding`). +/// The field type must conform to ``ExpressibleByRawBits`` and ``BitCountProviding``. /// /// Consecutive `@mask` fields are grouped together and read from a shared byte buffer. /// Gaps or interleaved non-mask fields start a new bitmask region. @@ -646,6 +650,7 @@ public macro mask() = #externalMacro( /// - An `init(bits: RawBits) throws` initializer /// /// - Parameters: +/// - bitEndian: The bit ordering for parsing. Use `.big` for MSB-first (default) or `.little` for LSB-first. /// - parsingAccessor: The accessor level for the generated initializer (default is `.follow`) /// - printingAccessor: The accessor level for the generated `bitCount` property (default is `.follow`) /// @@ -682,6 +687,7 @@ public macro mask() = #externalMacro( names: arbitrary ) public macro ParseBitmask( + bitEndian: Endianness = .big, parsingAccessor: ExtensionAccessor = .follow, printingAccessor: ExtensionAccessor = .follow, ) = #externalMacro( diff --git a/Sources/BinaryParseKit/Documentation.docc/Articles/GetStarted.md b/Sources/BinaryParseKit/Documentation.docc/Articles/GetStarted.md index e2c29d1..f6acff3 100644 --- a/Sources/BinaryParseKit/Documentation.docc/Articles/GetStarted.md +++ b/Sources/BinaryParseKit/Documentation.docc/Articles/GetStarted.md @@ -28,7 +28,7 @@ dependencies: [ ## Parsing -We have two parsing macros: ``ParseStruct(parsingAccessor:printingAccessor:)`` and ``ParseEnum(parsingAccessor:printingAccessor:)``. They work together with decorative macros such as ``parse()``, ``match()``, ``skip(byteCount:because:)``, etc. +We have two byte parsing macros: ``ParseStruct(bitEndian:parsingAccessor:printingAccessor:)wwww`` and ``ParseEnum(bitEndian:parsingAccessor:printingAccessor:)``. They work together with decorative macros such as ``parse()``, ``match()``, ``skip(byteCount:because:)``, etc. In addition, we have ``ParseBitmask(bitEndian:parsingAccessor:printingAccessor:)`` to handle bitmask parsing. ### Parse Struct diff --git a/Sources/BinaryParseKit/Documentation.docc/BinaryParseKit.md b/Sources/BinaryParseKit/Documentation.docc/BinaryParseKit.md index ac68f21..6aca74b 100644 --- a/Sources/BinaryParseKit/Documentation.docc/BinaryParseKit.md +++ b/Sources/BinaryParseKit/Documentation.docc/BinaryParseKit.md @@ -19,7 +19,7 @@ BinaryParseKit provides a convenient and type-safe way to parse binary data in S ### Struct Parsing Macros -- ``ParseStruct(parsingAccessor:printingAccessor:)`` +- ``ParseStruct(bitEndian:parsingAccessor:printingAccessor:)`` - ``parse()`` - ``parse(byteCount:)`` - ``parse(endianness:)`` @@ -32,15 +32,28 @@ BinaryParseKit provides a convenient and type-safe way to parse binary data in S ### Enum Parsing Macros -- ``ParseEnum(parsingAccessor:printingAccessor:)`` +- ``ParseEnum(bitEndian:parsingAccessor:printingAccessor:)`` - ``match()`` - ``match(byte:)`` - ``match(bytes:)`` +- ``match(length:)`` - ``matchAndTake()`` - ``matchAndTake(byte:)`` - ``matchAndTake(bytes:)`` - ``matchDefault()`` +### Bitmask Parsing Macros + +- ``ParseBitmask(bitEndian:parsingAccessor:printingAccessor:)`` +- ``mask()`` +- ``mask(bitCount:)`` +- ``RawBitsSpan`` +- ``ExpressibleByRawBits`` +- ``BitCountProviding`` +- ``RawBits`` +- ``RawBitsConvertible`` +- ``BitmaskParsableError`` + ### Parsable Protocols - ``Parsable`` diff --git a/Sources/BinaryParseKit/Extensions/ExpressibleByRawBits+.swift b/Sources/BinaryParseKit/Extensions/ExpressibleByRawBits+.swift index 1e98e2f..6d1cdd8 100644 --- a/Sources/BinaryParseKit/Extensions/ExpressibleByRawBits+.swift +++ b/Sources/BinaryParseKit/Extensions/ExpressibleByRawBits+.swift @@ -10,11 +10,12 @@ import Foundation // MARK: - Bool Conformance extension Bool: ExpressibleByRawBits { - public typealias RawBitsInteger = UInt8 - - public init(bits: RawBitsInteger) throws { - // bits is right-aligned, check LSB - self = (bits & 0x01) != 0 + public init(bits: borrowing RawBitsSpan) throws { + // Extract the first bit from the span + precondition(bits.bitCount == 1, "Bool requires only 1 bit") + let booleanInteger = bits.loadUnsafe(as: UInt8.self, bitCount: 1) + assert(booleanInteger == 0 || booleanInteger == 1, "Bool raw bits must be 0 or 1") + self = booleanInteger != 0 } } @@ -30,10 +31,10 @@ extension Bool: RawBitsConvertible { // MARK: - Integer Conformances extension UInt8: ExpressibleByRawBits { - public typealias RawBitsInteger = UInt8 - - public init(bits: RawBitsInteger) throws { - self = bits + public init(bits: borrowing RawBitsSpan) throws { + // Extract up to 8 bits from the span and convert to UInt8 + precondition(bits.bitCount <= 8, "UInt8 can hold at most 8 bits") + self = try bits.load(as: UInt8.self) } } @@ -48,10 +49,10 @@ extension UInt8: RawBitsConvertible { } extension Int8: ExpressibleByRawBits { - public typealias RawBitsInteger = UInt8 - - public init(bits: RawBitsInteger) throws { - self = Int8(bitPattern: bits) + public init(bits: borrowing RawBitsSpan) throws { + // Extract up to 8 bits from the span and convert to Int8 + let unsigned = try UInt8(bits: bits) + self = Int8(bitPattern: unsigned) } } diff --git a/Sources/BinaryParseKit/Protocols/BitmaskParsable.swift b/Sources/BinaryParseKit/Protocols/BitmaskParsable.swift index 0783abf..903ed39 100644 --- a/Sources/BinaryParseKit/Protocols/BitmaskParsable.swift +++ b/Sources/BinaryParseKit/Protocols/BitmaskParsable.swift @@ -10,8 +10,6 @@ import BinaryParsing public enum BitmaskParsableError: Error, Sendable { /// The bit count is not supported for the target type. case unsupportedBitCount - /// The raw bits integer type is not wide enough to hold the extracted bits. - case rawBitsIntegerNotWideEnough /// The specified bit count is less than what the type requires (Type.bitCount). case insufficientBitsAvailable } @@ -23,39 +21,29 @@ public enum BitmaskParsableError: Error, Sendable { /// Types conforming to this protocol can be constructed from raw bits, /// enabling bit-level parsing within binary data structures. /// -/// The `RawBitsInteger` associated type specifies the integer type used -/// to receive the extracted bits. The bits passed to `init(bits:)` are: -/// - **MSB-first extracted**: The first bits in the source become the most significant bits -/// - **Right-aligned**: The extracted bits are positioned at the LSB of the integer -/// - **Excess bits masked to 0**: Only the extracted bits are set; higher bits are zero -/// -/// For example, extracting 3 bits `0b011` from input `[0b0110_0000]` yields -/// `bits = 0b0000_0011` (value 3) when `RawBitsInteger` is `UInt8`. +/// The bits passed to `init(bits:)` are represented as a ``RawBitsSpan``, +/// which provides a view into a contiguous sequence of bytes. /// /// - Note: Callee assumes that `bits` contain sufficient bits for the type. For instance, -/// if the type requires 5 bits, the caller must ensure that `bits` contains at least 5 bits extracted +/// if the type requires 5 bits, the caller must ensure that `bits.bitCount >= 5`. /// /// Example: /// ```swift /// struct Priority: ExpressibleByRawBits { -/// typealias RawBitsInteger = UInt8 /// let value: UInt8 /// -/// init(bits: RawBitsInteger) throws { -/// self.value = UInt8(bits) +/// init(bits: borrowing RawBitsSpan) throws { +/// // Extract the value from the bits +/// self.value = bits._bytes.unsafeLoad(fromByteOffset: 0, as: UInt8.self) >> (8 - bits.bitCount) /// } /// } /// ``` public protocol ExpressibleByRawBits { - /// The integer type used to receive raw bits during parsing. - associatedtype RawBitsInteger: FixedWidthInteger - /// Creates an instance from extracted bits. /// - /// - Parameter bits: The extracted bits, right-aligned in the integer with - /// excess bits masked to 0. The bits are MSB-first extracted from the source. + /// - Parameter bits: The extracted bits as a span /// - Throws: An error if the bits cannot be converted to this type - init(bits: RawBitsInteger) throws + init(bits: borrowing RawBitsSpan) throws } /// A protocol for types that declare their bit width. diff --git a/Sources/BinaryParseKit/Types/RawBits.swift b/Sources/BinaryParseKit/Types/RawBits.swift index 2ce2739..5ecb992 100644 --- a/Sources/BinaryParseKit/Types/RawBits.swift +++ b/Sources/BinaryParseKit/Types/RawBits.swift @@ -7,10 +7,11 @@ import Foundation -/// A struct providing arbitrary-width bit storage for bitmask parsing operations. +/// A struct providing arbitrary-width bit storage for bitmask printing. /// /// `RawBits` stores a sequence of bits using `Data` for byte storage. -/// It provides operations for slicing, equality comparison, and bitwise operations. +/// It provides operations for equality comparison and appending. +/// It's currently not optimized, and can be improved later. public struct RawBits: Sendable { /// The number of valid bits stored. public let size: Int @@ -72,54 +73,6 @@ public struct RawBits: Sendable { public var byteCount: Int { (size + Self.BitsPerWord - 1) / Self.BitsPerWord } - - /// Extracts a single bit at the specified index (MSB-first ordering). - /// - /// - Parameter index: The bit index (0 is the most significant bit of the first byte) - /// - Returns: `true` if the bit is 1, `false` if 0 - public func bit(at index: Int) -> Bool { - precondition(index >= 0 && index < size, "Bit index out of range") - let byteIndex = index / Self.BitsPerWord - let bitOffset = index % Self.BitsPerWord - let byte = data[data.startIndex + byteIndex] - // MSB-first: bit 0 is the most significant bit (0x80) - return (byte & (0x80 >> bitOffset)) != 0 - } - - /// Extracts bits from the specified range as a UInt64. - /// - /// - Parameters: - /// - start: The starting bit index (inclusive, MSB-first) - /// - count: The number of bits to extract (max 64) - /// - Returns: The extracted bits as a UInt64, right-aligned - public func extractBits(from start: Int, count: Int) -> UInt8 { - precondition(start >= 0, "Start index must be non-negative") - precondition(count >= 0 && count <= 8, "Count must be 0-64") - precondition(start + count <= size, "Range exceeds size") - - if count == 0 { return 0 } - - let startByte = start / Self.BitsPerWord - let bitOffset = start % Self.BitsPerWord - let bytesSpanned = (count + (Self.BitsPerWord - 1)) / Self.BitsPerWord - - let dataSpan = data.bytes - - if bytesSpanned == 1 { - var value = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte, as: UInt8.self) - value <<= bitOffset - value >>= (8 - count) - return value - } else { - // Slow path: need 9 bytes (bitOffset > 0 and count = 64) - var highValue = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte, as: UInt8.self) - let lowByte = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte + Self.BitsPerWord, as: UInt8.self) - - highValue <<= bitOffset - highValue |= lowByte >> (Self.BitsPerWord - bitOffset) - return highValue - } - } } // MARK: - Concatenation @@ -180,60 +133,6 @@ public extension RawBits { } } -// MARK: - Slicing - -public extension RawBits { - /// Creates a new RawBits containing a contiguous range of bits. - /// - /// - Parameters: - /// - start: The starting bit index (inclusive) - /// - count: The number of bits to include - /// - Returns: A new RawBits containing the specified bits - func slice(from start: Int, count: Int) -> RawBits { - precondition(start >= 0, "Start index must be non-negative") - precondition(count >= 0, "Count must be non-negative") - precondition(start + count <= size, "Slice range exceeds size") - - if count == 0 { - return RawBits() - } - - let resultByteCount = (count + Self.BitsPerWord - 1) / Self.BitsPerWord - var resultData = Data(repeating: 0, count: resultByteCount) - - let startByte = start / Self.BitsPerWord - let bitOffset = start % Self.BitsPerWord - let dataSpan = data.bytes - - if bitOffset == 0 { - // Fast path: byte-aligned, just copy bytes - for i in 0 ..< resultByteCount { - resultData[i] = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte + i, as: UInt8.self) - } - } else { - // Combine bytes with shifting, loading each byte only once - var i = 0 - var currentByte = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte, as: UInt8.self) - - while i < resultByteCount { - var value = currentByte << bitOffset - - let nextByteIndex = startByte + i + 1 - if nextByteIndex < data.count { - let nextByte = unsafe dataSpan.unsafeLoad(fromByteOffset: nextByteIndex, as: UInt8.self) - value |= nextByte >> (Self.BitsPerWord - bitOffset) - currentByte = nextByte - } - - resultData[i] = value - i += 1 - } - } - - return RawBits(data: resultData, size: count) - } -} - // MARK: - Equality extension RawBits: Equatable { @@ -261,157 +160,3 @@ extension RawBits: Equatable { return true } } - -// MARK: - Slice Equality with Offset - -public extension RawBits { - /// Compares a portion of this RawBits against another RawBits. - /// - /// - Parameters: - /// - other: The RawBits to compare against - /// - offset: The starting bit offset in this RawBits - /// - Returns: `true` if the bits match - func sliceEquals(_ other: RawBits, at offset: Int = 0) -> Bool { - precondition(offset >= 0, "Offset must be non-negative") - precondition(offset + other.size <= size, "Range exceeds size") - - if other.size == 0 { - return true - } - - let startByte = offset / Self.BitsPerWord - let bitOffset = offset % Self.BitsPerWord - - let fullBytes = other.size / Self.BitsPerWord - let remainingBits = other.size % Self.BitsPerWord - - if bitOffset == 0 { - // Fast path: byte-aligned, direct comparison - for i in 0 ..< fullBytes - where data[data.startIndex + startByte + i] != other.data[other.data.startIndex + i] { - return false - } - - // Compare tail (remaining bits) - if remainingBits > 0 { - let selfByte = data[data.startIndex + startByte + fullBytes] - let otherByte = other.data[other.data.startIndex + fullBytes] - let mask: UInt8 = 0xFF << (Self.BitsPerWord - remainingBits) - if (selfByte & mask) != (otherByte & mask) { return false } - } - } else { - // Non-aligned: shift self's bytes to align with other - let selfSpan = data.bytes - var i = 0 - var currentByte = unsafe selfSpan.unsafeLoad(fromByteOffset: startByte, as: UInt8.self) - - // Compare middle (full bytes) - while i < fullBytes { - var selfValue = currentByte << bitOffset - - let nextByteIndex = startByte + i + 1 - if nextByteIndex < data.count { - let nextByte = unsafe selfSpan.unsafeLoad(fromByteOffset: nextByteIndex, as: UInt8.self) - selfValue |= nextByte >> (Self.BitsPerWord - bitOffset) - currentByte = nextByte - } - - if selfValue != other.data[other.data.startIndex + i] { return false } - i += 1 - } - - // Compare tail (remaining bits) - if remainingBits > 0 { - var selfValue = currentByte << bitOffset - - let nextByteIndex = startByte + i + 1 - if nextByteIndex < data.count { - let nextByte = unsafe selfSpan.unsafeLoad(fromByteOffset: nextByteIndex, as: UInt8.self) - selfValue |= nextByte >> (Self.BitsPerWord - bitOffset) - } - - let otherByte = other.data[other.data.startIndex + fullBytes] - let mask: UInt8 = 0xFF << (Self.BitsPerWord - remainingBits) - if (selfValue & mask) != (otherByte & mask) { return false } - } - } - - return true - } -} - -// MARK: - Bitwise Operations - -public extension RawBits { - /// Performs bitwise AND with another RawBits. - /// - /// The result size is the minimum of the two operand sizes. - static func & (lhs: RawBits, rhs: RawBits) -> [UInt8] { - let resultSize = min(lhs.size, rhs.size) - let byteCount = (resultSize + BitsPerWord - 1) / BitsPerWord - - var result = [UInt8](repeating: 0, count: byteCount) - for i in 0 ..< byteCount { - let lhsByte = lhs.data[lhs.data.startIndex + i] - let rhsByte = rhs.data[rhs.data.startIndex + i] - result[i] = lhsByte & rhsByte - } - - // Mask the last byte if needed - let remainingBits = resultSize % BitsPerWord - if remainingBits > 0, byteCount > 0 { - let mask: UInt8 = 0xFF << (8 - remainingBits) - result[byteCount - 1] &= mask - } - - return result - } - - /// Performs bitwise OR with another RawBits. - /// - /// The result size is the minimum of the two operand sizes. - static func | (lhs: RawBits, rhs: RawBits) -> [UInt8] { - let resultSize = min(lhs.size, rhs.size) - let byteCount = (resultSize + BitsPerWord - 1) / BitsPerWord - - var result = [UInt8](repeating: 0, count: byteCount) - for i in 0 ..< byteCount { - let lhsByte = lhs.data[lhs.data.startIndex + i] - let rhsByte = rhs.data[rhs.data.startIndex + i] - result[i] = lhsByte | rhsByte - } - - // Mask the last byte if needed - let remainingBits = resultSize % BitsPerWord - if remainingBits > 0, byteCount > 0 { - let mask: UInt8 = 0xFF << (8 - remainingBits) - result[byteCount - 1] &= mask - } - - return result - } - - /// Performs bitwise XOR with another RawBits. - /// - /// The result size is the minimum of the two operand sizes. - static func ^ (lhs: RawBits, rhs: RawBits) -> [UInt8] { - let resultSize = min(lhs.size, rhs.size) - let byteCount = (resultSize + BitsPerWord - 1) / BitsPerWord - - var result = [UInt8](repeating: 0, count: byteCount) - for i in 0 ..< byteCount { - let lhsByte = lhs.data[lhs.data.startIndex + i] - let rhsByte = rhs.data[rhs.data.startIndex + i] - result[i] = lhsByte ^ rhsByte - } - - // Mask the last byte if needed - let remainingBits = resultSize % BitsPerWord - if remainingBits > 0, byteCount > 0 { - let mask: UInt8 = 0xFF << (8 - remainingBits) - result[byteCount - 1] &= mask - } - - return result - } -} diff --git a/Sources/BinaryParseKit/Utils/ParsingUtils.swift b/Sources/BinaryParseKit/Utils/ParsingUtils.swift index 26793d7..1660da3 100644 --- a/Sources/BinaryParseKit/Utils/ParsingUtils.swift +++ b/Sources/BinaryParseKit/Utils/ParsingUtils.swift @@ -8,6 +8,7 @@ import BinaryParsing /// Matches the given bytes in the input parser span. /// - Warning: This function is used by `@ParseEnum` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __match(_ bytes: borrowing [UInt8], in input: borrowing BinaryParsing.ParserSpan) -> Bool { if bytes.isEmpty { return true } @@ -29,6 +30,7 @@ public func __match(_ bytes: borrowing [UInt8], in input: borrowing BinaryParsin /// Matches when the remaining bytes in the input parser span equals the specified length. /// - Warning: This function is used by `@ParseEnum` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __match(length: Int, in input: borrowing BinaryParsing.ParserSpan) -> Bool { input.count == length @@ -36,21 +38,25 @@ public func __match(length: Int, in input: borrowing BinaryParsing.ParserSpan) - /// Asserts that the given type conforms to `Parsable`. /// - Warning: This function is used to `@parse` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertParsable(_: (some Parsable).Type) {} /// Asserts that the given type conforms to `SizedParsable`. /// - Warning: This function is used to `@parse` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertSizedParsable(_: (some SizedParsable).Type) {} /// Asserts that the given type conforms to `EndianParsable`. /// - Warning: This function is used to `@parse` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertEndianParsable(_: (some EndianParsable).Type) {} /// Asserts that the given type conforms to `EndianSizedParsable`. /// - Warning: This function is used to `@parse` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertEndianSizedParsable(_: (some EndianSizedParsable).Type) {} @@ -58,97 +64,21 @@ public func __assertEndianSizedParsable(_: (some EndianSizedParsable).Type) {} /// Asserts that the given type conforms to `BitmaskParsable`. /// - Warning: This function is used by `@mask()` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertBitmaskParsable(_: (some ExpressibleByRawBits & BitCountProviding).Type) {} /// Asserts that the given type conforms to `ExpressibleByRawBits`. /// - Warning: This function is used by `@mask(bitCount:)` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertExpressibleByRawBits(_: (some ExpressibleByRawBits).Type) {} -/// Extracts bits from a ParserSpan and returns them as a right-aligned FixedWidthInteger. -/// -/// The bits are extracted in MSB-first order and returned right-aligned in the integer. -/// Excess bits in the integer are masked to 0. -/// -/// - Parameters: -/// - type: The integer type to return -/// - input: The source ParserSpan -/// - offset: Bit offset to start extraction -/// - count: Number of bits to extract -/// - Returns: The extracted bits right-aligned in the integer with excess bits masked to 0 -/// -/// Example: Input `[0b0110_1101]` extracting 3 bits at offset 0: -/// - MSB-first extraction: bits 0, 1, 2 → values 0, 1, 1 -/// - Right-aligned result: 0b0000_0011 = 3 -/// - Warning: This function is used by bitmask macros and should not be used directly. -/// - Important: `input` __must__ have at least `(offset + count + 7) / 8` bytes available. -@inlinable -func __extractBitsAsInteger( - _: I.Type, - from input: borrowing BinaryParsing.ParserSpan, - offset: Int, - count: Int, -) throws -> I { - precondition(count >= 0, "Count has to be grater than 0") - - guard count <= I.bitWidth else { - throw BitmaskParsableError.rawBitsIntegerNotWideEnough - } - - guard count > 0 else { return 0 } - - let startByte = offset / 8 - let bitOffset = offset % 8 - let dataSpan = input.bytes - - // For small extractions (up to 8 bits), use optimized single/double byte path - if count <= 8 { - var value: UInt8 - if bitOffset + count <= 8 { - // Single byte extraction - value = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte, as: UInt8.self) - value <<= bitOffset - value >>= (8 - count) - } else { - // Two byte extraction - let highByte = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte, as: UInt8.self) - let lowByte = unsafe dataSpan.unsafeLoad(fromByteOffset: startByte + 1, as: UInt8.self) - let combined = (UInt16(highByte) << 8) | UInt16(lowByte) - value = UInt8((combined << bitOffset) >> (16 - count)) - } - return I(value) - } - - // For larger extractions, build the integer using << and | for speed - var result: I = 0 - var bitsRemaining = count - var currentByteIndex = startByte - var currentBitOffset = bitOffset - - while bitsRemaining > 0 { - let bitsInCurrentByte = min(8 - currentBitOffset, bitsRemaining) - let byte = unsafe dataSpan.unsafeLoad(fromByteOffset: currentByteIndex, as: UInt8.self) - - // Extract bits from this byte: shift left to clear leading bits, shift right to position - let extracted = (byte << currentBitOffset) >> (8 - bitsInCurrentByte) - - // Add to result - result = (result << bitsInCurrentByte) | I(extracted) - - bitsRemaining -= bitsInCurrentByte - currentByteIndex += 1 - currentBitOffset = 0 - } - - // The result is already right-aligned and masked by construction - return result -} - // MARK: - RawBits Conversion Utilities /// Asserts that the given type conforms to `RawBitsConvertible` and `BitCountProviding`. /// - Warning: This function is used by `@ParseBitmask` macro and should not be used directly. +@_documentation(visibility: internal) @inlinable public func __assertRawBitsConvertible(_: (some RawBitsConvertible & BitCountProviding).Type) {} @@ -159,6 +89,7 @@ public func __assertRawBitsConvertible(_: (some RawBitsConvertible & BitCountPro /// - bitCount: The number of bits to produce /// - Returns: The raw bits representation /// - Throws: An error if the conversion cannot be performed +@_documentation(visibility: internal) @inlinable public func __toRawBits( _ value: some RawBitsConvertible, @@ -169,96 +100,54 @@ public func __toRawBits( // MARK: - Bit Adjustment Utilities for @mask(bitCount:) -/// Overload for types that also conform to BitCountProviding - handles bit count validation and adjustment. +/// Creates an instance of the specified type from the given raw bits span, +/// +/// Overload for types that also conform to ``BitCountProviding``. +/// +/// - Warning: This function is used by macro generation and should not be used directly. +@_documentation(visibility: internal) @inlinable -func __createFromBits( +public func __createFromBits( _: T.Type, - fieldBits: some FixedWidthInteger, + fieldBits: borrowing RawBitsSpan, fieldRequestedBitCount: Int, + bitEndian: BinaryParsing.Endianness = .big, ) throws -> T { let typeBitCount = T.bitCount if fieldRequestedBitCount < typeBitCount { throw BitmaskParsableError.insufficientBitsAvailable } else if fieldRequestedBitCount > typeBitCount { - let adjustedBits = fieldBits >> (fieldRequestedBitCount - typeBitCount) - return try T(bits: T.RawBitsInteger(truncatingIfNeeded: adjustedBits)) + // Need to adjust the bit count in the span to match what the type expects + // For big endian, the significant bits are at the start (MSB side) + // For little endian, the significant bits are at the end (LSB side) + let adjustedBitCount = typeBitCount + let adjustedSpan: RawBitsSpan + switch bitEndian { + case .big: + adjustedSpan = fieldBits.__extracting(unchecked: (), first: adjustedBitCount) + case .little: + adjustedSpan = fieldBits.__extracting(unchecked: (), last: adjustedBitCount) + default: + fatalError("Unexpected bit endianness value: \(bitEndian)") + } + return try T(bits: adjustedSpan) } else { - return try T(bits: T.RawBitsInteger(truncatingIfNeeded: fieldBits)) + return try T(bits: fieldBits) } } -/// Fallback overload for types that only conform to ExpressibleByRawBits. +/// Creates an instance of the specified type from the given raw bits span, +/// +/// Fallback overload for types that only conform to ``ExpressibleByRawBits``. +/// +/// - Warning: This function is used by macro generation and should not be used directly. +@_documentation(visibility: internal) @inlinable -func __createFromBits( +public func __createFromBits( _: T.Type, - fieldBits: some FixedWidthInteger, + fieldBits: borrowing RawBitsSpan, fieldRequestedBitCount _: Int, + bitEndian _: BinaryParsing.Endianness = .big, ) throws -> T { - try T(bits: T.RawBitsInteger(truncatingIfNeeded: fieldBits)) -} - -/// Specialized overload for fields that also conform to BitCountProviding - enables bit count validation. -@inlinable -public func __maskParsing( - from bits: Parent.RawBitsInteger, - parentType _: Parent.Type, - fieldType: Field.Type, - fieldRequestedBitCount: Int, - at bitPosition: Int, -) throws -> Field { - let shift = Parent.RawBitsInteger.bitWidth - bitPosition - fieldRequestedBitCount - let mask: Parent.RawBitsInteger = (1 << fieldRequestedBitCount) &- 1 - let fieldBits: Parent.RawBitsInteger = (bits >> shift) & mask - - return try __createFromBits(fieldType, fieldBits: fieldBits, fieldRequestedBitCount: fieldRequestedBitCount) -} - -/// Fallback overload for fields that only conform to ExpressibleByRawBits. -@inlinable -public func __maskParsing( - from bits: Parent.RawBitsInteger, - parentType _: Parent.Type, - fieldType: Field.Type, - fieldRequestedBitCount: Int, - at bitPosition: Int, -) throws -> Field { - let shift = Parent.RawBitsInteger.bitWidth - bitPosition - fieldRequestedBitCount - let mask: Parent.RawBitsInteger = (1 << fieldRequestedBitCount) &- 1 - let fieldBits: Parent.RawBitsInteger = (bits >> shift) & mask - - return try __createFromBits(fieldType, fieldBits: fieldBits, fieldRequestedBitCount: fieldRequestedBitCount) -} - -/// Specialized overload for fields that also conform to BitCountProviding - enables bit count validation. -@inlinable -public func __maskParsing( - from span: borrowing BinaryParsing.ParserSpan, - fieldType: Field.Type, - fieldRequestedBitCount: Int, - at bitOffset: Int, -) throws -> Field { - let fieldBits = try __extractBitsAsInteger( - UInt64.self, - from: span, - offset: bitOffset, - count: fieldRequestedBitCount, - ) - return try __createFromBits(fieldType, fieldBits: fieldBits, fieldRequestedBitCount: fieldRequestedBitCount) -} - -/// Fallback overload for fields that only conform to ExpressibleByRawBits. -@inlinable -public func __maskParsing( - from span: borrowing BinaryParsing.ParserSpan, - fieldType: Field.Type, - fieldRequestedBitCount: Int, - at bitOffset: Int, -) throws -> Field { - let fieldBits = try __extractBitsAsInteger( - UInt64.self, - from: span, - offset: bitOffset, - count: fieldRequestedBitCount, - ) - return try __createFromBits(fieldType, fieldBits: fieldBits, fieldRequestedBitCount: fieldRequestedBitCount) + try T(bits: fieldBits) } diff --git a/Sources/BinaryParseKit/Utils/PrinterUtils.swift b/Sources/BinaryParseKit/Utils/PrinterUtils.swift index a4fca69..1aa15ea 100644 --- a/Sources/BinaryParseKit/Utils/PrinterUtils.swift +++ b/Sources/BinaryParseKit/Utils/PrinterUtils.swift @@ -6,6 +6,7 @@ // /// - Note: This function is intended to be used only by the macro system. +@_documentation(visibility: internal) public func __getPrinterIntel(_ value: T) throws -> PrinterIntel { if let intel = (value as? Printable) { return try intel.printerIntel() diff --git a/Sources/BinaryParseKit/Utils/RawBitsSpan.swift b/Sources/BinaryParseKit/Utils/RawBitsSpan.swift new file mode 100644 index 0000000..8c0f68b --- /dev/null +++ b/Sources/BinaryParseKit/Utils/RawBitsSpan.swift @@ -0,0 +1,333 @@ +// +// RawBitsSpan.swift +// BinaryParseKit +// +// Created by Larry Zeng on 1/8/26. +// + +/// A non-escapable, non-copyable span representing a sequence of raw bits. +/// +/// `RawBitsSpan` provides a view into a contiguous sequence of bytes with an associated bit count, +/// enabling efficient bit-level operations without allocating or copying data. +/// +/// Example: +/// ```swift +/// let data: [UInt8] = [0b1101_0000] +/// data.withUnsafeBytes { buffer in +/// let span = RawBitsSpan(RawSpan(buffer), bitOffset: 1, bitCount: 4) +/// // Represents the bits: 1, 0, 1, 0 (4 bits of 0b1101_0000 after the first bit) +/// } +/// ``` +public struct RawBitsSpan: ~Escapable, ~Copyable { + public typealias Buffer = Span + + /// The underlying byte span containing the raw bits. + @usableFromInline + private(set) var _bytes: Buffer + + /// The bit offset from the start of the buffer + @usableFromInline + private(set) var _bitOffset: Int + + /// The number of valid bits in the span. + /// + /// This value indicates how many bits from `bitOffset` are considered + /// part of this bit sequence. + @usableFromInline + private(set) var _bitCount: Int + + /// The bit index where this RawBitsSpan starts (inclusive) + /// + /// This value is always between 0 and 7 (inclusive), representing the offset from the beginning of the first byte. + @inlinable + public var bitStartIndex: Int { + _bitOffset % 8 + } + + /// The bit index where this RawBitsSpan ends (exclusive) + /// + /// Equals to ``bitStartIndex`` + ``bitCount``. + @inlinable + public var bitEndIndex: Int { + bitStartIndex + bitCount + } + + /// Public accessor for underlying bytes. + /// + /// The bits are from in the range between ``bitStartIndex`` and ``bitEndIndex`` + public var bytes: Buffer { + @inlinable + @_lifetime(copy self) + borrowing get { + unsafe _bytes.extracting(unchecked: _bitOffset / 8 ..< (_bitOffset + _bitCount + 7) / 8) + } + } + + /// The bit count held by this span. + @inlinable + public var bitCount: Int { + _bitCount + } + + /// The number of bytes of ``bitCount`` + /// + /// Calculated as `(bitCount + 7) / 8` + @inlinable + public var byteCount: Int { + (bitCount + 7) / 8 + } + + /// The number of bytes used (touched/spanned) in the buffer + /// to cover all bits from ``bitStartIndex`` to ``bitEndIndex`` + /// + /// Calculated as `(_bitCount + bitStartIndex + 7) / 8` + @inlinable + public var bufferByteCount: Int { + (bitCount + bitStartIndex + 7) / 8 + } + + /// Creates a new raw bits span with a bit offset. + /// + /// - Parameters: + /// - bytes: The underlying byte span containing the raw bits + /// - bitOffset: The bit offset from the start (can be >= 8, will be normalized) + /// - bitCount: The number of valid bits in the span + /// + /// - Precondition: `bitOffset` must be non-negative + /// - Precondition: `bitCount` must be non-negative + /// - Precondition: The total bits (bitOffset + bitCount) must not exceed available bits in `bytes` + @inlinable + @_lifetime(copy bytes) + public init(_ bytes: RawSpan, bitOffset: Int = 0, bitCount: Int) { + precondition(bitOffset >= 0, "bitOffset must be non-negative") + precondition(bitCount >= 0, "bitCount must be non-negative") + precondition( + bitOffset + bitCount <= bytes.byteCount * 8, + "bitOffset + bitCount exceeds available bits in bytes", + ) + + self = .init(unchecked: (), bytes, bitOffset: bitOffset, bitCount: bitCount) + } + + @inlinable + @_lifetime(copy bytes) + init(unchecked _: Void, _ bytes: RawSpan, bitOffset: Int, bitCount: Int) { + self = .init(unchecked: (), Buffer(_bytes: bytes), bitOffset: bitOffset, bitCount: bitCount) + } + + @inlinable + @_lifetime(copy bytes) + init(unchecked _: Void, _ bytes: Buffer, bitOffset: Int, bitCount: Int) { + _bytes = bytes + _bitOffset = bitOffset + _bitCount = bitCount + } + + /// Copying a `RawBitsSpan`. + @inlinable + @_lifetime(copy other) + public init(copying other: borrowing RawBitsSpan) { + _bytes = other._bytes + _bitOffset = other._bitOffset + _bitCount = other._bitCount + } + + /// Converts the bits to a fixed-width integer value. + /// + /// The extracted are always right aligned in the resulting integer. That is, + /// the least significant bits of the integer correspond to the extracted bits. + /// + /// - Parameters: + /// - type: The integer type to convert to (optional, can be inferred) + /// - bitCount: The number of bits to extract (optional, defaults to `T.bitCount`) + /// - Returns: The extracted bits as a right-aligned integer + /// + /// Example: + /// ```swift + /// let data: [UInt8] = [0b1010_0000] + /// data.withUnsafeBytes { buffer in + /// let span = RawBitsSpan(RawSpan(buffer), bitOffset: 0, bitCount: 4) + /// let value: UInt8 = try span.load() // Returns 0b0000_1010 (10) + /// let partial: UInt8 = try span.load(bitCount: 2) // Returns 0b0000_0010 (2) + /// } + /// ``` + @inlinable + public borrowing func load(as _: T.Type = T.self, bitCount: Int? = nil) throws -> T { + let effectiveBitCount = Swift.min(bitCount ?? _bitCount, T.bitWidth) + + guard effectiveBitCount > 0 else { return 0 } + + // Validate that the requested bitCount doesn't exceed available bits + guard effectiveBitCount <= _bitCount else { + preconditionFailure("Requested bitCount (\(effectiveBitCount)) exceeds available bits (\(_bitCount))") + } + + return loadUnsafe(as: T.self, bitCount: effectiveBitCount) + } + + /// Loads bits as a fixed-width integer without bounds checking. + /// + /// This extracts bits starting from `_bitOffset` (which can be >= 8) in MSB-first order. + /// + /// - Parameters: + /// - type: The integer type to convert to (optional, can be inferred) + /// - bitCount: The number of bits to extract (optional, defaults to `I.bitCount`) + /// - Returns: The extracted bits as a right-aligned integer + @inlinable + public borrowing func loadUnsafe(as _: I.Type, bitCount: Int? = nil) -> I { + let count = Swift.min(bitCount ?? _bitCount, I.bitWidth) + precondition(count >= 0, "Count has to be greater than 0") + + guard count > 0 else { return 0 } + + let startByte = _bitOffset / 8 + let bitOffset = _bitOffset % 8 + let dataSpan = _bytes + + // For small extractions (up to 8 bits), use optimized single/double byte path + if count <= 8 { + var value: UInt8 + if bitOffset + count <= 8 { + // Single byte extraction + value = dataSpan[startByte] + value <<= bitOffset + value >>= (8 - count) + } else { + // Two byte extraction + let highByte = dataSpan[startByte] + let lowByte = dataSpan[startByte + 1] + let combined = (UInt16(highByte) << 8) | UInt16(lowByte) + value = UInt8((combined << bitOffset) >> (16 - count)) + } + return I(value) + } + + // For larger extractions, build the integer using << and | for speed + var result: I = 0 + var bitsRemaining = count + var currentByteIndex = startByte + var currentBitOffset = bitOffset + + while bitsRemaining > 0 { + let bitsInCurrentByte = min(8 - currentBitOffset, bitsRemaining) + let byte = dataSpan[currentByteIndex] + + // Extract bits from this byte: shift left to clear leading bits, shift right to position + // Keep operations in UInt8 space to ensure proper bit masking, then convert to I + let extracted = I((byte << currentBitOffset) >> (8 - bitsInCurrentByte)) + + // Add to result + result = (result << bitsInCurrentByte) | extracted + + bitsRemaining -= bitsInCurrentByte + currentByteIndex += 1 + currentBitOffset = 0 + } + + // The result is already right-aligned and masked by construction + return result + } + + // MARK: - Extracting (non-mutating) + + /// Unchecked version of ``extracting(first:)`` + @_documentation(visibility: internal) + @inlinable + @_lifetime(borrow self) + public borrowing func __extracting(unchecked _: Void, first count: Int) -> RawBitsSpan { + RawBitsSpan(unchecked: (), _bytes, bitOffset: _bitOffset, bitCount: count) + } + + /// Unchecked version of ``extracting(last:)`` + @_documentation(visibility: internal) + @inlinable + @_lifetime(borrow self) + public borrowing func __extracting(unchecked _: Void, last count: Int) -> RawBitsSpan { + let newBitOffset = _bitOffset + (_bitCount - count) + return RawBitsSpan(unchecked: (), _bytes, bitOffset: newBitOffset, bitCount: count) + } + + /// Returns a new `RawBitsSpan` containing the first `count` bits from this span. + /// + /// - Parameter count: The number of bits to extract from the start of the span + /// - Returns: A new `RawBitsSpan` containing the first `count` bits + /// + /// - Precondition: `count` must be non-negative and not exceed `self.bitCount` + @inlinable + @_lifetime(borrow self) + public borrowing func extracting(first count: Int) -> RawBitsSpan { + precondition(count >= 0, "count must be non-negative") + precondition(count <= _bitCount, "count must not exceed available bits") + return __extracting(unchecked: (), first: count) + } + + /// Returns a new `RawBitsSpan` containing the last `count` bits from this span. + /// + /// - Parameter count: The number of bits to extract from the end of the span + /// - Returns: A new `RawBitsSpan` containing the last `count` bits + /// + /// - Precondition: `count` must be non-negative and not exceed `self.bitCount` + @inlinable + @_lifetime(borrow self) + public borrowing func extracting(last count: Int) -> RawBitsSpan { + precondition(count >= 0, "count must be non-negative") + precondition(count <= _bitCount, "count must not exceed available bits") + return __extracting(unchecked: (), last: count) + } + + // MARK: - Slicing (mutating) + + /// Unchecked version of ``slicing(first:)`` + @_documentation(visibility: internal) + @inlinable + @_lifetime(copy self) + public mutating func __slicing(unchecked _: Void, first count: Int) -> RawBitsSpan { + let sliced = RawBitsSpan(unchecked: (), _bytes, bitOffset: _bitOffset, bitCount: count) + _bitOffset += count + _bitCount -= count + return sliced + } + + /// Unchecked version of ``slicing(last:)`` + @_documentation(visibility: internal) + @inlinable + @_lifetime(copy self) + public mutating func __slicing(unchecked _: Void, last count: Int) -> RawBitsSpan { + let newBitOffset = _bitOffset + (_bitCount - count) + let sliced = RawBitsSpan(unchecked: (), _bytes, bitOffset: newBitOffset, bitCount: count) + _bitCount -= count + return sliced + } + + /// Removes and returns the first `count` bits from this span. + /// + /// After this call, `self` contains the remaining bits (original bits after the sliced portion). + /// + /// - Parameter count: The number of bits to slice from the start of the span + /// - Returns: A new `RawBitsSpan` containing the sliced (first `count`) bits + /// + /// - Precondition: `count` must be non-negative and not exceed `self.bitCount` + @inlinable + @_lifetime(copy self) + public mutating func slicing(first count: Int) -> RawBitsSpan { + precondition(count >= 0, "count must be non-negative") + precondition(count <= _bitCount, "count must not exceed available bits") + return __slicing(unchecked: (), first: count) + } + + /// Removes and returns the last `count` bits from this span. + /// + /// After this call, `self` contains the remaining bits (original bits before the sliced portion). + /// + /// - Parameter count: The number of bits to slice from the end of the span + /// - Returns: A new `RawBitsSpan` containing the sliced (last `count`) bits + /// + /// - Precondition: `count` must be non-negative and not exceed `self.bitCount` + @inlinable + @_lifetime(copy self) + public mutating func slicing(last count: Int) -> RawBitsSpan { + precondition(count >= 0, "count must be non-negative") + precondition(count <= _bitCount, "count must not exceed available bits") + return __slicing(unchecked: (), last: count) + } +} diff --git a/Sources/BinaryParseKitClient/main.swift b/Sources/BinaryParseKitClient/main.swift index 99db97a..446f06d 100644 --- a/Sources/BinaryParseKitClient/main.swift +++ b/Sources/BinaryParseKitClient/main.swift @@ -1,5 +1,6 @@ import BinaryParseKit import BinaryParsing +import Foundation extension [UInt8]: SizedParsable { public init(parsing input: inout ParserSpan, byteCount: Int) throws(ThrownParsingError) { diff --git a/Sources/BinaryParseKitMacros/Macros/Extensions/String+.swift b/Sources/BinaryParseKitMacros/Macros/Extensions/String+.swift deleted file mode 100644 index 7ef7758..0000000 --- a/Sources/BinaryParseKitMacros/Macros/Extensions/String+.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// String+.swift -// BinaryParseKit -// -// Created by Larry Zeng on 12/30/25. -// - -extension String { - func escapeForVariableName() -> String { - replacingOccurrences(of: ".", with: "_") - } -} diff --git a/Sources/BinaryParseKitMacros/Macros/ParseBitmask/ConstructParseBitmaskMacro.swift b/Sources/BinaryParseKitMacros/Macros/ParseBitmask/ConstructParseBitmaskMacro.swift index 274f1a6..0d25b17 100644 --- a/Sources/BinaryParseKitMacros/Macros/ParseBitmask/ConstructParseBitmaskMacro.swift +++ b/Sources/BinaryParseKitMacros/Macros/ParseBitmask/ConstructParseBitmaskMacro.swift @@ -24,7 +24,7 @@ public struct ConstructParseBitmaskMacro: ExtensionMacro { let type = type.trimmed - let accessorInfo = try extractAccessor( + let configuration = try extractMacroConfiguration( from: node, attachedTo: declaration, in: context, @@ -53,51 +53,56 @@ public struct ConstructParseBitmaskMacro: ExtensionMacro { "extension \(type): \(raw: Constants.Protocols.expressibleByRawBitsProtocol), \(raw: Constants.Protocols.bitCountProvidingProtocol)", ) { // Static bitCount property - try VariableDeclSyntax("\(accessorInfo.printingAccessor) static var bitCount: Int") { + try VariableDeclSyntax("\(configuration.printingAccessor) static var bitCount: Int") { totalBitCountExpr } // init(bits:) initializer try InitializerDeclSyntax( - "\(accessorInfo.parsingAccessor) init(bits: RawBitsInteger) throws", + "\(configuration.parsingAccessor) init(bits: borrowing BinaryParseKit.RawBitsSpan) throws", ) { - "var bitPosition = 0" + let bitsSpan = context.makeUniqueName("__bitsSpan") + "var \(bitsSpan) = RawBitsSpan(copying: bits)" for fieldInfo in fieldVisitor.fields.values { + let subSpan = context.makeUniqueName("__subSpan") + let bitCount = context.makeUniqueName("__bitCount") + let fieldType = fieldInfo.type + let fieldName = fieldInfo.name + let bitCountExpr: ExprSyntax = switch fieldInfo.maskInfo.bitCount { case let .specified(count): count.expr case .inferred: - "(\(fieldInfo.type)).bitCount" + "(\(fieldType)).bitCount" } switch fieldInfo.maskInfo.bitCount { case .specified: """ - // Parse `\(fieldInfo.name)` of type `\(fieldInfo - .type)` with specified bit count \(bitCountExpr) + // Parse `\(fieldName)` of type `\(fieldType)` with specified bit count \(bitCountExpr) \(raw: Constants.UtilityFunctions.assertExpressibleByRawBits)((\(fieldInfo.type)).self) """ case .inferred: """ - // Parse `\(fieldInfo.name)` of type `\(fieldInfo.type)` with inferred bit count - \(raw: Constants.UtilityFunctions.assertBitmaskParsable)((\(fieldInfo.type)).self) + // Parse `\(fieldName)` of type `\(fieldType)` with inferred bit count + \(raw: Constants.UtilityFunctions.assertBitmaskParsable)((\(fieldType)).self) """ } - // Extract field bits from the integer: shift right and mask - // bits >> (RawBitsInteger.bitWidth - bitPosition - fieldBitCount) & mask + // Extract field bits from the span + // Use first/last slicing based on bit endianness + let slicingMethod = configuration.isBigEndian ? "first" : "last" """ do { - let fieldBitCount = \(bitCountExpr) - self.\(fieldInfo.name) = try \(raw: Constants.UtilityFunctions.maskParsing)( - from: bits, - parentType: Self.self, - fieldType: (\(fieldInfo.type)).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let \(bitCount) = \(bitCountExpr) + let \(subSpan) = \(bitsSpan).__slicing(unchecked: (), \(raw: slicingMethod): \(bitCount)) + self.\(fieldName) = try \(raw: Constants.UtilityFunctions.createFromBits)( + (\(fieldType)).self, + fieldBits: \(subSpan), + fieldRequestedBitCount: \(bitCount), + bitEndian: .\(raw: configuration.bitEndian), ) - bitPosition += fieldBitCount } """ } @@ -110,7 +115,7 @@ public struct ConstructParseBitmaskMacro: ExtensionMacro { ) { // toRawBits(bitCount:) method try FunctionDeclSyntax( - "\(accessorInfo.printingAccessor) func toRawBits(bitCount: Int) throws -> BinaryParseKit.RawBits", + "\(configuration.printingAccessor) func toRawBits(bitCount: Int) throws -> BinaryParseKit.RawBits", ) { "var result = BinaryParseKit.RawBits()" @@ -142,7 +147,7 @@ public struct ConstructParseBitmaskMacro: ExtensionMacro { ) { // printerIntel() method try FunctionDeclSyntax( - "\(accessorInfo.printingAccessor) func printerIntel() throws -> PrinterIntel", + "\(configuration.printingAccessor) func printerIntel() throws -> PrinterIntel", ) { "let bits = try self.toRawBits(bitCount: Self.bitCount)" "return .bitmask(.init(bits: bits))" diff --git a/Sources/BinaryParseKitMacros/Macros/ParseEnum/ConstructParseEnumMacro.swift b/Sources/BinaryParseKitMacros/Macros/ParseEnum/ConstructParseEnumMacro.swift index 3068132..96c6232 100644 --- a/Sources/BinaryParseKitMacros/Macros/ParseEnum/ConstructParseEnumMacro.swift +++ b/Sources/BinaryParseKitMacros/Macros/ParseEnum/ConstructParseEnumMacro.swift @@ -23,7 +23,7 @@ public struct ConstructEnumParseMacro: ExtensionMacro { throw ParseEnumMacroError.onlyEnumsAreSupported } - let accessorInfo = try extractAccessor( + let configuration = try extractMacroConfiguration( from: attributeNode, attachedTo: enumDeclaration, in: context, @@ -38,14 +38,14 @@ public struct ConstructEnumParseMacro: ExtensionMacro { let parsingExtension = try buildParsingExtension( type: type, parseInfo: parseInfo, - accessorInfo: accessorInfo, + accessorInfo: configuration, context: context, ) let printerExtension = try buildPrinterExtension( type: type, parseInfo: parseInfo, - accessorInfo: accessorInfo, + accessorInfo: configuration, context: context, ) @@ -117,6 +117,7 @@ public struct ConstructEnumParseMacro: ExtensionMacro { try generateEnumMaskGroupBlock( maskActions: maskActions, caseElementName: caseParseInfo.caseElementName, + bitEndian: accessorInfo.bitEndian, context: context, ) } @@ -184,7 +185,8 @@ public struct ConstructEnumParseMacro: ExtensionMacro { ), ) } - }) + }, + ) let caseCodeBlock = try CodeBlockItemListSyntax { let bytesTakenInMatching = context.makeUniqueName("__bytesTakenInMatching") diff --git a/Sources/BinaryParseKitMacros/Macros/ParseEnum/EnumCaseParseInfo.swift b/Sources/BinaryParseKitMacros/Macros/ParseEnum/EnumCaseParseInfo.swift index b5e3fa3..da8bb57 100644 --- a/Sources/BinaryParseKitMacros/Macros/ParseEnum/EnumCaseParseInfo.swift +++ b/Sources/BinaryParseKitMacros/Macros/ParseEnum/EnumCaseParseInfo.swift @@ -71,10 +71,8 @@ enum EnumMatchTarget { // @matchAndTake(byte: byte) -> .byte([byte]) // @matchAndTake(bytes: bytes) -> .bytes(bytes) case bytes(ExprSyntax?) - // @match(length: length) -> .length(length) - case length(ExprSyntax) - // @matchDefault - case `default` + case length(ExprSyntax) // @match(length: length) -> .length(length) + case `default` // @matchDefault var matchBytes: ExprSyntax?? { if case let .bytes(bytes) = self { diff --git a/Sources/BinaryParseKitMacros/Macros/ParseStruct/ConstructParseStructMacro.swift b/Sources/BinaryParseKitMacros/Macros/ParseStruct/ConstructParseStructMacro.swift index ae7a1ef..bb1a866 100644 --- a/Sources/BinaryParseKitMacros/Macros/ParseStruct/ConstructParseStructMacro.swift +++ b/Sources/BinaryParseKitMacros/Macros/ParseStruct/ConstructParseStructMacro.swift @@ -21,7 +21,7 @@ public struct ConstructStructParseMacro: ExtensionMacro { throw ParseStructMacroError.onlyStructsAreSupported } - let accessorInfo = try extractAccessor( + let configuration = try extractMacroConfiguration( from: attributeNode, attachedTo: structDeclaration, in: context, @@ -38,7 +38,7 @@ public struct ConstructStructParseMacro: ExtensionMacro { let extensionSyntax = try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.parsableProtocol)") { try InitializerDeclSyntax( - "\(accessorInfo.parsingAccessor) init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))", + "\(configuration.parsingAccessor) init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))", ) { for actionGroup in actionGroups { switch actionGroup { @@ -52,7 +52,11 @@ public struct ConstructStructParseMacro: ExtensionMacro { case let .skip(skipInfo): generateSkipBlock(variableName: skipInfo.variableName, skipInfo: skipInfo.skipInfo) case let .maskGroup(maskFields): - try generateMaskGroupBlock(maskActions: maskFields, context: context) + try generateMaskGroupBlock( + maskActions: maskFields, + bitEndian: configuration.bitEndian, + context: context, + ) } } } @@ -60,7 +64,7 @@ public struct ConstructStructParseMacro: ExtensionMacro { let printerExtension = try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.printableProtocol)") { - try FunctionDeclSyntax("\(accessorInfo.printingAccessor) func printerIntel() throws -> PrinterIntel") { + try FunctionDeclSyntax("\(configuration.printingAccessor) func printerIntel() throws -> PrinterIntel") { var printingInfo: [PrintableFieldInfo] = [] for parseAction in actionGroups { switch parseAction { diff --git a/Sources/BinaryParseKitMacros/Macros/Supports/Constants.swift b/Sources/BinaryParseKitMacros/Macros/Supports/Constants.swift index 192cef8..2189876 100644 --- a/Sources/BinaryParseKitMacros/Macros/Supports/Constants.swift +++ b/Sources/BinaryParseKitMacros/Macros/Supports/Constants.swift @@ -1,9 +1,9 @@ -// -// Constants.swift -// BinaryParseKit -// -// Created by Larry Zeng on 11/6/25. -// +/// +/// Constants.swift +/// BinaryParseKit +/// +/// Created by Larry Zeng on 11/6/25. +/// struct PackageMember: CustomStringConvertible { let package: String let name: String @@ -54,8 +54,7 @@ extension Constants { static let assertExpressibleByRawBits = PackageMember(name: "__assertExpressibleByRawBits") static let assertRawBitsConvertible = PackageMember(name: "__assertRawBitsConvertible") static let toRawBits = PackageMember(name: "__toRawBits") - static let maskParsing = PackageMember(name: "__maskParsing") - static let maskParsingFromSpan = PackageMember(name: "__maskParsing") + static let createFromBits = PackageMember(name: "__createFromBits") } } @@ -65,12 +64,6 @@ extension Constants { } } -extension Constants { - enum BitmaskParsableError { - static let failedToParse = PackageMember(name: "BitmaskParsableError.rawBitsIntegerNotWideEnough") - } -} - extension Constants { enum BinaryParsing { private static let packageName = "BinaryParsing" diff --git a/Sources/BinaryParseKitMacros/Macros/Supports/MacroAccessorVisitor.swift b/Sources/BinaryParseKitMacros/Macros/Supports/MacroConfigurationVisitor.swift similarity index 62% rename from Sources/BinaryParseKitMacros/Macros/Supports/MacroAccessorVisitor.swift rename to Sources/BinaryParseKitMacros/Macros/Supports/MacroConfigurationVisitor.swift index 882aef3..cf7b99d 100644 --- a/Sources/BinaryParseKitMacros/Macros/Supports/MacroAccessorVisitor.swift +++ b/Sources/BinaryParseKitMacros/Macros/Supports/MacroConfigurationVisitor.swift @@ -1,5 +1,5 @@ // -// MacroAccessorVisitor.swift +// MacroConfigurationVisitor.swift // BinaryParseKit // // Created by Larry Zeng on 11/26/25. @@ -10,10 +10,11 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -enum MacroAccessorError: DiagnosticMessage, Error { +enum MacroConfigurationError: DiagnosticMessage, Error { case invalidAccessor(String) case moreThanOneModifier(modifiers: String) case unknownAccessor + case invalidBitEndian(String) var message: String { switch self { @@ -23,6 +24,8 @@ enum MacroAccessorError: DiagnosticMessage, Error { "More than one modifier found: \(modifiers). Only one modifier is allowed." case .unknownAccessor: "You have used unknown accessor in `@ParseStruct` or `@ParseEnum`." + case let .invalidBitEndian(value): + #"Invalid bitEndian value: \#(value); Please use .big or .little."# } } @@ -35,16 +38,18 @@ enum MacroAccessorError: DiagnosticMessage, Error { var severity: SwiftDiagnostics.DiagnosticSeverity { switch self { - case .invalidAccessor, .moreThanOneModifier, .unknownAccessor: .error + case .invalidAccessor, .moreThanOneModifier, .unknownAccessor, .invalidBitEndian: .error } } } -class MacroAccessorVisitor: SyntaxVisitor { +class MacroConfigurationVisitor: SyntaxVisitor { private static let defaultAccessor = ExtensionAccessor.follow - private(set) var printingAccessor: ExtensionAccessor = MacroAccessorVisitor.defaultAccessor - private(set) var parsingAccessor: ExtensionAccessor = MacroAccessorVisitor.defaultAccessor + private(set) var printingAccessor: ExtensionAccessor = MacroConfigurationVisitor.defaultAccessor + private(set) var parsingAccessor: ExtensionAccessor = MacroConfigurationVisitor.defaultAccessor + /// The bit endian value: "big" or "little". Defaults to "big". + private(set) var bitEndian: String = "big" private let context: any MacroExpansionContext @@ -55,30 +60,48 @@ class MacroAccessorVisitor: SyntaxVisitor { override func visit(_ node: LabeledExprSyntax) -> SyntaxVisitorContinueKind { let labelText = node.label?.text - switch labelText { - case "printingAccessor": - setACL(to: \.printingAccessor, with: node) - case "parsingAccessor": - setACL(to: \.parsingAccessor, with: node) - default: - break + do { + switch labelText { + case "printingAccessor": + try setACL(to: \.printingAccessor, with: node) + case "parsingAccessor": + try setACL(to: \.parsingAccessor, with: node) + case "bitEndian": + try setBitEndian(with: node) + default: + break + } + } catch { + context.diagnose(.init(node: node, message: error)) } + return .skipChildren } + private func setBitEndian(with node: LabeledExprSyntax) throws(MacroConfigurationError) { + let expression = node.expression + guard let memberAccessSyntax = expression.as(MemberAccessExprSyntax.self) else { + throw MacroConfigurationError.invalidBitEndian(expression.description) + } + guard memberAccessSyntax.base == nil else { + throw MacroConfigurationError.invalidBitEndian(expression.description) + } + + let value = memberAccessSyntax.declName.baseName.text + guard value == "big" || value == "little" else { + throw MacroConfigurationError.invalidBitEndian(value) + } + bitEndian = value + } + private func setACL( - to keypath: ReferenceWritableKeyPath, + to keypath: ReferenceWritableKeyPath, with node: LabeledExprSyntax, - ) { + ) throws(MacroConfigurationError) { let acl = parseACL(from: node) self[keyPath: keypath] = acl if case let .unknown(value) = acl { - context.diagnose( - .init( - node: node, - message: MacroAccessorError.invalidAccessor(value), - ), - ) + throw MacroConfigurationError.invalidAccessor(value) } } @@ -101,6 +124,13 @@ class MacroAccessorVisitor: SyntaxVisitor { struct AccessorInfo { let parsingAccessor: DeclModifierSyntax let printingAccessor: DeclModifierSyntax + /// The bit endian value: "big" or "little". + let bitEndian: String + + /// Whether bit parsing should use big endian (MSB-first). + var isBigEndian: Bool { + bitEndian == "big" + } } private let allAccessModifiers: Set = [ @@ -112,30 +142,30 @@ private let allAccessModifiers: Set = [ ] private let defaultAccessModifier: TokenKind = .keyword(.internal) -func extractAccessor( +func extractMacroConfiguration( from attributeNode: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, in context: some MacroExpansionContext, -) throws(MacroAccessorError) -> AccessorInfo { +) throws(MacroConfigurationError) -> AccessorInfo { let accessModifiers = declaration.modifiers .filter { modifier in allAccessModifiers.contains(modifier.name.tokenKind) } guard accessModifiers.count < 2 else { - throw MacroAccessorError + throw MacroConfigurationError .moreThanOneModifier(modifiers: accessModifiers.map(\.name.text).joined(separator: ", ")) } let modifierToken = accessModifiers.first?.name.tokenKind ?? defaultAccessModifier - let accessorVisitor = MacroAccessorVisitor(context: context) + let accessorVisitor = MacroConfigurationVisitor(context: context) accessorVisitor.walk(attributeNode) guard let parsingAccessor = accessorVisitor.parsingAccessor.getAccessorToken(defaultAccessor: modifierToken), let printingAccessor = accessorVisitor.printingAccessor.getAccessorToken(defaultAccessor: modifierToken) else { - throw MacroAccessorError.unknownAccessor + throw MacroConfigurationError.unknownAccessor } return .init( @@ -145,5 +175,6 @@ func extractAccessor( printingAccessor: .init( name: TokenSyntax(printingAccessor, presence: .present), ), + bitEndian: accessorVisitor.bitEndian, ) } diff --git a/Sources/BinaryParseKitMacros/Macros/Supports/Utilities.swift b/Sources/BinaryParseKitMacros/Macros/Supports/Utilities.swift index 8ad2a57..6915e56 100644 --- a/Sources/BinaryParseKitMacros/Macros/Supports/Utilities.swift +++ b/Sources/BinaryParseKitMacros/Macros/Supports/Utilities.swift @@ -153,18 +153,21 @@ func generatePrintableFields(_ infos: [PrintableFieldInfo]) -> ArrayElementListS /// Generates code to parse a group of consecutive @mask fields for structs func generateMaskGroupBlock( maskActions: [ParseActionGroup.MaskGroupAction], + bitEndian: String, context: some MacroExpansionContext, ) throws -> CodeBlockItemListSyntax { guard !maskActions.isEmpty else { return CodeBlockItemListSyntax {} } + let isBigEndian = bitEndian == "big" + let slicingMethod = isBigEndian ? "first" : "last" + // Calculate total bits needed // For each field, we use either the explicit bitCount or the type's bitCount let bitsVarName = context.makeUniqueName("__bitmask_totalBits") let bytesVarName = context.makeUniqueName("__bitmask_byteCount") let spanVarName = context.makeUniqueName("__bitmask_span") - let offsetVarName = context.makeUniqueName("__bitmask_offset") // Calculate total bits let bitCountExprs = maskActions.map { maskAction in @@ -190,15 +193,19 @@ func generateMaskGroupBlock( "let \(bytesVarName) = (\(bitsVarName) + 7) / 8" // Get a sliced span for bitmask bytes - "let \(spanVarName) = try span.sliceSpan(byteCount: \(bytesVarName))" - - // Track offset for each field - "var \(offsetVarName) = 0" + // Big endian: start from bit 0 (MSB-first) + // Little endian: start from end of padding bits (LSB-first) + if isBigEndian { + "var \(spanVarName) = try RawBitsSpan(span.sliceSpan(byteCount: \(bytesVarName)).bytes, bitOffset: 0, bitCount: \(bitsVarName))" + } else { + "var \(spanVarName) = try RawBitsSpan(span.sliceSpan(byteCount: \(bytesVarName)).bytes, bitOffset: \(bytesVarName) * 8 - \(bitsVarName), bitCount: \(bitsVarName))" + } // Parse each field for action in maskActions { let variableName = action.variableName let fieldType = action.variableType + let subSpan = context.makeUniqueName("__subSpan") switch action.maskInfo.bitCount { case let .specified(count): @@ -209,14 +216,16 @@ func generateMaskGroupBlock( \(raw: Constants.UtilityFunctions.assertExpressibleByRawBits)((\(fieldType)).self) """ """ - self.\(variableName) = try \(raw: Constants.UtilityFunctions.maskParsingFromSpan)( - from: \(spanVarName), - fieldType: (\(fieldType)).self, + let \(subSpan) = \(spanVarName).__slicing(unchecked: (), \(raw: slicingMethod): \(countExpr)) + """ + """ + self.\(variableName) = try \(raw: Constants.UtilityFunctions.createFromBits)( + (\(fieldType)).self, + fieldBits: \(subSpan), fieldRequestedBitCount: \(countExpr), - at: \(offsetVarName), + bitEndian: .\(raw: bitEndian), ) """ - "\(offsetVarName) += \(countExpr)" case .inferred: // Assert BitmaskParsable for inferred bit count let countExpr: ExprSyntax = "(\(fieldType.trimmed)).bitCount" @@ -225,14 +234,16 @@ func generateMaskGroupBlock( \(raw: Constants.UtilityFunctions.assertBitmaskParsable)((\(fieldType)).self) """ """ - self.\(variableName) = try \(raw: Constants.UtilityFunctions.maskParsingFromSpan)( - from: \(spanVarName), - fieldType: (\(fieldType)).self, + let \(subSpan) = \(spanVarName).__slicing(unchecked: (), \(raw: slicingMethod): \(countExpr)) + """ + """ + self.\(variableName) = try \(raw: Constants.UtilityFunctions.createFromBits)( + (\(fieldType)).self, + fieldBits: \(subSpan), fieldRequestedBitCount: \(countExpr), - at: \(offsetVarName), + bitEndian: .\(raw: bitEndian), ) """ - "\(offsetVarName) += \(countExpr)" } } } @@ -242,17 +253,20 @@ func generateMaskGroupBlock( func generateEnumMaskGroupBlock( maskActions: [ParseActionGroup.MaskGroupAction], caseElementName: TokenSyntax, + bitEndian: String, context: some MacroExpansionContext, ) throws -> CodeBlockItemListSyntax { guard !maskActions.isEmpty else { return CodeBlockItemListSyntax {} } + let isBigEndian = bitEndian == "big" + let slicingMethod = isBigEndian ? "first" : "last" + // Calculate total bits needed let bitsVarName = context.makeUniqueName("__bitmask_totalBits") let bytesVarName = context.makeUniqueName("__bitmask_byteCount") let spanVarName = context.makeUniqueName("__bitmask_span") - let offsetVarName = context.makeUniqueName("__bitmask_offset") // Calculate total bits let bitCountExprs = maskActions.map { maskAction in @@ -279,15 +293,19 @@ func generateEnumMaskGroupBlock( "let \(bytesVarName) = (\(bitsVarName) + 7) / 8" // Get a sliced span for bitmask bytes - "let \(spanVarName) = try span.sliceSpan(byteCount: \(bytesVarName))" - - // Track offset for each field - "var \(offsetVarName) = 0" + // Big endian: start from bit 0 (MSB-first) + // Little endian: start from end of padding bits (LSB-first) + if isBigEndian { + "var \(spanVarName) = try RawBitsSpan(span.sliceSpan(byteCount: \(bytesVarName)).bytes, bitOffset: 0, bitCount: \(bitsVarName))" + } else { + "var \(spanVarName) = try RawBitsSpan(span.sliceSpan(byteCount: \(bytesVarName)).bytes, bitOffset: \(bytesVarName) * 8 - \(bitsVarName), bitCount: \(bitsVarName))" + } // Parse each field for maskAction in maskActions { let variableName = maskAction.variableName let fieldType = maskAction.variableType + let subSpan = context.makeUniqueName("__subSpan") switch maskAction.maskInfo.bitCount { case let .specified(count): @@ -298,14 +316,16 @@ func generateEnumMaskGroupBlock( \(raw: Constants.UtilityFunctions.assertExpressibleByRawBits)((\(fieldType)).self) """ """ - let \(variableName) = try \(raw: Constants.UtilityFunctions.maskParsingFromSpan)( - from: \(spanVarName), - fieldType: (\(fieldType)).self, + let \(subSpan) = \(spanVarName).__slicing(unchecked: (), \(raw: slicingMethod): \(countExpr)) + """ + """ + let \(variableName) = try \(raw: Constants.UtilityFunctions.createFromBits)( + (\(fieldType)).self, + fieldBits: \(subSpan), fieldRequestedBitCount: \(countExpr), - at: \(offsetVarName), + bitEndian: .\(raw: bitEndian), ) """ - "\(offsetVarName) += \(count.expr)" case .inferred: // Assert BitmaskParsable for inferred bit count let countExpr: ExprSyntax = "(\(fieldType.trimmed)).bitCount" @@ -314,14 +334,16 @@ func generateEnumMaskGroupBlock( \(raw: Constants.UtilityFunctions.assertBitmaskParsable)((\(fieldType)).self) """ """ - let \(variableName) = try \(raw: Constants.UtilityFunctions.maskParsingFromSpan)( - from: \(spanVarName), - fieldType: (\(fieldType)).self, + let \(subSpan) = \(spanVarName).__slicing(unchecked: (), \(raw: slicingMethod): \(countExpr)) + """ + """ + let \(variableName) = try \(raw: Constants.UtilityFunctions.createFromBits)( + (\(fieldType)).self, + fieldBits: \(subSpan), fieldRequestedBitCount: \(countExpr), - at: \(offsetVarName), + bitEndian: .\(raw: bitEndian), ) """ - "\(offsetVarName) += \(countExpr)" } } } diff --git a/Tests/BinaryParseKitMacroTests/BinaryParseKitBitmaskTests.swift b/Tests/BinaryParseKitMacroTests/BinaryParseKitBitmaskTests.swift index ff2f02d..db7c32d 100644 --- a/Tests/BinaryParseKitMacroTests/BinaryParseKitBitmaskTests.swift +++ b/Tests/BinaryParseKitMacroTests/BinaryParseKitBitmaskTests.swift @@ -39,46 +39,43 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { 1 + 3 + (Bool).bitCount } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `flag1` of type `Bool` with specified bit count 1 BinaryParseKit.__assertExpressibleByRawBits((Bool).self) do { - let fieldBitCount = 1 - self.flag1 = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Bool).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = 1 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } // Parse `value` of type `UInt8` with specified bit count 3 BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) do { - let fieldBitCount = 3 - self.value = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (UInt8).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu0_ = 3 + let __macro_local_9__subSpanfMu0_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu0_) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu0_, + bitEndian: .big, ) - bitPosition += fieldBitCount } // Parse `flag2` of type `Bool` with inferred bit count BinaryParseKit.__assertBitmaskParsable((Bool).self) do { - let fieldBitCount = (Bool).bitCount - self.flag2 = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Bool).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu1_ = (Bool).bitCount + let __macro_local_9__subSpanfMu1_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu1_) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu1_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -130,6 +127,29 @@ extension BinaryParseKitMacroTests { } } + @Test + func `multiple enum case in one mask decl`() { + assertMacro { + """ + @ParseBitmask + struct Flags { + @mask(bitCount: 1) + var flag1: Bool, flag2: Bool + } + """ + } diagnostics: { + """ + @ParseBitmask + struct Flags { + @mask(bitCount: 1) + ┬───────────────── + ╰─ 🛑 peer macro can only be applied to a single variable + var flag1: Bool, flag2: Bool + } + """ + } + } + @Test func `field without mask attribute`() { assertMacro { @@ -198,20 +218,19 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { 1 } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `flag` of type `Bool` with specified bit count 1 BinaryParseKit.__assertExpressibleByRawBits((Bool).self) do { - let fieldBitCount = 1 - self.flag = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Bool).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = 1 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.flag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -263,20 +282,19 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { 4 } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `value` of type `UInt8` with specified bit count 4 BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) do { - let fieldBitCount = 4 - self.value = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (UInt8).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = 4 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -330,20 +348,19 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { 8 } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `rawValue` of type `UInt8` with specified bit count 8 BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) do { - let fieldBitCount = 8 - self.rawValue = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (UInt8).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = 8 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.rawValue = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -390,20 +407,19 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { 8 } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `value` of type `UInt8` with specified bit count 8 BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) do { - let fieldBitCount = 8 - self.value = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (UInt8).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = 8 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -451,33 +467,31 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { (Bool).bitCount + (Bool).bitCount } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `flag1` of type `Bool` with inferred bit count BinaryParseKit.__assertBitmaskParsable((Bool).self) do { - let fieldBitCount = (Bool).bitCount - self.flag1 = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Bool).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = (Bool).bitCount + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } // Parse `flag2` of type `Bool` with inferred bit count BinaryParseKit.__assertBitmaskParsable((Bool).self) do { - let fieldBitCount = (Bool).bitCount - self.flag2 = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Bool).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu0_ = (Bool).bitCount + let __macro_local_9__subSpanfMu0_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu0_) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu0_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -525,20 +539,19 @@ extension BinaryParseKitMacroTests { public static var bitCount: Int { 1 } - public init(bits: RawBitsInteger) throws { - var bitPosition = 0 + public init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `flag` of type `Bool` with specified bit count 1 BinaryParseKit.__assertExpressibleByRawBits((Bool).self) do { - let fieldBitCount = 1 - self.flag = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Bool).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = 1 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.flag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -731,20 +744,19 @@ extension BinaryParseKitMacroTests { internal static var bitCount: Int { (Flag).bitCount } - internal init(bits: RawBitsInteger) throws { - var bitPosition = 0 + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) // Parse `a` of type `Flag` with inferred bit count BinaryParseKit.__assertBitmaskParsable((Flag).self) do { - let fieldBitCount = (Flag).bitCount - self.a = try BinaryParseKit.__maskParsing( - from: bits, - parentType: Self.self, - fieldType: (Flag).self, - fieldRequestedBitCount: fieldBitCount, - at: bitPosition + let __macro_local_10__bitCountfMu_ = (Flag).bitCount + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.a = try BinaryParseKit.__createFromBits( + (Flag).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, ) - bitPosition += fieldBitCount } } } @@ -768,5 +780,226 @@ extension BinaryParseKitMacroTests { """ } } + + @Test + func `little endian bit parsing`() { + assertMacro { + """ + @ParseBitmask(bitEndian: .little) + struct LittleEndianFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } expansion: { + """ + struct LittleEndianFlags { + var flag1: Bool + var value: UInt8 + var flag2: Bool + } + + extension LittleEndianFlags: BinaryParseKit.ExpressibleByRawBits, BinaryParseKit.BitCountProviding { + internal static var bitCount: Int { + 1 + 3 + (Bool).bitCount + } + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) + // Parse `flag1` of type `Bool` with specified bit count 1 + BinaryParseKit.__assertExpressibleByRawBits((Bool).self) + do { + let __macro_local_10__bitCountfMu_ = 1 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), last: __macro_local_10__bitCountfMu_) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .little, + ) + } + // Parse `value` of type `UInt8` with specified bit count 3 + BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) + do { + let __macro_local_10__bitCountfMu0_ = 3 + let __macro_local_9__subSpanfMu0_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), last: __macro_local_10__bitCountfMu0_) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu0_, + bitEndian: .little, + ) + } + // Parse `flag2` of type `Bool` with inferred bit count + BinaryParseKit.__assertBitmaskParsable((Bool).self) + do { + let __macro_local_10__bitCountfMu1_ = (Bool).bitCount + let __macro_local_9__subSpanfMu1_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), last: __macro_local_10__bitCountfMu1_) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu1_, + bitEndian: .little, + ) + } + } + } + + extension LittleEndianFlags: BinaryParseKit.RawBitsConvertible { + internal func toRawBits(bitCount: Int) throws -> BinaryParseKit.RawBits { + var result = BinaryParseKit.RawBits() + // Convert `flag1` of type `Bool` with specified bit count 1 + result = result.appending(try BinaryParseKit.__toRawBits(self.flag1, bitCount: 1)) + // Convert `value` of type `UInt8` with specified bit count 3 + result = result.appending(try BinaryParseKit.__toRawBits(self.value, bitCount: 3)) + // Convert `flag2` of type `Bool` with inferred bit count + BinaryParseKit.__assertRawBitsConvertible((Bool).self) + result = result.appending(try BinaryParseKit.__toRawBits(self.flag2, bitCount: (Bool).bitCount)) + return result + } + } + + extension LittleEndianFlags: BinaryParseKit.Printable { + internal func printerIntel() throws -> PrinterIntel { + let bits = try self.toRawBits(bitCount: Self.bitCount) + return .bitmask(.init(bits: bits)) + } + } + """ + } + } + + @Test + func `explicit big endian bit parsing`() { + assertMacro { + """ + @ParseBitmask(bitEndian: .big) + struct LittleEndianFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } expansion: { + """ + struct LittleEndianFlags { + var flag1: Bool + var value: UInt8 + var flag2: Bool + } + + extension LittleEndianFlags: BinaryParseKit.ExpressibleByRawBits, BinaryParseKit.BitCountProviding { + internal static var bitCount: Int { + 1 + 3 + (Bool).bitCount + } + internal init(bits: borrowing BinaryParseKit.RawBitsSpan) throws { + var __macro_local_10__bitsSpanfMu_ = RawBitsSpan(copying: bits) + // Parse `flag1` of type `Bool` with specified bit count 1 + BinaryParseKit.__assertExpressibleByRawBits((Bool).self) + do { + let __macro_local_10__bitCountfMu_ = 1 + let __macro_local_9__subSpanfMu_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu_) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu_, + bitEndian: .big, + ) + } + // Parse `value` of type `UInt8` with specified bit count 3 + BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) + do { + let __macro_local_10__bitCountfMu0_ = 3 + let __macro_local_9__subSpanfMu0_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu0_) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu0_, + bitEndian: .big, + ) + } + // Parse `flag2` of type `Bool` with inferred bit count + BinaryParseKit.__assertBitmaskParsable((Bool).self) + do { + let __macro_local_10__bitCountfMu1_ = (Bool).bitCount + let __macro_local_9__subSpanfMu1_ = __macro_local_10__bitsSpanfMu_.__slicing(unchecked: (), first: __macro_local_10__bitCountfMu1_) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, + fieldRequestedBitCount: __macro_local_10__bitCountfMu1_, + bitEndian: .big, + ) + } + } + } + + extension LittleEndianFlags: BinaryParseKit.RawBitsConvertible { + internal func toRawBits(bitCount: Int) throws -> BinaryParseKit.RawBits { + var result = BinaryParseKit.RawBits() + // Convert `flag1` of type `Bool` with specified bit count 1 + result = result.appending(try BinaryParseKit.__toRawBits(self.flag1, bitCount: 1)) + // Convert `value` of type `UInt8` with specified bit count 3 + result = result.appending(try BinaryParseKit.__toRawBits(self.value, bitCount: 3)) + // Convert `flag2` of type `Bool` with inferred bit count + BinaryParseKit.__assertRawBitsConvertible((Bool).self) + result = result.appending(try BinaryParseKit.__toRawBits(self.flag2, bitCount: (Bool).bitCount)) + return result + } + } + + extension LittleEndianFlags: BinaryParseKit.Printable { + internal func printerIntel() throws -> PrinterIntel { + let bits = try self.toRawBits(bitCount: Self.bitCount) + return .bitmask(.init(bits: bits)) + } + } + """ + } + } + + @Test + func `non .big/.little as bitEndian should fail`() { + assertMacro { + """ + @ParseBitmask(bitEndian: someVariable) + struct LittleEndianFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } diagnostics: { + """ + @ParseBitmask(bitEndian: someVariable) + ┬────────────────────── + ╰─ 🛑 Invalid bitEndian value: someVariable; Please use .big or .little. + struct LittleEndianFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } + } } } diff --git a/Tests/BinaryParseKitMacroTests/BinaryParseKitEnumTests.swift b/Tests/BinaryParseKitMacroTests/BinaryParseKitEnumTests.swift index 06d76d7..c08fd96 100644 --- a/Tests/BinaryParseKitMacroTests/BinaryParseKitEnumTests.swift +++ b/Tests/BinaryParseKitMacroTests/BinaryParseKitEnumTests.swift @@ -1155,26 +1155,25 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields for `flags` let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 7 let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `__macro_local_15__mask_0th_arg_fMu_` of type Bool from bits BinaryParseKit.__assertExpressibleByRawBits((Bool).self) - let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: 1, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 1 // Parse `__macro_local_15__mask_1th_arg_fMu_` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 7) + let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, fieldRequestedBitCount: 7, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 7 // construct `flags` with above associated values self = .flags(__macro_local_15__mask_0th_arg_fMu_, __macro_local_15__mask_1th_arg_fMu_) return @@ -1186,35 +1185,34 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields for `mixed` let __macro_local_19__bitmask_totalBitsfMu0_ = 4 + (Bool).bitCount + (Bool).bitCount let __macro_local_19__bitmask_byteCountfMu0_ = (__macro_local_19__bitmask_totalBitsfMu0_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu0_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_) - var __macro_local_16__bitmask_offsetfMu0_ = 0 + var __macro_local_14__bitmask_spanfMu0_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu0_) // Parse `value` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - let value = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu1_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: 4) + let value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu1_, fieldRequestedBitCount: 4, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += 4 // Parse `firstFlag` of type Bool from bits BinaryParseKit.__assertBitmaskParsable((Bool).self) - let firstFlag = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu2_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: (Bool).bitCount) + let firstFlag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu2_, fieldRequestedBitCount: (Bool).bitCount, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += (Bool).bitCount // Parse `secondFlag` of type Bool from bits BinaryParseKit.__assertBitmaskParsable((Bool).self) - let secondFlag = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu3_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: (Bool).bitCount) + let secondFlag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu3_, fieldRequestedBitCount: (Bool).bitCount, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += (Bool).bitCount // construct `mixed` with above associated values self = .mixed(__macro_local_16__parse_0th_arg_fMu_, value: value, firstFlag: firstFlag, secondFlag: secondFlag) return @@ -1283,59 +1281,56 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields for `flags` let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 2 let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `__macro_local_15__mask_0th_arg_fMu_` of type Bool from bits BinaryParseKit.__assertExpressibleByRawBits((Bool).self) - let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: 1, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 1 // Parse `__macro_local_15__mask_1th_arg_fMu_` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 2) + let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, fieldRequestedBitCount: 2, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 2 // Parse `__macro_local_16__parse_2th_arg_fMu_` of type UInt8 BinaryParseKit.__assertParsable((UInt8).self) let __macro_local_16__parse_2th_arg_fMu_ = try UInt8(parsing: &span) // Parse bitmask fields for `flags` let __macro_local_19__bitmask_totalBitsfMu0_ = 7 let __macro_local_19__bitmask_byteCountfMu0_ = (__macro_local_19__bitmask_totalBitsfMu0_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu0_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_) - var __macro_local_16__bitmask_offsetfMu0_ = 0 + var __macro_local_14__bitmask_spanfMu0_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu0_) // Parse `__macro_local_15__mask_3th_arg_fMu_` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - let __macro_local_15__mask_3th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu1_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: 7) + let __macro_local_15__mask_3th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu1_, fieldRequestedBitCount: 7, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += 7 // Skip 2 because of "skip", before parsing `flags` try span.seek(toRelativeOffset: 2) // Parse bitmask fields for `flags` let __macro_local_19__bitmask_totalBitsfMu1_ = 4 let __macro_local_19__bitmask_byteCountfMu1_ = (__macro_local_19__bitmask_totalBitsfMu1_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu1_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu1_) - var __macro_local_16__bitmask_offsetfMu1_ = 0 + var __macro_local_14__bitmask_spanfMu1_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu1_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu1_) // Parse `__macro_local_15__mask_4th_arg_fMu_` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - let __macro_local_15__mask_4th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu1_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu2_ = __macro_local_14__bitmask_spanfMu1_.__slicing(unchecked: (), first: 4) + let __macro_local_15__mask_4th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu2_, fieldRequestedBitCount: 4, - at: __macro_local_16__bitmask_offsetfMu1_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu1_ += 4 // construct `flags` with above associated values self = .flags(__macro_local_15__mask_0th_arg_fMu_, __macro_local_15__mask_1th_arg_fMu_, __macro_local_16__parse_2th_arg_fMu_, __macro_local_15__mask_3th_arg_fMu_, __macro_local_15__mask_4th_arg_fMu_) return @@ -1524,17 +1519,16 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields for `a` let __macro_local_19__bitmask_totalBitsfMu_ = (Flag).bitCount let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `__macro_local_15__mask_0th_arg_fMu_` of type Flag from bits BinaryParseKit.__assertBitmaskParsable((Flag).self) - let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Flag).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: (Flag).bitCount) + let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (Flag).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: (Flag).bitCount, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += (Flag).bitCount // construct `a` with above associated values self = .a(__macro_local_15__mask_0th_arg_fMu_) return @@ -1548,17 +1542,16 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields for `b` let __macro_local_19__bitmask_totalBitsfMu0_ = (Flag).bitCount let __macro_local_19__bitmask_byteCountfMu0_ = (__macro_local_19__bitmask_totalBitsfMu0_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu0_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_) - var __macro_local_16__bitmask_offsetfMu0_ = 0 + var __macro_local_14__bitmask_spanfMu0_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu0_) // Parse `__macro_local_15__mask_1th_arg_fMu_` of type Flag from bits BinaryParseKit.__assertBitmaskParsable((Flag).self) - let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (Flag).self, + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: (Flag).bitCount) + let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (Flag).self, + fieldBits: __macro_local_9__subSpanfMu0_, fieldRequestedBitCount: (Flag).bitCount, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += (Flag).bitCount // construct `b` with above associated values self = .b(__macro_local_16__parse_0th_arg_fMu_, __macro_local_15__mask_1th_arg_fMu_) return @@ -1598,6 +1591,177 @@ extension BinaryParseKitMacroTests { """# } } + + @Test + func `little endian enum with mask associated values`() { + assertMacro { + """ + @ParseEnum(bitEndian: .little) + enum LittleEndianTestEnum { + @match(byte: 0x01) + @mask(bitCount: 1) + @mask(bitCount: 7) + case flags(Bool, UInt8) + } + """ + } expansion: { + #""" + enum LittleEndianTestEnum { + case flags(Bool, UInt8) + } + + extension LittleEndianTestEnum: BinaryParseKit.Parsable { + internal init(parsing span: inout BinaryParsing.ParserSpan) throws(BinaryParsing.ThrownParsingError) { + if BinaryParseKit.__match([0x01], in: span) { + // Parse bitmask fields for `flags` + let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 7 + let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: __macro_local_19__bitmask_byteCountfMu_ * 8 - __macro_local_19__bitmask_totalBitsfMu_, bitCount: __macro_local_19__bitmask_totalBitsfMu_) + // Parse `__macro_local_15__mask_0th_arg_fMu_` of type Bool from bits + BinaryParseKit.__assertExpressibleByRawBits((Bool).self) + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), last: 1) + let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: 1, + bitEndian: .little, + ) + // Parse `__macro_local_15__mask_1th_arg_fMu_` of type UInt8 from bits + BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), last: 7) + let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: 7, + bitEndian: .little, + ) + // construct `flags` with above associated values + self = .flags(__macro_local_15__mask_0th_arg_fMu_, __macro_local_15__mask_1th_arg_fMu_) + return + } + throw BinaryParseKit.BinaryParserKitError.failedToParse("Failed to find a match for LittleEndianTestEnum, at \(span.startPosition)") + } + } + + extension LittleEndianTestEnum: BinaryParseKit.Printable { + internal func printerIntel() throws -> PrinterIntel { + switch self { + case let .flags(__macro_local_15__mask_0th_arg_fMu0_, __macro_local_15__mask_1th_arg_fMu0_): + let __macro_local_22__bytesTakenInMatchingfMu_: [UInt8] = [0x01] + // bits from __macro_local_15__mask_0th_arg_fMu0_, __macro_local_15__mask_1th_arg_fMu0_ + let __macro_local_10__maskBitsfMu_ = try BinaryParseKit.__toRawBits(__macro_local_15__mask_0th_arg_fMu0_, bitCount: 1).appending(BinaryParseKit.__toRawBits(__macro_local_15__mask_1th_arg_fMu0_, bitCount: 7)) + return .enum( + .init( + bytes: __macro_local_22__bytesTakenInMatchingfMu_, + parseType: .match, + fields: [.init(byteCount: nil, endianness: nil, intel: .bitmask(.init(bits: __macro_local_10__maskBitsfMu_)))], + ) + ) + } + } + } + """# + } + } + + @Test + func `explicit big endian enum with mask associated values`() { + assertMacro { + """ + @ParseEnum(bitEndian: .big) + enum LittleEndianTestEnum { + @match(byte: 0x01) + @mask(bitCount: 1) + @mask(bitCount: 7) + case flags(Bool, UInt8) + } + """ + } expansion: { + #""" + enum LittleEndianTestEnum { + case flags(Bool, UInt8) + } + + extension LittleEndianTestEnum: BinaryParseKit.Parsable { + internal init(parsing span: inout BinaryParsing.ParserSpan) throws(BinaryParsing.ThrownParsingError) { + if BinaryParseKit.__match([0x01], in: span) { + // Parse bitmask fields for `flags` + let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 7 + let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) + // Parse `__macro_local_15__mask_0th_arg_fMu_` of type Bool from bits + BinaryParseKit.__assertExpressibleByRawBits((Bool).self) + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + let __macro_local_15__mask_0th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: 1, + bitEndian: .big, + ) + // Parse `__macro_local_15__mask_1th_arg_fMu_` of type UInt8 from bits + BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 7) + let __macro_local_15__mask_1th_arg_fMu_ = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: 7, + bitEndian: .big, + ) + // construct `flags` with above associated values + self = .flags(__macro_local_15__mask_0th_arg_fMu_, __macro_local_15__mask_1th_arg_fMu_) + return + } + throw BinaryParseKit.BinaryParserKitError.failedToParse("Failed to find a match for LittleEndianTestEnum, at \(span.startPosition)") + } + } + + extension LittleEndianTestEnum: BinaryParseKit.Printable { + internal func printerIntel() throws -> PrinterIntel { + switch self { + case let .flags(__macro_local_15__mask_0th_arg_fMu0_, __macro_local_15__mask_1th_arg_fMu0_): + let __macro_local_22__bytesTakenInMatchingfMu_: [UInt8] = [0x01] + // bits from __macro_local_15__mask_0th_arg_fMu0_, __macro_local_15__mask_1th_arg_fMu0_ + let __macro_local_10__maskBitsfMu_ = try BinaryParseKit.__toRawBits(__macro_local_15__mask_0th_arg_fMu0_, bitCount: 1).appending(BinaryParseKit.__toRawBits(__macro_local_15__mask_1th_arg_fMu0_, bitCount: 7)) + return .enum( + .init( + bytes: __macro_local_22__bytesTakenInMatchingfMu_, + parseType: .match, + fields: [.init(byteCount: nil, endianness: nil, intel: .bitmask(.init(bits: __macro_local_10__maskBitsfMu_)))], + ) + ) + } + } + } + """# + } + } + + @Test + func `non .big/.little as bitEndian should fail`() { + assertMacro { + """ + @ParseEnum(bitEndian: Value.someVariable) + enum LittleEndianTestEnum { + @match(byte: 0x01) + @mask(bitCount: 1) + @mask(bitCount: 7) + case flags(Bool, UInt8) + } + """ + } diagnostics: { + """ + @ParseEnum(bitEndian: Value.someVariable) + ┬──────────────────────────── + ╰─ 🛑 Invalid bitEndian value: Value.someVariable; Please use .big or .little. + enum LittleEndianTestEnum { + @match(byte: 0x01) + @mask(bitCount: 1) + @mask(bitCount: 7) + case flags(Bool, UInt8) + } + """ + } + } } } diff --git a/Tests/BinaryParseKitMacroTests/BinaryParseKitStructTests.swift b/Tests/BinaryParseKitMacroTests/BinaryParseKitStructTests.swift index 36b366a..5be6419 100644 --- a/Tests/BinaryParseKitMacroTests/BinaryParseKitStructTests.swift +++ b/Tests/BinaryParseKitMacroTests/BinaryParseKitStructTests.swift @@ -816,35 +816,34 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 3 + (Bool).bitCount let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `flag1` of type Bool from bits BinaryParseKit.__assertExpressibleByRawBits((Bool).self) - self.flag1 = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: 1, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 1 // Parse `value` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - self.value = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 3) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, fieldRequestedBitCount: 3, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 3 // Parse `flag2` of type Bool from bits BinaryParseKit.__assertBitmaskParsable((Bool).self) - self.flag2 = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu1_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: (Bool).bitCount) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, fieldRequestedBitCount: (Bool).bitCount, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += (Bool).bitCount } } @@ -899,26 +898,25 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 7 let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `flag` of type Bool from bits BinaryParseKit.__assertExpressibleByRawBits((Bool).self) - self.flag = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + self.flag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: 1, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 1 // Parse `data` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - self.data = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 7) + self.data = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, fieldRequestedBitCount: 7, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 7 // Parse `footer` of type UInt16 BinaryParseKit.__assertParsable((UInt16).self) self.footer = try UInt16(parsing: &span) @@ -992,61 +990,59 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 4 let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `topFlag` of type Bool from bits BinaryParseKit.__assertExpressibleByRawBits((Bool).self) - self.topFlag = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + self.topFlag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: 1, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 1 // Parse `topData` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - self.topData = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 4) + self.topData = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, fieldRequestedBitCount: 4, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += 4 // Parse `divider` of type UInt16 BinaryParseKit.__assertParsable((UInt16).self) self.divider = try UInt16(parsing: &span) // Parse bitmask fields let __macro_local_19__bitmask_totalBitsfMu0_ = 1 + 4 + 2 let __macro_local_19__bitmask_byteCountfMu0_ = (__macro_local_19__bitmask_totalBitsfMu0_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu0_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_) - var __macro_local_16__bitmask_offsetfMu0_ = 0 + var __macro_local_14__bitmask_spanfMu0_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu0_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu0_) // Parse `bottomFlag` of type Bool from bits BinaryParseKit.__assertExpressibleByRawBits((Bool).self) - self.bottomFlag = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (Bool).self, + let __macro_local_9__subSpanfMu1_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: 1) + self.bottomFlag = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, fieldRequestedBitCount: 1, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += 1 // Parse `bottomData` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - self.bottomData = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu2_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: 4) + self.bottomData = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu2_, fieldRequestedBitCount: 4, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += 4 // Parse `bottomAdditionalData` of type UInt8 from bits BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) - self.bottomAdditionalData = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu0_, - fieldType: (UInt8).self, + let __macro_local_9__subSpanfMu3_ = __macro_local_14__bitmask_spanfMu0_.__slicing(unchecked: (), first: 2) + self.bottomAdditionalData = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu3_, fieldRequestedBitCount: 2, - at: __macro_local_16__bitmask_offsetfMu0_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu0_ += 2 // Parse `footer` of type UInt16 BinaryParseKit.__assertParsable((UInt16).self) self.footer = try UInt16(parsing: &span) @@ -1218,17 +1214,16 @@ extension BinaryParseKitMacroTests { // Parse bitmask fields let __macro_local_19__bitmask_totalBitsfMu_ = (Flag).bitCount let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 - let __macro_local_14__bitmask_spanfMu_ = try span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_) - var __macro_local_16__bitmask_offsetfMu_ = 0 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) // Parse `a` of type Flag from bits BinaryParseKit.__assertBitmaskParsable((Flag).self) - self.a = try BinaryParseKit.__maskParsing( - from: __macro_local_14__bitmask_spanfMu_, - fieldType: (Flag).self, + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: (Flag).bitCount) + self.a = try BinaryParseKit.__createFromBits( + (Flag).self, + fieldBits: __macro_local_9__subSpanfMu_, fieldRequestedBitCount: (Flag).bitCount, - at: __macro_local_16__bitmask_offsetfMu_, + bitEndian: .big, ) - __macro_local_16__bitmask_offsetfMu_ += (Flag).bitCount // Parse `b` of type Flag BinaryParseKit.__assertParsable((Flag).self) self.b = try Flag(parsing: &span) @@ -1249,6 +1244,191 @@ extension BinaryParseKitMacroTests { """ } } + + @Test + func `little endian mask fields in struct`() { + assertMacro { + """ + @ParseStruct(bitEndian: .little) + struct LittleEndianBitFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } expansion: { + """ + struct LittleEndianBitFlags { + var flag1: Bool + var value: UInt8 + var flag2: Bool + } + + extension LittleEndianBitFlags: BinaryParseKit.Parsable { + internal init(parsing span: inout BinaryParsing.ParserSpan) throws(BinaryParsing.ThrownParsingError) { + // Parse bitmask fields + let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 3 + (Bool).bitCount + let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: __macro_local_19__bitmask_byteCountfMu_ * 8 - __macro_local_19__bitmask_totalBitsfMu_, bitCount: __macro_local_19__bitmask_totalBitsfMu_) + // Parse `flag1` of type Bool from bits + BinaryParseKit.__assertExpressibleByRawBits((Bool).self) + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), last: 1) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: 1, + bitEndian: .little, + ) + // Parse `value` of type UInt8 from bits + BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), last: 3) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: 3, + bitEndian: .little, + ) + // Parse `flag2` of type Bool from bits + BinaryParseKit.__assertBitmaskParsable((Bool).self) + let __macro_local_9__subSpanfMu1_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), last: (Bool).bitCount) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, + fieldRequestedBitCount: (Bool).bitCount, + bitEndian: .little, + ) + } + } + + extension LittleEndianBitFlags: BinaryParseKit.Printable { + internal func printerIntel() throws -> PrinterIntel { + // bits from flag1, value, flag2 + let __macro_local_10__maskBitsfMu_ = try BinaryParseKit.__toRawBits(flag1, bitCount: 1).appending(BinaryParseKit.__toRawBits(value, bitCount: 3)).appending(BinaryParseKit.__toRawBits(flag2, bitCount: (Bool).bitCount)) + return .struct( + .init( + fields: [.init(byteCount: nil, endianness: nil, intel: .bitmask(.init(bits: __macro_local_10__maskBitsfMu_)))] + ) + ) + } + } + """ + } + } + + @Test + func `explicit big endian mask fields in struct`() { + assertMacro { + """ + @ParseStruct(bitEndian: .big) + struct LittleEndianBitFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } expansion: { + """ + struct LittleEndianBitFlags { + var flag1: Bool + var value: UInt8 + var flag2: Bool + } + + extension LittleEndianBitFlags: BinaryParseKit.Parsable { + internal init(parsing span: inout BinaryParsing.ParserSpan) throws(BinaryParsing.ThrownParsingError) { + // Parse bitmask fields + let __macro_local_19__bitmask_totalBitsfMu_ = 1 + 3 + (Bool).bitCount + let __macro_local_19__bitmask_byteCountfMu_ = (__macro_local_19__bitmask_totalBitsfMu_ + 7) / 8 + var __macro_local_14__bitmask_spanfMu_ = try RawBitsSpan(span.sliceSpan(byteCount: __macro_local_19__bitmask_byteCountfMu_).bytes, bitOffset: 0, bitCount: __macro_local_19__bitmask_totalBitsfMu_) + // Parse `flag1` of type Bool from bits + BinaryParseKit.__assertExpressibleByRawBits((Bool).self) + let __macro_local_9__subSpanfMu_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 1) + self.flag1 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu_, + fieldRequestedBitCount: 1, + bitEndian: .big, + ) + // Parse `value` of type UInt8 from bits + BinaryParseKit.__assertExpressibleByRawBits((UInt8).self) + let __macro_local_9__subSpanfMu0_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: 3) + self.value = try BinaryParseKit.__createFromBits( + (UInt8).self, + fieldBits: __macro_local_9__subSpanfMu0_, + fieldRequestedBitCount: 3, + bitEndian: .big, + ) + // Parse `flag2` of type Bool from bits + BinaryParseKit.__assertBitmaskParsable((Bool).self) + let __macro_local_9__subSpanfMu1_ = __macro_local_14__bitmask_spanfMu_.__slicing(unchecked: (), first: (Bool).bitCount) + self.flag2 = try BinaryParseKit.__createFromBits( + (Bool).self, + fieldBits: __macro_local_9__subSpanfMu1_, + fieldRequestedBitCount: (Bool).bitCount, + bitEndian: .big, + ) + } + } + + extension LittleEndianBitFlags: BinaryParseKit.Printable { + internal func printerIntel() throws -> PrinterIntel { + // bits from flag1, value, flag2 + let __macro_local_10__maskBitsfMu_ = try BinaryParseKit.__toRawBits(flag1, bitCount: 1).appending(BinaryParseKit.__toRawBits(value, bitCount: 3)).appending(BinaryParseKit.__toRawBits(flag2, bitCount: (Bool).bitCount)) + return .struct( + .init( + fields: [.init(byteCount: nil, endianness: nil, intel: .bitmask(.init(bits: __macro_local_10__maskBitsfMu_)))] + ) + ) + } + } + """ + } + } + + @Test + func `non .big/.little as bitEndian should fail`() { + assertMacro { + """ + @ParseStruct(bitEndian: Something.big) + struct LittleEndianBitFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } diagnostics: { + """ + @ParseStruct(bitEndian: Something.big) + ┬─────────────────────── + ╰─ 🛑 Invalid bitEndian value: Something.big; Please use .big or .little. + struct LittleEndianBitFlags { + @mask(bitCount: 1) + var flag1: Bool + + @mask(bitCount: 3) + var value: UInt8 + + @mask + var flag2: Bool + } + """ + } + } } } diff --git a/Tests/BinaryParseKitTests/CreateFromBitsTests.swift b/Tests/BinaryParseKitTests/CreateFromBitsTests.swift index 984aad2..7f56c8e 100644 --- a/Tests/BinaryParseKitTests/CreateFromBitsTests.swift +++ b/Tests/BinaryParseKitTests/CreateFromBitsTests.swift @@ -15,55 +15,51 @@ struct CreateFromBitsTests { /// Type with BitCountProviding, requires exactly 6 bits struct Strict6Bit: ExpressibleByRawBits, BitCountProviding, Equatable { - typealias RawBitsInteger = UInt8 static let bitCount = 6 let value: UInt8 - init(bits: UInt8) { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load(as: UInt8.self) } } /// Type with BitCountProviding, requires exactly 4 bits struct Strict4Bit: ExpressibleByRawBits, BitCountProviding, Equatable { - typealias RawBitsInteger = UInt8 static let bitCount = 4 let value: UInt8 - init(bits: UInt8) { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load(as: UInt8.self) } } /// Type with BitCountProviding, requires exactly 1 bit struct Strict1Bit: ExpressibleByRawBits, BitCountProviding, Equatable { - typealias RawBitsInteger = UInt8 static let bitCount = 1 - let value: Bool + let value: UInt8 - init(bits: UInt8) { - value = (bits & 1) == 1 + init(bits: borrowing RawBitsSpan) throws { + let intValue: UInt8 = try bits.load() + value = (intValue & 1) } } /// Type WITHOUT BitCountProviding - should always pass through struct Flexible8Bit: ExpressibleByRawBits, Equatable { - typealias RawBitsInteger = UInt8 let value: UInt8 - init(bits: UInt8) { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load(as: UInt8.self) } } /// Type with BitCountProviding using UInt16 as RawBitsInteger struct Strict12Bit: ExpressibleByRawBits, BitCountProviding, Equatable { - typealias RawBitsInteger = UInt16 static let bitCount = 12 let value: UInt16 - init(bits: UInt16) { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load(as: UInt16.self) } } @@ -72,28 +68,44 @@ struct CreateFromBitsTests { @Test("Throws insufficientBitsAvailable when fieldBitCount < typeBitCount (5 < 6)") func throwsWhenInsufficientBits5vs6() { #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - _ = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0b11111), fieldRequestedBitCount: 5) + let bitsInteger: UInt8 = 0b11111 + _ = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 5) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 5) + } } } @Test("Throws insufficientBitsAvailable when fieldBitCount < typeBitCount (3 < 4)") func throwsWhenInsufficientBits3vs4() { #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - _ = try __createFromBits(Strict4Bit.self, fieldBits: UInt8(0b111), fieldRequestedBitCount: 3) + let bitsInteger: UInt8 = 0b111 + _ = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 3) + return try __createFromBits(Strict4Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 3) + } } } @Test("Throws insufficientBitsAvailable when fieldBitCount < typeBitCount (0 < 1)") func throwsWhenInsufficientBits0vs1() { #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - _ = try __createFromBits(Strict1Bit.self, fieldBits: UInt8(0), fieldRequestedBitCount: 0) + let bitsInteger: UInt8 = 0 + _ = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 0) + return try __createFromBits(Strict1Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 0) + } } } @Test("Throws insufficientBitsAvailable when fieldBitCount < typeBitCount (11 < 12)") func throwsWhenInsufficientBits11vs12() { #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - _ = try __createFromBits(Strict12Bit.self, fieldBits: UInt16(0x7FF), fieldRequestedBitCount: 11) + let bitsInteger: UInt16 = 0x7FF + _ = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 11) + return try __createFromBits(Strict12Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 11) + } } } @@ -101,29 +113,55 @@ struct CreateFromBitsTests { @Test("Exact match: fieldBitCount == typeBitCount (6 == 6)") func exactMatch6Bits() throws { - // fieldBits = 0b101101 = 45 - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0b101101), fieldRequestedBitCount: 6) + // fieldBits = 0b101101 = 45 positioned at MSB of byte + // 0b101101_00 = 0xB4 + let bitsInteger: UInt8 = 0b1011_0100 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 6) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 6) + } #expect(result.value == 0b101101) } @Test("Exact match: fieldBitCount == typeBitCount (4 == 4)") func exactMatch4Bits() throws { - let result = try __createFromBits(Strict4Bit.self, fieldBits: UInt8(0b1010), fieldRequestedBitCount: 4) + // 0b1010 positioned at MSB of byte = 0b1010_0000 = 0xA0 + let bitsInteger: UInt8 = 0b1010_0000 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 4) + return try __createFromBits(Strict4Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 4) + } #expect(result.value == 0b1010) } @Test("Exact match: fieldBitCount == typeBitCount (1 == 1)") func exactMatch1Bit() throws { - let result1 = try __createFromBits(Strict1Bit.self, fieldBits: UInt8(1), fieldRequestedBitCount: 1) - #expect(result1.value == true) + // 1 bit = 1 at MSB = 0b1000_0000 = 0x80 + let bitsInteger1: UInt8 = 0b1000_0000 + let result1 = try bitsInteger1.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 1) + return try __createFromBits(Strict1Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 1) + } + #expect(result1.value == 1) - let result0 = try __createFromBits(Strict1Bit.self, fieldBits: UInt8(0), fieldRequestedBitCount: 1) - #expect(result0.value == false) + // 1 bit = 0 at MSB = 0b0000_0000 = 0x00 + let bitsInteger0: UInt8 = 0b0000_0000 + let result0 = try bitsInteger0.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 1) + return try __createFromBits(Strict1Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 1) + } + #expect(result0.value == 0) } @Test("Exact match: fieldBitCount == typeBitCount (12 == 12)") func exactMatch12Bits() throws { - let result = try __createFromBits(Strict12Bit.self, fieldBits: UInt16(0xABC), fieldRequestedBitCount: 12) + // 0xABC (12 bits) positioned at MSB of 16-bit value + // 0xABC << 4 = 0xABC0 + let bitsInteger: UInt16 = 0xABC0 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 12) + return try __createFromBits(Strict12Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 12) + } #expect(result.value == 0xABC) } @@ -134,16 +172,23 @@ struct CreateFromBitsTests { // fieldBits = 0b10110100 (8 bits) // typeBitCount = 6, so shift right by 2 // Result = 0b101101 = 45 - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0b1011_0100), fieldRequestedBitCount: 8) + let bitsInteger: UInt8 = 0b1011_0100 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 8) + } #expect(result.value == 0b101101) } @Test("Excess bits: takes MSB when fieldBitCount > typeBitCount (7 > 6)") func excessBits7vs6() throws { - // fieldBits = 0b1011010 (7 bits, value 90) - // typeBitCount = 6, so shift right by 1 - // Result = 0b101101 = 45 - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0b1011010), fieldRequestedBitCount: 7) + // fieldBits = 7 bits positioned at MSB: 1011010_0 = 0b1011_0100 + // typeBitCount = 6, so takes first 6 bits = 0b101101 = 45 + let bitsInteger: UInt8 = 0b1011_0100 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 7) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 7) + } #expect(result.value == 0b101101) } @@ -152,22 +197,33 @@ struct CreateFromBitsTests { // fieldBits = 0b11010101 (8 bits) // typeBitCount = 4, so shift right by 4 // Result = 0b1101 = 13 - let result = try __createFromBits(Strict4Bit.self, fieldBits: UInt8(0b1101_0101), fieldRequestedBitCount: 8) + let bitsInteger: UInt8 = 0b1101_0101 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try __createFromBits(Strict4Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 8) + } #expect(result.value == 0b1101) } @Test("Excess bits: takes MSB when fieldBitCount > typeBitCount (3 > 1)") func excessBits3vs1() throws { - // fieldBits = 0b101 (3 bits) - // typeBitCount = 1, so shift right by 2 - // Result = 0b1 -> true - let result = try __createFromBits(Strict1Bit.self, fieldBits: UInt8(0b101), fieldRequestedBitCount: 3) - #expect(result.value == true) - - // fieldBits = 0b011 (3 bits) - // Result after shift = 0b0 -> false - let result2 = try __createFromBits(Strict1Bit.self, fieldBits: UInt8(0b011), fieldRequestedBitCount: 3) - #expect(result2.value == false) + // fieldBits = 3 bits positioned at MSB: 101_00000 = 0b1010_0000 + // typeBitCount = 1, so takes first 1 bit = 0b1 = 1 + let bitsInteger1: UInt8 = 0b1010_0000 + let result = try bitsInteger1.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 3) + return try __createFromBits(Strict1Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 3) + } + #expect(result.value == 1) + + // fieldBits = 3 bits positioned at MSB: 011_00000 = 0b0110_0000 + // Takes first 1 bit = 0b0 = 0 + let bitsInteger2: UInt8 = 0b0110_0000 + let result2 = try bitsInteger2.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 3) + return try __createFromBits(Strict1Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 3) + } + #expect(result2.value == 0) } @Test("Excess bits: takes MSB when fieldBitCount > typeBitCount (16 > 12)") @@ -175,7 +231,11 @@ struct CreateFromBitsTests { // fieldBits = 0xABCD (16 bits) // typeBitCount = 12, so shift right by 4 // Result = 0xABC - let result = try __createFromBits(Strict12Bit.self, fieldBits: UInt16(0xABCD), fieldRequestedBitCount: 16) + let bitsInteger: UInt16 = 0xABCD + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try __createFromBits(Strict12Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 16) + } #expect(result.value == 0xABC) } @@ -183,40 +243,61 @@ struct CreateFromBitsTests { @Test("Type without BitCountProviding: passes through bits directly") func flexibleTypePassThrough() throws { - let result = try __createFromBits(Flexible8Bit.self, fieldBits: UInt8(0xAB), fieldRequestedBitCount: 8) + let bitsInteger: UInt8 = 0xAB + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try __createFromBits(Flexible8Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 8) + } #expect(result.value == 0xAB) } @Test("Type without BitCountProviding: passes through even with small fieldBitCount") func flexibleTypeSmallBitCount() throws { - // Even though we say fieldBitCount is 4, there's no BitCountProviding to check - let result = try __createFromBits(Flexible8Bit.self, fieldBits: UInt8(0b1010), fieldRequestedBitCount: 4) + // 0b1010 positioned at MSB = 0b1010_0000 + // Reading 4 bits gives 0b1010 = 10 + let bitsInteger: UInt8 = 0b1010_0000 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 4) + return try __createFromBits(Flexible8Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 4) + } #expect(result.value == 0b1010) } @Test("Type without BitCountProviding: passes through with large fieldBitCount") func flexibleTypeLargeBitCount() throws { - let result = try __createFromBits(Flexible8Bit.self, fieldBits: UInt8(0xFF), fieldRequestedBitCount: 16) + let bitsInteger: UInt8 = 0xFF + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try __createFromBits(Flexible8Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 16) + } #expect(result.value == 0xFF) } // MARK: - Edge cases with truncation - @Test("Truncation when fieldBits integer is wider than RawBitsInteger") + @Test("Truncation when fieldBits integer is wider than expected") func truncationWiderInteger() throws { - // Using UInt16 fieldBits but type has UInt8 RawBitsInteger - // Value 0x1234 truncates to 0x34 - let result = try __createFromBits(Flexible8Bit.self, fieldBits: UInt16(0x1234), fieldRequestedBitCount: 16) - #expect(result.value == 0x34) + // Using UInt16 value but reading as UInt8 + // Value 0x1234 - reading first byte from big-endian + let bitsInteger: UInt16 = 0x1234 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try __createFromBits(Flexible8Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 16) + } + #expect(result.value == 0x12) // Reading the first byte from big-endian } @Test("Truncation after MSB adjustment") func truncationAfterMSBAdjustment() throws { // fieldBits = 0x1234 (16 bits), fieldBitCount = 16 - // Strict6Bit.bitCount = 6, so shift right by 10 - // 0x1234 >> 10 = 0x04 (only lower bits remain after shift) - // Then truncate to UInt8: 0x04 - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt16(0x1234), fieldRequestedBitCount: 16) + // Strict6Bit.bitCount = 6, so adjust to take first 6 bits + // 0x1234 in binary: 0001 0010 0011 0100 + // First 6 bits: 000100 = 0x04 + let bitsInteger: UInt16 = 0x1234 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 16) + } #expect(result.value == 0x04) } @@ -224,13 +305,21 @@ struct CreateFromBitsTests { @Test("Zero fieldBits with exact match") func zeroFieldBitsExactMatch() throws { - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0), fieldRequestedBitCount: 6) + let bitsInteger: UInt8 = 0 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 6) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 6) + } #expect(result.value == 0) } @Test("Zero fieldBits with excess bits") func zeroFieldBitsExcessBits() throws { - let result = try __createFromBits(Strict4Bit.self, fieldBits: UInt8(0), fieldRequestedBitCount: 8) + let bitsInteger: UInt8 = 0 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try __createFromBits(Strict4Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 8) + } #expect(result.value == 0) } @@ -239,15 +328,23 @@ struct CreateFromBitsTests { @Test("Maximum fieldBits with exact match") func maxFieldBitsExactMatch() throws { // 6 bits max = 0b111111 = 63 - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0b111111), fieldRequestedBitCount: 6) + let bitsInteger: UInt8 = 0b1111_1100 + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 6) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 6) + } #expect(result.value == 63) } @Test("Maximum fieldBits with excess bits takes MSB") func maxFieldBitsExcessTakesMSB() throws { // 8 bits all ones = 0xFF - // Shift right by 2 for 6-bit type = 0b111111 = 63 - let result = try __createFromBits(Strict6Bit.self, fieldBits: UInt8(0xFF), fieldRequestedBitCount: 8) + // Take first 6 bits = 0b111111 = 63 + let bitsInteger: UInt8 = 0xFF + let result = try bitsInteger.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try __createFromBits(Strict6Bit.self, fieldBits: rawBits, fieldRequestedBitCount: 8) + } #expect(result.value == 63) } } diff --git a/Tests/BinaryParseKitTests/Extensions/UInt16+RawBits.swift b/Tests/BinaryParseKitTests/Extensions/UInt16+RawBits.swift index f006872..666d30a 100644 --- a/Tests/BinaryParseKitTests/Extensions/UInt16+RawBits.swift +++ b/Tests/BinaryParseKitTests/Extensions/UInt16+RawBits.swift @@ -10,10 +10,8 @@ import BinaryParsing import Foundation extension UInt16: ExpressibleByRawBits { - public typealias RawBitsInteger = UInt16 - - public init(bits: RawBitsInteger) throws { - self = bits + public init(bits: borrowing RawBitsSpan) throws { + self = try bits.load(as: Self.self) } } diff --git a/Tests/BinaryParseKitTests/Extensions/UInt32+RawBits.swift b/Tests/BinaryParseKitTests/Extensions/UInt32+RawBits.swift index 9a5e055..119602d 100644 --- a/Tests/BinaryParseKitTests/Extensions/UInt32+RawBits.swift +++ b/Tests/BinaryParseKitTests/Extensions/UInt32+RawBits.swift @@ -10,10 +10,8 @@ import BinaryParsing import Foundation extension UInt32: ExpressibleByRawBits { - public typealias RawBitsInteger = UInt32 - - public init(bits: RawBitsInteger) throws { - self = bits + public init(bits: borrowing RawBitsSpan) throws { + self = try bits.load(as: Self.self) } } diff --git a/Tests/BinaryParseKitTests/ExtractBitsAsIntegerTests.swift b/Tests/BinaryParseKitTests/ExtractBitsAsIntegerTests.swift deleted file mode 100644 index 581669c..0000000 --- a/Tests/BinaryParseKitTests/ExtractBitsAsIntegerTests.swift +++ /dev/null @@ -1,282 +0,0 @@ -// -// ExtractBitsAsIntegerTests.swift -// BinaryParseKit -// -// Created by Larry Zeng on 1/6/26. -// -@testable import BinaryParseKit -import BinaryParsing -import Foundation -import Testing - -@Suite("__extractBitsAsInteger Tests") -struct ExtractBitsAsIntegerTests { - @Test("Extract 8 bits as UInt8") - func extract8BitsAsUInt8() throws { - let data = Data([0b1101_0011]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 8) - #expect(value == 0b1101_0011) - } - } - - @Test("Extract first 4 bits (MSB-first)") - func extractFirst4Bits() throws { - // 0b1101_0011 - first 4 bits = 0b1101 = 13 - let data = Data([0b1101_0011]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 4) - #expect(value == 0b1101) - } - } - - @Test("Extract last 4 bits (MSB-first)") - func extractLast4Bits() throws { - // 0b1101_0011 - last 4 bits = 0b0011 = 3 - let data = Data([0b1101_0011]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 4, count: 4) - #expect(value == 0b0011) - } - } - - @Test("Extract middle 4 bits") - func extractMiddle4Bits() throws { - // 0b1101_0011 - bits [2,6) = 0b0100 = 4 - let data = Data([0b1101_0011]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 2, count: 4) - #expect(value == 0b0100) - } - } - - @Test("Extract single bit (true)") - func extractSingleBitTrue() throws { - // 0b1000_0000 - bit 0 = 1 - let data = Data([0b1000_0000]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 1) - #expect(value == 1) - } - } - - @Test("Extract single bit (false)") - func extractSingleBitFalse() throws { - // 0b0111_1111 - bit 0 = 0 - let data = Data([0b0111_1111]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 1) - #expect(value == 0) - } - } - - @Test("Extract 16 bits as UInt16") - func extract16BitsAsUInt16() throws { - // 0x1234 in big endian - let data = Data([0x12, 0x34]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt16.self, from: span, offset: 0, count: 16) - #expect(value == 0x1234) - } - } - - @Test("Extract bits spanning byte boundary") - func extractBitsSpanningByteBoundary() throws { - // 0b1010_1100 0b1100_1010 - bits [4,12) spans bytes - // bits 4-7 from byte 0: 1100 - // bits 8-11 from byte 1: 1100 - // result: 0b1100_1100 = 204 - let data = Data([0b1010_1100, 0b1100_1010]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 4, count: 8) - #expect(value == 0b1100_1100) - } - } - - @Test("Extract 10 bits spanning byte boundary") - func extract10BitsSpanningBoundary() throws { - // 0b1010_1010 0b1100_1100 - bits [3,13) - // bits 3-7: 01010 - // bits 8-12: 11001 - // result: 0b01010_11001 = 345 - let data = Data([0b1010_1010, 0b1100_1100]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt16.self, from: span, offset: 3, count: 10) - #expect(value == 0b01_0101_1001) - } - } - - @Test("Extract zero bits returns zero") - func extractZeroBits() throws { - let data = Data([0xFF]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 0) - #expect(value == 0) - } - } - - @Test("MSB-first bit extraction per spec") - func msbFirstBitExtractionPerSpec() throws { - // Spec scenario: byte 0b11010011 - // Field A: 2 bits = 0b11 = 3 - // Field B: 4 bits = 0b0100 = 4 - // Field C: 2 bits = 0b11 = 3 - let data = Data([0b1101_0011]) - try data.withParserSpan { span in - let fieldA = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 2) - let fieldB = try __extractBitsAsInteger(UInt8.self, from: span, offset: 2, count: 4) - let fieldC = try __extractBitsAsInteger(UInt8.self, from: span, offset: 6, count: 2) - - #expect(fieldA == 3) - #expect(fieldB == 4) - #expect(fieldC == 3) - } - } - - // MARK: - Multi-byte spanning tests - - @Test("Extract 16 bits spanning 3 bytes (offset in middle of first byte)") - func extract16BitsSpanning3Bytes() throws { - // Bytes: [0b1010_0101, 0b1100_0011, 0b1111_0000] - // Extract 16 bits starting at bit offset 4 (middle of first byte) - // Bits 4-7 from byte 0: 0101 - // Bits 8-15 from byte 1: 11000011 - // Bits 16-19 from byte 2: 1111 - // Result: 0101_1100_0011_1111 = 0x5C3F - let data = Data([0b1010_0101, 0b1100_0011, 0b1111_0000]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt16.self, from: span, offset: 4, count: 16) - #expect(value == 0b0101_1100_0011_1111) - } - } - - @Test("Extract 24 bits spanning 4 bytes") - func extract24BitsSpanning4Bytes() throws { - // Bytes: [0b1111_0000, 0b1010_1010, 0b0101_0101, 0b1100_1100] - // Extract 24 bits starting at bit offset 4 - // Result spans bytes 0-3 - let data = Data([0b1111_0000, 0b1010_1010, 0b0101_0101, 0b1100_1100]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt32.self, from: span, offset: 4, count: 24) - // Bits: 0000_1010_1010_0101_0101_1100 = 0x0AA55C - #expect(value == 0x0AA55C) - } - } - - @Test("Extract 32 bits as UInt32") - func extract32BitsAsUInt32() throws { - let data = Data([0x12, 0x34, 0x56, 0x78]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt32.self, from: span, offset: 0, count: 32) - #expect(value == 0x1234_5678) - } - } - - @Test("Extract 32 bits with offset spanning 5 bytes") - func extract32BitsWithOffset() throws { - // Start at bit 4, extract 32 bits (spans 5 bytes) - let data = Data([0b1111_0001, 0x23, 0x45, 0x67, 0x89]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt32.self, from: span, offset: 4, count: 32) - // First nibble of byte 0 (0001) + bytes 1-4 shifted - #expect(value == 0x1234_5678) - } - } - - @Test("Extract 64 bits as UInt64") - func extract64BitsAsUInt64() throws { - let data = Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt64.self, from: span, offset: 0, count: 64) - #expect(value == 0x0123_4567_89AB_CDEF) - } - } - - @Test("Extract 64 bits with small offset spanning 9 bytes") - func extract64BitsWithSmallOffset() throws { - // Start at bit 1, extract 64 bits - // Input: [0]0000001 00100011 01000101 01100111 10001001 10101011 11001101 11101111 0[0000000] - // ^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^ - // bits 1-7 8-15 16-23 24-31 32-39 40-47 48-55 56-63 bit 64 - // Expected: 00000001 00100011 01000101 01100111 10001001 10101011 11001101 11101111 - // = 0x0123456789ABCDEF - let data = Data([ - 0b0000_0001, - 0b0010_0011, - 0b0100_0101, - 0b0110_0111, - 0b1000_1001, - 0b1010_1011, - 0b1100_1101, - 0b1110_1111, - 0b0000_0000, - ]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt64.self, from: span, offset: 1, count: 64) - #expect(value == 0b0000_0010_0100_0110_1000_1010_1100_1111_0001_0011_0101_0111_1001_1011_1101_1110) - } - } - - // MARK: - Error cases - - @Test("Throws when count exceeds integer bit width (UInt8)") - func throwsWhenCountExceedsUInt8Width() throws { - let data = Data([0xFF, 0xFF]) - #expect(throws: BitmaskParsableError.rawBitsIntegerNotWideEnough) { - try data.withParserSpan { span in - _ = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 9) - } - } - } - - // MARK: - Boundary cases - - @Test("Extract exactly at integer bit width boundary (32 bits)") - func extractExactly32Bits() throws { - let data = Data([0xDE, 0xAD, 0xBE, 0xEF]) - try data.withParserSpan { span in - let value = try __extractBitsAsInteger(UInt32.self, from: span, offset: 0, count: 32) - #expect(value == 0xDEAD_BEEF) - } - } - - // MARK: - Unusual offsets - - @Test("Extract with offset at last bit of byte") - func extractWithOffsetAtLastBitOfByte() throws { - // Offset 7 means we start at the LSB of first byte - let data = Data([0b0000_0001, 0b1010_1010, 0b0000_0000]) - try data.withParserSpan { span in - // Extract 9 bits starting at bit 7 - // Bit 7 from byte 0: 1 - // Bits 0-7 from byte 1: 10101010 - // Result: 1_1010_1010 = 0x1AA - let value = try __extractBitsAsInteger(UInt16.self, from: span, offset: 7, count: 9) - #expect(value == 0b1_1010_1010) - } - } - - @Test("Extract single bit from various positions") - func extractSingleBitFromVariousPositions() throws { - let data = Data([0b1010_0101]) - try data.withParserSpan { span in - let bit0 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 0, count: 1) - let bit1 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 1, count: 1) - let bit2 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 2, count: 1) - let bit3 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 3, count: 1) - let bit4 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 4, count: 1) - let bit5 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 5, count: 1) - let bit6 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 6, count: 1) - let bit7 = try __extractBitsAsInteger(UInt8.self, from: span, offset: 7, count: 1) - - #expect(bit0 == 1) - #expect(bit1 == 0) - #expect(bit2 == 1) - #expect(bit3 == 0) - #expect(bit4 == 0) - #expect(bit5 == 1) - #expect(bit6 == 0) - #expect(bit7 == 1) - } - } -} diff --git a/Tests/BinaryParseKitTests/MaskParsingTests.swift b/Tests/BinaryParseKitTests/MaskParsingTests.swift deleted file mode 100644 index e5bd264..0000000 --- a/Tests/BinaryParseKitTests/MaskParsingTests.swift +++ /dev/null @@ -1,727 +0,0 @@ -// -// MaskParsingTests.swift -// BinaryParseKit -// -// Created by Larry Zeng on 1/7/26. -// -@testable import BinaryParseKit -import BinaryParsing -import Foundation -import Testing - -@Suite("__maskParsing Tests") -struct MaskParsingUtilityTests { - // MARK: - Test Types - - /// A type that only conforms to ExpressibleByRawBits (no BitCountProviding) - private struct SimpleRawBitsType: ExpressibleByRawBits, Equatable { - typealias RawBitsInteger = UInt8 - let value: UInt8 - - init(bits: UInt8) { - value = bits - } - } - - /// A type that conforms to both ExpressibleByRawBits and BitCountProviding - private struct Strict6BitType: ExpressibleByRawBits, BitCountProviding, Equatable { - typealias RawBitsInteger = UInt8 - static let bitCount = 6 - let value: UInt8 - - init(bits: UInt8) { - value = bits - } - } - - /// A type with 1-bit width for edge case testing - private struct Strict1BitType: ExpressibleByRawBits, BitCountProviding, Equatable { - typealias RawBitsInteger = UInt8 - static let bitCount = 1 - let value: UInt8 - - init(bits: UInt8) { - value = bits - } - } - - /// A parent type for testing __maskParsing from bits - private struct Parent16Bit: ExpressibleByRawBits { - typealias RawBitsInteger = UInt16 - let value: UInt16 - - init(bits: UInt16) { - value = bits - } - } - - @Suite("__maskParsing from bits (BitCountProviding)") - struct MaskParsingFromBitsWithBitCountTests {} - - @Suite("__maskParsing from bits (ExpressibleByRawBits only)") - struct MaskParsingFromBitsNoBitCountTests {} - - @Suite("__maskParsing from span (BitCountProviding)") - struct MaskParsingFromSpanWithBitCountTests {} - - @Suite("__maskParsing from span (ExpressibleByRawBits only)") - struct MaskParsingFromSpanNoBitCountTests {} - - @Suite("__maskParsing Edge Cases") - struct MaskParsingEdgeCaseTests {} -} - -// MARK: - Tests for __maskParsing from bits (BitCountProviding) - -extension MaskParsingUtilityTests.MaskParsingFromBitsWithBitCountTests { - @Test("Extract field at position 0") - func extractFieldAtPosition0() throws { - // Binary: 1011_0100_0000_0000 = 0xB400 - // Extract 6 bits at position 0: 101101 = 45 - let bits: UInt16 = 0b1011_0100_0000_0000 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - #expect(result.value == 0b101101) - } - - @Test("Extract field at middle position") - func extractFieldAtMiddlePosition() throws { - // Binary: 0000_1011_0100_0000 = 0x0B40 - // Extract 6 bits at position 4: 101101 = 45 - let bits: UInt16 = 0b0000_1011_0100_0000 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 4, - ) - #expect(result.value == 0b101101) - } - - @Test("Extract field at end position") - func extractFieldAtEndPosition() throws { - // Binary: 0000_0000_0010_1101 = 0x002D - // Extract 6 bits at position 10: 101101 = 45 - let bits: UInt16 = 0b0000_0000_0010_1101 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 10, - ) - #expect(result.value == 0b101101) - } - - @Test("Throws error when bitCount < Field.bitCount") - func throwsWhenInsufficientBits() { - // Try to extract 5 bits into a 6-bit type - let bits: UInt16 = 0xFFFF - #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - let _: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 5, - at: 0, - ) - } - } - - @Test("Takes MSB when bitCount > Field.bitCount") - func takesMSBWhenExcessBits() throws { - // Binary: 1011_0111_1100_0000 = 0xB7C0 - // Extract 10 bits at position 0: 1011011111 - // Type only takes 6 MSB bits: 101101 = 45 - let bits: UInt16 = 0b1011_0111_1100_0000 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 10, - at: 0, - ) - #expect(result.value == 0b101101) - } - - @Test("Single bit extraction") - func singleBitExtraction() throws { - // Extract 1 bit at position 0: should be 1 - let bits: UInt16 = 0b1000_0000_0000_0000 - let result: MaskParsingUtilityTests.Strict1BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict1BitType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - #expect(result.value == 1) - - // Extract 1 bit at position 0: should be 0 - let bits2: UInt16 = 0b0111_1111_1111_1111 - let result2: MaskParsingUtilityTests.Strict1BitType = try __maskParsing( - from: bits2, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict1BitType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - #expect(result2.value == 0) - } - - @Test("All zeros extraction") - func allZerosExtraction() throws { - let bits: UInt16 = 0x0000 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - #expect(result.value == 0) - } - - @Test("All ones extraction") - func allOnesExtraction() throws { - // Extract 6 bits of all 1s: 111111 = 63 - let bits: UInt16 = 0b1111_1100_0000_0000 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - #expect(result.value == 0b111111) - } -} - -// MARK: - Tests for __maskParsing from bits (ExpressibleByRawBits only) - -extension MaskParsingUtilityTests.MaskParsingFromBitsNoBitCountTests { - @Test("Extract field at position 0") - func extractFieldAtPosition0() throws { - // Binary: 1010_1100_0000_0000 = 0xAC00 - // Extract 8 bits at position 0: 10101100 = 172 - let bits: UInt16 = 0b1010_1100_0000_0000 - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 0, - ) - #expect(result.value == 0b1010_1100) - } - - @Test("Extract field at middle position") - func extractFieldAtMiddlePosition() throws { - // Binary: 0000_1010_1100_0000 = 0x0AC0 - // Extract 8 bits at position 4: 10101100 = 172 - let bits: UInt16 = 0b0000_1010_1100_0000 - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 4, - ) - #expect(result.value == 0b1010_1100) - } - - @Test("No bit count validation (does not throw)") - func noBitCountValidation() throws { - // SimpleRawBitsType doesn't conform to BitCountProviding, - // so no validation should occur even with small bitCount - let bits: UInt16 = 0b1110_0000_0000_0000 - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 3, - at: 0, - ) - // Just truncates to fit RawBitsInteger - #expect(result.value == 0b111) - } - - @Test("No MSB adjustment (takes raw bits)") - func noMSBAdjustment() throws { - // Binary: 1011_0111_1100_0000 = 0xB7C0 - // Extract 10 bits at position 0: 1011011111 - // SimpleRawBitsType takes all bits (truncated to UInt8): 0b10111111 = 0xBF - let bits: UInt16 = 0b1011_0111_1100_0000 - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 10, - at: 0, - ) - // 1011011111 truncated to UInt8 = 0b10111111 = 191 - #expect(result.value == 0b10_1101_1111 & 0xFF) - } - - @Test("Single bit extraction") - func singleBitExtraction() throws { - let bits: UInt16 = 0b1000_0000_0000_0000 - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - #expect(result.value == 1) - } - - @Test("All zeros extraction") - func allZerosExtraction() throws { - let bits: UInt16 = 0x0000 - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 0, - ) - #expect(result.value == 0) - } - - @Test("All ones extraction") - func allOnesExtraction() throws { - let bits: UInt16 = 0xFFFF - let result: MaskParsingUtilityTests.SimpleRawBitsType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 0, - ) - #expect(result.value == 0xFF) - } -} - -// MARK: - Tests for __maskParsing from span (BitCountProviding) - -extension MaskParsingUtilityTests.MaskParsingFromSpanWithBitCountTests { - @Test("Extract field at bit offset 0") - func extractFieldAtOffset0() throws { - // Data: [0b1011_0100] = extract 6 bits: 101101 = 45 - let data = Data([0b1011_0100]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - } - #expect(result.value == 0b101101) - } - - @Test("Extract field at middle bit offset") - func extractFieldAtMiddleOffset() throws { - // Data: [0b0010_1101, 0b00xx_xxxx] - // Extract 6 bits at offset 2: 101101 = 45 - let data = Data([0b0010_1101, 0b0000_0000]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 2, - ) - } - #expect(result.value == 0b101101) - } - - @Test("Extract field spanning byte boundary") - func extractFieldSpanningByteBoundary() throws { - // Data: [0b0000_1011, 0b0100_0000] - // Extract 6 bits at offset 4: bits 4-9 = 101101 = 45 - let data = Data([0b0000_1011, 0b0100_0000]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 4, - ) - } - #expect(result.value == 0b101101) - } - - @Test("Throws error when bitCount < Field.bitCount") - func throwsWhenInsufficientBits() { - let data = Data([0xFF]) - #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - try data.withParserSpan { span in - let _: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 5, - at: 0, - ) - } - } - } - - @Test("Takes MSB when bitCount > Field.bitCount") - func takesMSBWhenExcessBits() throws { - // Data: [0b1011_0111, 0b1100_0000] - // Extract 10 bits at offset 0: 1011011111 - // Type takes 6 MSB: 101101 = 45 - let data = Data([0b1011_0111, 0b1100_0000]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 10, - at: 0, - ) - } - #expect(result.value == 0b101101) - } - - @Test("Single bit extraction") - func singleBitExtraction() throws { - let data = Data([0b1000_0000]) - let result: MaskParsingUtilityTests.Strict1BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict1BitType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - } - #expect(result.value == 1) - - let data2 = Data([0b0111_1111]) - let result2: MaskParsingUtilityTests.Strict1BitType = try data2.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict1BitType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - } - #expect(result2.value == 0) - } - - @Test("All zeros extraction") - func allZerosExtraction() throws { - let data = Data([0x00]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - } - #expect(result.value == 0) - } - - @Test("All ones extraction") - func allOnesExtraction() throws { - let data = Data([0b1111_1100]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - } - #expect(result.value == 0b111111) - } - - @Test("Extract from multi-byte data") - func extractFromMultiByteData() throws { - // Data: [0xAB, 0xCD, 0xEF] - // Extract 6 bits at offset 12: bits from 0xCD (last 4) + 0xEF (first 2) - let data = Data([0xAB, 0xCD, 0xEF]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 12, - ) - } - // Offset 12 = byte 1 bit 4, extract 6 bits - // 0xCD = 1100_1101, bits 4-7 = 1101 - // 0xEF = 1110_1111, bits 0-1 = 11 - // Result = 1101_11 = 55 - #expect(result.value == 0b110111) - } -} - -// MARK: - Tests for __maskParsing from span (ExpressibleByRawBits only) - -extension MaskParsingUtilityTests.MaskParsingFromSpanNoBitCountTests { - @Test("Extract field at bit offset 0") - func extractFieldAtOffset0() throws { - let data = Data([0b1010_1100]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 0, - ) - } - #expect(result.value == 0b1010_1100) - } - - @Test("Extract field at middle bit offset") - func extractFieldAtMiddleOffset() throws { - // Data: [0b0010_1011, 0b0000_0000] - // Extract 6 bits at offset 2: 101011 - let data = Data([0b0010_1011, 0b0000_0000]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 6, - at: 2, - ) - } - #expect(result.value == 0b101011) - } - - @Test("No bit count validation (does not throw)") - func noBitCountValidation() throws { - // SimpleRawBitsType doesn't conform to BitCountProviding, - // so no validation should occur - let data = Data([0b1110_0000]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 3, - at: 0, - ) - } - #expect(result.value == 0b111) - } - - @Test("No MSB adjustment (takes raw bits)") - func noMSBAdjustment() throws { - // Data: [0b1011_0111, 0b1100_0000] - // Extract 10 bits at offset 0: 1011011111 - // Truncated to UInt8 = last 8 bits = 0b10111111 - let data = Data([0b1011_0111, 0b1100_0000]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 10, - at: 0, - ) - } - // 10 bits = 1011011111, truncated to 8 bits = 10111111 = 191 - #expect(result.value == 0b10_1101_1111 & 0xFF) - } - - @Test("Single bit extraction") - func singleBitExtraction() throws { - let data = Data([0b1000_0000]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - } - #expect(result.value == 1) - } - - @Test("Extract spanning byte boundary") - func extractSpanningByteBoundary() throws { - // Data: [0b0000_1010, 0b1100_0000] - // Extract 8 bits at offset 4: 10101100 = 172 - let data = Data([0b0000_1010, 0b1100_0000]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 4, - ) - } - #expect(result.value == 0b1010_1100) - } - - @Test("All zeros extraction") - func allZerosExtraction() throws { - let data = Data([0x00]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 0, - ) - } - #expect(result.value == 0) - } - - @Test("All ones extraction") - func allOnesExtraction() throws { - let data = Data([0xFF]) - let result: MaskParsingUtilityTests.SimpleRawBitsType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 8, - at: 0, - ) - } - #expect(result.value == 0xFF) - } -} - -// MARK: - Edge Case Tests - -extension MaskParsingUtilityTests.MaskParsingEdgeCaseTests { - @Test("Extract at last valid bit position") - func extractAtLastValidPosition() throws { - // 16-bit parent, extract 6 bits at position 10 (last valid position) - let bits: UInt16 = 0b0000_0000_0010_1101 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 10, - ) - #expect(result.value == 0b101101) - } - - @Test("Exactly matching bit count") - func exactlyMatchingBitCount() throws { - // fieldRequestedBitCount == Field.bitCount (6 == 6) - let bits: UInt16 = 0b1011_0100_0000_0000 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - #expect(result.value == 0b101101) - } - - @Test("Large excess bits with MSB extraction") - func largeExcessBitsMSBExtraction() throws { - // Request 15 bits but type only needs 6 - // Binary: 1111_0000_1111_0010 = 0xF0F2 - // 15 bits at position 0: 111100001111001 (take 6 MSB: 111100 = 60) - let bits: UInt16 = 0b1111_0000_1111_0010 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 15, - at: 0, - ) - #expect(result.value == 0b111100) - } - - @Test("Single bit type with exactly 1 bit requested") - func singleBitTypeExactMatch() throws { - let bits: UInt16 = 0b1000_0000_0000_0000 - let result: MaskParsingUtilityTests.Strict1BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict1BitType.self, - fieldRequestedBitCount: 1, - at: 0, - ) - #expect(result.value == 1) - } - - @Test("Single bit type throws when requesting 0 bits") - func singleBitTypeThrowsOnZeroBits() { - let bits: UInt16 = 0xFFFF - #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - let _: MaskParsingUtilityTests.Strict1BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict1BitType.self, - fieldRequestedBitCount: 0, - at: 0, - ) - } - } - - @Test("Alternating bit pattern") - func alternatingBitPattern() throws { - // 0b1010_1010... pattern - let bits: UInt16 = 0b1010_1010_1010_1010 - let result: MaskParsingUtilityTests.Strict6BitType = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 0, - ) - #expect(result.value == 0b101010) - } - - @Test("Span with offset at byte boundary") - func spanWithOffsetAtByteBoundary() throws { - let data = Data([0x00, 0b1011_0100]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 8, - ) - } - #expect(result.value == 0b101101) - } - - @Test("Span with offset one before byte boundary") - func spanWithOffsetOneBeforeByteBoundary() throws { - // Extract 6 bits starting at bit 7 (crosses byte boundary) - let data = Data([0b0000_0001, 0b0110_1000]) - let result: MaskParsingUtilityTests.Strict6BitType = try data.withParserSpan { span in - try __maskParsing( - from: span, - fieldType: MaskParsingUtilityTests.Strict6BitType.self, - fieldRequestedBitCount: 6, - at: 7, - ) - } - // Bit 7 from byte 0 = 1, bits 0-4 from byte 1 = 01101 - // Result = 1_01101 = 45 - #expect(result.value == 0b101101) - } - - @Test("Extract field with zero bit count") - func extractFieldWithZeroBitCount() throws { - // Binary: 0000_0000_0010_1101 = 0x002D - // Extract 6 bits at position 10: 101101 = 45 - let bits: UInt16 = 0b0000_0000_0010_1101 - let result = try __maskParsing( - from: bits, - parentType: MaskParsingUtilityTests.Parent16Bit.self, - fieldType: MaskParsingUtilityTests.SimpleRawBitsType.self, - fieldRequestedBitCount: 0, - at: 10, - ) - #expect(result.value == 0) - } -} diff --git a/Tests/BinaryParseKitTests/Parsing/BitmaskParsingTest.swift b/Tests/BinaryParseKitTests/Parsing/BitmaskParsingTest.swift index 03345d5..487c222 100644 --- a/Tests/BinaryParseKitTests/Parsing/BitmaskParsingTest.swift +++ b/Tests/BinaryParseKitTests/Parsing/BitmaskParsingTest.swift @@ -18,9 +18,7 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Basic Bitmask Struct @ParseBitmask - struct BasicFlags { - typealias RawBitsInteger = UInt8 - + struct BasicFlags: Equatable { @mask(bitCount: 1) var flag1: UInt8 @@ -34,26 +32,33 @@ extension ParsingTests.BitmaskParsingTest { @Test("Basic bitmask struct parsing") func basicBitmaskParsing() throws { // Binary: 1 010 0011 = 0xA3 - let flags = try BasicFlags(bits: 0xA3) - #expect(flags.flag1 == 1) - #expect(flags.value == 2) - #expect(flags.nibble == 3) + let data = Data([0xA3]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } + #expect(flags == BasicFlags(flag1: 0b1, value: 0b010, nibble: 0b0011)) } @Test("Basic bitmask struct - all zeros") func basicBitmaskAllZeros() throws { - let flags = try BasicFlags(bits: 0x00) - #expect(flags.flag1 == 0) - #expect(flags.value == 0) - #expect(flags.nibble == 0) + let data = Data([0x00]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } + #expect(flags == BasicFlags(flag1: 0b0, value: 0b000, nibble: 0b0000)) } @Test("Basic bitmask struct - all ones") func basicBitmaskAllOnes() throws { - let flags = try BasicFlags(bits: 0xFF) - #expect(flags.flag1 == 1) - #expect(flags.value == 7) - #expect(flags.nibble == 15) + // Binary: 1 111 1111 = 0xFF + let data = Data([0xFF]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } + #expect(flags == BasicFlags(flag1: 0b1, value: 0b111, nibble: 0b1111)) } @Test("BasicFlags bitCount is correct") @@ -64,20 +69,26 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Single Field Bitmask @ParseBitmask - struct SingleFlag { - typealias RawBitsInteger = UInt8 - + struct SingleFlag: Equatable { @mask(bitCount: 1) var flag: UInt8 } @Test("Single field bitmask") func singleFieldBitmask() throws { - let flag1 = try SingleFlag(bits: 0x80) - #expect(flag1.flag == 1) + let data1 = Data([0x80]) + let flag1 = try data1.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 1) + return try SingleFlag(bits: rawBits) + } + #expect(flag1 == SingleFlag(flag: 0b1)) - let flag0 = try SingleFlag(bits: 0x00) - #expect(flag0.flag == 0) + let data0 = Data([0x00]) + let flag0 = try data0.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 1) + return try SingleFlag(bits: rawBits) + } + #expect(flag0 == SingleFlag(flag: 0b0)) } @Test("SingleFlag bitCount is correct") @@ -88,9 +99,7 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Multi-Byte Bitmask @ParseBitmask - struct WideBitmask { - typealias RawBitsInteger = UInt16 - + struct WideBitmask: Equatable { @mask(bitCount: 4) var high: UInt8 @@ -105,10 +114,12 @@ extension ParsingTests.BitmaskParsingTest { func multiByteBitmask() throws { // Binary: 1010 10110011 0100 // Bytes: [0xAB, 0x34] = 0xAB34 - let wide = try WideBitmask(bits: 0xAB34) - #expect(wide.high == 10) // 0b1010 - #expect(wide.middle == 179) // 0b10110011 - #expect(wide.low == 4) // 0b0100 + let data = Data([0xAB, 0x34]) + let wide = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try WideBitmask(bits: rawBits) + } + #expect(wide == WideBitmask(high: 0b1010, middle: 0b1011_0011, low: 0b0100)) } @Test("WideBitmask bitCount is correct") @@ -119,9 +130,7 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Different Integer Types @ParseBitmask - struct MixedIntegerTypes { - typealias RawBitsInteger = UInt32 - + struct MixedIntegerTypes: Equatable { @mask(bitCount: 8) var byte: UInt8 @@ -135,10 +144,12 @@ extension ParsingTests.BitmaskParsingTest { @Test("Bitmask with different integer types") func mixedIntegerTypes() throws { // 0x12 | 0x3456 | 0x78 = 0x12345678 - let mixed = try MixedIntegerTypes(bits: 0x1234_5678) - #expect(mixed.byte == 0x12) - #expect(mixed.word == 0x3456) - #expect(mixed.signed == 0x78) + let data = Data([0x12, 0x34, 0x56, 0x78]) + let mixed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + return try MixedIntegerTypes(bits: rawBits) + } + #expect(mixed == MixedIntegerTypes(byte: 0x12, word: 0x3456, signed: 0x78)) } @Test("MixedIntegerTypes bitCount is correct") @@ -149,9 +160,7 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Bitmask with Computed Properties (Ignored) @ParseBitmask - struct BitmaskWithComputed { - typealias RawBitsInteger = UInt8 - + struct BitmaskWithComputed: Equatable { @mask(bitCount: 4) var value: UInt8 @@ -168,8 +177,12 @@ extension ParsingTests.BitmaskParsingTest { @Test("Computed properties are ignored in bitmask") func bitmaskIgnoresComputed() throws { // 1010 = 10, in MSB position: 0xA0 - let bitmask = try BitmaskWithComputed(bits: 0xA0) - #expect(bitmask.value == 10) + let data = Data([0xA0]) + let bitmask = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 4) + return try BitmaskWithComputed(bits: rawBits) + } + #expect(bitmask == BitmaskWithComputed(value: 0b1010)) #expect(bitmask.computedDouble == 20) #expect(bitmask.computedWithGetSet == 10) } @@ -182,9 +195,7 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Bitmask with Static Properties (Ignored) @ParseBitmask - struct BitmaskWithStatic { - typealias RawBitsInteger = UInt8 - + struct BitmaskWithStatic: Equatable { static let defaultValue: UInt8 = 0 @mask(bitCount: 8) @@ -193,8 +204,12 @@ extension ParsingTests.BitmaskParsingTest { @Test("Static properties are ignored in bitmask") func bitmaskIgnoresStatic() throws { - let bitmask = try BitmaskWithStatic(bits: 0x42) - #expect(bitmask.value == 0x42) + let data = Data([0x42]) + let bitmask = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BitmaskWithStatic(bits: rawBits) + } + #expect(bitmask == BitmaskWithStatic(value: 0x42)) #expect(BitmaskWithStatic.defaultValue == 0) } @@ -206,9 +221,7 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Non-Byte-Aligned Bitmask @ParseBitmask - struct NonByteAligned { - typealias RawBitsInteger = UInt16 - + struct NonByteAligned: Equatable { @mask(bitCount: 3) var first: UInt8 @@ -227,10 +240,12 @@ extension ParsingTests.BitmaskParsingTest { // Actually: 0xACC0 as full 16-bit, then shift // Let's calculate: 101 01100 11 in MSB position of 16 bits = 10101100_11000000 = 0xACC0 // The init expects MSB-aligned input - let bitmask = try NonByteAligned(bits: 0xACC0) - #expect(bitmask.first == 5) // 0b101 - #expect(bitmask.second == 12) // 0b01100 - #expect(bitmask.third == 3) // 0b11 + let data = Data([0xAC, 0xC0]) + let bitmask = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10) + return try NonByteAligned(bits: rawBits) + } + #expect(bitmask == NonByteAligned(first: 0b101, second: 0b01100, third: 0b11)) } @Test("NonByteAligned bitCount is correct") @@ -241,17 +256,19 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Large Value Bitmask @ParseBitmask - struct LargeValueBitmask { - typealias RawBitsInteger = UInt32 - + struct LargeValueBitmask: Equatable { @mask(bitCount: 32) var large: UInt32 } @Test("Large 32-bit bitmask value") func largeBitmaskValue() throws { - let bitmask = try LargeValueBitmask(bits: 0x1234_5678) - #expect(bitmask.large == 0x1234_5678) + let data = Data([0x12, 0x34, 0x56, 0x78]) + let bitmask = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + return try LargeValueBitmask(bits: rawBits) + } + #expect(bitmask == LargeValueBitmask(large: 0x1234_5678)) } @Test("LargeValueBitmask bitCount is correct") @@ -262,12 +279,15 @@ extension ParsingTests.BitmaskParsingTest { // MARK: - Logic Tests (Insufficient & Excess Bits) struct Strict6Bit: ExpressibleByRawBits, BitCountProviding, RawBitsConvertible, Equatable { - typealias RawBitsInteger = UInt8 static let bitCount = 6 let value: UInt8 - init(bits: UInt8) { - value = bits + init(value: UInt8) { + self.value = value & 0b0011_1111 + } + + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load() } func toRawBits(bitCount: Int) throws -> RawBits { @@ -276,8 +296,7 @@ extension ParsingTests.BitmaskParsingTest { } @ParseBitmask - struct InsufficientBitsStruct { - typealias RawBitsInteger = UInt8 + struct InsufficientBitsStruct: Equatable { @mask(bitCount: 5) var field: ParsingTests.BitmaskParsingTest.Strict6Bit } @@ -285,49 +304,441 @@ extension ParsingTests.BitmaskParsingTest { @Test("Throws error when bitCount < Type.bitCount") func insufficientBits() { #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - try InsufficientBitsStruct(bits: 0xFF) + let data = Data([0xFF]) + try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 5) + _ = try InsufficientBitsStruct(bits: rawBits) + } } } @ParseBitmask - struct SameBitCountBitsStruct { - typealias RawBitsInteger = UInt16 + struct SameBitCountBitsStruct: Equatable { @mask(bitCount: 6) var field: ParsingTests.BitmaskParsingTest.Strict6Bit } @Test("Exact bitCount equal to Type.bitCount") func sameBitCountBits() throws { - let input: UInt16 = 0b1011_0101_0000_0000 - let parsed = try SameBitCountBitsStruct(bits: input) - #expect(parsed.field.value == 0b101101) + let data = Data([0b1011_0101, 0b0000_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 6) + return try SameBitCountBitsStruct(bits: rawBits) + } + #expect(parsed == SameBitCountBitsStruct(field: Strict6Bit(value: 0b101101))) } @ParseBitmask - struct SufficientBitsStruct { - typealias RawBitsInteger = UInt16 + struct SufficientBitsStruct: Equatable { @mask(bitCount: 7) var field: ParsingTests.BitmaskParsingTest.Strict6Bit } @Test("Exact bitCount equal to Type.bitCount") func sufficientBits() throws { - let input: UInt16 = 0b1011_0101_0000_0000 - let parsed = try SufficientBitsStruct(bits: input) - #expect(parsed.field.value == 0b101101) + let data = Data([0b1011_0101, 0b0000_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 7) + return try SufficientBitsStruct(bits: rawBits) + } + #expect(parsed == SufficientBitsStruct(field: Strict6Bit(value: 0b101101))) } @ParseBitmask - struct ExcessBitsStruct { - typealias RawBitsInteger = UInt16 + struct ExcessBitsStruct: Equatable { @mask(bitCount: 15) var field: ParsingTests.BitmaskParsingTest.Strict6Bit } @Test("Takes MSB when bitCount > Type.bitCount") func excessBits() throws { - let input: UInt16 = 0b1111_0000_1111_0010 - let parsed = try ExcessBitsStruct(bits: input) - #expect(parsed.field.value == 0b111100) + let data = Data([0b1111_0000, 0b1111_0010]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 15) + return try ExcessBitsStruct(bits: rawBits) + } + #expect(parsed == ExcessBitsStruct(field: Strict6Bit(value: 0b111100))) + } +} + +// MARK: - Little Endian (LSB) Bitmask Integration Tests + +extension ParsingTests.BitmaskParsingTest { + // MARK: - Basic Little Endian Bitmask + + /// Tests that @ParseBitmask(bitEndian: .little) compiles and parses successfully. + @ParseBitmask(bitEndian: .little) + struct LittleEndianBasicFlags: Equatable { + @mask(bitCount: 1) + var flag1: UInt8 + + @mask(bitCount: 3) + var value: UInt8 + + @mask(bitCount: 4) + var nibble: UInt8 + } + + @Test("Little endian basic bitmask parses without error") + func littleEndianBasicParsing() throws { + let data = Data([0b1010_0011]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianBasicFlags(bits: rawBits) + } + // Verify parsing completed and values are within expected ranges + #expect( + flags == .init( + flag1: 0b1, + value: 0b001, + nibble: 0b1010, + ), + ) + } + + @Test("Little endian basic bitmask - all zeros") + func littleEndianBasicAllZeros() throws { + let data = Data([0x00]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianBasicFlags(bits: rawBits) + } + #expect(flags == .init(flag1: 0b0, value: 0b000, nibble: 0b0000)) + } + + @Test("Little endian basic bitmask - all ones") + func littleEndianBasicAllOnes() throws { + let data = Data([0xFF]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianBasicFlags(bits: rawBits) + } + #expect(flags == .init(flag1: 0b1, value: 0b111, nibble: 0b1111)) + } + + @Test("LittleEndianBasicFlags bitCount is correct") + func littleEndianBasicBitCount() { + #expect(LittleEndianBasicFlags.bitCount == 8) + } + + // MARK: - Little Endian Multi-Byte + + @ParseBitmask(bitEndian: .little) + struct LittleEndianWideBitmask: Equatable { + @mask(bitCount: 4) + var low: UInt8 + + @mask(bitCount: 8) + var middle: UInt8 + + @mask(bitCount: 4) + var high: UInt8 + } + + @Test("Little endian multi-byte bitmask spanning 2 bytes") + func littleEndianMultiByte() throws { + let data = Data([0b1010_1011, 0b0011_0100]) + let wide = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try LittleEndianWideBitmask(bits: rawBits) + } + + // Verify parsing completed and values are within expected ranges + #expect(LittleEndianWideBitmask.bitCount == 16) + #expect( + wide == .init( + low: 0b0100, + middle: 0b1011_0011, + high: 0b1010, + ), + ) + } + + @ParseBitmask(bitEndian: .little) + struct LittleEndianUnalignedWideBitmask: Equatable { + @mask(bitCount: 2) + var low: UInt8 + + @mask(bitCount: 3) + var middle: UInt8 + + @mask(bitCount: 5) + var high: UInt8 + } + + @Test("Little endian multi-byte bitmask spanning 2 bytes") + func littleEndianUnalignedMultiByte() throws { + // Input: 0b10101011 0b00110101 (16 bits, but only 10 used) + // Fields: low(2) + middle(3) + high(5) = 10 bits + // LSB mode slicing(last:) extracts from END of the 10-bit range: + // Actual values: low=1, middle=5, high=25 + let data = Data([0b1010_1011, 0b0011_0101]) + let wide = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try LittleEndianUnalignedWideBitmask(bits: rawBits) + } + #expect(LittleEndianUnalignedWideBitmask.bitCount == 10) + #expect( + wide == .init( + low: 0b01, + middle: 0b101, + high: 0b11001, + ), + ) + } + + // MARK: - Little Endian Single Bit Field + + @ParseBitmask(bitEndian: .little) + struct LittleEndianSingleBitBitmask: Equatable { + @mask(bitCount: 1) + var flag: UInt8 + } + + @Test("Little endian single bit - LSB is 1") + func littleEndianSingleBitOne() throws { + let data = Data([0b0000_0001]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianSingleBitBitmask(bits: rawBits) + } + #expect(parsed == .init(flag: 0b1)) + } + + @Test("Little endian single bit - LSB is 0") + func littleEndianSingleBitZero() throws { + let data = Data([0b1000_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianSingleBitBitmask(bits: rawBits) + } + #expect(parsed == .init(flag: 0b0)) + } + + // MARK: - Little Endian 3 Bytes Crossing + + @ParseBitmask(bitEndian: .little) + struct LittleEndianThreeByteBitmask: Equatable { + @mask(bitCount: 5) + var first: UInt8 + + @mask(bitCount: 10) + var second: UInt16 + + @mask(bitCount: 9) + var third: UInt16 + } + + @Test("Little endian bitmask spanning 3 bytes") + func littleEndianThreeByteCrossing() throws { + let data = Data([0b1101_0101, 0b1011_0011, 0b1111_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 24) + return try LittleEndianThreeByteBitmask(bits: rawBits) + } + #expect(LittleEndianThreeByteBitmask.bitCount == 24) + #expect( + parsed == .init( + first: 0b10000, + second: 0b01_1001_1111, + third: 0b1_1010_1011, + ), + ) + } + + // MARK: - Little Endian Different Integer Types + + @ParseBitmask(bitEndian: .little) + struct LittleEndianMixedTypes: Equatable { + @mask(bitCount: 4) + var small: UInt8 + + @mask(bitCount: 12) + var medium: UInt16 + + @mask(bitCount: 16) + var large: UInt16 + } + + @Test("Little endian bitmask with mixed integer types") + func littleEndianMixedTypes() throws { + let data = Data([0b1010_1111, 0b1100_1100, 0b0011_0011, 0b1111_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + return try LittleEndianMixedTypes(bits: rawBits) + } + #expect(LittleEndianMixedTypes.bitCount == 32) + #expect( + parsed == .init( + small: 0b0000, + medium: 0b0011_0011_1111, + large: 0b1010_1111_1100_1100, + ), + ) + } + + // MARK: - Little Endian Alternating Pattern + + @ParseBitmask(bitEndian: .little) + struct LittleEndianAlternatingBitmask: Equatable { + @mask(bitCount: 1) + var b0: UInt8 + @mask(bitCount: 1) + var b1: UInt8 + @mask(bitCount: 1) + var b2: UInt8 + @mask(bitCount: 1) + var b3: UInt8 + @mask(bitCount: 1) + var b4: UInt8 + @mask(bitCount: 1) + var b5: UInt8 + @mask(bitCount: 1) + var b6: UInt8 + @mask(bitCount: 1) + var b7: UInt8 + } + + @Test("Little endian single bit fields extract in LSB order") + func littleEndianAlternatingPattern() throws { + // Input: 0b10101010 + let data = Data([0b1010_1010]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianAlternatingBitmask(bits: rawBits) + } + // LSB order: b0=bit0, b1=bit1, ..., b7=bit7 + #expect( + parsed == .init( + b0: 0b0, + b1: 0b1, + b2: 0b0, + b3: 0b1, + b4: 0b0, + b5: 0b1, + b6: 0b0, + b7: 0b1, + ), + ) + } + + // MARK: - Little Endian Non-Power-of-Two Fields + + @ParseBitmask(bitEndian: .little) + struct LittleEndianNonPowerOfTwoBitmask: Equatable { + @mask(bitCount: 3) + var three: UInt8 + + @mask(bitCount: 5) + var five: UInt8 + + @mask(bitCount: 7) + var seven: UInt8 + + @mask(bitCount: 9) + var nine: UInt16 + } + + @Test("Little endian bitmask with non-power-of-two field sizes") + func littleEndianNonPowerOfTwo() throws { + // Input: 0b11111010 0b10010101 0b10110011 + // Fields: three(3) + five(5) + seven(7) + nine(9) = 24 bits + // LSB mode slicing(last:) extracts from END: + // Actual values: three=3, five=22, seven=21, nine=501 + let data = Data([0b1111_1010, 0b1001_0101, 0b1011_0011]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 24) + return try LittleEndianNonPowerOfTwoBitmask(bits: rawBits) + } + #expect(LittleEndianNonPowerOfTwoBitmask.bitCount == 24) + #expect( + parsed == .init( + three: 0b011, + five: 0b10110, + seven: 0b0010101, + nine: 0b1_1111_0101, + ), + ) + } + + // MARK: - Little Endian vs Big Endian Comparison + + @ParseBitmask(bitEndian: .big) + struct BigEndianComparisonBitmask: Equatable { + @mask(bitCount: 3) + var first: UInt8 + + @mask(bitCount: 5) + var second: UInt8 + } + + @ParseBitmask(bitEndian: .little) + struct LittleEndianComparisonBitmask: Equatable { + @mask(bitCount: 3) + var first: UInt8 + + @mask(bitCount: 5) + var second: UInt8 + } + + @Test("Big vs Little endian - same data produces different values") + func bigVsLittleEndianComparison() throws { + // Input: 0b10110011 + let data = Data([0b1011_0011]) + + let bigParsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BigEndianComparisonBitmask(bits: rawBits) + } + // Big endian (MSB first): first=bits[0-2]=0b101, second=bits[3-7]=0b10011 + #expect( + bigParsed == .init( + first: 0b101, + second: 0b10011, + ), + ) + + let littleParsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try LittleEndianComparisonBitmask(bits: rawBits) + } + // Little endian (LSB first): first=bits[0-2]=0b011, second=bits[3-7]=0b10110 + #expect( + littleParsed == .init( + first: 0b011, + second: 0b10110, + ), + ) + } + + // MARK: - Little Endian with Maximum Values + + @ParseBitmask(bitEndian: .little) + struct LittleEndianMaxValuesBitmask: Equatable { + @mask(bitCount: 3) + var three: UInt8 + + @mask(bitCount: 6) + var six: UInt8 + + @mask(bitCount: 7) + var seven: UInt8 + } + + @Test("Little endian bitmask maximum values") + func littleEndianMaxValues() throws { + // All ones: 0b11111111 0b11111111 + let data = Data([0b1111_1111, 0b1111_1111]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try LittleEndianMaxValuesBitmask(bits: rawBits) + } + #expect( + parsed == .init( + three: 0b111, + six: 0b111111, + seven: 0b1111111, + ), + ) } } diff --git a/Tests/BinaryParseKitTests/Parsing/EnumMaskParsingTest.swift b/Tests/BinaryParseKitTests/Parsing/EnumMaskParsingTest.swift index 47b038d..8502a89 100644 --- a/Tests/BinaryParseKitTests/Parsing/EnumMaskParsingTest.swift +++ b/Tests/BinaryParseKitTests/Parsing/EnumMaskParsingTest.swift @@ -31,11 +31,8 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with mask associated values") func enumWithMaskValues() throws { - // Match byte 0x01 (consumed), then parse: 1 0110100 = 0xB4 - // First mask: 1 (1 bit) -> 1 - // Second mask: 0110100 (7 bits) -> 52 let flags = try BasicEnumWithMask(parsing: Data([0x01, 0b1011_0100])) - #expect(flags == .flags(1, 52)) + #expect(flags == .flags(0b1, 0b0110100)) // Simple case still works let simple = try BasicEnumWithMask(parsing: Data([0x02, 0x12, 0x34])) @@ -44,15 +41,15 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with mask - all zeros") func enumWithMaskAllZeros() throws { - let flags = try BasicEnumWithMask(parsing: Data([0x01, 0x00])) - #expect(flags == .flags(0, 0)) + let flags = try BasicEnumWithMask(parsing: Data([0x01, 0b0000_0000])) + #expect(flags == .flags(0b0, 0b0000000)) } @Test("Enum with mask - all ones") func enumWithMaskAllOnes() throws { // 1 1111111 = 0xFF let flags = try BasicEnumWithMask(parsing: Data([0x01, 0b1111_1111])) - #expect(flags == .flags(1, 127)) + #expect(flags == .flags(0b1, 0b1111111)) } // MARK: - Mixed Parse and Mask @@ -73,12 +70,12 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with mixed @parse and @mask") func enumMixedParseAndMask() throws { // Match 0x01 (consumed), then parse UInt16 BE (0x1234), then parse mask byte: 1010 0101 = 0xA5 - let mixed = try MixedParseAndMask(parsing: Data([0x01, 0x12, 0x34, 0xA5])) - #expect(mixed == .mixed(0x1234, nibble1: 10, nibble2: 5)) + let mixed = try MixedParseAndMask(parsing: Data([0x01, 0x12, 0x34, 0b1010_0101])) + #expect(mixed == .mixed(0x1234, nibble1: 0b1010, nibble2: 0b0101)) // Single mask case - let single = try MixedParseAndMask(parsing: Data([0x02, 0x42])) - #expect(single == .singleMask(0x42)) + let single = try MixedParseAndMask(parsing: Data([0x02, 0b0100_0010])) + #expect(single == .singleMask(0b0100_0010)) } // MARK: - Multiple Mask Groups in Same Case @@ -96,12 +93,14 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with multiple separate mask groups") func enumMultipleMaskGroups() throws { - // Match 0x01 (consumed) - // First mask group: 11 010110 = 0xD6 -> group1a=3, group1b=22 - // Separator: 0xFF - // Second mask group: 1010 0101 = 0xA5 -> group2a=10, group2b=5 - let complex = try MultipleMaskGroups(parsing: Data([0x01, 0xD6, 0xFF, 0xA5])) - #expect(complex == .complex(group1a: 3, group1b: 22, separator: 0xFF, group2a: 10, group2b: 5)) + let complex = try MultipleMaskGroups(parsing: Data([0x01, 0b1101_0110, 0xFF, 0b1010_0101])) + #expect(complex == .complex( + group1a: 0b11, + group1b: 0b010110, + separator: 0xFF, + group2a: 0b1010, + group2b: 0b0101, + )) } // MARK: - Mask with Skip @@ -118,8 +117,8 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with skip before mask fields") func enumMaskWithSkip() throws { // Match 0x01 (consumed), skip 2 bytes, then parse mask: 1100 0011 = 0xC3 - let result = try MaskWithSkip(parsing: Data([0x01, 0xFF, 0xFF, 0xC3])) - #expect(result == .withPadding(12, 3)) + let result = try MaskWithSkip(parsing: Data([0x01, 0xFF, 0xFF, 0b1100_0011])) + #expect(result == .withPadding(0b1100, 0b0011)) } // MARK: - Mask with matchDefault @@ -138,8 +137,8 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with mask and default case") func enumMaskWithDefault() throws { // Known case - let known = try MaskWithDefault(parsing: Data([0x01, 0xAB])) - #expect(known == .known(10, 11)) + let known = try MaskWithDefault(parsing: Data([0x01, 0b1010_1011])) + #expect(known == .known(0b1010, 0b1011)) // Unknown case (fallback) let unknown = try MaskWithDefault(parsing: Data([0xFF])) @@ -158,9 +157,6 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Enum with multi-byte mask field") func enumMultiByteMask() throws { - // Match 0x01 (consumed), then parse 16 bits: 1010 1011 0011 0100 = 0xAB34 - // First 12 bits: 1010 1011 0011 = 0xAB3 = 2739 (right-aligned) - // Last 4 bits: 0100 = 4 let result = try MultiByteMask(parsing: Data([0x01, 0b1010_1011, 0b0011_0100])) #expect(result == .wide(0b1010_1011_0011, 0b0100)) } @@ -169,12 +165,15 @@ extension ParsingTests.EnumMaskParsingTest { /// A type with bitCount = 6 and RawBitsInteger = UInt8 for testing bit count logic. struct Strict6Bit: ExpressibleByRawBits, BitCountProviding, RawBitsConvertible, Equatable { - typealias RawBitsInteger = UInt8 static let bitCount = 6 let value: UInt8 - init(bits: UInt8) { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load(as: UInt8.self) + } + + init(value: UInt8) { + self.value = value } func toRawBits(bitCount: Int) throws -> RawBits { @@ -192,7 +191,7 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Throws error when bitCount < Type.bitCount") func insufficientBits() { #expect(throws: BitmaskParsableError.insufficientBitsAvailable) { - try InsufficientBitsEnum(parsing: Data([0x01, 0xFF])) + try InsufficientBitsEnum(parsing: Data([0x01, 0b1111_1111])) } } @@ -205,9 +204,8 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Exact bitCount equal to Type.bitCount") func sameBitCountBits() throws { - // Match 0x01, then input: 1011_0100 (first 6 bits: 101101 = 45) let value = try SameBitCountEnum(parsing: Data([0x01, 0b1011_0100])) - #expect(value == .test(Strict6Bit(bits: 0b101101))) + #expect(value == .test(Strict6Bit(value: 0b101101))) } @ParseEnum @@ -219,10 +217,8 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Takes MSB when bitCount > Type.bitCount (7 > 6)") func sufficientBits() throws { - // Match 0x01, then input: 1011_0101 (first 7 bits: 1011010 = 90) - // Take MSB 6 bits: 101101 = 45 let value = try SufficientBitsEnum(parsing: Data([0x01, 0b1011_0101])) - #expect(value == .test(Strict6Bit(bits: 0b101101))) + #expect(value == .test(Strict6Bit(value: 0b101101))) } @ParseEnum @@ -234,9 +230,531 @@ extension ParsingTests.EnumMaskParsingTest { @Test("Takes MSB when bitCount > Type.bitCount (15 > 6)") func excessBits() throws { - // Match 0x01, then input: 0b1111_0000_1111_0010 (15 bits: 111100001111001) - // Take MSB 6 bits: 111100 = 60 let value = try ExcessBitsEnum(parsing: Data([0x01, 0b1111_0000, 0b1111_0010])) - #expect(value == .test(Strict6Bit(bits: 0b111100))) + #expect(value == .test(Strict6Bit(value: 0b111100))) + } +} + +// MARK: - Little Endian (LSB) Enum Mask Integration Tests + +extension ParsingTests.EnumMaskParsingTest { + // MARK: - Basic Little Endian Mask in Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianBasicEnumWithMask: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 1) + @mask(bitCount: 7) + case flags(UInt8, UInt8) + + @matchAndTake(byte: 0x02) + @parse(endianness: .big) + case simple(UInt16) + } + + @Test("Little endian enum with mask associated values") + func littleEndianEnumWithMaskValues() throws { + let flags = try LittleEndianBasicEnumWithMask(parsing: Data([0x01, 0b1011_0100])) + #expect(flags == .flags(0b0, 0b1011010)) + + // Simple case still works (parse is not affected by bitEndian) + let simple = try LittleEndianBasicEnumWithMask(parsing: Data([0x02, 0x12, 0x34])) + #expect(simple == .simple(0x1234)) + } + + @Test("Little endian enum with mask - all zeros") + func littleEndianEnumWithMaskAllZeros() throws { + let flags = try LittleEndianBasicEnumWithMask(parsing: Data([0x01, 0b0000_0000])) + #expect(flags == .flags(0b0, 0b0000000)) + } + + @Test("Little endian enum with mask - all ones") + func littleEndianEnumWithMaskAllOnes() throws { + let flags = try LittleEndianBasicEnumWithMask(parsing: Data([0x01, 0b1111_1111])) + #expect(flags == .flags(0b1, 0b1111111)) + } + + // MARK: - Big vs Little Endian Enum Comparison + + @ParseEnum(bitEndian: .little) + enum LittleEndianEnumComparison: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 3) + case value(UInt8) + } + + @Test("Big vs Little endian enum comparison - same data, different results") + func bigVsLittleEndianEnumComparison() throws { + let littleEndian = try LittleEndianEnumComparison(parsing: Data([0x01, 0b1011_0011])) + #expect(littleEndian == .value(0b011)) // 011 from LSB + } + + // MARK: - Little Endian Mixed Parse and Mask + + @ParseEnum(bitEndian: .little) + enum LittleEndianMixedParseAndMask: Equatable { + @matchAndTake(byte: 0x01) + @parse(endianness: .big) + @mask(bitCount: 4) + @mask(bitCount: 4) + case mixed(UInt16, nibble1: UInt8, nibble2: UInt8) + + @matchAndTake(byte: 0x02) + @mask(bitCount: 8) + case singleMask(UInt8) + } + + @Test("Little endian enum with mixed @parse and @mask") + func littleEndianEnumMixedParseAndMask() throws { + let mixed = try LittleEndianMixedParseAndMask(parsing: Data([0x01, 0x12, 0x34, 0b1010_0101])) + #expect(mixed == .mixed(0x1234, nibble1: 0b0101, nibble2: 0b1010)) + + // Single mask case - full byte + let single = try LittleEndianMixedParseAndMask(parsing: Data([0x02, 0b0100_0010])) + #expect(single == .singleMask(0b0100_0010)) + } + + // MARK: - Little Endian Multiple Mask Groups + + @ParseEnum(bitEndian: .little) + enum LittleEndianMultipleMaskGroups: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 2) + @mask(bitCount: 5) + @parse(endianness: .big) + @mask(bitCount: 5) + @mask(bitCount: 4) + @mask(bitCount: 2) + @parse(endianness: .big) + @mask(bitCount: 4) + @mask(bitCount: 3) + case complex( + group1a: UInt8, + group1b: UInt8, + separator1: UInt8, + group2a: UInt8, + group2b: UInt8, + group2c: UInt8, + separator2: UInt8, + group3a: UInt8, + group3c: UInt8, + ) + } + + @Test("Little endian enum with multiple separate mask groups") + func littleEndianEnumMultipleMaskGroups() throws { + let complex = try LittleEndianMultipleMaskGroups(parsing: Data([ + 0x01, + 0b1101_0110, + 0xFF, + 0b0101_1010, + 0b1011_1010, + 0xFF, + 0b1010_0101, + ])) + #expect( + complex == .complex( + group1a: 0b10, + group1b: 0b10101, + separator1: 0xFF, + group2a: 0b11010, + group2b: 0b0101, + group2c: 0b01, + separator2: 0xFF, + group3a: 0b0101, + group3c: 0b010, + ), + ) + } + + // MARK: - Little Endian with Skip + + @ParseEnum(bitEndian: .little) + enum LittleEndianMaskWithSkip: Equatable { + @matchAndTake(byte: 0x01) + @skip(byteCount: 2, because: "reserved") + @mask(bitCount: 4) + @mask(bitCount: 4) + case withPadding(UInt8, UInt8) + } + + @Test("Little endian enum with skip before mask fields") + func littleEndianEnumMaskWithSkip() throws { + let result = try LittleEndianMaskWithSkip(parsing: Data([0x01, 0xFF, 0xFF, 0b1100_0011])) + #expect(result == .withPadding(0b0011, 0b1100)) + } + + // MARK: - Little Endian 1 bit + + @ParseEnum(bitEndian: .little) + enum LittleEndianSingleBitEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 1) + case flag(UInt8) + } + + @Test("Little endian enum single bit - LSB is 1") + func littleEndianEnumSingleBitOne() throws { + let result = try LittleEndianSingleBitEnum(parsing: Data([0x01, 0b0000_0001])) + #expect(result == .flag(0b1)) + } + + @Test("Little endian enum single bit - LSB is 0") + func littleEndianEnumSingleBitZero() throws { + let result = try LittleEndianSingleBitEnum(parsing: Data([0x01, 0b1000_0000])) + #expect(result == .flag(0b0)) + } + + @Test("Little endian enum single bit - 0xFE has LSB 0") + func littleEndianEnumSingleBitFE() throws { + let result = try LittleEndianSingleBitEnum(parsing: Data([0x01, 0b1111_1110])) + #expect(result == .flag(0b0)) + } + + // MARK: - Little Endian with matchDefault + + @ParseEnum(bitEndian: .little) + enum LittleEndianMaskWithDefault: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 4) + @mask(bitCount: 4) + case known(UInt8, UInt8) + + @matchDefault + case unknown + } + + @Test("Little endian enum with mask and default case") + func littleEndianEnumMaskWithDefault() throws { + let known = try LittleEndianMaskWithDefault(parsing: Data([0x01, 0b1010_1011])) + #expect(known == .known(0b1011, 0b1010)) + + // Unknown case (fallback) + let unknown = try LittleEndianMaskWithDefault(parsing: Data([0xFF])) + #expect(unknown == .unknown) + } + + // MARK: - Little Endian Excess Bits + + @ParseEnum(bitEndian: .little) + enum LittleEndianExcessBitsEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 10) + case test(ParsingTests.EnumMaskParsingTest.Strict6Bit) + } + + @Test("Little endian takes LSB when bitCount > Type.bitCount") + func littleEndianEnumExcessBits() throws { + let value = try LittleEndianExcessBitsEnum(parsing: Data([0x01, 0b1111_0000, 0b1100_0100])) + #expect(value == .test(Strict6Bit(value: 0b000100))) + } + + // MARK: - Little Endian Unaligned Single Byte Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianUnalignedSingleByte: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 2) + @mask(bitCount: 3) + @mask(bitCount: 3) + case threeFields(first: UInt8, second: UInt8, third: UInt8) + } + + @Test("Little endian enum unaligned single byte") + func littleEndianEnumUnalignedSingleByte() throws { + let result = try LittleEndianUnalignedSingleByte(parsing: Data([0x01, 0b1101_0101])) + #expect( + result == .threeFields( + first: 0b01, + second: 0b101, + third: 0b110, + ), + ) + } + + // MARK: - Little Endian Three Byte Crossing Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianThreeByteCrossing: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 5) + @mask(bitCount: 11) + @mask(bitCount: 8) + case wideFields(first: UInt8, second: UInt16, third: UInt8) + } + + @Test("Little endian enum spanning 3 bytes") + func littleEndianEnumThreeByteCrossing() throws { + let result = try LittleEndianThreeByteCrossing(parsing: Data([ + 0x01, + 0b1101_0101, 0b1011_0011, 0b1111_0000, + ])) + #expect( + result == .wideFields( + first: 0b010000, + second: 0b101_1001_1111, + third: 0b1101_0101, + ), + ) + } + + // MARK: - Little Endian Single Bit Fields Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianSingleBitFieldsEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 1) + @mask(bitCount: 1) + @mask(bitCount: 1) + @mask(bitCount: 1) + @mask(bitCount: 1) + @mask(bitCount: 1) + @mask(bitCount: 1) + @mask(bitCount: 1) + case eightBits( + b0: UInt8, b1: UInt8, b2: UInt8, b3: UInt8, + b4: UInt8, b5: UInt8, b6: UInt8, b7: UInt8, + ) + } + + @Test("Little endian enum single bit fields") + func littleEndianEnumSingleBitFields() throws { + let result = try LittleEndianSingleBitFieldsEnum(parsing: Data([0x01, 0b1010_1010])) + #expect( + result == .eightBits( + b0: 0b0, + b1: 0b1, + b2: 0b0, + b3: 0b1, + b4: 0b0, + b5: 0b1, + b6: 0b0, + b7: 0b1, + ), + ) + } + + // MARK: - Little Endian Non-Power-of-Two Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianNonPowerOfTwoEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 3) + @mask(bitCount: 5) + @mask(bitCount: 7) + @mask(bitCount: 9) + case nonPowerOfTwo(three: UInt8, five: UInt8, seven: UInt8, nine: UInt16) + } + + @Test("Little endian enum with non-power-of-two fields") + func littleEndianEnumNonPowerOfTwo() throws { + let result = try LittleEndianNonPowerOfTwoEnum(parsing: Data([ + 0x01, + 0b1111_1010, 0b1001_0101, 0b1011_0011, + ])) + #expect( + result == .nonPowerOfTwo( + three: 0b011, + five: 0b10110, + seven: 0b0010101, + nine: 0b1_1111_0101, + ), + ) + } + + // MARK: - Big vs Little Endian Enum Comparison + + @ParseEnum(bitEndian: .big) + enum BigEndianEnumComparison: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 3) + @mask(bitCount: 5) + case values(first: UInt8, second: UInt8) + } + + @ParseEnum(bitEndian: .little) + enum LittleEndianEnumComparisonFull: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 3) + @mask(bitCount: 5) + case values(first: UInt8, second: UInt8) + } + + @Test("Big vs Little endian enum - same data produces different values") + func bigVsLittleEndianEnumComparisonFull() throws { + let data = Data([0x01, 0b1011_0011]) + let bigResult = try BigEndianEnumComparison(parsing: data) + #expect( + bigResult == .values( + first: 0b101, + second: 0b10011, + ), + ) + + let littleResult = try LittleEndianEnumComparisonFull(parsing: data) + #expect( + littleResult == .values( + first: 0b011, + second: 0b10110, + ), + ) + } + + // MARK: - Little Endian Multiple Mask Groups with Multiple Separators + + @ParseEnum(bitEndian: .little) + enum LittleEndianMultipleSeparators: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 3) + @mask(bitCount: 5) + @parse(endianness: .big) + @mask(bitCount: 4) + @mask(bitCount: 4) + @parse(endianness: .big) + @mask(bitCount: 2) + @mask(bitCount: 6) + case complex( + g1a: UInt8, + g1b: UInt8, + sep1: UInt8, + g2a: UInt8, + g2b: UInt8, + sep2: UInt16, + g3a: UInt8, + g3b: UInt8, + ) + } + + @Test("Little endian enum with multiple parse separators") + func littleEndianEnumMultipleSeparators() throws { + let result = try LittleEndianMultipleSeparators(parsing: Data([ + 0x01, // match + 0b1101_0101, // mask group 1 (3+5=8 bits) + 0xAA, // sep1 + 0b1011_0011, // mask group 2 (4+4=8 bits) + 0x12, 0x34, // sep2 (UInt16 big endian) + 0b1110_0010, // mask group 3 (2+6=8 bits) + ])) + #expect( + result == .complex( + g1a: 0b101, // bits[0-2] + g1b: 0b11010, // bits[3-7] + sep1: 0xAA, + g2a: 0b0011, // bits[0-3] + g2b: 0b1011, // bits[4-7] + sep2: 0x1234, + g3a: 0b10, // bits[0-1] + g3b: 0b111000, // bits[2-7] + ), + ) + } + + // MARK: - Little Endian Maximum Values Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianMaxValuesEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 3) + @mask(bitCount: 6) + @mask(bitCount: 7) + case maxValues(three: UInt8, six: UInt8, seven: UInt8) + } + + @Test("Little endian enum maximum values") + func littleEndianEnumMaxValues() throws { + // All ones + let result = try LittleEndianMaxValuesEnum(parsing: Data([0x01, 0b1111_1111, 0b1111_1111])) + #expect( + result == .maxValues( + three: 0b111, + six: 0b111111, + seven: 0b1111111, + ), + ) + } + + // MARK: - Little Endian All Zeros Enum + + @Test("Little endian enum all zeros") + func littleEndianEnumAllZeros() throws { + let result = try LittleEndianMaxValuesEnum(parsing: Data([0x01, 0x00, 0x00])) + #expect( + result == .maxValues( + three: 0b0, + six: 0b0, + seven: 0b0, + ), + ) + } + + // MARK: - Little Endian Wide Bitmask Enum + + @ParseEnum(bitEndian: .little) + enum LittleEndianWideBitmaskEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 12) + @mask(bitCount: 20) + case wideValues(twelve: UInt16, twenty: UInt32) + } + + @Test("Little endian enum wide bitmask fields") + func littleEndianEnumWideBitmask() throws { + let result = try LittleEndianWideBitmaskEnum(parsing: Data([ + 0x01, + 0b1101_0101, 0b1011_0011, 0b1111_0000, 0b1010_1010, + ])) + #expect( + result == .wideValues( + twelve: 0b0000_1010_1010, + twenty: 0b1101_0101_1011_0011_1111, + ), + ) + } + + // MARK: - Little Endian Enum with Multiple Cases + + @ParseEnum(bitEndian: .little) + enum LittleEndianMultiCaseEnum: Equatable { + @matchAndTake(byte: 0x01) + @mask(bitCount: 4) + @mask(bitCount: 4) + case caseA(low: UInt8, high: UInt8) + + @matchAndTake(byte: 0x02) + @mask(bitCount: 2) + @mask(bitCount: 3) + @mask(bitCount: 3) + case caseB(first: UInt8, second: UInt8, third: UInt8) + + @matchAndTake(byte: 0x03) + @parse(endianness: .big) + case caseC(value: UInt16) + + @matchDefault + case unknown + } + + @Test("Little endian enum with multiple cases - case A") + func littleEndianEnumMultiCaseA() throws { + let result = try LittleEndianMultiCaseEnum(parsing: Data([0x01, 0b1010_1111])) + #expect(result == .caseA(low: 0b1111, high: 0b1010)) + } + + @Test("Little endian enum with multiple cases - case B") + func littleEndianEnumMultiCaseB() throws { + let result = try LittleEndianMultiCaseEnum(parsing: Data([0x02, 0b1101_0101])) + #expect(result == .caseB(first: 0b01, second: 0b101, third: 0b110)) + } + + @Test("Little endian enum with multiple cases - case C (parse not affected)") + func littleEndianEnumMultiCaseC() throws { + let result = try LittleEndianMultiCaseEnum(parsing: Data([0x03, 0x12, 0x34])) + #expect(result == .caseC(value: 0x1234)) + } + + @Test("Little endian enum with multiple cases - default") + func littleEndianEnumMultiCaseDefault() throws { + let result = try LittleEndianMultiCaseEnum(parsing: Data([0xFF])) + #expect(result == .unknown) } } diff --git a/Tests/BinaryParseKitTests/Parsing/StructMaskParsingTest.swift b/Tests/BinaryParseKitTests/Parsing/StructMaskParsingTest.swift index 6b62e01..a598486 100644 --- a/Tests/BinaryParseKitTests/Parsing/StructMaskParsingTest.swift +++ b/Tests/BinaryParseKitTests/Parsing/StructMaskParsingTest.swift @@ -19,16 +19,19 @@ extension ParsingTests.StructMaskParsingTest { /// A simple flag type that conforms to BitmaskParsable with 1 bit. struct Flag: ExpressibleByRawBits, BitCountProviding, RawBitsConvertible, Equatable { - typealias RawBitsInteger = UInt8 - static var bitCount: Int { 1 } + static var bitCount: Int { + 1 + } + let value: Bool init(value: Bool) { self.value = value } - init(bits: RawBitsInteger) throws { - value = bits & 1 == 1 + init(bits: borrowing RawBitsSpan) throws { + let intValue: UInt8 = try bits.load() + value = intValue & 1 == 1 } func toRawBits(bitCount: Int) throws -> RawBits { @@ -38,17 +41,19 @@ extension ParsingTests.StructMaskParsingTest { /// A 4-bit nibble type that conforms to BitmaskParsable. struct Nibble: ExpressibleByRawBits, BitCountProviding, RawBitsConvertible, Equatable { - typealias RawBitsInteger = UInt8 - static var bitCount: Int { 4 } + static var bitCount: Int { + 4 + } + let value: UInt8 init(value: UInt8) { - precondition(value <= 0x0F, "Nibble value must be 0-15") + precondition(value <= 0b1111, "Nibble value must be 0-15") self.value = value } - init(bits: RawBitsInteger) throws { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load() } func toRawBits(bitCount: Int) throws -> RawBits { @@ -58,17 +63,19 @@ extension ParsingTests.StructMaskParsingTest { /// A 3-bit value type for testing. struct ThreeBit: ExpressibleByRawBits, BitCountProviding, RawBitsConvertible, Equatable { - typealias RawBitsInteger = UInt8 - static var bitCount: Int { 3 } + static var bitCount: Int { + 3 + } + let value: UInt8 init(value: UInt8) { - precondition(value <= 0x07, "ThreeBit value must be 0-7") + precondition(value <= 0b111, "ThreeBit value must be 0-7") self.value = value } - init(bits: RawBitsInteger) throws { - value = bits + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load() } func toRawBits(bitCount: Int) throws -> RawBits { @@ -79,7 +86,7 @@ extension ParsingTests.StructMaskParsingTest { // MARK: - Basic Mask Fields with Explicit Bit Count @ParseStruct - struct BasicBitmaskExplicit { + struct BasicBitmaskExplicit: Equatable { @mask(bitCount: 1) var flag1: UInt8 @@ -92,37 +99,27 @@ extension ParsingTests.StructMaskParsingTest { @Test("Basic bitmask parsing with explicit bit counts - all bits from single byte") func basicBitmaskExplicitParsing() throws { - // Binary: 1 010 0011 = 0xA3 - // flag1 = 1 (bit 0) -> 1 - // value = 010 (bits 1-3) -> 2 - // nibble = 0011 (bits 4-7) -> 3 - let parsed = try BasicBitmaskExplicit(parsing: Data([0xA3])) - #expect(parsed.flag1 == 1) - #expect(parsed.value == 2) - #expect(parsed.nibble == 3) + let parsed = try BasicBitmaskExplicit(parsing: Data([0b1010_0011])) + #expect(parsed == BasicBitmaskExplicit(flag1: 0b1, value: 0b010, nibble: 0b0011)) } @Test("Basic bitmask parsing - all zeros") func basicBitmaskExplicitAllZeros() throws { - let parsed = try BasicBitmaskExplicit(parsing: Data([0x00])) - #expect(parsed.flag1 == 0) - #expect(parsed.value == 0) - #expect(parsed.nibble == 0) + let parsed = try BasicBitmaskExplicit(parsing: Data([0b0000_0000])) + #expect(parsed == BasicBitmaskExplicit(flag1: 0, value: 0, nibble: 0)) } @Test("Basic bitmask parsing - all ones") func basicBitmaskExplicitAllOnes() throws { // Binary: 1 111 1111 = 0xFF - let parsed = try BasicBitmaskExplicit(parsing: Data([0xFF])) - #expect(parsed.flag1 == 1) - #expect(parsed.value == 7) // 0b111 = 7 - #expect(parsed.nibble == 15) // 0b1111 = 15 + let parsed = try BasicBitmaskExplicit(parsing: Data([0b1111_1111])) + #expect(parsed == BasicBitmaskExplicit(flag1: 0b1, value: 0b111, nibble: 0b1111)) } // MARK: - Inferred Bit Count with Custom Types @ParseStruct - struct BitmaskInferred { + struct BitmaskInferred: Equatable { @mask var flag1: ParsingTests.StructMaskParsingTest.Flag @@ -135,18 +132,12 @@ extension ParsingTests.StructMaskParsingTest { @Test("Inferred bitCount from BitmaskParsable type") func inferredBitmaskParsing() throws { - // Binary: 1 0 000101 = 0x85 - // flag1 = 1 (bit 0) -> Flag(true) - // flag2 = 0 (bit 1) -> Flag(false) - // value = 000101 (bits 2-7) -> 5 - let parsed = try BitmaskInferred(parsing: Data([0x85])) - #expect(parsed.flag1 == Flag(value: true)) - #expect(parsed.flag2 == Flag(value: false)) - #expect(parsed.value == 5) + let parsed = try BitmaskInferred(parsing: Data([0b1000_0101])) + #expect(parsed == BitmaskInferred(flag1: Flag(value: true), flag2: Flag(value: false), value: 0b000101)) } @ParseStruct - struct BitmaskAllInferred { + struct BitmaskAllInferred: Equatable { @mask var first: ParsingTests.StructMaskParsingTest.Flag @@ -159,20 +150,18 @@ extension ParsingTests.StructMaskParsingTest { @Test("All fields with inferred bit counts") func allInferredBitmaskParsing() throws { - // Binary: 1 1010 011 = 0xD3 - // first = 1 -> Flag(true) - // second = 1010 -> Nibble(10) - // third = 011 -> ThreeBit(3) - let parsed = try BitmaskAllInferred(parsing: Data([0xD3])) - #expect(parsed.first == Flag(value: true)) - #expect(parsed.second == Nibble(value: 10)) - #expect(parsed.third == ThreeBit(value: 3)) + let parsed = try BitmaskAllInferred(parsing: Data([0b1101_0011])) + #expect(parsed == BitmaskAllInferred( + first: Flag(value: true), + second: Nibble(value: 0b1010), + third: ThreeBit(value: 0b011), + )) } // MARK: - Multi-Byte Bitmask @ParseStruct - struct MultiBytesBitmask { + struct MultiBytesBitmask: Equatable { @mask(bitCount: 4) var high: UInt8 @@ -185,21 +174,14 @@ extension ParsingTests.StructMaskParsingTest { @Test("Multi-byte bitmask spanning 2 bytes") func multiBytesBitmaskParsing() throws { - // Binary: 1010 10110011 0100 - // Bytes: [0xAB, 0x34] - // high = 1010 (bits 0-3) -> 10 - // middle = 10110011 (bits 4-11) -> 0xB3 = 179 - // low = 0100 (bits 12-15) -> 4 - let parsed = try MultiBytesBitmask(parsing: Data([0xAB, 0x34])) - #expect(parsed.high == 10) // 0b1010 - #expect(parsed.middle == 179) // 0b10110011 - #expect(parsed.low == 4) // 0b0100 + let parsed = try MultiBytesBitmask(parsing: Data([0b1010_1011, 0b0011_0100])) + #expect(parsed == MultiBytesBitmask(high: 0b1010, middle: 0b1011_0011, low: 0b0100)) } // MARK: - Mixed Parse and Mask @ParseStruct - struct MixedParseMask { + struct MixedParseMask: Equatable { @parse(endianness: .big) var header: UInt8 @@ -215,22 +197,14 @@ extension ParsingTests.StructMaskParsingTest { @Test("Mixed @parse and @mask fields") func mixedParseMaskParsing() throws { - // header = 0x42 - // Binary for mask byte: 1 0110100 = 0xB4 - // flag = 1 - // value = 0110100 = 52 - // footer = 0x1234 - let parsed = try MixedParseMask(parsing: Data([0x42, 0xB4, 0x12, 0x34])) - #expect(parsed.header == 0x42) - #expect(parsed.flag == 1) - #expect(parsed.value == 52) - #expect(parsed.footer == 0x1234) + let parsed = try MixedParseMask(parsing: Data([0x42, 0b1011_0100, 0x12, 0x34])) + #expect(parsed == MixedParseMask(header: 0x42, flag: 0b1, value: 0b0110100, footer: 0x1234)) } // MARK: - Multiple Mask Groups @ParseStruct - struct MultipleMaskGroups { + struct MultipleMaskGroups: Equatable { @mask(bitCount: 4) var first: UInt8 @@ -249,21 +223,20 @@ extension ParsingTests.StructMaskParsingTest { @Test("Multiple separate mask groups") func multipleMaskGroupsParsing() throws { - // First group: 1010 0101 = 0xA5 -> first=10, second=5 - // Separator: 0xFF - // Second group: 11 010110 = 0xD6 -> third=3, fourth=22 - let parsed = try MultipleMaskGroups(parsing: Data([0xA5, 0xFF, 0xD6])) - #expect(parsed.first == 10) - #expect(parsed.second == 5) - #expect(parsed.separator == 0xFF) - #expect(parsed.third == 3) - #expect(parsed.fourth == 22) + let parsed = try MultipleMaskGroups(parsing: Data([0b1010_0101, 0xFF, 0b1101_0110])) + #expect(parsed == MultipleMaskGroups( + first: 0b1010, + second: 0b0101, + separator: 0xFF, + third: 0b11, + fourth: 0b010110, + )) } // MARK: - Error Cases @ParseStruct - struct MaskWithInsufficientData { + struct MaskWithInsufficientData: Equatable { @mask(bitCount: 4) var first: UInt8 @@ -282,7 +255,7 @@ extension ParsingTests.StructMaskParsingTest { // MARK: - Skip with Mask @ParseStruct - struct SkipWithMask { + struct SkipWithMask: Equatable { @skip(byteCount: 2, because: "header padding") @mask(bitCount: 4) var value: UInt8 @@ -294,21 +267,23 @@ extension ParsingTests.StructMaskParsingTest { @Test("Skip before mask fields") func skipBeforeMaskParsing() throws { // Skip 2 bytes, then parse mask byte: 1100 0011 = 0xC3 - let parsed = try SkipWithMask(parsing: Data([0xFF, 0xFF, 0xC3])) - #expect(parsed.value == 12) // 0b1100 - #expect(parsed.flags == 3) // 0b0011 + let parsed = try SkipWithMask(parsing: Data([0xFF, 0xFF, 0b1100_0011])) + #expect(parsed == SkipWithMask(value: 0b1100, flags: 0b0011)) } // MARK: - Logic Tests (Insufficient & Excess Bits) /// A type with bitCount = 6 and RawBitsInteger = UInt8 for testing bit count logic. struct Strict6Bit: ExpressibleByRawBits, BitCountProviding, RawBitsConvertible, Equatable { - typealias RawBitsInteger = UInt8 static let bitCount = 6 let value: UInt8 - init(bits: UInt8) { - value = bits + init(value: UInt8) { + self.value = value & 0b0011_1111 + } + + init(bits: borrowing RawBitsSpan) throws { + value = try bits.load() } func toRawBits(bitCount: Int) throws -> RawBits { @@ -317,7 +292,7 @@ extension ParsingTests.StructMaskParsingTest { } @ParseStruct - struct InsufficientBitsStruct { + struct InsufficientBitsStruct: Equatable { @mask(bitCount: 5) var field: ParsingTests.StructMaskParsingTest.Strict6Bit } @@ -330,7 +305,7 @@ extension ParsingTests.StructMaskParsingTest { } @ParseStruct - struct SameBitCountStruct { + struct SameBitCountStruct: Equatable { @mask(bitCount: 6) var field: ParsingTests.StructMaskParsingTest.Strict6Bit } @@ -339,11 +314,11 @@ extension ParsingTests.StructMaskParsingTest { func sameBitCountBits() throws { // Input: 1011_0100 (first 6 bits: 101101 = 45) let parsed = try SameBitCountStruct(parsing: Data([0b1011_0100])) - #expect(parsed.field.value == 0b101101) + #expect(parsed == SameBitCountStruct(field: Strict6Bit(value: 0b101101))) } @ParseStruct - struct SufficientBitsStruct { + struct SufficientBitsStruct: Equatable { @mask(bitCount: 7) var field: ParsingTests.StructMaskParsingTest.Strict6Bit } @@ -353,20 +328,445 @@ extension ParsingTests.StructMaskParsingTest { // Input: 1011_0101 (first 7 bits: 1011010 = 90) // Take MSB 6 bits: 101101 = 45 let parsed = try SufficientBitsStruct(parsing: Data([0b1011_0101])) - #expect(parsed.field.value == 0b101101) + #expect(parsed == SufficientBitsStruct(field: Strict6Bit(value: 0b101101))) } @ParseStruct - struct ExcessBitsStruct { + struct ExcessBitsStruct: Equatable { @mask(bitCount: 15) var field: ParsingTests.StructMaskParsingTest.Strict6Bit } @Test("Takes MSB when bitCount > Type.bitCount (15 > 6)") func excessBits() throws { - // Input: 0b1111_0000_1111_0010 (15 bits: 111100001111001) - // Take MSB 6 bits: 111100 = 60 let parsed = try ExcessBitsStruct(parsing: Data([0b1111_0000, 0b1111_0010])) - #expect(parsed.field.value == 0b111100) + #expect(parsed == ExcessBitsStruct(field: Strict6Bit(value: 0b111100))) + } +} + +// MARK: - Little Endian (LSB) Struct Mask Integration Tests + +extension ParsingTests.StructMaskParsingTest { + // MARK: - Basic Little Endian Mask Fields + + /// Tests that @ParseStruct(bitEndian: .little) compiles and parses successfully. + @ParseStruct(bitEndian: .little) + struct LittleEndianBasicMask: Equatable { + @mask(bitCount: 1) + var flag1: UInt8 + + @mask(bitCount: 3) + var value: UInt8 + + @mask(bitCount: 4) + var nibble: UInt8 + } + + @Test("Little endian struct mask parses without error") + func littleEndianBasicMaskParsing() throws { + let parsed = try LittleEndianBasicMask(parsing: Data([0b1010_0011])) + #expect( + parsed == .init( + flag1: 0b1, + value: 0b001, + nibble: 0b1010, + ), + ) + } + + @Test("Little endian struct mask - all zeros") + func littleEndianBasicMaskAllZeros() throws { + let parsed = try LittleEndianBasicMask(parsing: Data([0x00])) + #expect( + parsed == .init( + flag1: 0, + value: 0, + nibble: 0, + ), + ) + } + + @Test("Little endian struct mask - all ones") + func littleEndianBasicMaskAllOnes() throws { + let parsed = try LittleEndianBasicMask(parsing: Data([0xFF])) + #expect( + parsed == .init( + flag1: 0b1, + value: 0b111, + nibble: 0b1111, + ), + ) + } + + // MARK: - Little Endian Mixed Parse and Mask + + @ParseStruct(bitEndian: .little) + struct LittleEndianMixedParseMask { + @parse(endianness: .big) + var header: UInt8 + + @mask(bitCount: 2) + var flag: UInt8 + + @mask(bitCount: 3) + var value: UInt8 + + @parse(endianness: .big) + var footer: UInt8 + } + + @Test("Little endian mixed @parse and @mask fields") + func littleEndianMixedParseMaskParsing() throws { + let parsed = try LittleEndianMixedParseMask(parsing: Data([0x42, 0b1011_0101, 0x99])) + #expect(parsed.header == 0x42) + #expect(parsed.flag == 0b01) + #expect(parsed.value == 0b101) + #expect(parsed.footer == 0x99) + } + + // MARK: - Little Endian with Custom Types + + @ParseStruct(bitEndian: .little) + struct LittleEndianWithCustomTypes: Equatable { + @mask + var flag: ParsingTests.StructMaskParsingTest.Flag + + @mask + var nibble: ParsingTests.StructMaskParsingTest.Nibble + + @mask + var threeBit: ParsingTests.StructMaskParsingTest.ThreeBit + } + + @Test("Little endian struct with inferred bit count custom types parses") + func littleEndianWithCustomTypesParsing() throws { + let parsed = try LittleEndianWithCustomTypes(parsing: Data([0b1101_1011])) + #expect( + parsed == .init( + flag: .init(value: true), + nibble: .init(value: 0b1101), + threeBit: .init(value: 0b110), + ), + ) + } + + // MARK: - Little Endian Multiple Mask Groups + + @ParseStruct(bitEndian: .little) + struct LittleEndianMultipleMaskGroups: Equatable { + @mask(bitCount: 3) + var first: UInt8 + + @mask(bitCount: 4) + var second: UInt8 + + @mask(bitCount: 7) + var third: UInt8 + + @parse(endianness: .big) + var separator: UInt8 + + @mask(bitCount: 1) + var fourth: UInt8 + + @mask(bitCount: 6) + var fifth: UInt8 + } + + @Test("Little endian multiple separate mask groups") + func littleEndianMultipleMaskGroupsParsing() throws { + let parsed = try LittleEndianMultipleMaskGroups(parsing: Data([0b1011_0100, 0b1010_0110, 0xFF, 0b1101_0110])) + #expect( + parsed == .init( + first: 0b110, + second: 0b0100, + third: 0b1101001, + separator: 0xFF, + fourth: 0b0, + fifth: 0b101011, + ), + ) + } + + // MARK: - Little Endian Unaligned Single Byte + + @ParseStruct(bitEndian: .little) + struct LittleEndianUnalignedSingleByte: Equatable { + @mask(bitCount: 2) + var first: UInt8 + + @mask(bitCount: 3) + var second: UInt8 + + @mask(bitCount: 3) + var third: UInt8 + } + + @Test("Little endian unaligned single byte parsing") + func littleEndianUnalignedSingleByte() throws { + let parsed = try LittleEndianUnalignedSingleByte(parsing: Data([0b1101_0101])) + #expect( + parsed == .init( + first: 0b01, + second: 0b101, + third: 0b110, + ), + ) + } + + // MARK: - Little Endian Three Byte Crossing + + @ParseStruct(bitEndian: .little) + struct LittleEndianThreeByteCrossing: Equatable { + @mask(bitCount: 5) + var first: UInt8 + + @mask(bitCount: 11) + var second: UInt16 + + @mask(bitCount: 8) + var third: UInt8 + } + + @Test("Little endian struct spanning 3 bytes") + func littleEndianThreeByteCrossingParsing() throws { + let parsed = try LittleEndianThreeByteCrossing(parsing: Data([0b1101_0101, 0b1011_0011, 0b1111_0000])) + #expect( + parsed == .init( + first: 0b10000, + second: 0b101_1001_1111, + third: 0b1101_0101, + ), + ) + } + + // MARK: - Little Endian Single Bit Fields + + @ParseStruct(bitEndian: .little) + struct LittleEndianSingleBitFields: Equatable { + @mask(bitCount: 1) + var b0: UInt8 + @mask(bitCount: 1) + var b1: UInt8 + @mask(bitCount: 1) + var b2: UInt8 + @mask(bitCount: 1) + var b3: UInt8 + @mask(bitCount: 1) + var b4: UInt8 + @mask(bitCount: 1) + var b5: UInt8 + @mask(bitCount: 1) + var b6: UInt8 + @mask(bitCount: 1) + var b7: UInt8 + } + + @Test("Little endian struct single bit fields") + func littleEndianSingleBitFieldsParsing() throws { + // Input: 0b10101010 + let parsed = try LittleEndianSingleBitFields(parsing: Data([0b1010_1010])) + // LSB order: b0=bit0=0, b1=bit1=1, etc. + #expect( + parsed == .init( + b0: 0b0, + b1: 0b1, + b2: 0b0, + b3: 0b1, + b4: 0b0, + b5: 0b1, + b6: 0b0, + b7: 0b1, + ), + ) + } + + // MARK: - Little Endian Non-Power-of-Two Fields + + @ParseStruct(bitEndian: .little) + struct LittleEndianNonPowerOfTwo: Equatable { + @mask(bitCount: 3) + var three: UInt8 + + @mask(bitCount: 5) + var five: UInt8 + + @mask(bitCount: 7) + var seven: UInt8 + + @mask(bitCount: 9) + var nine: UInt16 + } + + @Test("Little endian struct with non-power-of-two fields") + func littleEndianNonPowerOfTwoParsing() throws { + let parsed = try LittleEndianNonPowerOfTwo(parsing: Data([0b1111_1010, 0b1001_0101, 0b1011_0011])) + #expect( + parsed == .init( + three: 0b011, + five: 0b10110, + seven: 0b0010101, + nine: 0b1_1111_0101, + ), + ) + } + + // MARK: - Little Endian vs Big Endian Struct Comparison + + @ParseStruct(bitEndian: .big) + struct BigEndianStructComparison: Equatable { + @mask(bitCount: 3) + var first: UInt8 + + @mask(bitCount: 5) + var second: UInt8 + } + + @ParseStruct(bitEndian: .little) + struct LittleEndianStructComparison: Equatable { + @mask(bitCount: 3) + var first: UInt8 + + @mask(bitCount: 5) + var second: UInt8 + } + + @Test("Big vs Little endian struct - same data produces different values") + func bigVsLittleEndianStructComparison() throws { + // Input: 0b10110011 + let bigParsed = try BigEndianStructComparison(parsing: Data([0b1011_0011])) + // Big endian (MSB first): first=bits[0-2]=0b101, second=bits[3-7]=0b10011 + #expect( + bigParsed == .init( + first: 0b101, + second: 0b10011, + ), + ) + + let littleParsed = try LittleEndianStructComparison(parsing: Data([0b1011_0011])) + // Little endian (LSB first): first=bits[0-2]=0b011, second=bits[3-7]=0b10110 + #expect( + littleParsed == .init( + first: 0b011, + second: 0b10110, + ), + ) + } + + // MARK: - Little Endian Multiple Parse Separators + + @ParseStruct(bitEndian: .little) + struct LittleEndianMultipleParseSeparators: Equatable { + @mask(bitCount: 3) + var first: UInt8 + + @mask(bitCount: 5) + var second: UInt8 + + @parse(endianness: .big) + var sep1: UInt8 + + @mask(bitCount: 4) + var third: UInt8 + + @mask(bitCount: 4) + var fourth: UInt8 + + @parse(endianness: .big) + var sep2: UInt16 + + @mask(bitCount: 2) + var fifth: UInt8 + + @mask(bitCount: 6) + var sixth: UInt8 + } + + @Test("Little endian struct with multiple parse separators") + func littleEndianMultipleParseSeparatorsParsing() throws { + let parsed = try LittleEndianMultipleParseSeparators(parsing: Data([ + 0b1101_0101, // mask group 1 + 0xAA, // sep1 + 0b1011_0011, // mask group 2 + 0x12, 0x34, // sep2 + 0b1110_0010, // mask group 3 + ])) + #expect( + parsed == .init( + first: 0b101, // bits[0-2] + second: 0b11010, // bits[3-7] + sep1: 0xAA, + third: 0b0011, // bits[0-3] + fourth: 0b1011, // bits[4-7] + sep2: 0x1234, + fifth: 0b10, // bits[0-1] + sixth: 0b111000, // bits[2-7] + ), + ) + } + + // MARK: - Little Endian with Skip + + @ParseStruct(bitEndian: .little) + struct LittleEndianWithSkip: Equatable { + @skip(byteCount: 2, because: "padding") + @mask(bitCount: 4) + var first: UInt8 + + @mask(bitCount: 4) + var second: UInt8 + } + + @Test("Little endian struct with skip before mask") + func littleEndianWithSkipParsing() throws { + let parsed = try LittleEndianWithSkip(parsing: Data([0xFF, 0xFF, 0b1011_0011])) + // After skip, parse mask byte: bits[0-3]=0b0011, bits[4-7]=0b1011 + #expect( + parsed == .init( + first: 0b0011, + second: 0b1011, + ), + ) + } + + // MARK: - Little Endian Maximum Values + + @ParseStruct(bitEndian: .little) + struct LittleEndianMaxValues: Equatable { + @mask(bitCount: 3) + var three: UInt8 + + @mask(bitCount: 6) + var six: UInt8 + + @mask(bitCount: 7) + var seven: UInt8 + } + + @Test("Little endian struct maximum values") + func littleEndianMaxValuesParsing() throws { + // All ones + let parsed = try LittleEndianMaxValues(parsing: Data([0b1111_1111, 0b1111_1111])) + #expect( + parsed == .init( + three: 0b111, + six: 0b111111, + seven: 0b1111111, + ), + ) + } + + // MARK: - Little Endian All Zeros + + @Test("Little endian struct all zeros") + func littleEndianAllZerosParsing() throws { + let parsed = try LittleEndianMaxValues(parsing: Data([0x00, 0x00])) + #expect( + parsed == .init( + three: 0b0, + six: 0b0, + seven: 0b0, + ), + ) } } diff --git a/Tests/BinaryParseKitTests/Printing/BitmaskPrintingTest.swift b/Tests/BinaryParseKitTests/Printing/BitmaskPrintingTest.swift index fda469c..b1bcae3 100644 --- a/Tests/BinaryParseKitTests/Printing/BitmaskPrintingTest.swift +++ b/Tests/BinaryParseKitTests/Printing/BitmaskPrintingTest.swift @@ -19,8 +19,6 @@ extension PrintingTests.BitmaskPrintingTest { @ParseBitmask struct BasicFlags { - typealias RawBitsInteger = UInt8 - @mask(bitCount: 1) var flag1: UInt8 @@ -35,7 +33,10 @@ extension PrintingTests.BitmaskPrintingTest { func basicBitmaskRoundTrip() throws { // Binary: 1 010 0011 = 0xA3 let originalBytes = Data([0b1010_0011]) - let flags = try BasicFlags(bits: 0b1010_0011) + let flags = try originalBytes.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } // Print back to bytes let printedBytes = try flags.printParsed(printer: .data) @@ -45,7 +46,10 @@ extension PrintingTests.BitmaskPrintingTest { @Test("Basic bitmask round-trip: all zeros") func basicBitmaskRoundTripAllZeros() throws { let originalBytes = Data([0b0000_0000]) - let flags = try BasicFlags(bits: 0b0000_0000) + let flags = try originalBytes.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } let printedBytes = try flags.printParsed(printer: .data) #expect(printedBytes == originalBytes) @@ -54,7 +58,10 @@ extension PrintingTests.BitmaskPrintingTest { @Test("Basic bitmask round-trip: all ones") func basicBitmaskRoundTripAllOnes() throws { let originalBytes = Data([0b1111_1111]) - let flags = try BasicFlags(bits: 0b1111_1111) + let flags = try originalBytes.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } let printedBytes = try flags.printParsed(printer: .data) #expect(printedBytes == originalBytes) @@ -64,8 +71,6 @@ extension PrintingTests.BitmaskPrintingTest { @ParseBitmask struct WideBitmask { - typealias RawBitsInteger = UInt16 - @mask(bitCount: 4) var high: UInt8 @@ -80,7 +85,10 @@ extension PrintingTests.BitmaskPrintingTest { func multiByteBitmaskRoundTrip() throws { // Binary: 1010 10110011 0100 = 0xAB34 let originalBytes = Data([0b1010_1011, 0b0011_0100]) - let wide = try WideBitmask(bits: 0b1010_1011_0011_0100) + let wide = try originalBytes.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 16) + return try WideBitmask(bits: rawBits) + } let printedBytes = try wide.printParsed(printer: .data) #expect(printedBytes == originalBytes) @@ -90,8 +98,6 @@ extension PrintingTests.BitmaskPrintingTest { @ParseBitmask struct NonByteAligned: Equatable { - typealias RawBitsInteger = UInt16 - @mask(bitCount: 3) var first: UInt8 @@ -107,7 +113,10 @@ extension PrintingTests.BitmaskPrintingTest { // Binary: 101 01100 11 = 10 bits // Byte representation: 10101100 11000000 = 0xACC0 (MSB-aligned in 16-bit) let originalData = Data([0b1010_1100, 0b1100_0000]) - let bitmask = try NonByteAligned(bits: 0b1010_1100_1100_0000) + let bitmask = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10) + return try NonByteAligned(bits: rawBits) + } let printedBytes = try bitmask.printParsed(printer: .data) // Should output 2 bytes with the 10 bits at MSB @@ -139,7 +148,11 @@ extension PrintingTests.BitmaskPrintingTest { @Test("printerIntel returns bitmask intel") func printerIntelReturnsBitmask() throws { - let flags = try BasicFlags(bits: 0b1010_0011) + let data = Data([0b1010_0011]) + let flags = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try BasicFlags(bits: rawBits) + } let intel = try flags.printerIntel() guard case let .bitmask(bitmaskIntel) = intel else { @@ -156,8 +169,6 @@ extension PrintingTests.BitmaskPrintingTest { /// 13-bit bitmask (not byte-aligned) @ParseBitmask struct ThirteenBitMask: Equatable { - typealias RawBitsInteger = UInt16 - @mask(bitCount: 5) var highBits: UInt8 @@ -173,7 +184,10 @@ extension PrintingTests.BitmaskPrintingTest { // 10101 1100 0011 000 (padded to 16 bits) -> highBits=21, middleBits=12, lowBits=3 // Bytes: 10101110 00011000 = 0xAE18 (MSB-aligned in 16-bit) let originalData = Data([0b1010_1110, 0b0001_1000]) - let parsed = try ThirteenBitMask(bits: 0b1010_1110_0001_1000) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 13) + return try ThirteenBitMask(bits: rawBits) + } #expect(parsed == ThirteenBitMask(highBits: 0b10101, middleBits: 0b1100, lowBits: 0b0011)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -182,8 +196,6 @@ extension PrintingTests.BitmaskPrintingTest { /// Eight single-bit fields @ParseBitmask struct EightSingleBits: Equatable { - typealias RawBitsInteger = UInt8 - @mask(bitCount: 1) var bit0: UInt8 @@ -213,7 +225,10 @@ extension PrintingTests.BitmaskPrintingTest { func eightSingleBitsRoundTrip() throws { // 10101010 -> bit0=1, bit1=0, bit2=1, bit3=0, bit4=1, bit5=0, bit6=1, bit7=0 let originalData = Data([0b1010_1010]) - let parsed = try EightSingleBits(bits: 0b1010_1010) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try EightSingleBits(bits: rawBits) + } #expect(parsed == EightSingleBits(bit0: 1, bit1: 0, bit2: 1, bit3: 0, bit4: 1, bit5: 0, bit6: 1, bit7: 0)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -222,7 +237,10 @@ extension PrintingTests.BitmaskPrintingTest { @Test("Eight single-bit fields all ones round-trip") func eightSingleBitsAllOnesRoundTrip() throws { let originalData = Data([0b1111_1111]) - let parsed = try EightSingleBits(bits: 0b1111_1111) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try EightSingleBits(bits: rawBits) + } #expect(parsed == EightSingleBits(bit0: 1, bit1: 1, bit2: 1, bit3: 1, bit4: 1, bit5: 1, bit6: 1, bit7: 1)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -231,7 +249,10 @@ extension PrintingTests.BitmaskPrintingTest { @Test("Eight single-bit fields all zeros round-trip") func eightSingleBitsAllZerosRoundTrip() throws { let originalData = Data([0b0000_0000]) - let parsed = try EightSingleBits(bits: 0b0000_0000) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 8) + return try EightSingleBits(bits: rawBits) + } #expect(parsed == EightSingleBits(bit0: 0, bit1: 0, bit2: 0, bit3: 0, bit4: 0, bit5: 0, bit6: 0, bit7: 0)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -240,8 +261,6 @@ extension PrintingTests.BitmaskPrintingTest { /// Large bitmask spanning 32 bits @ParseBitmask struct LargeBitmask: Equatable { - typealias RawBitsInteger = UInt32 - @mask(bitCount: 20) var largePart: UInt32 @@ -253,7 +272,10 @@ extension PrintingTests.BitmaskPrintingTest { func largeBitmaskRoundTrip() throws { // 20 bits + 12 bits = 32 bits total let originalData = Data([0x12, 0x34, 0x56, 0x78]) - let parsed = try LargeBitmask(bits: 0x1234_5678) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + return try LargeBitmask(bits: rawBits) + } // 0x12345678 -> first 20 bits = 0x12345 (= 74565), last 12 bits = 0x678 (= 1656) #expect(parsed == LargeBitmask(largePart: 0x12345, smallPart: 0x678)) let printedBytes = try parsed.printParsed(printer: .data) @@ -263,8 +285,6 @@ extension PrintingTests.BitmaskPrintingTest { /// Asymmetric bit widths (1, 2, 3, 4, 5, 6, 7 bits = 28 bits total) @ParseBitmask struct AsymmetricBitWidths: Equatable { - typealias RawBitsInteger = UInt32 - @mask(bitCount: 1) var oneBit: UInt8 @@ -293,7 +313,10 @@ extension PrintingTests.BitmaskPrintingTest { // 1 11 101 0110 01111 010101 0101010 0000 // Bytes: 11110101 10011110 10101010 10100000 = 0xF59EAAA0 (MSB-aligned in 32-bit) let originalData = Data([0b1111_0101, 0b1001_1110, 0b1010_1010, 0b1010_0000]) - let parsed = try AsymmetricBitWidths(bits: 0b1111_0101_1001_1110_1010_1010_1010_0000) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 28) + return try AsymmetricBitWidths(bits: rawBits) + } #expect(parsed == AsymmetricBitWidths( oneBit: 1, twoBits: 0b11, @@ -310,8 +333,6 @@ extension PrintingTests.BitmaskPrintingTest { /// Two equal 16-bit halves @ParseBitmask struct TwoHalves: Equatable { - typealias RawBitsInteger = UInt32 - @mask(bitCount: 16) var upperHalf: UInt16 @@ -322,7 +343,10 @@ extension PrintingTests.BitmaskPrintingTest { @Test("Two 16-bit halves round-trip") func twoHalvesRoundTrip() throws { let originalData = Data([0x12, 0x34, 0x56, 0x78]) - let parsed = try TwoHalves(bits: 0x1234_5678) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 32) + return try TwoHalves(bits: rawBits) + } #expect(parsed == TwoHalves(upperHalf: 0x1234, lowerHalf: 0x5678)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -331,8 +355,6 @@ extension PrintingTests.BitmaskPrintingTest { /// Single 3-bit field (minimal non-byte-aligned) @ParseBitmask struct ThreeBitField: Equatable { - typealias RawBitsInteger = UInt8 - @mask(bitCount: 3) var value: UInt8 } @@ -341,7 +363,10 @@ extension PrintingTests.BitmaskPrintingTest { func threeBitFieldRoundTrip() throws { // 101 00000 = 0xA0 (MSB-aligned in 8-bit) let originalData = Data([0b1010_0000]) - let parsed = try ThreeBitField(bits: 0b1010_0000) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 3) + return try ThreeBitField(bits: rawBits) + } #expect(parsed == ThreeBitField(value: 0b101)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -355,8 +380,6 @@ extension PrintingTests.BitmaskPrintingTest { /// 24-bit bitmask (3 bytes exactly) @ParseBitmask struct TwentyFourBits: Equatable { - typealias RawBitsInteger = UInt32 - @mask(bitCount: 8) var firstByte: UInt8 @@ -370,7 +393,10 @@ extension PrintingTests.BitmaskPrintingTest { @Test("24-bit (3 bytes) bitmask round-trip") func twentyFourBitsRoundTrip() throws { let originalData = Data([0xAB, 0xCD, 0xEF]) - let parsed = try TwentyFourBits(bits: 0xABCD_EF00) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 24) + return try TwentyFourBits(bits: rawBits) + } #expect(parsed == TwentyFourBits(firstByte: 0xAB, secondByte: 0xCD, thirdByte: 0xEF)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -384,8 +410,6 @@ extension PrintingTests.BitmaskPrintingTest { /// 7-bit bitmask (one bit less than a byte) @ParseBitmask struct SevenBits: Equatable { - typealias RawBitsInteger = UInt8 - @mask(bitCount: 3) var high: UInt8 @@ -397,7 +421,10 @@ extension PrintingTests.BitmaskPrintingTest { func sevenBitsRoundTrip() throws { // 101 0110 0 = 0xAC (MSB-aligned in 8-bit, with trailing 0 padding) let originalData = Data([0b1010_1100]) - let parsed = try SevenBits(bits: 0b1010_1100) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 7) + return try SevenBits(bits: rawBits) + } #expect(parsed == SevenBits(high: 0b101, low: 0b0110)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -411,8 +438,6 @@ extension PrintingTests.BitmaskPrintingTest { /// 9-bit bitmask (one bit more than a byte) @ParseBitmask struct NineBits: Equatable { - typealias RawBitsInteger = UInt16 - @mask(bitCount: 4) var high: UInt8 @@ -424,7 +449,10 @@ extension PrintingTests.BitmaskPrintingTest { func nineBitsRoundTrip() throws { // 1010 10110 0000000 = 0xAB00 (MSB-aligned in 16-bit, with trailing padding) let originalData = Data([0b1010_1011, 0b0000_0000]) - let parsed = try NineBits(bits: 0b1010_1011_0000_0000) + let parsed = try originalData.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 9) + return try NineBits(bits: rawBits) + } #expect(parsed == NineBits(high: 0b1010, low: 0b10110)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == originalData) @@ -442,7 +470,11 @@ extension PrintingTests.BitmaskPrintingTest { // 10 bits: 101 01100 11 // MSB-aligned in 16-bit: 10101100 11000000 = 0xACC0 // Output: 10101100 11000000 (padding bits are 0s) - let parsed = try NonByteAligned(bits: 0b1010_1100_1100_0000) + let data = Data([0b1010_1100, 0b1100_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 10) + return try NonByteAligned(bits: rawBits) + } #expect(parsed == NonByteAligned(first: 0b101, second: 0b01100, third: 0b11)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == Data([0b1010_1100, 0b1100_0000])) @@ -453,7 +485,11 @@ extension PrintingTests.BitmaskPrintingTest { // 13 bits: 10101 1100 0011 // MSB-aligned in 16-bit: 10101110 00011000 = 0xAE18 // Output: 10101110 00011000 (padding: 000) - let parsed = try ThirteenBitMask(bits: 0b1010_1110_0001_1000) + let data = Data([0b1010_1110, 0b0001_1000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 13) + return try ThirteenBitMask(bits: rawBits) + } #expect(parsed == ThirteenBitMask(highBits: 0b10101, middleBits: 0b1100, lowBits: 0b0011)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == Data([0b1010_1110, 0b0001_1000])) @@ -464,7 +500,11 @@ extension PrintingTests.BitmaskPrintingTest { // 3 bits: 101 // MSB-aligned in 8-bit: 10100000 // Output: 10100000 - let parsed = try ThreeBitField(bits: 0b1010_0000) + let data = Data([0b1010_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 3) + return try ThreeBitField(bits: rawBits) + } #expect(parsed == ThreeBitField(value: 0b101)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == Data([0b1010_0000])) @@ -475,7 +515,11 @@ extension PrintingTests.BitmaskPrintingTest { // 7 bits: 101 0110 // MSB-aligned in 8-bit: 10101100 // Output: 10101100 - let parsed = try SevenBits(bits: 0b1010_1100) + let data = Data([0b1010_1100]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 7) + return try SevenBits(bits: rawBits) + } #expect(parsed == SevenBits(high: 0b101, low: 0b0110)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == Data([0b1010_1100])) @@ -486,7 +530,11 @@ extension PrintingTests.BitmaskPrintingTest { // 9 bits: 1010 10110 // MSB-aligned in 16-bit: 10101011 00000000 = 0xAB00 // Output: 10101011 00000000 - let parsed = try NineBits(bits: 0b1010_1011_0000_0000) + let data = Data([0b1010_1011, 0b0000_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 9) + return try NineBits(bits: rawBits) + } #expect(parsed == NineBits(high: 0b1010, low: 0b10110)) let printedBytes = try parsed.printParsed(printer: .data) #expect(printedBytes == Data([0b1010_1011, 0b0000_0000])) @@ -497,7 +545,11 @@ extension PrintingTests.BitmaskPrintingTest { // 28 bits with 4 padding bits // MSB-aligned in 32-bit: 11110101 10011110 10101010 10100000 = 0xF59EAAA0 // Output should have padding bits = 0000 - let parsed = try AsymmetricBitWidths(bits: 0b1111_0101_1001_1110_1010_1010_1010_0000) + let data = Data([0b1111_0101, 0b1001_1110, 0b1010_1010, 0b1010_0000]) + let parsed = try data.withParserSpan { parserSpan in + let rawBits = RawBitsSpan(parserSpan.bytes, bitOffset: 0, bitCount: 28) + return try AsymmetricBitWidths(bits: rawBits) + } #expect(parsed == AsymmetricBitWidths( oneBit: 1, twoBits: 0b11, diff --git a/Tests/BinaryParseKitTests/TestRawBits.swift b/Tests/BinaryParseKitTests/TestRawBits.swift index 5944683..5dd6fb3 100644 --- a/Tests/BinaryParseKitTests/TestRawBits.swift +++ b/Tests/BinaryParseKitTests/TestRawBits.swift @@ -113,202 +113,6 @@ struct RawBitsTests { } } - @Suite("Bit Extraction") - struct BitExtraction { - @Test("Extract single bit MSB-first") - func extractSingleBit() { - // 0b11010011 -> MSB-first: bit 0 = 1, bit 1 = 1, bit 2 = 0, bit 3 = 1, etc. - let data = Data([0b1101_0011]) - let bits = RawBits(data: data, size: 8) - - #expect(bits.bit(at: 0) == true) // MSB = 1 - #expect(bits.bit(at: 1) == true) // 1 - #expect(bits.bit(at: 2) == false) // 0 - #expect(bits.bit(at: 3) == true) // 1 - #expect(bits.bit(at: 4) == false) // 0 - #expect(bits.bit(at: 5) == false) // 0 - #expect(bits.bit(at: 6) == true) // 1 - #expect(bits.bit(at: 7) == true) // LSB = 1 - } - - @Test("Extract bits as UInt64") - func extractBitsAsUInt64() { - // 0b11010011 (0xD3) - let data = Data([0b1101_0011]) - let bits = RawBits(data: data, size: 8) - - // Extract first 4 bits: 0b1101 = 13 - let first4 = bits.extractBits(from: 0, count: 4) - #expect(first4 == 13) - - // Extract last 4 bits: 0b0011 = 3 - let last4 = bits.extractBits(from: 4, count: 4) - #expect(last4 == 3) - - // Extract middle 4 bits (2-5): 0b0100 = 4 - let middle4 = bits.extractBits(from: 2, count: 4) - #expect(middle4 == 4) - } - - @Test("MSB-first bit extraction per spec") - func msbFirstBitExtraction() { - // Spec scenario: byte 0b11010011 - // Field A: 2 bits = 0b11 = 3 - // Field B: 4 bits = 0b0100 = 4 - // Field C: 2 bits = 0b11 = 3 - let data = Data([0b1101_0011]) - let bits = RawBits(data: data, size: 8) - - let fieldA = bits.extractBits(from: 0, count: 2) - let fieldB = bits.extractBits(from: 2, count: 4) - let fieldC = bits.extractBits(from: 6, count: 2) - - #expect(fieldA == 3) - #expect(fieldB == 4) - #expect(fieldC == 3) - } - } - - @Suite("Slicing") - struct Slicing { - @Test("Slice extraction from middle") - func sliceFromMiddle() { - // 0b1010110011001010 spread across 2 bytes - let data = Data([0b1010_1100, 0b1100_1010]) - let bits = RawBits(data: data, size: 16) - - // Slice bits 4-11 (8 bits) - let slice = bits.slice(from: 4, count: 8) - #expect(slice.size == 8) - } - - @Test("Slice from beginning") - func sliceFromBeginning() { - let data = Data([0b1111_0000, 0b0000_1111]) - let bits = RawBits(data: data, size: 10) - - // Slice first 4 bits - let slice = bits.slice(from: 0, count: 4) - #expect(slice.size == 4) - } - - @Test("Empty slice") - func emptySlice() { - let data = Data([0xFF]) - let bits = RawBits(data: data, size: 8) - - let slice = bits.slice(from: 4, count: 0) - #expect(slice.size == 0) - } - - @Test("Slice with proper normalization - issue example") - func sliceWithNormalization() { - // Test case from issue: - // RawBits(data = 01010101, size = 8) with slice [1, 2) - // Should produce RawBits(data = 10000000, size = 1) - let data = Data([0b0101_0101]) - let bits = RawBits(data: data, size: 8) - - // Slice from index 1, count 1 (which is [1, 2) in range notation) - let sliced = bits.slice(from: 1, count: 1) - - #expect(sliced.size == 1) - #expect(sliced.data[0] == 0b1000_0000) - #expect(sliced.bit(at: 0) == true) // bit 1 from original was '1' - } - - @Test("Slice realigns bits to MSB") - func sliceRealignsBitsToMSB() { - // Original: 0b11110000 - // Slice bits [4, 8) -> last 4 bits are 0b0000 - let data = Data([0b1111_0000]) - let bits = RawBits(data: data, size: 8) - - let sliced = bits.slice(from: 4, count: 4) - - #expect(sliced.size == 4) - #expect(sliced.data[0] == 0b0000_0000) // 0b0000 aligned to MSB - } - - @Test("Slice with unaligned multi-byte") - func sliceUnalignedMultiByte() { - // Original: 0b10101010 11001100 - // Slice bits [3, 13) -> 10 bits starting from bit 3 - // Bits 3-7 from byte 0: 01010 - // Bits 8-12 from byte 1: 11001 - // Result: 0b01010 11001 -> 0b01010110 01000000 - let data = Data([0b1010_1010, 0b1100_1100]) - let bits = RawBits(data: data, size: 16) - - let sliced = bits.slice(from: 3, count: 10) - - #expect(sliced.size == 10) - #expect(sliced.data.count == 2) - // Expected: 01010110 01000000 - #expect(sliced.data[0] == 0b0101_0110) - #expect(sliced.data[1] == 0b0100_0000) - } - - @Test("Slice single bit from different positions") - func sliceSingleBitFromDifferentPositions() { - let data = Data([0b1011_0100]) - let bits = RawBits(data: data, size: 8) - - // Bit 0 (MSB) = 1 - let bit0 = bits.slice(from: 0, count: 1) - #expect(bit0.data[0] == 0b1000_0000) - - // Bit 2 = 1 - let bit2 = bits.slice(from: 2, count: 1) - #expect(bit2.data[0] == 0b1000_0000) - - // Bit 3 = 1 - let bit3 = bits.slice(from: 3, count: 1) - #expect(bit3.data[0] == 0b1000_0000) - - // Bit 5 = 1 - let bit5 = bits.slice(from: 5, count: 1) - #expect(bit5.data[0] == 0b1000_0000) - - // Bit 1 = 0 - let bit1 = bits.slice(from: 1, count: 1) - #expect(bit1.data[0] == 0b0000_0000) - } - - @Test("Slice preserves bit values across byte boundaries") - func slicePreservesBitValuesAcrossBoundaries() { - // Create a pattern that spans multiple bytes - let data = Data([0b1111_0000, 0b1010_1010, 0b0000_1111]) - let bits = RawBits(data: data, size: 24) - - // Slice middle byte plus some bits from neighbors - // From bit 6 to bit 18 (12 bits) - let sliced = bits.slice(from: 6, count: 12) - - #expect(sliced.size == 12) - - // Verify each bit matches the original - for i in 0 ..< 12 { - #expect(sliced.bit(at: i) == bits.bit(at: 6 + i)) - } - } - - @Test("Slice result is properly normalized") - func sliceResultIsNormalized() { - // Verify that sliced data has trailing bits zeroed - let data = Data([0b1111_1111]) - let bits = RawBits(data: data, size: 8) - - // Slice 3 bits from the middle - let sliced = bits.slice(from: 2, count: 3) - - #expect(sliced.size == 3) - #expect(sliced.data.count == 1) - // 3 bits: 111, aligned to MSB: 11100000 - #expect(sliced.data[0] == 0b1110_0000) - } - } - @Suite("Equality") struct Equality { @Test("Equal RawBits") @@ -344,85 +148,4 @@ struct RawBitsTests { #expect(bits1 == bits2) } } - - @Suite("Slice Equality with Offset") - struct SliceEqualityWithOffset { - @Test("Slice equality match at offset 0") - func sliceEqualityMatchAtOffset0() { - let dataA = Data([0b1111_0000]) - let dataB = Data([0b1111_0000]) - - let bitsA = RawBits(data: dataA, size: 8) - let bitsB = RawBits(data: dataB, size: 4) - - #expect(bitsA.sliceEquals(bitsB, at: 0) == true) - } - - @Test("Slice equality mismatch at offset 4") - func sliceEqualityMismatchAtOffset4() { - let dataA = Data([0b1111_0000]) - let dataB = Data([0b1111_0000]) - - let bitsA = RawBits(data: dataA, size: 8) - let bitsB = RawBits(data: dataB, size: 4) - - // At offset 4, bitsA has 0b0000, bitsB has 0b1111 - #expect(bitsA.sliceEquals(bitsB, at: 4) == false) - } - } - - @Suite("Bitwise Operations") - struct BitwiseOperations { - @Test("Bitwise AND") - func bitwiseAnd() { - let dataA = Data([0b1010_0000]) - let dataB = Data([0b1100_0000]) - - let bitsA = RawBits(data: dataA, size: 4) - let bitsB = RawBits(data: dataB, size: 4) - - let result = bitsA & bitsB - // 0b1010 & 0b1100 = 0b1000 - #expect(result == [0b1000_0000]) - } - - @Test("Bitwise OR") - func bitwiseOr() { - let dataA = Data([0b1010_0000]) - let dataB = Data([0b1100_0000]) - - let bitsA = RawBits(data: dataA, size: 4) - let bitsB = RawBits(data: dataB, size: 4) - - let result = bitsA | bitsB - // 0b1010 | 0b1100 = 0b1110 - #expect(result == [0b1110_0000]) - } - - @Test("Bitwise XOR") - func bitwiseXor() { - let dataA = Data([0b1010_0000]) - let dataB = Data([0b1100_0000]) - - let bitsA = RawBits(data: dataA, size: 4) - let bitsB = RawBits(data: dataB, size: 4) - - let result = bitsA ^ bitsB - // 0b1010 ^ 0b1100 = 0b0110 - #expect(result == [0b0110_0000]) - } - - @Test("Bitwise AND with different sizes") - func bitwiseAndDifferentSizes() { - let dataA = Data([0xFF]) - let dataB = Data([0b1111_0000]) - - let bitsA = RawBits(data: dataA, size: 8) - let bitsB = RawBits(data: dataB, size: 4) - - let result = bitsA & bitsB - // Result size is minimum (4 bits) - #expect(result.count == 1) - } - } } diff --git a/Tests/BinaryParseKitTests/Utils.swift b/Tests/BinaryParseKitTests/Utils.swift new file mode 100644 index 0000000..176c539 --- /dev/null +++ b/Tests/BinaryParseKitTests/Utils.swift @@ -0,0 +1,16 @@ +// +// Utils.swift +// BinaryParseKit +// +// Created by Larry Zeng on 1/8/26. +// +import BinaryParsing +import Foundation + +extension FixedWidthInteger { + func withParserSpan(_ body: @escaping (inout ParserSpan) throws(E) -> T) rethrows -> T { + try withUnsafeBytes(of: bigEndian) { buffer in + try Data(buffer).withParserSpan(body) + } + } +}