Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions Sources/AnyCodingKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ struct AnyCodingKey: CodingKey, Equatable {

func key<K: CodingKey>() -> K {
if let intValue = self.intValue {
return K(intValue: intValue)!
guard let key = K(intValue: intValue) else {
preconditionFailure("CodingKey \(K.self) failed to initialize with intValue: \(intValue)")
}
return key
} else if let stringValue = self._stringValue {
return K(stringValue: stringValue)!
guard let key = K(stringValue: stringValue) else {
preconditionFailure("CodingKey \(K.self) failed to initialize with stringValue: \(stringValue)")
}
return key
} else {
fatalError("AnyCodingKey created without a string or int value")
preconditionFailure("AnyCodingKey created without a string or int value")
}
}
}
Expand All @@ -49,7 +55,13 @@ extension AnyCodingKey: Encodable {
} else if let stringValue = self._stringValue {
try container.encode(stringValue)
} else {
fatalError("AnyCodingKey created without a string or int value")
throw EncodingError.invalidValue(
self,
EncodingError.Context(
codingPath: encoder.codingPath,
debugDescription: "AnyCodingKey created without a string or int value"
)
)
}
}
}
Expand All @@ -60,9 +72,17 @@ extension AnyCodingKey: Decodable {
if let intValue = try? value.decode(Int.self) {
self._stringValue = nil
self.intValue = intValue
} else {
self._stringValue = try! value.decode(String.self)
} else if let stringValue = try? value.decode(String.self) {
self._stringValue = stringValue
self.intValue = nil
} else {
throw DecodingError.typeMismatch(
AnyCodingKey.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected Int or String key"
)
)
}
}
}
4 changes: 2 additions & 2 deletions Sources/CBOR.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ public indirect enum CBOR : Equatable, Hashable,
set(x) {
switch (self, position) {
case (var .array(l), let .unsignedInt(i)):
l[Int(i)] = x!
l[Int(i)] = x ?? .null // nil becomes CBOR.null
self = .array(l)
case (var .map(l), let i):
l[i] = x!
l[i] = x // nil removes the key from dictionary
self = .map(l)
default: break
}
Expand Down
16 changes: 10 additions & 6 deletions Sources/CBORDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,13 @@ public class CBORDecoder {
}

private func readN(_ n: Int) throws -> [CBOR] {
return try (0..<n).map { _ in
guard let r = try decodeItem() else { throw CBORError.unfinishedSequence }
return r
var result: [CBOR] = []
result.reserveCapacity(min(n, 1024)) // Reserve capacity but cap at reasonable size
for _ in 0..<n {
guard let item = try decodeItem() else { throw CBORError.unfinishedSequence }
result.append(item)
}
return result
}

func readUntilBreak() throws -> [CBOR] {
Expand Down Expand Up @@ -110,9 +113,10 @@ public class CBORDecoder {
}

public func decodeItem() throws -> CBOR? {
guard currentDepth <= options.maximumDepth
else { throw CBORError.maximumDepthExceeded }

guard currentDepth <= options.maximumDepth else {
throw CBORError.maximumDepthExceeded
}

currentDepth += 1
defer { currentDepth -= 1 }
let b = try istream.popByte()
Expand Down
10 changes: 8 additions & 2 deletions Sources/CBOREncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension CBOR: CBOREncodable {
case let .byteString(bs): return CBOR.encodeByteString(bs, options: options)
case let .utf8String(str): return str.encode(options: options)
case let .array(a): return CBOR.encodeArray(a, options: options)
case let .map(m): return CBOR.encodeMap(m, options: options)
case let .map(m): return try! CBOR.encodeMap(m, options: options)
#if canImport(Foundation)
case let .date(d): return CBOR.encodeDate(d, options: options)
#endif
Expand Down Expand Up @@ -206,7 +206,13 @@ extension Array where Element: CBOREncodable {

extension Dictionary where Key: CBOREncodable, Value: CBOREncodable {
public func encode(options: CBOROptions = CBOROptions()) -> [UInt8] {
return CBOR.encodeMap(self, options: options)
do {
return try CBOR.encodeMap(self, options: options)
} catch {
// This can only fail if forbidNonStringMapKeys is true and Key is not a String.
// This is a programming error, not a data error.
preconditionFailure("Failed to encode dictionary with key type \(Key.self): \(error)")
}
}

public func toCBOR(options: CBOROptions = CBOROptions()) -> CBOR {
Expand Down
20 changes: 14 additions & 6 deletions Sources/CBOREncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ extension CBOR {

array.withUnsafeBytes { bufferPtr in
guard let ptr = bufferPtr.baseAddress?.bindMemory(to: UInt8.self, capacity: bytelength) else {
fatalError("Invalid pointer")
// This should never happen with valid Swift arrays, but handle it gracefully
preconditionFailure("Failed to get pointer to array memory")
}
var j = 0
for i in 0..<bytelength {
Expand All @@ -52,7 +53,13 @@ extension CBOR {
}

public static func encode<A: CBOREncodable, B: CBOREncodable>(_ dict: [A: B], options: CBOROptions = CBOROptions()) -> [UInt8] {
return encodeMap(dict, options: options)
do {
return try encodeMap(dict, options: options)
} catch {
// This can only fail if forbidNonStringMapKeys is true and A is not a String.
// This is a programming error, not a data error.
preconditionFailure("Failed to encode dictionary with key type \(A.self): \(error)")
}
}

// MARK: - major 0: unsigned integer
Expand Down Expand Up @@ -128,9 +135,9 @@ extension CBOR {

// MARK: - major 5: a map of pairs of data items

public static func encodeMap<A: CBOREncodable, B: CBOREncodable>(_ map: [A: B], options: CBOROptions = CBOROptions()) -> [UInt8] {
public static func encodeMap<A: CBOREncodable, B: CBOREncodable>(_ map: [A: B], options: CBOROptions = CBOROptions()) throws -> [UInt8] {
if options.forbidNonStringMapKeys {
try! ensureStringKey(A.self)
try ensureStringKey(A.self)
}
var res: [UInt8] = []
res.reserveCapacity(1 + map.count * (MemoryLayout<A>.size + MemoryLayout<B>.size + 2))
Expand Down Expand Up @@ -253,9 +260,9 @@ extension CBOR {
return res
}

public static func encodeMapChunk<A: CBOREncodable, B: CBOREncodable>(_ map: [A: B], options: CBOROptions = CBOROptions()) -> [UInt8] {
public static func encodeMapChunk<A: CBOREncodable, B: CBOREncodable>(_ map: [A: B], options: CBOROptions = CBOROptions()) throws -> [UInt8] {
if options.forbidNonStringMapKeys {
try! ensureStringKey(A.self)
try ensureStringKey(A.self)
}
var res: [UInt8] = []
let count = map.count
Expand Down Expand Up @@ -292,6 +299,7 @@ extension CBOR {
AnnotatedMapDateStrategy.typeKey: AnnotatedMapDateStrategy.typeValue,
AnnotatedMapDateStrategy.valueKey: dateCBOR
]
// String keys are guaranteed, so this should never throw
return try! CBOR.encodeMap(map, options: options)
case .taggedAsEpochTimestamp:
var res: [UInt8] = [0b110_00001] // Epoch timestamp tag is 1
Expand Down
7 changes: 4 additions & 3 deletions Sources/CBOROptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ public struct CBOROptions {
let useStringKeys: Bool
let dateStrategy: DateStrategy
let forbidNonStringMapKeys: Bool
/// The maximum number of nested items, inclusive, to decode. A maximum set to 0 dissallows anything other than top-level primitives.
// The maximum number of nested items, inclusive, to decode. A maximum set to 0 disallows
// anything other than top-level primitives.
let maximumDepth: Int
let shouldSortMapKeys: Bool

Expand All @@ -11,13 +12,13 @@ public struct CBOROptions {
dateStrategy: DateStrategy = .taggedAsEpochTimestamp,
forbidNonStringMapKeys: Bool = false,
maximumDepth: Int = .max,
shouldShortMapKeys: Bool = true
shouldSortMapKeys: Bool = true
) {
self.useStringKeys = useStringKeys
self.dateStrategy = dateStrategy
self.forbidNonStringMapKeys = forbidNonStringMapKeys
self.maximumDepth = maximumDepth
self.shouldSortMapKeys = shouldShortMapKeys
self.shouldSortMapKeys = shouldSortMapKeys
}

func toCodableEncoderOptions() -> CodableCBOREncoder._Options {
Expand Down
30 changes: 25 additions & 5 deletions Sources/Decoder/CodableCBORDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation
final public class CodableCBORDecoder {
public var useStringKeys: Bool = false
public var dateStrategy: DateStrategy = .taggedAsEpochTimestamp
public var maximumDepth: Int = .max

struct _Options {
let useStringKeys: Bool
Expand All @@ -29,7 +30,7 @@ final public class CodableCBORDecoder {
}

var options: _Options {
return _Options(useStringKeys: self.useStringKeys, dateStrategy: self.dateStrategy)
return _Options(useStringKeys: self.useStringKeys, dateStrategy: self.dateStrategy, maximumDepth: self.maximumDepth)
}

public init() {}
Expand Down Expand Up @@ -66,6 +67,7 @@ final public class CodableCBORDecoder {
func setOptions(_ newOptions: _Options) {
self.useStringKeys = newOptions.useStringKeys
self.dateStrategy = newOptions.dateStrategy
self.maximumDepth = newOptions.maximumDepth
}
}

Expand All @@ -78,34 +80,52 @@ final class _CBORDecoder {
fileprivate var data: ArraySlice<UInt8>

let options: CodableCBORDecoder._Options
var currentDepth: Int

init(data: ArraySlice<UInt8>, options: CodableCBORDecoder._Options) {
init(data: ArraySlice<UInt8>, options: CodableCBORDecoder._Options, currentDepth: Int = 0) {
self.data = data
self.options = options
self.currentDepth = currentDepth
}
}

extension _CBORDecoder: Decoder {
func container<Key: CodingKey>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
guard self.currentDepth < self.options.maximumDepth else {
let context = DecodingError.Context(
codingPath: self.codingPath,
debugDescription: "Maximum decoding depth of \(self.options.maximumDepth) exceeded"
)
throw DecodingError.dataCorrupted(context)
}

try ensureMap(self.data.first, keyType: Key.self)

let container = KeyedContainer<Key>(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
let container = KeyedContainer<Key>(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
self.container = container

return KeyedDecodingContainer(container)
}

func unkeyedContainer() throws -> UnkeyedDecodingContainer {
guard self.currentDepth < self.options.maximumDepth else {
let context = DecodingError.Context(
codingPath: self.codingPath,
debugDescription: "Maximum decoding depth of \(self.options.maximumDepth) exceeded"
)
throw DecodingError.dataCorrupted(context)
}

try ensureArray(self.data.first)

let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
let container = UnkeyedContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
self.container = container

return container
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
let container = SingleValueContainer(data: self.data, codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
self.container = container

return container
Expand Down
33 changes: 21 additions & 12 deletions Sources/Decoder/KeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ extension _CBORDecoder {
var codingPath: [CodingKey]
var userInfo: [CodingUserInfoKey: Any]
let options: CodableCBORDecoder._Options
let currentDepth: Int

init(data: ArraySlice<UInt8>, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options) {
init(data: ArraySlice<UInt8>, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], options: CodableCBORDecoder._Options, currentDepth: Int = 0) {
self.codingPath = codingPath
self.userInfo = userInfo
self.data = data
self.index = self.data.startIndex
self.options = options
self.currentDepth = currentDepth
}

func checkCanDecodeValue(forKey key: Key) throws {
Expand All @@ -41,23 +43,28 @@ extension _CBORDecoder {

var nestedContainers: [AnyCodingKey: CBORDecodingContainer] = [:]

let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo, options: self.options)
let unkeyedContainer = UnkeyedContainer(data: self.data.suffix(from: self.index), codingPath: self.codingPath, userInfo: self.userInfo, options: self.options, currentDepth: self.currentDepth)
unkeyedContainer.count = count * 2

var iterator = unkeyedContainer.nestedContainers.makeIterator()

for _ in 0..<count {
guard let keyContainer = iterator.next() as? _CBORDecoder.SingleValueContainer,
let container = iterator.next() else {
fatalError() // FIXME
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: self.codingPath,
debugDescription: "Malformed map data: expected key-value pairs"
)
)
}

let keyVal: AnyCodingKey
if self.options.useStringKeys {
let stringKey = try! keyContainer.decode(String.self)
let stringKey = try keyContainer.decode(String.self)
keyVal = AnyCodingKey(stringValue: stringKey)
} else {
keyVal = try! keyContainer.decode(AnyCodingKey.self)
keyVal = try keyContainer.decode(AnyCodingKey.self)
}
nestedContainers[keyVal] = container
}
Expand Down Expand Up @@ -89,7 +96,7 @@ extension _CBORDecoder {
// each key-value pair in the map.
let nextIndex = self.data.startIndex.advanced(by: 1)
let remainingData = self.data.suffix(from: nextIndex)
count = try? CBORDecoder(input: remainingData.map { $0 }).readPairsUntilBreak().keys.count
count = try? CBORDecoder(input: remainingData.map { $0 }, options: self.options.toCBOROptions()).readPairsUntilBreak().keys.count
default:
let context = DecodingError.Context(
codingPath: self.codingPath,
Expand Down Expand Up @@ -140,9 +147,10 @@ extension _CBORDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
try checkCanDecodeValue(forKey: key)

let container = try self.nestedContainers()[anyCodingKeyForKey(key)]!
let decoder = CodableCBORDecoder()
decoder.setOptions(self.options)
return try decoder.decode(T.self, from: container.data)
let innerDecoder = _CBORDecoder(data: container.data, options: self.options, currentDepth: self.currentDepth + 1)
innerDecoder.codingPath = self.codingPath + [key]
innerDecoder.userInfo = self.userInfo
return try T(from: innerDecoder)
}

func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
Expand All @@ -165,17 +173,18 @@ extension _CBORDecoder.KeyedContainer: KeyedDecodingContainerProtocol {
data: anyCodingKeyedContainer.data,
codingPath: anyCodingKeyedContainer.codingPath,
userInfo: anyCodingKeyedContainer.userInfo,
options: anyCodingKeyedContainer.options
options: anyCodingKeyedContainer.options,
currentDepth: anyCodingKeyedContainer.currentDepth
)
return KeyedDecodingContainer(container)
}

func superDecoder() throws -> Decoder {
return _CBORDecoder(data: self.data, options: self.options)
return _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1)
}

func superDecoder(forKey key: Key) throws -> Decoder {
let decoder = _CBORDecoder(data: self.data, options: self.options)
let decoder = _CBORDecoder(data: self.data, options: self.options, currentDepth: self.currentDepth + 1)
decoder.codingPath = [key]

return decoder
Expand Down
Loading