@haneullabs/bcs 2.1.2 → 2.1.3
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/CHANGELOG.md +18 -0
- package/dist/bcs-type.d.mts.map +1 -1
- package/dist/bcs-type.mjs +48 -20
- package/dist/bcs-type.mjs.map +1 -1
- package/dist/bcs.d.mts.map +1 -1
- package/dist/bcs.mjs +40 -17
- package/dist/bcs.mjs.map +1 -1
- package/dist/reader.d.mts +1 -0
- package/dist/reader.d.mts.map +1 -1
- package/dist/reader.mjs +25 -11
- package/dist/reader.mjs.map +1 -1
- package/dist/writer.d.mts +2 -0
- package/dist/writer.d.mts.map +1 -1
- package/dist/writer.mjs +26 -19
- package/dist/writer.mjs.map +1 -1
- package/package.json +2 -2
- package/src/bcs-type.ts +68 -35
- package/src/bcs.ts +57 -25
- package/src/reader.ts +39 -16
- package/src/writer.ts +30 -25
package/dist/reader.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.mjs","names":[],"sources":["../src/reader.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\
|
|
1
|
+
{"version":3,"file":"reader.mjs","names":[],"sources":["../src/reader.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Class used for reading BCS data chunk by chunk. Meant to be used\n * by some wrapper, which will make sure that data is valid and is\n * matching the desired format.\n *\n * @example\n * // data for this example is:\n * // { a: u8, b: u32, c: bool, d: u64 }\n *\n * let reader = new BcsReader(\"647f1a060001ffffe7890423c78a050102030405\");\n * let field1 = reader.read8();\n * let field2 = reader.read32();\n * let field3 = reader.read8() === '1'; // bool\n * let field4 = reader.read64();\n * // ....\n *\n * Reading vectors is another deal in bcs. To read a vector, you first need to read\n * its length using {@link readULEB}. Here's an example:\n * @example\n * // data encoded: { field: [1, 2, 3, 4, 5] }\n * let reader = new BcsReader(\"050102030405\");\n * let vec_length = reader.readULEB();\n * let elements = [];\n * for (let i = 0; i < vec_length; i++) {\n * elements.push(reader.read8());\n * }\n * console.log(elements); // [1,2,3,4,5]\n *\n * @param {String} data HEX-encoded data (serialized BCS)\n */\nexport class BcsReader {\n\tprivate dataView: DataView;\n\tprivate bytes: Uint8Array;\n\tprivate bytePosition: number = 0;\n\n\t/**\n\t * @param {Uint8Array} data Data to use as a buffer.\n\t */\n\tconstructor(data: Uint8Array) {\n\t\tthis.dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);\n\t\tthis.bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n\t}\n\t/**\n\t * Shift current cursor position by `bytes`.\n\t *\n\t * @param {Number} bytes Number of bytes to\n\t * @returns {this} Self for possible chaining.\n\t */\n\tshift(bytes: number) {\n\t\tthis.bytePosition += bytes;\n\t\treturn this;\n\t}\n\t/**\n\t * Read U8 value from the buffer and shift cursor by 1.\n\t * @returns\n\t */\n\tread8(): number {\n\t\tconst position = this.bytePosition;\n\t\tif (position >= this.bytes.length) {\n\t\t\tthrow new RangeError('Offset is outside the bounds of the DataView');\n\t\t}\n\t\tthis.bytePosition = position + 1;\n\t\treturn this.bytes[position];\n\t}\n\t/**\n\t * Read U16 value from the buffer and shift cursor by 2.\n\t * @returns\n\t */\n\tread16(): number {\n\t\tconst value = this.dataView.getUint16(this.bytePosition, true);\n\t\tthis.shift(2);\n\t\treturn value;\n\t}\n\t/**\n\t * Read U32 value from the buffer and shift cursor by 4.\n\t * @returns\n\t */\n\tread32(): number {\n\t\tconst value = this.dataView.getUint32(this.bytePosition, true);\n\t\tthis.shift(4);\n\t\treturn value;\n\t}\n\t/**\n\t * Read U64 value from the buffer and shift cursor by 8.\n\t * @returns\n\t */\n\tread64(): string {\n\t\tconst value = this.dataView.getBigUint64(this.bytePosition, true);\n\t\tthis.shift(8);\n\t\treturn value.toString(10);\n\t}\n\t/**\n\t * Read U128 value from the buffer and shift cursor by 16.\n\t */\n\tread128(): string {\n\t\tconst value1 = BigInt(this.read64());\n\t\tconst value2 = BigInt(this.read64());\n\t\tconst result = value2.toString(16) + value1.toString(16).padStart(16, '0');\n\n\t\treturn BigInt('0x' + result).toString(10);\n\t}\n\t/**\n\t * Read U128 value from the buffer and shift cursor by 32.\n\t * @returns\n\t */\n\tread256(): string {\n\t\tconst value1 = BigInt(this.read128());\n\t\tconst value2 = BigInt(this.read128());\n\t\tconst result = value2.toString(16) + value1.toString(16).padStart(32, '0');\n\n\t\treturn BigInt('0x' + result).toString(10);\n\t}\n\t/**\n\t * Read `num` number of bytes from the buffer and shift cursor by `num`.\n\t * @param num Number of bytes to read.\n\t */\n\treadBytes(num: number): Uint8Array {\n\t\t// The underlying buffer may extend past this view, so check against the view's own length\n\t\tif (this.bytePosition + num > this.bytes.length) {\n\t\t\tthrow new RangeError('Offset is outside the bounds of the DataView');\n\t\t}\n\t\tconst start = this.bytePosition + this.dataView.byteOffset;\n\t\tconst value = new Uint8Array(this.dataView.buffer, start, num);\n\n\t\tthis.shift(num);\n\n\t\treturn value;\n\t}\n\t/**\n\t * Read ULEB value - an integer of varying size. Used for enum indexes and\n\t * vector lengths.\n\t * @returns {Number} The ULEB value.\n\t */\n\treadULEB(): number {\n\t\tconst bytes = this.bytes;\n\t\tlet position = this.bytePosition;\n\t\tlet value = 0;\n\t\tlet multiplier = 1;\n\t\twhile (true) {\n\t\t\tif (position >= bytes.length) {\n\t\t\t\tthrow new Error('ULEB decode error: buffer overflow');\n\t\t\t}\n\t\t\tconst byte = bytes[position++];\n\t\t\tvalue += (byte & 0x7f) * multiplier;\n\t\t\tif ((byte & 0x80) === 0) {\n\t\t\t\t// A trailing zero group adds no value, so the encoding is not minimal\n\t\t\t\tif (byte === 0 && position - this.bytePosition > 1) {\n\t\t\t\t\tthrow new Error('ULEB decode error: non-canonical encoding');\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmultiplier *= 128;\n\t\t}\n\n\t\t// Also rejects NaN, from very long encodings where multiplier overflows to Infinity\n\t\tif (!(value <= Number.MAX_SAFE_INTEGER)) {\n\t\t\tthrow new Error('ULEB decode error: value exceeds MAX_SAFE_INTEGER');\n\t\t}\n\t\tthis.bytePosition = position;\n\t\treturn value;\n\t}\n\t/**\n\t * Read a BCS vector: read a length and then apply function `cb` X times\n\t * where X is the length of the vector, defined as ULEB in BCS bytes.\n\t * @param cb Callback to process elements of vector.\n\t * @returns {Array<Any>} Array of the resulting values, returned by callback.\n\t */\n\treadVec(cb: (reader: BcsReader, i: number, length: number) => any): any[] {\n\t\tconst length = this.readULEB();\n\t\tconst result = [];\n\t\tfor (let i = 0; i < length; i++) {\n\t\t\tresult.push(cb(this, i, length));\n\t\t}\n\t\treturn result;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAa,YAAb,MAAuB;;;;CAQtB,YAAY,MAAkB;EALC,KAAA,eAAA;EAM9B,KAAK,WAAW,IAAI,SAAS,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;EAC1E,KAAK,QAAQ,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;CAC1E;;;;;;;CAOA,MAAM,OAAe;EACpB,KAAK,gBAAgB;EACrB,OAAO;CACR;;;;;CAKA,QAAgB;EACf,MAAM,WAAW,KAAK;EACtB,IAAI,YAAY,KAAK,MAAM,QAC1B,MAAM,IAAI,WAAW,8CAA8C;EAEpE,KAAK,eAAe,WAAW;EAC/B,OAAO,KAAK,MAAM;CACnB;;;;;CAKA,SAAiB;EAChB,MAAM,QAAQ,KAAK,SAAS,UAAU,KAAK,cAAc,IAAI;EAC7D,KAAK,MAAM,CAAC;EACZ,OAAO;CACR;;;;;CAKA,SAAiB;EAChB,MAAM,QAAQ,KAAK,SAAS,UAAU,KAAK,cAAc,IAAI;EAC7D,KAAK,MAAM,CAAC;EACZ,OAAO;CACR;;;;;CAKA,SAAiB;EAChB,MAAM,QAAQ,KAAK,SAAS,aAAa,KAAK,cAAc,IAAI;EAChE,KAAK,MAAM,CAAC;EACZ,OAAO,MAAM,SAAS,EAAE;CACzB;;;;CAIA,UAAkB;EACjB,MAAM,SAAS,OAAO,KAAK,OAAO,CAAC;EAEnC,MAAM,SADS,OAAO,KAAK,OAAO,CACd,CAAC,CAAC,SAAS,EAAE,IAAI,OAAO,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,GAAG;EAEzE,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE;CACzC;;;;;CAKA,UAAkB;EACjB,MAAM,SAAS,OAAO,KAAK,QAAQ,CAAC;EAEpC,MAAM,SADS,OAAO,KAAK,QAAQ,CACf,CAAC,CAAC,SAAS,EAAE,IAAI,OAAO,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,GAAG;EAEzE,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE;CACzC;;;;;CAKA,UAAU,KAAyB;EAElC,IAAI,KAAK,eAAe,MAAM,KAAK,MAAM,QACxC,MAAM,IAAI,WAAW,8CAA8C;EAEpE,MAAM,QAAQ,KAAK,eAAe,KAAK,SAAS;EAChD,MAAM,QAAQ,IAAI,WAAW,KAAK,SAAS,QAAQ,OAAO,GAAG;EAE7D,KAAK,MAAM,GAAG;EAEd,OAAO;CACR;;;;;;CAMA,WAAmB;EAClB,MAAM,QAAQ,KAAK;EACnB,IAAI,WAAW,KAAK;EACpB,IAAI,QAAQ;EACZ,IAAI,aAAa;EACjB,OAAO,MAAM;GACZ,IAAI,YAAY,MAAM,QACrB,MAAM,IAAI,MAAM,oCAAoC;GAErD,MAAM,OAAO,MAAM;GACnB,UAAU,OAAO,OAAQ;GACzB,KAAK,OAAO,SAAU,GAAG;IAExB,IAAI,SAAS,KAAK,WAAW,KAAK,eAAe,GAChD,MAAM,IAAI,MAAM,2CAA2C;IAE5D;GACD;GACA,cAAc;EACf;EAGA,IAAI,EAAE,SAAS,OAAO,mBACrB,MAAM,IAAI,MAAM,mDAAmD;EAEpE,KAAK,eAAe;EACpB,OAAO;CACR;;;;;;;CAOA,QAAQ,IAAkE;EACzE,MAAM,SAAS,KAAK,SAAS;EAC7B,MAAM,SAAS,CAAC;EAChB,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAC3B,OAAO,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;EAEhC,OAAO;CACR;AACD"}
|
package/dist/writer.d.mts
CHANGED
|
@@ -27,6 +27,7 @@ interface BcsWriterOptions {
|
|
|
27
27
|
*/
|
|
28
28
|
declare class BcsWriter {
|
|
29
29
|
private dataView;
|
|
30
|
+
private bytes;
|
|
30
31
|
private bytePosition;
|
|
31
32
|
private size;
|
|
32
33
|
private maxSize;
|
|
@@ -84,6 +85,7 @@ declare class BcsWriter {
|
|
|
84
85
|
* @returns {this}
|
|
85
86
|
*/
|
|
86
87
|
write256(value: number | bigint): this;
|
|
88
|
+
private writeBigLittleEndian;
|
|
87
89
|
/**
|
|
88
90
|
* Write a ULEB value into a buffer and shift cursor position by number of bytes
|
|
89
91
|
* written.
|
package/dist/writer.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.d.mts","names":[],"sources":["../src/writer.ts"],"mappings":";;UAOiB;;EAEhB;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;cAqBY;UACJ;UACA;UACA;UACA;UACA;EAER,cACC,aACA,SACA,iBACE;
|
|
1
|
+
{"version":3,"file":"writer.d.mts","names":[],"sources":["../src/writer.ts"],"mappings":";;UAOiB;;EAEhB;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;cAqBY;UACJ;UACA;UACA;UACA;UACA;UACA;EAER,cACC,aACA,SACA,iBACE;UAQK;;;;;;;EA4BR,MAAM;;;;;;EASN,OAAO;;;;;;EAWP,WAAW,OAAO;;;;;;EAWlB,QAAQ;;;;;;EAUR,QAAQ;;;;;;EAUR,QAAQ;;;;;;;EASR,SAAS;;;;;;;EAST,SAAS;UAKD;;;;;;;EAgBR,UAAU;;;;;;;;;EAqBV,SAAS,eAAe,KAAK,QAAQ,WAAW,SAAS,WAAW;;;;;GAWlE,OAAO,aAAa,iBAAiB;;;;;EAWvC,WAAW,WAAW;;;;;EAQtB,SAAS,UAAU"}
|
package/dist/writer.mjs
CHANGED
|
@@ -25,6 +25,7 @@ var BcsWriter = class {
|
|
|
25
25
|
this.maxSize = maxSize;
|
|
26
26
|
this.allocateSize = allocateSize;
|
|
27
27
|
this.dataView = new DataView(new ArrayBuffer(initialSize));
|
|
28
|
+
this.bytes = new Uint8Array(this.dataView.buffer);
|
|
28
29
|
}
|
|
29
30
|
ensureSizeOrGrow(bytes) {
|
|
30
31
|
const requiredSize = this.bytePosition + bytes;
|
|
@@ -33,8 +34,10 @@ var BcsWriter = class {
|
|
|
33
34
|
if (requiredSize > nextSize) throw new Error(`Attempting to serialize to BCS, but buffer does not have enough size. Allocated size: ${this.size}, Max size: ${this.maxSize}, Required size: ${requiredSize}`);
|
|
34
35
|
this.size = nextSize;
|
|
35
36
|
const nextBuffer = new ArrayBuffer(this.size);
|
|
36
|
-
new Uint8Array(nextBuffer)
|
|
37
|
+
const nextBytes = new Uint8Array(nextBuffer);
|
|
38
|
+
nextBytes.set(this.bytes);
|
|
37
39
|
this.dataView = new DataView(nextBuffer);
|
|
40
|
+
this.bytes = nextBytes;
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
@@ -54,7 +57,7 @@ var BcsWriter = class {
|
|
|
54
57
|
*/
|
|
55
58
|
write8(value) {
|
|
56
59
|
this.ensureSizeOrGrow(1);
|
|
57
|
-
this.
|
|
60
|
+
this.bytes[this.bytePosition] = Number(value);
|
|
58
61
|
return this.shift(1);
|
|
59
62
|
}
|
|
60
63
|
/**
|
|
@@ -64,7 +67,7 @@ var BcsWriter = class {
|
|
|
64
67
|
*/
|
|
65
68
|
writeBytes(bytes) {
|
|
66
69
|
this.ensureSizeOrGrow(bytes.length);
|
|
67
|
-
|
|
70
|
+
this.bytes.set(bytes, this.bytePosition);
|
|
68
71
|
return this.shift(bytes.length);
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
@@ -93,8 +96,7 @@ var BcsWriter = class {
|
|
|
93
96
|
* @returns {this}
|
|
94
97
|
*/
|
|
95
98
|
write64(value) {
|
|
96
|
-
|
|
97
|
-
return this;
|
|
99
|
+
return this.writeBigLittleEndian(BigInt(value), 8);
|
|
98
100
|
}
|
|
99
101
|
/**
|
|
100
102
|
* Write a U128 value into a buffer and shift cursor position by 16.
|
|
@@ -103,8 +105,7 @@ var BcsWriter = class {
|
|
|
103
105
|
* @returns {this}
|
|
104
106
|
*/
|
|
105
107
|
write128(value) {
|
|
106
|
-
|
|
107
|
-
return this;
|
|
108
|
+
return this.writeBigLittleEndian(BigInt(value), 16);
|
|
108
109
|
}
|
|
109
110
|
/**
|
|
110
111
|
* Write a U256 value into a buffer and shift cursor position by 16.
|
|
@@ -113,8 +114,16 @@ var BcsWriter = class {
|
|
|
113
114
|
* @returns {this}
|
|
114
115
|
*/
|
|
115
116
|
write256(value) {
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
return this.writeBigLittleEndian(BigInt(value), 32);
|
|
118
|
+
}
|
|
119
|
+
writeBigLittleEndian(value, size) {
|
|
120
|
+
this.ensureSizeOrGrow(size);
|
|
121
|
+
let remaining = value > 0n ? value : 0n;
|
|
122
|
+
for (let offset = 0; offset < size; offset += 8) {
|
|
123
|
+
this.dataView.setBigUint64(this.bytePosition + offset, BigInt.asUintN(64, remaining), true);
|
|
124
|
+
remaining >>= 64n;
|
|
125
|
+
}
|
|
126
|
+
return this.shift(size);
|
|
118
127
|
}
|
|
119
128
|
/**
|
|
120
129
|
* Write a ULEB value into a buffer and shift cursor position by number of bytes
|
|
@@ -123,6 +132,14 @@ var BcsWriter = class {
|
|
|
123
132
|
* @returns {this}
|
|
124
133
|
*/
|
|
125
134
|
writeULEB(value) {
|
|
135
|
+
if (value >= 0 && Number.isSafeInteger(value)) {
|
|
136
|
+
let remaining = value;
|
|
137
|
+
while (remaining >= 128) {
|
|
138
|
+
this.write8(remaining % 128 | 128);
|
|
139
|
+
remaining = Math.floor(remaining / 128);
|
|
140
|
+
}
|
|
141
|
+
return this.write8(remaining);
|
|
142
|
+
}
|
|
126
143
|
ulebEncode(value).forEach((el) => this.write8(el));
|
|
127
144
|
return this;
|
|
128
145
|
}
|
|
@@ -162,16 +179,6 @@ var BcsWriter = class {
|
|
|
162
179
|
return encodeStr(this.toBytes(), encoding);
|
|
163
180
|
}
|
|
164
181
|
};
|
|
165
|
-
function toLittleEndian(bigint, size) {
|
|
166
|
-
const result = new Uint8Array(size);
|
|
167
|
-
let i = 0;
|
|
168
|
-
while (bigint > 0) {
|
|
169
|
-
result[i] = Number(bigint % BigInt(256));
|
|
170
|
-
bigint = bigint / BigInt(256);
|
|
171
|
-
i += 1;
|
|
172
|
-
}
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
182
|
//#endregion
|
|
176
183
|
export { BcsWriter };
|
|
177
184
|
|
package/dist/writer.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.mjs","names":[],"sources":["../src/writer.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type { Encoding } from './types.js';\nimport { ulebEncode } from './uleb.js';\nimport { encodeStr } from './utils.js';\n\nexport interface BcsWriterOptions {\n\t/** The initial size (in bytes) of the buffer tht will be allocated */\n\tinitialSize?: number;\n\t/** The maximum size (in bytes) that the buffer is allowed to grow to */\n\tmaxSize?: number;\n\t/** The amount of bytes that will be allocated whenever additional memory is required */\n\tallocateSize?: number;\n}\n\n/**\n * Class used to write BCS data into a buffer. Initializer requires\n * some size of a buffer to init; default value for this buffer is 1KB.\n *\n * Most methods are chainable, so it is possible to write them in one go.\n *\n * @example\n * let serialized = new BcsWriter()\n * .write8(10)\n * .write32(1000000)\n * .write64(10000001000000)\n * .hex();\n */\n\n/**\n * Set of methods that allows data encoding/decoding as standalone\n * BCS value or a part of a composed structure/vector.\n */\nexport class BcsWriter {\n\tprivate dataView: DataView<ArrayBuffer>;\n\tprivate bytePosition: number = 0;\n\tprivate size: number;\n\tprivate maxSize: number;\n\tprivate allocateSize: number;\n\n\tconstructor({\n\t\tinitialSize = 1024,\n\t\tmaxSize = Infinity,\n\t\tallocateSize = 1024,\n\t}: BcsWriterOptions = {}) {\n\t\tthis.size = initialSize;\n\t\tthis.maxSize = maxSize;\n\t\tthis.allocateSize = allocateSize;\n\t\tthis.dataView = new DataView(new ArrayBuffer(initialSize));\n\t}\n\n\tprivate ensureSizeOrGrow(bytes: number) {\n\t\tconst requiredSize = this.bytePosition + bytes;\n\t\tif (requiredSize > this.size) {\n\t\t\tconst nextSize = Math.min(\n\t\t\t\tthis.maxSize,\n\t\t\t\tMath.max(this.size + requiredSize, this.size + this.allocateSize),\n\t\t\t);\n\t\t\tif (requiredSize > nextSize) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Attempting to serialize to BCS, but buffer does not have enough size. Allocated size: ${this.size}, Max size: ${this.maxSize}, Required size: ${requiredSize}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.size = nextSize;\n\t\t\tconst nextBuffer = new ArrayBuffer(this.size);\n\t\t\
|
|
1
|
+
{"version":3,"file":"writer.mjs","names":[],"sources":["../src/writer.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type { Encoding } from './types.js';\nimport { ulebEncode } from './uleb.js';\nimport { encodeStr } from './utils.js';\n\nexport interface BcsWriterOptions {\n\t/** The initial size (in bytes) of the buffer tht will be allocated */\n\tinitialSize?: number;\n\t/** The maximum size (in bytes) that the buffer is allowed to grow to */\n\tmaxSize?: number;\n\t/** The amount of bytes that will be allocated whenever additional memory is required */\n\tallocateSize?: number;\n}\n\n/**\n * Class used to write BCS data into a buffer. Initializer requires\n * some size of a buffer to init; default value for this buffer is 1KB.\n *\n * Most methods are chainable, so it is possible to write them in one go.\n *\n * @example\n * let serialized = new BcsWriter()\n * .write8(10)\n * .write32(1000000)\n * .write64(10000001000000)\n * .hex();\n */\n\n/**\n * Set of methods that allows data encoding/decoding as standalone\n * BCS value or a part of a composed structure/vector.\n */\nexport class BcsWriter {\n\tprivate dataView: DataView<ArrayBuffer>;\n\tprivate bytes: Uint8Array<ArrayBuffer>;\n\tprivate bytePosition: number = 0;\n\tprivate size: number;\n\tprivate maxSize: number;\n\tprivate allocateSize: number;\n\n\tconstructor({\n\t\tinitialSize = 1024,\n\t\tmaxSize = Infinity,\n\t\tallocateSize = 1024,\n\t}: BcsWriterOptions = {}) {\n\t\tthis.size = initialSize;\n\t\tthis.maxSize = maxSize;\n\t\tthis.allocateSize = allocateSize;\n\t\tthis.dataView = new DataView(new ArrayBuffer(initialSize));\n\t\tthis.bytes = new Uint8Array(this.dataView.buffer);\n\t}\n\n\tprivate ensureSizeOrGrow(bytes: number) {\n\t\tconst requiredSize = this.bytePosition + bytes;\n\t\tif (requiredSize > this.size) {\n\t\t\tconst nextSize = Math.min(\n\t\t\t\tthis.maxSize,\n\t\t\t\tMath.max(this.size + requiredSize, this.size + this.allocateSize),\n\t\t\t);\n\t\t\tif (requiredSize > nextSize) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Attempting to serialize to BCS, but buffer does not have enough size. Allocated size: ${this.size}, Max size: ${this.maxSize}, Required size: ${requiredSize}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.size = nextSize;\n\t\t\tconst nextBuffer = new ArrayBuffer(this.size);\n\t\t\tconst nextBytes = new Uint8Array(nextBuffer);\n\t\t\tnextBytes.set(this.bytes);\n\t\t\tthis.dataView = new DataView(nextBuffer);\n\t\t\tthis.bytes = nextBytes;\n\t\t}\n\t}\n\n\t/**\n\t * Shift current cursor position by `bytes`.\n\t *\n\t * @param {Number} bytes Number of bytes to\n\t * @returns {this} Self for possible chaining.\n\t */\n\tshift(bytes: number): this {\n\t\tthis.bytePosition += bytes;\n\t\treturn this;\n\t}\n\t/**\n\t * Write a U8 value into a buffer and shift cursor position by 1.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite8(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(1);\n\t\tthis.bytes[this.bytePosition] = Number(value);\n\t\treturn this.shift(1);\n\t}\n\n\t/**\n\t * Write a U8 value into a buffer and shift cursor position by 1.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twriteBytes(bytes: Uint8Array): this {\n\t\tthis.ensureSizeOrGrow(bytes.length);\n\t\tthis.bytes.set(bytes, this.bytePosition);\n\n\t\treturn this.shift(bytes.length);\n\t}\n\t/**\n\t * Write a U16 value into a buffer and shift cursor position by 2.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite16(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(2);\n\t\tthis.dataView.setUint16(this.bytePosition, Number(value), true);\n\t\treturn this.shift(2);\n\t}\n\t/**\n\t * Write a U32 value into a buffer and shift cursor position by 4.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twrite32(value: number | bigint): this {\n\t\tthis.ensureSizeOrGrow(4);\n\t\tthis.dataView.setUint32(this.bytePosition, Number(value), true);\n\t\treturn this.shift(4);\n\t}\n\t/**\n\t * Write a U64 value into a buffer and shift cursor position by 8.\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite64(value: number | bigint): this {\n\t\treturn this.writeBigLittleEndian(BigInt(value), 8);\n\t}\n\t/**\n\t * Write a U128 value into a buffer and shift cursor position by 16.\n\t *\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite128(value: number | bigint): this {\n\t\treturn this.writeBigLittleEndian(BigInt(value), 16);\n\t}\n\t/**\n\t * Write a U256 value into a buffer and shift cursor position by 16.\n\t *\n\t * @param {bigint} value Value to write.\n\t * @returns {this}\n\t */\n\twrite256(value: number | bigint): this {\n\t\treturn this.writeBigLittleEndian(BigInt(value), 32);\n\t}\n\n\t// Negative values encode as zero and values that overflow `size` keep their low bytes\n\tprivate writeBigLittleEndian(value: bigint, size: number): this {\n\t\tthis.ensureSizeOrGrow(size);\n\t\tlet remaining = value > 0n ? value : 0n;\n\t\tfor (let offset = 0; offset < size; offset += 8) {\n\t\t\tthis.dataView.setBigUint64(this.bytePosition + offset, BigInt.asUintN(64, remaining), true);\n\t\t\tremaining >>= 64n;\n\t\t}\n\n\t\treturn this.shift(size);\n\t}\n\t/**\n\t * Write a ULEB value into a buffer and shift cursor position by number of bytes\n\t * written.\n\t * @param {Number} value Value to write.\n\t * @returns {this}\n\t */\n\twriteULEB(value: number): this {\n\t\tif (value >= 0 && Number.isSafeInteger(value)) {\n\t\t\tlet remaining = value;\n\t\t\twhile (remaining >= 0x80) {\n\t\t\t\tthis.write8((remaining % 0x80) | 0x80);\n\t\t\t\tremaining = Math.floor(remaining / 0x80);\n\t\t\t}\n\t\t\treturn this.write8(remaining);\n\t\t}\n\n\t\tulebEncode(value).forEach((el) => this.write8(el));\n\t\treturn this;\n\t}\n\t/**\n\t * Write a vector into a buffer by first writing the vector length and then calling\n\t * a callback on each passed value.\n\t *\n\t * @param {Array<Any>} vector Array of elements to write.\n\t * @param {WriteVecCb} cb Callback to call on each element of the vector.\n\t * @returns {this}\n\t */\n\twriteVec(vector: any[], cb: (writer: BcsWriter, el: any, i: number, len: number) => void): this {\n\t\tthis.writeULEB(vector.length);\n\t\tArray.from(vector).forEach((el, i) => cb(this, el, i, vector.length));\n\t\treturn this;\n\t}\n\n\t/**\n\t * Adds support for iterations over the object.\n\t * @returns {Uint8Array}\n\t */\n\t// oxlint-disable-next-line require-yields\n\t*[Symbol.iterator](): Iterator<number, Iterable<number>> {\n\t\tfor (let i = 0; i < this.bytePosition; i++) {\n\t\t\tyield this.dataView.getUint8(i);\n\t\t}\n\t\treturn this.toBytes();\n\t}\n\n\t/**\n\t * Get underlying buffer taking only value bytes (in case initial buffer size was bigger).\n\t * @returns {Uint8Array} Resulting bcs.\n\t */\n\ttoBytes(): Uint8Array<ArrayBuffer> {\n\t\treturn new Uint8Array(this.dataView.buffer.slice(0, this.bytePosition));\n\t}\n\n\t/**\n\t * Represent data as 'hex' or 'base64'\n\t * @param encoding Encoding to use: 'base64' or 'hex'\n\t */\n\ttoString(encoding: Encoding): string {\n\t\treturn encodeStr(this.toBytes(), encoding);\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haneullabs/bcs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "BCS - Canonical Binary Serialization implementation for JavaScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Haneul Labs <build@haneul-labs.com>",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@scure/base": "^2.3.0",
|
|
54
|
-
"@haneullabs/utils": "^0.4.
|
|
54
|
+
"@haneullabs/utils": "^0.4.2"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"clean": "rm -rf tsconfig.tsbuildinfo ./dist",
|
package/src/bcs-type.ts
CHANGED
|
@@ -22,6 +22,12 @@ export class BcsType<T, Input = T, const Name extends string = string> {
|
|
|
22
22
|
validate: (value: Input) => void;
|
|
23
23
|
#write: (value: Input, writer: BcsWriter) => void;
|
|
24
24
|
#serialize: (value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>;
|
|
25
|
+
// Set by transform: validates and writes while converting the input only once. Only used
|
|
26
|
+
// while validate is still the one transform created, so a reassigned validate is honoured.
|
|
27
|
+
#validatedWrite: ((value: Input, writer: BcsWriter) => void) | null = null;
|
|
28
|
+
#validatedSerialize:
|
|
29
|
+
((value: Input, options?: BcsWriterOptions) => Uint8Array<ArrayBuffer>) | null = null;
|
|
30
|
+
#transformValidate: ((value: Input) => void) | null = null;
|
|
25
31
|
|
|
26
32
|
constructor(
|
|
27
33
|
options: {
|
|
@@ -52,11 +58,18 @@ export class BcsType<T, Input = T, const Name extends string = string> {
|
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
write(value: Input, writer: BcsWriter) {
|
|
61
|
+
if (this.#validatedWrite && this.validate === this.#transformValidate) {
|
|
62
|
+
this.#validatedWrite(value, writer);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
55
65
|
this.validate(value);
|
|
56
66
|
this.#write(value, writer);
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
serialize(value: Input, options?: BcsWriterOptions) {
|
|
70
|
+
if (this.#validatedSerialize && this.validate === this.#transformValidate) {
|
|
71
|
+
return new SerializedBcs(this, this.#validatedSerialize(value, options));
|
|
72
|
+
}
|
|
60
73
|
this.validate(value);
|
|
61
74
|
return new SerializedBcs(this, this.#serialize(value, options));
|
|
62
75
|
}
|
|
@@ -87,7 +100,7 @@ export class BcsType<T, Input = T, const Name extends string = string> {
|
|
|
87
100
|
input?: (val: Input2) => Input;
|
|
88
101
|
output?: (value: T) => T2;
|
|
89
102
|
} & BcsTypeOptions<T2, Input2, NewName>) {
|
|
90
|
-
|
|
103
|
+
const type = new BcsType<T2, Input2, NewName>({
|
|
91
104
|
name: (name ?? this.name) as NewName,
|
|
92
105
|
read: (reader) => (output ? output(this.read(reader)) : (this.read(reader) as never)),
|
|
93
106
|
write: (value, writer) => this.#write(input ? input(value) : (value as never), writer),
|
|
@@ -99,6 +112,23 @@ export class BcsType<T, Input = T, const Name extends string = string> {
|
|
|
99
112
|
this.validate(input ? input(value) : (value as never));
|
|
100
113
|
},
|
|
101
114
|
});
|
|
115
|
+
|
|
116
|
+
// Equivalent to validate() followed by the write above, but calls input() once instead of twice
|
|
117
|
+
type.#transformValidate = type.validate;
|
|
118
|
+
type.#validatedWrite = (value, writer) => {
|
|
119
|
+
validate?.(value);
|
|
120
|
+
const converted = input ? input(value) : (value as never);
|
|
121
|
+
this.validate(converted);
|
|
122
|
+
this.#write(converted, writer);
|
|
123
|
+
};
|
|
124
|
+
type.#validatedSerialize = (value, options) => {
|
|
125
|
+
validate?.(value);
|
|
126
|
+
const converted = input ? input(value) : (value as never);
|
|
127
|
+
this.validate(converted);
|
|
128
|
+
return this.#serialize(converted, options);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return type;
|
|
102
132
|
}
|
|
103
133
|
}
|
|
104
134
|
|
|
@@ -253,9 +283,7 @@ export function stringLikeBcsType<const Name extends string = string>({
|
|
|
253
283
|
write: (hex, writer) => {
|
|
254
284
|
const bytes = toBytes(hex);
|
|
255
285
|
writer.writeULEB(bytes.length);
|
|
256
|
-
|
|
257
|
-
writer.write8(bytes[i]);
|
|
258
|
-
}
|
|
286
|
+
writer.writeBytes(bytes);
|
|
259
287
|
},
|
|
260
288
|
serialize: (value) => {
|
|
261
289
|
const bytes = toBytes(value);
|
|
@@ -325,14 +353,16 @@ export class BcsStruct<
|
|
|
325
353
|
Name
|
|
326
354
|
> {
|
|
327
355
|
constructor({ name, fields, ...options }: BcsStructOptions<T, Name>) {
|
|
328
|
-
const
|
|
356
|
+
const fieldNames = Object.keys(fields);
|
|
357
|
+
const fieldTypes = fieldNames.map((field) => fields[field]);
|
|
358
|
+
const fieldCount = fieldNames.length;
|
|
329
359
|
|
|
330
360
|
super({
|
|
331
361
|
name,
|
|
332
362
|
serializedSize: (values) => {
|
|
333
363
|
let total = 0;
|
|
334
|
-
for (
|
|
335
|
-
const size =
|
|
364
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
365
|
+
const size = fieldTypes[i].serializedSize(values[fieldNames[i]]);
|
|
336
366
|
if (size == null) {
|
|
337
367
|
return null;
|
|
338
368
|
}
|
|
@@ -344,15 +374,15 @@ export class BcsStruct<
|
|
|
344
374
|
},
|
|
345
375
|
read: (reader) => {
|
|
346
376
|
const result: Record<string, unknown> = {};
|
|
347
|
-
for (
|
|
348
|
-
result[
|
|
377
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
378
|
+
result[fieldNames[i]] = fieldTypes[i].read(reader);
|
|
349
379
|
}
|
|
350
380
|
|
|
351
381
|
return result as never;
|
|
352
382
|
},
|
|
353
383
|
write: (value, writer) => {
|
|
354
|
-
for (
|
|
355
|
-
|
|
384
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
385
|
+
fieldTypes[i].write(value[fieldNames[i]], writer);
|
|
356
386
|
}
|
|
357
387
|
},
|
|
358
388
|
...options,
|
|
@@ -399,6 +429,7 @@ export class BcsEnum<
|
|
|
399
429
|
> {
|
|
400
430
|
constructor({ fields, ...options }: BcsEnumOptions<T, Name>) {
|
|
401
431
|
const canonicalOrder = Object.entries(fields as object);
|
|
432
|
+
const variantIndex = new Map(canonicalOrder.map(([name], i) => [name, i]));
|
|
402
433
|
super({
|
|
403
434
|
read: (reader) => {
|
|
404
435
|
const index = reader.readULEB();
|
|
@@ -410,21 +441,26 @@ export class BcsEnum<
|
|
|
410
441
|
|
|
411
442
|
const [kind, type] = enumEntry;
|
|
412
443
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
444
|
+
const value = type?.read(reader) ?? true;
|
|
445
|
+
// Assigning to __proto__ would set the prototype rather than add a property
|
|
446
|
+
if (kind === '__proto__') {
|
|
447
|
+
return { [kind]: value, $kind: kind } as never;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Avoid a computed-key object literal, which V8 builds through a slow runtime path
|
|
451
|
+
const result: Record<string, unknown> = {};
|
|
452
|
+
result[kind] = value;
|
|
453
|
+
result.$kind = kind;
|
|
454
|
+
|
|
455
|
+
return result as never;
|
|
417
456
|
},
|
|
418
457
|
write: (value, writer) => {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (optionName === name) {
|
|
426
|
-
writer.writeULEB(i);
|
|
427
|
-
optionType?.write(val, writer);
|
|
458
|
+
// The first own key that names a variant selects it
|
|
459
|
+
for (const name of Object.keys(value)) {
|
|
460
|
+
const index = variantIndex.get(name);
|
|
461
|
+
if (index !== undefined) {
|
|
462
|
+
writer.writeULEB(index);
|
|
463
|
+
canonicalOrder[index][1]?.write(value[name], writer);
|
|
428
464
|
return;
|
|
429
465
|
}
|
|
430
466
|
}
|
|
@@ -436,21 +472,18 @@ export class BcsEnum<
|
|
|
436
472
|
throw new TypeError(`Expected object, found ${typeof value}`);
|
|
437
473
|
}
|
|
438
474
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
475
|
+
let variantCount = 0;
|
|
476
|
+
for (const key of Object.keys(value)) {
|
|
477
|
+
if (value[key] !== undefined && variantIndex.has(key)) {
|
|
478
|
+
variantCount++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
442
481
|
|
|
443
|
-
if (
|
|
482
|
+
if (variantCount !== 1) {
|
|
444
483
|
throw new TypeError(
|
|
445
|
-
`Expected object with one key, but found ${
|
|
484
|
+
`Expected object with one key, but found ${variantCount} for type ${options.name}}`,
|
|
446
485
|
);
|
|
447
486
|
}
|
|
448
|
-
|
|
449
|
-
const [variant] = keys;
|
|
450
|
-
|
|
451
|
-
if (!Object.hasOwn(fields, variant)) {
|
|
452
|
-
throw new TypeError(`Invalid enum variant ${variant}`);
|
|
453
|
-
}
|
|
454
487
|
},
|
|
455
488
|
});
|
|
456
489
|
}
|
package/src/bcs.ts
CHANGED
|
@@ -82,27 +82,34 @@ function option<T, Input, Name extends string = string>(
|
|
|
82
82
|
function option<T extends BcsType<any>>(
|
|
83
83
|
type: T,
|
|
84
84
|
): BcsType<InferBcsType<T> | null, InferBcsInput<T> | null | undefined, `Option<${T['name']}>`> {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
output: (value) => {
|
|
99
|
-
if (value.$kind === 'Some') {
|
|
100
|
-
return value.Some as InferBcsType<T>;
|
|
101
|
-
}
|
|
102
|
-
|
|
85
|
+
// Encoded like enum Option { None, Some(T) }, but read and written directly rather than
|
|
86
|
+
// through an intermediate { None } / { Some } enum object
|
|
87
|
+
const name = `Option<${type.name}>` as const;
|
|
88
|
+
return new BcsType<
|
|
89
|
+
InferBcsType<T> | null,
|
|
90
|
+
InferBcsInput<T> | null | undefined,
|
|
91
|
+
`Option<${T['name']}>`
|
|
92
|
+
>({
|
|
93
|
+
name,
|
|
94
|
+
read: (reader) => {
|
|
95
|
+
const index = reader.readULEB();
|
|
96
|
+
if (index === 0) {
|
|
103
97
|
return null;
|
|
104
|
-
}
|
|
105
|
-
|
|
98
|
+
}
|
|
99
|
+
if (index === 1) {
|
|
100
|
+
return type.read(reader);
|
|
101
|
+
}
|
|
102
|
+
throw new TypeError(`Unknown value ${index} for enum ${name}`);
|
|
103
|
+
},
|
|
104
|
+
write: (value, writer) => {
|
|
105
|
+
if (value == null) {
|
|
106
|
+
writer.write8(0);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
writer.write8(1);
|
|
110
|
+
type.write(value, writer);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
function vector<T extends BcsType<any>, Name extends string = `vector<${T['name']}>`>(
|
|
@@ -211,6 +218,32 @@ function map<K extends BcsType<any>, V extends BcsType<any>>(
|
|
|
211
218
|
});
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
// Created on first use, so importing the package doesn't require TextEncoder/TextDecoder
|
|
222
|
+
let utf8Encoder: TextEncoder | undefined;
|
|
223
|
+
let utf8Decoder: TextDecoder | undefined;
|
|
224
|
+
function decoder() {
|
|
225
|
+
return (utf8Decoder ??= new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Most strings in Haneul data (module, function and type names) are short ASCII. Decoding those
|
|
229
|
+
// directly avoids the fixed cost of a TextDecoder call; anything else goes through the decoder.
|
|
230
|
+
const ASCII_FAST_PATH_MAX_LENGTH = 64;
|
|
231
|
+
function decodeUtf8(bytes: Uint8Array): string {
|
|
232
|
+
if (bytes.length <= ASCII_FAST_PATH_MAX_LENGTH) {
|
|
233
|
+
let result = '';
|
|
234
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
235
|
+
const byte = bytes[i];
|
|
236
|
+
if (byte >= 0x80) {
|
|
237
|
+
return decoder().decode(bytes);
|
|
238
|
+
}
|
|
239
|
+
result += String.fromCharCode(byte);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return decoder().decode(bytes);
|
|
245
|
+
}
|
|
246
|
+
|
|
214
247
|
export const bcs = {
|
|
215
248
|
/**
|
|
216
249
|
* Creates a BcsType that can be used to read and write an 8-bit unsigned integer.
|
|
@@ -362,7 +395,7 @@ export const bcs = {
|
|
|
362
395
|
size,
|
|
363
396
|
read: (reader) => reader.readBytes(size),
|
|
364
397
|
write: (value, writer) => {
|
|
365
|
-
writer.writeBytes(new Uint8Array(value));
|
|
398
|
+
writer.writeBytes(value instanceof Uint8Array ? value : new Uint8Array(value));
|
|
366
399
|
},
|
|
367
400
|
...options,
|
|
368
401
|
name: (options?.name ?? `bytes[${size}]`) as `bytes[${T}]`,
|
|
@@ -392,7 +425,7 @@ export const bcs = {
|
|
|
392
425
|
return reader.readBytes(length);
|
|
393
426
|
},
|
|
394
427
|
write: (value, writer) => {
|
|
395
|
-
const array = new Uint8Array(value);
|
|
428
|
+
const array = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
396
429
|
writer.writeULEB(array.length);
|
|
397
430
|
writer.writeBytes(array);
|
|
398
431
|
},
|
|
@@ -418,9 +451,8 @@ export const bcs = {
|
|
|
418
451
|
*/
|
|
419
452
|
string(options?: BcsTypeOptions<string>) {
|
|
420
453
|
return stringLikeBcsType({
|
|
421
|
-
toBytes: (value) => new TextEncoder().encode(value),
|
|
422
|
-
fromBytes:
|
|
423
|
-
new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes),
|
|
454
|
+
toBytes: (value) => (utf8Encoder ??= new TextEncoder()).encode(value),
|
|
455
|
+
fromBytes: decodeUtf8,
|
|
424
456
|
...options,
|
|
425
457
|
name: (options?.name ?? 'string') as 'string',
|
|
426
458
|
});
|
package/src/reader.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Mysten Labs, Inc.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { ulebDecode } from './uleb.js';
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* Class used for reading BCS data chunk by chunk. Meant to be used
|
|
8
6
|
* by some wrapper, which will make sure that data is valid and is
|
|
@@ -35,6 +33,7 @@ import { ulebDecode } from './uleb.js';
|
|
|
35
33
|
*/
|
|
36
34
|
export class BcsReader {
|
|
37
35
|
private dataView: DataView;
|
|
36
|
+
private bytes: Uint8Array;
|
|
38
37
|
private bytePosition: number = 0;
|
|
39
38
|
|
|
40
39
|
/**
|
|
@@ -42,6 +41,7 @@ export class BcsReader {
|
|
|
42
41
|
*/
|
|
43
42
|
constructor(data: Uint8Array) {
|
|
44
43
|
this.dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
44
|
+
this.bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Shift current cursor position by `bytes`.
|
|
@@ -58,9 +58,12 @@ export class BcsReader {
|
|
|
58
58
|
* @returns
|
|
59
59
|
*/
|
|
60
60
|
read8(): number {
|
|
61
|
-
const
|
|
62
|
-
this.
|
|
63
|
-
|
|
61
|
+
const position = this.bytePosition;
|
|
62
|
+
if (position >= this.bytes.length) {
|
|
63
|
+
throw new RangeError('Offset is outside the bounds of the DataView');
|
|
64
|
+
}
|
|
65
|
+
this.bytePosition = position + 1;
|
|
66
|
+
return this.bytes[position];
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
66
69
|
* Read U16 value from the buffer and shift cursor by 2.
|
|
@@ -85,12 +88,9 @@ export class BcsReader {
|
|
|
85
88
|
* @returns
|
|
86
89
|
*/
|
|
87
90
|
read64(): string {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const result = value2.toString(16) + value1.toString(16).padStart(8, '0');
|
|
92
|
-
|
|
93
|
-
return BigInt('0x' + result).toString(10);
|
|
91
|
+
const value = this.dataView.getBigUint64(this.bytePosition, true);
|
|
92
|
+
this.shift(8);
|
|
93
|
+
return value.toString(10);
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Read U128 value from the buffer and shift cursor by 16.
|
|
@@ -118,6 +118,10 @@ export class BcsReader {
|
|
|
118
118
|
* @param num Number of bytes to read.
|
|
119
119
|
*/
|
|
120
120
|
readBytes(num: number): Uint8Array {
|
|
121
|
+
// The underlying buffer may extend past this view, so check against the view's own length
|
|
122
|
+
if (this.bytePosition + num > this.bytes.length) {
|
|
123
|
+
throw new RangeError('Offset is outside the bounds of the DataView');
|
|
124
|
+
}
|
|
121
125
|
const start = this.bytePosition + this.dataView.byteOffset;
|
|
122
126
|
const value = new Uint8Array(this.dataView.buffer, start, num);
|
|
123
127
|
|
|
@@ -131,12 +135,31 @@ export class BcsReader {
|
|
|
131
135
|
* @returns {Number} The ULEB value.
|
|
132
136
|
*/
|
|
133
137
|
readULEB(): number {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
const bytes = this.bytes;
|
|
139
|
+
let position = this.bytePosition;
|
|
140
|
+
let value = 0;
|
|
141
|
+
let multiplier = 1;
|
|
142
|
+
while (true) {
|
|
143
|
+
if (position >= bytes.length) {
|
|
144
|
+
throw new Error('ULEB decode error: buffer overflow');
|
|
145
|
+
}
|
|
146
|
+
const byte = bytes[position++];
|
|
147
|
+
value += (byte & 0x7f) * multiplier;
|
|
148
|
+
if ((byte & 0x80) === 0) {
|
|
149
|
+
// A trailing zero group adds no value, so the encoding is not minimal
|
|
150
|
+
if (byte === 0 && position - this.bytePosition > 1) {
|
|
151
|
+
throw new Error('ULEB decode error: non-canonical encoding');
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
multiplier *= 128;
|
|
156
|
+
}
|
|
139
157
|
|
|
158
|
+
// Also rejects NaN, from very long encodings where multiplier overflows to Infinity
|
|
159
|
+
if (!(value <= Number.MAX_SAFE_INTEGER)) {
|
|
160
|
+
throw new Error('ULEB decode error: value exceeds MAX_SAFE_INTEGER');
|
|
161
|
+
}
|
|
162
|
+
this.bytePosition = position;
|
|
140
163
|
return value;
|
|
141
164
|
}
|
|
142
165
|
/**
|