2018-12-14 16:43:03 +00:00
|
|
|
/**
|
|
|
|
* Stream class for parsing binary protocols.
|
|
|
|
*
|
|
|
|
* @author n1474335 [n1474335@gmail.com]
|
|
|
|
* @author tlwr [toby@toby.codes]
|
|
|
|
* @copyright Crown Copyright 2018
|
|
|
|
* @license Apache-2.0
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A Stream can be used to traverse a binary blob, interpreting sections of it
|
|
|
|
* as various data types.
|
|
|
|
*/
|
|
|
|
export default class Stream {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stream constructor.
|
|
|
|
*
|
|
|
|
* @param {Uint8Array} input
|
|
|
|
*/
|
|
|
|
constructor(input) {
|
|
|
|
this.bytes = input;
|
2019-01-03 18:40:22 +00:00
|
|
|
this.length = this.bytes.length;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.position = 0;
|
|
|
|
this.bitPos = 0;
|
2018-12-14 16:43:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a number of bytes from the current position.
|
|
|
|
*
|
|
|
|
* @param {number} numBytes
|
|
|
|
* @returns {Uint8Array}
|
|
|
|
*/
|
|
|
|
getBytes(numBytes) {
|
2019-01-03 18:40:22 +00:00
|
|
|
if (this.position > this.length) return undefined;
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
const newPosition = this.position + numBytes;
|
|
|
|
const bytes = this.bytes.slice(this.position, newPosition);
|
|
|
|
this.position = newPosition;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
2018-12-14 16:43:03 +00:00
|
|
|
return bytes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Interpret the following bytes as a string, stopping at the next null byte or
|
|
|
|
* the supplied limit.
|
|
|
|
*
|
|
|
|
* @param {number} numBytes
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
readString(numBytes) {
|
2019-01-03 18:40:22 +00:00
|
|
|
if (this.position > this.length) return undefined;
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
let result = "";
|
|
|
|
for (let i = this.position; i < this.position + numBytes; i++) {
|
|
|
|
const currentByte = this.bytes[i];
|
|
|
|
if (currentByte === 0) break;
|
|
|
|
result += String.fromCharCode(currentByte);
|
|
|
|
}
|
|
|
|
this.position += numBytes;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
2018-12-14 16:43:03 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Interpret the following bytes as an integer in big or little endian.
|
|
|
|
*
|
|
|
|
* @param {number} numBytes
|
|
|
|
* @param {string} [endianness="be"]
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
readInt(numBytes, endianness="be") {
|
2019-01-03 18:40:22 +00:00
|
|
|
if (this.position > this.length) return undefined;
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
let val = 0;
|
|
|
|
if (endianness === "be") {
|
|
|
|
for (let i = this.position; i < this.position + numBytes; i++) {
|
|
|
|
val = val << 8;
|
|
|
|
val |= this.bytes[i];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (let i = this.position + numBytes - 1; i >= this.position; i--) {
|
|
|
|
val = val << 8;
|
|
|
|
val |= this.bytes[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.position += numBytes;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
2018-12-14 16:43:03 +00:00
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
2019-01-04 18:12:49 +00:00
|
|
|
/**
|
|
|
|
* Reads a number of bits from the buffer.
|
|
|
|
*
|
|
|
|
* @TODO Add endianness
|
|
|
|
*
|
|
|
|
* @param {number} numBits
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
readBits(numBits) {
|
|
|
|
if (this.position > this.length) return undefined;
|
|
|
|
|
|
|
|
let bitBuf = 0,
|
|
|
|
bitBufLen = 0;
|
|
|
|
|
|
|
|
// Add remaining bits from current byte
|
2019-01-10 17:30:52 +00:00
|
|
|
bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
|
2019-01-04 18:12:49 +00:00
|
|
|
bitBufLen = 8 - this.bitPos;
|
|
|
|
this.bitPos = 0;
|
|
|
|
|
|
|
|
// Not enough bits yet
|
|
|
|
while (bitBufLen < numBits) {
|
|
|
|
bitBuf |= this.bytes[this.position++] << bitBufLen;
|
|
|
|
bitBufLen += 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reverse back to numBits
|
|
|
|
if (bitBufLen > numBits) {
|
|
|
|
const excess = bitBufLen - numBits;
|
2019-01-10 17:30:52 +00:00
|
|
|
bitBuf &= (1 << numBits) - 1;
|
2019-01-04 18:12:49 +00:00
|
|
|
bitBufLen -= excess;
|
|
|
|
this.position--;
|
|
|
|
this.bitPos = 8 - excess;
|
|
|
|
}
|
|
|
|
|
|
|
|
return bitBuf;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates the bit mask based on the current bit position.
|
|
|
|
*
|
|
|
|
* @param {number} bitPos
|
|
|
|
* @returns {number} The bit mask
|
|
|
|
*/
|
|
|
|
function bitMask(bitPos) {
|
2019-01-10 17:30:52 +00:00
|
|
|
return 256 - (1 << bitPos);
|
2019-01-04 18:12:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
/**
|
|
|
|
* Consume the stream until we reach the specified byte or sequence of bytes.
|
|
|
|
*
|
|
|
|
* @param {number|List<number>} val
|
|
|
|
*/
|
|
|
|
continueUntil(val) {
|
2019-01-03 18:40:22 +00:00
|
|
|
if (this.position > this.length) return;
|
|
|
|
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
if (typeof val === "number") {
|
2019-01-03 18:40:22 +00:00
|
|
|
while (++this.position < this.length && this.bytes[this.position] !== val) {
|
2018-12-14 16:43:03 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// val is an array
|
2019-11-11 15:47:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
2019-11-13 09:02:36 +00:00
|
|
|
* Builds the skip forward table from the value to be searched.
|
2019-11-11 15:47:16 +00:00
|
|
|
*
|
2019-11-13 09:02:36 +00:00
|
|
|
* @param {Uint8Array} val
|
|
|
|
* @param {Number} len
|
|
|
|
* @returns {Uint8Array}
|
2019-11-11 15:47:16 +00:00
|
|
|
*/
|
|
|
|
function preprocess(val, len) {
|
|
|
|
const skiptable = new Array();
|
2019-11-13 09:02:36 +00:00
|
|
|
val.forEach((element, index) => {
|
2019-11-11 15:47:16 +00:00
|
|
|
skiptable[element] = len - index;
|
|
|
|
});
|
|
|
|
return skiptable;
|
|
|
|
}
|
|
|
|
|
|
|
|
const length = val.length;
|
|
|
|
|
|
|
|
const initial = val[length-1];
|
|
|
|
|
|
|
|
this.position = length;
|
|
|
|
|
|
|
|
// Get the skip table.
|
|
|
|
const skiptable = preprocess(val, length);
|
|
|
|
let found = true;
|
|
|
|
|
|
|
|
while (this.position < this.length) {
|
|
|
|
|
|
|
|
// Until we hit the final element of val in the stream.
|
|
|
|
while ((this.position < this.length) && (this.bytes[this.position++] !== initial));
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
found = true;
|
2019-11-11 15:47:16 +00:00
|
|
|
|
|
|
|
// Loop through the elements comparing them to val.
|
2019-11-13 09:02:36 +00:00
|
|
|
for (let x = length-1; x > -1; x--) {
|
|
|
|
if (this.bytes[this.position-length + x] !== val[x]) {
|
2018-12-14 16:43:03 +00:00
|
|
|
found = false;
|
2019-11-11 15:47:16 +00:00
|
|
|
|
|
|
|
// If element is not equal to val's element then jump forward by the correct amount.
|
|
|
|
this.position += skiptable[val[x]];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (found) {
|
2019-11-13 09:02:36 +00:00
|
|
|
this.position -= length;
|
2019-11-11 15:47:16 +00:00
|
|
|
break;
|
2018-12-14 16:43:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-11 15:47:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Consume bytes if it matches the supplied value.
|
|
|
|
*
|
2019-11-13 09:02:36 +00:00
|
|
|
* @param {Number} val
|
2019-11-11 15:47:16 +00:00
|
|
|
*/
|
|
|
|
consumeWhile(val) {
|
2019-11-13 09:02:36 +00:00
|
|
|
while ((this.position < this.length) && (this.bytes[(this.position++)] === val))
|
|
|
|
this.position--;
|
2019-11-11 15:47:16 +00:00
|
|
|
}
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
/**
|
|
|
|
* Consume the next byte if it matches the supplied value.
|
|
|
|
*
|
|
|
|
* @param {number} val
|
|
|
|
*/
|
|
|
|
consumeIf(val) {
|
2019-01-04 18:12:49 +00:00
|
|
|
if (this.bytes[this.position] === val) {
|
2018-12-14 16:43:03 +00:00
|
|
|
this.position++;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
|
|
|
}
|
2018-12-14 16:43:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move forwards through the stream by the specified number of bytes.
|
|
|
|
*
|
|
|
|
* @param {number} numBytes
|
|
|
|
*/
|
|
|
|
moveForwardsBy(numBytes) {
|
2019-01-03 18:40:22 +00:00
|
|
|
const pos = this.position + numBytes;
|
|
|
|
if (pos < 0 || pos > this.length)
|
|
|
|
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
|
|
|
this.position = pos;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
2019-01-03 18:40:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Move backwards through the stream by the specified number of bytes.
|
|
|
|
*
|
|
|
|
* @param {number} numBytes
|
|
|
|
*/
|
|
|
|
moveBackwardsBy(numBytes) {
|
|
|
|
const pos = this.position - numBytes;
|
|
|
|
if (pos < 0 || pos > this.length)
|
|
|
|
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
|
|
|
this.position = pos;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
2018-12-14 16:43:03 +00:00
|
|
|
}
|
|
|
|
|
2019-01-11 17:44:13 +00:00
|
|
|
/**
|
|
|
|
* Move backwards through the strem by the specified number of bits.
|
|
|
|
*
|
|
|
|
* @param {number} numBits
|
|
|
|
*/
|
|
|
|
moveBackwardsByBits(numBits) {
|
|
|
|
if (numBits <= this.bitPos) {
|
|
|
|
this.bitPos -= numBits;
|
|
|
|
} else {
|
|
|
|
if (this.bitPos > 0) {
|
|
|
|
numBits -= this.bitPos;
|
|
|
|
this.bitPos = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (numBits > 0) {
|
|
|
|
this.moveBackwardsBy(1);
|
|
|
|
this.bitPos = 8;
|
|
|
|
this.moveBackwardsByBits(numBits);
|
|
|
|
numBits -= 8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-14 16:43:03 +00:00
|
|
|
/**
|
|
|
|
* Move to a specified position in the stream.
|
|
|
|
*
|
|
|
|
* @param {number} pos
|
|
|
|
*/
|
|
|
|
moveTo(pos) {
|
2019-01-03 18:40:22 +00:00
|
|
|
if (pos < 0 || pos > this.length)
|
2018-12-14 16:43:03 +00:00
|
|
|
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
|
|
|
this.position = pos;
|
2019-01-04 18:12:49 +00:00
|
|
|
this.bitPos = 0;
|
2018-12-14 16:43:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if there are more bytes left in the stream.
|
|
|
|
*
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
hasMore() {
|
2019-01-03 18:40:22 +00:00
|
|
|
return this.position < this.length;
|
2018-12-14 16:43:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a slice of the stream up to the current position.
|
|
|
|
*
|
|
|
|
* @returns {Uint8Array}
|
|
|
|
*/
|
|
|
|
carve() {
|
2019-01-04 18:12:49 +00:00
|
|
|
if (this.bitPos > 0) this.position++;
|
2018-12-14 16:43:03 +00:00
|
|
|
return this.bytes.slice(0, this.position);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|