Skip to content

Commit d37225d

Browse files
authored
Merge pull request #54 from outfoxx/feature/cbor-canonical
CBOR: Support deterministic mode
2 parents 9c5adf2 + 0dacd3e commit d37225d

File tree

6 files changed

+195
-9
lines changed

6 files changed

+195
-9
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ format:
4444
swiftformat --config .swiftformat Sources/ Tests/
4545

4646
lint: make-test-results-dir
47-
swiftlint lint --reporter html > TestResults/lint.html
47+
- swiftlint lint --reporter html > TestResults/lint.html
4848

4949
view_lint: lint
5050
open TestResults/lint.html

Sources/PotentCBOR/CBOREncoder.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@ import PotentCodables
1717
///
1818
public class CBOREncoder: ValueEncoder<CBOR, CBOREncoderTransform>, EncodesToData {
1919

20+
/// Encoder with the default options
2021
public static let `default` = CBOREncoder()
2122

23+
/// Encoder with deterministic encoding enabled
24+
public static let deterministic = {
25+
let encoder = CBOREncoder()
26+
encoder.deterministic = true
27+
return encoder
28+
}()
29+
2230
// MARK: Options
2331

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

50+
/// Enables or disables CBOR deterministic encoding.
51+
open var deterministic: Bool = false
52+
4253
/// The options set on the top-level encoder.
4354
override public var options: CBOREncoderTransform.Options {
4455
return CBOREncoderTransform.Options(
4556
dateEncodingStrategy: dateEncodingStrategy,
4657
keyEncodingStrategy: keyEncodingStrategy,
58+
deterministic: deterministic,
4759
userInfo: userInfo
4860
)
4961
}
@@ -67,6 +79,7 @@ public struct CBOREncoderTransform: InternalEncoderTransform, InternalValueSeria
6779
public struct Options: InternalEncoderOptions {
6880
public let dateEncodingStrategy: CBOREncoder.DateEncodingStrategy
6981
public let keyEncodingStrategy: KeyEncodingStrategy
82+
public let deterministic: Bool
7083
public let userInfo: [CodingUserInfoKey: Any]
7184
}
7285

@@ -254,7 +267,12 @@ public struct CBOREncoderTransform: InternalEncoderTransform, InternalValueSeria
254267
}
255268

256269
public static func data(from value: CBOR, options: Options) throws -> Data {
257-
return try CBORSerialization.data(from: value)
270+
var encodingOptions = CBORSerialization.EncodingOptions()
271+
if options.deterministic {
272+
encodingOptions.insert(.deterministic)
273+
}
274+
275+
return try CBORSerialization.data(from: value, options: encodingOptions)
258276
}
259277

260278
}

Sources/PotentCBOR/CBORSerialization.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,24 @@ public enum CBORSerialization {
4141
case invalidIntegerSize
4242
}
4343

44+
/// Options for encoding CBOR
45+
public enum EncodingOption {
46+
/// Enable deterministic encoding
47+
case deterministic
48+
}
49+
50+
/// Set of CBOR encoding options
51+
public typealias EncodingOptions = Set<EncodingOption>
52+
4453
/// Serialize `CBOR` value into a byte data.
4554
///
4655
/// - Parameters:
4756
/// - with: The ``CBOR`` item to serialize
4857
/// - Throws:
4958
/// - `Swift.Error`: If any stream I/O error is encountered
50-
public static func data(from value: CBOR) throws -> Data {
59+
public static func data(from value: CBOR, options: EncodingOptions = []) throws -> Data {
5160
let stream = CBORDataStream()
52-
let encoder = CBORWriter(stream: stream)
61+
let encoder = CBORWriter(stream: stream, deterministic: options.contains(.deterministic))
5362
try encoder.encode(value)
5463
return stream.data
5564
}

Sources/PotentCBOR/CBORWriter.swift

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ internal struct CBORWriter {
2323
}
2424

2525
private(set) var stream: CBOROutputStream
26+
private let deterministic: Bool
2627

27-
init(stream: CBOROutputStream) {
28+
init(stream: CBOROutputStream, deterministic: Bool) {
2829
self.stream = stream
30+
self.deterministic = deterministic
2931
}
3032

3133
/// Encodes a single CBOR item.
@@ -45,8 +47,8 @@ internal struct CBORWriter {
4547
case .tagged(let tag, let value): try encodeTagged(tag: tag, value: value)
4648
case .simple(let value): try encodeSimpleValue(value)
4749
case .boolean(let bool): try encodeBool(bool)
48-
case .half(let half): try encodeHalf(half)
49-
case .float(let float): try encodeFloat(float)
50+
case .half(let half): deterministic ? try encodeDouble(CBOR.Double(half)) : try encodeHalf(half)
51+
case .float(let float): deterministic ? try encodeDouble(CBOR.Double(float)) : try encodeFloat(float)
5052
case .double(let double): try encodeDouble(double)
5153
}
5254
}
@@ -172,7 +174,18 @@ internal struct CBORWriter {
172174
/// - `Swift.Error`: If any I/O error occurs
173175
private func encodeMap(_ map: CBOR.Map) throws {
174176
try encodeLength(map.count, majorType: 0b101)
175-
try encodeMapChunk(map)
177+
if deterministic {
178+
try map.map { (try deterministicBytes(of: $0), ($0, $1)) }
179+
.sorted { (itemA, itemB) in itemA.0.lexicographicallyPrecedes(itemB.0) }
180+
.map { $1 }
181+
.forEach { key, value in
182+
try encode(key)
183+
try encode(value)
184+
}
185+
}
186+
else {
187+
try encodeMapChunk(map)
188+
}
176189
}
177190

178191
/// Encodes a map chunk of CBOR item pairs.
@@ -243,6 +256,15 @@ internal struct CBORWriter {
243256
/// - Throws:
244257
/// - `Swift.Error`: If any I/O error occurs
245258
private func encodeFloat(_ val: CBOR.Float) throws {
259+
if deterministic {
260+
if val.isNaN {
261+
return try encodeHalf(.nan)
262+
}
263+
let half = CBOR.Half(val)
264+
if CBOR.Float(half) == val {
265+
return try encodeHalf(half)
266+
}
267+
}
246268
try stream.writeByte(0xFA)
247269
try stream.writeInt(val.bitPattern)
248270
}
@@ -252,6 +274,15 @@ internal struct CBORWriter {
252274
/// - Throws:
253275
/// - `Swift.Error`: If any I/O error occurs
254276
private func encodeDouble(_ val: CBOR.Double) throws {
277+
if deterministic {
278+
if val.isNaN {
279+
return try encodeFloat(.nan)
280+
}
281+
let float = CBOR.Float(val)
282+
if CBOR.Double(float) == val {
283+
return try encodeFloat(float)
284+
}
285+
}
255286
try stream.writeByte(0xFB)
256287
try stream.writeInt(val.bitPattern)
257288
}
@@ -298,4 +329,9 @@ internal struct CBORWriter {
298329
try block(self)
299330
}
300331

332+
func deterministicBytes(of value: CBOR) throws -> Data {
333+
let out = CBORDataStream()
334+
try CBORWriter(stream: out, deterministic: true).encode(value)
335+
return out.data
336+
}
301337
}

Tests/CBOREncoderTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,26 @@ class CBOREncoderTests: XCTestCase {
173173
)
174174
}
175175

176+
func testEncodeDeterministicMaps() throws {
177+
178+
struct Test: Codable {
179+
struct Sub: Codable {
180+
var value: Int
181+
}
182+
183+
var test: String
184+
var sub: Sub
185+
}
186+
187+
print(try CBOR.Encoder.deterministic.encode(Test(test: "a", sub: .init(value: 5))).hexEncodedString())
188+
189+
XCTAssertEqual(
190+
try CBOR.Encoder.deterministic.encode(Test(test: "a", sub: .init(value: 5))),
191+
Data([0xA2, 0x63, 0x73, 0x75, 0x62, 0xA1, 0x65, 0x76, 0x61, 0x6C,
192+
0x75, 0x65, 0x05, 0x64, 0x74, 0x65, 0x73, 0x74, 0x61, 0x61])
193+
)
194+
}
195+
176196
func testEncodingDoesntTranslateMapKeys() throws {
177197
let encoder = CBOREncoder()
178198
encoder.keyEncodingStrategy = .convertToSnakeCase

Tests/CBORWriterTests.swift

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ class CBORWriterTests: XCTestCase {
3333

3434
func encode(block: (CBORWriter) throws -> Void) rethrows -> [UInt8] {
3535
let stream = CBORDataStream()
36-
let encoder = CBORWriter(stream: stream)
36+
let encoder = CBORWriter(stream: stream, deterministic: false)
37+
try block(encoder)
38+
return Array(stream.data)
39+
}
40+
41+
func encodeDeterministic(block: (CBORWriter) throws -> Void) rethrows -> [UInt8] {
42+
let stream = CBORDataStream()
43+
let encoder = CBORWriter(stream: stream, deterministic: true)
3744
try block(encoder)
3845
return Array(stream.data)
3946
}
@@ -281,6 +288,102 @@ class CBORWriterTests: XCTestCase {
281288
])
282289
}
283290

291+
func testEncodeDeterministicMaps() throws {
292+
XCTAssertEqual(try encodeDeterministic { try $0.encode([:] as CBOR) }, [0xA0])
293+
294+
let map: CBOR = [100: 2, 10: 1, "z": 4, [100]: 6, -1: 3, "aa": 5, false: 8, [-1]: 7]
295+
296+
XCTAssertEqual(
297+
try encodeDeterministic { try $0.encode(map) },
298+
[0xA8, 0x0A, 0x01, 0x18, 0x64, 0x02, 0x20, 0x03, 0x61, 0x7A, 0x04, 0x62,
299+
0x61, 0x61, 0x05, 0x81, 0x18, 0x64, 0x06, 0x81, 0x20, 0x07, 0xF4, 0x08]
300+
)
301+
}
302+
303+
func testEncodeDeterministicDoubles() throws {
304+
// Can only be represented as Double
305+
XCTAssertEqual(
306+
try encodeDeterministic { try $0.encode(.double(1.23)) },
307+
[0xFB, 0x3F, 0xF3, 0xAE, 0x14, 0x7A, 0xE1, 0x47, 0xAE]
308+
)
309+
// Can be exactly represented by Double and Float
310+
XCTAssertEqual(
311+
try encodeDeterministic { try $0.encode(.double(131008.0)) },
312+
[0xFA, 0x47, 0xFF, 0xE0, 0x00]
313+
)
314+
// Can be exactly represented by Double, Float and Half
315+
XCTAssertEqual(
316+
try encodeDeterministic { try $0.encode(.double(0.5)) },
317+
[0xF9, 0x38, 0x00]
318+
)
319+
// Encode infinity
320+
XCTAssertEqual(
321+
try encodeDeterministic { try $0.encode(.double(.infinity)) },
322+
[0xF9, 0x7C, 0x00]
323+
)
324+
XCTAssertEqual(
325+
try encodeDeterministic { try $0.encode(.double(-.infinity)) },
326+
[0xF9, 0xFC, 0x00]
327+
)
328+
// Encode NaN
329+
XCTAssertEqual(
330+
try encodeDeterministic { try $0.encode(.double(.nan)) },
331+
[0xF9, 0x7E, 0x00]
332+
)
333+
}
334+
335+
func testEncodeDeterministicFloat() throws {
336+
XCTAssertEqual(
337+
try encodeDeterministic { try $0.encode(.float(1.23)) },
338+
[0xFA, 0x3F, 0x9D, 0x70, 0xA4]
339+
)
340+
// Can be represented as Float
341+
XCTAssertEqual(
342+
try encodeDeterministic { try $0.encode(.float(131008.0)) },
343+
[0xFa, 0x47, 0xFF, 0xE0, 0x00]
344+
)
345+
// Can be exactly represented by Float and Half
346+
XCTAssertEqual(
347+
try encodeDeterministic { try $0.encode(.float(0.5)) },
348+
[0xF9, 0x38, 0x00]
349+
)
350+
// Encode infinity
351+
XCTAssertEqual(
352+
try encodeDeterministic { try $0.encode(.float(.infinity)) },
353+
[0xF9, 0x7C, 0x00]
354+
)
355+
XCTAssertEqual(
356+
try encodeDeterministic { try $0.encode(.float(-.infinity)) },
357+
[0xF9, 0xFC, 0x00]
358+
)
359+
// Encode NaN
360+
XCTAssertEqual(
361+
try encodeDeterministic { try $0.encode(.float(.nan)) },
362+
[0xF9, 0x7E, 0x00]
363+
)
364+
}
365+
366+
func testEncodeDeterministicHalf() throws {
367+
XCTAssertEqual(
368+
try encodeDeterministic { try $0.encode(.half(1.23)) },
369+
[0xF9, 0x3C, 0xEC]
370+
)
371+
// Encode infinity
372+
XCTAssertEqual(
373+
try encodeDeterministic { try $0.encode(.float(.infinity)) },
374+
[0xF9, 0x7C, 0x00]
375+
)
376+
XCTAssertEqual(
377+
try encodeDeterministic { try $0.encode(.float(-.infinity)) },
378+
[0xF9, 0xFC, 0x00]
379+
)
380+
// Encode NaN
381+
XCTAssertEqual(
382+
try encodeDeterministic { try $0.encode(.float(.nan)) },
383+
[0xF9, 0x7E, 0x00]
384+
)
385+
}
386+
284387
func testEncodeTagged() {
285388
let bignum = Data([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) // 2**64
286389
let bignumCBOR = CBOR.byteString(bignum)

0 commit comments

Comments
 (0)