| import { decode, encode } from './text'; |
|
|
| const defaultByteLength = 1024 * 8; |
|
|
| const hostBigEndian = (() => { |
| const array = new Uint8Array(4); |
| const view = new Uint32Array(array.buffer); |
| return !((view[0] = 1) & array[0]); |
| })(); |
|
|
| type InputData = number | ArrayBufferLike | ArrayBufferView | IOBuffer | Buffer; |
|
|
| const typedArrays = { |
| int8: globalThis.Int8Array, |
| uint8: globalThis.Uint8Array, |
| int16: globalThis.Int16Array, |
| uint16: globalThis.Uint16Array, |
| int32: globalThis.Int32Array, |
| uint32: globalThis.Uint32Array, |
| uint64: globalThis.BigUint64Array, |
| int64: globalThis.BigInt64Array, |
| float32: globalThis.Float32Array, |
| float64: globalThis.Float64Array, |
| }; |
|
|
| type TypedArrays = typeof typedArrays; |
|
|
| interface IOBufferOptions { |
| |
| |
| |
| offset?: number; |
| } |
|
|
| export class IOBuffer { |
| |
| |
| |
| public buffer: ArrayBufferLike; |
|
|
| |
| |
| |
| public byteLength: number; |
|
|
| |
| |
| |
| public byteOffset: number; |
|
|
| |
| |
| |
| public length: number; |
|
|
| |
| |
| |
| public offset: number; |
|
|
| private lastWrittenByte: number; |
| private littleEndian: boolean; |
|
|
| private _data: DataView; |
| private _mark: number; |
| private _marks: number[]; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public constructor( |
| data: InputData = defaultByteLength, |
| options: IOBufferOptions = {}, |
| ) { |
| let dataIsGiven = false; |
| if (typeof data === 'number') { |
| data = new ArrayBuffer(data); |
| } else { |
| dataIsGiven = true; |
| this.lastWrittenByte = data.byteLength; |
| } |
| const offset = options.offset ? options.offset >>> 0 : 0; |
| const byteLength = data.byteLength - offset; |
| let dvOffset = offset; |
| if (ArrayBuffer.isView(data) || data instanceof IOBuffer) { |
| if (data.byteLength !== data.buffer.byteLength) { |
| dvOffset = data.byteOffset + offset; |
| } |
| data = data.buffer; |
| } |
| if (dataIsGiven) { |
| this.lastWrittenByte = byteLength; |
| } else { |
| this.lastWrittenByte = 0; |
| } |
| this.buffer = data; |
| this.length = byteLength; |
| this.byteLength = byteLength; |
| this.byteOffset = dvOffset; |
| this.offset = 0; |
| this.littleEndian = true; |
| this._data = new DataView(this.buffer, dvOffset, byteLength); |
| this._mark = 0; |
| this._marks = []; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public available(byteLength = 1): boolean { |
| return this.offset + byteLength <= this.length; |
| } |
|
|
| |
| |
| |
| |
| |
| public isLittleEndian(): boolean { |
| return this.littleEndian; |
| } |
|
|
| |
| |
| |
| |
| public setLittleEndian(): this { |
| this.littleEndian = true; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| public isBigEndian(): boolean { |
| return !this.littleEndian; |
| } |
|
|
| |
| |
| |
| |
| public setBigEndian(): this { |
| this.littleEndian = false; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public skip(n = 1): this { |
| this.offset += n; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public back(n = 1): this { |
| this.offset -= n; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public seek(offset: number): this { |
| this.offset = offset; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public mark(): this { |
| this._mark = this.offset; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public reset(): this { |
| this.offset = this._mark; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public pushMark(): this { |
| this._marks.push(this.offset); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public popMark(): this { |
| const offset = this._marks.pop(); |
| if (offset === undefined) { |
| throw new Error('Mark stack empty'); |
| } |
| this.seek(offset); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| public rewind(): this { |
| this.offset = 0; |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public ensureAvailable(byteLength = 1): this { |
| if (!this.available(byteLength)) { |
| const lengthNeeded = this.offset + byteLength; |
| const newLength = lengthNeeded * 2; |
| const newArray = new Uint8Array(newLength); |
| newArray.set(new Uint8Array(this.buffer)); |
| this.buffer = newArray.buffer; |
| this.length = newLength; |
| this.byteLength = newLength; |
| this._data = new DataView(this.buffer); |
| } |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public readBoolean(): boolean { |
| return this.readUint8() !== 0; |
| } |
|
|
| |
| |
| |
| |
| public readInt8(): number { |
| return this._data.getInt8(this.offset++); |
| } |
|
|
| |
| |
| |
| |
| public readUint8(): number { |
| return this._data.getUint8(this.offset++); |
| } |
|
|
| |
| |
| |
| |
| public readByte(): number { |
| return this.readUint8(); |
| } |
|
|
| |
| |
| |
| |
| |
| public readBytes(n = 1): Uint8Array { |
| return this.readArray(n, 'uint8'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public readArray<T extends keyof typeof typedArrays>( |
| size: number, |
| type: T, |
| ): InstanceType<TypedArrays[T]> { |
| const bytes = typedArrays[type].BYTES_PER_ELEMENT * size; |
| const offset = this.byteOffset + this.offset; |
| const slice = this.buffer.slice(offset, offset + bytes); |
| if ( |
| this.littleEndian === hostBigEndian && |
| type !== 'uint8' && |
| type !== 'int8' |
| ) { |
| const slice = new Uint8Array(this.buffer.slice(offset, offset + bytes)); |
| slice.reverse(); |
| const returnArray = new typedArrays[type](slice.buffer); |
| this.offset += bytes; |
| returnArray.reverse(); |
| return returnArray as InstanceType<TypedArrays[T]>; |
| } |
| const returnArray = new typedArrays[type](slice); |
| this.offset += bytes; |
| return returnArray as InstanceType<TypedArrays[T]>; |
| } |
|
|
| |
| |
| |
| |
| public readInt16(): number { |
| const value = this._data.getInt16(this.offset, this.littleEndian); |
| this.offset += 2; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readUint16(): number { |
| const value = this._data.getUint16(this.offset, this.littleEndian); |
| this.offset += 2; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readInt32(): number { |
| const value = this._data.getInt32(this.offset, this.littleEndian); |
| this.offset += 4; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readUint32(): number { |
| const value = this._data.getUint32(this.offset, this.littleEndian); |
| this.offset += 4; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readFloat32(): number { |
| const value = this._data.getFloat32(this.offset, this.littleEndian); |
| this.offset += 4; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readFloat64(): number { |
| const value = this._data.getFloat64(this.offset, this.littleEndian); |
| this.offset += 8; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readBigInt64(): bigint { |
| const value = this._data.getBigInt64(this.offset, this.littleEndian); |
| this.offset += 8; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readBigUint64(): bigint { |
| const value = this._data.getBigUint64(this.offset, this.littleEndian); |
| this.offset += 8; |
| return value; |
| } |
|
|
| |
| |
| |
| |
| public readChar(): string { |
| |
| return String.fromCharCode(this.readInt8()); |
| } |
|
|
| |
| |
| |
| |
| |
| public readChars(n = 1): string { |
| let result = ''; |
| for (let i = 0; i < n; i++) { |
| result += this.readChar(); |
| } |
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public readUtf8(n = 1): string { |
| return decode(this.readBytes(n)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public decodeText(n = 1, encoding = 'utf8'): string { |
| return decode(this.readBytes(n), encoding); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeBoolean(value: unknown): this { |
| this.writeUint8(value ? 0xff : 0x00); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public writeInt8(value: number): this { |
| this.ensureAvailable(1); |
| this._data.setInt8(this.offset++, value); |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeUint8(value: number): this { |
| this.ensureAvailable(1); |
| this._data.setUint8(this.offset++, value); |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| public writeByte(value: number): this { |
| return this.writeUint8(value); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeBytes(bytes: ArrayLike<number>): this { |
| this.ensureAvailable(bytes.length); |
| |
| for (let i = 0; i < bytes.length; i++) { |
| this._data.setUint8(this.offset++, bytes[i]); |
| } |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeInt16(value: number): this { |
| this.ensureAvailable(2); |
| this._data.setInt16(this.offset, value, this.littleEndian); |
| this.offset += 2; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeUint16(value: number): this { |
| this.ensureAvailable(2); |
| this._data.setUint16(this.offset, value, this.littleEndian); |
| this.offset += 2; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeInt32(value: number): this { |
| this.ensureAvailable(4); |
| this._data.setInt32(this.offset, value, this.littleEndian); |
| this.offset += 4; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeUint32(value: number): this { |
| this.ensureAvailable(4); |
| this._data.setUint32(this.offset, value, this.littleEndian); |
| this.offset += 4; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeFloat32(value: number): this { |
| this.ensureAvailable(4); |
| this._data.setFloat32(this.offset, value, this.littleEndian); |
| this.offset += 4; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeFloat64(value: number): this { |
| this.ensureAvailable(8); |
| this._data.setFloat64(this.offset, value, this.littleEndian); |
| this.offset += 8; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeBigInt64(value: bigint): this { |
| this.ensureAvailable(8); |
| this._data.setBigInt64(this.offset, value, this.littleEndian); |
| this.offset += 8; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeBigUint64(value: bigint): this { |
| this.ensureAvailable(8); |
| this._data.setBigUint64(this.offset, value, this.littleEndian); |
| this.offset += 8; |
| this._updateLastWrittenByte(); |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeChar(str: string): this { |
| |
| return this.writeUint8(str.charCodeAt(0)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeChars(str: string): this { |
| for (let i = 0; i < str.length; i++) { |
| |
| this.writeUint8(str.charCodeAt(i)); |
| } |
| return this; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public writeUtf8(str: string): this { |
| return this.writeBytes(encode(str)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public toArray(): Uint8Array { |
| return new Uint8Array(this.buffer, this.byteOffset, this.lastWrittenByte); |
| } |
|
|
| |
| |
| |
| |
| public getWrittenByteLength() { |
| return this.lastWrittenByte - this.byteOffset; |
| } |
|
|
| |
| |
| |
| |
| private _updateLastWrittenByte(): void { |
| if (this.offset > this.lastWrittenByte) { |
| this.lastWrittenByte = this.offset; |
| } |
| } |
| } |
|
|