192 lines
4.7 KiB
JavaScript
192 lines
4.7 KiB
JavaScript
/**
|
|
* @module decoding
|
|
*/
|
|
|
|
/* global Buffer */
|
|
|
|
import * as globals from './globals.mjs'
|
|
|
|
/**
|
|
* A Decoder handles the decoding of an ArrayBuffer.
|
|
*/
|
|
export class Decoder {
|
|
/**
|
|
* @param {ArrayBuffer} buffer Binary data to decode
|
|
*/
|
|
constructor (buffer) {
|
|
this.arr = new Uint8Array(buffer)
|
|
this.pos = 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @function
|
|
* @param {ArrayBuffer} buffer
|
|
* @return {Decoder}
|
|
*/
|
|
export const createDecoder = buffer => new Decoder(buffer)
|
|
|
|
/**
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {boolean}
|
|
*/
|
|
export const hasContent = decoder => decoder.pos !== decoder.arr.length
|
|
|
|
/**
|
|
* Clone a decoder instance.
|
|
* Optionally set a new position parameter.
|
|
*
|
|
* @function
|
|
* @param {Decoder} decoder The decoder instance
|
|
* @param {number} [newPos] Defaults to current position
|
|
* @return {Decoder} A clone of `decoder`
|
|
*/
|
|
export const clone = (decoder, newPos = decoder.pos) => {
|
|
let _decoder = createDecoder(decoder.arr.buffer)
|
|
_decoder.pos = newPos
|
|
return _decoder
|
|
}
|
|
|
|
/**
|
|
* Read `len` bytes as an ArrayBuffer.
|
|
* @function
|
|
* @param {Decoder} decoder The decoder instance
|
|
* @param {number} len The length of bytes to read
|
|
* @return {ArrayBuffer}
|
|
*/
|
|
export const readArrayBuffer = (decoder, len) => {
|
|
const arrayBuffer = globals.createUint8ArrayFromLen(len)
|
|
const view = globals.createUint8ArrayFromBuffer(decoder.arr.buffer, decoder.pos, len)
|
|
arrayBuffer.set(view)
|
|
decoder.pos += len
|
|
return arrayBuffer.buffer
|
|
}
|
|
|
|
/**
|
|
* Read variable length payload as ArrayBuffer
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {ArrayBuffer}
|
|
*/
|
|
export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decoder))
|
|
|
|
/**
|
|
* Read the rest of the content as an ArrayBuffer
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {ArrayBuffer}
|
|
*/
|
|
export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length - decoder.pos)
|
|
|
|
/**
|
|
* Skip one byte, jump to the next position.
|
|
* @function
|
|
* @param {Decoder} decoder The decoder instance
|
|
* @return {number} The next position
|
|
*/
|
|
export const skip8 = decoder => decoder.pos++
|
|
|
|
/**
|
|
* Read one byte as unsigned integer.
|
|
* @function
|
|
* @param {Decoder} decoder The decoder instance
|
|
* @return {number} Unsigned 8-bit integer
|
|
*/
|
|
export const readUint8 = decoder => decoder.arr[decoder.pos++]
|
|
|
|
/**
|
|
* Read 4 bytes as unsigned integer.
|
|
*
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {number} An unsigned integer.
|
|
*/
|
|
export const readUint32 = decoder => {
|
|
let uint =
|
|
decoder.arr[decoder.pos] +
|
|
(decoder.arr[decoder.pos + 1] << 8) +
|
|
(decoder.arr[decoder.pos + 2] << 16) +
|
|
(decoder.arr[decoder.pos + 3] << 24)
|
|
decoder.pos += 4
|
|
return uint
|
|
}
|
|
|
|
/**
|
|
* Look ahead without incrementing position.
|
|
* to the next byte and read it as unsigned integer.
|
|
*
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {number} An unsigned integer.
|
|
*/
|
|
export const peekUint8 = decoder => decoder.arr[decoder.pos]
|
|
|
|
/**
|
|
* Read unsigned integer (32bit) with variable length.
|
|
* 1/8th of the storage is used as encoding overhead.
|
|
* * numbers < 2^7 is stored in one bytlength
|
|
* * numbers < 2^14 is stored in two bylength
|
|
*
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {number} An unsigned integer.length
|
|
*/
|
|
export const readVarUint = decoder => {
|
|
let num = 0
|
|
let len = 0
|
|
while (true) {
|
|
let r = decoder.arr[decoder.pos++]
|
|
num = num | ((r & 0b1111111) << len)
|
|
len += 7
|
|
if (r < 1 << 7) {
|
|
return num >>> 0 // return unsigned number!
|
|
}
|
|
if (len > 35) {
|
|
throw new Error('Integer out of range!')
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read string of variable length
|
|
* * varUint is used to store the length of the string
|
|
*
|
|
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
|
|
* when String.fromCodePoint is fed with all characters as arguments.
|
|
* But most environments have a maximum number of arguments per functions.
|
|
* For effiency reasons we apply a maximum of 10000 characters at once.
|
|
*
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {String} The read String.
|
|
*/
|
|
export const readVarString = decoder => {
|
|
let remainingLen = readVarUint(decoder)
|
|
let encodedString = ''
|
|
while (remainingLen > 0) {
|
|
const nextLen = remainingLen < 10000 ? remainingLen : 10000
|
|
const bytes = new Array(nextLen)
|
|
for (let i = 0; i < nextLen; i++) {
|
|
bytes[i] = decoder.arr[decoder.pos++]
|
|
}
|
|
encodedString += String.fromCodePoint.apply(null, bytes)
|
|
remainingLen -= nextLen
|
|
}
|
|
return decodeURIComponent(escape(encodedString))
|
|
}
|
|
|
|
/**
|
|
* Look ahead and read varString without incrementing position
|
|
*
|
|
* @function
|
|
* @param {Decoder} decoder
|
|
* @return {string}
|
|
*/
|
|
export const peekVarString = decoder => {
|
|
let pos = decoder.pos
|
|
let s = readVarString(decoder)
|
|
decoder.pos = pos
|
|
return s
|
|
}
|