Skip to content

Commit 1ad235a

Browse files
committed
Fill out test coverage of BinaryParsing
1 parent d4724d1 commit 1ad235a

16 files changed

+1478
-46
lines changed

Scripts/format.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ echo "Formatting Swift sources in $(pwd)"
1818
# Run the format / lint commands
1919
git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place
2020
git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
21+
22+
sed -i '' 's/borrowbuffer/borrow buffer/g' "Sources/BinaryParsing/Parser Types/ParserSpan.swift"

Sources/BinaryParsing/Parser Types/ParserRange.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
public struct ParserRange: Hashable {
12+
public struct ParserRange: Hashable, Sendable {
1313
@usableFromInline
1414
internal var range: Range<Int>
1515

Sources/BinaryParsing/Parser Types/ParserSource.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ extension Data: ParserSpanProvider {
122122
#endif
123123

124124
extension ParserSpanProvider where Self: RandomAccessCollection<UInt8> {
125+
@discardableResult
125126
@inlinable
126127
public func withParserSpan<T>(
127128
_ body: (inout ParserSpan) throws(ThrownParsingError) -> T

Sources/BinaryParsing/Parser Types/ParsingError.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ extension ParsingError: CustomStringConvertible {
9494
}
9595
}
9696
}
97+
98+
extension ParsingError.Status: CustomStringConvertible {
99+
public var description: String {
100+
switch self.rawValue {
101+
case .insufficientData:
102+
"insufficient data"
103+
case .invalidValue:
104+
"invalid value"
105+
case .userError:
106+
"user error"
107+
}
108+
}
109+
}
97110
#endif
98111

99112
#if !$Embedded

Sources/BinaryParsing/Parser Types/Seeking.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,10 @@ extension ParserSpan {
9797
throws(ParsingError)
9898
{
9999
guard let offset = Int(exactly: offset),
100-
(0..._bytes.byteCount).contains(offset)
100+
(0...endPosition).contains(offset)
101101
else {
102102
throw ParsingError(status: .invalidValue, location: startPosition)
103103
}
104-
self._lowerBound = _bytes.byteCount &- offset
105-
self._upperBound = _bytes.byteCount
104+
self._lowerBound = endPosition &- offset
106105
}
107106
}

Sources/BinaryParsing/Parser Types/Slicing.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension ParserSpan {
1515
public mutating func sliceSpan(byteCount: some FixedWidthInteger)
1616
throws(ParsingError) -> ParserSpan
1717
{
18-
guard let byteCount = Int(exactly: byteCount), count >= 0 else {
18+
guard let byteCount = Int(exactly: byteCount), byteCount >= 0 else {
1919
throw ParsingError(status: .invalidValue, location: startPosition)
2020
}
2121
guard count >= byteCount else {
@@ -60,18 +60,25 @@ extension ParserSpan {
6060
try sliceSpan(objectStride: objectStride, objectCount: objectCount)
6161
.parserRange
6262
}
63+
64+
@inlinable
65+
@lifetime(&self)
66+
public mutating func sliceRemainingRange() -> ParserRange {
67+
divide(atOffset: self.count).parserRange
68+
}
6369
}
6470

6571
extension ParserSpan {
6672
@inlinable
6773
@lifetime(copy self)
68-
@available(macOS 9999, *)
74+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *)
6975
public mutating func sliceUTF8Span(byteCount: some FixedWidthInteger)
7076
throws(ParsingError) -> UTF8Span
7177
{
7278
let rawSpan = try sliceSpan(byteCount: byteCount).bytes
7379
do {
74-
return try UTF8Span(validating: Span<UInt8>(_bytes: rawSpan))
80+
let span = Span<UInt8>(_bytes: rawSpan)
81+
return try UTF8Span(validating: span)
7582
} catch {
7683
throw ParsingError(status: .userError, location: startPosition)
7784
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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 ArrayParsingTests {
16+
private let testBuffer: [UInt8] = [
17+
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
18+
]
19+
20+
private let emptyBuffer: [UInt8] = []
21+
22+
@Test
23+
func parseRemainingBytes() throws {
24+
try testBuffer.withParserSpan { span in
25+
let parsedArray = try Array(parsingRemainingBytes: &span)
26+
#expect(parsedArray == testBuffer)
27+
#expect(span.count == 0)
28+
}
29+
30+
// Test parsing after consuming part of the buffer
31+
try testBuffer.withParserSpan { span in
32+
try span.seek(toRelativeOffset: 3)
33+
let parsedArray = try Array(parsingRemainingBytes: &span)
34+
#expect(parsedArray[...] == testBuffer.dropFirst(3))
35+
#expect(span.count == 0)
36+
}
37+
38+
// Test with an empty span
39+
try emptyBuffer.withParserSpan { span in
40+
let parsedArray = try [UInt8](parsingRemainingBytes: &span)
41+
#expect(parsedArray.isEmpty)
42+
}
43+
}
44+
45+
@Test
46+
func parseByteCount() throws {
47+
try testBuffer.withParserSpan { span in
48+
let parsedArray = try [UInt8](parsing: &span, byteCount: 5)
49+
#expect(parsedArray[...] == testBuffer.prefix(5))
50+
#expect(span.count == 5)
51+
52+
let parsedArray2 = try [UInt8](parsing: &span, byteCount: 3)
53+
#expect(parsedArray2[...] == testBuffer.dropFirst(5).prefix(3))
54+
#expect(span.count == 2)
55+
}
56+
57+
// 'byteCount' == 0
58+
try testBuffer.withParserSpan { span in
59+
let parsedArray = try [UInt8](parsing: &span, byteCount: 0)
60+
#expect(parsedArray.isEmpty)
61+
#expect(span.count == testBuffer.count)
62+
}
63+
64+
// 'byteCount' greater than available bytes
65+
try testBuffer.withParserSpan { span in
66+
#expect(throws: ParsingError.self) {
67+
_ = try [UInt8](parsing: &span, byteCount: testBuffer.count + 1)
68+
}
69+
#expect(span.count == testBuffer.count)
70+
}
71+
}
72+
73+
@Test
74+
func parseArrayOfFixedSize() throws {
75+
// Arrays of fixed-size integers
76+
try testBuffer.withParserSpan { span in
77+
let parsedArray = try Array(parsing: &span, count: 5) { input in
78+
try UInt8(parsing: &input)
79+
}
80+
#expect(parsedArray[...] == testBuffer.prefix(5))
81+
#expect(span.count == 5)
82+
83+
// Parse two UInt16 values
84+
let parsedArray2 = try Array(parsing: &span, count: 2) { input in
85+
try UInt16(parsingBigEndian: &input)
86+
}
87+
#expect(parsedArray2 == [0x0607, 0x0809])
88+
#expect(span.count == 1)
89+
90+
// Fail to parse one UInt16
91+
#expect(throws: ParsingError.self) {
92+
_ = try Array(parsing: &span, count: 1) { input in
93+
try UInt16(parsingBigEndian: &input)
94+
}
95+
}
96+
97+
let lastByte = try Array(
98+
parsing: &span,
99+
count: 1,
100+
parser: UInt8.init(parsing:))
101+
#expect(lastByte == [0x0A])
102+
#expect(span.count == 0)
103+
}
104+
105+
// Parsing count = 0 always succeeds
106+
try testBuffer.withParserSpan { span in
107+
let parsedArray1 = try Array(parsing: &span, count: 0) { input in
108+
try UInt64(parsingBigEndian: &input)
109+
}
110+
#expect(parsedArray1.isEmpty)
111+
#expect(span.count == testBuffer.count)
112+
113+
try span.seek(toOffsetFromEnd: 0)
114+
let parsedArray2 = try Array(parsing: &span, count: 0) { input in
115+
try UInt64(parsingBigEndian: &input)
116+
}
117+
#expect(parsedArray2.isEmpty)
118+
#expect(span.count == 0)
119+
}
120+
121+
// Non-'Int' count that would overflow
122+
_ = try testBuffer.withParserSpan { span in
123+
#expect(throws: ParsingError.self) {
124+
_ = try [UInt8](parsing: &span, count: UInt.max) { input in
125+
try UInt8(parsing: &input)
126+
}
127+
}
128+
}
129+
}
130+
131+
@Test
132+
func parseArrayOfCustomTypes() throws {
133+
// Define a custom struct to test with
134+
struct CustomType {
135+
var value: UInt8
136+
var doubled: UInt8
137+
138+
init(parsing input: inout ParserSpan) throws {
139+
self.value = try UInt8(parsing: &input)
140+
guard let d = self.value *? 2 else {
141+
throw TestError("Doubled value too large for UInt8")
142+
}
143+
self.doubled = d
144+
}
145+
}
146+
147+
try testBuffer.withParserSpan { span in
148+
let parsedArray = try Array(parsing: &span, count: 5) { input in
149+
try CustomType(parsing: &input)
150+
}
151+
152+
#expect(parsedArray.map(\.value) == [0x01, 0x02, 0x03, 0x04, 0x05])
153+
#expect(parsedArray.map(\.doubled) == [0x02, 0x04, 0x06, 0x08, 0x0A])
154+
#expect(span.count == 5)
155+
}
156+
157+
_ = try [0x0f, 0xf0].withParserSpan { span in
158+
#expect(throws: TestError.self) {
159+
try Array(parsingAll: &span, parser: CustomType.init(parsing:))
160+
}
161+
}
162+
}
163+
164+
@Test
165+
func parseAllAvailableElements() throws {
166+
// Parse as UInt8
167+
try testBuffer.withParserSpan { span in
168+
let parsedArray = try Array(parsingAll: &span) { input in
169+
try UInt8(parsing: &input)
170+
}
171+
#expect(parsedArray == testBuffer)
172+
#expect(span.count == 0)
173+
}
174+
175+
// Parse as UInt16
176+
try testBuffer.withParserSpan { span in
177+
let parsedArray = try Array(parsingAll: &span) { input in
178+
try UInt16(parsingBigEndian: &input)
179+
}
180+
#expect(parsedArray == [0x0102, 0x0304, 0x0506, 0x0708, 0x090A])
181+
#expect(span.count == 0)
182+
}
183+
184+
// Parse as UInt16 with recovery
185+
let oddBuffer: [UInt8] = [0x01, 0x02, 0x03, 0x04, 0x05]
186+
try oddBuffer.withParserSpan { span in
187+
let parsedArray = try Array(parsingAll: &span) { input in
188+
do {
189+
return try UInt16(parsingBigEndian: &input)
190+
} catch {
191+
if input.count == 1 {
192+
return try UInt16(parsing: &input, storedAs: UInt8.self)
193+
}
194+
throw error
195+
}
196+
}
197+
198+
// Two complete 'UInt16' values plus one 'UInt16' from the last byte
199+
#expect(parsedArray == [0x0102, 0x0304, 0x0005])
200+
#expect(span.count == 0)
201+
}
202+
203+
// Test with empty buffer
204+
try emptyBuffer.withParserSpan { span in
205+
let parsedArray = try Array(parsingAll: &span) { input in
206+
try UInt8(parsing: &input)
207+
}
208+
#expect(parsedArray.isEmpty)
209+
#expect(span.count == 0)
210+
}
211+
}
212+
213+
@Test
214+
func parseArrayWithErrorHandling() throws {
215+
struct ValidatedUInt8 {
216+
var value: UInt8
217+
218+
init(parsing input: inout ParserSpan) throws {
219+
self.value = try UInt8(parsing: &input)
220+
if value > 5 {
221+
throw TestError("Value \(value) exceeds maximum allowed value of 5")
222+
}
223+
}
224+
}
225+
226+
try testBuffer.withParserSpan { span in
227+
// This should fail because values in the buffer exceed 5
228+
#expect(throws: TestError.self) {
229+
_ = try Array(parsing: &span, count: testBuffer.count) { input in
230+
try ValidatedUInt8(parsing: &input)
231+
}
232+
}
233+
// Even though the parsing failed, it should have consumed some elements
234+
#expect(span.count < testBuffer.count)
235+
236+
// Reset and try just parsing the valid values
237+
try span.seek(toAbsoluteOffset: 0)
238+
let parsedArray = try Array(parsing: &span, count: 5) { input in
239+
try ValidatedUInt8(parsing: &input)
240+
}
241+
#expect(parsedArray.map(\.value) == [0x01, 0x02, 0x03, 0x04, 0x05])
242+
}
243+
}
244+
}

0 commit comments

Comments
 (0)