@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/LICENSE +3 -2
- package/README.md +1 -1
- package/dist/index.cjs +1941 -1497
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +613 -327
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +613 -327
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +2149 -1754
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +1932 -1511
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -16
- package/src/bignum.ts +347 -0
- package/src/byte-string.ts +21 -17
- package/src/cbor-codable.ts +4 -0
- package/src/cbor-tagged-codable.ts +4 -0
- package/src/cbor-tagged-decodable.ts +33 -5
- package/src/cbor-tagged-encodable.ts +15 -0
- package/src/cbor-tagged.ts +11 -1
- package/src/cbor.ts +73 -18
- package/src/conveniences.ts +23 -5
- package/src/date.ts +35 -28
- package/src/decode.ts +13 -0
- package/src/diag.ts +233 -196
- package/src/dump.ts +18 -7
- package/src/error.ts +4 -0
- package/src/exact.ts +4 -0
- package/src/float.ts +25 -6
- package/src/global.d.ts +4 -0
- package/src/globals.d.ts +5 -0
- package/src/index.ts +65 -13
- package/src/map.ts +27 -23
- package/src/prelude.ts +12 -2
- package/src/set.ts +37 -10
- package/src/simple.ts +15 -2
- package/src/sortable.ts +70 -0
- package/src/stdlib.ts +4 -0
- package/src/string-util.ts +4 -0
- package/src/tag.ts +51 -2
- package/src/tags-store.ts +53 -68
- package/src/tags.ts +68 -6
- package/src/varint.ts +16 -11
- package/src/walk.ts +77 -281
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
return formatNegative(cbor.value);
|
|
222
|
+
type DiagItem = DiagItemNode | DiagItemGroup;
|
|
214
223
|
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
interface DiagItemNode {
|
|
225
|
+
kind: "item";
|
|
226
|
+
value: string;
|
|
227
|
+
}
|
|
217
228
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
return formatArray(cbor.value, opts);
|
|
240
|
+
const item = (value: string): DiagItemNode => ({ kind: "item", value });
|
|
223
241
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
return formatTagged(cbor.tag, cbor.value, opts);
|
|
254
|
+
const isGroup = (i: DiagItem): boolean => i.kind === "group";
|
|
229
255
|
|
|
230
|
-
|
|
231
|
-
return formatSimple(cbor.value);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
256
|
+
const containsGroup = (i: DiagItem): boolean => i.kind === "group" && i.items.some(isGroup);
|
|
234
257
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
*
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
448
|
+
function formatBytes(value: Uint8Array): string {
|
|
449
|
+
return `h'${bytesToHex(value)}'`;
|
|
450
|
+
}
|
|
406
451
|
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
240
|
-
const tag = createTag(
|
|
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
package/src/exact.ts
CHANGED
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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;
|