cborg 4.2.18 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [4.3.0](https://github.com/rvagg/cborg/compare/v4.2.18...v4.3.0) (2025-11-05)
2
+
3
+ ### Features
4
+
5
+ * support RFC8949 deterministic encoding ([#150](https://github.com/rvagg/cborg/issues/150)) ([6d44cc0](https://github.com/rvagg/cborg/commit/6d44cc0778b81bdfac6d654ab1893ac25b08eba3))
6
+
1
7
  ## [4.2.18](https://github.com/rvagg/cborg/compare/v4.2.17...v4.2.18) (2025-10-16)
2
8
 
3
9
  ### Trivial Changes
package/README.md CHANGED
@@ -33,6 +33,7 @@
33
33
  * [Tag decoders](#tag-decoders)
34
34
  * [Decoding with a custom tokeniser](#decoding-with-a-custom-tokeniser)
35
35
  * [Deterministic encoding recommendations](#deterministic-encoding-recommendations)
36
+ * [RFC 8949 deterministic mode](#rfc-8949-deterministic-mode)
36
37
  * [Round-trip consistency](#round-trip-consistency)
37
38
  * [JSON mode](#json-mode)
38
39
  * [Example](#example-1)
@@ -209,7 +210,7 @@ Encode a JavaScript object and return a `Uint8Array` with the CBOR byte represen
209
210
  * Integers larger than `Number.MAX_SAFE_INTEGER` or less than `Number.MIN_SAFE_INTEGER` will be encoded as floats. There is no way to safely determine whether a number has a fractional part outside of this range.
210
211
  * `BigInt`s are supported by default within the 64-bit unsigned range but will be also be encoded to their smallest possible representation (so will not round-trip as a `BigInt` if they are smaller than `Number.MAX_SAFE_INTEGER`). Larger `BigInt`s require a tag (officially tags 2 and 3).
211
212
  * Floats will be encoded in their smallest possible representations: 16-bit, 32-bit or 64-bit. Unless the `float64` option is supplied.
212
- * Object properties are sorted according to the original [RFC 7049](https://tools.ietf.org/html/rfc7049) canonical representation recommended method: length-first and then bytewise. Note that this recommendation has changed in [RFC 8949](https://tools.ietf.org/html/rfc8949) to be plain bytewise (this is not currently supported but pull requests are welcome to add it as an option).
213
+ * Object properties are sorted according to the original [RFC 7049](https://tools.ietf.org/html/rfc7049) canonical representation recommended method: length-first and then bytewise. Note that this recommendation has changed in [RFC 8949](https://tools.ietf.org/html/rfc8949) to be plain bytewise, use the `rfc8949EncodeOptions` to apply this rule.
213
214
  * The only CBOR major 7 "simple values" supported are `true`, `false`, `undefined` and `null`. "Simple values" outside of this range are intentionally not supported (pull requests welcome to enable them with an option).
214
215
  * Objects, arrays, strings and bytes are encoded as fixed-length, encoding as indefinite length is intentionally not supported.
215
216
 
@@ -439,7 +440,7 @@ cborg is designed with deterministic encoding forms as a primary feature. It is
439
440
  By default, cborg will always **encode** objects to the same bytes by applying some strictness rules:
440
441
 
441
442
  * Using smallest-possible representations for ints, negative ints, floats and lengthed object lengths.
442
- * Always sorting maps using the _original_ recommended [RFC 7049](https://tools.ietf.org/html/rfc7049) map key ordering rules.
443
+ * Always sorting maps using the _original_ recommended [RFC 7049](https://tools.ietf.org/html/rfc7049) map key ordering rules. (Note that this has changed in [RFC 8949](https://tools.ietf.org/html/rfc8949) to be plain bytewise sorting, use the `rfc8949EncodeOptions` to apply this rule).
443
444
  * Omitting support for tags (therefore omitting support for exotic object types).
444
445
  * Applying deterministic rules to `number` differentiation - if a fractional part is missing and it's within the safe integer boundary, it's encoded as an integer, otherwise it's encoded as a float.
445
446
 
@@ -451,6 +452,24 @@ By default, cborg allows for some flexibility on **decode** of objects, which wi
451
452
  * Not providing any tag decoders, or ensuring that tag decoders are strict about their forms (e.g. a bigint decoder could reject bigints that could have fit into a standard major 0 64-bit integer).
452
453
  * Overriding type decoders where they may introduce undesired flexibility.
453
454
 
455
+ ### RFC 8949 deterministic mode
456
+
457
+ RFC 8949 updates the canonical map ordering recommendation to plain bytewise comparisons. The `rfc8949EncodeOptions` export configures cborg to follow this rule and can be passed directly to `encode`:
458
+
459
+ ```js
460
+ import { encode, rfc8949EncodeOptions } from 'cborg'
461
+
462
+ const bytes = encode(obj, rfc8949EncodeOptions)
463
+ ```
464
+
465
+ You can also merge these defaults with your own preferences:
466
+
467
+ ```js
468
+ import { encode, rfc8949EncodeOptions } from 'cborg'
469
+
470
+ const bytes = encode(obj, { ...rfc8949EncodeOptions, typeEncoders: YOUR_TYPE_ENCODERS })
471
+ ```
472
+
454
473
  Currently, there are two areas that cborg cannot impose strictness requirements (pull requests welcome!):
455
474
 
456
475
  * Smallest-possible floats, or always-float64 cannot be enforced on decode.
package/cborg.js CHANGED
@@ -1,4 +1,4 @@
1
- import { encode } from './lib/encode.js'
1
+ import { encode, rfc8949EncodeOptions } from './lib/encode.js'
2
2
  import { decode, decodeFirst, Tokeniser, tokensToObject } from './lib/decode.js'
3
3
  import { Token, Type } from './lib/token.js'
4
4
 
@@ -17,6 +17,7 @@ export {
17
17
  Tokeniser as Tokenizer,
18
18
  tokensToObject,
19
19
  encode,
20
+ rfc8949EncodeOptions,
20
21
  Token,
21
22
  Type
22
23
  }
package/lib/encode.js CHANGED
@@ -3,7 +3,7 @@ import { Token, Type } from './token.js'
3
3
  import { Bl } from './bl.js'
4
4
  import { encodeErrPrefix } from './common.js'
5
5
  import { quickEncodeToken } from './jump.js'
6
- import { asU8A } from './byte-utils.js'
6
+ import { asU8A, compare } from './byte-utils.js'
7
7
 
8
8
  import { encodeUint } from './0uint.js'
9
9
  import { encodeNegint } from './1negint.js'
@@ -30,6 +30,13 @@ const defaultEncodeOptions = {
30
30
  quickEncodeToken
31
31
  }
32
32
 
33
+ /** @type {EncodeOptions} */
34
+ export const rfc8949EncodeOptions = Object.freeze({
35
+ float64: true,
36
+ mapSorter: rfc8949MapSorter,
37
+ quickEncodeToken
38
+ })
39
+
33
40
  /** @returns {TokenTypeEncoder[]} */
34
41
  export function makeCborEncoders () {
35
42
  const encoders = []
@@ -352,19 +359,6 @@ Recommendation: stick to single key types or you'll get into trouble, and prefer
352
359
  string keys because it's much simpler that way.
353
360
  */
354
361
 
355
- /*
356
- (UPDATE, Dec 2020)
357
- https://tools.ietf.org/html/rfc8949 is the updated CBOR spec and clarifies some
358
- of the questions above with a new recommendation for sorting order being much
359
- closer to what would be expected in other environments (i.e. no length-first
360
- weirdness).
361
- This new sorting order is not yet implemented here but could be added as an
362
- option. "Determinism" (canonicity) is system dependent and it's difficult to
363
- change existing systems that are built with existing expectations. So if a new
364
- ordering is introduced here, the old needs to be kept as well with the user
365
- having the option.
366
- */
367
-
368
362
  /**
369
363
  * @param {TokenOrNestedTokens[]} entries
370
364
  * @param {EncodeOptions} options
@@ -404,6 +398,40 @@ function mapSorter (e1, e2) {
404
398
  return tcmp
405
399
  }
406
400
 
401
+ /**
402
+ * @typedef {Token & { _keyBytes?: Uint8Array }} TokenEx
403
+ *
404
+ * @param {(Token|Token[])[]} e1
405
+ * @param {(Token|Token[])[]} e2
406
+ * @returns {number}
407
+ */
408
+ function rfc8949MapSorter (e1, e2) {
409
+ if (e1[0] instanceof Token && e2[0] instanceof Token) {
410
+ const t1 = /** @type {TokenEx} */ (e1[0])
411
+ const t2 = /** @type {TokenEx} */ (e2[0])
412
+
413
+ if (!t1._keyBytes) {
414
+ t1._keyBytes = encodeRfc8949(t1.value)
415
+ }
416
+
417
+ if (!t2._keyBytes) {
418
+ t2._keyBytes = encodeRfc8949(t2.value)
419
+ }
420
+
421
+ return compare(t1._keyBytes, t2._keyBytes)
422
+ }
423
+
424
+ throw new Error('rfc8949MapSorter: complex key types are not supported yet')
425
+ }
426
+
427
+ /**
428
+ * @param {any} data
429
+ * @returns {Uint8Array}
430
+ */
431
+ function encodeRfc8949 (data) {
432
+ return encodeCustom(data, cborEncoders, rfc8949EncodeOptions)
433
+ }
434
+
407
435
  /**
408
436
  * @param {Bl} buf
409
437
  * @param {TokenOrNestedTokens} tokens
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cborg",
3
- "version": "4.2.18",
3
+ "version": "4.3.0",
4
4
  "description": "Fast CBOR with a focus on strictness",
5
5
  "main": "cborg.js",
6
6
  "type": "module",
@@ -0,0 +1,46 @@
1
+ /* eslint-env mocha */
2
+
3
+ import * as chai from 'chai'
4
+ import { encode, rfc8949EncodeOptions } from '../cborg.js'
5
+ import { toHex } from '../lib/byte-utils.js'
6
+
7
+ const { assert } = chai
8
+
9
+ describe('RFC8949 deterministic encoding', () => {
10
+ it('should encode', () => {
11
+ const value = new Map()
12
+ // https://datatracker.ietf.org/doc/html/rfc8949#section-4.2.1
13
+ value.set(false, false)
14
+ // value.set([-1], [-1])
15
+ // value.set([100], [100])
16
+ value.set('aa', 'aa')
17
+ value.set('z', 'z')
18
+ value.set(-1, -1)
19
+ value.set(10, 10)
20
+ value.set(100, 100)
21
+
22
+ const data = encode(value, rfc8949EncodeOptions)
23
+ assert.deepEqual(
24
+ toHex(data),
25
+ 'a60a0a186418642020617a617a626161626161f4f4'
26
+ )
27
+ // {10: 10, 100: 100, -1: -1, "z": "z", "aa": "aa", false: false}
28
+ })
29
+
30
+ it('will throw on complex key types', () => {
31
+ const value = new Map()
32
+ // https://datatracker.ietf.org/doc/html/rfc8949#section-4.2.1
33
+ value.set(false, false)
34
+ value.set([-1], [-1])
35
+ value.set([100], [100])
36
+ value.set('aa', 'aa')
37
+ value.set('z', 'z')
38
+ value.set(-1, -1)
39
+ value.set(10, 10)
40
+ value.set(100, 100)
41
+
42
+ assert.throws(() => {
43
+ encode(value, rfc8949EncodeOptions)
44
+ })
45
+ })
46
+ })
package/types/cborg.d.ts CHANGED
@@ -19,7 +19,8 @@ import { decodeFirst } from './lib/decode.js';
19
19
  import { Tokeniser } from './lib/decode.js';
20
20
  import { tokensToObject } from './lib/decode.js';
21
21
  import { encode } from './lib/encode.js';
22
+ import { rfc8949EncodeOptions } from './lib/encode.js';
22
23
  import { Token } from './lib/token.js';
23
24
  import { Type } from './lib/token.js';
24
- export { decode, decodeFirst, Tokeniser as Tokenizer, tokensToObject, encode, Token, Type };
25
+ export { decode, decodeFirst, Tokeniser as Tokenizer, tokensToObject, encode, rfc8949EncodeOptions, Token, Type };
25
26
  //# sourceMappingURL=cborg.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cborg.d.ts","sourceRoot":"","sources":["../cborg.js"],"names":[],"mappings":";;;yBAMa,OAAO,aAAa,EAAE,UAAU;;;;0BAEhC,OAAO,aAAa,EAAE,mBAAmB;;;;4BACzC,OAAO,aAAa,EAAE,aAAa;;;;4BACnC,OAAO,aAAa,EAAE,aAAa;uBATe,iBAAiB;4BAAjB,iBAAiB;0BAAjB,iBAAiB;+BAAjB,iBAAiB;uBADzD,iBAAiB;sBAEZ,gBAAgB;qBAAhB,gBAAgB"}
1
+ {"version":3,"file":"cborg.d.ts","sourceRoot":"","sources":["../cborg.js"],"names":[],"mappings":";;;yBAMa,OAAO,aAAa,EAAE,UAAU;;;;0BAEhC,OAAO,aAAa,EAAE,mBAAmB;;;;4BACzC,OAAO,aAAa,EAAE,aAAa;;;;4BACnC,OAAO,aAAa,EAAE,aAAa;uBATe,iBAAiB;4BAAjB,iBAAiB;0BAAjB,iBAAiB;+BAAjB,iBAAiB;uBADnC,iBAAiB;qCAAjB,iBAAiB;sBAElC,gBAAgB;qBAAhB,gBAAgB"}
@@ -1,5 +1,10 @@
1
1
  /** @returns {TokenTypeEncoder[]} */
2
2
  export function makeCborEncoders(): TokenTypeEncoder[];
3
+ /** @type {EncodeOptions} */
4
+ export const rfc8949EncodeOptions: EncodeOptions;
5
+ export type TokenEx = Token & {
6
+ _keyBytes?: Uint8Array;
7
+ };
3
8
  export type EncodeOptions = import("../interface").EncodeOptions;
4
9
  export type OptionalTypeEncoder = import("../interface").OptionalTypeEncoder;
5
10
  export type Reference = import("../interface").Reference;
@@ -47,4 +52,5 @@ export class Ref implements Reference {
47
52
  */
48
53
  includes(obj: object | any[]): boolean;
49
54
  }
55
+ import { Token } from './token.js';
50
56
  //# sourceMappingURL=encode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../lib/encode.js"],"names":[],"mappings":"AAgCA,oCAAoC;AACpC,oCADc,gBAAgB,EAAE,CAY/B;4BA3BY,OAAO,cAAc,EAAE,aAAa;kCACpC,OAAO,cAAc,EAAE,mBAAmB;wBAC1C,OAAO,cAAc,EAAE,SAAS;gCAChC,OAAO,cAAc,EAAE,iBAAiB;+BACxC,OAAO,cAAc,EAAE,gBAAgB;kCACvC,OAAO,cAAc,EAAE,mBAAmB;AAgQvD;;;;;GAKG;AACH,oCALW,GAAG,YACH,aAAa,aACb,SAAS,GACP,mBAAmB,CAgB/B;AA2JD;;;;GAIG;AACH,6BAJW,GAAG,YACH,aAAa,GACX,UAAU,CAKtB;AAvCD;;;;;GAKG;AACH,mCALW,GAAG,YACH,gBAAgB,EAAE,WAClB,aAAa,GACX,UAAU,CAyBtB;AAjZD,8BAA8B;AAC9B,4BADiB,SAAS;IA0BxB;;;;OAIG;IACH,0BAJW,SAAS,GAAC,SAAS,OACnB,MAAM,GAAC,GAAG,EAAE,GACV,SAAS,CAOrB;IAlCD;;;OAGG;IACH,iBAHW,MAAM,GAAC,GAAG,EAAE,UACZ,SAAS,GAAC,SAAS,EAK7B;IAFC,oBAAc;IACd,qDAAoB;IAGtB;;;OAGG;IACH,cAHW,MAAM,GAAC,GAAG,EAAE,GACV,OAAO,CAWnB;CAaF"}
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../lib/encode.js"],"names":[],"mappings":"AAuCA,oCAAoC;AACpC,oCADc,gBAAgB,EAAE,CAY/B;AAnBD,4BAA4B;AAC5B,mCADW,aAAa,CAKtB;sBA4WW,KAAK,GAAG;IAAE,SAAS,CAAC,EAAE,UAAU,CAAA;CAAE;4BAhYlC,OAAO,cAAc,EAAE,aAAa;kCACpC,OAAO,cAAc,EAAE,mBAAmB;wBAC1C,OAAO,cAAc,EAAE,SAAS;gCAChC,OAAO,cAAc,EAAE,iBAAiB;+BACxC,OAAO,cAAc,EAAE,gBAAgB;kCACvC,OAAO,cAAc,EAAE,mBAAmB;AAuQvD;;;;;GAKG;AACH,oCALW,GAAG,YACH,aAAa,aACb,SAAS,GACP,mBAAmB,CAgB/B;AAgLD;;;;GAIG;AACH,6BAJW,GAAG,YACH,aAAa,GACX,UAAU,CAKtB;AAvCD;;;;;GAKG;AACH,mCALW,GAAG,YACH,gBAAgB,EAAE,WAClB,aAAa,GACX,UAAU,CAyBtB;AAtaD,8BAA8B;AAC9B,4BADiB,SAAS;IA0BxB;;;;OAIG;IACH,0BAJW,SAAS,GAAC,SAAS,OACnB,MAAM,GAAC,GAAG,EAAE,GACV,SAAS,CAOrB;IAlCD;;;OAGG;IACH,iBAHW,MAAM,GAAC,GAAG,EAAE,UACZ,SAAS,GAAC,SAAS,EAK7B;IAFC,oBAAc;IACd,qDAAoB;IAGtB;;;OAGG;IACH,cAHW,MAAM,GAAC,GAAG,EAAE,GACV,OAAO,CAWnB;CAaF;sBA7F2B,YAAY"}