Stream.mjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * Stream class for parsing binary protocols.
  3. *
  4. * @author n1474335 [n1474335@gmail.com]
  5. * @author tlwr [toby@toby.codes]
  6. * @copyright Crown Copyright 2018
  7. * @license Apache-2.0
  8. *
  9. */
  10. /**
  11. * A Stream can be used to traverse a binary blob, interpreting sections of it
  12. * as various data types.
  13. */
  14. export default class Stream {
  15. /**
  16. * Stream constructor.
  17. *
  18. * @param {Uint8Array} input
  19. */
  20. constructor(input) {
  21. this.bytes = input;
  22. this.length = this.bytes.length;
  23. this.position = 0;
  24. this.bitPos = 0;
  25. }
  26. /**
  27. * Get a number of bytes from the current position.
  28. *
  29. * @param {number} numBytes
  30. * @returns {Uint8Array}
  31. */
  32. getBytes(numBytes) {
  33. if (this.position > this.length) return undefined;
  34. const newPosition = this.position + numBytes;
  35. const bytes = this.bytes.slice(this.position, newPosition);
  36. this.position = newPosition;
  37. this.bitPos = 0;
  38. return bytes;
  39. }
  40. /**
  41. * Interpret the following bytes as a string, stopping at the next null byte or
  42. * the supplied limit.
  43. *
  44. * @param {number} numBytes
  45. * @returns {string}
  46. */
  47. readString(numBytes) {
  48. if (this.position > this.length) return undefined;
  49. let result = "";
  50. for (let i = this.position; i < this.position + numBytes; i++) {
  51. const currentByte = this.bytes[i];
  52. if (currentByte === 0) break;
  53. result += String.fromCharCode(currentByte);
  54. }
  55. this.position += numBytes;
  56. this.bitPos = 0;
  57. return result;
  58. }
  59. /**
  60. * Interpret the following bytes as an integer in big or little endian.
  61. *
  62. * @param {number} numBytes
  63. * @param {string} [endianness="be"]
  64. * @returns {number}
  65. */
  66. readInt(numBytes, endianness="be") {
  67. if (this.position > this.length) return undefined;
  68. let val = 0;
  69. if (endianness === "be") {
  70. for (let i = this.position; i < this.position + numBytes; i++) {
  71. val = val << 8;
  72. val |= this.bytes[i];
  73. }
  74. } else {
  75. for (let i = this.position + numBytes - 1; i >= this.position; i--) {
  76. val = val << 8;
  77. val |= this.bytes[i];
  78. }
  79. }
  80. this.position += numBytes;
  81. this.bitPos = 0;
  82. return val;
  83. }
  84. /**
  85. * Reads a number of bits from the buffer.
  86. *
  87. * @TODO Add endianness
  88. *
  89. * @param {number} numBits
  90. * @returns {number}
  91. */
  92. readBits(numBits) {
  93. if (this.position > this.length) return undefined;
  94. let bitBuf = 0,
  95. bitBufLen = 0;
  96. // Add remaining bits from current byte
  97. bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
  98. bitBufLen = 8 - this.bitPos;
  99. this.bitPos = 0;
  100. // Not enough bits yet
  101. while (bitBufLen < numBits) {
  102. bitBuf |= this.bytes[this.position++] << bitBufLen;
  103. bitBufLen += 8;
  104. }
  105. // Reverse back to numBits
  106. if (bitBufLen > numBits) {
  107. const excess = bitBufLen - numBits;
  108. bitBuf &= (1 << numBits) - 1;
  109. bitBufLen -= excess;
  110. this.position--;
  111. this.bitPos = 8 - excess;
  112. }
  113. return bitBuf;
  114. /**
  115. * Calculates the bit mask based on the current bit position.
  116. *
  117. * @param {number} bitPos
  118. * @returns {number} The bit mask
  119. */
  120. function bitMask(bitPos) {
  121. return 256 - (1 << bitPos);
  122. }
  123. }
  124. /**
  125. * Consume the stream until we reach the specified byte or sequence of bytes.
  126. *
  127. * @param {number|List<number>} val
  128. */
  129. continueUntil(val) {
  130. if (this.position > this.length) return;
  131. this.bitPos = 0;
  132. if (typeof val === "number") {
  133. while (++this.position < this.length && this.bytes[this.position] !== val) {
  134. continue;
  135. }
  136. return;
  137. }
  138. // val is an array
  139. /**
  140. * Builds the skip forward table from the value to be searched.
  141. *
  142. * @param {Uint8Array} val
  143. * @param {Number} len
  144. * @returns {Uint8Array}
  145. */
  146. function preprocess(val, len) {
  147. const skiptable = new Array();
  148. val.forEach((element, index) => {
  149. skiptable[element] = len - index;
  150. });
  151. return skiptable;
  152. }
  153. const length = val.length;
  154. const initial = val[length-1];
  155. this.position = length;
  156. // Get the skip table.
  157. const skiptable = preprocess(val, length);
  158. let found = true;
  159. while (this.position < this.length) {
  160. // Until we hit the final element of val in the stream.
  161. while ((this.position < this.length) && (this.bytes[this.position++] !== initial));
  162. found = true;
  163. // Loop through the elements comparing them to val.
  164. for (let x = length-1; x >= 0; x--) {
  165. if (this.bytes[this.position - length + x] !== val[x]) {
  166. found = false;
  167. // If element is not equal to val's element then jump forward by the correct amount.
  168. this.position += skiptable[val[x]];
  169. break;
  170. }
  171. }
  172. if (found) {
  173. this.position -= length;
  174. break;
  175. }
  176. }
  177. }
  178. /**
  179. * Consume bytes if they match the supplied value.
  180. *
  181. * @param {Number} val
  182. */
  183. consumeWhile(val) {
  184. while (this.position < this.length) {
  185. if (this.bytes[this.position] !== val) {
  186. break;
  187. }
  188. this.position++;
  189. }
  190. this.bitPos = 0;
  191. }
  192. /**
  193. * Consume the next byte if it matches the supplied value.
  194. *
  195. * @param {number} val
  196. */
  197. consumeIf(val) {
  198. if (this.bytes[this.position] === val) {
  199. this.position++;
  200. this.bitPos = 0;
  201. }
  202. }
  203. /**
  204. * Move forwards through the stream by the specified number of bytes.
  205. *
  206. * @param {number} numBytes
  207. */
  208. moveForwardsBy(numBytes) {
  209. const pos = this.position + numBytes;
  210. if (pos < 0 || pos > this.length)
  211. throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
  212. this.position = pos;
  213. this.bitPos = 0;
  214. }
  215. /**
  216. * Move backwards through the stream by the specified number of bytes.
  217. *
  218. * @param {number} numBytes
  219. */
  220. moveBackwardsBy(numBytes) {
  221. const pos = this.position - numBytes;
  222. if (pos < 0 || pos > this.length)
  223. throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
  224. this.position = pos;
  225. this.bitPos = 0;
  226. }
  227. /**
  228. * Move backwards through the strem by the specified number of bits.
  229. *
  230. * @param {number} numBits
  231. */
  232. moveBackwardsByBits(numBits) {
  233. if (numBits <= this.bitPos) {
  234. this.bitPos -= numBits;
  235. } else {
  236. if (this.bitPos > 0) {
  237. numBits -= this.bitPos;
  238. this.bitPos = 0;
  239. }
  240. while (numBits > 0) {
  241. this.moveBackwardsBy(1);
  242. this.bitPos = 8;
  243. this.moveBackwardsByBits(numBits);
  244. numBits -= 8;
  245. }
  246. }
  247. }
  248. /**
  249. * Move to a specified position in the stream.
  250. *
  251. * @param {number} pos
  252. */
  253. moveTo(pos) {
  254. if (pos < 0 || pos > this.length)
  255. throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
  256. this.position = pos;
  257. this.bitPos = 0;
  258. }
  259. /**
  260. * Returns true if there are more bytes left in the stream.
  261. *
  262. * @returns {boolean}
  263. */
  264. hasMore() {
  265. return this.position < this.length;
  266. }
  267. /**
  268. * Returns a slice of the stream up to the current position.
  269. *
  270. * @param {number} [start=0]
  271. * @param {number} [finish=this.position]
  272. * @returns {Uint8Array}
  273. */
  274. carve(start=0, finish=this.position) {
  275. if (this.bitPos > 0) finish++;
  276. return this.bytes.slice(start, finish);
  277. }
  278. }