diff --git a/Sources/BinaryParsing/Parser Types/ParserSource.swift b/Sources/BinaryParsing/Parser Types/ParserSource.swift index 9ad8c6c..545f7f9 100644 --- a/Sources/BinaryParsing/Parser Types/ParserSource.swift +++ b/Sources/BinaryParsing/Parser Types/ParserSource.swift @@ -19,6 +19,15 @@ public protocol ExpressibleByParsing { } extension ExpressibleByParsing { + @_alwaysEmitIntoClient + public init( + parsing data: some ParserSpanProvider + ) throws(ThrownParsingError) { + self = try data.withParserSpan(Self.init(parsing:)) + } + + @_alwaysEmitIntoClient + @_disfavoredOverload public init(parsing data: some RandomAccessCollection) throws(ThrownParsingError) { @@ -41,28 +50,26 @@ extension RandomAccessCollection { ) throws(ThrownParsingError) -> T? { #if canImport(Foundation) if let data = self as? Foundation.Data { - do { - return try data.withUnsafeBytes { buffer -> T in - var span = ParserSpan(_unsafeBytes: buffer) - return try body(&span) - } - } catch { - // Workaround for lack of typed-throwing API on Data - // swift-format-ignore: NeverForceUnwrap - throw error as! ThrownParsingError + let result = data.withUnsafeBytes { buffer in + var span = ParserSpan(_unsafeBytes: buffer) + return Result { try body(&span) } + } + switch result { + case .success(let t): return t + case .failure(let e): throw e } } #endif - do { - return try self.withContiguousStorageIfAvailable { buffer in - let rawBuffer = UnsafeRawBufferPointer(buffer) - var span = ParserSpan(_unsafeBytes: rawBuffer) - return try body(&span) - } - } catch { - // Workaround for lack of typed-throwing API on Collection - // swift-format-ignore: NeverForceUnwrap - throw error as! ThrownParsingError + + let result = self.withContiguousStorageIfAvailable { buffer in + let rawBuffer = UnsafeRawBufferPointer(buffer) + var span = ParserSpan(_unsafeBytes: rawBuffer) + return Result { try body(&span) } + } + switch result { + case .success(let t): return t + case .failure(let e): throw e + case nil: return nil } } } @@ -70,83 +77,85 @@ extension RandomAccessCollection { // MARK: ParserSpanProvider public protocol ParserSpanProvider { - func withParserSpan( - _ body: (inout ParserSpan) throws(ThrownParsingError) -> T - ) throws(ThrownParsingError) -> T + func withParserSpan( + _ body: (inout ParserSpan) throws(E) -> T + ) throws(E) -> T } -#if canImport(Foundation) -extension Data: ParserSpanProvider { +extension ParserSpanProvider { + #if !$Embedded + @_alwaysEmitIntoClient @inlinable public func withParserSpan( - _ body: (inout ParserSpan) throws(ThrownParsingError) -> T - ) throws(ThrownParsingError) -> T { - do { - return try withUnsafeBytes { buffer -> T in - // FIXME: RawSpan getter - // var span = ParserSpan(buffer.bytes) - var span = ParserSpan(_unsafeBytes: buffer) - return try body(&span) - } - } catch { - // Workaround for lack of typed-throwing API on Data - // swift-format-ignore: NeverForceUnwrap - throw error as! ThrownParsingError + usingRange range: inout ParserRange, + _ body: (inout ParserSpan) throws -> T + ) throws -> T { + try withParserSpan { span in + var subspan = try span.seeking(toRange: range) + defer { range = subspan.parserRange } + return try body(&subspan) } } + #endif @_alwaysEmitIntoClient @inlinable public func withParserSpan( usingRange range: inout ParserRange, - _ body: (inout ParserSpan) throws(ThrownParsingError) -> T - ) throws(ThrownParsingError) -> T { - do { - return try withUnsafeBytes { (buffer) throws(ThrownParsingError) -> T in - // FIXME: RawSpan getter - // var span = try ParserSpan(buffer.bytes) - var span = try ParserSpan(_unsafeBytes: buffer) - .seeking(toRange: range) - defer { - range = span.parserRange - } - return try body(&span) - } - } catch { - // Workaround for lack of typed-throwing API on Data - // swift-format-ignore: NeverForceUnwrap - throw error as! ThrownParsingError + _ body: (inout ParserSpan) throws(ParsingError) -> T + ) throws(ParsingError) -> T { + try withParserSpan { (span) throws(ParsingError) in + var subspan = try span.seeking(toRange: range) + defer { range = subspan.parserRange } + return try body(&subspan) } } } -#endif -extension ParserSpanProvider where Self: RandomAccessCollection { - @discardableResult +#if canImport(Foundation) +extension Data: ParserSpanProvider { @inlinable - public func withParserSpan( - _ body: (inout ParserSpan) throws(ThrownParsingError) -> T - ) throws(ThrownParsingError) -> T { - do { - guard - let result = try self.withContiguousStorageIfAvailable({ buffer in - // FIXME: RawSpan getter - // var span = ParserSpan(UnsafeRawBufferPointer(buffer).bytes) - let rawBuffer = UnsafeRawBufferPointer(buffer) - var span = ParserSpan(_unsafeBytes: rawBuffer) - return try body(&span) - }) - else { - throw ParsingError(status: .userError, location: 0) - } - return result - } catch { - // Workaround for lack of typed-throwing API on Collection - // swift-format-ignore: NeverForceUnwrap - throw error as! ThrownParsingError + public func withParserSpan( + _ body: (inout ParserSpan) throws(E) -> T + ) throws(E) -> T { + let result = withUnsafeBytes { buffer in + var span = ParserSpan(_unsafeBytes: buffer) + return Result { () throws(E) in try body(&span) } + } + switch result { + case .success(let t): return t + case .failure(let e): throw e + } + } +} +#endif + +extension [UInt8]: ParserSpanProvider { + public func withParserSpan( + _ body: (inout ParserSpan) throws(E) -> T + ) throws(E) -> T { + let result = self.withUnsafeBytes { rawBuffer in + var span = ParserSpan(_unsafeBytes: rawBuffer) + return Result { () throws(E) in try body(&span) } + } + switch result { + case .success(let t): return t + case .failure(let e): throw e } } } -extension [UInt8]: ParserSpanProvider {} -extension ArraySlice: ParserSpanProvider {} +extension ArraySlice: ParserSpanProvider { + public func withParserSpan( + _ body: (inout ParserSpan) throws(E) -> T + ) throws(E) -> T { + let result = self.withUnsafeBytes { rawBuffer in + var span = ParserSpan(_unsafeBytes: rawBuffer) + return Result { () throws(E) in try body(&span) } + } + switch result { + case .success(let t): return t + case .failure(let e): throw e + } + } +} diff --git a/Sources/BinaryParsing/Parser Types/ParsingError.swift b/Sources/BinaryParsing/Parser Types/ParsingError.swift index a20b89c..87e43d7 100644 --- a/Sources/BinaryParsing/Parser Types/ParsingError.swift +++ b/Sources/BinaryParsing/Parser Types/ParsingError.swift @@ -121,7 +121,7 @@ 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 take use error-throwing APIs in an embedded context. +/// to still 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/Parsers/Array.swift b/Sources/BinaryParsing/Parsers/Array.swift index 6dd9bce..1aab389 100644 --- a/Sources/BinaryParsing/Parsers/Array.swift +++ b/Sources/BinaryParsing/Parsers/Array.swift @@ -34,13 +34,14 @@ extension Array where Element == UInt8 { } extension Array { + #if !$Embedded @inlinable @lifetime(&input) public init( parsing input: inout ParserSpan, count: some FixedWidthInteger, - parser: (inout ParserSpan) throws(ThrownParsingError) -> Element - ) throws(ThrownParsingError) { + parser: (inout ParserSpan) throws -> Element + ) throws { let count = try Int(throwingOnOverflow: count) self = [] self.reserveCapacity(count) @@ -50,13 +51,30 @@ extension Array { try self.append(parser(&input)) } } + #endif @inlinable @lifetime(&input) - public init( + public init( + parsing input: inout ParserSpan, + count: Int, + parser: (inout ParserSpan) throws(E) -> Element + ) throws(E) { + self = [] + self.reserveCapacity(count) + // This doesn't throw (e.g. on empty) because `parser` can produce valid + // values no matter the state of `input`. + for _ in 0..( parsingAll input: inout ParserSpan, - parser: (inout ParserSpan) throws(ThrownParsingError) -> Element - ) throws(ThrownParsingError) { + parser: (inout ParserSpan) throws(E) -> Element + ) throws(E) { self = [] while !input.isEmpty { try self.append(parser(&input)) diff --git a/Sources/BinaryParsing/Parsers/Range.swift b/Sources/BinaryParsing/Parsers/Range.swift index 53b3439..27c7c40 100644 --- a/Sources/BinaryParsing/Parsers/Range.swift +++ b/Sources/BinaryParsing/Parsers/Range.swift @@ -12,11 +12,28 @@ // MARK: Start & Count extension Range where Bound: FixedWidthInteger { + #if !$Embedded @lifetime(&input) public init( parsingStartAndCount input: inout ParserSpan, - parser: (inout ParserSpan) throws(ThrownParsingError) -> Bound - ) throws(ThrownParsingError) { + parser: (inout ParserSpan) throws -> Bound + ) throws { + let start = try parser(&input) + let count = try parser(&input) + guard count >= 0, let end = start +? count else { + throw ParsingError( + status: .invalidValue, + location: input.startPosition) + } + self = Range(uncheckedBounds: (start, end)) + } + #endif + + @lifetime(&input) + public init( + parsingStartAndCount input: inout ParserSpan, + parser: (inout ParserSpan) throws(ParsingError) -> Bound + ) throws(ParsingError) { let start = try parser(&input) let count = try parser(&input) guard count >= 0, let end = start +? count else { @@ -29,22 +46,44 @@ extension Range where Bound: FixedWidthInteger { } extension ClosedRange where Bound: FixedWidthInteger { + #if !$Embedded @available( *, deprecated, message: "The behavior of this parser is unintuitive; instead, parse the start and count separately, then form the end of the closed range." ) - @lifetime(&input) + @lifetime(&_input) public init( - parsingStartAndCount input: inout ParserSpan, - parser: (inout ParserSpan) throws(ThrownParsingError) -> Bound - ) throws(ThrownParsingError) { - let start = try parser(&input) - let count = try parser(&input) + parsingStartAndCount _input: inout ParserSpan, + parser: (inout ParserSpan) throws -> Bound + ) throws { + let start = try parser(&_input) + let count = try parser(&_input) guard count > 0, let end = start +? count -? 1 else { throw ParsingError( status: .invalidValue, - location: input.startPosition) + location: _input.startPosition) + } + self = ClosedRange(uncheckedBounds: (start, end)) + } + #endif + + @available( + *, deprecated, + message: + "The behavior of this parser is unintuitive; instead, parse the start and count separately, then form the end of the closed range." + ) + @lifetime(&_input) + public init( + parsingStartAndCount _input: inout ParserSpan, + parser: (inout ParserSpan) throws(ParsingError) -> Bound + ) throws(ParsingError) { + let start = try parser(&_input) + let count = try parser(&_input) + guard count > 0, let end = start +? count -? 1 else { + throw ParsingError( + status: .invalidValue, + location: _input.startPosition) } self = ClosedRange(uncheckedBounds: (start, end)) } @@ -53,11 +92,28 @@ extension ClosedRange where Bound: FixedWidthInteger { // MARK: Start & End extension Range { + #if !$Embedded + @lifetime(&input) + public init( + parsingStartAndEnd input: inout ParserSpan, + boundsParser parser: (inout ParserSpan) throws -> Bound + ) throws { + let start = try parser(&input) + let end = try parser(&input) + guard start <= end else { + throw ParsingError( + status: .invalidValue, + location: input.startPosition) + } + self = Range(uncheckedBounds: (start, end)) + } + #endif + @lifetime(&input) public init( parsingStartAndEnd input: inout ParserSpan, - boundsParser parser: (inout ParserSpan) throws(ThrownParsingError) -> Bound - ) throws(ThrownParsingError) { + boundsParser parser: (inout ParserSpan) throws(ParsingError) -> Bound + ) throws(ParsingError) { let start = try parser(&input) let end = try parser(&input) guard start <= end else { @@ -70,11 +126,28 @@ extension Range { } extension ClosedRange { + #if !$Embedded + @lifetime(&input) + public init( + parsingStartAndEnd input: inout ParserSpan, + boundsParser parser: (inout ParserSpan) throws -> Bound + ) throws { + let start = try parser(&input) + let end = try parser(&input) + guard start <= end else { + throw ParsingError( + status: .invalidValue, + location: input.startPosition) + } + self = ClosedRange(uncheckedBounds: (start, end)) + } + #endif + @lifetime(&input) public init( parsingStartAndEnd input: inout ParserSpan, - boundsParser parser: (inout ParserSpan) throws(ThrownParsingError) -> Bound - ) throws(ThrownParsingError) { + boundsParser parser: (inout ParserSpan) throws(ParsingError) -> Bound + ) throws(ParsingError) { let start = try parser(&input) let end = try parser(&input) guard start <= end else { diff --git a/Tests/BinaryParsingTests/RangeParsingTests.swift b/Tests/BinaryParsingTests/RangeParsingTests.swift index 7d2e335..d1f3e4f 100644 --- a/Tests/BinaryParsingTests/RangeParsingTests.swift +++ b/Tests/BinaryParsingTests/RangeParsingTests.swift @@ -31,7 +31,7 @@ struct RangeParsingTests { #expect(range2 == 3..<7) } - _ = try buffer.withParserSpan { span in + buffer.withParserSpan { span in // Negative count #expect(throws: ParsingError.self) { try Range(parsingStartAndCount: &span) { span in @@ -39,6 +39,13 @@ struct RangeParsingTests { } } + // Invalid values, non-throwing closure + #expect(throws: ParsingError.self) { + try Range(parsingStartAndCount: &span) { span in + UInt64.max + } + } + // Insufficient data #expect(throws: ParsingError.self) { try Range( @@ -55,8 +62,7 @@ struct RangeParsingTests { } } - @Test - func startAndEnd() throws { + @Test func startAndEnd() throws { try buffer.withParserSpan { span in let range1 = try Range(parsingStartAndEnd: &span) { span in try Int16(parsingBigEndian: &span) @@ -68,7 +74,7 @@ struct RangeParsingTests { #expect(range2 == 3..<4) } - _ = try buffer.withParserSpan { span in + buffer.withParserSpan { span in // Reversed start and end #expect(throws: ParsingError.self) { try Range(parsingStartAndEnd: &span) { span in @@ -76,6 +82,13 @@ struct RangeParsingTests { } } + // Invalid ends, non-throwing closure + #expect(throws: ParsingError.self) { + try Range(parsingStartAndEnd: &span) { span in + Double.nan + } + } + // Insufficient data #expect(throws: ParsingError.self) { try Range( @@ -108,7 +121,7 @@ struct RangeParsingTests { #expect(range2 == 3...6) } - _ = try buffer.withParserSpan { span in + buffer.withParserSpan { span in // Reversed start and end #expect(throws: ParsingError.self) { try ClosedRange(parsingStartAndCount: &span) { span in @@ -116,6 +129,13 @@ struct RangeParsingTests { } } + // Invalid values, non-throwing closure + #expect(throws: ParsingError.self) { + try ClosedRange(parsingStartAndCount: &span) { span in + UInt64.max + } + } + // Insufficient data #expect(throws: ParsingError.self) { try ClosedRange( @@ -145,7 +165,7 @@ struct RangeParsingTests { #expect(range2 == 3...4) } - _ = try buffer.withParserSpan { span in + buffer.withParserSpan { span in // Reversed start and end #expect(throws: ParsingError.self) { try ClosedRange(parsingStartAndEnd: &span) { span in @@ -153,6 +173,13 @@ struct RangeParsingTests { } } + // Invalid ends, non-throwing closure + #expect(throws: ParsingError.self) { + try ClosedRange(parsingStartAndEnd: &span) { span in + Double.nan + } + } + // Insufficient data #expect(throws: ParsingError.self) { try ClosedRange(