Skip to content

Commit

Permalink
Merge pull request #54 from outfoxx/feature/cbor-canonical
Browse files Browse the repository at this point in the history
CBOR: Support deterministic mode
  • Loading branch information
kdubb committed Jun 14, 2023
2 parents 9c5adf2 + 0dacd3e commit d37225d
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ format:
swiftformat --config .swiftformat Sources/ Tests/

lint: make-test-results-dir
swiftlint lint --reporter html > TestResults/lint.html
- swiftlint lint --reporter html > TestResults/lint.html

view_lint: lint
open TestResults/lint.html
Expand Down
20 changes: 19 additions & 1 deletion Sources/PotentCBOR/CBOREncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ import PotentCodables
///
public class CBOREncoder: ValueEncoder<CBOR, CBOREncoderTransform>, EncodesToData {

/// Encoder with the default options
public static let `default` = CBOREncoder()

/// Encoder with deterministic encoding enabled
public static let deterministic = {
let encoder = CBOREncoder()
encoder.deterministic = true
return encoder
}()

// MARK: Options

/// The strategy to use for encoding `Date` values.
Expand All @@ -39,11 +47,15 @@ public class CBOREncoder: ValueEncoder<CBOR, CBOREncoderTransform>, EncodesToDat
/// The strategy to use in encoding dates. Defaults to `.iso8601`.
open var dateEncodingStrategy: DateEncodingStrategy = .iso8601

/// Enables or disables CBOR deterministic encoding.
open var deterministic: Bool = false

/// The options set on the top-level encoder.
override public var options: CBOREncoderTransform.Options {
return CBOREncoderTransform.Options(
dateEncodingStrategy: dateEncodingStrategy,
keyEncodingStrategy: keyEncodingStrategy,
deterministic: deterministic,
userInfo: userInfo
)
}
Expand All @@ -67,6 +79,7 @@ public struct CBOREncoderTransform: InternalEncoderTransform, InternalValueSeria
public struct Options: InternalEncoderOptions {
public let dateEncodingStrategy: CBOREncoder.DateEncodingStrategy
public let keyEncodingStrategy: KeyEncodingStrategy
public let deterministic: Bool
public let userInfo: [CodingUserInfoKey: Any]
}

Expand Down Expand Up @@ -254,7 +267,12 @@ public struct CBOREncoderTransform: InternalEncoderTransform, InternalValueSeria
}

public static func data(from value: CBOR, options: Options) throws -> Data {
return try CBORSerialization.data(from: value)
var encodingOptions = CBORSerialization.EncodingOptions()
if options.deterministic {
encodingOptions.insert(.deterministic)
}

return try CBORSerialization.data(from: value, options: encodingOptions)
}

}
Expand Down
13 changes: 11 additions & 2 deletions Sources/PotentCBOR/CBORSerialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,24 @@ public enum CBORSerialization {
case invalidIntegerSize
}

/// Options for encoding CBOR
public enum EncodingOption {
/// Enable deterministic encoding
case deterministic
}

/// Set of CBOR encoding options
public typealias EncodingOptions = Set<EncodingOption>

/// Serialize `CBOR` value into a byte data.
///
/// - Parameters:
/// - with: The ``CBOR`` item to serialize
/// - Throws:
/// - `Swift.Error`: If any stream I/O error is encountered
public static func data(from value: CBOR) throws -> Data {
public static func data(from value: CBOR, options: EncodingOptions = []) throws -> Data {
let stream = CBORDataStream()
let encoder = CBORWriter(stream: stream)
let encoder = CBORWriter(stream: stream, deterministic: options.contains(.deterministic))
try encoder.encode(value)
return stream.data
}
Expand Down
44 changes: 40 additions & 4 deletions Sources/PotentCBOR/CBORWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ internal struct CBORWriter {
}

private(set) var stream: CBOROutputStream
private let deterministic: Bool

init(stream: CBOROutputStream) {
init(stream: CBOROutputStream, deterministic: Bool) {
self.stream = stream
self.deterministic = deterministic
}

/// Encodes a single CBOR item.
Expand All @@ -45,8 +47,8 @@ internal struct CBORWriter {
case .tagged(let tag, let value): try encodeTagged(tag: tag, value: value)
case .simple(let value): try encodeSimpleValue(value)
case .boolean(let bool): try encodeBool(bool)
case .half(let half): try encodeHalf(half)
case .float(let float): try encodeFloat(float)
case .half(let half): deterministic ? try encodeDouble(CBOR.Double(half)) : try encodeHalf(half)
case .float(let float): deterministic ? try encodeDouble(CBOR.Double(float)) : try encodeFloat(float)
case .double(let double): try encodeDouble(double)
}
}
Expand Down Expand Up @@ -172,7 +174,18 @@ internal struct CBORWriter {
/// - `Swift.Error`: If any I/O error occurs
private func encodeMap(_ map: CBOR.Map) throws {
try encodeLength(map.count, majorType: 0b101)
try encodeMapChunk(map)
if deterministic {
try map.map { (try deterministicBytes(of: $0), ($0, $1)) }
.sorted { (itemA, itemB) in itemA.0.lexicographicallyPrecedes(itemB.0) }
.map { $1 }
.forEach { key, value in
try encode(key)
try encode(value)
}
}
else {
try encodeMapChunk(map)
}
}

/// Encodes a map chunk of CBOR item pairs.
Expand Down Expand Up @@ -243,6 +256,15 @@ internal struct CBORWriter {
/// - Throws:
/// - `Swift.Error`: If any I/O error occurs
private func encodeFloat(_ val: CBOR.Float) throws {
if deterministic {
if val.isNaN {
return try encodeHalf(.nan)
}
let half = CBOR.Half(val)
if CBOR.Float(half) == val {
return try encodeHalf(half)
}
}
try stream.writeByte(0xFA)
try stream.writeInt(val.bitPattern)
}
Expand All @@ -252,6 +274,15 @@ internal struct CBORWriter {
/// - Throws:
/// - `Swift.Error`: If any I/O error occurs
private func encodeDouble(_ val: CBOR.Double) throws {
if deterministic {
if val.isNaN {
return try encodeFloat(.nan)
}
let float = CBOR.Float(val)
if CBOR.Double(float) == val {
return try encodeFloat(float)
}
}
try stream.writeByte(0xFB)
try stream.writeInt(val.bitPattern)
}
Expand Down Expand Up @@ -298,4 +329,9 @@ internal struct CBORWriter {
try block(self)
}

func deterministicBytes(of value: CBOR) throws -> Data {
let out = CBORDataStream()
try CBORWriter(stream: out, deterministic: true).encode(value)
return out.data
}
}
20 changes: 20 additions & 0 deletions Tests/CBOREncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@ class CBOREncoderTests: XCTestCase {
)
}

func testEncodeDeterministicMaps() throws {

struct Test: Codable {
struct Sub: Codable {
var value: Int
}

var test: String
var sub: Sub
}

print(try CBOR.Encoder.deterministic.encode(Test(test: "a", sub: .init(value: 5))).hexEncodedString())

XCTAssertEqual(
try CBOR.Encoder.deterministic.encode(Test(test: "a", sub: .init(value: 5))),
Data([0xA2, 0x63, 0x73, 0x75, 0x62, 0xA1, 0x65, 0x76, 0x61, 0x6C,
0x75, 0x65, 0x05, 0x64, 0x74, 0x65, 0x73, 0x74, 0x61, 0x61])
)
}

func testEncodingDoesntTranslateMapKeys() throws {
let encoder = CBOREncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
Expand Down
105 changes: 104 additions & 1 deletion Tests/CBORWriterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ class CBORWriterTests: XCTestCase {

func encode(block: (CBORWriter) throws -> Void) rethrows -> [UInt8] {
let stream = CBORDataStream()
let encoder = CBORWriter(stream: stream)
let encoder = CBORWriter(stream: stream, deterministic: false)
try block(encoder)
return Array(stream.data)
}

func encodeDeterministic(block: (CBORWriter) throws -> Void) rethrows -> [UInt8] {
let stream = CBORDataStream()
let encoder = CBORWriter(stream: stream, deterministic: true)
try block(encoder)
return Array(stream.data)
}
Expand Down Expand Up @@ -281,6 +288,102 @@ class CBORWriterTests: XCTestCase {
])
}

func testEncodeDeterministicMaps() throws {
XCTAssertEqual(try encodeDeterministic { try $0.encode([:] as CBOR) }, [0xA0])

let map: CBOR = [100: 2, 10: 1, "z": 4, [100]: 6, -1: 3, "aa": 5, false: 8, [-1]: 7]

XCTAssertEqual(
try encodeDeterministic { try $0.encode(map) },
[0xA8, 0x0A, 0x01, 0x18, 0x64, 0x02, 0x20, 0x03, 0x61, 0x7A, 0x04, 0x62,
0x61, 0x61, 0x05, 0x81, 0x18, 0x64, 0x06, 0x81, 0x20, 0x07, 0xF4, 0x08]
)
}

func testEncodeDeterministicDoubles() throws {
// Can only be represented as Double
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.double(1.23)) },
[0xFB, 0x3F, 0xF3, 0xAE, 0x14, 0x7A, 0xE1, 0x47, 0xAE]
)
// Can be exactly represented by Double and Float
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.double(131008.0)) },
[0xFA, 0x47, 0xFF, 0xE0, 0x00]
)
// Can be exactly represented by Double, Float and Half
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.double(0.5)) },
[0xF9, 0x38, 0x00]
)
// Encode infinity
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.double(.infinity)) },
[0xF9, 0x7C, 0x00]
)
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.double(-.infinity)) },
[0xF9, 0xFC, 0x00]
)
// Encode NaN
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.double(.nan)) },
[0xF9, 0x7E, 0x00]
)
}

func testEncodeDeterministicFloat() throws {
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(1.23)) },
[0xFA, 0x3F, 0x9D, 0x70, 0xA4]
)
// Can be represented as Float
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(131008.0)) },
[0xFa, 0x47, 0xFF, 0xE0, 0x00]
)
// Can be exactly represented by Float and Half
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(0.5)) },
[0xF9, 0x38, 0x00]
)
// Encode infinity
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(.infinity)) },
[0xF9, 0x7C, 0x00]
)
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(-.infinity)) },
[0xF9, 0xFC, 0x00]
)
// Encode NaN
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(.nan)) },
[0xF9, 0x7E, 0x00]
)
}

func testEncodeDeterministicHalf() throws {
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.half(1.23)) },
[0xF9, 0x3C, 0xEC]
)
// Encode infinity
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(.infinity)) },
[0xF9, 0x7C, 0x00]
)
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(-.infinity)) },
[0xF9, 0xFC, 0x00]
)
// Encode NaN
XCTAssertEqual(
try encodeDeterministic { try $0.encode(.float(.nan)) },
[0xF9, 0x7E, 0x00]
)
}

func testEncodeTagged() {
let bignum = Data([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) // 2**64
let bignumCBOR = CBOR.byteString(bignum)
Expand Down

0 comments on commit d37225d

Please sign in to comment.