@aptos-labs/ts-sdk
Version:
Aptos TypeScript SDK
485 lines • 19.2 kB
JavaScript
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0
import { MAX_U32_NUMBER } from "./consts.js";
import { Hex } from "../core/hex.js";
/**
* Shared TextDecoder instance for string deserialization to avoid repeated instantiation.
*/
const TEXT_DECODER = new TextDecoder();
/**
* Maximum allowed length for deserialized byte arrays and strings.
* This prevents memory exhaustion attacks from malformed BCS data.
* Set to 10MB which should be sufficient for any legitimate use case.
*/
const MAX_DESERIALIZE_BYTES_LENGTH = 10 * 1024 * 1024; // 10MB
/**
* Maximum allowed vector element count. Without this cap a malformed BCS blob
* with a ULEB128 length close to `MAX_U32_NUMBER` (~4.29 billion) would cause
* `deserializeVector` to spin for billions of iterations before the inner
* `read()` bounds check trips, effectively a CPU-exhaustion DoS. Matches the
* canonical `MAX_SEQUENCE_LENGTH = 2^31 - 1` bound used elsewhere in BCS.
*
* Note: written as `2 ** 31 - 1` rather than `(1 << 31) - 1` because the
* bitwise shift operates on 32-bit *signed* integers in JavaScript, which
* would produce a negative value here.
*/
const MAX_DESERIALIZE_VECTOR_LENGTH = 2 ** 31 - 1;
/**
* A class that provides methods for deserializing various data types from a byte buffer.
* It supports deserialization of primitive types, strings, and complex objects using a BCS (Binary Common Serialization) layout.
* @group Implementation
* @category BCS
*/
export class Deserializer {
buffer;
offset;
/**
* Creates a new instance of the class with a copy of the provided data buffer.
* This prevents outside mutation of the buffer.
*
* @param data - The data to be copied into the internal buffer as a Uint8Array.
* @group Implementation
* @category BCS
*/
constructor(data) {
// copies data to prevent outside mutation of buffer.
this.buffer = new ArrayBuffer(data.length);
new Uint8Array(this.buffer).set(data, 0);
this.offset = 0;
}
static fromHex(hex) {
const data = Hex.hexInputToUint8Array(hex);
return new Deserializer(data);
}
/**
* Reads a specified number of bytes from the buffer and advances the offset.
* Returns a view into the buffer rather than copying for better performance.
*
* SECURITY NOTE: This returns a view, not a copy. Callers that expose the result
* externally MUST call .slice() to create a copy. Internal numeric deserializers
* (deserializeU8, deserializeU16, etc.) only read values and don't expose the view.
* deserializeBytes() and deserializeFixedBytes() call .slice() before returning.
*
* @param length - The number of bytes to read from the buffer.
* @throws Throws an error if the read operation exceeds the buffer's length.
* @group Implementation
* @category BCS
*/
read(length) {
if (this.offset + length > this.buffer.byteLength) {
throw new Error("Reached to the end of buffer");
}
// Use subarray to return a view instead of slice which copies
// SECURITY: View is safe because numeric deserializers only read values,
// and byte deserializers call .slice() before returning to caller
const bytes = new Uint8Array(this.buffer, this.offset, length);
this.offset += length;
return bytes;
}
/**
* Returns the number of bytes remaining in the buffer.
*
* This information is useful to determine if there's more data to be read.
*
* @returns The number of bytes remaining in the buffer.
* @group Implementation
* @category BCS
*/
remaining() {
return this.buffer.byteLength - this.offset;
}
/**
* Asserts that the buffer has no remaining bytes.
*
* @throws {Error} Throws an error if there are remaining bytes in the buffer.
* @group Implementation
* @category BCS
*/
assertFinished() {
if (this.remaining() !== 0) {
throw new Error("Buffer has remaining bytes");
}
}
/**
* Deserializes a UTF-8 encoded string from a byte array. It first reads the length of the string in bytes,
* followed by the actual byte content, and decodes it into a string.
*
* BCS layout for "string": string_length | string_content
* where string_length is a u32 integer encoded as a uleb128 integer, equal to the number of bytes in string_content.
*
* @example
* ```typescript
* const deserializer = new Deserializer(new Uint8Array([8, 49, 50, 51, 52, 97, 98, 99, 100]));
* assert(deserializer.deserializeStr() === "1234abcd");
* ```
* @group Implementation
* @category BCS
*/
deserializeStr() {
const value = this.deserializeBytes();
return TEXT_DECODER.decode(value);
}
/**
* @deprecated use `deserializeOption("string")` instead.
*
* The BCS layout for Optional<String> is 0 if none, else 1 followed by the string length and string content.
* @returns The deserialized string if it exists, otherwise undefined.
* @example
* ```typescript
* const deserializer = new Deserializer(new Uint8Array([0x00]));
* assert(deserializer.deserializeOptionStr() === undefined);
* const deserializer = new Deserializer(new Uint8Array([1, 8, 49, 50, 51, 52, 97, 98, 99, 100]));
* assert(deserializer.deserializeOptionStr() === "1234abcd");
* ```
*/
deserializeOptionStr() {
return this.deserializeOption("string");
}
deserializeOption(type, len) {
const exists = this.deserializeBool();
if (!exists)
return undefined;
if (type === "string") {
return this.deserializeStr();
}
if (type === "bytes") {
return this.deserializeBytes();
}
if (type === "fixedBytes") {
if (len === undefined) {
throw new Error("Fixed bytes length not provided");
}
return this.deserializeFixedBytes(len);
}
return this.deserialize(type);
}
/**
* Deserializes an array of bytes.
*
* The BCS layout for "bytes" consists of a bytes_length followed by the bytes themselves, where bytes_length is a u32 integer
* encoded as a uleb128 integer, indicating the length of the bytes array.
*
* @returns {Uint8Array} The deserialized array of bytes (a copy, safe to modify).
* @throws {Error} If the length exceeds the maximum allowed (10MB) to prevent memory exhaustion.
* @group Implementation
* @category BCS
*/
deserializeBytes() {
const len = this.deserializeUleb128AsU32();
// Security: Prevent memory exhaustion from malformed data
if (len > MAX_DESERIALIZE_BYTES_LENGTH) {
throw new Error(`Deserialization error: byte array length ${len} exceeds maximum allowed ${MAX_DESERIALIZE_BYTES_LENGTH}`);
}
// Return a copy so caller can safely modify without affecting buffer
return this.read(len).slice();
}
/**
* Deserializes an array of bytes of a specified length.
*
* @param len - The number of bytes to read from the source.
* @returns {Uint8Array} The deserialized array of bytes (a copy, safe to modify).
* @group Implementation
* @category BCS
*/
deserializeFixedBytes(len) {
// Return a copy so caller can safely modify without affecting buffer
return this.read(len).slice();
}
/**
* Deserializes a boolean value from a byte stream.
*
* The BCS layout for a boolean uses one byte, where "0x01" represents true and "0x00" represents false.
* An error is thrown if the byte value is not valid.
*
* @returns The deserialized boolean value.
* @throws Throws an error if the boolean value is invalid.
* @group Implementation
* @category BCS
*/
deserializeBool() {
const bool = this.read(1)[0];
if (bool !== 1 && bool !== 0) {
throw new Error("Invalid boolean value");
}
return bool === 1;
}
/**
* Deserializes a uint8 number from the binary data.
*
* BCS layout for "uint8": One byte. Binary format in little-endian representation.
*
* @returns {number} The deserialized uint8 number.
* @group Implementation
* @category BCS
*/
deserializeU8() {
return this.read(1)[0];
}
/**
* Deserializes a uint16 number from a binary format in little-endian representation.
*
* BCS layout for "uint16": Two bytes.
* @example
* ```typescript
* const deserializer = new Deserializer(new Uint8Array([0x34, 0x12]));
* assert(deserializer.deserializeU16() === 4660);
* ```
* @group Implementation
* @category BCS
*/
deserializeU16() {
const bytes = this.read(2);
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(0, true);
}
/**
* Deserializes a uint32 number from a binary format in little-endian representation.
*
* BCS layout for "uint32": Four bytes.
* @example
* ```typescript
* const deserializer = new Deserializer(new Uint8Array([0x78, 0x56, 0x34, 0x12]));
* assert(deserializer.deserializeU32() === 305419896);
* ```
* @group Implementation
* @category BCS
*/
deserializeU32() {
const bytes = this.read(4);
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true);
}
/**
* Deserializes a uint64 number.
*
* This function combines two 32-bit values to return a 64-bit unsigned integer in little-endian representation.
* @example
* ```typescript
* const deserializer = new Deserializer(new Uint8Array([0x00, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12]));
* assert(deserializer.deserializeU64() === 1311768467750121216);
* ```
* @group Implementation
* @category BCS
*/
deserializeU64() {
const low = this.deserializeU32();
const high = this.deserializeU32();
// combine the two 32-bit values and return (little endian)
return BigInt((BigInt(high) << BigInt(32)) | BigInt(low));
}
/**
* Deserializes a uint128 number from its binary representation.
* This function combines two 64-bit values to return a single uint128 value in little-endian format.
*
* @returns {BigInt} The deserialized uint128 number.
* @group Implementation
* @category BCS
*/
deserializeU128() {
const low = this.deserializeU64();
const high = this.deserializeU64();
// combine the two 64-bit values and return (little endian)
return BigInt((high << BigInt(64)) | low);
}
/**
* Deserializes a uint256 number from its binary representation.
*
* The BCS layout for "uint256" consists of thirty-two bytes in little-endian format.
*
* @returns {BigInt} The deserialized uint256 number.
* @group Implementation
* @category BCS
*/
deserializeU256() {
const low = this.deserializeU128();
const high = this.deserializeU128();
// combine the two 128-bit values and return (little endian)
return BigInt((high << BigInt(128)) | low);
}
/**
* Deserializes an 8-bit signed integer from the binary data.
* BCS layout for "int8": One byte. Binary format in little-endian representation.
*
* @returns {number} The deserialized int8 number.
* @group Implementation
* @category BCS
*/
deserializeI8() {
const bytes = this.read(1);
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt8(0);
}
/**
* Deserializes a 16-bit signed integer from a binary format in little-endian representation.
* BCS layout for "int16": Two bytes.
*
* @returns {number} The deserialized int16 number.
* @group Implementation
* @category BCS
*/
deserializeI16() {
const bytes = this.read(2);
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt16(0, true);
}
/**
* Deserializes a 32-bit signed integer from a binary format in little-endian representation.
* BCS layout for "int32": Four bytes.
*
* @returns {number} The deserialized int32 number.
* @group Implementation
* @category BCS
*/
deserializeI32() {
const bytes = this.read(4);
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt32(0, true);
}
/**
* Deserializes a 64-bit signed integer.
* This function combines two 32-bit values to return a 64-bit signed integer in little-endian representation.
*
* @returns {bigint} The deserialized int64 number.
* @group Implementation
* @category BCS
*/
deserializeI64() {
const low = this.deserializeU32();
const high = this.deserializeU32();
// combine the two 32-bit values (little endian)
const unsigned = BigInt((BigInt(high) << BigInt(32)) | BigInt(low));
// Convert from unsigned to signed using two's complement
const signBit = BigInt(1) << BigInt(63);
if (unsigned >= signBit) {
return unsigned - (BigInt(1) << BigInt(64));
}
return unsigned;
}
/**
* Deserializes a 128-bit signed integer from its binary representation.
* This function combines two 64-bit values to return a single int128 value in little-endian format.
*
* @returns {bigint} The deserialized int128 number.
* @group Implementation
* @category BCS
*/
deserializeI128() {
const low = this.deserializeU64();
const high = this.deserializeU64();
// combine the two 64-bit values (little endian)
const unsigned = BigInt((high << BigInt(64)) | low);
// Convert from unsigned to signed using two's complement
const signBit = BigInt(1) << BigInt(127);
if (unsigned >= signBit) {
return unsigned - (BigInt(1) << BigInt(128));
}
return unsigned;
}
/**
* Deserializes a 256-bit signed integer from its binary representation.
* BCS layout for "int256": Thirty-two bytes in little-endian format.
*
* @returns {bigint} The deserialized int256 number.
* @group Implementation
* @category BCS
*/
deserializeI256() {
const low = this.deserializeU128();
const high = this.deserializeU128();
// combine the two 128-bit values (little endian)
const unsigned = BigInt((high << BigInt(128)) | low);
// Convert from unsigned to signed using two's complement
const signBit = BigInt(1) << BigInt(255);
if (unsigned >= signBit) {
return unsigned - (BigInt(1) << BigInt(256));
}
return unsigned;
}
/**
* Deserializes a uleb128 encoded uint32 number.
*
* This function is used for interpreting lengths of variable-length sequences and tags of enum values in BCS encoding.
*
* @throws {Error} Throws an error if the parsed value exceeds the maximum uint32 number.
* @throws {Error} Throws an error if the uleb128 encoding is malformed (too many bytes).
* @returns {number} The deserialized uint32 value.
* @group Implementation
* @category BCS
*/
deserializeUleb128AsU32() {
let value = BigInt(0);
let shift = 0;
// Maximum 5 bytes for uleb128-encoded u32 (7 bits per byte, 5*7=35 bits > 32 bits needed)
const MAX_ULEB128_BYTES = 5;
let bytesRead = 0;
while (bytesRead < MAX_ULEB128_BYTES) {
const byte = this.deserializeU8();
bytesRead += 1;
value |= BigInt(byte & 0x7f) << BigInt(shift);
// Early overflow check before continuing
if (value > MAX_U32_NUMBER) {
throw new Error("Overflow while parsing uleb128-encoded uint32 value");
}
if ((byte & 0x80) === 0) {
break;
}
shift += 7;
}
// If we read MAX_ULEB128_BYTES and the last byte had continuation bit set, it's malformed
if (bytesRead === MAX_ULEB128_BYTES && value > MAX_U32_NUMBER) {
throw new Error("Overflow while parsing uleb128-encoded uint32 value");
}
return Number(value);
}
/**
* Helper function that primarily exists to support alternative syntax for deserialization.
* That is, if we have a `const deserializer: new Deserializer(...)`, instead of having to use
* `MyClass.deserialize(deserializer)`, we can call `deserializer.deserialize(MyClass)`.
*
* @example const deserializer = new Deserializer(new Uint8Array([1, 2, 3]));
* const value = deserializer.deserialize(MyClass); // where MyClass has a `deserialize` function
* // value is now an instance of MyClass
* // equivalent to `const value = MyClass.deserialize(deserializer)`
* @param cls The BCS-deserializable class to deserialize the buffered bytes into.
*
* @returns the deserialized value of class type T
* @group Implementation
* @category BCS
*/
deserialize(cls) {
// NOTE: `deserialize` in `cls.deserialize(this)` here is a static method defined in `cls`,
// It is separate from the `deserialize` instance method defined here in Deserializer.
return cls.deserialize(this);
}
/**
* Deserializes an array of BCS Deserializable values given an existing Deserializer instance with a loaded byte buffer.
*
* @param cls The BCS-deserializable class to deserialize the buffered bytes into.
* @returns An array of deserialized values of type T.
* @example
* // serialize a vector of addresses
* const addresses = new Array<AccountAddress>(
* AccountAddress.from("0x1"),
* AccountAddress.from("0x2"),
* AccountAddress.from("0xa"),
* AccountAddress.from("0xb"),
* );
* const serializer = new Serializer();
* serializer.serializeVector(addresses);
* const serializedBytes = serializer.toUint8Array();
*
* // deserialize the bytes into an array of addresses
* const deserializer = new Deserializer(serializedBytes);
* const deserializedAddresses = deserializer.deserializeVector(AccountAddress);
* // deserializedAddresses is now an array of AccountAddress instances
* @group Implementation
* @category BCS
*/
deserializeVector(cls) {
const length = this.deserializeUleb128AsU32();
if (length > MAX_DESERIALIZE_VECTOR_LENGTH) {
throw new Error(`Vector length ${length} exceeds maximum allowed length of ${MAX_DESERIALIZE_VECTOR_LENGTH}`);
}
const vector = new Array();
for (let i = 0; i < length; i += 1) {
vector.push(this.deserialize(cls));
}
return vector;
}
}
//# sourceMappingURL=deserializer.js.map