Skip to content
Merged
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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ do {

```swift
import CBOR
import Foundation

// Define your data structures
struct Person: Codable {
Expand Down
100 changes: 44 additions & 56 deletions Sources/CBOR/CBOR.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#elseif os(Windows)
import ucrt
#endif

// MARK: - CBOR Type

/// A CBOR value
public indirect enum CBOR: Equatable {
public enum CBOR: Equatable, Sendable {
/// A positive unsigned integer
case unsignedInt(UInt64)
/// A negative integer
Expand All @@ -29,7 +25,7 @@ public indirect enum CBOR: Equatable {
/// A map of CBOR key-value pairs
case map([CBORMapPair])
/// A tagged CBOR value
case tagged(UInt64, CBOR)
indirect case tagged(UInt64, CBOR)
/// A simple value
case simple(UInt8)
/// A boolean value
Expand All @@ -42,14 +38,18 @@ public indirect enum CBOR: Equatable {
case float(Double)

/// Encodes the CBOR value to bytes
public func encode(into output: inout [UInt8]) {
_encode(self, into: &output)
}

public func encode() -> [UInt8] {
var output: [UInt8] = []
_encode(self, into: &output)
encode(into: &output)
return output
}

/// Decodes a CBOR value from bytes
public static func decode(_ bytes: [UInt8]) throws -> CBOR {
public static func decode(_ bytes: [UInt8]) throws(CBORError) -> CBOR {
var reader = CBORReader(data: bytes)
let value = try _decode(reader: &reader)

Expand All @@ -67,7 +67,7 @@ public indirect enum CBOR: Equatable {
/// - Parameters:
/// - key: The key of the pair
/// - value: The value of the pair
public struct CBORMapPair: Equatable {
public struct CBORMapPair: Equatable, Sendable {
public let key: CBOR
public let value: CBOR

Expand All @@ -84,6 +84,7 @@ public struct CBORMapPair: Equatable {
/// - Parameters:
/// - value: The CBOR value to encode
/// - output: The output buffer to write the encoded bytes to
@inline(__always)
private func _encode(_ value: CBOR, into output: inout [UInt8]) {
switch value {
case .unsignedInt(let u):
Expand All @@ -103,11 +104,9 @@ private func _encode(_ value: CBOR, into output: inout [UInt8]) {
encodeUnsigned(major: 2, value: UInt64(bytes.count), into: &output)
output.append(contentsOf: bytes)
case .textString(let string):
if let utf8 = string.data(using: .utf8) {
let bytes = [UInt8](utf8)
encodeUnsigned(major: 3, value: UInt64(bytes.count), into: &output)
output.append(contentsOf: bytes)
}
let bytes = [UInt8](string.utf8)
encodeUnsigned(major: 3, value: UInt64(bytes.count), into: &output)
output.append(contentsOf: bytes)
case .array(let array):
encodeUnsigned(major: 4, value: UInt64(array.count), into: &output)
for item in array {
Expand Down Expand Up @@ -138,8 +137,7 @@ private func _encode(_ value: CBOR, into output: inout [UInt8]) {
case .float(let f):
// Encode as IEEE 754 double-precision float
output.append(0xfb)
var value = f
withUnsafeBytes(of: &value) { bytes in
withUnsafeBytes(of: f) { bytes in
// Append bytes in big-endian order
for i in (0..<8).reversed() {
output.append(bytes[i])
Expand Down Expand Up @@ -192,7 +190,7 @@ private func encodeUnsigned(major: UInt8, value: UInt64, into output: inout [UIn
/// - reader: The reader to decode from
/// - Returns: The decoded CBOR value
/// - Throws: A `CBORError` if the decoding fails
private func _decode(reader: inout CBORReader) throws -> CBOR {
private func _decode(reader: inout CBORReader) throws(CBORError) -> CBOR {
let initial = try reader.readByte()

// Check for break marker (0xff)
Expand All @@ -214,16 +212,16 @@ private func _decode(reader: inout CBORReader) throws -> CBOR {

case 2: // byte string
let length = try readUIntValue(additional: additional, reader: &reader)
guard length <= UInt64(Int.max) else {
throw CBORError.lengthTooLarge(length)
guard length <= reader.maximumStringLength else {
throw CBORError.lengthTooLarge(length, maximum: reader.maximumStringLength)
}

return .byteString(try reader.readBytes(Int(length)))
return .byteString(Array(try reader.readBytes(Int(length))))

case 3: // text string
let length = try readUIntValue(additional: additional, reader: &reader)
guard length <= UInt64(Int.max) else {
throw CBORError.lengthTooLarge(length)
guard length <= reader.maximumStringLength else {
throw CBORError.lengthTooLarge(length, maximum: reader.maximumStringLength)
}

let bytes = try reader.readBytes(Int(length))
Expand All @@ -236,25 +234,25 @@ private func _decode(reader: inout CBORReader) throws -> CBOR {

case 4: // array
let count = try readUIntValue(additional: additional, reader: &reader)
guard count <= UInt64(Int.max) else {
throw CBORError.lengthTooLarge(count)
guard count <= reader.maximumElementCount else {
throw CBORError.lengthTooLarge(count, maximum: reader.maximumElementCount)
}

var items: [CBOR] = []
for _ in 0..<Int(count) {
for _ in 0..<count {
items.append(try _decode(reader: &reader))
}

return .array(items)

case 5: // map
let count = try readUIntValue(additional: additional, reader: &reader)
guard count <= UInt64(Int.max) else {
throw CBORError.lengthTooLarge(count)
guard count <= reader.maximumElementCount else {
throw CBORError.lengthTooLarge(count, maximum: reader.maximumElementCount)
}

var pairs: [CBORMapPair] = []
for _ in 0..<Int(count) {
for _ in 0..<count {
let key = try _decode(reader: &reader)
let value = try _decode(reader: &reader)
pairs.append(CBORMapPair(key: key, value: value))
Expand All @@ -277,8 +275,7 @@ private func _decode(reader: inout CBORReader) throws -> CBOR {
let simple = try reader.readByte()
return .simple(simple)
case 25: // IEEE 754 Half-Precision Float (16 bits)
let bytes = try reader.readBytes(2)
let bits = UInt16(bytes[0]) << 8 | UInt16(bytes[1])
let bits = try reader.readBigEndianInteger(UInt16.self)
// Convert half-precision to double
let sign = (bits & 0x8000) != 0
let exponent = Int((bits & 0x7C00) >> 10)
Expand All @@ -296,15 +293,12 @@ private func _decode(reader: inout CBORReader) throws -> CBOR {
return .float(sign ? -value : value)

case 26: // IEEE 754 Single-Precision Float (32 bits)
let bytes = try reader.readBytes(4)
let bits = UInt32(bytes[0]) << 24 | UInt32(bytes[1]) << 16 | UInt32(bytes[2]) << 8 | UInt32(bytes[3])
let bits = try reader.readBigEndianInteger(UInt32.self)
let float = Float(bitPattern: bits)
return .float(Double(float))

case 27: // IEEE 754 Double-Precision Float (64 bits)
let bytes = try reader.readBytes(8)
let bits = UInt64(bytes[0]) << 56 | UInt64(bytes[1]) << 48 | UInt64(bytes[2]) << 40 | UInt64(bytes[3]) << 32 |
UInt64(bytes[4]) << 24 | UInt64(bytes[5]) << 16 | UInt64(bytes[6]) << 8 | UInt64(bytes[7])
let bits = try reader.readBigEndianInteger(UInt64.self)
let double = Double(bitPattern: bits)
return .float(double)

Expand All @@ -321,27 +315,21 @@ private func _decode(reader: inout CBORReader) throws -> CBOR {
}

/// Reads an unsigned integer value based on the additional information.
private func readUIntValue(additional: UInt8, reader: inout CBORReader) throws -> UInt64 {
// Check for indefinite length first
if additional == 31 {
throw CBORError.indefiniteLengthNotSupported
}

if additional < 24 {
private func readUIntValue(additional: UInt8, reader: inout CBORReader) throws(CBORError) -> UInt64 {
switch additional {
case 0...23:
return UInt64(additional)
} else if additional == 24 {
case 24:
return UInt64(try reader.readByte())
} else if additional == 25 {
let bytes = try reader.readBytes(2)
return UInt64(bytes[0]) << 8 | UInt64(bytes[1])
} else if additional == 26 {
let bytes = try reader.readBytes(4)
return UInt64(bytes[0]) << 24 | UInt64(bytes[1]) << 16 | UInt64(bytes[2]) << 8 | UInt64(bytes[3])
} else if additional == 27 {
let bytes = try reader.readBytes(8)
return UInt64(bytes[0]) << 56 | UInt64(bytes[1]) << 48 | UInt64(bytes[2]) << 40 | UInt64(bytes[3]) << 32 |
UInt64(bytes[4]) << 24 | UInt64(bytes[5]) << 16 | UInt64(bytes[6]) << 8 | UInt64(bytes[7])
} else {
case 25:
return try UInt64(reader.readBigEndianInteger(UInt16.self))
case 26:
return try UInt64(reader.readBigEndianInteger(UInt32.self))
case 27:
return try reader.readBigEndianInteger(UInt64.self)
case 31:
throw CBORError.indefiniteLengthNotSupported
default:
throw CBORError.invalidInitialByte(additional)
}
}
79 changes: 12 additions & 67 deletions Sources/CBOR/CBORError.swift
Original file line number Diff line number Diff line change
@@ -1,75 +1,24 @@
#if canImport(FoundationEssentials)
import FoundationEssentials
#elseif canImport(Foundation)
import Foundation
#endif

// MARK: - Error Types

/// Errors that can occur during CBOR encoding and decoding.
///
/// These errors provide detailed information about what went wrong during
/// CBOR processing operations, helping developers diagnose and fix issues
/// in their CBOR data or usage of the CBOR API.
public enum CBORError: Error {
public enum CBORError: Error, Equatable, Sendable {
/// The input data is not valid CBOR.
///
/// This error occurs when the decoder encounters data that doesn't conform to
/// the CBOR specification (RFC 8949). This could be due to corrupted data,
/// incomplete data, or data encoded with a different format entirely.
case invalidCBOR

/// Expected a specific type but found another.
///
/// This error occurs when trying to decode a CBOR value as a specific type,
/// but the actual type of the value doesn't match the expected type.
/// - Parameters:
/// - expected: The type that was expected (e.g., "String", "Int", "Array")
/// - actual: The actual type that was found in the CBOR data
case typeMismatch(expected: String, actual: String)

/// Array index out of bounds.
///
/// This error occurs when attempting to access an element in a CBOR array
/// using an index that is outside the valid range for the array.
/// - Parameters:
/// - index: The requested index that was attempted to be accessed
/// - count: The actual number of elements in the array (valid indices are 0..<count)
case outOfBounds(index: Int, count: Int)

/// Required key missing from map.
///
/// This error occurs when trying to decode a CBOR map into a Swift struct or class,
/// but a required key is not present in the map.
/// - Parameter key: The name of the missing key
case missingKey(String)

/// Value conversion failed.
///
/// This error occurs when a CBOR value cannot be converted to the requested Swift type,
/// even though the CBOR type is compatible with the requested type.
/// - Parameter message: A description of what went wrong during the conversion
case valueConversionFailed(String)

/// Invalid UTF-8 string data.
///
/// This error occurs when decoding a CBOR text string that contains invalid UTF-8 sequences.
/// All CBOR text strings must contain valid UTF-8 data according to the specification.
case invalidUTF8

/// Integer overflow during encoding/decoding.
///
/// This error occurs when a CBOR integer value is too large to fit into the
/// corresponding Swift integer type (e.g., trying to decode a UInt64.max into an Int).
case integerOverflow

/// Tag value is not supported.
///
/// This error occurs when the decoder encounters a CBOR tag that is not supported
/// by the current implementation.
/// - Parameter tag: The unsupported tag number
case unsupportedTag(UInt64)

/// Reached end of data while decoding.
///
/// This error occurs when the decoder unexpectedly reaches the end of the input data
Expand All @@ -89,7 +38,7 @@ public enum CBORError: Error {
/// This error occurs when a CBOR string, array, or map has a length that is too large
/// to be processed by the current implementation, typically due to memory constraints.
/// - Parameter length: The length value that exceeded the implementation's limits
case lengthTooLarge(UInt64)
case lengthTooLarge(UInt64, maximum: UInt64)

/// Indefinite length encoding is not supported for this type.
///
Expand All @@ -105,32 +54,21 @@ public enum CBORError: Error {
case extraDataFound
}

@_unavailableInEmbedded
extension CBORError: CustomStringConvertible {
/// A human-readable description of the error.
public var description: String {
switch self {
case .invalidCBOR:
return "Invalid CBOR data: The input does not conform to the CBOR specification (RFC 8949)"
case .typeMismatch(let expected, let actual):
return "Type mismatch: expected \(expected), found \(actual)"
case .outOfBounds(let index, let count):
return "Array index out of bounds: attempted to access index \(index), but array only contains \(count) elements (valid indices are 0..<\(count))"
case .missingKey(let key):
return "Missing key: required key '\(key)' was not found in the CBOR map"
case .valueConversionFailed(let message):
return "Value conversion failed: \(message)"
case .invalidUTF8:
return "Invalid UTF-8 data: the CBOR text string contains invalid UTF-8 sequences"
case .integerOverflow:
return "Integer overflow: the CBOR integer value is too large for the target Swift integer type"
case .unsupportedTag(let tag):
return "Unsupported tag: tag \(tag) is not supported by this implementation"
case .prematureEnd:
return "Unexpected end of data: reached the end of input before completing the CBOR value"
case .invalidInitialByte(let byte):
return "Invalid initial byte: 0x\(String(byte, radix: 16, uppercase: true)) is not a valid CBOR initial byte"
case .lengthTooLarge(let length):
return "Length too large: the specified length \(length) exceeds the implementation's limits"
case .lengthTooLarge(let length, let maximum):
return "Length too large: the specified length \(length) exceeds the implementation's limits of \(maximum)"
case .indefiniteLengthNotSupported:
return "Indefinite length encoding not supported: this implementation does not support indefinite length encoding for this type"
case .extraDataFound:
Expand All @@ -139,6 +77,13 @@ extension CBORError: CustomStringConvertible {
}
}

#if canImport(FoundationEssentials)
import FoundationEssentials
#elseif canImport(Foundation)
import Foundation
#endif

@_unavailableInEmbedded
extension CBORError: LocalizedError {
public var errorDescription: String? {
return description
Expand Down
Loading
Loading