A fast, safe, and efficient CBOR serialization library for Swift on any platform. swiftpackageindex.com/thecoolwinter/CBOR/1.1.1/documentation/cbor
atproto swift cbor

Introduce `decodeMultiple` API

+102 -1
+1 -1
FUZZ.md
··· 5 5 First build the package in release mode with the fuzzer and address checkers on: 6 6 7 7 ```bash 8 - swift build -c release --sanitize fuzzer,address 8 + swift build -c release --sanitize fuzzer 9 9 ``` 10 10 11 11 Then run it:
+50
Sources/CBOR/Decoder/CBORDecoder.swift
··· 68 68 } 69 69 } 70 70 71 + /// Decodes multiple instances of the given type from CBOR binary data. 72 + /// 73 + /// Some BLOBs are made up of multiple CBOR-encoded datas concatenated without valid CBOR dividers (eg in an array 74 + /// container). This method decodes that kind of data. It will attempt to decode an instance of the given type, 75 + /// once done, if there's more data, it will continue to attempt to decode more instances. 76 + /// 77 + /// - Parameters: 78 + /// - type: The decodable type to deserialize. 79 + /// - data: The CBOR data to decode from. 80 + /// - Returns: An instance of the decoded type. 81 + /// - Throws: A ``DecodingError`` with context and a debug description for a failed deserialization operation. 82 + public func decodeMultiple<T: Decodable>(_ type: T.Type, from data: Data) throws -> [T] { 83 + do { 84 + return try data.withUnsafeBytes { 85 + let data = $0[...] 86 + let reader = DataReader(data: data) 87 + let scanner = CBORScanner(data: reader, options: options) 88 + let results = try scanner.scan() 89 + 90 + guard !results.isEmpty else { 91 + throw ScanError.unexpectedEndOfData 92 + } 93 + 94 + let context = DecodingContext(options: options, results: results) 95 + var nextRegion: DataRegion? = results.load(at: 0) 96 + 97 + var accumulator: [T] = [] 98 + 99 + while let region = nextRegion { 100 + let value = try SingleValueCBORDecodingContainer(context: context, data: region).decode(T.self) 101 + accumulator.append(value) 102 + let nextMapIndex = results.siblingIndex(region.mapOffset) 103 + if nextMapIndex < results.count { 104 + nextRegion = results.load(at: results.siblingIndex(region.mapOffset)) 105 + } else { 106 + nextRegion = nil 107 + } 108 + } 109 + 110 + return accumulator 111 + } 112 + } catch { 113 + if let error = error as? ScanError { 114 + try throwScanError(error) 115 + } else { 116 + throw error 117 + } 118 + } 119 + } 120 + 71 121 private func throwScanError(_ error: ScanError) throws -> Never { 72 122 switch error { 73 123 case .unexpectedEndOfData:
+4
Sources/CBOR/Decoder/Scanner/CBORScanner+Results.swift
··· 25 25 private var map: [Int] = [] 26 26 private var reader: DataReader 27 27 28 + var count: Int { 29 + map.count 30 + } 31 + 28 32 var isEmpty: Bool { 29 33 map.isEmpty 30 34 }
+3
Sources/Fuzzing/main.swift
··· 26 26 func tryDecode<T: Decodable>(_ type: T.Type) { 27 27 do { 28 28 blackhole(try CBORDecoder(rejectIndeterminateLengths: false).decode(T.self, from: data)) 29 + blackhole(try CBORDecoder(rejectIndeterminateLengths: true).decode(T.self, from: data)) 30 + blackhole(try CBORDecoder(rejectIndeterminateLengths: false).decodeMultiple(T.self, from: data)) 31 + blackhole(try CBORDecoder(rejectIndeterminateLengths: true).decodeMultiple(T.self, from: data)) 29 32 } catch { 30 33 // ignore decode errors 31 34 }
+44
Tests/CBORTests/DecodeMultipleTests.swift
··· 1 + // 2 + // DecodeMultipleTests.swift 3 + // CBOR 4 + // 5 + // Created by Khan Winter on 9/10/25. 6 + // 7 + 8 + import Testing 9 + #if canImport(FoundationEssentials) 10 + import FoundationEssentials 11 + #else 12 + import Foundation 13 + #endif 14 + @testable import CBOR 15 + 16 + @Suite 17 + struct DecodeMultipleTests { 18 + @Test 19 + func decodeMultipleInts() throws { 20 + var value: [UInt8] = try CBORDecoder().decodeMultiple(UInt8.self, from: [0]) 21 + #expect(value == [0]) 22 + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [1, 1]) 23 + #expect(value == [1, 1]) 24 + // Just below max arg size 25 + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [23, 23]) 26 + #expect(value == [23, 23]) 27 + // Just above max arg size 28 + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, 24, 24, 24]) 29 + #expect(value == [24, 24]) 30 + // Max Int 31 + value = try CBORDecoder().decodeMultiple(UInt8.self, from: [24, UInt8.max]) 32 + #expect(value == [UInt8.max]) 33 + 34 + #expect(throws: DecodingError.self) { try CBORDecoder().decodeMultiple(UInt8.self, from: [128, 128]) } 35 + #expect(throws: DecodingError.self) { try CBORDecoder().decodeMultiple(UInt8.self, from: [23, 128]) } 36 + } 37 + 38 + @Test 39 + func mutlipleMaps() throws { 40 + let data = "A262414201614102A262414201614102".asHexData() 41 + let dictionary = try CBORDecoder().decodeMultiple([String: Int].self, from: data) 42 + #expect(dictionary == [["AB": 1, "A": 2], ["AB": 1, "A": 2]]) 43 + } 44 + }