From a354597a97940de792fb4faaf0a26c17a2631686 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:22:06 -0500 Subject: [PATCH] Introduce `decodeMultiple` API --- FUZZ.md | 2 +- Sources/CBOR/Decoder/CBORDecoder.swift | 50 +++++++++++++++++++ .../Decoder/Scanner/CBORScanner+Results.swift | 4 ++ Sources/Fuzzing/main.swift | 3 ++ Tests/CBORTests/DecodeMultipleTests.swift | 44 ++++++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 Tests/CBORTests/DecodeMultipleTests.swift diff --git a/FUZZ.md b/FUZZ.md index 2f523d8..0b82aec 100644 --- a/FUZZ.md +++ b/FUZZ.md @@ -5,7 +5,7 @@ The `Fuzzing` target allows the library to be fuzzed using libfuzzer. First build the package in release mode with the fuzzer and address checkers on: ```bash -swift build -c release --sanitize fuzzer,address +swift build -c release --sanitize fuzzer ``` Then run it: diff --git a/Sources/CBOR/Decoder/CBORDecoder.swift b/Sources/CBOR/Decoder/CBORDecoder.swift index d77db10..bdb5ab8 100644 --- a/Sources/CBOR/Decoder/CBORDecoder.swift +++ b/Sources/CBOR/Decoder/CBORDecoder.swift @@ -68,6 +68,56 @@ public struct CBORDecoder { } } + /// Decodes multiple instances of the given type from CBOR binary data. + /// + /// Some BLOBs are made up of multiple CBOR-encoded datas concatenated without valid CBOR dividers (eg in an array + /// container). This method decodes that kind of data. It will attempt to decode an instance of the given type, + /// once done, if there's more data, it will continue to attempt to decode more instances. + /// + /// - Parameters: + /// - type: The decodable type to deserialize. + /// - data: The CBOR data to decode from. + /// - Returns: An instance of the decoded type. + /// - Throws: A ``DecodingError`` with context and a debug description for a failed deserialization operation. + public func decodeMultiple(_ type: T.Type, from data: Data) throws -> [T] { + do { + return try data.withUnsafeBytes { + let data = $0[...] + let reader = DataReader(data: data) + let scanner = CBORScanner(data: reader, options: options) + let results = try scanner.scan() + + guard !results.isEmpty else { + throw ScanError.unexpectedEndOfData + } + + let context = DecodingContext(options: options, results: results) + var nextRegion: DataRegion? = results.load(at: 0) + + var accumulator: [T] = [] + + while let region = nextRegion { + let value = try SingleValueCBORDecodingContainer(context: context, data: region).decode(T.self) + accumulator.append(value) + let nextMapIndex = results.siblingIndex(region.mapOffset) + if nextMapIndex < results.count { + nextRegion = results.load(at: results.siblingIndex(region.mapOffset)) + } else { + nextRegion = nil + } + } + + return accumulator + } + } catch { + if let error = error as? ScanError { + try throwScanError(error) + } else { + throw error + } + } + } + private func throwScanError(_ error: ScanError) throws -> Never { switch error { case .unexpectedEndOfData: diff --git a/Sources/CBOR/Decoder/Scanner/CBORScanner+Results.swift b/Sources/CBOR/Decoder/Scanner/CBORScanner+Results.swift index e07e31e..8fdc360 100644 --- a/Sources/CBOR/Decoder/Scanner/CBORScanner+Results.swift +++ b/Sources/CBOR/Decoder/Scanner/CBORScanner+Results.swift @@ -25,6 +25,10 @@ extension CBORScanner { private var map: [Int] = [] private var reader: DataReader + var count: Int { + map.count + } + var isEmpty: Bool { map.isEmpty } diff --git a/Sources/Fuzzing/main.swift b/Sources/Fuzzing/main.swift index 5da6163..01489e5 100644 --- a/Sources/Fuzzing/main.swift +++ b/Sources/Fuzzing/main.swift @@ -26,6 +26,9 @@ public func fuzz(_ start: UnsafeRawPointer, _ count: Int) -> CInt { func tryDecode(_ type: T.Type) { do { blackhole(try CBORDecoder(rejectIndeterminateLengths: false).decode(T.self, from: data)) + blackhole(try CBORDecoder(rejectIndeterminateLengths: true).decode(T.self, from: data)) + blackhole(try CBORDecoder(rejectIndeterminateLengths: false).decodeMultiple(T.self, from: data)) + blackhole(try CBORDecoder(rejectIndeterminateLengths: true).decodeMultiple(T.self, from: data)) } catch { // ignore decode errors } diff --git a/Tests/CBORTests/DecodeMultipleTests.swift b/Tests/CBORTests/DecodeMultipleTests.swift new file mode 100644 index 0000000..0c89f88 --- /dev/null +++ b/Tests/CBORTests/DecodeMultipleTests.swift @@ -0,0 +1,44 @@ +// +// DecodeMultipleTests.swift +// CBOR +// +// Created by Khan Winter on 9/10/25. +// + +import Testing +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +@testable import CBOR + +@Suite +struct DecodeMultipleTests { + @Test + func decodeMultipleInts() throws { + var value: [UInt8] = try CBORDecoder().decodeMultiple(UInt8.self, from: [0]) + #expect(value == [0]) + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [1, 1]) + #expect(value == [1, 1]) + // Just below max arg size + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [23, 23]) + #expect(value == [23, 23]) + // Just above max arg size + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, 24, 24, 24]) + #expect(value == [24, 24]) + // Max Int + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, UInt8.max]) + #expect(value == [UInt8.max]) + + #expect(throws: DecodingError.self) { try CBORDecoder().decodeMultiple(UInt8.self, from: [128, 128]) } + #expect(throws: DecodingError.self) { try CBORDecoder().decodeMultiple(UInt8.self, from: [23, 128]) } + } + + @Test + func mutlipleMaps() throws { + let data = "A262414201614102A262414201614102".asHexData() + let dictionary = try CBORDecoder().decodeMultiple([String: Int].self, from: data) + #expect(dictionary == [["AB": 1, "A": 2], ["AB": 1, "A": 2]]) + } +} -- 2.51.2