Skip to content

Fill out test coverage of BinaryParsing #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 13, 2025
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
2 changes: 2 additions & 0 deletions Scripts/format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ echo "Formatting Swift sources in $(pwd)"
# Run the format / lint commands
git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place
git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel

sed -i '' 's/borrowbuffer/borrow buffer/g' "Sources/BinaryParsing/Parser Types/ParserSpan.swift"
2 changes: 1 addition & 1 deletion Sources/BinaryParsing/Parser Types/ParserRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//
//===----------------------------------------------------------------------===//

public struct ParserRange: Hashable {
public struct ParserRange: Hashable, Sendable {
@usableFromInline
internal var range: Range<Int>

Expand Down
1 change: 1 addition & 0 deletions Sources/BinaryParsing/Parser Types/ParserSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ extension Data: ParserSpanProvider {
#endif

extension ParserSpanProvider where Self: RandomAccessCollection<UInt8> {
@discardableResult
@inlinable
public func withParserSpan<T>(
_ body: (inout ParserSpan) throws(ThrownParsingError) -> T
Expand Down
13 changes: 13 additions & 0 deletions Sources/BinaryParsing/Parser Types/ParsingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ extension ParsingError: CustomStringConvertible {
}
}
}

extension ParsingError.Status: CustomStringConvertible {
public var description: String {
switch self.rawValue {
case .insufficientData:
"insufficient data"
case .invalidValue:
"invalid value"
case .userError:
"user error"
}
}
}
#endif

#if !$Embedded
Expand Down
5 changes: 2 additions & 3 deletions Sources/BinaryParsing/Parser Types/Seeking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,10 @@ extension ParserSpan {
throws(ParsingError)
{
guard let offset = Int(exactly: offset),
(0..._bytes.byteCount).contains(offset)
(0...endPosition).contains(offset)
else {
throw ParsingError(status: .invalidValue, location: startPosition)
}
self._lowerBound = _bytes.byteCount &- offset
self._upperBound = _bytes.byteCount
self._lowerBound = endPosition &- offset
}
}
13 changes: 10 additions & 3 deletions Sources/BinaryParsing/Parser Types/Slicing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension ParserSpan {
public mutating func sliceSpan(byteCount: some FixedWidthInteger)
throws(ParsingError) -> ParserSpan
{
guard let byteCount = Int(exactly: byteCount), count >= 0 else {
guard let byteCount = Int(exactly: byteCount), byteCount >= 0 else {
throw ParsingError(status: .invalidValue, location: startPosition)
}
guard count >= byteCount else {
Expand Down Expand Up @@ -60,18 +60,25 @@ extension ParserSpan {
try sliceSpan(objectStride: objectStride, objectCount: objectCount)
.parserRange
}

@inlinable
@lifetime(&self)
public mutating func sliceRemainingRange() -> ParserRange {
divide(atOffset: self.count).parserRange
}
}

extension ParserSpan {
@inlinable
@lifetime(copy self)
@available(macOS 9999, *)
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *)
public mutating func sliceUTF8Span(byteCount: some FixedWidthInteger)
throws(ParsingError) -> UTF8Span
{
let rawSpan = try sliceSpan(byteCount: byteCount).bytes
do {
return try UTF8Span(validating: Span<UInt8>(_bytes: rawSpan))
let span = Span<UInt8>(_bytes: rawSpan)
return try UTF8Span(validating: span)
} catch {
throw ParsingError(status: .userError, location: startPosition)
}
Expand Down
244 changes: 244 additions & 0 deletions Tests/BinaryParsingTests/ArrayParsingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Binary Parsing open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

import BinaryParsing
import Testing

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

private let emptyBuffer: [UInt8] = []

@Test
func parseRemainingBytes() throws {
try testBuffer.withParserSpan { span in
let parsedArray = try Array(parsingRemainingBytes: &span)
#expect(parsedArray == testBuffer)
#expect(span.count == 0)
}

// Test parsing after consuming part of the buffer
try testBuffer.withParserSpan { span in
try span.seek(toRelativeOffset: 3)
let parsedArray = try Array(parsingRemainingBytes: &span)
#expect(parsedArray[...] == testBuffer.dropFirst(3))
#expect(span.count == 0)
}

// Test with an empty span
try emptyBuffer.withParserSpan { span in
let parsedArray = try [UInt8](parsingRemainingBytes: &span)
#expect(parsedArray.isEmpty)
}
}

@Test
func parseByteCount() throws {
try testBuffer.withParserSpan { span in
let parsedArray = try [UInt8](parsing: &span, byteCount: 5)
#expect(parsedArray[...] == testBuffer.prefix(5))
#expect(span.count == 5)

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

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

// 'byteCount' greater than available bytes
try testBuffer.withParserSpan { span in
#expect(throws: ParsingError.self) {
_ = try [UInt8](parsing: &span, byteCount: testBuffer.count + 1)
}
#expect(span.count == testBuffer.count)
}
}

@Test
func parseArrayOfFixedSize() throws {
// Arrays of fixed-size integers
try testBuffer.withParserSpan { span in
let parsedArray = try Array(parsing: &span, count: 5) { input in
try UInt8(parsing: &input)
}
#expect(parsedArray[...] == testBuffer.prefix(5))
#expect(span.count == 5)

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

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

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

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

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

// Non-'Int' count that would overflow
_ = try testBuffer.withParserSpan { span in
#expect(throws: ParsingError.self) {
_ = try [UInt8](parsing: &span, count: UInt.max) { input in
try UInt8(parsing: &input)
}
}
}
}

@Test
func parseArrayOfCustomTypes() throws {
// Define a custom struct to test with
struct CustomType {
var value: UInt8
var doubled: UInt8

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

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

#expect(parsedArray.map(\.value) == [0x01, 0x02, 0x03, 0x04, 0x05])
#expect(parsedArray.map(\.doubled) == [0x02, 0x04, 0x06, 0x08, 0x0A])
#expect(span.count == 5)
}

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

@Test
func parseAllAvailableElements() throws {
// Parse as UInt8
try testBuffer.withParserSpan { span in
let parsedArray = try Array(parsingAll: &span) { input in
try UInt8(parsing: &input)
}
#expect(parsedArray == testBuffer)
#expect(span.count == 0)
}

// Parse as UInt16
try testBuffer.withParserSpan { span in
let parsedArray = try Array(parsingAll: &span) { input in
try UInt16(parsingBigEndian: &input)
}
#expect(parsedArray == [0x0102, 0x0304, 0x0506, 0x0708, 0x090A])
#expect(span.count == 0)
}

// Parse as UInt16 with recovery
let oddBuffer: [UInt8] = [0x01, 0x02, 0x03, 0x04, 0x05]
try oddBuffer.withParserSpan { span in
let parsedArray = try Array(parsingAll: &span) { input in
do {
return try UInt16(parsingBigEndian: &input)
} catch {
if input.count == 1 {
return try UInt16(parsing: &input, storedAs: UInt8.self)
}
throw error
}
}

// Two complete 'UInt16' values plus one 'UInt16' from the last byte
#expect(parsedArray == [0x0102, 0x0304, 0x0005])
#expect(span.count == 0)
}

// Test with empty buffer
try emptyBuffer.withParserSpan { span in
let parsedArray = try Array(parsingAll: &span) { input in
try UInt8(parsing: &input)
}
#expect(parsedArray.isEmpty)
#expect(span.count == 0)
}
}

@Test
func parseArrayWithErrorHandling() throws {
struct ValidatedUInt8 {
var value: UInt8

init(parsing input: inout ParserSpan) throws {
self.value = try UInt8(parsing: &input)
if value > 5 {
throw TestError("Value \(value) exceeds maximum allowed value of 5")
}
}
}

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

// Reset and try just parsing the valid values
try span.seek(toAbsoluteOffset: 0)
let parsedArray = try Array(parsing: &span, count: 5) { input in
try ValidatedUInt8(parsing: &input)
}
#expect(parsedArray.map(\.value) == [0x01, 0x02, 0x03, 0x04, 0x05])
}
}
}
Loading