UNPKG

datastream-js

Version:

DataStream.js is a library for reading data from ArrayBuffers

1,351 lines 60.4 kB
import { TextEncoder, TextDecoder } from "text-encoding"; /** * DataStream reads scalars, arrays and structs of data from an ArrayBuffer. * It's like a file-like DataView on steroids. * * @param {ArrayBuffer} arrayBuffer ArrayBuffer to read from. * @param {?Number} byteOffset Offset from arrayBuffer beginning for the DataStream. * @param {?Boolean} endianness DataStream.BIG_ENDIAN or DataStream.LITTLE_ENDIAN (the default). */ export default class DataStream { constructor(arrayBuffer, byteOffset, endianness = DataStream.LITTLE_ENDIAN) { this.endianness = endianness; this.position = 0; /** * Whether to extend DataStream buffer when trying to write beyond its size. * If set, the buffer is reallocated to twice its current size until the * requested write fits the buffer. * @type {boolean} */ this._dynamicSize = true; /** * Virtual byte length of the DataStream backing buffer. * Updated to be max of original buffer size and last written size. * If dynamicSize is false is set to buffer size. * @type {number} */ this._byteLength = 0; /** * Seek position where DataStream#readStruct ran into a problem. * Useful for debugging struct parsing. * * @type {number} */ this.failurePosition = 0; this._byteOffset = byteOffset || 0; if (arrayBuffer instanceof ArrayBuffer) { this.buffer = arrayBuffer; } else if (typeof arrayBuffer === "object") { this.dataView = arrayBuffer; if (byteOffset) { this._byteOffset += byteOffset; } } else { this.buffer = new ArrayBuffer(arrayBuffer || 1); } } get dynamicSize() { return this._dynamicSize; } set dynamicSize(v) { if (!v) { this._trimAlloc(); } this._dynamicSize = v; } /** * Returns the byte length of the DataStream object. * @type {number} */ get byteLength() { return this._byteLength - this._byteOffset; } /** * Set/get the backing ArrayBuffer of the DataStream object. * The setter updates the DataView to point to the new buffer. * @type {Object} */ get buffer() { this._trimAlloc(); return this._buffer; } set buffer(v) { this._buffer = v; this._dataView = new DataView(this._buffer, this._byteOffset); this._byteLength = this._buffer.byteLength; } /** * Set/get the byteOffset of the DataStream object. * The setter updates the DataView to point to the new byteOffset. * @type {number} */ get byteOffset() { return this._byteOffset; } set byteOffset(v) { this._byteOffset = v; this._dataView = new DataView(this._buffer, this._byteOffset); this._byteLength = this._buffer.byteLength; } /** * Set/get the backing DataView of the DataStream object. * The setter updates the buffer and byteOffset to point to the DataView values. * @type get: DataView, set: {buffer: ArrayBuffer, byteOffset: number, byteLength: number} */ get dataView() { return this._dataView; } set dataView(v) { this._byteOffset = v.byteOffset; this._buffer = v.buffer; this._dataView = new DataView(this._buffer, this._byteOffset); this._byteLength = this._byteOffset + v.byteLength; } bigEndian() { this.endianness = DataStream.BIG_ENDIAN; return this; } /** * Internal function to resize the DataStream buffer when required. * @param {number} extra Number of bytes to add to the buffer allocation. * @return {null} */ _realloc(extra) { if (!this._dynamicSize) { return; } const req = this._byteOffset + this.position + extra; let blen = this._buffer.byteLength; if (req <= blen) { if (req > this._byteLength) { this._byteLength = req; } return; } if (blen < 1) { blen = 1; } while (req > blen) { blen *= 2; } const buf = new ArrayBuffer(blen); const src = new Uint8Array(this._buffer); const dst = new Uint8Array(buf, 0, src.length); dst.set(src); this.buffer = buf; this._byteLength = req; } /** * Internal function to trim the DataStream buffer when required. * Used for stripping out the extra bytes from the backing buffer when * the virtual byteLength is smaller than the buffer byteLength (happens after * growing the buffer with writes and not filling the extra space completely). * @return {null} */ _trimAlloc() { if (this._byteLength === this._buffer.byteLength) { return; } const buf = new ArrayBuffer(this._byteLength); const dst = new Uint8Array(buf); const src = new Uint8Array(this._buffer, 0, dst.length); dst.set(src); this.buffer = buf; } /** * Sets the DataStream read/write position to given position. * Clamps between 0 and DataStream length. * @param {number} pos Position to seek to. * @return {null} */ seek(pos) { const npos = Math.max(0, Math.min(this.byteLength, pos)); this.position = isNaN(npos) || !isFinite(npos) ? 0 : npos; } /** * Returns true if the DataStream seek pointer is at the end of buffer and * there's no more data to read. * @return {boolean} True if the seek pointer is at the end of the buffer. */ isEof() { return this.position >= this.byteLength; } /** * Maps an Int32Array into the DataStream buffer, swizzling it to native * endianness in-place. The current offset from the start of the buffer needs to * be a multiple of element size, just like with typed array views. * * Nice for quickly reading in data. Warning: potentially modifies the buffer * contents. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} Int32Array to the DataStream backing buffer. */ mapInt32Array(length, e) { this._realloc(length * 4); const arr = new Int32Array(this._buffer, this.byteOffset + this.position, length); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += length * 4; return arr; } /** * Maps an Int16Array into the DataStream buffer, swizzling it to native * endianness in-place. The current offset from the start of the buffer needs to * be a multiple of element size, just like with typed array views. * * Nice for quickly reading in data. Warning: potentially modifies the buffer * contents. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} Int16Array to the DataStream backing buffer. */ mapInt16Array(length, e) { this._realloc(length * 2); const arr = new Int16Array(this._buffer, this.byteOffset + this.position, length); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += length * 2; return arr; } /** * Maps an Int8Array into the DataStream buffer. * * Nice for quickly reading in data. * * @param {number} length Number of elements to map. * @return {Object} Int8Array to the DataStream backing buffer. */ mapInt8Array(length) { this._realloc(length); const arr = new Int8Array(this._buffer, this.byteOffset + this.position, length); this.position += length; return arr; } /** * Maps a Uint32Array into the DataStream buffer, swizzling it to native * endianness in-place. The current offset from the start of the buffer needs to * be a multiple of element size, just like with typed array views.* * Nice for quickly reading in data. Warning: potentially modifies the buffer * contents.* * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} Uint32Array to the DataStream backing buffer. */ mapUint32Array(length, e) { this._realloc(length * 4); const arr = new Uint32Array(this._buffer, this.byteOffset + this.position, length); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += length * 4; return arr; } /** * Maps a Uint16Array into the DataStream buffer, swizzling it to native * endianness in-place. The current offset from the start of the buffer needs to * be a multiple of element size, just like with typed array views. * * Nice for quickly reading in data. Warning: potentially modifies the buffer * contents. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} Uint16Array to the DataStream backing buffer. */ mapUint16Array(length, e) { this._realloc(length * 2); const arr = new Uint16Array(this._buffer, this.byteOffset + this.position, length); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += length * 2; return arr; } /** * Maps a Uint8Array into the DataStream buffer. * * Nice for quickly reading in data. * * @param {number} length Number of elements to map. * @return {Object} Uint8Array to the DataStream backing buffer. */ mapUint8Array(length) { this._realloc(length); const arr = new Uint8Array(this._buffer, this.byteOffset + this.position, length); this.position += length; return arr; } /** * Maps a Float64Array into the DataStream buffer, swizzling it to native * endianness in-place. The current offset from the start of the buffer needs to * be a multiple of element size, just like with typed array views. * * Nice for quickly reading in data. Warning: potentially modifies the buffer * contents. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} Float64Array to the DataStream backing buffer. */ mapFloat64Array(length, e) { this._realloc(length * 8); const arr = new Float64Array(this._buffer, this.byteOffset + this.position, length); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += length * 8; return arr; } /** * Maps a Float32Array into the DataStream buffer, swizzling it to native * endianness in-place. The current offset from the start of the buffer needs to * be a multiple of element size, just like with typed array views. * * Nice for quickly reading in data. Warning: potentially modifies the buffer * contents. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} Float32Array to the DataStream backing buffer. */ mapFloat32Array(length, e) { this._realloc(length * 4); const arr = new Float32Array(this._buffer, this.byteOffset + this.position, length); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += length * 4; return arr; } /** * Reads an Int32Array of desired length and endianness from the DataStream. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} The read Int32Array. */ readInt32Array(length, e) { length = length == null ? this.byteLength - this.position / 4 : length; const arr = new Int32Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += arr.byteLength; return arr; } /** * Reads an Int16Array of desired length and endianness from the DataStream. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} The read Int16Array. */ readInt16Array(length, e) { length = length == null ? this.byteLength - this.position / 2 : length; const arr = new Int16Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += arr.byteLength; return arr; } /** * Reads an Int8Array of desired length from the DataStream. * * @param {number} length Number of elements to map. * @return {Object} The read Int8Array. */ readInt8Array(length) { length = length == null ? this.byteLength - this.position : length; const arr = new Int8Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); this.position += arr.byteLength; return arr; } /** * Reads a Uint32Array of desired length and endianness from the DataStream. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} The read Uint32Array. */ readUint32Array(length, e) { length = length == null ? this.byteLength - this.position / 4 : length; const arr = new Uint32Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += arr.byteLength; return arr; } /** * Reads a Uint16Array of desired length and endianness from the DataStream. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} The read Uint16Array. */ readUint16Array(length, e) { length = length == null ? this.byteLength - this.position / 2 : length; const arr = new Uint16Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += arr.byteLength; return arr; } /** * Reads a Uint8Array of desired length from the DataStream. * * @param {number} length Number of elements to map. * @return {Object} The read Uint8Array. */ readUint8Array(length) { length = length == null ? this.byteLength - this.position : length; const arr = new Uint8Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); this.position += arr.byteLength; return arr; } /** * Reads a Float64Array of desired length and endianness from the DataStream. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} The read Float64Array. */ readFloat64Array(length, e) { length = length == null ? this.byteLength - this.position / 8 : length; const arr = new Float64Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += arr.byteLength; return arr; } /** * Reads a Float32Array of desired length and endianness from the DataStream. * * @param {number} length Number of elements to map. * @param {?boolean} e Endianness of the data to read. * @return {Object} The read Float32Array. */ readFloat32Array(length, e) { length = length == null ? this.byteLength - this.position / 4 : length; const arr = new Float32Array(length); DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); DataStream.arrayToNative(arr, e == null ? this.endianness : e); this.position += arr.byteLength; return arr; } /** * Writes an Int32Array of specified endianness to the DataStream. * * @param {Object} arr The array to write. * @param {?boolean} e Endianness of the data to write. */ writeInt32Array(arr, e) { this._realloc(arr.length * 4); if (arr instanceof Int32Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapInt32Array(arr.length, e); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeInt32(arr[i], e); } } return this; } /** * Writes an Int16Array of specified endianness to the DataStream. * * @param {Object} arr The array to write. * @param {?boolean} e Endianness of the data to write. */ writeInt16Array(arr, e) { this._realloc(arr.length * 2); if (arr instanceof Int16Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapInt16Array(arr.length, e); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeInt16(arr[i], e); } } return this; } /** * Writes an Int8Array to the DataStream. * * @param {Object} arr The array to write. */ writeInt8Array(arr) { this._realloc(arr.length); if (arr instanceof Int8Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapInt8Array(arr.length); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeInt8(arr[i]); } } return this; } /** * Writes a Uint32Array of specified endianness to the DataStream. * * @param {Object} arr The array to write. * @param {?boolean} e Endianness of the data to write. */ writeUint32Array(arr, e) { this._realloc(arr.length * 4); if (arr instanceof Uint32Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapUint32Array(arr.length, e); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeUint32(arr[i], e); } } return this; } /** * Writes a Uint16Array of specified endianness to the DataStream. * * @param {Object} arr The array to write. * @param {?boolean} e Endianness of the data to write. */ writeUint16Array(arr, e) { this._realloc(arr.length * 2); if (arr instanceof Uint16Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapUint16Array(arr.length, e); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeUint16(arr[i], e); } } return this; } /** * Writes a Uint8Array to the DataStream. * * @param {Object} arr The array to write. */ writeUint8Array(arr) { this._realloc(arr.length); if (arr instanceof Uint8Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapUint8Array(arr.length); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeUint8(arr[i]); } } return this; } /** * Writes a Float64Array of specified endianness to the DataStream. * * @param {Object} arr The array to write. * @param {?boolean} e Endianness of the data to write. */ writeFloat64Array(arr, e) { this._realloc(arr.length * 8); if (arr instanceof Float64Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapFloat64Array(arr.length, e); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeFloat64(arr[i], e); } } return this; } /** * Writes a Float32Array of specified endianness to the DataStream. * * @param {Object} arr The array to write. * @param {?boolean} e Endianness of the data to write. */ writeFloat32Array(arr, e) { this._realloc(arr.length * 4); if (arr instanceof Float32Array && (this.byteOffset + this.position) % arr.BYTES_PER_ELEMENT === 0) { DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, arr.byteOffset, arr.byteLength); this.mapFloat32Array(arr.length, e); } else { // tslint:disable-next-line prefer-for-of for (let i = 0; i < arr.length; i++) { this.writeFloat32(arr[i], e); } } return this; } /** * Reads a 32-bit int from the DataStream with the desired endianness. * * @param {?boolean} e Endianness of the number. * @return {number} The read number. */ readInt32(e) { const v = this._dataView.getInt32(this.position, e == null ? this.endianness : e); this.position += 4; return v; } /** * Reads a 16-bit int from the DataStream with the desired endianness. * * @param {?boolean} e Endianness of the number. * @return {number} The read number. */ readInt16(e) { const v = this._dataView.getInt16(this.position, e == null ? this.endianness : e); this.position += 2; return v; } /** * Reads an 8-bit int from the DataStream. * * @return {number} The read number. */ readInt8() { const v = this._dataView.getInt8(this.position); this.position += 1; return v; } /** * Reads a 32-bit unsigned int from the DataStream with the desired endianness. * * @param {?boolean} e Endianness of the number. * @return {number} The read number. */ readUint32(e) { const v = this._dataView.getUint32(this.position, e == null ? this.endianness : e); this.position += 4; return v; } /** * Reads a 16-bit unsigned int from the DataStream with the desired endianness. * * @param {?boolean} e Endianness of the number. * @return {number} The read number. */ readUint16(e) { const v = this._dataView.getUint16(this.position, e == null ? this.endianness : e); this.position += 2; return v; } /** * Reads an 8-bit unsigned int from the DataStream. * * @return {number} The read number. */ readUint8() { const v = this._dataView.getUint8(this.position); this.position += 1; return v; } /** * Reads a 32-bit float from the DataStream with the desired endianness. * * @param {?boolean} e Endianness of the number. * @return {number} The read number. */ readFloat32(e) { const v = this._dataView.getFloat32(this.position, e == null ? this.endianness : e); this.position += 4; return v; } /** * Reads a 64-bit float from the DataStream with the desired endianness. * * @param {?boolean} e Endianness of the number. * @return {number} The read number. */ readFloat64(e) { const v = this._dataView.getFloat64(this.position, e == null ? this.endianness : e); this.position += 8; return v; } /** * Writes a 32-bit int to the DataStream with the desired endianness. * * @param {number} v Number to write. * @param {?boolean} e Endianness of the number. */ writeInt32(v, e) { this._realloc(4); this._dataView.setInt32(this.position, v, e == null ? this.endianness : e); this.position += 4; return this; } /** * Writes a 16-bit int to the DataStream with the desired endianness. * * @param {number} v Number to write. * @param {?boolean} e Endianness of the number. */ writeInt16(v, e) { this._realloc(2); this._dataView.setInt16(this.position, v, e == null ? this.endianness : e); this.position += 2; return this; } /** * Writes an 8-bit int to the DataStream. * * @param {number} v Number to write. */ writeInt8(v) { this._realloc(1); this._dataView.setInt8(this.position, v); this.position += 1; return this; } /** * Writes a 32-bit unsigned int to the DataStream with the desired endianness. * * @param {number} v Number to write. * @param {?boolean} e Endianness of the number. */ writeUint32(v, e) { this._realloc(4); this._dataView.setUint32(this.position, v, e == null ? this.endianness : e); this.position += 4; return this; } /** * Writes a 16-bit unsigned int to the DataStream with the desired endianness. * * @param {number} v Number to write. * @param {?boolean} e Endianness of the number. */ writeUint16(v, e) { this._realloc(2); this._dataView.setUint16(this.position, v, e == null ? this.endianness : e); this.position += 2; return this; } /** * Writes an 8-bit unsigned int to the DataStream. * * @param {number} v Number to write. */ writeUint8(v) { this._realloc(1); this._dataView.setUint8(this.position, v); this.position += 1; return this; } /** * Writes a 32-bit float to the DataStream with the desired endianness. * * @param {number} v Number to write. * @param {?boolean} e Endianness of the number. */ writeFloat32(v, e) { this._realloc(4); this._dataView.setFloat32(this.position, v, e == null ? this.endianness : e); this.position += 4; return this; } /** * Writes a 64-bit float to the DataStream with the desired endianness. * * @param {number} v Number to write. * @param {?boolean} e Endianness of the number. */ writeFloat64(v, e) { this._realloc(8); this._dataView.setFloat64(this.position, v, e == null ? this.endianness : e); this.position += 8; return this; } /** * Copies byteLength bytes from the src buffer at srcOffset to the * dst buffer at dstOffset. * * @param {Object} dst Destination ArrayBuffer to write to. * @param {number} dstOffset Offset to the destination ArrayBuffer. * @param {Object} src Source ArrayBuffer to read from. * @param {number} srcOffset Offset to the source ArrayBuffer. * @param {number} byteLength Number of bytes to copy. */ static memcpy(dst, dstOffset, src, srcOffset, byteLength) { const dstU8 = new Uint8Array(dst, dstOffset, byteLength); const srcU8 = new Uint8Array(src, srcOffset, byteLength); dstU8.set(srcU8); } /** * Converts array to native endianness in-place. * * @param {Object} array Typed array to convert. * @param {boolean} arrayIsLittleEndian True if the data in the array is * little-endian. Set false for big-endian. * @return {Object} The converted typed array. */ static arrayToNative(array, arrayIsLittleEndian) { if (arrayIsLittleEndian === this.endianness) { return array; } else { return this.flipArrayEndianness(array); // ??? } } /** * Converts native endianness array to desired endianness in-place. * * @param {Object} array Typed array to convert. * @param {boolean} littleEndian True if the converted array should be * little-endian. Set false for big-endian. * @return {Object} The converted typed array. */ static nativeToEndian(array, littleEndian) { if (this.endianness === littleEndian) { return array; } else { return this.flipArrayEndianness(array); } } /** * Flips typed array endianness in-place. * * @param {Object} array Typed array to flip. * @return {Object} The converted typed array. */ static flipArrayEndianness(array) { const u8 = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); for (let i = 0; i < array.byteLength; i += array.BYTES_PER_ELEMENT) { for ( // tslint:disable-next-line one-variable-per-declaration let j = i + array.BYTES_PER_ELEMENT - 1, k = i; j > k; j--, k++) { const tmp = u8[k]; u8[k] = u8[j]; u8[j] = tmp; } } return array; } /** * Creates an array from an array of character codes. * Uses String.fromCharCode in chunks for memory efficiency and then concatenates * the resulting string chunks. * * @param {TypedArray} array Array of character codes. * @return {string} String created from the character codes. */ static createStringFromArray(array) { const chunkSize = 0x8000; const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(String.fromCharCode.apply(null, array.subarray(i, i + chunkSize))); } return chunks.join(""); } /** * Reads a struct of data from the DataStream. The struct is defined as * a flat array of [name, type]-pairs. See the example below: * * ds.readStruct([ * 'headerTag', 'uint32', // Uint32 in DataStream endianness. * 'headerTag2', 'uint32be', // Big-endian Uint32. * 'headerTag3', 'uint32le', // Little-endian Uint32. * 'array', ['[]', 'uint32', 16], // Uint32Array of length 16. * 'array2Length', 'uint32', * 'array2', ['[]', 'uint32', 'array2Length'] // Uint32Array of length array2Length * ]); * * The possible values for the type are as follows: * * // Number types * * // Unsuffixed number types use DataStream endianness. * // To explicitly specify endianness, suffix the type with * // 'le' for little-endian or 'be' for big-endian, * // e.g. 'int32be' for big-endian int32. * * 'uint8' -- 8-bit unsigned int * 'uint16' -- 16-bit unsigned int * 'uint32' -- 32-bit unsigned int * 'int8' -- 8-bit int * 'int16' -- 16-bit int * 'int32' -- 32-bit int * 'float32' -- 32-bit float * 'float64' -- 64-bit float * * // String types * 'cstring' -- ASCII string terminated by a zero byte. * 'string:N' -- ASCII string of length N, where N is a literal integer. * 'string:variableName' -- ASCII string of length $variableName, * where 'variableName' is a previously parsed number in the current struct. * 'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET. * 'u16string:N' -- UCS-2 string of length N in DataStream endianness. * 'u16stringle:N' -- UCS-2 string of length N in little-endian. * 'u16stringbe:N' -- UCS-2 string of length N in big-endian. * * // Complex types * [name, type, name_2, type_2, ..., name_N, type_N] -- Struct * function(dataStream, struct) {} -- Callback function to read and return data. * {get: function(dataStream, struct) {}, * set: function(dataStream, struct) {}} * -- Getter/setter functions to read and return data, handy for using the same * struct definition for reading and writing structs. * ['[]', type, length] -- Array of given type and length. The length can be either * a number, a string that references a previously-read * field, or a callback function(struct, dataStream, type){}. * If length is '*', reads in as many elements as it can. * * @param {Object} structDefinition Struct definition object. * @return {Object} The read struct. Null if failed to read struct. * * @deprecated use DataStream.read/write(TypeDef) instead of readStruct/writeStruct */ readStruct(structDefinition) { const struct = {}; let t; let v; const p = this.position; for (let i = 0; i < structDefinition.length; i += 2) { t = structDefinition[i + 1]; v = this.readType(t, struct); if (v == null) { if (this.failurePosition === 0) { this.failurePosition = this.position; } this.position = p; return null; } struct[structDefinition[i]] = v; } return struct; } /** ex: * const def = [ * ["obj", [["num", "Int8"], * ["greet", "Utf8WithLen"], * ["a1", "Int16*"]] * ], * ["a2", "Uint16*"] * ]; * const o = {obj: { * num: 5, * greet: "Xin chào", * a1: [-3, 0, 4, 9, 0x7FFF], * }, * a2: [3, 0, 4, 9, 0xFFFF] * }); * ds.write(def, o); * expect: new DataStream(ds.buffer).read(def) deepEqual o */ read(def) { const o = {}; let d; for (d of def) { const v = d[0]; const t = d[1]; if (typeof t === "string") { if (t.endsWith("*")) { const len = this.readUint16(); o[v] = this["read" + t.substr(0, t.length - 1) + "Array"](len); } else { o[v] = this["read" + t](); } } else { o[v] = this.read(t); } } return o; } /** ex: * const def = [ * ["obj", [["num", "Int8"], * ["greet", "Utf8WithLen"], * ["a1", "Int16*"]] * ], * ["a2", "Uint16*"] * ]; * const o = {obj: { * num: 5, * greet: "Xin chào", * a1: [-3, 0, 4, 9, 0x7FFF], * }, * a2: [3, 0, 4, 9, 0xFFFF] * }); * ds.write(def, o); * expect: new DataStream(ds.buffer).read(def) deepEqual o */ write(def, o) { let d; for (d of def) { const v = d[0]; const t = d[1]; if (typeof t === "string") { if (t.endsWith("*")) { const arr = o[v]; this.writeUint16(arr.length); this["write" + t.substr(0, t.length - 1) + "Array"](arr); } else { this["write" + t](o[v]); } } else { this.write(t, o[v]); } } return this; } /** convenient method to write data. ex, instead of write data as in jsdoc of `write` method, we can: * const def = [ * ["Int8", "Utf8WithLen", "Int16*"], * "Uint16*" * ]; * const a = [ * [5, "Xin chào", [-3, 0, 4, 9, 0x7FFF]], * [3, 0, 4, 9, 0xFFFF] * ]; * ds.writeArray(def, a) */ writeArray(def, a) { let t; let i; for (i = 0; i < def.length; i++) { t = def[i]; if (typeof t === "string") { if (t.endsWith("*")) { const arr = a[i]; this.writeUint16(arr.length); this["write" + t.substr(0, t.length - 1) + "Array"](arr); } else { this["write" + t](a[i]); } } else { this.writeArray(t, a[i]); } } return this; } /** * Read UCS-2 string of desired length and endianness from the DataStream. * * @param {number} length The length of the string to read. * @param {boolean} endianness The endianness of the string data in the DataStream. * @return {string} The read string. */ readUCS2String(length, endianness) { return DataStream.createStringFromArray(this.readUint16Array(length, endianness)); } /** * Write a UCS-2 string of desired endianness to the DataStream. The * lengthOverride argument lets you define the number of characters to write. * If the string is shorter than lengthOverride, the extra space is padded with * zeroes. * * @param {string} str The string to write. * @param {?boolean} endianness The endianness to use for the written string data. * @param {?number} lengthOverride The number of characters to write. */ writeUCS2String(str, endianness, lengthOverride) { if (lengthOverride == null) { lengthOverride = str.length; } let i = 0; for (; i < str.length && i < lengthOverride; i++) { this.writeUint16(str.charCodeAt(i), endianness); } for (; i < lengthOverride; i++) { this.writeUint16(0); } return this; } /** * Read a string of desired length and encoding from the DataStream. * * @param {number} length The length of the string to read in bytes. * @param {?string} encoding The encoding of the string data in the DataStream. * Defaults to ASCII. * @return {string} The read string. */ readString(length, encoding) { if (encoding == null || encoding === "ASCII") { return DataStream.createStringFromArray(this.mapUint8Array(length == null ? this.byteLength - this.position : length)); } else { return new TextDecoder(encoding).decode(this.mapUint8Array(length)); } } /** * Writes a string of desired length and encoding to the DataStream. * * @param {string} s The string to write. * @param {?string} encoding The encoding for the written string data. * Defaults to ASCII. * @param {?number} length The number of characters to write. */ writeString(s, encoding, length) { if (encoding == null || encoding === "ASCII") { if (length != null) { let i; const len = Math.min(s.length, length); for (i = 0; i < len; i++) { this.writeUint8(s.charCodeAt(i)); } for (; i < length; i++) { this.writeUint8(0); } } else { for (let i = 0; i < s.length; i++) { this.writeUint8(s.charCodeAt(i)); } } } else { this.writeUint8Array(new TextEncoder(encoding).encode(s.substring(0, length))); } return this; } /** writeUint16(utf8 length of `s`) then write utf8 `s` */ writeUtf8WithLen(s) { const arr = new TextEncoder("utf-8").encode(s); return this.writeUint16(arr.length).writeUint8Array(arr); } /** readUint16 into `len` then read `len` Uint8 then parse into the result utf8 string */ readUtf8WithLen() { const len = this.readUint16(); return new TextDecoder("utf-8").decode(this.mapUint8Array(len)); } /** * Read null-terminated string of desired length from the DataStream. Truncates * the returned string so that the null byte is not a part of it. * * @param {?number} length The length of the string to read. * @return {string} The read string. */ readCString(length) { const blen = this.byteLength - this.position; const u8 = new Uint8Array(this._buffer, this._byteOffset + this.position); let len = blen; if (length != null) { len = Math.min(length, blen); } let i = 0; for (; i < len && u8[i] !== 0; i++) { // find first zero byte } const s = DataStream.createStringFromArray(this.mapUint8Array(i)); if (length != null) { this.position += len - i; } else if (i !== blen) { this.position += 1; // trailing zero if not at end of buffer } return s; } /** * Writes a null-terminated string to DataStream and zero-pads it to length * bytes. If length is not given, writes the string followed by a zero. * If string is longer than length, the written part of the string does not have * a trailing zero. * * @param {string} s The string to write. * @param {?number} length The number of characters to write. */ writeCString(s, length) { if (length != null) { let i; const len = Math.min(s.length, length); for (i = 0; i < len; i++) { this.writeUint8(s.charCodeAt(i)); } for (; i < length; i++) { this.writeUint8(0); } } else { for (let i = 0; i < s.length; i++) { this.writeUint8(s.charCodeAt(i)); } this.writeUint8(0); } return this; } /** * Reads an object of type t from the DataStream, passing struct as the thus-far * read struct to possible callbacks that refer to it. Used by readStruct for * reading in the values, so the type is one of the readStruct types. * * @param {Object} t Type of the object to read. * @param {?Object} struct Struct to refer to when resolving length references * and for calling callbacks. * @return {?Object} Returns the object on successful read, null on unsuccessful. */ readType(t, struct) { if (typeof t === "function") { return t(this, struct); } else if (typeof t === "object" && !(t instanceof Array)) { return t.get(this, struct); } else if (t instanceof Array && t.length !== 3) { return this.readStruct(t); } let v = null; let lengthOverride = null; let charset = "ASCII"; const pos = this.position; if (typeof t === "string" && /:/.test(t)) { const tp = t.split(":"); t = tp[0]; const len = tp[1]; // allow length to be previously parsed variable // e.g. 'string:fieldLength', if `fieldLength` has been parsed previously. // else, assume literal integer e.g., 'string:4' lengthOverride = parseInt(struct[len] != null ? struct[len] : len, 10); } if (typeof t === "string" && /,/.test(t)) { const tp = t.split(","); t = tp[0]; charset = tp[1]; } switch (t) { case "uint8": v = this.readUint8(); break; case "int8": v = this.readInt8(); break; case "uint16": v = this.readUint16(this.endianness); break; case "int16": v = this.readInt16(this.endianness); break; case "uint32": v = this.readUint32(this.endianness); break; case "int32": v = this.readInt32(this.endianness); break; case "float32": v = this.readFloat32(this.endianness); break; case "float64": v = this.readFloat64(this.endianness); break; case "uint16be": v = this.readUint16(DataStream.BIG_ENDIAN); break; case "int16be": v = this.readInt16(DataStream.BIG_ENDIAN); break; case "uint32be": v = this.readUint32(DataStream.BIG_ENDIAN); break; case "int32be": v = this.readInt32(DataStream.BIG_ENDIAN); break; case "float32be": v = this.readFloat32(DataStream.BIG_ENDIAN); break; case "float64be": v = this.readFloat64(DataStream.BIG_ENDIAN); break; case "uint16le": v = this.readUint16(DataStream.LITTLE_ENDIAN); break; case "int16le": v = this.readInt16(DataStream.LITTLE_ENDIAN); break; case "uint32le": v = this.readUint32(DataStream.LITTLE_ENDIAN); break; case "int32le": v = this.readInt32(DataStream.LITTLE_ENDIAN); break; case "float32le": v = this.readFloat32(DataStream.LITTLE_ENDIAN); break; case "float64le": v = this.readFloat64(DataStream.LITTLE_ENDIAN); break; case "cstring": v = this.readCString(lengthOverride); break; case "string": v = this.readString(lengthOverride, charset); break; case "u16string": v = this.readUCS2String(lengthOverride, this.endianness); break; case "u16stringle": v = this.readUCS2String(lengthOverride, DataStream.LITTLE_ENDIAN); break; case "u16stringbe": v = this.readUCS2String(lengthOverride, DataStream.BIG_ENDIAN); break; default: if (t.length === 3) { const ta = t[1]; const len = t[2]; let length = 0; if (typeof len === "function") { length = len(struct, this, t); } else if (typeof len === "string" && struct[len] != null) { length = parseInt(struct[len], 10); } else { length = parseInt(len, 10); } if (typeof ta === "string") { const tap = ta.replace(/(le|be)$/, ""); let endianness = null; if (/le$/.test(ta)) { endianness = DataStream.LITTLE_ENDIAN; } else if (/be$/.test(ta)) { endianness = DataStream.BIG_ENDIAN; } if (len === "*") { length = null; } switch (tap) { case "uint8": v = this.readUint8Array(length); break; case "uint16": v = this.readUint16Array(length, endianness); break; case "uint32": v = this.readUint32Array(length, endianness); break; case "int8": v = this.readInt8Array(length); break; case "int16": v = this.readInt16Array(length, endianness); break; case "int32": v = this.readInt32Array(length, endianness); break; case "float32": v = this.readFloat32