@bcts/dcbor 1.0.0-beta.3 → 1.0.0-beta.4

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/src/float.ts CHANGED
@@ -28,7 +28,6 @@ import * as byteData from "byte-data";
28
28
  import { encodeVarInt } from "./varint";
29
29
  import { MajorType } from "./cbor";
30
30
  import { ExactU64, ExactU32, ExactU16, ExactI128 } from "./exact";
31
- import { CborError } from "./error";
32
31
 
33
32
  /**
34
33
  * Canonical NaN representation in CBOR: 0xf97e00
@@ -102,9 +101,10 @@ export const f64CborData = (value: number): Uint8Array => {
102
101
  if (i128 !== undefined) {
103
102
  const i = ExactU64.exactFromI128(-1n - i128);
104
103
  if (i !== undefined) {
105
- // Encode as a negative integer
106
- const num = typeof i === "bigint" ? Number(i) : i;
107
- return encodeVarInt(num, MajorType.Negative);
104
+ // Encode as a negative integer. Pass the bigint straight through:
105
+ // encodeVarInt handles u64 losslessly via setBigUint64, whereas
106
+ // narrowing through Number() would round magnitudes above 2^53.
107
+ return encodeVarInt(i, MajorType.Negative);
108
108
  }
109
109
  }
110
110
  }
@@ -112,8 +112,7 @@ export const f64CborData = (value: number): Uint8Array => {
112
112
  // Try numeric reduction to unsigned integer
113
113
  const u = ExactU64.exactFromF64(n);
114
114
  if (u !== undefined) {
115
- const num = typeof u === "bigint" ? Number(u) : u;
116
- return encodeVarInt(num, MajorType.Unsigned);
115
+ return encodeVarInt(u, MajorType.Unsigned);
117
116
  }
118
117
 
119
118
  // Canonical NaN
@@ -130,32 +129,6 @@ export const f64CborData = (value: number): Uint8Array => {
130
129
  return new Uint8Array([majorByte, ...bytes]);
131
130
  };
132
131
 
133
- /**
134
- * Validate canonical encoding for f64.
135
- *
136
- * Counterpart to Rust's `validate_canonical_f64`. **NOT used by the
137
- * decoder.** JavaScript's `Number` type does not preserve NaN payload
138
- * bits — every NaN collapses to a single value — which means the
139
- * Rust-style `n.to_bits() != 0x7e00` distinction can't be made on a
140
- * post-decoded `number`. The TS decoder uses
141
- * {@link checkCanonicalEncoding} (re-encode-and-compare) instead, which
142
- * handles every same-failure case including non-canonical NaNs because
143
- * the canonicalising encoder always re-emits the canonical bit pattern.
144
- *
145
- * Kept for API parity with Rust's `pub(crate)` helper, plus as a
146
- * documentation anchor; not recommended for callers.
147
- *
148
- * @internal
149
- */
150
- export const validateCanonicalF64 = (n: number): void => {
151
- const f32Bytes = numberToBinary32(n);
152
- const f32 = binary32ToNumber(f32Bytes);
153
-
154
- if (n === f32 || n === Math.trunc(n) || Number.isNaN(n)) {
155
- throw new CborError({ type: "NonCanonicalNumeric" });
156
- }
157
- };
158
-
159
132
  /**
160
133
  * Encode f32 value to CBOR data bytes.
161
134
  * Implements numeric reduction and canonical encoding rules.
@@ -172,12 +145,12 @@ export const f32CborData = (value: number): Uint8Array => {
172
145
  return f16CborData(f);
173
146
  }
174
147
 
175
- // Try numeric reduction to negative integer
148
+ // Try numeric reduction to negative integer. Math.fround keeps `-1 - n` in
149
+ // f32 precision to match Rust's `-1f32 - n`.
176
150
  if (n < 0.0) {
177
- const u = ExactU64.exactFromF32(-1.0 - n);
151
+ const u = ExactU64.exactFromF32(Math.fround(-1.0 - n));
178
152
  if (u !== undefined) {
179
- const num = typeof u === "bigint" ? Number(u) : u;
180
- return encodeVarInt(num, MajorType.Negative);
153
+ return encodeVarInt(u, MajorType.Negative);
181
154
  }
182
155
  }
183
156
 
@@ -197,23 +170,6 @@ export const f32CborData = (value: number): Uint8Array => {
197
170
  return new Uint8Array([0xfa, ...bytes]);
198
171
  };
199
172
 
200
- /**
201
- * Validate canonical encoding for f32.
202
- *
203
- * @see {@link validateCanonicalF64} — same caveat about JS NaN bit
204
- * preservation. The decoder relies on {@link checkCanonicalEncoding}.
205
- *
206
- * @internal
207
- */
208
- export const validateCanonicalF32 = (n: number): void => {
209
- const f16Bytes = numberToBinary16(n);
210
- const f16 = binary16ToNumber(f16Bytes);
211
-
212
- if (n === f16 || n === Math.trunc(n) || Number.isNaN(n)) {
213
- throw new CborError({ type: "NonCanonicalNumeric" });
214
- }
215
- };
216
-
217
173
  /**
218
174
  * Encode f16 value to CBOR data bytes.
219
175
  * Implements numeric reduction and canonical encoding rules.
@@ -227,8 +183,7 @@ export const f16CborData = (value: number): Uint8Array => {
227
183
  if (n < 0.0) {
228
184
  const u = ExactU64.exactFromF64(-1.0 - n);
229
185
  if (u !== undefined) {
230
- const num = typeof u === "bigint" ? Number(u) : u;
231
- return encodeVarInt(num, MajorType.Negative);
186
+ return encodeVarInt(u, MajorType.Negative);
232
187
  }
233
188
  }
234
189
 
@@ -249,23 +204,37 @@ export const f16CborData = (value: number): Uint8Array => {
249
204
  };
250
205
 
251
206
  /**
252
- * Validate canonical encoding for f16.
207
+ * Render a float to its diagnostic string, matching Rust's `Simple` Display.
253
208
  *
254
- * @see {@link validateCanonicalF64} same caveat about JS NaN bit
255
- * preservation. The decoder relies on {@link checkCanonicalEncoding}.
209
+ * Finite non-zero values with magnitude in [1e-4, 1e16) print in decimal with
210
+ * at least one fractional digit (whole values get a trailing `.0`); everything
211
+ * else prints in exponential form. Zero prints as `0.0`/`-0.0`.
256
212
  *
257
- * @internal
213
+ * JS already produces the same shortest round-tripping digits; we only fix up
214
+ * the notation threshold, the `e+` → `e` exponent, and the `.0` suffix.
215
+ *
216
+ * @param value - The float value
217
+ * @returns Rust-`Display`-compatible string
258
218
  */
259
- export const validateCanonicalF16 = (value: number): void => {
260
- const n = value;
261
- const f = n;
262
-
263
- const f16Bytes = numberToBinary16(value);
264
- const bits = new DataView(f16Bytes.buffer).getUint16(0, false);
265
-
266
- if (f === Math.trunc(f) || (Number.isNaN(value) && bits !== 0x7e00)) {
267
- throw new CborError({ type: "NonCanonicalNumeric" });
219
+ export const floatDisplayString = (value: number): string => {
220
+ if (Number.isNaN(value)) return "NaN";
221
+ if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
222
+ if (value === 0) return Object.is(value, -0) ? "-0.0" : "0.0";
223
+
224
+ const abs = Math.abs(value);
225
+ if (abs >= 1e-4 && abs < 1e16) {
226
+ // In this range String() never switches to exponential. Ensure at least
227
+ // one fractional digit.
228
+ let str = String(value);
229
+ if (!str.includes(".")) {
230
+ str = `${str}.0`;
231
+ }
232
+ return str;
268
233
  }
234
+
235
+ // Drop the `+` in the exponent to match Rust (`1.5e+20` → `1.5e20`);
236
+ // negative exponents keep their sign.
237
+ return value.toExponential().replace("e+", "e");
269
238
  };
270
239
 
271
240
  /**
package/src/index.ts CHANGED
@@ -25,6 +25,9 @@ export {
25
25
  type CborMapType,
26
26
  type CborTaggedType,
27
27
  type CborSimpleType,
28
+ // Conversion interfaces accepted by `cbor()` / referenced by `CborInput`.
29
+ type ToCbor,
30
+ type TaggedCborEncodable,
28
31
  } from "./cbor";
29
32
 
30
33
  // Simple value types
@@ -59,6 +62,7 @@ export {
59
62
  tagWithValue,
60
63
  tagWithStaticName,
61
64
  tagsEqual,
65
+ tagValuesEqual,
62
66
  } from "./tag";
63
67
  export {
64
68
  type CborTagged,
@@ -77,7 +81,10 @@ export {
77
81
  type TagsStoreTrait,
78
82
  type CborSummarizer,
79
83
  type SummarizerResult,
84
+ type TagsStoreOpt,
80
85
  getGlobalTagsStore,
86
+ withTags,
87
+ withTagsMut,
81
88
  } from "./tags-store";
82
89
  export * from "./tags";
83
90
  export { registerTags, registerTagsIn, tagsForValues } from "./tags";
package/src/map.ts CHANGED
@@ -217,19 +217,19 @@ export class CborMap {
217
217
  * Matches Rust's Map::insert_next().
218
218
  */
219
219
  setNext<K extends CborInput, V extends CborInput>(key: K, value: V): void {
220
- const lastEntry = this._dict.max();
221
- if (lastEntry === undefined) {
222
- this.set(key, value);
223
- return;
224
- }
225
220
  const keyCbor = cbor(key);
226
221
  const newKey = cborData(keyCbor);
227
222
  if (this._dict.has(newKey)) {
228
223
  throw new CborError({ type: "DuplicateMapKey" });
229
224
  }
230
- const lastEntryKey = this._makeKey(lastEntry.key);
231
- if (lexicographicallyCompareBytes(newKey, lastEntryKey) <= 0) {
232
- throw new CborError({ type: "MisorderedMapKey" });
225
+ // Enforce strict ascending order by comparing the new key against the
226
+ // greatest existing encoded key, read from the sorted store's last item.
227
+ // Mirrors Rust's insert_next/last_key_value.
228
+ const greatest = this._dict.store.max();
229
+ if (greatest !== undefined) {
230
+ if (lexicographicallyCompareBytes(newKey, greatest.key) <= 0) {
231
+ throw new CborError({ type: "MisorderedMapKey" });
232
+ }
233
233
  }
234
234
  this._dict.set(newKey, { key: keyCbor, value: cbor(value) });
235
235
  }
package/src/prelude.ts CHANGED
@@ -56,9 +56,9 @@ export { CborDate } from "./date";
56
56
 
57
57
  // Tag handling
58
58
  export type { Tag, TagValue } from "./tag";
59
- export { createTag, tagWithValue, tagWithStaticName, tagsEqual } from "./tag";
59
+ export { createTag, tagWithValue, tagWithStaticName, tagsEqual, tagValuesEqual } from "./tag";
60
60
  export { TagsStore, getGlobalTagsStore, withTags, withTagsMut } from "./tags-store";
61
- export type { TagsStoreTrait } from "./tags-store";
61
+ export type { TagsStoreTrait, CborSummarizer, SummarizerResult, TagsStoreOpt } from "./tags-store";
62
62
  export { tagsForValues } from "./tags";
63
63
 
64
64
  // Format options
package/src/set.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  * Copyright © 2025-2026 Parity Technologies
4
4
  *
5
5
  *
6
- * Set data structure for CBOR with tag(258) encoding.
6
+ * Set data structure for CBOR.
7
7
  *
8
- * A Set is encoded as an array with no duplicate elements,
9
- * tagged with tag(258) to indicate set semantics.
8
+ * A Set encodes to a plain (untagged) array of unique elements in canonical
9
+ * ascending CBOR-byte order. There's no tag-258 here: this matches Rust
10
+ * dcbor's `From<Set> for CBOR`, which emits an untagged array.
10
11
  *
11
12
  * @module set
12
13
  */
@@ -14,20 +15,11 @@
14
15
  import { type Cbor, MajorType, type CborInput } from "./cbor";
15
16
  import { cbor, cborData } from "./cbor";
16
17
  import { CborMap } from "./map";
17
- import { createTag, type Tag } from "./tag";
18
- import { TAG_SET } from "./tags";
19
- import {
20
- type CborTaggedEncodable,
21
- type CborTaggedDecodable,
22
- createTaggedCbor,
23
- validateTag,
24
- extractTaggedContent,
25
- } from "./cbor-tagged";
26
18
  import { extractCbor } from "./conveniences";
27
19
  import { CborError } from "./error";
28
20
 
29
21
  /**
30
- * CBOR Set type with tag(258) encoding.
22
+ * CBOR Set type, encoded as a plain (untagged) array.
31
23
  *
32
24
  * Internally uses a CborMap to ensure unique elements with deterministic ordering.
33
25
  * Elements are ordered by their CBOR encoding (lexicographic byte order).
@@ -46,11 +38,11 @@ import { CborError } from "./error";
46
38
  * console.log(set.contains(2)); // true
47
39
  * console.log(set.contains(99)); // false
48
40
  *
49
- * // Encode to CBOR
50
- * const tagged = set.taggedCbor();
41
+ * // Encode to CBOR (untagged array)
42
+ * const c = set.toCbor();
51
43
  * ```
52
44
  */
53
- export class CborSet implements CborTaggedEncodable, CborTaggedDecodable<CborSet> {
45
+ export class CborSet {
54
46
  private readonly _map: CborMap;
55
47
 
56
48
  constructor() {
@@ -135,13 +127,12 @@ export class CborSet implements CborTaggedEncodable, CborTaggedDecodable<CborSet
135
127
  }
136
128
 
137
129
  /**
138
- * Insert an element into the set, requiring it to be strictly greater
139
- * (in canonical CBOR-encoded byte order) than every previously-inserted
140
- * element. Used by the decoder to reject misordered or duplicate
141
- * elements in tag-258 set encodings.
130
+ * Insert an element that must sort strictly after every element inserted so
131
+ * far (canonical CBOR-byte order). The decoder uses this to reject
132
+ * misordered or duplicate elements.
142
133
  *
143
134
  * Mirrors Rust `Set::insert_next` (`pub(crate)`); exposed here because
144
- * TypeScript doesn't have a crate-private visibility level.
135
+ * TypeScript has no crate-private visibility.
145
136
  *
146
137
  * @throws CborError of type `MisorderedMap` if `value` would not preserve
147
138
  * strict ascending CBOR-byte order, or `DuplicateMapKey` for an exact
@@ -377,32 +368,38 @@ export class CborSet implements CborTaggedEncodable, CborTaggedDecodable<CborSet
377
368
  }
378
369
 
379
370
  // =========================================================================
380
- // CborTagged Implementation
371
+ // CBOR Encoding / Decoding (untagged array — Rust parity)
381
372
  // =========================================================================
382
373
 
383
- cborTags(): Tag[] {
384
- return [createTag(TAG_SET, "set")];
385
- }
386
-
374
+ /**
375
+ * Encode the set as an (untagged) CBOR array of its elements, in canonical
376
+ * ascending CBOR-byte order. Matches Rust `From<Set> for CBOR`.
377
+ *
378
+ * @returns CBOR array
379
+ */
387
380
  untaggedCbor(): Cbor {
388
381
  // Encode as an array of values
389
382
  const values = this.values();
390
383
  return cbor(values);
391
384
  }
392
385
 
393
- taggedCbor(): Cbor {
394
- return createTaggedCbor(this);
395
- }
396
-
386
+ /**
387
+ * Decode a CborSet from a CBOR array into this instance.
388
+ *
389
+ * Mirrors Rust `Set::try_from_vec`, calling `insert_next` per item, so the
390
+ * array must already be in strict ascending CBOR-byte order with no
391
+ * duplicates (else `MisorderedMapKey`/`DuplicateMapKey`).
392
+ *
393
+ * @param c - CBOR array value
394
+ * @returns this
395
+ * @throws CborError of type `WrongType` if `c` is not an array.
396
+ */
397
397
  fromUntaggedCbor(c: Cbor): CborSet {
398
398
  if (c.type !== MajorType.Array) {
399
399
  throw new CborError({ type: "WrongType" });
400
400
  }
401
401
 
402
402
  this.clear();
403
- // Mirrors Rust `Set::try_from_vec` which calls `insert_next` per item:
404
- // a tag-258 wire encoding must already be in strict ascending CBOR-byte
405
- // order with no duplicates.
406
403
  for (const value of c.value) {
407
404
  this.insertNext(extractCbor(value) as CborInput);
408
405
  }
@@ -410,21 +407,14 @@ export class CborSet implements CborTaggedEncodable, CborTaggedDecodable<CborSet
410
407
  return this;
411
408
  }
412
409
 
413
- fromTaggedCbor(c: Cbor): CborSet {
414
- const expectedTags = this.cborTags();
415
- validateTag(c, expectedTags);
416
- const content = extractTaggedContent(c);
417
- return this.fromUntaggedCbor(content);
418
- }
419
-
420
410
  /**
421
- * Decode a CborSet from tagged CBOR (static method).
411
+ * Decode a CborSet from a CBOR array.
422
412
  *
423
- * @param cbor - Tagged CBOR value with tag(258)
413
+ * @param c - CBOR array value
424
414
  * @returns Decoded CborSet instance
425
415
  */
426
- static fromTaggedCborStatic(cbor: Cbor): CborSet {
427
- return new CborSet().fromTaggedCbor(cbor);
416
+ static fromCbor(c: Cbor): CborSet {
417
+ return new CborSet().fromUntaggedCbor(c);
428
418
  }
429
419
 
430
420
  // =========================================================================
@@ -441,12 +431,12 @@ export class CborSet implements CborTaggedEncodable, CborTaggedDecodable<CborSet
441
431
  }
442
432
 
443
433
  /**
444
- * Convert to CBOR bytes (tagged).
434
+ * Convert to encoded CBOR bytes.
445
435
  *
446
436
  * @returns Encoded CBOR bytes
447
437
  */
448
438
  toBytes(): Uint8Array {
449
- return cborData(this.taggedCbor());
439
+ return cborData(this.untaggedCbor());
450
440
  }
451
441
 
452
442
  /**
package/src/tag.ts CHANGED
@@ -80,16 +80,23 @@ export const tagWithValue = (value: TagValue): Tag => ({ value });
80
80
  export const tagWithStaticName = (value: TagValue, name: string): Tag => ({ value, name });
81
81
 
82
82
  /**
83
- * Compare two tags for equality. Mirrors Rust's `PartialEq for Tag`, which
84
- * compares by `value` only and ignores the optional `name`.
83
+ * Compare two tag values for equality, normalizing `number` vs `bigint`.
84
+ * A raw `===` would treat `100n` and `100` as unequal, so a large tag that
85
+ * decoded to a `bigint` wouldn't match the same value written as a `number`.
85
86
  */
86
- export const tagsEqual = (a: Tag, b: Tag): boolean => {
87
- if (typeof a.value === "bigint" || typeof b.value === "bigint") {
88
- return BigInt(a.value) === BigInt(b.value);
87
+ export const tagValuesEqual = (a: TagValue, b: TagValue): boolean => {
88
+ if (typeof a === "bigint" || typeof b === "bigint") {
89
+ return BigInt(a) === BigInt(b);
89
90
  }
90
- return a.value === b.value;
91
+ return a === b;
91
92
  };
92
93
 
94
+ /**
95
+ * Compare two tags for equality. Mirrors Rust's `PartialEq for Tag`, which
96
+ * compares by `value` only and ignores the optional `name`.
97
+ */
98
+ export const tagsEqual = (a: Tag, b: Tag): boolean => tagValuesEqual(a.value, b.value);
99
+
93
100
  /**
94
101
  * Get the string representation of a tag.
95
102
  * Internal function used for error messages.
package/src/tags-store.ts CHANGED
@@ -34,6 +34,16 @@ export type SummarizerResult =
34
34
  */
35
35
  export type CborSummarizer = (cbor: Cbor, flat: boolean) => SummarizerResult;
36
36
 
37
+ /**
38
+ * Selects which tag store the diagnostic/hex formatters consult when resolving
39
+ * tag names and summarizers (mirrors Rust's `TagsStoreOpt`):
40
+ *
41
+ * - a concrete {@link TagsStore} to use
42
+ * - `"global"` for the process-wide store
43
+ * - `"none"` to skip name/summary resolution
44
+ */
45
+ export type TagsStoreOpt = TagsStore | "global" | "none";
46
+
37
47
  /**
38
48
  * Interface for tag store operations.
39
49
  */
package/src/walk.ts CHANGED
@@ -89,8 +89,7 @@ export const edgeLabel = (edge: EdgeTypeVariant): string | undefined => {
89
89
  * Can be either a single CBOR value or a key-value pair from a map.
90
90
  */
91
91
  export type WalkElement =
92
- | { type: "single"; cbor: Cbor }
93
- | { type: "keyvalue"; key: Cbor; value: Cbor };
92
+ { type: "single"; cbor: Cbor } | { type: "keyvalue"; key: Cbor; value: Cbor };
94
93
 
95
94
  /**
96
95
  * Helper functions for WalkElement