datastream-js
Version:
DataStream.js is a library for reading data from ArrayBuffers
1,351 lines • 60.4 kB
JavaScript
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