@bcts/dcbor 1.0.0-alpha.8 → 1.0.0-beta.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/src/diag.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Enhanced diagnostic formatting for CBOR values.
3
7
  *
4
8
  * Provides multiple formatting options including
@@ -16,6 +20,7 @@ import type { CborMap } from "./map";
16
20
  import { getGlobalTagsStore, type TagsStore } from "./tags-store";
17
21
  import type { Tag } from "./tag";
18
22
  import type { WalkElement } from "./walk";
23
+ import { flanked } from "./string-util";
19
24
 
20
25
  /**
21
26
  * Options for diagnostic formatting.
@@ -47,9 +52,14 @@ export interface DiagFormatOpts {
47
52
 
48
53
  /**
49
54
  * Tag store to use for tag name resolution.
50
- * - TagsStore instance: Use specific store
51
- * - 'global': Use global singleton store
52
- * - 'none': Don't use any store (show tag numbers)
55
+ *
56
+ * Mirrors Rust's `TagsStoreOpt<'a>` enum (`Custom(&'a dyn TagsStoreTrait)`,
57
+ * `Global`, `None`). The TS port models the same three-way choice as a
58
+ * string-literal union — semantically equivalent, just stringly-typed.
59
+ *
60
+ * - `TagsStore` instance: use this specific store (Rust `Custom`)
61
+ * - `'global'`: use global singleton store (Rust `Global`)
62
+ * - `'none'`: don't resolve names; print bare tag numbers (Rust `None`)
53
63
  *
54
64
  * @default 'global'
55
65
  */
@@ -100,7 +110,9 @@ const DEFAULT_OPTS = {
100
110
  */
101
111
  export function diagnosticOpt(cbor: Cbor, opts?: DiagFormatOpts): string {
102
112
  const options = { ...DEFAULT_OPTS, ...opts };
103
- return formatDiagnostic(cbor, options);
113
+ // `summarize` implies `flat` per Rust `DiagFormatOpts::summarize`.
114
+ if (options.summarize === true) options.flat = true;
115
+ return diagFormat(diagItem(cbor, options), options);
104
116
  }
105
117
 
106
118
  /**
@@ -167,15 +179,14 @@ export function diagnosticFlat(input: Cbor | WalkElement): string {
167
179
  "type" in input &&
168
180
  (input.type === "single" || input.type === "keyvalue")
169
181
  ) {
170
- const element = input as WalkElement;
171
- if (element.type === "single") {
172
- return diagnosticOpt(element.cbor, { flat: true });
182
+ if (input.type === "single") {
183
+ return diagnosticOpt(input.cbor, { flat: true });
173
184
  } else {
174
- return `${diagnosticOpt(element.key, { flat: true })}: ${diagnosticOpt(element.value, { flat: true })}`;
185
+ return `${diagnosticOpt(input.key, { flat: true })}: ${diagnosticOpt(input.value, { flat: true })}`;
175
186
  }
176
187
  }
177
188
  // Otherwise treat as Cbor
178
- return diagnosticOpt(input as Cbor, { flat: true });
189
+ return diagnosticOpt(input, { flat: true });
179
190
  }
180
191
 
181
192
  /**
@@ -199,195 +210,218 @@ export function summary(cbor: Cbor): string {
199
210
  return diagnosticOpt(cbor, { summarize: true, flat: true });
200
211
  }
201
212
 
202
- /**
203
- * Internal recursive formatter.
204
- *
205
- * @internal
206
- */
207
- function formatDiagnostic(cbor: Cbor, opts: DiagFormatOpts): string {
208
- switch (cbor.type) {
209
- case MajorType.Unsigned:
210
- return formatUnsigned(cbor.value);
213
+ // =====================================================================
214
+ // DiagItem AST
215
+ //
216
+ // Mirrors Rust `DiagItem` enum in `bc-dcbor-rust/src/diag.rs`. Building an
217
+ // AST first lets the formatter use Rust-identical multi-line heuristics
218
+ // (`contains_group || total_strings_len > 20 || greatest_strings_len > 20`)
219
+ // rather than ad-hoc thresholds applied at recursion time.
220
+ // =====================================================================
211
221
 
212
- case MajorType.Negative:
213
- return formatNegative(cbor.value);
222
+ type DiagItem = DiagItemNode | DiagItemGroup;
214
223
 
215
- case MajorType.ByteString:
216
- return formatBytes(cbor.value);
224
+ interface DiagItemNode {
225
+ kind: "item";
226
+ value: string;
227
+ }
217
228
 
218
- case MajorType.Text:
219
- return formatText(cbor.value);
229
+ interface DiagItemGroup {
230
+ kind: "group";
231
+ begin: string;
232
+ end: string;
233
+ items: DiagItem[];
234
+ /** True for maps (`{...}`) — items alternate key, value. */
235
+ isPairs: boolean;
236
+ /** Optional comment rendered as ` / comment /` after the line. */
237
+ comment?: string;
238
+ }
220
239
 
221
- case MajorType.Array:
222
- return formatArray(cbor.value, opts);
240
+ const item = (value: string): DiagItemNode => ({ kind: "item", value });
223
241
 
224
- case MajorType.Map:
225
- return formatMap(cbor.value, opts);
242
+ const group = (
243
+ begin: string,
244
+ end: string,
245
+ items: DiagItem[],
246
+ isPairs: boolean,
247
+ comment?: string,
248
+ ): DiagItemGroup => {
249
+ const g: DiagItemGroup = { kind: "group", begin, end, items, isPairs };
250
+ if (comment !== undefined) g.comment = comment;
251
+ return g;
252
+ };
226
253
 
227
- case MajorType.Tagged:
228
- return formatTagged(cbor.tag, cbor.value, opts);
254
+ const isGroup = (i: DiagItem): boolean => i.kind === "group";
229
255
 
230
- case MajorType.Simple:
231
- return formatSimple(cbor.value);
232
- }
233
- }
256
+ const containsGroup = (i: DiagItem): boolean => i.kind === "group" && i.items.some(isGroup);
234
257
 
235
- /**
236
- * Format unsigned integer.
237
- */
238
- function formatUnsigned(value: number | bigint): string {
239
- return String(value);
240
- }
258
+ const totalStringsLen = (i: DiagItem): number =>
259
+ i.kind === "item" ? i.value.length : i.items.reduce((acc, c) => acc + totalStringsLen(c), 0);
260
+
261
+ const greatestStringsLen = (i: DiagItem): number =>
262
+ i.kind === "item"
263
+ ? i.value.length
264
+ : i.items.reduce((acc, c) => Math.max(acc, totalStringsLen(c)), 0);
241
265
 
242
266
  /**
243
- * Format negative integer.
267
+ * Mirrors Rust `DiagItem::joined`: alternates between `pairSeparator`
268
+ * (after even-indexed items — keys) and `itemSeparator` (after odd-indexed
269
+ * items — values). Falls back to `itemSeparator` for non-pair groups.
244
270
  */
245
- function formatNegative(value: number | bigint): string {
246
- // Value is stored as magnitude, convert to actual negative value for display
247
- if (typeof value === "bigint") {
248
- return String(-value - 1n);
249
- } else {
250
- return String(-value - 1);
271
+ function joined(elements: string[], itemSeparator: string, pairSeparator?: string): string {
272
+ const sep = pairSeparator ?? itemSeparator;
273
+ let result = "";
274
+ const len = elements.length;
275
+ for (let i = 0; i < len; i++) {
276
+ result += elements[i];
277
+ if (i !== len - 1) {
278
+ result += (i & 1) !== 0 ? itemSeparator : sep;
279
+ }
251
280
  }
281
+ return result;
252
282
  }
253
283
 
254
- /**
255
- * Format byte string.
256
- */
257
- function formatBytes(value: Uint8Array): string {
258
- return `h'${bytesToHex(value)}'`;
259
- }
284
+ const diagFormat = (i: DiagItem, opts: DiagFormatOpts): string => diagFormatOpt(i, 0, "", opts);
260
285
 
261
- /**
262
- * Format text string.
263
- */
264
- function formatText(value: string): string {
265
- // Escape special characters
266
- const escaped = value
267
- .replace(/\\/g, "\\\\")
268
- .replace(/"/g, '\\"')
269
- .replace(/\n/g, "\\n")
270
- .replace(/\r/g, "\\r")
271
- .replace(/\t/g, "\\t");
272
- return `"${escaped}"`;
286
+ function diagFormatOpt(
287
+ i: DiagItem,
288
+ level: number,
289
+ separator: string,
290
+ opts: DiagFormatOpts,
291
+ ): string {
292
+ if (i.kind === "item") {
293
+ return formatLine(level, opts, i.value, separator, undefined);
294
+ }
295
+ if (
296
+ opts.flat !== true &&
297
+ (containsGroup(i) || totalStringsLen(i) > 20 || greatestStringsLen(i) > 20)
298
+ ) {
299
+ return multilineComposition(i, level, separator, opts);
300
+ }
301
+ return singleLineComposition(i, level, separator, opts);
273
302
  }
274
303
 
275
- /**
276
- * Format array.
277
- */
278
- function formatArray(items: readonly Cbor[], opts: DiagFormatOpts): string {
279
- if (items.length === 0) {
280
- return "[]";
304
+ function formatLine(
305
+ level: number,
306
+ opts: DiagFormatOpts,
307
+ string: string,
308
+ separator: string,
309
+ comment: string | undefined,
310
+ ): string {
311
+ const indent = opts.flat === true ? "" : " ".repeat(level * 4);
312
+ const result = `${indent}${string}${separator}`;
313
+ if (comment !== undefined) {
314
+ return `${result} / ${comment} /`;
281
315
  }
316
+ return result;
317
+ }
282
318
 
283
- // Format items first to check their lengths
284
- const formatted = items.map((item) => formatDiagnostic(item, opts));
285
-
286
- // Decide between single-line and multi-line based on complexity
287
- const shouldUseMultiLine =
288
- opts.flat !== true &&
289
- (containsComplexStructure(items) ||
290
- formatted.join(", ").length > 20 ||
291
- formatted.some((s) => s.length > 20));
292
-
293
- if (shouldUseMultiLine) {
294
- // Multi-line formatting
295
- const indent = opts.indent ?? 0;
296
- const indentStr = (opts.indentString ?? " ").repeat(indent);
297
- const itemIndentStr = (opts.indentString ?? " ").repeat(indent + 1);
298
-
299
- const formattedWithIndent = items.map((item) => {
300
- const childOpts = { ...opts, indent: indent + 1 };
301
- const itemStr = formatDiagnostic(item, childOpts);
302
- return `${itemIndentStr}${itemStr}`;
303
- });
304
-
305
- return `[\n${formattedWithIndent.join(",\n")}\n${indentStr}]`;
319
+ function singleLineComposition(
320
+ i: DiagItem,
321
+ level: number,
322
+ separator: string,
323
+ opts: DiagFormatOpts,
324
+ ): string {
325
+ let str: string;
326
+ let comment: string | undefined;
327
+ if (i.kind === "item") {
328
+ str = i.value;
329
+ comment = undefined;
306
330
  } else {
307
- // Single-line formatting
308
- return `[${formatted.join(", ")}]`;
331
+ const components = i.items.map((c) =>
332
+ c.kind === "item" ? c.value : singleLineComposition(c, level + 1, separator, opts),
333
+ );
334
+ const pairSeparator = i.isPairs ? ": " : ", ";
335
+ str = flanked(joined(components, ", ", pairSeparator), i.begin, i.end);
336
+ comment = i.comment;
309
337
  }
338
+ return formatLine(level, opts, str, separator, comment);
310
339
  }
311
340
 
312
- /**
313
- * Check if items contain complex structures (arrays or maps).
314
- */
315
- function containsComplexStructure(items: readonly Cbor[]): boolean {
316
- return items.some((item) => item.type === MajorType.Array || item.type === MajorType.Map);
341
+ function multilineComposition(
342
+ i: DiagItem,
343
+ level: number,
344
+ separator: string,
345
+ opts: DiagFormatOpts,
346
+ ): string {
347
+ if (i.kind === "item") return i.value;
348
+ const lines: string[] = [];
349
+ // Opening line: print `begin` (with comment) at this level, never flat.
350
+ const openOpts: DiagFormatOpts = { ...opts, flat: false };
351
+ lines.push(formatLine(level, openOpts, i.begin, "", i.comment));
352
+ for (let idx = 0; idx < i.items.length; idx++) {
353
+ const sep = idx === i.items.length - 1 ? "" : i.isPairs && (idx & 1) === 0 ? ":" : ",";
354
+ lines.push(diagFormatOpt(i.items[idx], level + 1, sep, opts));
355
+ }
356
+ // Closing line: print `end` at the parent level, with the outer separator.
357
+ lines.push(formatLine(level, opts, i.end, separator, undefined));
358
+ return lines.join("\n");
317
359
  }
318
360
 
319
- /**
320
- * Format map.
321
- */
322
- function formatMap(map: CborMap, opts: DiagFormatOpts): string {
323
- // Extract entries from CborMap or use empty array
324
- const entries = map?.entriesArray ?? [];
361
+ // =====================================================================
362
+ // AST construction (`diag_item` in Rust)
363
+ // =====================================================================
325
364
 
326
- if (entries.length === 0) {
327
- return "{}";
328
- }
329
-
330
- interface FormattedPair {
331
- key: string;
332
- value: string;
365
+ function diagItem(cbor: Cbor, opts: DiagFormatOpts): DiagItem {
366
+ switch (cbor.type) {
367
+ case MajorType.Unsigned:
368
+ return item(formatUnsigned(cbor.value));
369
+ case MajorType.Negative:
370
+ return item(formatNegative(cbor.value));
371
+ case MajorType.ByteString:
372
+ return item(formatBytes(cbor.value));
373
+ case MajorType.Text:
374
+ return item(formatText(cbor.value));
375
+ case MajorType.Array:
376
+ return item_array(cbor.value, opts);
377
+ case MajorType.Map:
378
+ return item_map(cbor.value, opts);
379
+ case MajorType.Tagged:
380
+ return item_tagged(cbor.tag, cbor.value, opts);
381
+ case MajorType.Simple:
382
+ return item(formatSimple(cbor.value));
333
383
  }
384
+ }
334
385
 
335
- // Format each key-value pair
336
- const formattedPairs: FormattedPair[] = entries.map((entry: { key: Cbor; value: Cbor }) => ({
337
- key: formatDiagnostic(entry.key, opts),
338
- value: formatDiagnostic(entry.value, opts),
339
- }));
340
-
341
- // Decide between single-line and multi-line based on complexity
342
- const totalLength = formattedPairs.reduce(
343
- (sum: number, pair: FormattedPair) => sum + pair.key.length + pair.value.length + 2,
344
- 0,
345
- ); // +2 for ": "
386
+ function item_array(items: readonly Cbor[], opts: DiagFormatOpts): DiagItem {
387
+ return group(
388
+ "[",
389
+ "]",
390
+ items.map((it) => diagItem(it, opts)),
391
+ false,
392
+ );
393
+ }
346
394
 
347
- const shouldUseMultiLine =
348
- opts.flat !== true &&
349
- (entries.some(
350
- (e: { key: Cbor; value: Cbor }) =>
351
- e.key.type === MajorType.Array ||
352
- e.key.type === MajorType.Map ||
353
- e.value.type === MajorType.Array ||
354
- e.value.type === MajorType.Map,
355
- ) ||
356
- totalLength > 40 ||
357
- entries.length > 3);
358
-
359
- if (shouldUseMultiLine) {
360
- // Multi-line formatting
361
- const indent = opts.indent ?? 0;
362
- const indentStr = (opts.indentString ?? " ").repeat(indent);
363
- const itemIndentStr = (opts.indentString ?? " ").repeat(indent + 1);
364
-
365
- const formattedEntries = formattedPairs.map((pair: FormattedPair) => {
366
- return `${itemIndentStr}${pair.key}:\n${itemIndentStr}${pair.value}`;
367
- });
368
-
369
- return `{\n${formattedEntries.join(",\n")}\n${indentStr}}`;
370
- } else {
371
- // Single-line formatting
372
- const pairs = formattedPairs.map((pair: FormattedPair) => `${pair.key}: ${pair.value}`);
373
- return `{${pairs.join(", ")}}`;
395
+ function item_map(map: CborMap, opts: DiagFormatOpts): DiagItem {
396
+ const entries = map?.entriesArray ?? [];
397
+ const flatItems: DiagItem[] = [];
398
+ for (const e of entries) {
399
+ flatItems.push(diagItem(e.key, opts));
400
+ flatItems.push(diagItem(e.value, opts));
374
401
  }
402
+ return group("{", "}", flatItems, true);
375
403
  }
376
404
 
377
- /**
378
- * Format tagged value.
379
- */
380
- function formatTagged(tag: number | bigint, content: Cbor, opts: DiagFormatOpts): string {
381
- // Check for summarizer first
405
+ function item_tagged(tag: number | bigint, content: Cbor, opts: DiagFormatOpts): DiagItem {
406
+ // Summarizer path matches Rust's summarization branch.
382
407
  if (opts.summarize === true) {
383
408
  const store = resolveTagsStore(opts.tags);
384
409
  const summarizer = store?.summarizer(tag);
385
410
  if (summarizer !== undefined) {
386
- return summarizer(content, opts.flat ?? false);
411
+ const result = summarizer(content, opts.flat ?? false);
412
+ if (result.ok) {
413
+ return item(result.value);
414
+ }
415
+ const errorMsg =
416
+ result.error.type === "Custom"
417
+ ? result.error.message
418
+ : result.error.type === "WrongTag"
419
+ ? `expected CBOR tag ${result.error.expected.value}, but got ${result.error.actual.value}`
420
+ : result.error.type;
421
+ return item(`<error: ${errorMsg}>`);
387
422
  }
388
423
  }
389
424
 
390
- // Get tag name as comment if annotation is enabled
391
425
  let comment: string | undefined;
392
426
  if (opts.annotate === true) {
393
427
  const store = resolveTagsStore(opts.tags);
@@ -398,25 +432,34 @@ function formatTagged(tag: number | bigint, content: Cbor, opts: DiagFormatOpts)
398
432
  }
399
433
  }
400
434
 
401
- // Always use tag number (not name) in the output
402
- const tagStr = String(tag);
435
+ return group(`${String(tag)}(`, ")", [diagItem(content, opts)], false, comment);
436
+ }
437
+
438
+ // Primitive formatters reused by both single- and multi-line paths.
439
+ function formatUnsigned(value: number | bigint): string {
440
+ return String(value);
441
+ }
442
+
443
+ function formatNegative(value: number | bigint): string {
444
+ if (typeof value === "bigint") return String(-value - 1n);
445
+ return String(-value - 1);
446
+ }
403
447
 
404
- // Format content
405
- const contentStr = formatDiagnostic(content, opts);
448
+ function formatBytes(value: Uint8Array): string {
449
+ return `h'${bytesToHex(value)}'`;
450
+ }
406
451
 
407
- // Add comment if present
408
- const result = `${tagStr}(${contentStr})`;
409
- if (comment !== undefined) {
410
- return `${result} / ${comment} /`;
411
- }
412
- return result;
452
+ function formatText(value: string): string {
453
+ const escaped = value
454
+ .replace(/\\/g, "\\\\")
455
+ .replace(/"/g, '\\"')
456
+ .replace(/\n/g, "\\n")
457
+ .replace(/\r/g, "\\r")
458
+ .replace(/\t/g, "\\t");
459
+ return `"${escaped}"`;
413
460
  }
414
461
 
415
- /**
416
- * Format simple value.
417
- */
418
462
  function formatSimple(value: Simple): string {
419
- // Handle discriminated union
420
463
  switch (value.type) {
421
464
  case "True":
422
465
  return "true";
@@ -430,33 +473,27 @@ function formatSimple(value: Simple): string {
430
473
  }
431
474
 
432
475
  /**
433
- * Format float value.
476
+ * Format a finite CBOR float to match Rust `Simple::format!("{:?}", v)`.
477
+ *
478
+ * - `1.0` → `"1.0"` (Rust Debug). JS `String(1.0)` gives `"1"` so we append `.0`.
479
+ * - `1.5` → `"1.5"`.
480
+ * - `1e100` → `"1e100"` (Rust uses no `+` sign in the exponent). JS uses `1e+100`.
481
+ * - Specials (NaN / ±Infinity) produce the exact Rust strings.
434
482
  */
435
483
  function formatFloat(value: number): string {
436
- if (isNaN(value)) {
437
- return "NaN";
438
- } else if (!isFinite(value)) {
439
- return value > 0 ? "Infinity" : "-Infinity";
440
- } else {
441
- // Show decimal point for clarity, unless already in scientific notation
442
- const str = String(value);
443
- // Scientific notation (contains 'e') or already has decimal point
444
- if (str.includes(".") || str.includes("e")) {
445
- return str;
446
- }
447
- return `${str}.0`;
484
+ if (Number.isNaN(value)) return "NaN";
485
+ if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
486
+ let str = String(value);
487
+ // Strip the JS-only `+` in scientific exponents to match Rust Debug format.
488
+ str = str.replace(/e\+/, "e");
489
+ if (!str.includes(".") && !str.includes("e")) {
490
+ str = `${str}.0`;
448
491
  }
492
+ return str;
449
493
  }
450
494
 
451
- /**
452
- * Resolve tags store from option.
453
- */
454
495
  function resolveTagsStore(tags?: TagsStore | "global" | "none"): TagsStore | undefined {
455
- if (tags === "none") {
456
- return undefined;
457
- } else if (tags === "global" || tags === undefined) {
458
- return getGlobalTagsStore();
459
- } else {
460
- return tags;
461
- }
496
+ if (tags === "none") return undefined;
497
+ if (tags === "global" || tags === undefined) return getGlobalTagsStore();
498
+ return tags;
462
499
  }
package/src/dump.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Hex dump utilities for CBOR data.
3
7
  *
4
8
  * Affordances for viewing the encoded binary representation of CBOR as hexadecimal.
@@ -37,6 +41,13 @@ export const bytesToHex = (bytes: Uint8Array): string => {
37
41
 
38
42
  /**
39
43
  * Convert hex string to bytes.
44
+ *
45
+ * **Whitespace tolerance.** This implementation strips ASCII whitespace
46
+ * before decoding so users can paste annotated hex dumps directly. Rust's
47
+ * `hex::decode` is strict and panics on any whitespace. This is a
48
+ * deliberate TS-side ergonomic divergence — callers who need strict
49
+ * Rust-compatible parsing should validate the input first (e.g.
50
+ * `if (/\s/.test(s)) throw …`).
40
51
  */
41
52
  export const hexToBytes = (hexString: string): Uint8Array => {
42
53
  const hex = hexString.replace(/\s/g, "");
@@ -223,10 +234,10 @@ function dumpItems(cbor: Cbor, level: number, opts: HexFormatOpts): DumpItem[] {
223
234
  if (tagValue === undefined) {
224
235
  throw new CborError({ type: "Custom", message: "Tagged CBOR value must have a tag" });
225
236
  }
226
- const header = encodeVarInt(
227
- typeof tagValue === "bigint" ? Number(tagValue) : tagValue,
228
- MajorType.Tagged,
229
- );
237
+ // Pass the tag value through directly — `encodeVarInt` accepts both
238
+ // `number` and `bigint`. The previous `Number(bigint)` cast was lossy
239
+ // for tags > MAX_SAFE_INTEGER.
240
+ const header = encodeVarInt(tagValue, MajorType.Tagged);
230
241
  const firstByte = header[0];
231
242
  if (firstByte === undefined) {
232
243
  throw new CborError({ type: "Custom", message: "Invalid varint encoding" });
@@ -235,9 +246,9 @@ function dumpItems(cbor: Cbor, level: number, opts: HexFormatOpts): DumpItem[] {
235
246
 
236
247
  const noteComponents: string[] = [`tag(${tagValue})`];
237
248
 
238
- // Add tag name if tags store is provided
239
- const numericTagValue = typeof tagValue === "bigint" ? Number(tagValue) : tagValue;
240
- const tag = createTag(numericTagValue);
249
+ // Add tag name if tags store is provided. `createTag` accepts the
250
+ // raw `tagValue` (number | bigint); no `Number()` coercion needed.
251
+ const tag = createTag(tagValue);
241
252
  const tagName = opts.tagsStore?.assignedNameForTag(tag);
242
253
  if (tagName !== undefined) {
243
254
  noteComponents.push(tagName);
package/src/error.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Error types for CBOR encoding and decoding.
3
7
  *
4
8
  * @module error
package/src/exact.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Exact numeric conversions.
3
7
  *
4
8
  * This module is based on the Swift `exactly` initializers.
package/src/float.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Float encoding and conversion utilities for dCBOR.
3
7
  *
4
8
  * # Floating Point Number Support in dCBOR
@@ -128,9 +132,20 @@ export const f64CborData = (value: number): Uint8Array => {
128
132
 
129
133
  /**
130
134
  * Validate canonical encoding for f64.
131
- * Matches Rust's validate_canonical_f64 function.
132
135
  *
133
- * TODO: Check if this is legacy code
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
134
149
  */
135
150
  export const validateCanonicalF64 = (n: number): void => {
136
151
  const f32Bytes = numberToBinary32(n);
@@ -184,9 +199,11 @@ export const f32CborData = (value: number): Uint8Array => {
184
199
 
185
200
  /**
186
201
  * Validate canonical encoding for f32.
187
- * Matches Rust's validate_canonical_f32 function.
188
202
  *
189
- * TODO: Check if this is legacy code
203
+ * @see {@link validateCanonicalF64} same caveat about JS NaN bit
204
+ * preservation. The decoder relies on {@link checkCanonicalEncoding}.
205
+ *
206
+ * @internal
190
207
  */
191
208
  export const validateCanonicalF32 = (n: number): void => {
192
209
  const f16Bytes = numberToBinary16(n);
@@ -233,9 +250,11 @@ export const f16CborData = (value: number): Uint8Array => {
233
250
 
234
251
  /**
235
252
  * Validate canonical encoding for f16.
236
- * Matches Rust's validate_canonical_f16 function.
237
253
  *
238
- * TODO: Check if this is legacy code
254
+ * @see {@link validateCanonicalF64} same caveat about JS NaN bit
255
+ * preservation. The decoder relies on {@link checkCanonicalEncoding}.
256
+ *
257
+ * @internal
239
258
  */
240
259
  export const validateCanonicalF16 = (value: number): void => {
241
260
  const n = value;