@bcts/dcbor 1.0.0-beta.2 → 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/dist/index.cjs +1371 -1180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -19
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +77 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +1371 -1180
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +1369 -1181
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/cbor-tagged-decodable.ts +2 -2
- package/src/cbor.ts +20 -9
- package/src/conveniences.ts +31 -13
- package/src/date.ts +67 -7
- package/src/decode.ts +18 -2
- package/src/diag.ts +14 -31
- package/src/dump.ts +3 -1
- package/src/exact.ts +49 -16
- package/src/float.ts +37 -68
- package/src/index.ts +7 -0
- package/src/map.ts +8 -8
- package/src/prelude.ts +2 -2
- package/src/set.ts +36 -46
- package/src/tag.ts +13 -6
- package/src/tags-store.ts +10 -0
- package/src/walk.ts +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bcts/dcbor",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blockchain Commons Deterministic CBOR (dCBOR) for TypeScript",
|
|
6
6
|
"license": "BSD-2-Clause-Patent",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
41
|
"clean": "rm -rf dist",
|
|
42
42
|
"docs": "typedoc",
|
|
43
|
-
"prepublishOnly": "npm run
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
44
|
},
|
|
45
45
|
"keywords": [
|
|
46
46
|
"cbor",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"@bcts/tsconfig": "^0.1.0",
|
|
61
61
|
"@eslint/js": "^10.0.1",
|
|
62
62
|
"@types/collections": "^5.1.5",
|
|
63
|
-
"@types/node": "^
|
|
64
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
65
|
-
"@typescript-eslint/parser": "^8.
|
|
66
|
-
"eslint": "^10.
|
|
63
|
+
"@types/node": "^26.0.1",
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
|
65
|
+
"@typescript-eslint/parser": "^8.62.0",
|
|
66
|
+
"eslint": "^10.6.0",
|
|
67
67
|
"ts-node": "^10.9.2",
|
|
68
68
|
"tsdown": "^0.22.3",
|
|
69
69
|
"typedoc": "^0.28.19",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { type Cbor, MajorType } from "./cbor";
|
|
19
19
|
import type { CborTagged } from "./cbor-tagged";
|
|
20
|
-
import type
|
|
20
|
+
import { tagValuesEqual, type Tag } from "./tag";
|
|
21
21
|
import { CborError } from "./error";
|
|
22
22
|
import { decodeCbor } from "./decode";
|
|
23
23
|
|
|
@@ -160,7 +160,7 @@ export const validateTag = (cbor: Cbor, expectedTags: Tag[]): Tag => {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const tagValue = cbor.tag;
|
|
163
|
-
const matchingTag = expectedTags.find((t) => t.value
|
|
163
|
+
const matchingTag = expectedTags.find((t) => tagValuesEqual(t.value, tagValue));
|
|
164
164
|
if (matchingTag === undefined) {
|
|
165
165
|
// Mirror Rust's `Error::WrongTag(expected, actual)` — produce the
|
|
166
166
|
// structured WrongTag variant rather than a stringly-typed Custom
|
package/src/cbor.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { encodeVarInt } from "./varint";
|
|
|
12
12
|
import { concatBytes } from "./stdlib";
|
|
13
13
|
import { bytesToHex, hexOpt } from "./dump";
|
|
14
14
|
import { hexToBytes } from "./dump";
|
|
15
|
-
import type
|
|
15
|
+
import { tagValuesEqual, type Tag } from "./tag";
|
|
16
16
|
import type { ByteString } from "./byte-string";
|
|
17
17
|
import type { CborDate } from "./date";
|
|
18
18
|
import { diagnosticOpt } from "./diag";
|
|
@@ -104,6 +104,10 @@ export type CborInput =
|
|
|
104
104
|
| CborInput[]
|
|
105
105
|
| Map<unknown, unknown>
|
|
106
106
|
| Set<unknown>
|
|
107
|
+
// Values that convert themselves to CBOR (e.g. untagged CborSet or a
|
|
108
|
+
// tagged-encodable type); `cbor()` dispatches to these at runtime.
|
|
109
|
+
| ToCbor
|
|
110
|
+
| TaggedCborEncodable
|
|
107
111
|
| Record<string, unknown>;
|
|
108
112
|
|
|
109
113
|
export const isCborNumber = (value: unknown): value is CborNumber => {
|
|
@@ -377,12 +381,19 @@ export const cbor = (value: CborInput): Cbor => {
|
|
|
377
381
|
} else if (value == -Infinity) {
|
|
378
382
|
result = { isCbor: true, type: MajorType.Simple, value: { type: "Float", value: -Infinity } };
|
|
379
383
|
} else if (typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
|
|
384
|
+
// A finite, whole-valued `number` beyond the safe-integer range (NaN,
|
|
385
|
+
// ±Infinity, and fractional values are handled above). Like Rust's
|
|
386
|
+
// `From<f64> for CBOR`, reduce any whole value in `[-(2^64), 2^64)` to a
|
|
387
|
+
// CBOR integer; values outside that range (e.g. `1e21`) stay floats.
|
|
388
|
+
// Go through BigInt so we don't lose precision above 2^53.
|
|
389
|
+
const big = BigInt(value);
|
|
390
|
+
if (big >= 0n && big <= 0xffffffffffffffffn) {
|
|
391
|
+
result = { isCbor: true, type: MajorType.Unsigned, value: big };
|
|
392
|
+
} else if (big < 0n && big >= -0x10000000000000000n) {
|
|
393
|
+
result = { isCbor: true, type: MajorType.Negative, value: -big - 1n };
|
|
394
|
+
} else {
|
|
395
|
+
result = { isCbor: true, type: MajorType.Simple, value: { type: "Float", value: value } };
|
|
396
|
+
}
|
|
386
397
|
} else if (
|
|
387
398
|
typeof value === "bigint" &&
|
|
388
399
|
(value > 0xffffffffffffffffn || value < -0x10000000000000000n)
|
|
@@ -817,7 +828,7 @@ export const attachMethods = <T extends Omit<Cbor, keyof CborMethods>>(obj: T):
|
|
|
817
828
|
typeof expectedTag === "object" && "value" in expectedTag
|
|
818
829
|
? expectedTag
|
|
819
830
|
: { value: expectedTag };
|
|
820
|
-
if (this.tag
|
|
831
|
+
if (!tagValuesEqual(this.tag, expected.value)) {
|
|
821
832
|
// Mirror Rust `Error::WrongTag(expected, actual)`.
|
|
822
833
|
throw new CborError({
|
|
823
834
|
type: "WrongTag",
|
|
@@ -837,7 +848,7 @@ export const attachMethods = <T extends Omit<Cbor, keyof CborMethods>>(obj: T):
|
|
|
837
848
|
throw new CborError({ type: "WrongType" });
|
|
838
849
|
}
|
|
839
850
|
const tagValue = this.tag;
|
|
840
|
-
const matchingTag = expectedTags.find((t) => t.value
|
|
851
|
+
const matchingTag = expectedTags.find((t) => tagValuesEqual(t.value, tagValue));
|
|
841
852
|
if (matchingTag === undefined) {
|
|
842
853
|
// Mirror Rust `Error::WrongTag(expected, actual)`.
|
|
843
854
|
throw new CborError({
|
package/src/conveniences.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import type { CborMap } from "./map";
|
|
30
30
|
import { isFloat as isSimpleFloat } from "./simple";
|
|
31
31
|
import { decodeCbor } from "./decode";
|
|
32
|
+
import { ExactF64 } from "./exact";
|
|
32
33
|
import { CborError } from "./error";
|
|
33
34
|
|
|
34
35
|
// ============================================================================
|
|
@@ -358,12 +359,20 @@ export const asBoolean = (cbor: Cbor): boolean | undefined => {
|
|
|
358
359
|
* @returns Float or undefined
|
|
359
360
|
*/
|
|
360
361
|
export const asFloat = (cbor: Cbor): number | undefined => {
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
// Integers coerce to float too, matching Rust's `TryFrom<CBOR> for f64`.
|
|
363
|
+
// dCBOR reduces whole-valued floats to integers (42.0 encodes as 42), so a
|
|
364
|
+
// float-only accessor would wrongly reject them. Returns undefined for
|
|
365
|
+
// non-numeric types and for integers not exactly representable as f64.
|
|
366
|
+
if (cbor.type === MajorType.Unsigned) {
|
|
367
|
+
return ExactF64.exactFromU64(cbor.value);
|
|
363
368
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
369
|
+
if (cbor.type === MajorType.Negative) {
|
|
370
|
+
// cbor.value holds the raw magnitude n; the actual value is -1 - n.
|
|
371
|
+
const f = ExactF64.exactFromU64(cbor.value);
|
|
372
|
+
return f === undefined ? undefined : -1 - f;
|
|
373
|
+
}
|
|
374
|
+
if (cbor.type === MajorType.Simple) {
|
|
375
|
+
return isSimpleFloat(cbor.value) ? cbor.value.value : undefined;
|
|
367
376
|
}
|
|
368
377
|
return undefined;
|
|
369
378
|
};
|
|
@@ -527,11 +536,20 @@ export const expectBoolean = (cbor: Cbor): boolean => {
|
|
|
527
536
|
* @throws {CborError} With type 'WrongType' if cbor is not a float
|
|
528
537
|
*/
|
|
529
538
|
export const expectFloat = (cbor: Cbor): number => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
539
|
+
// Numeric types coerce to float (OutOfRange if an integer isn't exactly
|
|
540
|
+
// representable as f64); anything else is WrongType. Matches Rust's
|
|
541
|
+
// `TryFrom<CBOR> for f64`.
|
|
542
|
+
if (cbor.type === MajorType.Unsigned || cbor.type === MajorType.Negative) {
|
|
543
|
+
const value = asFloat(cbor);
|
|
544
|
+
if (value === undefined) {
|
|
545
|
+
throw new CborError({ type: "OutOfRange" });
|
|
546
|
+
}
|
|
547
|
+
return value;
|
|
533
548
|
}
|
|
534
|
-
|
|
549
|
+
if (cbor.type === MajorType.Simple && isSimpleFloat(cbor.value)) {
|
|
550
|
+
return cbor.value.value;
|
|
551
|
+
}
|
|
552
|
+
throw new CborError({ type: "WrongType" });
|
|
535
553
|
};
|
|
536
554
|
|
|
537
555
|
/**
|
|
@@ -722,7 +740,7 @@ export const hasTag = (cbor: Cbor, tag: number | bigint): boolean => {
|
|
|
722
740
|
if (cbor.type !== MajorType.Tagged) {
|
|
723
741
|
return false;
|
|
724
742
|
}
|
|
725
|
-
return cbor.tag
|
|
743
|
+
return tagValuesEqual(cbor.tag, tag);
|
|
726
744
|
};
|
|
727
745
|
|
|
728
746
|
/**
|
|
@@ -733,7 +751,7 @@ export const hasTag = (cbor: Cbor, tag: number | bigint): boolean => {
|
|
|
733
751
|
* @returns Tagged content or undefined
|
|
734
752
|
*/
|
|
735
753
|
export const getTaggedContent = (cbor: Cbor, tag: number | bigint): Cbor | undefined => {
|
|
736
|
-
if (cbor.type === MajorType.Tagged && cbor.tag
|
|
754
|
+
if (cbor.type === MajorType.Tagged && tagValuesEqual(cbor.tag, tag)) {
|
|
737
755
|
return cbor.value;
|
|
738
756
|
}
|
|
739
757
|
return undefined;
|
|
@@ -754,7 +772,7 @@ export const expectTaggedContent = (cbor: Cbor, tag: number | bigint): Cbor => {
|
|
|
754
772
|
if (cbor.type !== MajorType.Tagged) {
|
|
755
773
|
throw new CborError({ type: "WrongType" });
|
|
756
774
|
}
|
|
757
|
-
if (cbor.tag
|
|
775
|
+
if (!tagValuesEqual(cbor.tag, tag)) {
|
|
758
776
|
throw new CborError({
|
|
759
777
|
type: "WrongTag",
|
|
760
778
|
expected: { value: tag },
|
|
@@ -769,7 +787,7 @@ export const expectTaggedContent = (cbor: Cbor, tag: number | bigint): Cbor => {
|
|
|
769
787
|
// These functions provide the API expected by the envelope package
|
|
770
788
|
// ============================================================================
|
|
771
789
|
|
|
772
|
-
import type
|
|
790
|
+
import { tagValuesEqual, type Tag } from "./tag";
|
|
773
791
|
import { getGlobalTagsStore } from "./tags-store";
|
|
774
792
|
|
|
775
793
|
/**
|
package/src/date.ts
CHANGED
|
@@ -33,6 +33,34 @@ import {
|
|
|
33
33
|
} from "./cbor-tagged";
|
|
34
34
|
import { CborError } from "./error";
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a timestamp (seconds since the Unix epoch) to whole seconds plus a
|
|
38
|
+
* non-negative, sub-second nanosecond part, matching Rust's
|
|
39
|
+
* `Date::from_timestamp` so dates round-trip byte-identically with the reference.
|
|
40
|
+
*
|
|
41
|
+
* The nanosecond part is computed like Rust's `as u32` cast: truncated toward
|
|
42
|
+
* zero and clamped to [0, u32::MAX]. So a negative fraction floors the value
|
|
43
|
+
* (`-1.5` becomes `-1.0`) and sub-nanosecond precision is dropped
|
|
44
|
+
* (`1.0000000005` becomes `1.0`).
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
function normalizeTimestampSeconds(seconds: number): number {
|
|
49
|
+
if (!Number.isFinite(seconds)) {
|
|
50
|
+
// chrono has no representation for a non-finite instant; reject with a
|
|
51
|
+
// typed error.
|
|
52
|
+
throw new CborError({ type: "InvalidDate", message: "non-finite timestamp" });
|
|
53
|
+
}
|
|
54
|
+
const whole = Math.trunc(seconds);
|
|
55
|
+
let nsecs = Math.trunc((seconds - whole) * 1_000_000_000);
|
|
56
|
+
if (nsecs < 0) {
|
|
57
|
+
nsecs = 0;
|
|
58
|
+
} else if (nsecs > 0xffffffff) {
|
|
59
|
+
nsecs = 0xffffffff;
|
|
60
|
+
}
|
|
61
|
+
return whole + nsecs / 1_000_000_000;
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
/**
|
|
37
65
|
* A CBOR-friendly representation of a date and time.
|
|
38
66
|
*
|
|
@@ -192,7 +220,9 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
|
|
|
192
220
|
*/
|
|
193
221
|
static fromTimestamp(secondsSinceUnixEpoch: number): CborDate {
|
|
194
222
|
const instance = new CborDate();
|
|
195
|
-
|
|
223
|
+
// Normalize on construction so the stored value (and thus its encoding,
|
|
224
|
+
// equality, and ordering) matches the reference.
|
|
225
|
+
instance._seconds = normalizeTimestampSeconds(secondsSinceUnixEpoch);
|
|
196
226
|
return instance;
|
|
197
227
|
}
|
|
198
228
|
|
|
@@ -222,12 +252,40 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
|
|
|
222
252
|
* ```
|
|
223
253
|
*/
|
|
224
254
|
static fromString(value: string): CborDate {
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
255
|
+
// Accept only strict RFC-3339 date-times (with seconds and an explicit
|
|
256
|
+
// `Z`/±HH:MM offset) or bare `YYYY-MM-DD` dates (read as UTC midnight),
|
|
257
|
+
// matching Rust's `Date::from_string`. The plain `new Date()` parser is far
|
|
258
|
+
// more lenient (and engine-dependent), so we gate it behind explicit regexes.
|
|
259
|
+
const invalidDate = new CborError({ type: "InvalidDate", message: "Invalid date string" });
|
|
260
|
+
|
|
261
|
+
// RFC-3339 date-time: `YYYY-MM-DDThh:mm:ss[.frac](Z|±hh:mm)`.
|
|
262
|
+
const rfc3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
263
|
+
// Date-only: `YYYY-MM-DD`.
|
|
264
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/;
|
|
265
|
+
|
|
266
|
+
let parsed: Date;
|
|
267
|
+
if (rfc3339.test(value)) {
|
|
268
|
+
parsed = new Date(value);
|
|
269
|
+
} else if (dateOnly.test(value)) {
|
|
270
|
+
// Treat a bare date as UTC midnight (chrono uses 00:00:00 UTC).
|
|
271
|
+
parsed = new Date(`${value}T00:00:00Z`);
|
|
272
|
+
} else {
|
|
273
|
+
throw invalidDate;
|
|
229
274
|
}
|
|
230
|
-
|
|
275
|
+
|
|
276
|
+
// `new Date` rolls impossible dates over (`2023-02-30` becomes Mar 2)
|
|
277
|
+
// rather than failing, so check the `YYYY-MM-DD` portion is a real calendar
|
|
278
|
+
// date. The first 10 chars are always `YYYY-MM-DD` given the regexes above.
|
|
279
|
+
const [y, m, d] = value.slice(0, 10).split("-").map(Number);
|
|
280
|
+
const probe = new Date(Date.UTC(y, m - 1, d));
|
|
281
|
+
const calendarValid =
|
|
282
|
+
probe.getUTCFullYear() === y && probe.getUTCMonth() === m - 1 && probe.getUTCDate() === d;
|
|
283
|
+
|
|
284
|
+
// ...and reject any residual unparseable input (e.g. an out-of-range time).
|
|
285
|
+
if (!calendarValid || isNaN(parsed.getTime())) {
|
|
286
|
+
throw invalidDate;
|
|
287
|
+
}
|
|
288
|
+
return CborDate.fromDatetime(parsed);
|
|
231
289
|
}
|
|
232
290
|
|
|
233
291
|
/**
|
|
@@ -439,7 +497,9 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
|
|
|
439
497
|
throw new CborError({ type: "WrongType" });
|
|
440
498
|
}
|
|
441
499
|
|
|
442
|
-
|
|
500
|
+
// Normalize the decoded value so it re-encodes to the same bytes as the
|
|
501
|
+
// reference (e.g. a tag-1 float of -1.5 decodes and re-encodes as integer -1).
|
|
502
|
+
this._seconds = normalizeTimestampSeconds(timestamp);
|
|
443
503
|
return this;
|
|
444
504
|
}
|
|
445
505
|
|
package/src/decode.ts
CHANGED
|
@@ -40,7 +40,12 @@ function at(data: DataView, index: number): number {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function from(data: DataView, index: number): DataView {
|
|
43
|
-
|
|
43
|
+
// Bound the sub-view to the remaining logical bytes; without a length it
|
|
44
|
+
// would span the whole backing ArrayBuffer, letting a nested decode read
|
|
45
|
+
// past the input's end when the input is itself a slice of a larger buffer.
|
|
46
|
+
// This matches Rust's `&data[index..]`, so an over-long inner item fails as
|
|
47
|
+
// an Underrun at its source instead of only at the top-level length check.
|
|
48
|
+
return new DataView(data.buffer, data.byteOffset + index, data.byteLength - index);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
function range(data: DataView, start: number, end: number): DataView {
|
|
@@ -165,7 +170,18 @@ function decodeCborInternal(data: DataView): { cbor: Cbor; len: number } {
|
|
|
165
170
|
throw new CborError({ type: "OutOfRange" });
|
|
166
171
|
}
|
|
167
172
|
const textBuf = parseBytes(from(data, varIntLen), textLen);
|
|
168
|
-
|
|
173
|
+
// dCBOR text strings must be valid UTF-8 (RFC 8949). Use a fatal decoder
|
|
174
|
+
// so invalid bytes throw rather than getting replaced with U+FFFD and
|
|
175
|
+
// silently accepted, matching the Rust reference's InvalidUtf8 rejection.
|
|
176
|
+
let text: string;
|
|
177
|
+
try {
|
|
178
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(textBuf);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
throw new CborError({
|
|
181
|
+
type: "InvalidUtf8",
|
|
182
|
+
message: e instanceof Error ? e.message : String(e),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
169
185
|
// dCBOR requires all text strings to be in Unicode Normalization Form C (NFC)
|
|
170
186
|
// Reject any strings that are not already in NFC form
|
|
171
187
|
if (text.normalize("NFC") !== text) {
|
package/src/diag.ts
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { type Cbor, MajorType, type Simple } from "./cbor";
|
|
18
18
|
import { bytesToHex } from "./dump";
|
|
19
|
+
import { errorToString } from "./error";
|
|
20
|
+
import { floatDisplayString } from "./float";
|
|
19
21
|
import type { CborMap } from "./map";
|
|
20
|
-
import { getGlobalTagsStore, type TagsStore } from "./tags-store";
|
|
22
|
+
import { getGlobalTagsStore, type TagsStore, type TagsStoreOpt } from "./tags-store";
|
|
21
23
|
import type { Tag } from "./tag";
|
|
22
24
|
import type { WalkElement } from "./walk";
|
|
23
25
|
import { flanked } from "./string-util";
|
|
@@ -63,7 +65,7 @@ export interface DiagFormatOpts {
|
|
|
63
65
|
*
|
|
64
66
|
* @default 'global'
|
|
65
67
|
*/
|
|
66
|
-
tags?:
|
|
68
|
+
tags?: TagsStoreOpt;
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* Current indentation level (internal use for recursion).
|
|
@@ -412,13 +414,9 @@ function item_tagged(tag: number | bigint, content: Cbor, opts: DiagFormatOpts):
|
|
|
412
414
|
if (result.ok) {
|
|
413
415
|
return item(result.value);
|
|
414
416
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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}>`);
|
|
417
|
+
// Use the shared error formatter so every variant gets its full message,
|
|
418
|
+
// including name-aware tag rendering for WrongTag (matches Rust).
|
|
419
|
+
return item(`<error: ${errorToString(result.error)}>`);
|
|
422
420
|
}
|
|
423
421
|
}
|
|
424
422
|
|
|
@@ -450,12 +448,9 @@ function formatBytes(value: Uint8Array): string {
|
|
|
450
448
|
}
|
|
451
449
|
|
|
452
450
|
function formatText(value: string): string {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
.replace(/\n/g, "\\n")
|
|
457
|
-
.replace(/\r/g, "\\r")
|
|
458
|
-
.replace(/\t/g, "\\t");
|
|
451
|
+
// Match Rust `format_string` (cbor.rs): only the double-quote is escaped;
|
|
452
|
+
// backslash, tab, newline, and carriage return are emitted verbatim.
|
|
453
|
+
const escaped = value.replace(/"/g, '\\"');
|
|
459
454
|
return `"${escaped}"`;
|
|
460
455
|
}
|
|
461
456
|
|
|
@@ -473,26 +468,14 @@ function formatSimple(value: Simple): string {
|
|
|
473
468
|
}
|
|
474
469
|
|
|
475
470
|
/**
|
|
476
|
-
* Format a
|
|
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.
|
|
471
|
+
* Format a CBOR float for diagnostic output. Shared with the hex-dump
|
|
472
|
+
* annotation path; see {@link floatDisplayString}.
|
|
482
473
|
*/
|
|
483
474
|
function formatFloat(value: number): string {
|
|
484
|
-
|
|
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`;
|
|
491
|
-
}
|
|
492
|
-
return str;
|
|
475
|
+
return floatDisplayString(value);
|
|
493
476
|
}
|
|
494
477
|
|
|
495
|
-
function resolveTagsStore(tags?:
|
|
478
|
+
function resolveTagsStore(tags?: TagsStoreOpt): TagsStore | undefined {
|
|
496
479
|
if (tags === "none") return undefined;
|
|
497
480
|
if (tags === "global" || tags === undefined) return getGlobalTagsStore();
|
|
498
481
|
return tags;
|
package/src/dump.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { type Cbor, MajorType, cborData } from "./cbor";
|
|
16
16
|
import { encodeVarInt } from "./varint";
|
|
17
|
+
import { floatDisplayString } from "./float";
|
|
17
18
|
import { flanked, sanitized } from "./string-util";
|
|
18
19
|
import type { TagsStore } from "./tags-store";
|
|
19
20
|
import { getGlobalTagsStore } from "./tags-store";
|
|
@@ -274,7 +275,8 @@ function dumpItems(cbor: Cbor, level: number, opts: HexFormatOpts): DumpItem[] {
|
|
|
274
275
|
} else if (simple.type === "Null") {
|
|
275
276
|
note = "null";
|
|
276
277
|
} else if (simple.type === "Float") {
|
|
277
|
-
|
|
278
|
+
// Use the same float formatting as diagnostic output, not raw JS coercion.
|
|
279
|
+
note = floatDisplayString(simple.value);
|
|
278
280
|
} else {
|
|
279
281
|
note = "simple";
|
|
280
282
|
}
|
package/src/exact.ts
CHANGED
|
@@ -25,6 +25,37 @@ const hasFract = (n: number): boolean => {
|
|
|
25
25
|
return n % 1 !== 0;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const U64_MAX_BIG = 0xffffffffffffffffn;
|
|
29
|
+
const I64_MAX_BIG = 0x7fffffffffffffffn;
|
|
30
|
+
const I64_MIN_BIG = -0x8000000000000000n;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Truncate a float to u64 with Rust's saturating `as u64` semantics: NaN and
|
|
34
|
+
* negatives clamp to 0, values at or above 2^64 clamp to u64::MAX. This lets the
|
|
35
|
+
* exact-float checks below mirror Rust's `(f as u64) == source` round-trip, where
|
|
36
|
+
* u64::MAX rounds to 2^64 in float but saturates back to u64::MAX.
|
|
37
|
+
*/
|
|
38
|
+
const saturateFloatToU64 = (f: number): bigint => {
|
|
39
|
+
if (Number.isNaN(f)) return 0n;
|
|
40
|
+
const t = Math.trunc(f);
|
|
41
|
+
if (t <= 0) return 0n;
|
|
42
|
+
const big = BigInt(t);
|
|
43
|
+
return big > U64_MAX_BIG ? U64_MAX_BIG : big;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Truncate a float to i64 with Rust's saturating `as i64` semantics: NaN clamps
|
|
48
|
+
* to 0, values clamp to i64::MAX or i64::MIN at the bounds.
|
|
49
|
+
*/
|
|
50
|
+
const saturateFloatToI64 = (f: number): bigint => {
|
|
51
|
+
if (Number.isNaN(f)) return 0n;
|
|
52
|
+
const t = Math.trunc(f);
|
|
53
|
+
const big = BigInt(t);
|
|
54
|
+
if (big > I64_MAX_BIG) return I64_MAX_BIG;
|
|
55
|
+
if (big < I64_MIN_BIG) return I64_MIN_BIG;
|
|
56
|
+
return big;
|
|
57
|
+
};
|
|
58
|
+
|
|
28
59
|
/**
|
|
29
60
|
* Exact conversions for i16 (-32768 to 32767).
|
|
30
61
|
*/
|
|
@@ -529,19 +560,18 @@ export class ExactF32 {
|
|
|
529
560
|
}
|
|
530
561
|
|
|
531
562
|
static exactFromU64(source: number | bigint): number | undefined {
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return
|
|
563
|
+
const srcBig = typeof source === "bigint" ? source : BigInt(source);
|
|
564
|
+
const f = binary32ToNumber(numberToBinary32(Number(srcBig)));
|
|
565
|
+
if (!Number.isFinite(f)) return undefined;
|
|
566
|
+
// Saturating round-trip so u64::MAX (which rounds up to 2^64 here) still matches.
|
|
567
|
+
return saturateFloatToU64(f) === srcBig ? f : undefined;
|
|
537
568
|
}
|
|
538
569
|
|
|
539
570
|
static exactFromI64(source: number | bigint): number | undefined {
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
return roundTrip === source ? f : undefined;
|
|
571
|
+
const srcBig = typeof source === "bigint" ? source : BigInt(source);
|
|
572
|
+
const f = binary32ToNumber(numberToBinary32(Number(srcBig)));
|
|
573
|
+
if (!Number.isFinite(f)) return undefined;
|
|
574
|
+
return saturateFloatToI64(f) === srcBig ? f : undefined;
|
|
545
575
|
}
|
|
546
576
|
|
|
547
577
|
static exactFromU128(source: bigint): number | undefined {
|
|
@@ -594,15 +624,18 @@ export class ExactF64 {
|
|
|
594
624
|
}
|
|
595
625
|
|
|
596
626
|
static exactFromU64(source: number | bigint): number | undefined {
|
|
597
|
-
const
|
|
598
|
-
const
|
|
599
|
-
|
|
627
|
+
const srcBig = typeof source === "bigint" ? source : BigInt(source);
|
|
628
|
+
const n = Number(srcBig);
|
|
629
|
+
if (!Number.isFinite(n)) return undefined;
|
|
630
|
+
// Saturating round-trip so u64::MAX (which rounds up to 2^64 here) still matches.
|
|
631
|
+
return saturateFloatToU64(n) === srcBig ? n : undefined;
|
|
600
632
|
}
|
|
601
633
|
|
|
602
634
|
static exactFromI64(source: number | bigint): number | undefined {
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
635
|
+
const srcBig = typeof source === "bigint" ? source : BigInt(source);
|
|
636
|
+
const n = Number(srcBig);
|
|
637
|
+
if (!Number.isFinite(n)) return undefined;
|
|
638
|
+
return saturateFloatToI64(n) === srcBig ? n : undefined;
|
|
606
639
|
}
|
|
607
640
|
|
|
608
641
|
static exactFromU128(source: bigint): number | undefined {
|