@bcts/dcbor 1.0.0-alpha.17 → 1.0.0-alpha.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bcts/dcbor",
3
- "version": "1.0.0-alpha.17",
3
+ "version": "1.0.0-alpha.18",
4
4
  "type": "module",
5
5
  "description": "Blockchain Commons Deterministic CBOR (dCBOR) for TypeScript",
6
6
  "license": "BSD-2-Clause-Patent",
@@ -71,9 +71,9 @@
71
71
  "@bcts/tsconfig": "^0.1.0",
72
72
  "@eslint/js": "^9.39.2",
73
73
  "@types/collections": "^5.1.5",
74
- "@types/node": "^25.0.10",
75
- "@typescript-eslint/eslint-plugin": "^8.53.1",
76
- "@typescript-eslint/parser": "^8.53.1",
74
+ "@types/node": "^25.2.0",
75
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
76
+ "@typescript-eslint/parser": "^8.54.0",
77
77
  "eslint": "^9.39.2",
78
78
  "ts-node": "^10.9.2",
79
79
  "tsdown": "^0.20.1",
package/src/bignum.ts ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * CBOR bignum (tags 2 and 3) support.
3
+ *
4
+ * This module provides conversion between CBOR and JavaScript BigInt types,
5
+ * implementing RFC 8949 §3.4.3 (Bignums) with dCBOR/CDE canonical encoding rules.
6
+ *
7
+ * Encoding:
8
+ * - `biguintToCbor` always encodes as tag 2 (positive bignum) with a byte
9
+ * string content.
10
+ * - `bigintToCbor` encodes as tag 2 for non-negative values or tag 3
11
+ * (negative bignum) for negative values.
12
+ * - No numeric reduction is performed: values are always encoded as bignums,
13
+ * even if they would fit in normal CBOR integers.
14
+ *
15
+ * Decoding:
16
+ * - Accepts CBOR integers (major types 0 and 1) and converts them to bigints.
17
+ * - Accepts tag 2 (positive bignum) and tag 3 (negative bignum) with byte
18
+ * string content.
19
+ * - Enforces shortest-form canonical representation for bignum magnitudes.
20
+ * - Rejects floating-point values.
21
+ *
22
+ * @module bignum
23
+ */
24
+
25
+ import { type Cbor, MajorType, toTaggedValue } from "./cbor";
26
+ import { CborError } from "./error";
27
+
28
+ // CBOR tag values (local constants matching tags.ts, avoiding circular import)
29
+ const TAG_2_POSITIVE_BIGNUM = 2;
30
+ const TAG_3_NEGATIVE_BIGNUM = 3;
31
+
32
+ /**
33
+ * Validates that a bignum magnitude byte string is in shortest canonical form.
34
+ *
35
+ * Matches Rust's `validate_bignum_magnitude()`.
36
+ *
37
+ * Rules:
38
+ * - For positive bignums (tag 2): empty byte string represents zero;
39
+ * non-empty must not have leading zero bytes.
40
+ * - For negative bignums (tag 3): byte string must not be empty
41
+ * (magnitude zero is encoded as `0x00`); must not have leading zero bytes
42
+ * except when the magnitude is zero (single `0x00`).
43
+ *
44
+ * @param bytes - The magnitude byte string to validate
45
+ * @param isNegative - Whether this is for a negative bignum (tag 3)
46
+ * @throws CborError with type NonCanonicalNumeric on validation failure
47
+ */
48
+ export function validateBignumMagnitude(bytes: Uint8Array, isNegative: boolean): void {
49
+ if (isNegative) {
50
+ // Tag 3: byte string must not be empty
51
+ if (bytes.length === 0) {
52
+ throw new CborError({ type: "NonCanonicalNumeric" });
53
+ }
54
+ // No leading zeros unless the entire magnitude is zero (single 0x00 byte)
55
+ if (bytes.length > 1 && bytes[0] === 0) {
56
+ throw new CborError({ type: "NonCanonicalNumeric" });
57
+ }
58
+ } else {
59
+ // Tag 2: empty byte string is valid (represents zero)
60
+ // Non-empty must not have leading zeros
61
+ if (bytes.length > 0 && bytes[0] === 0) {
62
+ throw new CborError({ type: "NonCanonicalNumeric" });
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Strips leading zero bytes from a byte array, returning the minimal
69
+ * representation.
70
+ *
71
+ * Matches Rust's `strip_leading_zeros()`.
72
+ *
73
+ * @param bytes - The byte array to strip
74
+ * @returns A subarray with leading zeros removed
75
+ */
76
+ export function stripLeadingZeros(bytes: Uint8Array): Uint8Array {
77
+ let start = 0;
78
+ while (start < bytes.length && bytes[start] === 0) {
79
+ start++;
80
+ }
81
+ return bytes.subarray(start);
82
+ }
83
+
84
+ /**
85
+ * Convert a non-negative bigint to a big-endian byte array.
86
+ *
87
+ * Zero returns an empty Uint8Array.
88
+ *
89
+ * @param value - A non-negative bigint value
90
+ * @returns Big-endian byte representation
91
+ */
92
+ export function bigintToBytes(value: bigint): Uint8Array {
93
+ if (value === 0n) return new Uint8Array(0);
94
+ const hex = value.toString(16);
95
+ const padded = hex.length % 2 !== 0 ? `0${hex}` : hex;
96
+ const bytes = new Uint8Array(padded.length / 2);
97
+ for (let i = 0; i < bytes.length; i++) {
98
+ bytes[i] = parseInt(padded.substring(i * 2, i * 2 + 2), 16);
99
+ }
100
+ return bytes;
101
+ }
102
+
103
+ /**
104
+ * Convert a big-endian byte array to a bigint.
105
+ *
106
+ * Empty array returns 0n.
107
+ *
108
+ * @param bytes - Big-endian byte representation
109
+ * @returns The bigint value
110
+ */
111
+ export function bytesToBigint(bytes: Uint8Array): bigint {
112
+ if (bytes.length === 0) return 0n;
113
+ let result = 0n;
114
+ for (const byte of bytes) {
115
+ result = (result << 8n) | BigInt(byte);
116
+ }
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Encode a non-negative bigint as a CBOR tag 2 (positive bignum).
122
+ *
123
+ * Matches Rust's `From<BigUint> for CBOR`.
124
+ *
125
+ * The value is always encoded as a bignum regardless of size.
126
+ * Zero is encoded as tag 2 with an empty byte string.
127
+ *
128
+ * @param value - A non-negative bigint (must be >= 0n)
129
+ * @returns CBOR tagged value
130
+ * @throws CborError with type OutOfRange if value is negative
131
+ */
132
+ export function biguintToCbor(value: bigint): Cbor {
133
+ if (value < 0n) {
134
+ throw new CborError({ type: "OutOfRange" });
135
+ }
136
+ const bytes = bigintToBytes(value);
137
+ const stripped = stripLeadingZeros(bytes);
138
+ return toTaggedValue(TAG_2_POSITIVE_BIGNUM, stripped);
139
+ }
140
+
141
+ /**
142
+ * Encode a bigint as a CBOR tag 2 or tag 3 bignum.
143
+ *
144
+ * Matches Rust's `From<BigInt> for CBOR`.
145
+ *
146
+ * - Non-negative values use tag 2 (positive bignum).
147
+ * - Negative values use tag 3 (negative bignum), where the encoded
148
+ * magnitude is `|value| - 1` per RFC 8949.
149
+ *
150
+ * @param value - Any bigint value
151
+ * @returns CBOR tagged value
152
+ */
153
+ export function bigintToCbor(value: bigint): Cbor {
154
+ if (value >= 0n) {
155
+ return biguintToCbor(value);
156
+ }
157
+ // Negative: use tag 3 with magnitude = |value| - 1
158
+ // For value = -1, magnitude = 1, so n = 0 -> encode as 0x00
159
+ // For value = -2, magnitude = 2, so n = 1 -> encode as 0x01
160
+ const magnitude = -value;
161
+ const n = magnitude - 1n;
162
+ const bytes = bigintToBytes(n);
163
+ const stripped = stripLeadingZeros(bytes);
164
+ // For n = 0 (value = -1), bigintToBytes returns empty, but we need 0x00
165
+ const contentBytes = stripped.length === 0 ? new Uint8Array([0]) : stripped;
166
+ return toTaggedValue(TAG_3_NEGATIVE_BIGNUM, contentBytes);
167
+ }
168
+
169
+ /**
170
+ * Decode a BigUint from an untagged CBOR byte string.
171
+ *
172
+ * Matches Rust's `biguint_from_untagged_cbor()`.
173
+ *
174
+ * This function is intended for use in tag summarizers where the tag has
175
+ * already been stripped. It expects a CBOR byte string representing the
176
+ * big-endian magnitude of a positive bignum (tag 2 content).
177
+ *
178
+ * Enforces canonical encoding: no leading zero bytes (except empty for zero).
179
+ *
180
+ * @param cbor - A CBOR value that should be a byte string
181
+ * @returns Non-negative bigint
182
+ * @throws CborError with type WrongType if not a byte string
183
+ * @throws CborError with type NonCanonicalNumeric if encoding is non-canonical
184
+ */
185
+ export function biguintFromUntaggedCbor(cbor: Cbor): bigint {
186
+ if (cbor.type !== MajorType.ByteString) {
187
+ throw new CborError({ type: "WrongType" });
188
+ }
189
+ const bytes = cbor.value;
190
+ validateBignumMagnitude(bytes, false);
191
+ return bytesToBigint(bytes);
192
+ }
193
+
194
+ /**
195
+ * Decode a BigInt from an untagged CBOR byte string for a negative bignum.
196
+ *
197
+ * Matches Rust's `bigint_from_negative_untagged_cbor()`.
198
+ *
199
+ * This function is intended for use in tag summarizers where the tag has
200
+ * already been stripped. It expects a CBOR byte string representing `n` where
201
+ * the actual value is `-1 - n` (tag 3 content per RFC 8949).
202
+ *
203
+ * Enforces canonical encoding: no leading zero bytes (except single `0x00`
204
+ * for -1).
205
+ *
206
+ * @param cbor - A CBOR value that should be a byte string
207
+ * @returns Negative bigint
208
+ * @throws CborError with type WrongType if not a byte string
209
+ * @throws CborError with type NonCanonicalNumeric if encoding is non-canonical
210
+ */
211
+ export function bigintFromNegativeUntaggedCbor(cbor: Cbor): bigint {
212
+ if (cbor.type !== MajorType.ByteString) {
213
+ throw new CborError({ type: "WrongType" });
214
+ }
215
+ const bytes = cbor.value;
216
+ validateBignumMagnitude(bytes, true);
217
+ const n = bytesToBigint(bytes);
218
+ const magnitude = n + 1n;
219
+ return -magnitude;
220
+ }
221
+
222
+ /**
223
+ * Convert CBOR to a non-negative bigint.
224
+ *
225
+ * Matches Rust's `TryFrom<CBOR> for BigUint`.
226
+ *
227
+ * Accepts:
228
+ * - Major type 0 (unsigned integer)
229
+ * - Tag 2 (positive bignum) with canonical byte string
230
+ *
231
+ * Rejects:
232
+ * - Major type 1 (negative integer) -> OutOfRange
233
+ * - Tag 3 (negative bignum) -> OutOfRange
234
+ * - Floating-point values -> WrongType
235
+ * - Non-canonical bignum encodings -> NonCanonicalNumeric
236
+ *
237
+ * @param cbor - The CBOR value to convert
238
+ * @returns Non-negative bigint
239
+ * @throws CborError
240
+ */
241
+ export function cborToBiguint(cbor: Cbor): bigint {
242
+ switch (cbor.type) {
243
+ case MajorType.Unsigned:
244
+ return BigInt(cbor.value);
245
+ case MajorType.Negative:
246
+ throw new CborError({ type: "OutOfRange" });
247
+ case MajorType.Tagged: {
248
+ const tagValue = Number(cbor.tag);
249
+ if (tagValue === TAG_2_POSITIVE_BIGNUM) {
250
+ const inner = cbor.value;
251
+ if (inner.type !== MajorType.ByteString) {
252
+ throw new CborError({ type: "WrongType" });
253
+ }
254
+ const bytes = inner.value;
255
+ validateBignumMagnitude(bytes, false);
256
+ return bytesToBigint(bytes);
257
+ } else if (tagValue === TAG_3_NEGATIVE_BIGNUM) {
258
+ throw new CborError({ type: "OutOfRange" });
259
+ }
260
+ throw new CborError({ type: "WrongType" });
261
+ }
262
+ case MajorType.ByteString:
263
+ case MajorType.Text:
264
+ case MajorType.Array:
265
+ case MajorType.Map:
266
+ case MajorType.Simple:
267
+ throw new CborError({ type: "WrongType" });
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Convert CBOR to a bigint (any sign).
273
+ *
274
+ * Matches Rust's `TryFrom<CBOR> for BigInt`.
275
+ *
276
+ * Accepts:
277
+ * - Major type 0 (unsigned integer)
278
+ * - Major type 1 (negative integer)
279
+ * - Tag 2 (positive bignum) with canonical byte string
280
+ * - Tag 3 (negative bignum) with canonical byte string
281
+ *
282
+ * Rejects:
283
+ * - Floating-point values -> WrongType
284
+ * - Non-canonical bignum encodings -> NonCanonicalNumeric
285
+ *
286
+ * @param cbor - The CBOR value to convert
287
+ * @returns A bigint value
288
+ * @throws CborError
289
+ */
290
+ export function cborToBigint(cbor: Cbor): bigint {
291
+ switch (cbor.type) {
292
+ case MajorType.Unsigned:
293
+ return BigInt(cbor.value);
294
+ case MajorType.Negative: {
295
+ // CBOR negative: value stored is n where actual = -1 - n
296
+ const n = BigInt(cbor.value);
297
+ const magnitude = n + 1n;
298
+ return -magnitude;
299
+ }
300
+ case MajorType.Tagged: {
301
+ const tagValue = Number(cbor.tag);
302
+ if (tagValue === TAG_2_POSITIVE_BIGNUM) {
303
+ const inner = cbor.value;
304
+ if (inner.type !== MajorType.ByteString) {
305
+ throw new CborError({ type: "WrongType" });
306
+ }
307
+ const bytes = inner.value;
308
+ validateBignumMagnitude(bytes, false);
309
+ const mag = bytesToBigint(bytes);
310
+ if (mag === 0n) {
311
+ return 0n;
312
+ }
313
+ return mag;
314
+ } else if (tagValue === TAG_3_NEGATIVE_BIGNUM) {
315
+ const inner = cbor.value;
316
+ if (inner.type !== MajorType.ByteString) {
317
+ throw new CborError({ type: "WrongType" });
318
+ }
319
+ const bytes = inner.value;
320
+ validateBignumMagnitude(bytes, true);
321
+ const n = bytesToBigint(bytes);
322
+ const magnitude = n + 1n;
323
+ return -magnitude;
324
+ }
325
+ throw new CborError({ type: "WrongType" });
326
+ }
327
+ case MajorType.ByteString:
328
+ case MajorType.Text:
329
+ case MajorType.Array:
330
+ case MajorType.Map:
331
+ case MajorType.Simple:
332
+ throw new CborError({ type: "WrongType" });
333
+ }
334
+ }
package/src/index.ts CHANGED
@@ -88,6 +88,16 @@ export { type Error, type Result, Ok, Err, errorMsg, errorToString, CborError }
88
88
  // Note: conveniences.ts is an internal module (not exported in Rust either)
89
89
  // The main convenience functions are exported from cbor.ts above
90
90
 
91
+ // BigNum support (CBOR tags 2/3, RFC 8949 §3.4.3)
92
+ export {
93
+ biguintToCbor,
94
+ bigintToCbor,
95
+ cborToBiguint,
96
+ cborToBigint,
97
+ biguintFromUntaggedCbor,
98
+ bigintFromNegativeUntaggedCbor,
99
+ } from "./bignum";
100
+
91
101
  // Float utilities
92
102
  export { hasFractionalPart } from "./float";
93
103
 
package/src/prelude.ts CHANGED
@@ -65,6 +65,9 @@ export type { HexFormatOpts } from "./dump";
65
65
  export { EdgeType } from "./walk";
66
66
  export type { WalkElement, EdgeTypeVariant, Visitor } from "./walk";
67
67
 
68
+ // BigNum support
69
+ export { biguintToCbor, bigintToCbor, cborToBiguint, cborToBigint } from "./bignum";
70
+
68
71
  // Error handling
69
72
  export type { Error, Result } from "./error";
70
73
  export { Ok, Err, errorMsg, errorToString, CborError } from "./error";
package/src/tags.ts CHANGED
@@ -40,6 +40,18 @@ export const TAG_POSITIVE_BIGNUM = 2;
40
40
  */
41
41
  export const TAG_NEGATIVE_BIGNUM = 3;
42
42
 
43
+ /**
44
+ * Name for tag 2 (positive bignum).
45
+ * Matches Rust's `TAG_NAME_POSITIVE_BIGNUM`.
46
+ */
47
+ export const TAG_NAME_POSITIVE_BIGNUM = "positive-bignum";
48
+
49
+ /**
50
+ * Name for tag 3 (negative bignum).
51
+ * Matches Rust's `TAG_NAME_NEGATIVE_BIGNUM`.
52
+ */
53
+ export const TAG_NAME_NEGATIVE_BIGNUM = "negative-bignum";
54
+
43
55
  /**
44
56
  * Tag 4: Decimal fraction [exponent, mantissa]
45
57
  */
@@ -152,6 +164,7 @@ import type { TagsStore, SummarizerResult } from "./tags-store";
152
164
  import { getGlobalTagsStore } from "./tags-store";
153
165
  import { CborDate } from "./date";
154
166
  import type { Cbor } from "./cbor";
167
+ import { biguintFromUntaggedCbor, bigintFromNegativeUntaggedCbor } from "./bignum";
155
168
 
156
169
  // Tag constants matching Rust
157
170
  export const TAG_DATE = 1;
@@ -177,6 +190,39 @@ export const registerTagsIn = (tagsStore: TagsStore): void => {
177
190
  return { ok: false, error: { type: "Custom", message } };
178
191
  }
179
192
  });
193
+
194
+ // Register bignum tags (matching Rust's #[cfg(feature = "num-bigint")] block)
195
+ const biguintTag = createTag(TAG_POSITIVE_BIGNUM, TAG_NAME_POSITIVE_BIGNUM);
196
+ const bigintTag = createTag(TAG_NEGATIVE_BIGNUM, TAG_NAME_NEGATIVE_BIGNUM);
197
+ tagsStore.insertAll([biguintTag, bigintTag]);
198
+
199
+ // Summarizer for tag 2 (positive bignum)
200
+ tagsStore.setSummarizer(
201
+ TAG_POSITIVE_BIGNUM,
202
+ (untaggedCbor: Cbor, _flat: boolean): SummarizerResult => {
203
+ try {
204
+ const value = biguintFromUntaggedCbor(untaggedCbor);
205
+ return { ok: true, value: `bignum(${value})` };
206
+ } catch (e) {
207
+ const message = e instanceof Error ? e.message : String(e);
208
+ return { ok: false, error: { type: "Custom", message } };
209
+ }
210
+ },
211
+ );
212
+
213
+ // Summarizer for tag 3 (negative bignum)
214
+ tagsStore.setSummarizer(
215
+ TAG_NEGATIVE_BIGNUM,
216
+ (untaggedCbor: Cbor, _flat: boolean): SummarizerResult => {
217
+ try {
218
+ const value = bigintFromNegativeUntaggedCbor(untaggedCbor);
219
+ return { ok: true, value: `bignum(${value})` };
220
+ } catch (e) {
221
+ const message = e instanceof Error ? e.message : String(e);
222
+ return { ok: false, error: { type: "Custom", message } };
223
+ }
224
+ },
225
+ );
180
226
  };
181
227
 
182
228
  /**