Skip to content

Commit f693ba8

Browse files
committed
Add parsers for InlineArray
This adds two parsers that produces inline arrays: - a byte-based parser that fills an InlineArray with `UInt8` values - a parser closure-based parser that fills an InlineArray with the results of the parser, called the required number of times.
1 parent 7c3165b commit f693ba8

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Binary Parsing open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
13+
extension InlineArray where Element == UInt8 {
14+
/// Creates a new inline array by copying the required number bytes from the
15+
/// given parser span.
16+
///
17+
/// - Parameter input: The `ParserSpan` to consume.
18+
/// - Throws: A `ParsingError` if `input` does not have at least `count`
19+
/// bytes remaining.
20+
@inlinable
21+
@_lifetime(&input)
22+
public init(parsing input: inout ParserSpan) throws {
23+
let slice = try input._divide(atByteOffset: Self.count)
24+
self = unsafe slice.withUnsafeBytes { buffer in
25+
InlineArray { unsafe buffer[$0] }
26+
}
27+
}
28+
}
29+
30+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
31+
extension InlineArray where Element: ~Copyable {
32+
/// Creates a new array by parsing the specified number of elements from the given
33+
/// parser span, using the provided closure for parsing.
34+
///
35+
/// The provided closure is called `count` times while initializing the inline array.
36+
/// For example, the following code parses a 16-element `InlineArray` of `UInt32`
37+
/// values from a `ParserSpan`. If the `input` parser span doesn't represent enough
38+
/// memory for those 16 values, the call will throw a `ParsingError`.
39+
///
40+
/// let integers = try InlineArray<16, UInt32>(parsing: &input) { input in
41+
/// try UInt32(parsingBigEndian: &input)
42+
/// }
43+
///
44+
/// You can also pass a parser initializer to this initializer as a value, if it has
45+
/// the correct shape:
46+
///
47+
/// let integers = try InlineArray<16, UInt32>(
48+
/// parsing: &input,
49+
/// parser: UInt32.init(parsingBigEndian:))
50+
///
51+
/// - Parameters:
52+
/// - input: The `ParserSpan` to consume.
53+
/// - parser: A closure that parses each element from `input`.
54+
/// - Throws: An error if one is thrown from `parser`.
55+
@inlinable
56+
@_lifetime(&input)
57+
public init(
58+
parsing input: inout ParserSpan,
59+
parser: (inout ParserSpan) throws -> Element
60+
) throws {
61+
self = try InlineArray { _ in
62+
try parser(&input)
63+
}
64+
}
65+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Binary Parsing open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import BinaryParsing
13+
import Testing
14+
15+
struct InlineArrayParsingTests {
16+
private let testBuffer: [UInt8] = [
17+
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
18+
]
19+
20+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
21+
@Test
22+
func parseBytes() throws {
23+
try testBuffer.withParserSpan { span in
24+
let parsedArray = try InlineArray<5, UInt8>(parsing: &span)
25+
#expect(parsedArray == testBuffer.prefix(5))
26+
#expect(span.count == 5)
27+
28+
let parsedArray2 = try InlineArray<3, UInt8>(parsing: &span)
29+
#expect(parsedArray2 == testBuffer[5...].prefix(3))
30+
#expect(span.count == 2)
31+
}
32+
33+
// 'byteCount' == 0
34+
try testBuffer.withParserSpan { span in
35+
let parsedArray = try InlineArray<0, UInt8>(parsing: &span)
36+
#expect(parsedArray.isEmpty)
37+
#expect(span.count == testBuffer.count)
38+
}
39+
40+
// 'byteCount' greater than available bytes
41+
testBuffer.withParserSpan { span in
42+
#expect(throws: ParsingError.self) {
43+
_ = try InlineArray<100, UInt8>(parsing: &span)
44+
}
45+
#expect(span.count == testBuffer.count)
46+
}
47+
}
48+
49+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
50+
@Test
51+
func parseArrayOfFixedSize() throws {
52+
// Arrays of fixed-size integers
53+
try testBuffer.withParserSpan { span in
54+
let parsedArray = try InlineArray<5, UInt8>(parsing: &span) { input in
55+
try UInt8(parsing: &input)
56+
}
57+
#expect(parsedArray == testBuffer.prefix(5))
58+
#expect(span.count == 5)
59+
60+
// Parse two UInt16 values
61+
let parsedArray2 = try InlineArray<2, UInt16>(parsing: &span) { input in
62+
try UInt16(parsingBigEndian: &input)
63+
}
64+
#expect(parsedArray2 == [0x0607, 0x0809])
65+
#expect(span.count == 1)
66+
67+
// Fail to parse one UInt16
68+
#expect(throws: ParsingError.self) {
69+
_ = try InlineArray<1, UInt16>(parsing: &span) { input in
70+
try UInt16(parsingBigEndian: &input)
71+
}
72+
}
73+
74+
let lastByte = try InlineArray<1, UInt8>(
75+
parsing: &span,
76+
parser: UInt8.init(parsing:))
77+
#expect(lastByte == [0x0A])
78+
#expect(span.count == 0)
79+
}
80+
81+
// Parsing count = 0 always succeeds
82+
try testBuffer.withParserSpan { span in
83+
let parsedArray1 = try InlineArray<0, UInt64>(parsing: &span) { input in
84+
try UInt64(parsingBigEndian: &input)
85+
}
86+
#expect(parsedArray1.isEmpty)
87+
#expect(span.count == testBuffer.count)
88+
89+
try span.seek(toOffsetFromEnd: 0)
90+
let parsedArray2 = try InlineArray<0, UInt64>(parsing: &span) { input in
91+
try UInt64(parsingBigEndian: &input)
92+
}
93+
#expect(parsedArray2.isEmpty)
94+
#expect(span.count == 0)
95+
}
96+
}
97+
98+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
99+
@Test
100+
func parseArrayOfCustomTypes() throws {
101+
// Define a custom struct to test with
102+
struct CustomType: Equatable {
103+
var value: UInt8
104+
var doubled: UInt8
105+
106+
init(parsing input: inout ParserSpan) throws {
107+
self.value = try UInt8(parsing: &input)
108+
guard let d = self.value *? 2 else {
109+
throw TestError("Doubled value too large for UInt8")
110+
}
111+
self.doubled = d
112+
}
113+
114+
init(_ value: UInt8) {
115+
self.value = value
116+
self.doubled = value * 2
117+
}
118+
}
119+
120+
try testBuffer.withParserSpan { span in
121+
let parsedArray = try InlineArray<5, CustomType>(parsing: &span) {
122+
input in
123+
try CustomType(parsing: &input)
124+
}
125+
126+
#expect(parsedArray == testBuffer.prefix(5).map(CustomType.init))
127+
#expect(span.count == 5)
128+
}
129+
130+
_ = [0x0f, 0xf0].withParserSpan { span in
131+
#expect(throws: TestError.self) {
132+
try InlineArray<2, CustomType>(
133+
parsing: &span, parser: CustomType.init(parsing:))
134+
}
135+
}
136+
}
137+
138+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
139+
@Test
140+
func parseArrayWithErrorHandling() throws {
141+
struct ValidatedUInt8: Equatable {
142+
var value: UInt8
143+
144+
init(_ v: UInt8) throws {
145+
if v > 5 {
146+
throw TestError("Value \(v) exceeds maximum allowed value of 5")
147+
}
148+
self.value = v
149+
}
150+
151+
init(parsing input: inout ParserSpan) throws {
152+
try self.init(UInt8(parsing: &input))
153+
}
154+
}
155+
156+
try testBuffer.withParserSpan { span in
157+
// This should fail because values in the buffer exceed 5
158+
#expect(throws: TestError.self) {
159+
_ = try InlineArray<10, ValidatedUInt8>(parsing: &span) { input in
160+
try ValidatedUInt8(parsing: &input)
161+
}
162+
}
163+
// Even though the parsing failed, it should have consumed some elements
164+
#expect(span.count < testBuffer.count)
165+
166+
// Reset and try just parsing the valid values
167+
try span.seek(toAbsoluteOffset: 0)
168+
let parsedArray = try InlineArray<5, ValidatedUInt8>(parsing: &span) {
169+
input in
170+
try ValidatedUInt8(parsing: &input)
171+
}
172+
let expectedArray = try testBuffer.prefix(5).map(ValidatedUInt8.init)
173+
#expect(parsedArray == expectedArray)
174+
}
175+
}
176+
}

Tests/BinaryParsingTests/TestingSupport.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ extension Array where Element == UInt8 {
139139
}
140140
}
141141

142+
/// Returns true if an inline array and a sequence of the same element are equivalent.
143+
@available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *)
144+
func == <T: Equatable, let n: Int>(
145+
lhs: InlineArray<n, T>, rhs: some Sequence<T>
146+
) -> Bool {
147+
var iterator = rhs.makeIterator()
148+
for i in 0..<n {
149+
guard lhs[i] == iterator.next() else { return false }
150+
}
151+
guard iterator.next() == nil else { return false }
152+
return true
153+
}
154+
142155
/// A seeded random number generator type.
143156
struct RapidRandom: RandomNumberGenerator {
144157
private var state: UInt64

0 commit comments

Comments
 (0)