From 14dea4c3d7f3de5607b7fcb005242f9acdc255bb Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Sat, 14 Jun 2025 12:11:41 -0500 Subject: [PATCH] Docs docs docs --- README.md | 5 +- .../Articles/ArrayParsers.md | 16 ++ .../Articles/GettingStarted.md | 150 +++++++++++++ .../Articles/IntegerParsers.md | 43 ++++ .../Articles/MiscellaneousParsers.md | 22 ++ .../Articles/OptionalOperations.md | 50 +++++ .../Articles/StringParsers.md | 16 ++ .../Articles/ThrowingOperations.md | 32 +++ .../Documentation.docc/BinaryParsing.md | 48 +++++ .../Extensions/ParserSpan.md | 52 +++++ .../Operations/OptionalOperations.swift | 10 +- .../Operations/Optionators.swift | 79 ++++--- .../Operations/ThrowingOperations.swift | 30 +++ .../Parser Types/Endianness.swift | 20 ++ .../Parser Types/ParserRange.swift | 16 ++ .../Parser Types/ParserSource.swift | 25 +++ .../Parser Types/ParserSpan.swift | 14 +- .../Parser Types/ParsingError.swift | 4 +- .../BinaryParsing/Parser Types/Seeking.swift | 201 ++++++++++++++++++ .../BinaryParsing/Parser Types/Slicing.swift | 104 +++++++++ Sources/BinaryParsing/Parsers/Array.swift | 92 +++++++- Sources/BinaryParsing/Parsers/Integer.swift | 192 ++++++++++++++++- .../Parsers/IntegerProtocols.swift | 8 +- Sources/BinaryParsing/Parsers/Range.swift | 72 +++++++ Sources/BinaryParsing/Parsers/String.swift | 39 +++- Sources/BinaryParsingMacros/Extensions.swift | 10 +- .../IntegerParsingTests.swift | 4 +- 27 files changed, 1297 insertions(+), 57 deletions(-) create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/ArrayParsers.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/GettingStarted.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/IntegerParsers.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/MiscellaneousParsers.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/OptionalOperations.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/StringParsers.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Articles/ThrowingOperations.md create mode 100644 Sources/BinaryParsing/Documentation.docc/BinaryParsing.md create mode 100644 Sources/BinaryParsing/Documentation.docc/Extensions/ParserSpan.md diff --git a/README.md b/README.md index fb65898..8391971 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,10 @@ import BinaryParsing extension QOI.Header { init(parsing input: inout ParserSpan) throws { - try #magicNumber("qoif", parsing: &input) + let magic = try UInt32(parsingBigEndian: &input) + guard magic == 0x71_6f_69_66 else { + throw QOIError() + } // Loading 'Int' requires a byte count or guaranteed-size storage self.width = try Int(parsing: &input, storedAsBigEndian: UInt32.self) diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/ArrayParsers.md b/Sources/BinaryParsing/Documentation.docc/Articles/ArrayParsers.md new file mode 100644 index 0000000..753b200 --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/ArrayParsers.md @@ -0,0 +1,16 @@ +# Array Parsers + +Parse arrays of bytes or other values. + +## Topics + +### Byte array parsers + +- ``Swift/Array/init(parsingRemainingBytes:)`` +- ``Swift/Array/init(parsing:byteCount:)`` + +### General array parsers + +- ``Swift/Array/init(parsingAll:parser:)`` +- ``Swift/Array/init(parsing:count:parser:)-(_,FixedWidthInteger,_)`` +- ``Swift/Array/init(parsing:count:parser:)-(_,Int,_)`` diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/GettingStarted.md b/Sources/BinaryParsing/Documentation.docc/Articles/GettingStarted.md new file mode 100644 index 0000000..9f00641 --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/GettingStarted.md @@ -0,0 +1,150 @@ +# Getting Started with BinaryParsing + +Get up to speed with a library designed to make parsing binary data safe, efficient, and easy to understand. + +## Overview + +The BinaryParsing library provides a comprehensive set of tools for safely parsing binary data in Swift. The library provides the ``ParserSpan`` type, a consumable, memory-safe view into binary data, and defines a convention for writing concise, composable parsing functions. + +Using the library's tools — including the span type, parser primitives, and operators for working with newly parsed values — you can prevent common pitfalls like buffer overruns, integer overflows, and type confusion that can lead to security vulnerabilities or crashes. + +### A span type for parsing + +A ``ParserSpan`` is a view into binary data that tracks your current position and the remaining number of bytes. All the provided parsers consume data from the start of the span, shrinking its size as they produce values. Unlike unsafe pointer operations, `ParserSpan` automatically prevents you from reading past the end of your data. + +### Library-provided parsers + +The library provides parsers for standard library integers, strings, ranges, and arrays of bytes or custom-parsed types. The convention for these is an initializer with an `inout ParserSpan` parameter, along with any other configuration parameters that are required. These parsers all throw a `ParsingError`, and throw when encoutering memory safety, type safety, or integer overflow errors. + +For example, the parsing initializers for `Int` take the parser span as well as storage type or storage size and endianness: + +```swift +let values = try myData.withParserSpan { input in + let value1 = try Int(parsing: &input, storedAsBigEndian: Int32.self) + let value2 = try Int(parsing: &input, byteCount: 4, endianness: .big) +} +``` + +Designing parser APIs as initializers is only a convention. If it feels more natural to write some parsers as free functions, static functions, or even as a parsing type, that's okay! You'll find cases of each of these in the project's [Examples directory][examples]. + +## Example: QOI Header + +Let's explore BinaryParsing through a real-world example: parsing the header for an image stored in the QOI ([Quite OK Image][qoi]) format. QOI is a simple lossless image format that demonstrates many common patterns in binary parsing. + +### The QOI header structure + +A QOI file begins with a 14-byte header, as shown in the specification: + +```c +qoi_header { + char magic[4]; // magic bytes "qoif" + uint32_t width; // image width in pixels (BE) + uint32_t height; // image height in pixels (BE) + uint8_t channels; // 3 = RGB, 4 = RGBA + uint8_t colorspace; // 0 = sRGB with linear alpha + // 1 = all channels linear +}; +``` + +### Parser implementation + +Our declaration for the header in Swift corresponds to the specification, with `width` and `height` defined as `Int` and custom enumerations for the channels and colorspace: + +```swift +extension QOI { + struct Header { + var width: Int + var height: Int + var channels: Channels + var colorspace: ColorSpace + } + + enum Channels: UInt8 { + case rgb = 3, rgba = 4 + } + + enum ColorSpace: UInt8 { + case sRGB = 0, linear = 1 + } +} +``` + +The parsing initializer follows the convention set by the library, with an `inout ParserSpan` parameter: + +```swift +extension QOI.Header { + init(parsing input: inout ParserSpan) throws { + // Parsing goes here! + } +} +``` + +Next, we'll walk through the implementation of that initializer, line by line, to look at the safety and ease of use in the BinaryParsing library APIs. + +#### Magic number validation + +The first value in the binary data is a "magic number" – a common practice in binary formats that acts as a quick check that you're reading the right kind of file and working with the correct endianness. The code uses a `UInt32` initialzer to load a 32-bit big-endian value, and then checks it for correctness using `guard`: + +```swift +let magic = try UInt32(parsingBigEndian: &input) +guard magic == 0x71_6f_69_66 else { + throw QOIError() +} +``` + +#### Parsing dimensions + +Next, the width and height are also stored as 32-bit values, but we want to use them in our type as `Int` values. Instead of parsing `UInt32` values and _then_ converting them to `Int`, we'll use an `Int` parser that specifies the storage type, handling any possible overflow: + +```swift +self.width = try Int(parsing: &input, storedAsBigEndian: UInt32.self) +self.height = try Int(parsing: &input, storedAsBigEndian: UInt32.self) +``` + +### Parsing `RawRepresentable` types + +Because the `Channels` and `ColorSpace` enumerations are backed by a `FixedWidthInteger` type, the library provides parsers that load and validate the parsed values. These parsers throw an error if the parsed value isn't one of the type's declared cases: + +```swift +self.channels = try Channels(parsing: &input) +self.colorspace = try ColorSpace(parsing: &input) +``` + +### Safe arithmetic + +After parsing all of the header's values, the last step is to perform some validation. Using the library's optional multiplication operator (`*?`) allows for concise arithmetic while preventing integer overflow errors: + +```swift +guard let pixelCount = width *? height, + pixelCount <= maxPixelCount, + width > 0, height > 0 +else { throw QOIError() } +``` + +### Bringing it together + +The full parser implementation, as shown below, protects against buffer overruns, integer overflow, arithmetic overflow, type invalidity, and pointer lifetime errors: + +```swift +extension QOI.Header { + init(parsing input: inout ParserSpan) throws { + let magic = try UInt32(parsingBigEndian: &input) + guard magic == 0x71_6f_69_66 else { + throw QOIError() + } + + self.width = try Int(parsing: &input, storedAsBigEndian: UInt32.self) + self.height = try Int(parsing: &input, storedAsBigEndian: UInt32.self) + self.channels = try Channels(parsing: &input) + self.colorspace = try ColorSpace(parsing: &input) + + guard let pixelCount = width *? height, + pixelCount <= maxPixelCount, + width > 0, height > 0 + else { throw QOIError() } + } +} +``` + +[qoi]: https://qoiformat.org/ +[examples]: https://github.com/apple/swift-binary-parsing/tree/main/Examples diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/IntegerParsers.md b/Sources/BinaryParsing/Documentation.docc/Articles/IntegerParsers.md new file mode 100644 index 0000000..bf43604 --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/IntegerParsers.md @@ -0,0 +1,43 @@ +# Integer Parsers + +Parse standard library integer types. + +## Overview + +The `BinaryParsing` integer parsers provide control over three different aspects of loading integers from raw data: + +- _Size:_ The size of the data in memory can be specified in three different ways. Use an integer type's direct parsing initializer, like `UInt16(parsingBigEndian:)`, to load from the exact size of the integer; use a parser with a `byteCount` parameter to specify an exact number of bytes; or use a parser like `Int(parsing:storedAsBigEndian:)` to load and convert from another integer's size in memory. +- _Endianness_: The endianness of a value in memory can be specified either by choosing a parsing initializer with the required endianness or by passing an ``Endianness`` value to a parser. Note that endianness is not relevant when parsing a single-byte integer or an integer stored as a single byte. +- _Signedness_: The signedness of the parsed value is chosen by the type being parsed or, for the parsers like `Int(parsing:storedAs:)`, by the storage type of the parsed value. + +## Topics + +### Fixed-size parsers + +- ``SingleByteInteger/init(parsing:)`` +- ``MultiByteInteger/init(parsingBigEndian:)`` +- ``MultiByteInteger/init(parsingLittleEndian:)`` +- ``MultiByteInteger/init(parsing:endianness:)`` + +### Byte count-based parsers + +- ``Swift/FixedWidthInteger/init(parsingBigEndian:byteCount:)`` +- ``Swift/FixedWidthInteger/init(parsingLittleEndian:byteCount:)`` +- ``Swift/FixedWidthInteger/init(parsing:endianness:byteCount:)`` + +### Parsing and converting + +- ``Swift/FixedWidthInteger/init(parsing:storedAs:)`` +- ``Swift/FixedWidthInteger/init(parsing:storedAsBigEndian:)`` +- ``Swift/FixedWidthInteger/init(parsing:storedAsLittleEndian:)`` +- ``Swift/FixedWidthInteger/init(parsing:storedAs:endianness:)`` + +### Endianness + +- ``Endianness`` + +### Supporting protocols + +- ``SingleByteInteger`` +- ``MultiByteInteger`` +- ``PlatformWidthInteger`` diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/MiscellaneousParsers.md b/Sources/BinaryParsing/Documentation.docc/Articles/MiscellaneousParsers.md new file mode 100644 index 0000000..5bf7dcb --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/MiscellaneousParsers.md @@ -0,0 +1,22 @@ +# Miscellaneous Parsers + +Parse ranges and custom raw representable types. + +## Topics + +### Range parsers + +- ``Swift/Range/init(parsingStartAndEnd:boundsParser:)-(_,(ParserSpan)(ParsingError)->Bound)`` +- ``Swift/Range/init(parsingStartAndCount:parser:)-(_,(ParserSpan)(ParsingError)->Bound)`` +- ``Swift/ClosedRange/init(parsingStartAndEnd:boundsParser:)-(_,(ParserSpan)(ParsingError)->Bound)`` + +### `RawRepresentable` parsers + +- ``Swift/RawRepresentable/init(parsing:)`` +- ``Swift/RawRepresentable/init(parsingBigEndian:)`` +- ``Swift/RawRepresentable/init(parsingLittleEndian:)`` +- ``Swift/RawRepresentable/init(parsing:endianness:)`` +- ``Swift/RawRepresentable/init(parsing:storedAs:)`` +- ``Swift/RawRepresentable/init(parsing:storedAsBigEndian:)`` +- ``Swift/RawRepresentable/init(parsing:storedAsLittleEndian:)`` +- ``Swift/RawRepresentable/init(parsing:storedAs:endianness:)`` diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/OptionalOperations.md b/Sources/BinaryParsing/Documentation.docc/Articles/OptionalOperations.md new file mode 100644 index 0000000..59b703f --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/OptionalOperations.md @@ -0,0 +1,50 @@ +# Optional Operations + +Safely perform calculations with optional-producing operators. + +## Overview + +Optional operators provide a way to seamlessly work with newly parsed +values without risk of integer overflow or other common errors that +may result in a runtime error. + +For example, the following code parses two values from a ``ParserSpan``, +and then uses them to create a range: + +```swift +let start = try UInt16(parsingBigEndian: &input) +let count = try UInt8(parsing: &input) +guard let range = start ..)`` +- ``Swift/Collection/subscript(ifInBounds:)-(Range)`` diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/StringParsers.md b/Sources/BinaryParsing/Documentation.docc/Articles/StringParsers.md new file mode 100644 index 0000000..86280bb --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/StringParsers.md @@ -0,0 +1,16 @@ +# String Parsers + +Parse strings of different lengths and encodings. + +## Topics + +### UTF-8 parsers + +- ``Swift/String/init(parsingNulTerminated:)`` +- ``Swift/String/init(parsingUTF8:)`` +- ``Swift/String/init(parsingUTF8:count:)`` + +### UTF-16 parsers + +- ``Swift/String/init(parsingUTF16:)`` +- ``Swift/String/init(parsingUTF16:codeUnitCount:)`` diff --git a/Sources/BinaryParsing/Documentation.docc/Articles/ThrowingOperations.md b/Sources/BinaryParsing/Documentation.docc/Articles/ThrowingOperations.md new file mode 100644 index 0000000..dbd1b10 --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Articles/ThrowingOperations.md @@ -0,0 +1,32 @@ +# Throwing Operations + +Use throwing variations of arithmetic methods, integer conversions, and collection subscripting. + +## Overview + +## Topics + +### Arithmetic operations + +- ``Swift/FixedWidthInteger/addingThrowingOnOverflow(_:)`` +- ``Swift/FixedWidthInteger/subtractingThrowingOnOverflow(_:)`` +- ``Swift/FixedWidthInteger/multipliedThrowingOnOverflow(by:)`` +- ``Swift/FixedWidthInteger/dividedThrowingOnOverflow(by:)`` +- ``Swift/FixedWidthInteger/remainderThrowingOnOverflow(dividingBy:)`` + +### Assigning arithmetic operations + +- ``Swift/FixedWidthInteger/addThrowingOnOverflow(_:)`` +- ``Swift/FixedWidthInteger/subtractThrowingOnOverflow(_:)`` +- ``Swift/FixedWidthInteger/multiplyThrowingOnOverflow(by:)`` +- ``Swift/FixedWidthInteger/divideThrowingOnOverflow(by:)`` +- ``Swift/FixedWidthInteger/formRemainderThrowingOnOverflow(dividingBy:)`` + +### Integer conversion + +- ``Swift/BinaryInteger/init(throwingOnOverflow:)`` + +### Collection subscripting + +- ``Swift/Collection/subscript(throwing:)->Self.Element`` +- ``Swift/Collection/subscript(throwing:)->Self.SubSequence`` diff --git a/Sources/BinaryParsing/Documentation.docc/BinaryParsing.md b/Sources/BinaryParsing/Documentation.docc/BinaryParsing.md new file mode 100644 index 0000000..53a4c46 --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/BinaryParsing.md @@ -0,0 +1,48 @@ +# ``BinaryParsing`` + +A library for building safe, efficient binary parsers in Swift. + +## Overview + +The `BinaryParsing` library provides a set of tools for safely parsing binary +data, while managing type and memory safety and eliminating common value-based +undefined behavior, such as integer overflow. The library provides: + +- ``ParserSpan`` and ``ParserRange``: a raw span that is designed for efficient + consumption of binary data, and a range type that represents a portion of that + span for deferred processing. A `ParserSpan` is most often consumed from the + front, and also supports seeking operations throughout the span. +- Parsing initializers for standard library integer types, strings, arrays, and + ranges, that specifically enable safe parsing practices. The library also + provides parsing initializers that validate the result for `RawRepresentable` + types. +- Optional-producing operators and throwing methods for common arithmetic and + other operations, for calculations with untrusted parsed values. +- Adapters for data and collection types to make parsing simple at the call + site. + + +## Topics + +### Essentials + +- +- ``ParserSpan`` +- ``ParserRange`` + +### Parsing tools + +- +- +- +- + +### Working with untrusted values + +- +- + +### Error handling + +- ``ParsingError`` +- ``ThrownParsingError`` diff --git a/Sources/BinaryParsing/Documentation.docc/Extensions/ParserSpan.md b/Sources/BinaryParsing/Documentation.docc/Extensions/ParserSpan.md new file mode 100644 index 0000000..b2791d4 --- /dev/null +++ b/Sources/BinaryParsing/Documentation.docc/Extensions/ParserSpan.md @@ -0,0 +1,52 @@ +# ``ParserSpan`` + +A non-owning, non-escaping view for parsing binary data. + +## Overview + + + +## Topics + +### Inspecting a Parser Span + +- ``count`` +- ``isEmpty`` +- ``startPosition`` +- ``endPosition`` +- ``bytes`` + +### Slicing a Range + +- ``sliceRange(byteCount:)`` +- ``sliceRange(objectStride:objectCount:)`` +- ``sliceRemainingRange()`` + +### Slicing a Span + +- ``sliceSpan(byteCount:)`` +- ``sliceSpan(objectStride:objectCount:)`` +- ``sliceUTF8Span(byteCount:)`` + +### Seeking to a Range + +- ``parserRange`` +- ``seek(toRange:)`` +- ``seeking(toRange:)`` + +### Seeking to a Relative Offset + +- ``seek(toRelativeOffset:)`` +- ``seek(toOffsetFromEnd:)`` +- ``seeking(toRelativeOffset:)`` +- ``seeking(toOffsetFromEnd:)`` + +### Seeking to an Absolute Offset + +- ``seek(toAbsoluteOffset:)`` +- ``seeking(toAbsoluteOffset:)`` + +### Advanced Tools + +- ``atomically(_:)`` +- ``withUnsafeBytes(_:)`` diff --git a/Sources/BinaryParsing/Operations/OptionalOperations.swift b/Sources/BinaryParsing/Operations/OptionalOperations.swift index 357af06..828135a 100644 --- a/Sources/BinaryParsing/Operations/OptionalOperations.swift +++ b/Sources/BinaryParsing/Operations/OptionalOperations.swift @@ -10,6 +10,8 @@ //===----------------------------------------------------------------------===// extension Collection { + /// Returns the element at the given index, or `nil` if the index is out of + /// bounds. @inlinable public subscript(ifInBounds i: Index) -> Element? { guard (startIndex..) -> SubSequence? { guard range.lowerBound >= startIndex, range.upperBound <= endIndex @@ -27,6 +31,8 @@ extension Collection { } extension Collection where Index == Int { + /// Returns the element at the given index after converting to `Int`, or + /// `nil` if the index is out of bounds. @_alwaysEmitIntoClient public subscript(ifInBounds i: some FixedWidthInteger) -> Element? { guard let i = Int(exactly: i), (startIndex..(ifInBounds bounds: Range) -> SubSequence? { + public subscript(ifInBounds bounds: Range) -> SubSequence? { guard let low = Int(exactly: bounds.lowerBound), let high = Int(exactly: bounds.upperBound), low >= startIndex, high <= endIndex else { diff --git a/Sources/BinaryParsing/Operations/Optionators.swift b/Sources/BinaryParsing/Operations/Optionators.swift index 1875b34..55d74be 100644 --- a/Sources/BinaryParsing/Operations/Optionators.swift +++ b/Sources/BinaryParsing/Operations/Optionators.swift @@ -24,45 +24,55 @@ infix operator .. Self { + public static func +? (a: Self, b: Self) -> Self { guard let a, let b else { return nil } - guard case (let r, false) = a.multipliedReportingOverflow(by: b) else { - return nil - } + guard case (let r, false) = a.addingReportingOverflow(b) else { return nil } return r } + /// Subtracts one value from another and produces their difference, if the + /// values are non-`nil` and the difference is representable. @inlinable @inline(__always) - public static func /? (a: Self, b: Self) -> Self { + public static func -? (a: Self, b: Self) -> Self { guard let a, let b else { return nil } - guard case (let r, false) = a.dividedReportingOverflow(by: b) else { + guard case (let r, false) = a.subtractingReportingOverflow(b) else { return nil } return r } + /// Multiplies two values and produces their product, if the values are + /// non-`nil` and the product is representable. @inlinable @inline(__always) - public static func %? (a: Self, b: Self) -> Self { + public static func *? (a: Self, b: Self) -> Self { guard let a, let b else { return nil } - guard case (let r, false) = a.remainderReportingOverflow(dividingBy: b) - else { return nil } + guard case (let r, false) = a.multipliedReportingOverflow(by: b) else { + return nil + } return r } + /// Returns the quotient of dividing the first value by the second, if the + /// values are non-`nil` and the quotient is representable. @inlinable @inline(__always) - public static func +? (a: Self, b: Self) -> Self { + public static func /? (a: Self, b: Self) -> Self { guard let a, let b else { return nil } - guard case (let r, false) = a.addingReportingOverflow(b) else { return nil } + guard case (let r, false) = a.dividedReportingOverflow(by: b) else { + return nil + } return r } + /// Returns the remainder of dividing the first value by the second, if the + /// values are non-`nil` and the remainder is representable. @inlinable @inline(__always) - public static func -? (a: Self, b: Self) -> Self { + public static func %? (a: Self, b: Self) -> Self { guard let a, let b else { return nil } - guard case (let r, false) = a.subtractingReportingOverflow(b) else { - return nil - } + guard case (let r, false) = a.remainderReportingOverflow(dividingBy: b) + else { return nil } return r } } @@ -70,38 +80,55 @@ extension Optional where Wrapped: FixedWidthInteger { // Avoid false positives for these assignment operator implementations // swift-format-ignore: NoAssignmentInExpressions extension Optional where Wrapped: FixedWidthInteger { + /// Adds two values and stores the result in the left-hand-side variable, + /// if the values are non-`nil` and the result is representable. + @inlinable @inline(__always) + public static func +?= (a: inout Self, b: Self) { + a = a +? b + } + + /// Subtracts the second value from the first and stores the difference in + /// the left-hand-side variable, if the values are non-`nil` and the + /// difference is representable. + @inlinable @inline(__always) + public static func -?= (a: inout Self, b: Self) { + a = a -? b + } + + /// Multiplies two values and stores the result in the left-hand-side variable, + /// if the values are non-`nil` and the result is representable. @inlinable @inline(__always) public static func *?= (a: inout Self, b: Self) { a = a *? b } + /// Divides the first value by the second and stores the quotient in the + /// left-hand-side variable, if the values are non-`nil` and the + /// quotient is representable. @inlinable @inline(__always) public static func /?= (a: inout Self, b: Self) { a = a /? b } + /// Divides the first value by the second and stores the quotient in the + /// left-hand-side variable, if the values are non-`nil` and the + /// quotient is representable. @inlinable @inline(__always) public static func %?= (a: inout Self, b: Self) { a = a %? b } - - @inlinable @inline(__always) - public static func +?= (a: inout Self, b: Self) { - a = a +? b - } - - @inlinable @inline(__always) - public static func -?= (a: inout Self, b: Self) { - a = a -? b - } } extension Optional where Wrapped: FixedWidthInteger & SignedNumeric { + /// Negates the value, if the value is non-`nil` and the result is + /// representable. @inlinable @inline(__always) public static prefix func -? (a: Self) -> Self { 0 -? a } } extension Optional where Wrapped: Comparable { + /// Creates a half-open range, if the bounds are non-`nil` and equal + /// or in ascending order. @inlinable @inline(__always) public static func .. Range? { guard let lhs, let rhs else { return nil } @@ -109,6 +136,8 @@ extension Optional where Wrapped: Comparable { return lhs.. ClosedRange? { guard let lhs, let rhs else { return nil } diff --git a/Sources/BinaryParsing/Operations/ThrowingOperations.swift b/Sources/BinaryParsing/Operations/ThrowingOperations.swift index a8fed31..9634a76 100644 --- a/Sources/BinaryParsing/Operations/ThrowingOperations.swift +++ b/Sources/BinaryParsing/Operations/ThrowingOperations.swift @@ -10,6 +10,8 @@ //===----------------------------------------------------------------------===// extension Collection { + /// Returns the element at the given index, throwing an error if the index is + /// not in bounds. @inlinable public subscript(throwing i: Index) -> Element { get throws(ParsingError) { @@ -20,6 +22,8 @@ extension Collection { } } + /// Returns the subsequence in the given range, throwing an error if the range + /// is not in bounds. @inlinable public subscript(throwing bounds: Range) -> SubSequence { get throws(ParsingError) { @@ -31,6 +35,10 @@ extension Collection { } extension Optional { + /// The value wrapped by this optional. + /// + /// If this optional is `nil`, accessing the `unwrapped` property throws an + /// error. @inlinable public var unwrapped: Wrapped { get throws(ParsingError) { @@ -44,6 +52,8 @@ extension Optional { } extension BinaryInteger { + /// Creates a new value from the given integer, throwing if the value would + /// overflow. @inlinable public init(throwingOnOverflow other: some BinaryInteger) throws(ParsingError) { @@ -57,6 +67,8 @@ extension BinaryInteger { extension FixedWidthInteger { // MARK: Nonmutating arithmetic + /// Returns the sum of this value and the given value, throwing an error if + /// overflow occurrs. @inlinable public func addingThrowingOnOverflow(_ other: Self) throws(ParsingError) -> Self @@ -68,6 +80,8 @@ extension FixedWidthInteger { return result } + /// Returns the difference obtained by subtracting the given value from this + /// value, throwing an error if overflow occurrs. @inlinable public func subtractingThrowingOnOverflow(_ other: Self) throws(ParsingError) -> Self @@ -79,6 +93,8 @@ extension FixedWidthInteger { return result } + /// Returns the product of this value and the given value, throwing an error + /// if overflow occurrs. @inlinable public func multipliedThrowingOnOverflow(by other: Self) throws(ParsingError) -> Self @@ -90,6 +106,8 @@ extension FixedWidthInteger { return result } + /// Returns the quotient obtained by dividing this value by the given value, + /// throwing an error if overflow occurrs. @inlinable public func dividedThrowingOnOverflow(by other: Self) throws(ParsingError) -> Self @@ -101,6 +119,8 @@ extension FixedWidthInteger { return result } + /// Returns the remainder after dividing this value by the given value, + /// throwing an error if overflow occurrs. @inlinable public func remainderThrowingOnOverflow(dividingBy other: Self) throws(ParsingError) -> Self @@ -114,12 +134,16 @@ extension FixedWidthInteger { // MARK: Mutating arithmetic + /// Adds the given value to this value, throwing an error if overflow + /// occurrs. @inlinable public mutating func addThrowingOnOverflow(_ other: Self) throws(ParsingError) { self = try self.addingThrowingOnOverflow(other) } + /// Subtracts the given value from this value, throwing an error if overflow + /// occurrs. @inlinable public mutating func subtractThrowingOnOverflow(_ other: Self) throws(ParsingError) @@ -127,6 +151,8 @@ extension FixedWidthInteger { self = try self.subtractingThrowingOnOverflow(other) } + /// Multiplies this value by the given value, throwing an error if overflow + /// occurrs. @inlinable public mutating func multiplyThrowingOnOverflow(by other: Self) throws(ParsingError) @@ -134,6 +160,8 @@ extension FixedWidthInteger { self = try self.multipliedThrowingOnOverflow(by: other) } + /// Divides this value by the given value, throwing an error if overflow + /// occurrs. @inlinable public mutating func divideThrowingOnOverflow(by other: Self) throws(ParsingError) @@ -141,6 +169,8 @@ extension FixedWidthInteger { self = try self.dividedThrowingOnOverflow(by: other) } + /// Replacees this value with the remainder of dividing by the given value, + /// throwing an error if overflow occurrs. @inlinable public mutating func formRemainderThrowingOnOverflow(dividingBy other: Self) throws(ParsingError) diff --git a/Sources/BinaryParsing/Parser Types/Endianness.swift b/Sources/BinaryParsing/Parser Types/Endianness.swift index aaab23e..7e35799 100644 --- a/Sources/BinaryParsing/Parser Types/Endianness.swift +++ b/Sources/BinaryParsing/Parser Types/Endianness.swift @@ -9,27 +9,47 @@ // //===----------------------------------------------------------------------===// +/// A value indicating the endianness, or byte order, of an integer value. +/// +/// Endianness refers to the sequential order in which bytes are arranged into +/// larger numerical values when stored in memory, in a file on disk, or during +/// transmission over a network. When parsing multibyte integer values, you +/// specify the endianness either by selecting a specific parsing API or by +/// providing an `Endianness` value. +/// +/// In big-endian format, the most significant byte (the "big end") is stored +/// at the lowest memory address, while in little-endian format, the least +/// significant byte (the "little end") is stored at the lowest memory address. +/// +/// For example, the 32-bit integer value `0x12345678` would be stored as +/// `12 34 56 78` in big-endian format and `78 56 34 12` in little-endian +/// format. public struct Endianness: Hashable { var _isBigEndian: Bool + /// Creates an endianness value from the specified Boolean value. public init(isBigEndian: Bool) { self._isBigEndian = isBigEndian } } extension Endianness { + /// The big-endian value. public static var big: Endianness { self.init(isBigEndian: true) } + /// The little-endian value. public static var little: Endianness { self.init(isBigEndian: false) } + /// A Boolean value inidicating whether the endianness is big-endian. public var isBigEndian: Bool { _isBigEndian } + /// A Boolean value inidicating whether the endianness is little-endian. public var isLittleEndian: Bool { !_isBigEndian } diff --git a/Sources/BinaryParsing/Parser Types/ParserRange.swift b/Sources/BinaryParsing/Parser Types/ParserRange.swift index 04b141f..cf5f641 100644 --- a/Sources/BinaryParsing/Parser Types/ParserRange.swift +++ b/Sources/BinaryParsing/Parser Types/ParserRange.swift @@ -9,6 +9,16 @@ // //===----------------------------------------------------------------------===// +/// A range of bytes within a `ParserSpan`. +/// +/// Use a `ParserRange` to store a range of bytes in a `ParserSpan`. You can +/// access the current range of a parser span by using its +/// ``ParserSpan/parserRange`` property, or consume a range from a parser span +/// by using one of the `slicingSpan` methods. +/// +/// To convert a `ParserRange` into a `ParserSpan` for continued parsing, use +/// either the ``ParserSpan/seeking(toRange:)`` or +/// ``ParserSpan/seek(toRange:)`` method. public struct ParserRange: Hashable, Sendable { @usableFromInline internal var range: Range @@ -18,16 +28,19 @@ public struct ParserRange: Hashable, Sendable { self.range = range } + /// A Boolean value indicating whether the range is empty. @_alwaysEmitIntoClient public var isEmpty: Bool { range.isEmpty } + /// The lower bound of the range. @_alwaysEmitIntoClient public var lowerBound: Int { range.lowerBound } + /// The upper, non-inclusive bound of the range. @_alwaysEmitIntoClient public var upperBound: Int { range.upperBound @@ -35,6 +48,8 @@ public struct ParserRange: Hashable, Sendable { } extension RandomAccessCollection where Index == Int { + /// Accesses the subsequence of this collection described by the given range, + /// throwing an error if the range is outside the collection's bounds. public subscript(_ range: ParserRange) -> SubSequence { get throws(ParsingError) { let validRange = startIndex...endIndex @@ -49,6 +64,7 @@ extension RandomAccessCollection where Index == Int { } extension ParserSpan { + /// The current range of this parser span. @inlinable public var parserRange: ParserRange { ParserRange(range: self.startPosition.. { + /// Executes the given closure with a `ParserSpan` over the contents of this + /// collection, if such a span is available. @inlinable public func withParserSpanIfAvailable( _ body: (inout ParserSpan) throws(ThrownParsingError) -> T @@ -76,7 +94,10 @@ extension RandomAccessCollection { // MARK: ParserSpanProvider +/// A type that provides access to a `ParserSpan`. public protocol ParserSpanProvider { + /// Executes the given closure with a `ParserSpan` over the contents of this + /// type. func withParserSpan( _ body: (inout ParserSpan) throws(E) -> T ) throws(E) -> T @@ -84,6 +105,8 @@ public protocol ParserSpanProvider { extension ParserSpanProvider { #if !$Embedded + /// Executes the given closure with a `ParserSpan` over the contents of this + /// type, consuming the given parser range instead of the full span. @_alwaysEmitIntoClient @inlinable public func withParserSpan( @@ -98,6 +121,8 @@ extension ParserSpanProvider { } #endif + /// Executes the given closure with a `ParserSpan` over the contents of this + /// type, consuming the given parser range instead of the full span. @_alwaysEmitIntoClient @inlinable public func withParserSpan( diff --git a/Sources/BinaryParsing/Parser Types/ParserSpan.swift b/Sources/BinaryParsing/Parser Types/ParserSpan.swift index 41ff61b..1379aca 100644 --- a/Sources/BinaryParsing/Parser Types/ParserSpan.swift +++ b/Sources/BinaryParsing/Parser Types/ParserSpan.swift @@ -11,7 +11,8 @@ /// A non-owning, non-escaping view for parsing binary data. /// -/// You can access a `ParserSpan` from a +/// You can access a `ParserSpan` from an array of bytes or a `Data` instance, +/// or construct one from an existing `RawSpan`. public struct ParserSpan: ~Escapable, ~Copyable { @usableFromInline var _bytes: RawSpan @@ -21,10 +22,13 @@ public struct ParserSpan: ~Escapable, ~Copyable { var _upperBound: Int /// Creates a parser span over the entire contents of the given raw span. + /// + /// The resulting `ParserSpan` has a lifetime copied from the raw span + /// passed as `bytes`. @inlinable - @lifetime(copy _bytes) - public init(_ _bytes: RawSpan) { - self._bytes = _bytes + @lifetime(copy bytes) + public init(_ bytes: RawSpan) { + self._bytes = bytes self._lowerBound = 0 self._upperBound = _bytes.byteCount } @@ -59,6 +63,8 @@ public struct ParserSpan: ~Escapable, ~Copyable { } /// A raw span over the current memory represented by this parser span. + /// + /// The resulting `RawSpan` has a lifetime copied from this parser span. public var bytes: RawSpan { @inlinable @lifetime(copy self) diff --git a/Sources/BinaryParsing/Parser Types/ParsingError.swift b/Sources/BinaryParsing/Parser Types/ParsingError.swift index 87e43d7..5b7dfb9 100644 --- a/Sources/BinaryParsing/Parser Types/ParsingError.swift +++ b/Sources/BinaryParsing/Parser Types/ParsingError.swift @@ -120,8 +120,8 @@ extension ParsingError.Status: CustomStringConvertible { /// /// In a build for embedded Swift, `ThrownParsingError` instead aliases the /// specific `ParsingError` type. Because embedded Swift supports only -/// fully-typed throws, and not the existential `any Error`, this allows you -/// to still use error-throwing APIs in an embedded context. +/// fully-typed throws, and not the existential `any Error`, this allows you to +/// still take use error-throwing APIs in an embedded context. public typealias ThrownParsingError = any Error #else // Documentation is built using the non-embedded build. diff --git a/Sources/BinaryParsing/Parser Types/Seeking.swift b/Sources/BinaryParsing/Parser Types/Seeking.swift index 111b800..34e3bc3 100644 --- a/Sources/BinaryParsing/Parser Types/Seeking.swift +++ b/Sources/BinaryParsing/Parser Types/Seeking.swift @@ -10,6 +10,17 @@ //===----------------------------------------------------------------------===// extension ParserSpan { + /// Returns a new parser span with its range set to the given range, throwing + /// an error if out of bounds. + /// + /// Use this method to create a new parser span that jumps to a previously + /// created parser range. For example, you can use one of the `sliceSpan` + /// methods to consume a range from a parser span for deferred parsing, and + /// then use this method when ready to parse the data in that range. + /// + /// - Parameter range: The range to seek to. + /// - Returns: A new parser span positioned at `range`. + /// - Throws: A `ParsingError` if `range` is out of bounds for this span. @inlinable @lifetime(copy self) public func seeking(toRange range: ParserRange) @@ -20,6 +31,34 @@ extension ParserSpan { return result } + /// Returns a new parser span with the start position moved by the specified + /// relative offset, throwing an error if out of bounds. + /// + /// This method creates a new parser span where only the start position has + /// been moved; the end position remains unchanged. The offset cannot move + /// the start position past the current end of the parser span. + /// + /// For example, this code starts with a parser span representing 64 bytes. + /// The code creates a new span offset 8 bytes forward within the original + /// span, then reads a `UInt64` from that new span: + /// + /// var input = array.parserSpan + /// // input.count == 64 + /// var offsetInput = try input.seeking(toRelativeOffset: 8) + /// // offsetInput.count == 56 + /// let value = try UInt64(parsingBigEndian: &offsetInput) + /// + /// Creating a new span with an offset greater than the original span's count + /// results in an error: + /// + /// var offsetInput = try input.seeking(toRelativeOffset: 72) // throws a 'ParsingError' + /// + /// - Parameter offset: The number of bytes to move the start position + /// forward in the span. `offset` must be non-negative and less than or + /// equal to `count`. + /// - Returns: A new parser span with the updated start position. + /// - Throws: A `ParsingError` if `offset` is not in the closed range + /// `0...count`. @inlinable @lifetime(copy self) public func seeking(toRelativeOffset offset: some FixedWidthInteger) @@ -30,6 +69,38 @@ extension ParserSpan { return result } + /// Returns a new parser span with the start position set to the specified + /// absolute offset, and the end position set to the end of the underlying + /// raw span, throwing an error if out of bounds. + /// + /// This method creates a new parser span using the count of the original + /// span's underlying `bytes` property, and sets both the start and end + /// position of the new parser span. + /// + /// For example, this code starts with a parser span representing 64 bytes. + /// The code reads data, then creates a new span positioned at the absolute + /// offset `56`: + /// + /// let input = array.parserSpan + /// // input.count == 64 + /// let value1 = try UInt64(parsingBigEndian: &input) + /// let value2 = try UInt64(parsingBigEndian: &input) + /// // input.count == 48 + /// let offsetInput = try input.seeking(toAbsoluteOffset: 56) + /// // offsetInput.startPosition == 56 + /// // offsetInput.count == 8 + /// + /// Creating a new span with a negative offset, or an offset greater than the + /// underlying byte count, results in an error: + /// + /// let offsetInput = try input.seeking(toAbsoluteOffset: -8) // throws a 'ParsingError' + /// + /// - Parameter offset: The absolute offset to move to within the underlying + /// raw span. `offset` must be non-negative and less than or equal to + /// `bytes.count`. + /// - Returns: A new parser span positioned at the specified absolute offset. + /// - Throws: A `ParsingError` if `offset` is not in the closed range + /// `0...bytes.count`. @inlinable @lifetime(copy self) public func seeking(toAbsoluteOffset offset: some FixedWidthInteger) @@ -40,6 +111,36 @@ extension ParserSpan { return result } + /// Returns a new parser span with the start position set to the specified + /// offset from the end of the span, throwing an error if out of bounds. + /// + /// This method creates a new parser span where only the start position has + /// been set to the specified offset from the end; the end position remains + /// unchanged. The offset cannot result in a start position that would be + /// outside the bounds of the original span. + /// + /// For example, this code starts with a parser span representing 64 bytes. + /// The code creates a new span starting at 16 bytes from the end of the + /// span, and then reads a `UInt64`: + /// + /// var input = array.parserSpan + /// // input.count == 64 + /// var offsetInput = try input.seeking(toOffsetFromEnd: 16) + /// let value = try UInt64(parsingBigEndian: &offsetInput) + /// // offsetInput.startPosition == 56 + /// // offsetInput.count == 8 + /// + /// Creating a new span with an offset greater than the original span's byte + /// count results in an error: + /// + /// let offsetInput = try input.seeking(toOffsetFromEnd: 72) // throws a 'ParsingError' + /// + /// - Parameter offset: The number of bytes to move backward from the end of + /// this span. `offset` must be non-negative and less than or equal to the + /// span's byte count. + /// - Returns: A new parser span with the updated start position. + /// - Throws: A `ParsingError` if `offset` is not in the closed range + /// `0...bytes.count`. @inlinable @lifetime(copy self) public func seeking(toOffsetFromEnd offset: some FixedWidthInteger) @@ -52,6 +153,16 @@ extension ParserSpan { } extension ParserSpan { + /// Updates the range of this parser span to the given range, throwing an + /// error if out of bounds. + /// + /// Use this method to jump to a previously created parser range. For + /// example, you can use one of the `sliceSpan` methods to consume a range + /// from a parser span for deferred parsing, and then use this method when + /// ready to parse the data in that range. + /// + /// - Parameter range: The range to seek to. + /// - Throws: A `ParsingError` if `range` is out of bounds for this span. @inlinable @lifetime(&self) public mutating func seek(toRange range: ParserRange) throws(ParsingError) { @@ -64,6 +175,34 @@ extension ParserSpan { self._upperBound = range.range.upperBound } + /// Updates this parser span by moving the start position by the specified + /// relative offset, throwing an error if out of bounds. + /// + /// This method moves only the start position of the parser span; the end + /// position is unaffected. The offset cannot move the start position past + /// the current end of the parser span. + /// + /// For example, this code starts with a parser span representing 64 bytes. + /// The code seeks 8 bytes forward within the span, then reads a `UInt64`, + /// leaving the span at a start position of `16`. + /// + /// var input = array.parserSpan + /// // input.count == 64 + /// try input.seek(toRelativeOffset: 8) + /// let value = try UInt64(parsingBigEndian: &input) + /// // input.startPosition == 16 + /// // input.count == 48 + /// + /// With `input` in this state, calling `seek(toRelativeOffset:)` with a + /// value greater than 50 results in an error: + /// + /// try input.seek(toRelativeOffset: 64) // throws a 'ParsingError' + /// + /// - Parameter offset: The number of bytes to move the start position + /// forward in the span. `offset` must be non-negative and less than or + /// equal to `count`. + /// - Throws: A `ParsingError` if `offset` is not in the closed range + /// `0...count`. @inlinable @lifetime(&self) public mutating func seek(toRelativeOffset offset: some FixedWidthInteger) @@ -77,6 +216,38 @@ extension ParserSpan { self._lowerBound += offset } + /// Updates this parser span by moving the start position to the specified + /// absolute offset, and resets the end position to the end of the underlying + /// raw span, throwing an error if out of bounds. + /// + /// This method always uses the count of this parser span's underlying + /// `bytes` raw span property, and updates both the start and end position of + /// the parser span. + /// + /// For example, this code starts with a parser span representing 64 bytes. + /// The code reads two `UInt64` values, and then seeks to the absolute offset + /// `56`. (Note that `56` would be out of bounds for the + /// ``seek(toRelativeOffset:)`` method, as there are only 48 remaining bytes + /// in the parser span.) + /// + /// var input = array.parserSpan + /// // input.count == 64 + /// let value1 = try UInt64(parsingBigEndian: &input) + /// let value2 = try UInt64(parsingBigEndian: &input) + /// // input.count == 48 + /// // input.bytes.count == 64 + /// try input.seek(toAbsoluteOffset: 56) + /// + /// Calling `seek(toAbsoluteOffset:)` with a negative value, or a value + /// greater than the underlying byte count, results in an error: + /// + /// try input.seek(toAbsoluteOffset: -8) // throws a 'ParsingError' + /// + /// - Parameter offset: The absolute offset to move to within the underlying + /// raw span. `offset` must be non-negative and less than or equal to + /// `bytes.count`. + /// - Throws: A `ParsingError` if `offset` is not in the closed range + /// `0...bytes.count`. @inlinable @lifetime(&self) public mutating func seek(toAbsoluteOffset offset: some FixedWidthInteger) @@ -91,6 +262,36 @@ extension ParserSpan { self._upperBound = _bytes.byteCount } + /// Updates this parser span by moving the start position to the specified + /// offset from the end position of the span, throwing an error if out of + /// bounds. + /// + /// This method moves only the start position of the parser span; the end + /// position is unaffected. The offset cannot move the start position + /// backwards in the overall span. + /// + /// For example, this code starts with a parser span representing 64 bytes. + /// The code seeks to 16 bytes from the end of the span, and then reads a + /// `UInt64`, leaving the span at a start position of `56`. + /// + /// var input = array.parserSpan + /// // input.count == 64 + /// try input.seek(toOffsetFromEnd: 16) + /// let value = try UInt64(parsingBigEndian: &input) + /// // input.startPosition == 56 + /// // input.count == 8 + /// + /// With `input` in this state, calling `seek(toOffsetFromEnd:)` with a value + /// greater than 8 results in an error, because that would move the start + /// position backward in the parser span: + /// + /// try input.seek(toOffsetFromEnd: 16) // throws a 'ParsingError' + /// + /// - Parameter offset: The number of bytes to move backward from the end of + /// this span. `offset` must be non-negative and less than or equal to + /// `count`. + /// - Throws: A `ParsingError` if `offset` is not in the closed range + /// `0...count`. @inlinable @lifetime(&self) public mutating func seek(toOffsetFromEnd offset: some FixedWidthInteger) diff --git a/Sources/BinaryParsing/Parser Types/Slicing.swift b/Sources/BinaryParsing/Parser Types/Slicing.swift index 00bdd10..edead43 100644 --- a/Sources/BinaryParsing/Parser Types/Slicing.swift +++ b/Sources/BinaryParsing/Parser Types/Slicing.swift @@ -10,6 +10,22 @@ //===----------------------------------------------------------------------===// extension ParserSpan { + /// Returns a new parser span covering the specified number of bytes from the + /// start of this parser span, shrinking this parser span by the same amount. + /// + /// Use `sliceSpan(byteCount:)` to retrieve a separate span for a parsing + /// sub-task when you know the size of the task. For example, each chunk in + /// the PNG format begins with an identifier and the size of the chunk, in + /// bytes. A PNG chunk parser could use this method to slice the correct size + /// for each chunk, and limit parsing to within the resulting span. + /// + /// - Parameter byteCount: The number of bytes to include in the resulting + /// span. `byteCount` must be non-negative, and less than or equal to the + /// number of bytes remaining in the span. + /// - Returns: A new parser span covering `byteCount` bytes. + /// - Throws: A `ParsingError` if `byteCount` cannot be represented as an + /// `Int`, if it's negative, or if there aren't enough bytes in the + /// original span. @inlinable @lifetime(copy self) public mutating func sliceSpan(byteCount: some FixedWidthInteger) @@ -24,6 +40,25 @@ extension ParserSpan { return divide(atOffset: byteCount) } + /// Returns a new parser span covering the specified number of bytes + /// calculated as the product of object count and stride from the start of + /// this parser span, shrinking this parser span by the same amount. + /// + /// Use `sliceSpan(objectStride:objectCount:)` when you need to retrieve a + /// span for parsing a collection of fixed-size objects. This is particularly + /// useful when parsing arrays of binary data with known element sizes. For + /// example, if you're parsing an array of 4-byte integers and know there are + /// 10 elements, you can use: + /// + /// let intArraySpan = try span.sliceSpan(objectStride: 4, objectCount: 10) + /// + /// - Parameters: + /// - objectStride: The size in bytes of each object in the collection. + /// - objectCount: The number of objects to include in the resulting span. + /// - Returns: A new parser span covering `objectStride * objectCount` bytes. + /// - Throws: A `ParsingError` if either `objectStride` or `objectCount` + /// cannot be represented as an `Int`, if their product would overflow, or + /// if the product is not in the range `0...count`. @inlinable @lifetime(copy self) public mutating func sliceSpan( @@ -43,6 +78,25 @@ extension ParserSpan { } extension ParserSpan { + /// Returns a parser range covering the specified number of bytes from the + /// start of this parser span, shrinking this parser span by the same amount. + /// + /// Use `sliceRange(byteCount:)` to retrieve a parser range for a deferred + /// parsing sub-task when you know the size of the task. For example, each + /// chunk in the PNG format begins with an identifier and the size of the + /// chunk, in bytes. A PNG chunk parser could use this method to slice the + /// correct size for each chunk, and limit parsing to within the resulting + /// span. + /// + /// To convert the resulting range back into a `ParserSpan`, use the + /// ``ParserSpan/seeking(toRange:)`` or ``ParserSpan/seek(toRange:)`` method. + /// + /// - Parameter byteCount: The number of bytes to include in the resulting + /// range. + /// - Returns: A parser range covering `byteCount` bytes. + /// - Throws: A `ParsingError` if `byteCount` cannot be represented as an + /// `Int`, if it's negative, or if there aren't enough bytes in the + /// original span. @inlinable @lifetime(&self) public mutating func sliceRange(byteCount: some FixedWidthInteger) @@ -51,6 +105,29 @@ extension ParserSpan { try sliceSpan(byteCount: byteCount).parserRange } + /// Returns a parser range covering the specified number of bytes calculated + /// as the product of object count and stride from the start of this parser + /// span, shrinking this parser span by the same amount. + /// + /// Use `sliceRange(objectStride:objectCount:)` when you need to retrieve a + /// parser range for a deferred parsing of a collection of fixed-size + /// objects. This is particularly useful for parsing arrays of binary data + /// with known element sizes that you want to process later. For example, if + /// you're parsing an array of 4-byte integers and know there are 10 + /// elements, you can use: + /// + /// let intArrayRange = try span.sliceRange(objectStride: 4, objectCount: 10) + /// + /// To convert the resulting range back into a `ParserSpan`, use the + /// ``ParserSpan/seeking(toRange:)`` or ``ParserSpan/seek(toRange:)`` method. + /// + /// - Parameters: + /// - objectStride: The size in bytes of each object in the collection. + /// - objectCount: The number of objects to include in the resulting range. + /// - Returns: A parser range covering `objectStride * objectCount` bytes. + /// - Throws: A `ParsingError` if either `objectStride` or `objectCount` + /// cannot be represented as an `Int`, if their product would overflow, or + /// if the product is not in the range `0...count`. @inlinable @lifetime(&self) public mutating func sliceRange( @@ -61,6 +138,15 @@ extension ParserSpan { .parserRange } + /// Returns a parser range covering the remaining bytes in this parser span, + /// setting this span to empty at its end position. + /// + /// Use `sliceRemainingRange()` to capture the remainder of this parser span + /// as a range for deferred parsing. After calling this method, the span + /// is empty, with its `startPosition` moved to match its `endPosition`. + /// + /// - Returns: A parser range covering the rest of the memory represented + /// by this parser span. @inlinable @lifetime(&self) public mutating func sliceRemainingRange() -> ParserRange { @@ -69,6 +155,24 @@ extension ParserSpan { } extension ParserSpan { + /// Returns a `UTF8Span` covering the specified number of bytes from the + /// start of this parser span, shrinking this parser span by the same amount. + /// + /// The bytes in the slice must be valid UTF-8. + /// + /// Use `sliceUTF8Span(byteCount:)` to retrieve a span for parsing text that + /// is UTF-8 encoded. This method verifies that the bytes are valid UTF-8, + /// giving you access to the `UTF8Span` Unicode processing operations. + /// + /// let textData = try span.sliceUTF8Span(byteCount: 42) + /// // textData is a UTF8Span that has been validated to contain proper UTF-8 + /// + /// - Parameter byteCount: The number of bytes to include in the resulting + /// UTF8Span. + /// - Returns: A new UTF8Span covering `byteCount` bytes. + /// - Throws: A `ParsingError` if `byteCount` cannot be represented as an + /// `Int`, if it's negative, if there aren't enough bytes in the original + /// span, or if the bytes don't form valid UTF-8. @inlinable @lifetime(copy self) @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) diff --git a/Sources/BinaryParsing/Parsers/Array.swift b/Sources/BinaryParsing/Parsers/Array.swift index ae1e0b3..ce298b7 100644 --- a/Sources/BinaryParsing/Parsers/Array.swift +++ b/Sources/BinaryParsing/Parsers/Array.swift @@ -10,17 +10,29 @@ //===----------------------------------------------------------------------===// extension Array where Element == UInt8 { + /// Creates a new array by copying the remaining bytes from the given parser + /// span. + /// + /// Unlike most parsers, this initializer does not throw. + /// + /// - Parameter input: The `ParserSpan` to consume. @inlinable @lifetime(&input) - public init(parsingRemainingBytes input: inout ParserSpan) - throws(ParsingError) - { + public init(parsingRemainingBytes input: inout ParserSpan) { defer { _ = input.divide(atOffset: input.count) } self = unsafe input.withUnsafeBytes { buffer in unsafe Array(buffer) } } + /// Creates a new array by copying the specified number of bytes from the + /// given parser span. + /// + /// - Parameters: + /// - input: The `ParserSpan` to consume. + /// - byteCount: The number of bytes to copy into the resulting array. + /// - Throws: A `ParsingError` if `input` does not have at least `byteCount` + /// bytes remaining. @inlinable @lifetime(&input) public init(parsing input: inout ParserSpan, byteCount: Int) @@ -35,6 +47,32 @@ extension Array where Element == UInt8 { extension Array { #if !$Embedded + /// 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 `byteCount` times while initializing the array. + /// For example, the following code parses an array of 16 `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 Array(parsing: &input, count: 16) { 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 Array( + /// parsing: &input, + /// count: 16, + /// parser: UInt32.init(parsingBigEndian:)) + /// + /// - Parameters: + /// - input: The `ParserSpan` to consume. + /// - count: The number of elements to parse from `input`. + /// - parser: A closure that parses each element from `input`. + /// - Throws: An error if one is thrown from `parser`, or if `count` isn't + /// representable. @inlinable @lifetime(&input) public init( @@ -53,6 +91,31 @@ extension Array { } #endif + /// 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 `byteCount` times while initializing the array. + /// For example, the following code parses an array of 16 `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 Array(parsing: &input, count: 16) { 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 Array( + /// parsing: &input, + /// count: 16, + /// parser: UInt32.init(parsingBigEndian:)) + /// + /// - Parameters: + /// - input: The `ParserSpan` to consume. + /// - count: The number of elements to parse from `input`. + /// - parser: A closure that parses each element from `input`. + /// - Throws: An error if one is thrown from `parser`. @inlinable @lifetime(&input) public init( @@ -69,6 +132,29 @@ extension Array { } } + /// Creates a new array by parsing elements from the given parser span until empty, + /// using the provided closure for parsing. + /// + /// The provided closure is called repeatedly while initializing the array. + /// For example, the following code parses as many `UInt32` values from a `ParserSpan` + /// as are remaining. If the `input` parser span represents memory that isn't + /// a multiple of `MemoryLayout.size`, the call will throw a `ParsingError`. + /// + /// let integers = try Array(parsingAll: &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 Array( + /// parsingAll: &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( diff --git a/Sources/BinaryParsing/Parsers/Integer.swift b/Sources/BinaryParsing/Parsers/Integer.swift index 7e8fcc8..4e57250 100644 --- a/Sources/BinaryParsing/Parsers/Integer.swift +++ b/Sources/BinaryParsing/Parsers/Integer.swift @@ -287,6 +287,14 @@ extension MultiByteInteger { unsafe self.init(_unchecked: (), _parsingBigEndian: &input) } + /// Creates an integer by parsing a big-endian value of this type's size from + /// the start of the given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by the size of this + /// integer. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// this integer type. @inlinable @lifetime(&input) public init(parsingBigEndian input: inout ParserSpan) throws(ParsingError) { @@ -300,6 +308,14 @@ extension MultiByteInteger { unsafe self.init(_unchecked: (), _parsingLittleEndian: &input) } + /// Creates an integer by parsing a little-endian value of this type's size + /// from the start of the given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by the size of this + /// integer. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// this integer type. @inlinable @lifetime(&input) public init(parsingLittleEndian input: inout ParserSpan) throws(ParsingError) @@ -319,6 +335,15 @@ extension MultiByteInteger { : Self(_unchecked: (), _parsingLittleEndian: &input) } + /// Creates an integer by parsing a value of this type's size, and the + /// specified endianness, from the start of the given parser span. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by the size of this integer. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// this integer type. @inlinable @lifetime(&input) public init(parsing input: inout ParserSpan, endianness: Endianness) @@ -339,6 +364,12 @@ extension SingleByteInteger { self = unsafe input.consumeUnchecked(type: Self.self) } + /// Creates an integer by parsing a single-byte value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by one byte. + /// - Throws: A `ParsingError` if `input` is empty. @inlinable public init(parsing input: inout ParserSpan) throws(ParsingError) { guard !input.isEmpty else { @@ -372,6 +403,21 @@ extension FixedWidthInteger where Self: BitwiseCopyable { _unchecked: (), _parsing: &input, endianness: .big, byteCount: byteCount) } + /// Creates an integer by parsing a big-endian value from the specified + /// number of bytes at the start of the given parser span. + /// + /// If `byteCount` is smaller than this type's size, the resulting value is + /// sign-extended, if necessary. If `byteCount` is larger than this type's + /// size, the padding must be consistent with a fixed-size integer of that + /// size. That is, the padding bits must sign extend the actual value, as all + /// zeroes or all ones throughout the padding. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by `byteCount`. + /// - byteCount: The number of bytes to read the value from. + /// - Throws: A `ParsingError` if `input` contains fewer than `byteCount` + /// bytes, or if the parsed value overflows this integer type. @inlinable @lifetime(&input) public init(parsingBigEndian input: inout ParserSpan, byteCount: Int) @@ -392,6 +438,21 @@ extension FixedWidthInteger where Self: BitwiseCopyable { byteCount: byteCount) } + /// Creates an integer by parsing a little-endian value from the specified + /// number of bytes at the start of the given parser span. + /// + /// If `byteCount` is smaller than this type's size, the resulting value is + /// sign-extended, if necessary. If `byteCount` is larger than this type's + /// size, the padding must be consistent with a fixed-size integer of that + /// size. That is, the padding bits must sign extend the actual value, as all + /// zeroes or all ones throughout the padding. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by `byteCount`. + /// - byteCount: The number of bytes to read the value from. + /// - Throws: A `ParsingError` if `input` contains fewer than `byteCount` + /// bytes, or if the parsed value overflows this integer type. @inlinable @lifetime(&input) public init(parsingLittleEndian input: inout ParserSpan, byteCount: Int) @@ -412,6 +473,23 @@ extension FixedWidthInteger where Self: BitwiseCopyable { byteCount: byteCount) } + /// Creates an integer by parsing a value with the specified endianness and + /// number of bytes at the start of the given parser span. + /// + /// If `byteCount` is smaller than this type's size, the resulting value is + /// sign-extended, if necessary. If `byteCount` is larger than this type's + /// size, the padding must be consistent with a fixed-size integer of that + /// size. That is, the padding bits must sign extend the actual value, as all + /// zeroes or all ones throughout the padding. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by `byteCount`. + /// - byteCount: The number of bytes to read the value from. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` contains fewer than `byteCount` + /// bytes, if the parsed value overflows this integer type, or if the + /// padding bytes are invalid. @inlinable @lifetime(&input) public init( @@ -427,17 +505,41 @@ extension FixedWidthInteger where Self: BitwiseCopyable { public init( _unchecked _: Void, parsing input: inout ParserSpan, - storedAsBigEndian: T.Type + storedAsBigEndian storageType: T.Type ) throws(ParsingError) { let result = unsafe T(_unchecked: (), _parsingBigEndian: &input) self = try Self(_throwing: result) } + /// Creates an integer by parsing and converting a big-endian value of the + /// given type from the start of the given parser span. + /// + /// The parsed value is interpreted using the signedness of `storageType`, + /// not the destination type. Using this parsing initializer is equivalent to + /// parsing a value of type `storageType` and then using + /// ``Swift/BinaryInteger/init(throwingOnOverflow:)`` to safely convert the + /// value to this type. + /// + /// // Using this parsing initializer: + /// let value1 = try Int(parsing: &input, storedAsBigEndian: UInt32.self) + /// + /// // Equivalent to: + /// let uint32Value = try UInt32(parsingBigEndian: &input) + /// let value2 = try Int(throwingOnOverflow: uint32Value) + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by the size of this integer. + /// - storageType: The integer type to parse from `input` before conversion + /// to the destination type. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// `storageType`, or if converting the parsed value to this integer type + /// overflows. @inlinable @lifetime(&input) public init( parsing input: inout ParserSpan, - storedAsBigEndian: T.Type + storedAsBigEndian storageType: T.Type ) throws(ParsingError) { let result = try T(_parsingBigEndian: &input) self = try Self(_throwing: result) @@ -449,17 +551,41 @@ extension FixedWidthInteger where Self: BitwiseCopyable { public init( _unchecked _: Void, parsing input: inout ParserSpan, - storedAsLittleEndian: T.Type + storedAsLittleEndian storageType: T.Type ) throws(ParsingError) { let result = unsafe T(_unchecked: (), _parsingLittleEndian: &input) self = try Self(_throwing: result) } + /// Creates an integer by parsing and converting a little-endian value of the + /// given type from the start of the given parser span. + /// + /// The parsed value is interpreted using the signedness of `storageType`, + /// not the destination type. Using this parsing initializer is equivalent to + /// parsing a value of type `storageType` and then using + /// ``Swift/BinaryInteger/init(throwingOnOverflow:)`` to safely convert the + /// value to this type. + /// + /// // Using this parsing initializer: + /// let value1 = try Int(parsing: &input, storedAsLittleEndian: UInt32.self) + /// + /// // Equivalent to: + /// let uint32Value = try UInt32(parsingLittleEndian: &input) + /// let value2 = try Int(throwingOnOverflow: uint32Value) + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by the size of this integer. + /// - storageType: The integer type to parse from `input` before conversion + /// to the destination type. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// `storageType`, or if converting the parsed value to this integer type + /// overflows. @inlinable @lifetime(&input) public init( parsing input: inout ParserSpan, - storedAsLittleEndian: T.Type + storedAsLittleEndian storageType: T.Type ) throws(ParsingError) { let result = try T(_parsingLittleEndian: &input) self = try Self(_throwing: result) @@ -481,11 +607,36 @@ extension FixedWidthInteger where Self: BitwiseCopyable { self = try Self(_throwing: result) } + /// Creates an integer by parsing and converting a value of the given type + /// and endianness from the start of the given parser span. + /// + /// The parsed value is interpreted using the signedness of `storageType`, + /// not the destination type. Using this parsing initializer is equivalent to + /// parsing a value of type `storageType` and then using + /// ``Swift/BinaryInteger/init(throwingOnOverflow:)`` to safely convert the + /// value to this type. + /// + /// // Using this parsing initializer: + /// let value1 = try Int(parsing: &input, storedAs: UInt32.self, endianness: .big) + /// + /// // Equivalent to: + /// let uint32Value = try UInt32(parsing: &input, endianness: .big) + /// let value2 = try Int(throwingOnOverflow: uint32Value) + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by the size of this integer. + /// - storageType: The integer type to parse from `input` before conversion + /// to the destination type. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// `storageType`, or if converting the parsed value to this integer type + /// overflows. @inlinable @lifetime(&input) public init( parsing input: inout ParserSpan, - storedAs: T.Type, + storedAs storageType: T.Type, endianness: Endianness ) throws(ParsingError) { let result = @@ -506,11 +657,36 @@ extension FixedWidthInteger where Self: BitwiseCopyable { self = try unsafe Self(_throwing: T(truncatingIfNeeded: input.consumeUnchecked())) } + /// Creates an integer by parsing and converting a value of the given + /// single-byte integer type from the start of the given parser span. + /// + /// The parsed value is interpreted using the signedness of `storageType`, + /// not the destination type. Using this parsing initializer is equivalent to + /// parsing a value of type `storageType` and then using + /// ``Swift/BinaryInteger/init(throwingOnOverflow:)`` to safely convert the + /// value to this type. + /// + /// // Using this parsing initializer: + /// let value1 = try Int8(parsing: &input, storedAs: UInt8.self) + /// + /// // Equivalent to: + /// let uint8Value = try UInt8(parsing: &input) + /// let value2 = try Int8(throwingOnOverflow: uint8Value) + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by the size of this integer. + /// - storageType: The integer type to parse from `input` before conversion + /// to the destination type. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// `storageType`, or if converting the parsed value to this integer type + /// overflows. @inlinable @lifetime(&input) public init( parsing input: inout ParserSpan, - storedAs: T.Type + storedAs storageType: T.Type ) throws(ParsingError) { guard let result = input.consume() else { throw ParsingError( @@ -575,7 +751,7 @@ extension RawRepresentable where RawValue: FixedWidthInteger & BitwiseCopyable { @lifetime(&input) public init( parsing input: inout ParserSpan, - storedAsBigEndian: T.Type + storedAsBigEndian storageType: T.Type ) throws(ParsingError) { self = try Self( _rawValueThrowing: @@ -586,7 +762,7 @@ extension RawRepresentable where RawValue: FixedWidthInteger & BitwiseCopyable { @lifetime(&input) public init( parsing input: inout ParserSpan, - storedAsLittleEndian: T.Type + storedAsLittleEndian storageType: T.Type ) throws(ParsingError) { self = try Self( _rawValueThrowing: diff --git a/Sources/BinaryParsing/Parsers/IntegerProtocols.swift b/Sources/BinaryParsing/Parsers/IntegerProtocols.swift index 963ffa2..cbf1cc9 100644 --- a/Sources/BinaryParsing/Parsers/IntegerProtocols.swift +++ b/Sources/BinaryParsing/Parsers/IntegerProtocols.swift @@ -11,14 +11,14 @@ /// A fixed-width integer with a single-byte size. /// -/// Single-byte integers can be loaded directly, and don't have a notion -/// of alignment or endianness. +/// Single-byte integers can be loaded directly, and don't have a notion of +/// alignment or endianness. public protocol SingleByteInteger: FixedWidthInteger, BitwiseCopyable {} /// A fixed-width integer with a size of two or more bytes. /// -/// Multi-byte integers can have both alignment and endianness, and are -/// always a fixed size in memory. +/// Multi-byte integers can have both alignment and endianness, and are always +/// a fixed size in memory. public protocol MultiByteInteger: FixedWidthInteger, BitwiseCopyable {} /// A fixed-width integer with a size that varies by platform. diff --git a/Sources/BinaryParsing/Parsers/Range.swift b/Sources/BinaryParsing/Parsers/Range.swift index dfc82d6..7755aa4 100644 --- a/Sources/BinaryParsing/Parsers/Range.swift +++ b/Sources/BinaryParsing/Parsers/Range.swift @@ -13,6 +13,18 @@ extension Range where Bound: FixedWidthInteger { #if !$Embedded + /// Creates a new half-open range by parsing the start and count of the range, + /// using the given parser closure for each value. + /// + /// Use this initializer to parse a range from data when the start and count + /// of the range both use the same storage. Alternatively, you can parse + /// the two values separately and then use optional operators to safely + /// construct the range. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. + /// - parser: The closure to use when parsing the start and count. + /// - Throws: An error if `parser` throws an error. @lifetime(&input) public init( parsingStartAndCount input: inout ParserSpan, @@ -29,6 +41,18 @@ extension Range where Bound: FixedWidthInteger { } #endif + /// Creates a new half-open range by parsing the start and count of the range, + /// using the given parser closure for each value. + /// + /// Use this initializer to parse a range from data when the start and count + /// of the range both use the same storage. Alternatively, you can parse + /// the two values separately and then use optional operators to safely + /// construct the range. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. + /// - parser: The closure to use when parsing the start and count. + /// - Throws: An error if `parser` throws an error. @lifetime(&input) public init( parsingStartAndCount input: inout ParserSpan, @@ -93,6 +117,18 @@ extension ClosedRange where Bound: FixedWidthInteger { extension Range { #if !$Embedded + /// Creates a new half-open range by parsing the start and end of the range, + /// using the given parser closure for each value. + /// + /// Use this initializer to parse a range from data when the start and end + /// of the range both use the same storage. Alternatively, you can parse + /// the two values separately and then use optional operators to safely + /// construct the range. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. + /// - parser: The closure to use when parsing the start and end. + /// - Throws: An error if `parser` throws an error. @lifetime(&input) public init( parsingStartAndEnd input: inout ParserSpan, @@ -109,6 +145,18 @@ extension Range { } #endif + /// Creates a new half-open range by parsing the start and end of the range, + /// using the given parser closure for each value. + /// + /// Use this initializer to parse a range from data when the start and end + /// of the range both use the same storage. Alternatively, you can parse + /// the two values separately and then use optional operators to safely + /// construct the range. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. + /// - parser: The closure to use when parsing the start and end. + /// - Throws: An error if `parser` throws an error. @lifetime(&input) public init( parsingStartAndEnd input: inout ParserSpan, @@ -127,6 +175,18 @@ extension Range { extension ClosedRange { #if !$Embedded + /// Creates a new closed range by parsing the start and end of the range, + /// using the given parser closure for each value. + /// + /// Use this initializer to parse a range from data when the start and end + /// of the range both use the same storage. Alternatively, you can parse + /// the two values separately and then use optional operators to safely + /// construct the range. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. + /// - parser: The closure to use when parsing the start and end. + /// - Throws: An error if `parser` throws an error. @lifetime(&input) public init( parsingStartAndEnd input: inout ParserSpan, @@ -143,6 +203,18 @@ extension ClosedRange { } #endif + /// Creates a new closed range by parsing the start and end of the range, + /// using the given parser closure for each value. + /// + /// Use this initializer to parse a range from data when the start and end + /// of the range both use the same storage. Alternatively, you can parse + /// the two values separately and then use optional operators to safely + /// construct the range. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. + /// - parser: The closure to use when parsing the start and end. + /// - Throws: An error if `parser` throws an error. @lifetime(&input) public init( parsingStartAndEnd input: inout ParserSpan, diff --git a/Sources/BinaryParsing/Parsers/String.swift b/Sources/BinaryParsing/Parsers/String.swift index e78b61a..6f14b1f 100644 --- a/Sources/BinaryParsing/Parsers/String.swift +++ b/Sources/BinaryParsing/Parsers/String.swift @@ -10,6 +10,10 @@ //===----------------------------------------------------------------------===// extension String { + /// Parses a nul-terminated UTF-8 string from the start of the given parser. + /// + /// The bytes of the string and the NUL are all consumed from `input`. This + /// initializer throws an error if `input` does not contain a NUL byte. @inlinable @lifetime(&input) public init(parsingNulTerminated input: inout ParserSpan) throws(ParsingError) @@ -25,22 +29,33 @@ extension String { _ = unsafe input.consumeUnchecked() } + /// Parses a UTF-8 string from the entire contents of the given parser. + /// + /// Unlike most parsers, this initializer does not throw. Any invalid UTF-8 + /// code units are repaired by replacing with the Unicode replacement + /// character `U+FFFD`. @inlinable @lifetime(&input) - public init(parsingUTF8 input: inout ParserSpan) throws(ParsingError) { + public init(parsingUTF8 input: inout ParserSpan) { let stringBytes = input.divide(at: input.endPosition) self = unsafe stringBytes.withUnsafeBytes { buffer in unsafe String(decoding: buffer, as: UTF8.self) } } + /// Parses a UTF-8 string from the specified number of bytes at the start of + /// the given parser. + /// + /// This initializer throws if `input` doesn't have the number of bytes + /// required by `count`. Any invalid UTF-8 code units are repaired by + /// replacing with the Unicode replacement character `U+FFFD`. @inlinable @lifetime(&input) public init(parsingUTF8 input: inout ParserSpan, count: Int) throws(ParsingError) { var slice = try input._divide(atByteOffset: count) - try self.init(parsingUTF8: &slice) + self.init(parsingUTF8: &slice) } @unsafe @@ -56,6 +71,12 @@ extension String { } } + /// Parses a UTF-16 string from the entire contents of the given parser. + /// + /// This initializer throws if the span has an odd count, and therefore can't + /// be interpreted as a series of `UInt16` values. Any invalid UTF-16 code + /// units or incomplete surrogate pairs are repaired by replacing with the + /// Unicode replacement character `U+FFFD`. @inlinable @lifetime(&input) public init(parsingUTF16 input: inout ParserSpan) throws(ParsingError) { @@ -65,6 +86,20 @@ extension String { unsafe try self.init(_uncheckedParsingUTF16: &input) } + /// Parses a UTF-16 string from the specified number of code units at the + /// start of the given parser. + /// + /// This initializer throws if `input` doesn't have the number of bytes + /// required by `codeUnitCount`. Any invalid UTF-16 code units or incomplete + /// surrogate pairs are repaired by replacing with the Unicode replacement + /// character `U+FFFD`. + /// + /// - Parameters: + /// - input: The parser span to parse the string from. `input` must have at + /// least `2 * codeUnitCount` bytes remaining. + /// - codeUnitCount: The number of UTF-16 code units to read from `input`. + /// - Throws: A `ParsingError` if `input` doesn't have at least + /// `2 * codeUnitCount` bytes remaining. @inlinable @lifetime(&input) public init(parsingUTF16 input: inout ParserSpan, codeUnitCount: Int) diff --git a/Sources/BinaryParsingMacros/Extensions.swift b/Sources/BinaryParsingMacros/Extensions.swift index 7afc0fe..99c1f1f 100644 --- a/Sources/BinaryParsingMacros/Extensions.swift +++ b/Sources/BinaryParsingMacros/Extensions.swift @@ -53,11 +53,11 @@ extension KeyPathExprSyntax { /// For example, if `typeName` is `"Foo"` and this key path expression is /// `\Foo.bar.baz`, this method returns `["bar", "baz"]`. /// - /// Since nested types aren't distinguished from property names in the syntax, - /// type names may be returned from this function. For example, in the key - /// path expression `\Int.Magnitude.bitWidth`, the first key path component - /// is the type name `Magnitude`, but calling this function with a `typeName` - /// of `"Int"` will yield `["Magnitude", "bitWidth"]`. + /// Since nested types aren't distinguished from property names in the + /// syntax, type names may be returned from this function. For example, in + /// the key path expression `\Int.Magnitude.bitWidth`, the first key path + /// component is the type name `Magnitude`, but calling this function with a + /// `typeName` of `"Int"` will yield `["Magnitude", "bitWidth"]`. func propertyNames(afterTypeName typeName: String) -> [String]? { // Break apart the type name (e.g. `Foo.Bar` -> [Foo, Bar]) and match the root let typeNameParts = typeName.split(separator: ".") diff --git a/Tests/BinaryParsingTests/IntegerParsingTests.swift b/Tests/BinaryParsingTests/IntegerParsingTests.swift index 12df29a..5c47a48 100644 --- a/Tests/BinaryParsingTests/IntegerParsingTests.swift +++ b/Tests/BinaryParsingTests/IntegerParsingTests.swift @@ -456,8 +456,8 @@ struct IntegerParsingTests { } } - /// Performs tests on all the different permutations of integer types - /// and storage types. + /// Performs tests on all the different permutations of integer types and + /// storage types. @Test func integerFuzzing() throws { var rng = getSeededRNG()