Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Sources/BinaryParsing/Parsers/InlineArray.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Binary Parsing open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
extension InlineArray where Element == UInt8 {
/// Creates a new inline array by copying the required number bytes from the
/// given parser span.
///
/// - Parameter input: The `ParserSpan` to consume.
/// - Throws: A `ParsingError` if `input` does not have at least `count`
/// bytes remaining.
@inlinable
@_lifetime(&input)
public init(parsing input: inout ParserSpan) throws {
let slice = try input._divide(atByteOffset: Self.count)
self = unsafe slice.withUnsafeBytes { buffer in
InlineArray { unsafe buffer[$0] }
}
}
}

@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
extension InlineArray where Element: ~Copyable {
/// Creates a new array by parsing the specified number of elements from the given
/// parser span, using the provided closure for parsing.
///
/// The provided closure is called `count` times while initializing the inline array.
/// For example, the following code parses a 16-element `InlineArray` of `UInt32`
/// values from a `ParserSpan`. If the `input` parser span doesn't represent enough
/// memory for those 16 values, the call will throw a `ParsingError`.
///
/// let integers = try InlineArray<16, UInt32>(parsing: &input) { input in
/// try UInt32(parsingBigEndian: &input)
/// }
///
/// You can also pass a parser initializer to this initializer as a value, if it has
/// the correct shape:
///
/// let integers = try InlineArray<16, UInt32>(
/// parsing: &input,
/// parser: UInt32.init(parsingBigEndian:))
///
/// - Parameters:
/// - input: The `ParserSpan` to consume.
/// - parser: A closure that parses each element from `input`.
/// - Throws: An error if one is thrown from `parser`.
@inlinable
@_lifetime(&input)
public init(
parsing input: inout ParserSpan,
parser: (inout ParserSpan) throws -> Element
) throws {
self = try InlineArray { _ in
try parser(&input)
}
}
}
176 changes: 176 additions & 0 deletions Tests/BinaryParsingTests/InlineArrayParsingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Binary Parsing open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

import BinaryParsing
import Testing

struct InlineArrayParsingTests {
private let testBuffer: [UInt8] = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
]

@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
@Test
func parseBytes() throws {
try testBuffer.withParserSpan { span in
let parsedArray = try InlineArray<5, UInt8>(parsing: &span)
#expect(parsedArray == testBuffer.prefix(5))
#expect(span.count == 5)

let parsedArray2 = try InlineArray<3, UInt8>(parsing: &span)
#expect(parsedArray2 == testBuffer[5...].prefix(3))
#expect(span.count == 2)
}

// 'byteCount' == 0
try testBuffer.withParserSpan { span in
let parsedArray = try InlineArray<0, UInt8>(parsing: &span)
#expect(parsedArray.isEmpty)
#expect(span.count == testBuffer.count)
}

// 'byteCount' greater than available bytes
testBuffer.withParserSpan { span in
#expect(throws: ParsingError.self) {
_ = try InlineArray<100, UInt8>(parsing: &span)
}
#expect(span.count == testBuffer.count)
}
}

@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
@Test
func parseArrayOfFixedSize() throws {
// Arrays of fixed-size integers
try testBuffer.withParserSpan { span in
let parsedArray = try InlineArray<5, UInt8>(parsing: &span) { input in
try UInt8(parsing: &input)
}
#expect(parsedArray == testBuffer.prefix(5))
#expect(span.count == 5)

// Parse two UInt16 values
let parsedArray2 = try InlineArray<2, UInt16>(parsing: &span) { input in
try UInt16(parsingBigEndian: &input)
}
#expect(parsedArray2 == [0x0607, 0x0809])
#expect(span.count == 1)

// Fail to parse one UInt16
#expect(throws: ParsingError.self) {
_ = try InlineArray<1, UInt16>(parsing: &span) { input in
try UInt16(parsingBigEndian: &input)
}
}

let lastByte = try InlineArray<1, UInt8>(
parsing: &span,
parser: UInt8.init(parsing:))
#expect(lastByte == [0x0A])
#expect(span.count == 0)
}

// Parsing count = 0 always succeeds
try testBuffer.withParserSpan { span in
let parsedArray1 = try InlineArray<0, UInt64>(parsing: &span) { input in
try UInt64(parsingBigEndian: &input)
}
#expect(parsedArray1.isEmpty)
#expect(span.count == testBuffer.count)

try span.seek(toOffsetFromEnd: 0)
let parsedArray2 = try InlineArray<0, UInt64>(parsing: &span) { input in
try UInt64(parsingBigEndian: &input)
}
#expect(parsedArray2.isEmpty)
#expect(span.count == 0)
}
}

@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
@Test
func parseArrayOfCustomTypes() throws {
// Define a custom struct to test with
struct CustomType: Equatable {
var value: UInt8
var doubled: UInt8

init(parsing input: inout ParserSpan) throws {
self.value = try UInt8(parsing: &input)
guard let d = self.value *? 2 else {
throw TestError("Doubled value too large for UInt8")
}
self.doubled = d
}

init(_ value: UInt8) {
self.value = value
self.doubled = value * 2
}
}

try testBuffer.withParserSpan { span in
let parsedArray = try InlineArray<5, CustomType>(parsing: &span) {
input in
try CustomType(parsing: &input)
}

#expect(parsedArray == testBuffer.prefix(5).map(CustomType.init))
#expect(span.count == 5)
}

_ = [0x0f, 0xf0].withParserSpan { span in
#expect(throws: TestError.self) {
try InlineArray<2, CustomType>(
parsing: &span, parser: CustomType.init(parsing:))
}
}
}

@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
@Test
func parseArrayWithErrorHandling() throws {
struct ValidatedUInt8: Equatable {
var value: UInt8

init(_ v: UInt8) throws {
if v > 5 {
throw TestError("Value \(v) exceeds maximum allowed value of 5")
}
self.value = v
}

init(parsing input: inout ParserSpan) throws {
try self.init(UInt8(parsing: &input))
}
}

try testBuffer.withParserSpan { span in
// This should fail because values in the buffer exceed 5
#expect(throws: TestError.self) {
_ = try InlineArray<10, ValidatedUInt8>(parsing: &span) { input in
try ValidatedUInt8(parsing: &input)
}
}
// Even though the parsing failed, it should have consumed some elements
#expect(span.count < testBuffer.count)

// Reset and try just parsing the valid values
try span.seek(toAbsoluteOffset: 0)
let parsedArray = try InlineArray<5, ValidatedUInt8>(parsing: &span) {
input in
try ValidatedUInt8(parsing: &input)
}
let expectedArray = try testBuffer.prefix(5).map(ValidatedUInt8.init)
#expect(parsedArray == expectedArray)
}
}
}
13 changes: 13 additions & 0 deletions Tests/BinaryParsingTests/TestingSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ extension Array where Element == UInt8 {
}
}

/// Returns true if an inline array and a sequence of the same element are equivalent.
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
func == <T: Equatable, let n: Int>(
lhs: InlineArray<n, T>, rhs: some Sequence<T>
) -> Bool {
var iterator = rhs.makeIterator()
for i in 0..<n {
guard lhs[i] == iterator.next() else { return false }
}
guard iterator.next() == nil else { return false }
return true
}

/// A seeded random number generator type.
struct RapidRandom: RandomNumberGenerator {
private var state: UInt64
Expand Down