@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/cbor.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  import { CborMap } from "./map";
2
8
  import type { Simple } from "./simple";
3
9
  import { simpleCborData, isFloat as isSimpleFloat } from "./simple";
@@ -11,8 +17,7 @@ import type { ByteString } from "./byte-string";
11
17
  import type { CborDate } from "./date";
12
18
  import { diagnosticOpt } from "./diag";
13
19
  import { decodeCbor } from "./decode";
14
- import type { TagsStore } from "./tags-store";
15
- import { getGlobalTagsStore } from "./tags-store";
20
+ import { getGlobalTagsStore, type TagsStore } from "./tags-store";
16
21
  import type { Visitor } from "./walk";
17
22
  import { walk } from "./walk";
18
23
  import { CborError } from "./error";
@@ -250,7 +255,7 @@ export interface CborMethods {
250
255
  * @param initialState - Initial state for the visitor
251
256
  * @param visitor - Visitor function called for each element
252
257
  */
253
- walk<State>(initialState: State, visitor: Visitor<State>): State;
258
+ walk<State>(initialState: State, visitor: Visitor<State>): void;
254
259
  /**
255
260
  * Validate that value has one of the expected tags.
256
261
  * @param expectedTags - Array of expected tag values
@@ -290,6 +295,40 @@ export interface TaggedCborEncodable {
290
295
  /**
291
296
  * Type guard to check if value has taggedCbor method.
292
297
  */
298
+ /**
299
+ * Resolve a numeric/bigint tag value to a `Tag` object, looking up the
300
+ * canonical name from the global tags store (matches Rust's
301
+ * `try_into_tagged_value` returning the stored `Tag`). Falls back to a
302
+ * name-less `{ value }` if no name is registered — never synthesizes a
303
+ * placeholder `tag-${value}` string.
304
+ */
305
+ const resolveTag = (value: number | bigint): Tag => {
306
+ const stored = getGlobalTagsStore().tagForValue(value);
307
+ if (stored !== undefined) return stored;
308
+ return { value };
309
+ };
310
+
311
+ /**
312
+ * Structural CBOR value equality.
313
+ *
314
+ * Mirrors Rust's `derive(PartialEq) for CBOR`. dCBOR encoding is
315
+ * deterministic, so two CBOR values are equal iff they encode to the
316
+ * same byte sequence — this is the simplest correct comparator.
317
+ *
318
+ * Use this rather than `===` (which compares JS object references) when
319
+ * you need value equality across two `Cbor` instances built independently.
320
+ */
321
+ export const cborEquals = (a: Cbor, b: Cbor): boolean => {
322
+ if (a === b) return true;
323
+ const aBytes = a.toData();
324
+ const bBytes = b.toData();
325
+ if (aBytes.length !== bBytes.length) return false;
326
+ for (let i = 0; i < aBytes.length; i++) {
327
+ if (aBytes[i] !== bBytes[i]) return false;
328
+ }
329
+ return true;
330
+ };
331
+
293
332
  const hasTaggedCbor = (value: unknown): value is TaggedCborEncodable => {
294
333
  return (
295
334
  typeof value === "object" &&
@@ -337,6 +376,20 @@ export const cbor = (value: CborInput): Cbor => {
337
376
  result = { isCbor: true, type: MajorType.Simple, value: { type: "Float", value: Infinity } };
338
377
  } else if (value == -Infinity) {
339
378
  result = { isCbor: true, type: MajorType.Simple, value: { type: "Float", value: -Infinity } };
379
+ } else if (typeof value === "number" && !Number.isSafeInteger(value)) {
380
+ // Mirrors Rust `f64::exact_from_f64`: any finite, integer-valued
381
+ // float that exceeds the safe-integer range (so adjacent integers
382
+ // can't be distinguished, e.g. `1e21`) cannot be encoded as a
383
+ // canonical integer. Route to Float instead — Rust's `From<f64>
384
+ // for CBOR` does the same fallback.
385
+ result = { isCbor: true, type: MajorType.Simple, value: { type: "Float", value: value } };
386
+ } else if (
387
+ typeof value === "bigint" &&
388
+ (value > 0xffffffffffffffffn || value < -0x10000000000000000n)
389
+ ) {
390
+ // bigint outside the [-(2^64), 2^64 - 1] CBOR-encodable integer
391
+ // range. Surface immediately rather than crashing in encodeVarInt.
392
+ throw new CborError({ type: "OutOfRange" });
340
393
  } else if (value < 0) {
341
394
  // Store the magnitude to encode, matching Rust's representation
342
395
  // For a negative value n, CBOR encodes it as -1-n, so we store -n-1
@@ -642,8 +695,7 @@ export const attachMethods = <T extends Omit<Cbor, keyof CborMethods>>(obj: T):
642
695
  if (this.type !== MajorType.Tagged) {
643
696
  return undefined;
644
697
  }
645
- const tag: Tag = { value: this.tag, name: `tag-${this.tag}` };
646
- return [tag, this.value];
698
+ return [resolveTag(this.tag), this.value];
647
699
  },
648
700
  asBool(this: Cbor): boolean | undefined {
649
701
  if (this.type !== MajorType.Simple) return undefined;
@@ -720,8 +772,7 @@ export const attachMethods = <T extends Omit<Cbor, keyof CborMethods>>(obj: T):
720
772
  `Cannot convert CBOR to Tagged: expected Tagged type, got ${getMajorTypeName(this.type)}`,
721
773
  );
722
774
  }
723
- const tag: Tag = { value: this.tag, name: `tag-${this.tag}` };
724
- return [tag, this.value];
775
+ return [resolveTag(this.tag), this.value];
725
776
  },
726
777
  toBool(this: Cbor): boolean {
727
778
  const result = this.asBool();
@@ -762,33 +813,37 @@ export const attachMethods = <T extends Omit<Cbor, keyof CborMethods>>(obj: T):
762
813
  if (this.type !== MajorType.Tagged) {
763
814
  throw new CborError({ type: "WrongType" });
764
815
  }
765
- const expectedValue =
766
- typeof expectedTag === "object" && "value" in expectedTag ? expectedTag.value : expectedTag;
767
- if (this.tag !== expectedValue) {
816
+ const expected: Tag =
817
+ typeof expectedTag === "object" && "value" in expectedTag
818
+ ? expectedTag
819
+ : { value: expectedTag };
820
+ if (this.tag !== expected.value) {
821
+ // Mirror Rust `Error::WrongTag(expected, actual)`.
768
822
  throw new CborError({
769
- type: "Custom",
770
- message: `Wrong tag: expected ${expectedValue}, got ${this.tag}`,
823
+ type: "WrongTag",
824
+ expected,
825
+ actual: { value: this.tag },
771
826
  });
772
827
  }
773
828
  return this.value;
774
829
  },
775
830
 
776
831
  // Advanced operations
777
- walk<State>(this: Cbor, initialState: State, visitor: Visitor<State>): State {
778
- return walk(this, initialState, visitor);
832
+ walk<State>(this: Cbor, initialState: State, visitor: Visitor<State>): void {
833
+ walk(this, initialState, visitor);
779
834
  },
780
835
  validateTag(this: Cbor, expectedTags: Tag[]): Tag {
781
836
  if (this.type !== MajorType.Tagged) {
782
837
  throw new CborError({ type: "WrongType" });
783
838
  }
784
- const expectedValues = expectedTags.map((t) => t.value);
785
839
  const tagValue = this.tag;
786
840
  const matchingTag = expectedTags.find((t) => t.value === tagValue);
787
841
  if (matchingTag === undefined) {
788
- const expectedStr = expectedValues.join(" or ");
842
+ // Mirror Rust `Error::WrongTag(expected, actual)`.
789
843
  throw new CborError({
790
- type: "Custom",
791
- message: `Wrong tag: expected ${expectedStr}, got ${tagValue}`,
844
+ type: "WrongTag",
845
+ expected: expectedTags[0],
846
+ actual: { value: tagValue },
792
847
  });
793
848
  }
794
849
  return matchingTag;
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Convenience utilities for working with CBOR values.
3
7
  *
4
8
  * Provides type-safe helpers for checking types, extracting values,
@@ -738,17 +742,26 @@ export const getTaggedContent = (cbor: Cbor, tag: number | bigint): Cbor | undef
738
742
  /**
739
743
  * Extract content if has specific tag, throwing if not.
740
744
  *
745
+ * Mirrors Rust `try_into_expected_tagged_value`. Throws `{ type: "WrongType" }`
746
+ * if `cbor` is not tagged at all, otherwise `{ type: "WrongTag", expected,
747
+ * actual }` if the tag doesn't match.
748
+ *
741
749
  * @param cbor - CBOR value
742
750
  * @param tag - Expected tag value
743
751
  * @returns Tagged content
744
- * @throws {CborError} With type 'WrongType' if cbor is not tagged with the expected tag
745
752
  */
746
753
  export const expectTaggedContent = (cbor: Cbor, tag: number | bigint): Cbor => {
747
- const content = getTaggedContent(cbor, tag);
748
- if (content === undefined) {
754
+ if (cbor.type !== MajorType.Tagged) {
749
755
  throw new CborError({ type: "WrongType" });
750
756
  }
751
- return content;
757
+ if (cbor.tag !== tag) {
758
+ throw new CborError({
759
+ type: "WrongTag",
760
+ expected: { value: tag },
761
+ actual: { value: cbor.tag },
762
+ });
763
+ }
764
+ return cbor.value;
752
765
  };
753
766
 
754
767
  // ============================================================================
@@ -757,6 +770,7 @@ export const expectTaggedContent = (cbor: Cbor, tag: number | bigint): Cbor => {
757
770
  // ============================================================================
758
771
 
759
772
  import type { Tag } from "./tag";
773
+ import { getGlobalTagsStore } from "./tags-store";
760
774
 
761
775
  /**
762
776
  * Extract tagged value as tuple [Tag, Cbor] if CBOR is tagged.
@@ -769,7 +783,11 @@ export const asTaggedValue = (cbor: Cbor): [Tag, Cbor] | undefined => {
769
783
  if (cbor.type !== MajorType.Tagged) {
770
784
  return undefined;
771
785
  }
772
- const tag: Tag = { value: cbor.tag, name: `tag-${cbor.tag}` };
786
+ // Mirrors Rust `try_into_tagged_value` which returns the stored `Tag`.
787
+ // Resolve the canonical name (if any) via the global tags store rather
788
+ // than synthesizing a `tag-${value}` placeholder.
789
+ const resolved = getGlobalTagsStore().tagForValue(cbor.tag);
790
+ const tag: Tag = resolved ?? { value: cbor.tag };
773
791
  return [tag, cbor.value];
774
792
  };
775
793
 
package/src/date.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
  * Date/time support for CBOR with tag(1) encoding.
3
7
  *
4
8
  * A CBOR-friendly representation of a date and time.
@@ -67,7 +71,20 @@ import { CborError } from "./error";
67
71
  * ```
68
72
  */
69
73
  export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDecodable<CborDate> {
70
- #datetime: Date;
74
+ /**
75
+ * Canonical timestamp in seconds since the Unix epoch as a JS `number`
76
+ * (`f64`). dCBOR encodes Date (tag 1) as a numeric value in seconds, so
77
+ * keeping `_seconds` as the source of truth avoids the millisecond-only
78
+ * round-trip precision loss that going through a JS `Date` instance
79
+ * introduced in earlier versions of this port.
80
+ *
81
+ * f64 still bounds the achievable precision (~16 decimal digits, so
82
+ * roughly microseconds for current epoch values); Rust's
83
+ * `chrono::DateTime<Utc>` retains nanoseconds in memory but encodes
84
+ * to the same f64 over the wire, so the *encode/decode round-trip* is
85
+ * byte-identical.
86
+ */
87
+ private _seconds: number;
71
88
 
72
89
  /**
73
90
  * Creates a new `CborDate` from the given JavaScript `Date`.
@@ -87,7 +104,7 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
87
104
  */
88
105
  static fromDatetime(dateTime: Date): CborDate {
89
106
  const instance = new CborDate();
90
- instance.#datetime = new Date(dateTime);
107
+ instance._seconds = dateTime.getTime() / 1000;
91
108
  return instance;
92
109
  }
93
110
 
@@ -174,10 +191,9 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
174
191
  * ```
175
192
  */
176
193
  static fromTimestamp(secondsSinceUnixEpoch: number): CborDate {
177
- const wholeSecondsSinceUnixEpoch = Math.trunc(secondsSinceUnixEpoch);
178
- const fractionalSeconds = secondsSinceUnixEpoch - wholeSecondsSinceUnixEpoch;
179
- const milliseconds = wholeSecondsSinceUnixEpoch * 1000 + fractionalSeconds * 1000;
180
- return CborDate.fromDatetime(new Date(milliseconds));
194
+ const instance = new CborDate();
195
+ instance._seconds = secondsSinceUnixEpoch;
196
+ return instance;
181
197
  }
182
198
 
183
199
  /**
@@ -209,7 +225,7 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
209
225
  // Try parsing as ISO 8601 date string
210
226
  const dt = new Date(value);
211
227
  if (isNaN(dt.getTime())) {
212
- throw new CborError({ type: "InvalidDate", message: `Invalid date string: ${value}` });
228
+ throw new CborError({ type: "InvalidDate", message: "Invalid date string" });
213
229
  }
214
230
  return CborDate.fromDatetime(dt);
215
231
  }
@@ -265,7 +281,7 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
265
281
  * ```
266
282
  */
267
283
  datetime(): Date {
268
- return new Date(this.#datetime);
284
+ return new Date(this._seconds * 1000);
269
285
  }
270
286
 
271
287
  /**
@@ -285,9 +301,7 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
285
301
  * ```
286
302
  */
287
303
  timestamp(): number {
288
- const wholeSecondsSinceUnixEpoch = Math.trunc(this.#datetime.getTime() / 1000);
289
- const msecs = this.#datetime.getTime() % 1000;
290
- return wholeSecondsSinceUnixEpoch + msecs / 1000.0;
304
+ return this._seconds;
291
305
  }
292
306
 
293
307
  /**
@@ -415,22 +429,17 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
415
429
  if (cbor.value.type === "Float") {
416
430
  timestamp = cbor.value.value;
417
431
  } else {
418
- throw new CborError({
419
- type: "Custom",
420
- message: "Invalid date CBOR: expected numeric value",
421
- });
432
+ // Mirrors Rust `f64::try_from(CBOR)` returning `Error::WrongType`
433
+ // for non-numeric / non-Float Simple values.
434
+ throw new CborError({ type: "WrongType" });
422
435
  }
423
436
  break;
424
437
 
425
438
  default:
426
- throw new CborError({
427
- type: "Custom",
428
- message: "Invalid date CBOR: expected numeric value",
429
- });
439
+ throw new CborError({ type: "WrongType" });
430
440
  }
431
441
 
432
- const date = CborDate.fromTimestamp(timestamp);
433
- this.#datetime = date.#datetime;
442
+ this._seconds = timestamp;
434
443
  return this;
435
444
  }
436
445
 
@@ -495,7 +504,7 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
495
504
  * ```
496
505
  */
497
506
  toString(): string {
498
- const dt = this.#datetime;
507
+ const dt = new Date(this._seconds * 1000);
499
508
  // Check only hours, minutes, and seconds (not milliseconds) to match Rust behavior
500
509
  const hasTime = dt.getUTCHours() !== 0 || dt.getUTCMinutes() !== 0 || dt.getUTCSeconds() !== 0;
501
510
 
@@ -521,7 +530,7 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
521
530
  * @returns true if dates represent the same moment in time
522
531
  */
523
532
  equals(other: CborDate): boolean {
524
- return this.#datetime.getTime() === other.#datetime.getTime();
533
+ return this._seconds === other._seconds;
525
534
  }
526
535
 
527
536
  /**
@@ -531,10 +540,8 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
531
540
  * @returns -1 if this < other, 0 if equal, 1 if this > other
532
541
  */
533
542
  compare(other: CborDate): number {
534
- const thisTime = this.#datetime.getTime();
535
- const otherTime = other.#datetime.getTime();
536
- if (thisTime < otherTime) return -1;
537
- if (thisTime > otherTime) return 1;
543
+ if (this._seconds < other._seconds) return -1;
544
+ if (this._seconds > other._seconds) return 1;
538
545
  return 0;
539
546
  }
540
547
 
@@ -548,6 +555,6 @@ export class CborDate implements CborTagged, CborTaggedEncodable, CborTaggedDeco
548
555
  }
549
556
 
550
557
  private constructor() {
551
- this.#datetime = new Date();
558
+ this._seconds = Date.now() / 1000;
552
559
  }
553
560
  }
package/src/decode.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  import {
2
8
  type Cbor,
3
9
  type CborNumber,
@@ -201,6 +207,13 @@ function decodeCborInternal(data: DataView): { cbor: Cbor; len: number } {
201
207
  switch (varIntLen) {
202
208
  case 3: {
203
209
  const f = binary16ToNumber(new Uint8Array(data.buffer, data.byteOffset + 1, 2));
210
+ // dCBOR canonical-encoding check via re-encode-and-compare. JS's
211
+ // `Number` type does not preserve NaN payload bits — every NaN
212
+ // collapses to the same value — so we cannot port Rust's
213
+ // `validate_canonical_f16(n)`'s bit-level check directly.
214
+ // Re-encoding round-trips through the canonicalising encoder,
215
+ // catching every non-canonical NaN, ±Infinity, and
216
+ // integer-reducible float. See float.ts for the encoder rules.
204
217
  checkCanonicalEncoding(f, new Uint8Array(data.buffer, data.byteOffset, varIntLen));
205
218
  const cborObj = {
206
219
  isCbor: true,