cborg 5.0.0 → 5.1.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 +12 -0
- package/README.md +67 -4
- package/cborg.js +5 -1
- package/lib/encode.js +18 -0
- package/lib/is.js +2 -1
- package/lib/tagged.js +78 -0
- package/package.json +1 -1
- package/test/test-tagged.js +231 -0
- package/types/cborg.d.ts +7 -1
- package/types/cborg.d.ts.map +1 -1
- package/types/lib/encode.d.ts.map +1 -1
- package/types/lib/is.d.ts.map +1 -1
- package/types/lib/tagged.d.ts +61 -0
- package/types/lib/tagged.d.ts.map +1 -0
- package/types/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [5.1.0](https://github.com/rvagg/cborg/compare/v5.0.1...v5.1.0) (2026-04-07)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add Tagged class for one-off tag emission and round-trip preservation ([#174](https://github.com/rvagg/cborg/issues/174)) ([e2e7c20](https://github.com/rvagg/cborg/commit/e2e7c207e79d7131e3e941772f1d70ce1363b94f))
|
|
6
|
+
|
|
7
|
+
## [5.0.1](https://github.com/rvagg/cborg/compare/v5.0.0...v5.0.1) (2026-04-02)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **types:** re-export TagDecodeControl off main export ([0d9f25a](https://github.com/rvagg/cborg/commit/0d9f25aadb1491e69f6c343d12537ac08ec1b41e))
|
|
12
|
+
|
|
1
13
|
## [5.0.0](https://github.com/rvagg/cborg/compare/v4.5.8...v5.0.0) (2026-03-31)
|
|
2
14
|
|
|
3
15
|
### ⚠ BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
**cborg** is also fast, and is suitable for the browser (is `Uint8Array` native) and Node.js.
|
|
8
8
|
|
|
9
|
-
**cborg** supports CBOR tags, but does not ship with them enabled by default. If you want tags, you need to plug them in to the encoder and decoder.
|
|
10
|
-
|
|
11
9
|
* [Example](#example)
|
|
12
10
|
* [CLI](#cli)
|
|
13
11
|
* [`cborg bin2diag [binary input]`](#cborg-bin2diag-binary-input)
|
|
@@ -30,6 +28,7 @@
|
|
|
30
28
|
* [Options](#options-1)
|
|
31
29
|
* [`decodeFirst(data[, options])`](#decodefirstdata-options)
|
|
32
30
|
* [`encodedLength(data[, options])`](#encodedlengthdata-options)
|
|
31
|
+
* [`Tagged`](#tagged)
|
|
33
32
|
* [Type encoders](#type-encoders)
|
|
34
33
|
* [Tag decoders](#tag-decoders)
|
|
35
34
|
* [Decoding with a custom tokeniser](#decoding-with-a-custom-tokeniser)
|
|
@@ -325,17 +324,81 @@ Calculate the byte length of the given data when encoded as CBOR with the option
|
|
|
325
324
|
|
|
326
325
|
A `tokensToLength()` function is available which deals directly with a tokenized form of the object, but this only recommended for advanced users.
|
|
327
326
|
|
|
327
|
+
### `Tagged`
|
|
328
|
+
|
|
329
|
+
For applications that need to wrap a value in an application-specific CBOR tag (COSE envelopes, dCBOR application tags, custom protocols) without the ceremony of registering a `typeEncoders` entry and a matching tag decoder, cborg exports a `Tagged` wrapper class. `Tagged` is symmetric: pass it to `encode()` to emit a tag, and use `Tagged.decoder(tag)` or `Tagged.preserve(...tags)` on the decode side to round-trip the tag without losing it.
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
import { encode, decode, Tagged } from 'cborg'
|
|
333
|
+
|
|
334
|
+
// Encode: wrap any value in a tag
|
|
335
|
+
const bytes = encode(new Tagged(1234, 'hello'))
|
|
336
|
+
|
|
337
|
+
// Decode: preserve the tag through decode
|
|
338
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(1234) })
|
|
339
|
+
decoded instanceof Tagged // true
|
|
340
|
+
decoded.tag // 1234
|
|
341
|
+
decoded.value // 'hello'
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`Tagged.value` can be anything cborg can encode, including arrays, maps, other `Tagged` instances, or values handled by your own `typeEncoders` — cborg recurses into it with the same encode pipeline.
|
|
345
|
+
|
|
346
|
+
For finer-grained control on decode, use `Tagged.decoder(tag)` directly to mix preserved tags with other decoders:
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
import { decode, Tagged } from 'cborg'
|
|
350
|
+
|
|
351
|
+
const value = decode(bytes, {
|
|
352
|
+
tags: {
|
|
353
|
+
16: Tagged.decoder(16), // preserve tag 16 as Tagged
|
|
354
|
+
96: Tagged.decoder(96), // preserve tag 96 as Tagged
|
|
355
|
+
42: cidDecoder // a custom decoder for tag 42
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
When to prefer `Tagged` over a custom `typeEncoders` entry:
|
|
361
|
+
|
|
362
|
+
- You have a one-off or application-specific tag and don't want to define a JS class for it.
|
|
363
|
+
- You want to inspect the tag number on the decode side rather than collapsing it into a JS type.
|
|
364
|
+
- Your downstream code already speaks `{ tag, value }`.
|
|
365
|
+
|
|
366
|
+
When to prefer a `typeEncoders` entry instead:
|
|
367
|
+
|
|
368
|
+
- You're systematically mapping a JS type to a tag (e.g. `CID` → tag 42, `Date` → tag 1) and want it to apply automatically wherever that type appears.
|
|
369
|
+
- You need a tighter binary representation than the default recursive encoding (e.g. flatten the value to a `bytes` token rather than a nested CBOR structure).
|
|
370
|
+
|
|
371
|
+
The default `Tagged` behaviour is itself implemented as a `typeEncoders` entry under the type name `'Tagged'`. You can override it by supplying your own `typeEncoders.Tagged` and returning `null` from it to fall through to the default for cases your override doesn't want to handle.
|
|
372
|
+
|
|
328
373
|
### Type encoders
|
|
329
374
|
|
|
330
375
|
The `typeEncoders` property to the `options` argument to `encode()` allows you to add additional functionality to cborg, or override existing functionality.
|
|
331
376
|
|
|
332
377
|
When converting JavaScript objects, types are differentiated using the method and naming used by [@sindresorhus/is](https://github.com/sindresorhus/is) _(a custom implementation is used internally for performance reasons)_ and an internal set of type encoders are used to convert objects to their appropriate CBOR form. Supported types are: `null`, `undefined`, `number`, `bigint`, `string`, `boolean`, `Array`, `Object`, `Map`, `Buffer`, `ArrayBuffer`, `DataView`, `Uint8Array` and all other `TypedArray`s (their underlying byte array is encoded, so they will all round-trip as a `Uint8Array` since the type information is lost). Any object that doesn't match a type in this list will cause an error to be thrown during decode. e.g. `encode(new Date())` will throw an error because there is no internal `Date` type encoder.
|
|
333
378
|
|
|
334
|
-
The `typeEncoders` option is an object whose property names match to @sindresorhus/is type names. When this option is provided and a property exists for any given object's type, the function
|
|
379
|
+
The `typeEncoders` option is an object whose property names match to @sindresorhus/is type names. When this option is provided and a property exists for any given object's type, the function is called with the signature `(obj, typ, options, refStack)`:
|
|
380
|
+
|
|
381
|
+
* `obj` - the value being encoded
|
|
382
|
+
* `typ` - the resolved type name (the same string used as the property key)
|
|
383
|
+
* `options` - the full encode options object, useful for recursive encoding (see below)
|
|
384
|
+
* `refStack` - an internal circular-reference tracker, opaque to user code, that should be threaded through any recursive call
|
|
335
385
|
|
|
336
386
|
If a type encoder function returns `null`, the default encoder, if any, is used instead.
|
|
337
387
|
|
|
338
|
-
If a type encoder function returns an array, cborg will expect it to contain zero or more `Token` objects that will be encoded to binary form.
|
|
388
|
+
If a type encoder function returns an array, cborg will expect it to contain zero or more `Token` objects (or nested arrays thereof) that will be encoded to binary form. To **recursively encode a nested JavaScript value** as part of your tokens (so that `typeEncoders` for the nested value still apply), import `objectToTokens` from `cborg` and call `objectToTokens(value, options, refStack)` from inside your encoder. This is how the built-in `Tagged` encoder, `mapEncoder`, and `setEncoder` recurse into their contents:
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
import { Token, Type, objectToTokens } from 'cborg'
|
|
392
|
+
|
|
393
|
+
function myWrapperEncoder (obj, _typ, options, refStack) {
|
|
394
|
+
return [
|
|
395
|
+
new Token(Type.tag, 1234),
|
|
396
|
+
objectToTokens(obj.inner, options, refStack)
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
For tag encoders that flatten their content to a leaf type (e.g. encode a `BigInt` as a `Type.bytes` token under tag 2), you don't need to recurse - just return the flat token sequence directly, as the `bigIntEncoder` example below does.
|
|
339
402
|
|
|
340
403
|
`Token`s map directly to CBOR entities. Each one has a `Type` and a `value`. A type encoder is responsible for turning a JavaScript object into a set of tags.
|
|
341
404
|
|
package/cborg.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { encode, encodeInto, rfc8949EncodeOptions } from './lib/encode.js'
|
|
1
|
+
import { encode, encodeInto, objectToTokens, rfc8949EncodeOptions } from './lib/encode.js'
|
|
2
2
|
import { decode, decodeFirst, Tokeniser, tokensToObject } from './lib/decode.js'
|
|
3
|
+
import { Tagged } from './lib/tagged.js'
|
|
3
4
|
import { Token, Type } from './lib/token.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Export the types that were present in the original manual cborg.d.ts
|
|
8
|
+
* @typedef {import('./interface.js').TagDecodeControl} TagDecodeControl
|
|
7
9
|
* @typedef {import('./interface.js').TagDecoder} TagDecoder
|
|
8
10
|
* There was originally just `TypeEncoder` so don't break types by renaming or not exporting
|
|
9
11
|
* @typedef {import('./interface.js').OptionalTypeEncoder} TypeEncoder
|
|
@@ -18,7 +20,9 @@ export {
|
|
|
18
20
|
tokensToObject,
|
|
19
21
|
encode,
|
|
20
22
|
encodeInto,
|
|
23
|
+
objectToTokens,
|
|
21
24
|
rfc8949EncodeOptions,
|
|
25
|
+
Tagged,
|
|
22
26
|
Token,
|
|
23
27
|
Type
|
|
24
28
|
}
|
package/lib/encode.js
CHANGED
|
@@ -294,6 +294,24 @@ const typeEncoders = {
|
|
|
294
294
|
return [new Token(Type.map, entries.length), entries, new Token(Type.break)]
|
|
295
295
|
}
|
|
296
296
|
return [new Token(Type.map, entries.length), entries]
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Encode a `Tagged` wrapper as a CBOR tag header followed by the encoded
|
|
301
|
+
* form of the wrapped value. The value is recursively tokenised through
|
|
302
|
+
* `objectToTokens()` so any registered `typeEncoders` apply to it.
|
|
303
|
+
*
|
|
304
|
+
* @param {any} obj
|
|
305
|
+
* @param {string} _typ
|
|
306
|
+
* @param {EncodeOptions} options
|
|
307
|
+
* @param {Reference} [refStack]
|
|
308
|
+
* @returns {TokenOrNestedTokens}
|
|
309
|
+
*/
|
|
310
|
+
Tagged (obj, _typ, options, refStack) {
|
|
311
|
+
return [
|
|
312
|
+
new Token(Type.tag, obj.tag),
|
|
313
|
+
objectToTokens(obj.value, options, refStack)
|
|
314
|
+
]
|
|
297
315
|
}
|
|
298
316
|
}
|
|
299
317
|
|
package/lib/is.js
CHANGED
package/lib/tagged.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../interface.js').TagDecodeControl} TagDecodeControl
|
|
3
|
+
* @typedef {(decode: TagDecodeControl) => Tagged} TaggedTagDecoder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A wrapper class for representing a CBOR tag with an arbitrary nested value.
|
|
8
|
+
*
|
|
9
|
+
* `Tagged` is a symmetric primitive: it can be passed to `encode()` to emit
|
|
10
|
+
* a CBOR tag header followed by the encoded form of `value`, and it can be
|
|
11
|
+
* returned from a tag decoder (via `Tagged.decoder(tag)` or `Tagged.preserve()`)
|
|
12
|
+
* to round-trip a tag through decode without losing the tag number.
|
|
13
|
+
*
|
|
14
|
+
* Use `Tagged` for one-off tag handling where defining a dedicated
|
|
15
|
+
* `typeEncoders` entry and tag decoder pair would be heavyweight, e.g. when
|
|
16
|
+
* wrapping a structure in a single application-specific tag (COSE, dCBOR
|
|
17
|
+
* envelopes, etc.).
|
|
18
|
+
*
|
|
19
|
+
* For systematic mapping of a JS type to a tag (e.g. CID -> tag 42), prefer
|
|
20
|
+
* a dedicated `typeEncoders` entry instead.
|
|
21
|
+
*/
|
|
22
|
+
export class Tagged {
|
|
23
|
+
/**
|
|
24
|
+
* @param {number} tag - CBOR tag number, a non-negative integer
|
|
25
|
+
* @param {any} value - The value to be tagged; encoded recursively
|
|
26
|
+
*/
|
|
27
|
+
constructor (tag, value) {
|
|
28
|
+
if (typeof tag !== 'number' || !Number.isInteger(tag) || tag < 0) {
|
|
29
|
+
throw new TypeError('Tagged: tag must be a non-negative integer')
|
|
30
|
+
}
|
|
31
|
+
this.tag = tag
|
|
32
|
+
this.value = value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a tag decoder for use in `decode()`'s `tags` option that returns the
|
|
37
|
+
* decoded content wrapped in a `Tagged` instance, preserving the tag number
|
|
38
|
+
* for the caller to inspect.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} tag - The CBOR tag number this decoder will be registered for
|
|
41
|
+
* @returns {TaggedTagDecoder}
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { decode, Tagged } from 'cborg'
|
|
45
|
+
* const value = decode(bytes, { tags: { 16: Tagged.decoder(16) } })
|
|
46
|
+
* // value instanceof Tagged; value.tag === 16
|
|
47
|
+
*/
|
|
48
|
+
static decoder (tag) {
|
|
49
|
+
return (decode) => new Tagged(tag, decode())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a `tags` option for `decode()` that wraps each listed tag number in
|
|
54
|
+
* a `Tagged` instance, preserving those tags through decode without
|
|
55
|
+
* registering a dedicated decoder per tag.
|
|
56
|
+
*
|
|
57
|
+
* @param {...number} tagNumbers - One or more CBOR tag numbers to preserve
|
|
58
|
+
* @returns {{[tagNumber: number]: TaggedTagDecoder}}
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* import { decode, Tagged } from 'cborg'
|
|
62
|
+
* const value = decode(bytes, { tags: Tagged.preserve(16, 96) })
|
|
63
|
+
*/
|
|
64
|
+
static preserve (...tagNumbers) {
|
|
65
|
+
/** @type {{[tagNumber: number]: TaggedTagDecoder}} */
|
|
66
|
+
const tags = {}
|
|
67
|
+
for (const tag of tagNumbers) {
|
|
68
|
+
tags[tag] = Tagged.decoder(tag)
|
|
69
|
+
}
|
|
70
|
+
return tags
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Symbol.toStringTag so the internal `is()` type detection returns 'Tagged'
|
|
75
|
+
// for instances, allowing the default Tagged typeEncoder to match.
|
|
76
|
+
Object.defineProperty(Tagged.prototype, Symbol.toStringTag, {
|
|
77
|
+
value: 'Tagged'
|
|
78
|
+
})
|
package/package.json
CHANGED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-env mocha */
|
|
2
|
+
|
|
3
|
+
import * as chai from 'chai'
|
|
4
|
+
|
|
5
|
+
import { decode, encode, encodeInto, rfc8949EncodeOptions, Tagged, Token, Type } from '../cborg.js'
|
|
6
|
+
import { encodedLength } from '../lib/length.js'
|
|
7
|
+
import { fromHex, toHex } from '../lib/byte-utils.js'
|
|
8
|
+
|
|
9
|
+
const { assert } = chai
|
|
10
|
+
|
|
11
|
+
describe('Tagged', () => {
|
|
12
|
+
describe('class', () => {
|
|
13
|
+
it('exposes tag and value', () => {
|
|
14
|
+
const t = new Tagged(16, 'hello')
|
|
15
|
+
assert.strictEqual(t.tag, 16)
|
|
16
|
+
assert.strictEqual(t.value, 'hello')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects non-integer tag', () => {
|
|
20
|
+
assert.throws(() => new Tagged(1.5, null), TypeError)
|
|
21
|
+
assert.throws(() => new Tagged(-1, null), TypeError)
|
|
22
|
+
// @ts-expect-error - intentional bad input
|
|
23
|
+
assert.throws(() => new Tagged('16', null), TypeError)
|
|
24
|
+
// @ts-expect-error - intentional bad input
|
|
25
|
+
assert.throws(() => new Tagged(undefined, null), TypeError)
|
|
26
|
+
assert.throws(() => new Tagged(NaN, null), TypeError)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Symbol.toStringTag is "Tagged"', () => {
|
|
30
|
+
assert.strictEqual(Object.prototype.toString.call(new Tagged(0, null)), '[object Tagged]')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('encode', () => {
|
|
35
|
+
it('encodes a small tag with primitive value', () => {
|
|
36
|
+
// RFC 8949 example: tag 1 wrapping 1363896240
|
|
37
|
+
const bytes = encode(new Tagged(1, 1363896240))
|
|
38
|
+
assert.strictEqual(toHex(bytes), 'c11a514b67b0')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('encodes a 1-byte tag header (>= 24)', () => {
|
|
42
|
+
const bytes = encode(new Tagged(32, 'http://example.com'))
|
|
43
|
+
assert.strictEqual(bytes[0], 0xd8)
|
|
44
|
+
assert.strictEqual(bytes[1], 32)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('encodes a 2-byte tag header (>= 256)', () => {
|
|
48
|
+
const bytes = encode(new Tagged(259, new Map()))
|
|
49
|
+
// d9 01 03 = tag(259), then a0 = map(0)
|
|
50
|
+
assert.strictEqual(toHex(bytes), 'd90103a0')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('encodes a 4-byte tag header (>= 65536)', () => {
|
|
54
|
+
const bytes = encode(new Tagged(0x10000, 0))
|
|
55
|
+
// da 00 01 00 00 = tag(65536), then 00 = uint(0)
|
|
56
|
+
assert.strictEqual(toHex(bytes), 'da0001000000')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('recurses into nested object value', () => {
|
|
60
|
+
const bytes = encode(new Tagged(16, [new Uint8Array([1, 2, 3]), new Map(), null]))
|
|
61
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(16), useMaps: true })
|
|
62
|
+
assert.instanceOf(decoded, Tagged)
|
|
63
|
+
assert.strictEqual(decoded.tag, 16)
|
|
64
|
+
assert.deepEqual(decoded.value[0], new Uint8Array([1, 2, 3]))
|
|
65
|
+
assert.instanceOf(decoded.value[1], Map)
|
|
66
|
+
assert.strictEqual(decoded.value[2], null)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('Tagged is overridable via typeEncoders (returning null falls through)', () => {
|
|
70
|
+
// Override that only special-cases tag 99 and falls through for everything else
|
|
71
|
+
let calls = 0
|
|
72
|
+
const bytes = encode(new Tagged(99, 'ignored'), {
|
|
73
|
+
typeEncoders: {
|
|
74
|
+
Tagged: (obj) => {
|
|
75
|
+
calls++
|
|
76
|
+
if (obj.tag !== 999) return null // fall through
|
|
77
|
+
return [new Token(Type.string, 'replaced')]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
assert.strictEqual(calls, 1)
|
|
82
|
+
// Falling through means default behaviour: tag(99) text("ignored")
|
|
83
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(99) })
|
|
84
|
+
assert.strictEqual(decoded.tag, 99)
|
|
85
|
+
assert.strictEqual(decoded.value, 'ignored')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('Tagged is overridable via typeEncoders (full replacement)', () => {
|
|
89
|
+
const bytes = encode(new Tagged(99, 'foo'), {
|
|
90
|
+
typeEncoders: {
|
|
91
|
+
Tagged: () => [new Token(Type.string, 'replaced')]
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
assert.strictEqual(decode(bytes), 'replaced')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('typeEncoders for inner types apply to wrapped value', () => {
|
|
98
|
+
// Custom Date encoder should fire when Date appears inside Tagged.value
|
|
99
|
+
let dateEncoderCalled = false
|
|
100
|
+
const bytes = encode(new Tagged(16, [new Date(0)]), {
|
|
101
|
+
typeEncoders: {
|
|
102
|
+
Date: (d) => {
|
|
103
|
+
dateEncoderCalled = true
|
|
104
|
+
return [new Token(Type.uint, d.getTime())]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
assert.isTrue(dateEncoderCalled)
|
|
109
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(16) })
|
|
110
|
+
assert.strictEqual(decoded.tag, 16)
|
|
111
|
+
assert.deepEqual(decoded.value, [0])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('rfc8949EncodeOptions sorts maps inside Tagged.value', () => {
|
|
115
|
+
// Build a Map whose insertion order differs from RFC 8949 bytewise order.
|
|
116
|
+
// RFC 8949 sorts keys by their canonical encoding bytes; for short
|
|
117
|
+
// strings the longer key sorts later.
|
|
118
|
+
const inner = new Map([
|
|
119
|
+
['bb', 2],
|
|
120
|
+
['a', 1]
|
|
121
|
+
])
|
|
122
|
+
const sortedDefault = encode(new Tagged(16, inner))
|
|
123
|
+
const sortedRfc8949 = encode(new Tagged(16, inner), rfc8949EncodeOptions)
|
|
124
|
+
|
|
125
|
+
// Default (RFC 7049, length-first) and RFC 8949 (bytewise) agree here:
|
|
126
|
+
// 'a' (1 byte) sorts before 'bb' (2 bytes) under both rules.
|
|
127
|
+
assert.deepEqual(sortedDefault, sortedRfc8949)
|
|
128
|
+
|
|
129
|
+
// Verify the tag header is present and the inner map is sorted
|
|
130
|
+
const decodedDefault = decode(sortedDefault, { tags: Tagged.preserve(16), useMaps: true })
|
|
131
|
+
assert.strictEqual(decodedDefault.tag, 16)
|
|
132
|
+
assert.instanceOf(decodedDefault.value, Map)
|
|
133
|
+
assert.deepEqual([...decodedDefault.value.keys()], ['a', 'bb'])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('detects circular references inside Tagged.value', () => {
|
|
137
|
+
const arr = []
|
|
138
|
+
arr.push(arr)
|
|
139
|
+
assert.throws(() => encode(new Tagged(16, arr)), /circular/i)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('works with encodeInto', () => {
|
|
143
|
+
const dest = new Uint8Array(64)
|
|
144
|
+
const { written } = encodeInto(new Tagged(1, 1363896240), dest)
|
|
145
|
+
assert.strictEqual(toHex(dest.subarray(0, written)), 'c11a514b67b0')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('works with encodedLength', () => {
|
|
149
|
+
const value = new Tagged(16, [new Uint8Array([1, 2, 3]), new Map([[1, 2]]), null])
|
|
150
|
+
const expected = encode(value).length
|
|
151
|
+
assert.strictEqual(encodedLength(value), expected)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('decode', () => {
|
|
156
|
+
it('Tagged.decoder builds a passthrough decoder', () => {
|
|
157
|
+
const bytes = fromHex('c11a514b67b0') // tag(1) 1363896240
|
|
158
|
+
const value = decode(bytes, { tags: { 1: Tagged.decoder(1) } })
|
|
159
|
+
assert.instanceOf(value, Tagged)
|
|
160
|
+
assert.strictEqual(value.tag, 1)
|
|
161
|
+
assert.strictEqual(value.value, 1363896240)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('Tagged.preserve builds a tags map for multiple tags', () => {
|
|
165
|
+
const tags = Tagged.preserve(16, 96)
|
|
166
|
+
assert.typeOf(tags[16], 'function')
|
|
167
|
+
assert.typeOf(tags[96], 'function')
|
|
168
|
+
assert.isUndefined(tags[1])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('Tagged.preserve with no arguments yields an empty tags map', () => {
|
|
172
|
+
const tags = Tagged.preserve()
|
|
173
|
+
assert.deepEqual(Object.keys(tags), [])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('unregistered tags still throw when Tagged.preserve is partial', () => {
|
|
177
|
+
// tag(2) wrapping bytes(0)
|
|
178
|
+
const bytes = fromHex('c240')
|
|
179
|
+
assert.throws(() => decode(bytes, { tags: Tagged.preserve(16) }), /tag not supported/)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('round-trip', () => {
|
|
184
|
+
it('round-trips a COSE_Encrypt0-shaped envelope', () => {
|
|
185
|
+
// Build a structure resembling COSE_Encrypt0 (RFC 9052):
|
|
186
|
+
// tag(16) [protected_bstr, unprotected_map, ciphertext_or_nil]
|
|
187
|
+
const protectedHeaders = encode(new Map([[1, -7]])) // alg = ES256
|
|
188
|
+
const unprotected = new Map([[5, new Uint8Array([0xaa, 0xbb])]]) // iv
|
|
189
|
+
const envelope = new Tagged(16, [protectedHeaders, unprotected, null])
|
|
190
|
+
|
|
191
|
+
const bytes = encode(envelope)
|
|
192
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(16), useMaps: true })
|
|
193
|
+
|
|
194
|
+
assert.instanceOf(decoded, Tagged)
|
|
195
|
+
assert.strictEqual(decoded.tag, 16)
|
|
196
|
+
assert.deepEqual(decoded.value[0], protectedHeaders)
|
|
197
|
+
assert.instanceOf(decoded.value[1], Map)
|
|
198
|
+
assert.deepEqual(decoded.value[1].get(5), new Uint8Array([0xaa, 0xbb]))
|
|
199
|
+
assert.strictEqual(decoded.value[2], null)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('round-trips deeply nested Tagged instances', () => {
|
|
203
|
+
let v = /** @type {any} */ ('leaf')
|
|
204
|
+
for (let i = 1; i <= 5; i++) {
|
|
205
|
+
v = new Tagged(i, v)
|
|
206
|
+
}
|
|
207
|
+
const bytes = encode(v)
|
|
208
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(1, 2, 3, 4, 5) })
|
|
209
|
+
let cur = decoded
|
|
210
|
+
for (let i = 5; i >= 1; i--) {
|
|
211
|
+
assert.instanceOf(cur, Tagged)
|
|
212
|
+
assert.strictEqual(cur.tag, i)
|
|
213
|
+
cur = cur.value
|
|
214
|
+
}
|
|
215
|
+
assert.strictEqual(cur, 'leaf')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('Tagged round-trips through rfc8949EncodeOptions', () => {
|
|
219
|
+
const value = new Tagged(16, [
|
|
220
|
+
new Tagged(2, new Uint8Array([0xff])),
|
|
221
|
+
new Map([['z', 1], ['a', 2]])
|
|
222
|
+
])
|
|
223
|
+
const bytes = encode(value, rfc8949EncodeOptions)
|
|
224
|
+
const decoded = decode(bytes, { tags: Tagged.preserve(2, 16), useMaps: true })
|
|
225
|
+
assert.strictEqual(decoded.tag, 16)
|
|
226
|
+
assert.strictEqual(decoded.value[0].tag, 2)
|
|
227
|
+
assert.deepEqual(decoded.value[0].value, new Uint8Array([0xff]))
|
|
228
|
+
assert.deepEqual([...decoded.value[1].keys()], ['a', 'z'])
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
package/types/cborg.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export the types that were present in the original manual cborg.d.ts
|
|
3
|
+
*/
|
|
4
|
+
export type TagDecodeControl = import("./interface.js").TagDecodeControl;
|
|
1
5
|
/**
|
|
2
6
|
* There was originally just `TypeEncoder` so don't break types by renaming or not exporting
|
|
3
7
|
*/
|
|
@@ -20,8 +24,10 @@ import { Tokeniser } from './lib/decode.js';
|
|
|
20
24
|
import { tokensToObject } from './lib/decode.js';
|
|
21
25
|
import { encode } from './lib/encode.js';
|
|
22
26
|
import { encodeInto } from './lib/encode.js';
|
|
27
|
+
import { objectToTokens } from './lib/encode.js';
|
|
23
28
|
import { rfc8949EncodeOptions } from './lib/encode.js';
|
|
29
|
+
import { Tagged } from './lib/tagged.js';
|
|
24
30
|
import { Token } from './lib/token.js';
|
|
25
31
|
import { Type } from './lib/token.js';
|
|
26
|
-
export { decode, decodeFirst, Tokeniser as Tokenizer, tokensToObject, encode, encodeInto, rfc8949EncodeOptions, Token, Type };
|
|
32
|
+
export { decode, decodeFirst, Tokeniser as Tokenizer, tokensToObject, encode, encodeInto, objectToTokens, rfc8949EncodeOptions, Tagged, Token, Type };
|
|
27
33
|
//# sourceMappingURL=cborg.d.ts.map
|
package/types/cborg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cborg.d.ts","sourceRoot":"","sources":["../cborg.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cborg.d.ts","sourceRoot":"","sources":["../cborg.js"],"names":[],"mappings":";;;+BAOa,OAAO,gBAAgB,EAAE,gBAAgB;;;;yBACzC,OAAO,gBAAgB,EAAE,UAAU;;;;0BAEnC,OAAO,gBAAgB,EAAE,mBAAmB;;;;4BAC5C,OAAO,gBAAgB,EAAE,aAAa;;;;4BACtC,OAAO,gBAAgB,EAAE,aAAa;uBAXY,iBAAiB;4BAAjB,iBAAiB;0BAAjB,iBAAiB;+BAAjB,iBAAiB;uBADP,iBAAiB;2BAAjB,iBAAiB;+BAAjB,iBAAiB;qCAAjB,iBAAiB;uBAEnE,iBAAiB;sBACZ,gBAAgB;qBAAhB,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../lib/encode.js"],"names":[],"mappings":"AAwCA,oCAAoC;AACpC,oCADc,gBAAgB,EAAE,CAY/B;AAnBD,4BAA4B;AAC5B,mCADW,aAAa,CAKtB;
|
|
1
|
+
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../lib/encode.js"],"names":[],"mappings":"AAwCA,oCAAoC;AACpC,oCADc,gBAAgB,EAAE,CAY/B;AAnBD,4BAA4B;AAC5B,mCADW,aAAa,CAKtB;sBAiZW,KAAK,GAAG;IAAE,SAAS,CAAC,EAAE,UAAU,CAAA;CAAE;4BAtalC,OAAO,iBAAiB,EAAE,aAAa;kCACvC,OAAO,iBAAiB,EAAE,mBAAmB;wBAC7C,OAAO,iBAAiB,EAAE,SAAS;gCACnC,OAAO,iBAAiB,EAAE,iBAAiB;+BAC3C,OAAO,iBAAiB,EAAE,gBAAgB;kCAC1C,OAAO,iBAAiB,EAAE,mBAAmB;yBAC7C,OAAO,iBAAiB,EAAE,UAAU;AA4SjD;;;;;GAKG;AACH,oCALW,GAAG,YACH,aAAa,aACb,SAAS,GACP,mBAAmB,CAgB/B;AAiUD;;;;GAIG;AACH,6BAJW,GAAG,YACH,aAAa,GACX,UAAU,CAatB;AA3DD;;;;;;GAMG;AACH,mCANW,GAAG,YACH,gBAAgB,EAAE,WAClB,aAAa,gBACb,UAAU,GACR,UAAU,CAoCtB;AAoBD;;;;;GAKG;AACH,iCALW,GAAG,eACH,UAAU,YACV,aAAa,GACX;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAc/B;AAloBD,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,wDAAoB;IAGtB;;;OAGG;IACH,cAHW,MAAM,GAAC,GAAG,EAAE,GACV,OAAO,CAWnB;CAaF;sBA9F2B,YAAY"}
|
package/types/lib/is.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is.d.ts","sourceRoot":"","sources":["../../lib/is.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"is.d.ts","sourceRoot":"","sources":["../../lib/is.js"],"names":[],"mappings":"AAmCA;;;GAGG;AACH,0BAHW,GAAG,GACD,MAAM,CAqClB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../interface.js').TagDecodeControl} TagDecodeControl
|
|
3
|
+
* @typedef {(decode: TagDecodeControl) => Tagged} TaggedTagDecoder
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A wrapper class for representing a CBOR tag with an arbitrary nested value.
|
|
7
|
+
*
|
|
8
|
+
* `Tagged` is a symmetric primitive: it can be passed to `encode()` to emit
|
|
9
|
+
* a CBOR tag header followed by the encoded form of `value`, and it can be
|
|
10
|
+
* returned from a tag decoder (via `Tagged.decoder(tag)` or `Tagged.preserve()`)
|
|
11
|
+
* to round-trip a tag through decode without losing the tag number.
|
|
12
|
+
*
|
|
13
|
+
* Use `Tagged` for one-off tag handling where defining a dedicated
|
|
14
|
+
* `typeEncoders` entry and tag decoder pair would be heavyweight, e.g. when
|
|
15
|
+
* wrapping a structure in a single application-specific tag (COSE, dCBOR
|
|
16
|
+
* envelopes, etc.).
|
|
17
|
+
*
|
|
18
|
+
* For systematic mapping of a JS type to a tag (e.g. CID -> tag 42), prefer
|
|
19
|
+
* a dedicated `typeEncoders` entry instead.
|
|
20
|
+
*/
|
|
21
|
+
export class Tagged {
|
|
22
|
+
/**
|
|
23
|
+
* Build a tag decoder for use in `decode()`'s `tags` option that returns the
|
|
24
|
+
* decoded content wrapped in a `Tagged` instance, preserving the tag number
|
|
25
|
+
* for the caller to inspect.
|
|
26
|
+
*
|
|
27
|
+
* @param {number} tag - The CBOR tag number this decoder will be registered for
|
|
28
|
+
* @returns {TaggedTagDecoder}
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* import { decode, Tagged } from 'cborg'
|
|
32
|
+
* const value = decode(bytes, { tags: { 16: Tagged.decoder(16) } })
|
|
33
|
+
* // value instanceof Tagged; value.tag === 16
|
|
34
|
+
*/
|
|
35
|
+
static decoder(tag: number): TaggedTagDecoder;
|
|
36
|
+
/**
|
|
37
|
+
* Build a `tags` option for `decode()` that wraps each listed tag number in
|
|
38
|
+
* a `Tagged` instance, preserving those tags through decode without
|
|
39
|
+
* registering a dedicated decoder per tag.
|
|
40
|
+
*
|
|
41
|
+
* @param {...number} tagNumbers - One or more CBOR tag numbers to preserve
|
|
42
|
+
* @returns {{[tagNumber: number]: TaggedTagDecoder}}
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* import { decode, Tagged } from 'cborg'
|
|
46
|
+
* const value = decode(bytes, { tags: Tagged.preserve(16, 96) })
|
|
47
|
+
*/
|
|
48
|
+
static preserve(...tagNumbers: number[]): {
|
|
49
|
+
[tagNumber: number]: TaggedTagDecoder;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* @param {number} tag - CBOR tag number, a non-negative integer
|
|
53
|
+
* @param {any} value - The value to be tagged; encoded recursively
|
|
54
|
+
*/
|
|
55
|
+
constructor(tag: number, value: any);
|
|
56
|
+
tag: number;
|
|
57
|
+
value: any;
|
|
58
|
+
}
|
|
59
|
+
export type TagDecodeControl = import("../interface.js").TagDecodeControl;
|
|
60
|
+
export type TaggedTagDecoder = (decode: TagDecodeControl) => Tagged;
|
|
61
|
+
//# sourceMappingURL=tagged.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tagged.d.ts","sourceRoot":"","sources":["../../lib/tagged.js"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;;;;GAeG;AACH;IAaE;;;;;;;;;;;;OAYG;IACH,oBARW,MAAM,GACJ,gBAAgB,CAS5B;IAED;;;;;;;;;;;OAWG;IACH,+BAPc,MAAM,EAAA,GACP;QAAC,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAA;KAAC,CAanD;IAhDD;;;OAGG;IACH,iBAHW,MAAM,SACN,GAAG,EAQb;IAFC,YAAc;IACd,WAAkB;CAwCrB;+BAtEY,OAAO,iBAAiB,EAAE,gBAAgB;+BAC1C,CAAC,MAAM,EAAE,gBAAgB,KAAK,MAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../cborg.js","../interface.ts","../lib/0uint.js","../lib/1negint.js","../lib/2bytes.js","../lib/3string.js","../lib/4array.js","../lib/5map.js","../lib/6tag.js","../lib/7float.js","../lib/bl.js","../lib/byte-utils.js","../lib/common.js","../lib/decode.js","../lib/diagnostic.js","../lib/diagnostic_test.js","../lib/encode.js","../lib/is.js","../lib/jump.js","../lib/length.js","../lib/taglib.js","../lib/token.js","../lib/extended/extended.js","../lib/json/decode.js","../lib/json/encode.js","../lib/json/json.js"],"version":"6.0.2"}
|
|
1
|
+
{"root":["../cborg.js","../interface.ts","../lib/0uint.js","../lib/1negint.js","../lib/2bytes.js","../lib/3string.js","../lib/4array.js","../lib/5map.js","../lib/6tag.js","../lib/7float.js","../lib/bl.js","../lib/byte-utils.js","../lib/common.js","../lib/decode.js","../lib/diagnostic.js","../lib/diagnostic_test.js","../lib/encode.js","../lib/is.js","../lib/jump.js","../lib/length.js","../lib/tagged.js","../lib/taglib.js","../lib/token.js","../lib/extended/extended.js","../lib/json/decode.js","../lib/json/encode.js","../lib/json/json.js"],"version":"6.0.2"}
|