src/utils/io-buffer.js
/**
* @file IO Buffer
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @private
*
* Adapted from https://github.com/image-js/iobuffer
* MIT License, Copyright (c) 2015 Michaël Zasso
*/
const defaultByteLength = 1024 * 8
const charArray = []
/**
* Class for writing and reading binary data
*/
class IOBuffer {
/**
* @param {undefined|number|ArrayBuffer|TypedArray|IOBuffer|Buffer} data - The data to construct the IOBuffer with.
*
* If it's a number, it will initialize the buffer with the number as
* the buffer's length. If it's undefined, it will initialize the buffer
* with a default length of 8 Kb. If its an ArrayBuffer, a TypedArray,
* an IOBuffer instance, or a Node.js Buffer, it will create a view over
* the underlying ArrayBuffer.
* @param {object} [params]
* @param {number} [params.offset=0] - Ignore the first n bytes of the ArrayBuffer
*/
constructor (data, params) {
const p = params || {}
let dataIsGiven = false
if (data === undefined) {
data = defaultByteLength
}
if (typeof data === 'number') {
data = new ArrayBuffer(data)
} else {
dataIsGiven = true
this._lastWrittenByte = data.byteLength
}
const offset = p.offset ? p.offset >>> 0 : 0
let byteLength = data.byteLength - offset
let dvOffset = offset
if (data.buffer) {
if (data.byteLength !== data.buffer.byteLength) {
dvOffset = data.byteOffset + offset
}
data = data.buffer
}
if (dataIsGiven) {
this._lastWrittenByte = byteLength
} else {
this._lastWrittenByte = 0
}
/**
* Reference to the internal ArrayBuffer object
* @type {ArrayBuffer}
*/
this.buffer = data
/**
* Byte length of the internal ArrayBuffer
* @type {Number}
*/
this.length = byteLength
/**
* Byte length of the internal ArrayBuffer
* @type {Number}
*/
this.byteLength = byteLength
/**
* Byte offset of the internal ArrayBuffer
* @type {Number}
*/
this.byteOffset = dvOffset
/**
* The current offset of the buffer's pointer
* @type {Number}
*/
this.offset = 0
this.littleEndian = true
this._data = new DataView(this.buffer, dvOffset, byteLength)
this._mark = 0
this._marks = []
}
/**
* Checks if the memory allocated to the buffer is sufficient to store more bytes after the offset
* @param {number} [byteLength=1] The needed memory in bytes
* @return {boolean} Returns true if there is sufficient space and false otherwise
*/
available (byteLength) {
if (byteLength === undefined) byteLength = 1
return (this.offset + byteLength) <= this.length
}
/**
* Check if little-endian mode is used for reading and writing multi-byte values
* @return {boolean} Returns true if little-endian mode is used, false otherwise
*/
isLittleEndian () {
return this.littleEndian
}
/**
* Set little-endian mode for reading and writing multi-byte values
* @return {IOBuffer}
*/
setLittleEndian () {
this.littleEndian = true
return this
}
/**
* Check if big-endian mode is used for reading and writing multi-byte values
* @return {boolean} Returns true if big-endian mode is used, false otherwise
*/
isBigEndian () {
return !this.littleEndian
}
/**
* Switches to big-endian mode for reading and writing multi-byte values
* @return {IOBuffer}
*/
setBigEndian () {
this.littleEndian = false
return this
}
/**
* Move the pointer n bytes forward
* @param {number} n
* @return {IOBuffer}
*/
skip (n) {
if (n === undefined) n = 1
this.offset += n
return this
}
/**
* Move the pointer to the given offset
* @param {number} offset
* @return {IOBuffer}
*/
seek (offset) {
this.offset = offset
return this
}
/**
* Store the current pointer offset.
* @see {@link IOBuffer#reset}
* @return {IOBuffer}
*/
mark () {
this._mark = this.offset
return this
}
/**
* Move the pointer back to the last pointer offset set by mark
* @see {@link IOBuffer#mark}
* @return {IOBuffer}
*/
reset () {
this.offset = this._mark
return this
}
/**
* Push the current pointer offset to the mark stack
* @see {@link IOBuffer#popMark}
* @return {IOBuffer}
*/
pushMark () {
this._marks.push(this.offset)
return this
}
/**
* Pop the last pointer offset from the mark stack, and set the current pointer offset to the popped value
* @see {@link IOBuffer#pushMark}
* @return {IOBuffer}
*/
popMark () {
const offset = this._marks.pop()
if (offset === undefined) throw new Error('Mark stack empty')
this.seek(offset)
return this
}
/**
* Move the pointer offset back to 0
* @return {IOBuffer}
*/
rewind () {
this.offset = 0
return this
}
/**
* Make sure the buffer has sufficient memory to write a given byteLength at the current pointer offset
* If the buffer's memory is insufficient, this method will create a new buffer (a copy) with a length
* that is twice (byteLength + current offset)
* @param {number} [byteLength = 1]
* @return {IOBuffer}
*/
ensureAvailable (byteLength) {
if (byteLength === undefined) byteLength = 1
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 = this.byteLength = newLength
this._data = new DataView(this.buffer)
}
return this
}
/**
* Read a byte and return false if the byte's value is 0, or true otherwise
* Moves pointer forward
* @return {boolean}
*/
readBoolean () {
return this.readUint8() !== 0
}
/**
* Read a signed 8-bit integer and move pointer forward
* @return {number}
*/
readInt8 () {
return this._data.getInt8(this.offset++)
}
/**
* Read an unsigned 8-bit integer and move pointer forward
* @return {number}
*/
readUint8 () {
return this._data.getUint8(this.offset++)
}
/**
* Alias for {@link IOBuffer#readUint8}
* @return {number}
*/
readByte () {
return this.readUint8()
}
/**
* Read n bytes and move pointer forward.
* @param {number} n
* @return {Uint8Array}
*/
readBytes (n) {
if (n === undefined) n = 1
var bytes = new Uint8Array(n)
for (var i = 0; i < n; i++) {
bytes[i] = this.readByte()
}
return bytes
}
/**
* Read a 16-bit signed integer and move pointer forward
* @return {number}
*/
readInt16 () {
var value = this._data.getInt16(this.offset, this.littleEndian)
this.offset += 2
return value
}
/**
* Read a 16-bit unsigned integer and move pointer forward
* @return {number}
*/
readUint16 () {
var value = this._data.getUint16(this.offset, this.littleEndian)
this.offset += 2
return value
}
/**
* Read a 32-bit signed integer and move pointer forward
* @return {number}
*/
readInt32 () {
var value = this._data.getInt32(this.offset, this.littleEndian)
this.offset += 4
return value
}
/**
* Read a 32-bit unsigned integer and move pointer forward
* @return {number}
*/
readUint32 () {
var value = this._data.getUint32(this.offset, this.littleEndian)
this.offset += 4
return value
}
/**
* Read a 32-bit floating number and move pointer forward
* @return {number}
*/
readFloat32 () {
var value = this._data.getFloat32(this.offset, this.littleEndian)
this.offset += 4
return value
}
/**
* Read a 64-bit floating number and move pointer forward
* @return {number}
*/
readFloat64 () {
var value = this._data.getFloat64(this.offset, this.littleEndian)
this.offset += 8
return value
}
/**
* Read 1-byte ascii character and move pointer forward
* @return {string}
*/
readChar () {
return String.fromCharCode(this.readInt8())
}
/**
* Read n 1-byte ascii characters and move pointer forward
* @param {number} n
* @return {string}
*/
readChars (n) {
if (n === undefined) n = 1
charArray.length = n
for (var i = 0; i < n; i++) {
charArray[i] = this.readChar()
}
return charArray.join('')
}
/**
* Write 0xff if the passed value is truthy, 0x00 otherwise
* @param {any} value
* @return {IOBuffer}
*/
writeBoolean (value) {
this.writeUint8(value ? 0xff : 0x00)
return this
}
/**
* Write value as an 8-bit signed integer
* @param {number} value
* @return {IOBuffer}
*/
writeInt8 (value) {
this.ensureAvailable(1)
this._data.setInt8(this.offset++, value)
this._updateLastWrittenByte()
return this
}
/**
* Write value as a 8-bit unsigned integer
* @param {number} value
* @return {IOBuffer}
*/
writeUint8 (value) {
this.ensureAvailable(1)
this._data.setUint8(this.offset++, value)
this._updateLastWrittenByte()
return this
}
/**
* An alias for {@link IOBuffer#writeUint8}
* @param {number} value
* @return {IOBuffer}
*/
writeByte (value) {
return this.writeUint8(value)
}
/**
* Write bytes
* @param {Array|Uint8Array} bytes
* @return {IOBuffer}
*/
writeBytes (bytes) {
this.ensureAvailable(bytes.length)
for (var i = 0; i < bytes.length; i++) {
this._data.setUint8(this.offset++, bytes[i])
}
this._updateLastWrittenByte()
return this
}
/**
* Write value as an 16-bit signed integer
* @param {number} value
* @return {IOBuffer}
*/
writeInt16 (value) {
this.ensureAvailable(2)
this._data.setInt16(this.offset, value, this.littleEndian)
this.offset += 2
this._updateLastWrittenByte()
return this
}
/**
* Write value as a 16-bit unsigned integer
* @param {number} value
* @return {IOBuffer}
*/
writeUint16 (value) {
this.ensureAvailable(2)
this._data.setUint16(this.offset, value, this.littleEndian)
this.offset += 2
this._updateLastWrittenByte()
return this
}
/**
* Write a 32-bit signed integer at the current pointer offset
* @param {number} value
* @return {IOBuffer}
*/
writeInt32 (value) {
this.ensureAvailable(4)
this._data.setInt32(this.offset, value, this.littleEndian)
this.offset += 4
this._updateLastWrittenByte()
return this
}
/**
* Write a 32-bit unsigned integer at the current pointer offset
* @param {number} value - The value to set
* @return {IOBuffer}
*/
writeUint32 (value) {
this.ensureAvailable(4)
this._data.setUint32(this.offset, value, this.littleEndian)
this.offset += 4
this._updateLastWrittenByte()
return this
}
/**
* Write a 32-bit floating number at the current pointer offset
* @param {number} value - The value to set
* @return {IOBuffer}
*/
writeFloat32 (value) {
this.ensureAvailable(4)
this._data.setFloat32(this.offset, value, this.littleEndian)
this.offset += 4
this._updateLastWrittenByte()
return this
}
/**
* Write a 64-bit floating number at the current pointer offset
* @param {number} value
* @return {IOBuffer}
*/
writeFloat64 (value) {
this.ensureAvailable(8)
this._data.setFloat64(this.offset, value, this.littleEndian)
this.offset += 8
this._updateLastWrittenByte()
return this
}
/**
* Write the charCode of the passed string's first character to the current pointer offset
* @param {string} str - The character to set
* @return {IOBuffer}
*/
writeChar (str) {
return this.writeUint8(str.charCodeAt(0))
}
/**
* Write the charCodes of the passed string's characters to the current pointer offset
* @param {string} str
* @return {IOBuffer}
*/
writeChars (str) {
for (var i = 0; i < str.length; i++) {
this.writeUint8(str.charCodeAt(i))
}
return this
}
/**
* Export a Uint8Array view of the internal buffer.
* The view starts at the byte offset and its length
* is calculated to stop at the last written byte or the original length.
* @return {Uint8Array}
*/
toArray () {
return new Uint8Array(this.buffer, this.byteOffset, this._lastWrittenByte)
}
/**
* Same as {@link IOBuffer#toArray} but returns a Buffer if possible. Otherwise returns a Uint8Array.
* @return {Buffer|Uint8Array}
*/
getBuffer () {
if (typeof Buffer !== 'undefined') {
return Buffer.from(this.toArray())
} else {
return this.toArray()
}
}
/**
* Update the last written byte offset
* @private
*/
_updateLastWrittenByte () {
if (this.offset > this._lastWrittenByte) {
this._lastWrittenByte = this.offset
}
}
}
export default IOBuffer