Skip to content

Commit 571b718

Browse files
committed
feat: big number support for cbor
bigint support in cbor
1 parent e5bb1e2 commit 571b718

File tree

6 files changed

+107
-8
lines changed

6 files changed

+107
-8
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Prints bytes as binary string with numbers.
3+
* @param bytes
4+
*/
5+
export function printBytes(bytes: Uint8Array) {
6+
return [...bytes].map((n) => ("0".repeat(8) + n.toString(2)).slice(-8) + ` (${n})`);
7+
}

packages/core/src/submodules/cbor/cbor-decode.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
Uint32,
3232
Uint64,
3333
} from "./cbor-types";
34+
import { printBytes } from "./byte-printer";
3435

3536
const USE_TEXT_DECODER = typeof TextDecoder !== "undefined";
3637
const USE_BUFFER = typeof Buffer !== "undefined";
@@ -119,11 +120,25 @@ export function decode(at: Uint32, to: Uint32): CborValueType {
119120
_offset = offset;
120121
return castBigInt(negativeInt);
121122
} else {
122-
const value = decode(at + offset, to);
123-
const valueOffset = _offset;
123+
if (minor === 2 || minor === 3) {
124+
const length = decodeCount(at + offset, to);
124125

125-
_offset = offset + valueOffset;
126-
return tag({ tag: castBigInt(unsignedInt), value });
126+
let b = BigInt(0);
127+
const start = at + offset + _offset;
128+
for (let i = start; i < start + length; ++i) {
129+
b = (b << BigInt(8)) | BigInt(payload[i]);
130+
}
131+
132+
_offset = offset + length;
133+
return minor === 3 ? -b - BigInt(1) : b;
134+
} else {
135+
const value = decode(at + offset, to);
136+
const valueOffset = _offset;
137+
138+
_offset = offset + valueOffset;
139+
140+
return tag({ tag: castBigInt(unsignedInt), value });
141+
}
127142
}
128143
case majorUtf8String:
129144
case majorMap:

packages/core/src/submodules/cbor/cbor-encode.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { fromUtf8 } from "@smithy/util-utf8";
22

33
import {
4+
alloc,
45
CborMajorType,
56
extendedFloat16,
67
extendedFloat32,
@@ -19,7 +20,6 @@ import {
1920
tagSymbol,
2021
Uint64,
2122
} from "./cbor-types";
22-
import { alloc } from "./cbor-types";
2323

2424
const USE_BUFFER = typeof Buffer !== "undefined";
2525

@@ -152,10 +152,30 @@ export function encode(_input: any): void {
152152
data[cursor++] = (major << 5) | extendedFloat32;
153153
dataView.setUint32(cursor, n);
154154
cursor += 4;
155-
} else {
155+
} else if (value < BigInt("18446744073709551616")) {
156156
data[cursor++] = (major << 5) | extendedFloat64;
157157
dataView.setBigUint64(cursor, value);
158158
cursor += 8;
159+
} else {
160+
// refer to https://www.rfc-editor.org/rfc/rfc8949.html#name-bignums
161+
const binaryBigInt = value.toString(2);
162+
const bigIntBytes = new Uint8Array(Math.ceil(binaryBigInt.length / 8));
163+
let b = value;
164+
let i = 0;
165+
while (bigIntBytes.byteLength - ++i >= 0) {
166+
bigIntBytes[bigIntBytes.byteLength - i] = Number(b & BigInt(255));
167+
b >>= BigInt(8);
168+
}
169+
ensureSpace(bigIntBytes.byteLength * 2);
170+
data[cursor++] = nonNegative ? 0b110_00010 : 0b110_00011;
171+
172+
if (USE_BUFFER) {
173+
encodeHeader(majorUnstructuredByteString, Buffer.byteLength(bigIntBytes));
174+
} else {
175+
encodeHeader(majorUnstructuredByteString, bigIntBytes.byteLength);
176+
}
177+
data.set(bigIntBytes, cursor);
178+
cursor += bigIntBytes.byteLength;
159179
}
160180
continue;
161181
} else if (input === null) {

packages/core/src/submodules/cbor/cbor.spec.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ describe("cbor", () => {
8888
{
8989
name: "negative float",
9090
data: -3015135.135135135,
91-
cbor: allocByteArray([0b111_11011, +193, +71, +0, +239, +145, +76, +27, +173]),
91+
cbor: allocByteArray([0b111_11011, 193, 71, 0, 239, 145, 76, 27, 173]),
9292
},
9393
{
9494
name: "positive float",
9595
data: 3015135.135135135,
96-
cbor: allocByteArray([0b111_11011, +65, +71, +0, +239, +145, +76, +27, +173]),
96+
cbor: allocByteArray([0b111_11011, 65, 71, 0, 239, 145, 76, 27, 173]),
9797
},
9898
{
9999
name: "various numbers",
@@ -214,6 +214,18 @@ describe("cbor", () => {
214214
65, 109, 110, 101, 115, 116, 101, 100, 32, 105, 116, 101, 109, 32, 66,
215215
]),
216216
},
217+
{
218+
name: "object containing big numbers",
219+
data: {
220+
map: {
221+
items: [BigInt(1e80)],
222+
},
223+
},
224+
cbor: allocByteArray([
225+
161, 99, 109, 97, 112, 161, 101, 105, 116, 101, 109, 115, 129, 194, 88, 34, 3, 95, 157, 234, 62, 31, 107, 224,
226+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
227+
]),
228+
},
217229
];
218230

219231
const toBytes = (hex: string) => {
@@ -226,6 +238,48 @@ describe("cbor", () => {
226238
};
227239

228240
describe("locally curated scenarios", () => {
241+
it("should round-trip bigInteger to major 6 with tag 2", () => {
242+
const bigInt = BigInt("1267650600228229401496703205376");
243+
const serialized = cbor.serialize(bigInt);
244+
245+
const major = serialized[0] >> 5;
246+
expect(major).toEqual(0b110); // 6
247+
248+
const tag = serialized[0] & 0b11111;
249+
expect(tag).toEqual(0b010); // 2
250+
251+
const byteStringCount = serialized[1];
252+
expect(byteStringCount).toEqual(0b010_01101); // major 2, 13 bytes
253+
254+
const byteString = serialized.slice(2);
255+
expect(byteString).toEqual(allocByteArray([0b000_10000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
256+
257+
const deserialized = cbor.deserialize(serialized);
258+
expect(deserialized).toEqual(bigInt);
259+
});
260+
261+
it("should round-trip negative bigInteger to major 6 with tag 3", () => {
262+
const bigInt = BigInt("-1267650600228229401496703205377");
263+
const serialized = cbor.serialize(bigInt);
264+
265+
const major = serialized[0] >> 5;
266+
expect(major).toEqual(0b110); // 6
267+
268+
const tag = serialized[0] & 0b11111;
269+
expect(tag).toEqual(0b011); // 3
270+
271+
const byteStringCount = serialized[1];
272+
expect(byteStringCount).toEqual(0b010_01101); // major 2, 13 bytes
273+
274+
const byteString = serialized.slice(2);
275+
expect(byteString).toEqual(allocByteArray([0b000_10000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
276+
277+
const deserialized = cbor.deserialize(serialized);
278+
expect(deserialized).toEqual(bigInt);
279+
});
280+
281+
it("should round-trip NumericValue to major 6 with tag 4", () => {});
282+
229283
it("should throw an error if serializing a tag with missing properties", () => {
230284
expect(() =>
231285
cbor.serialize({

packages/core/src/submodules/serde/parse-utils.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from "./parse-utils";
2323
import { expectBoolean, expectNumber, expectString } from "./parse-utils";
2424

25+
logger.warn = () => {};
26+
2527
describe("parseBoolean", () => {
2628
it('Returns true for "true"', () => {
2729
expect(parseBoolean("true")).toEqual(true);

packages/core/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export default defineConfig({
55
exclude: ["**/*.{integ,e2e,browser}.spec.ts"],
66
include: ["**/*.spec.ts"],
77
environment: "node",
8+
hideSkippedTests: true,
89
},
910
});

0 commit comments

Comments
 (0)