Stream.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. let found = false;
  140. while (!found && this.position < this.length) {
  141. while (++this.position < this.length && this.bytes[this.position] !== val[0]) {
  142. continue;
  143. }
  144. found = true;
  145. for (let i = 1; i < val.length; i++) {
  146. if (this.position + i > this.length || this.bytes[this.position + i] !== val[i])
  147. found = false;
  148. }
  149. }
  150. }
  151. /**
  152. * Consume the next byte if it matches the supplied value.
  153. *
  154. * @param {number} val
  155. */
  156. consumeIf(val) {
  157. if (this.bytes[this.position] === val) {
  158. this.position++;
  159. this.bitPos = 0;
  160. }
  161. }
  162. /**
  163. * Move forwards through the stream by the specified number of bytes.
  164. *
  165. * @param {number} numBytes
  166. */
  167. moveForwardsBy(numBytes) {
  168. const pos = this.position + numBytes;
  169. if (pos < 0 || pos > this.length)
  170. throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
  171. this.position = pos;
  172. this.bitPos = 0;
  173. }
  174. /**
  175. * Move backwards through the stream by the specified number of bytes.
  176. *
  177. * @param {number} numBytes
  178. */
  179. moveBackwardsBy(numBytes) {
  180. const pos = this.position - numBytes;
  181. if (pos < 0 || pos > this.length)
  182. throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
  183. this.position = pos;
  184. this.bitPos = 0;
  185. }
  186. /**
  187. * Move backwards through the strem by the specified number of bits.
  188. *
  189. * @param {number} numBits
  190. */
  191. moveBackwardsByBits(numBits) {
  192. if (numBits <= this.bitPos) {
  193. this.bitPos -= numBits;
  194. } else {
  195. if (this.bitPos > 0) {
  196. numBits -= this.bitPos;
  197. this.bitPos = 0;
  198. }
  199. while (numBits > 0) {
  200. this.moveBackwardsBy(1);
  201. this.bitPos = 8;
  202. this.moveBackwardsByBits(numBits);
  203. numBits -= 8;
  204. }
  205. }
  206. }
  207. /**
  208. * Move to a specified position in the stream.
  209. *
  210. * @param {number} pos
  211. */
  212. moveTo(pos) {
  213. if (pos < 0 || pos > this.length)
  214. throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
  215. this.position = pos;
  216. this.bitPos = 0;
  217. }
  218. /**
  219. * Returns true if there are more bytes left in the stream.
  220. *
  221. * @returns {boolean}
  222. */
  223. hasMore() {
  224. return this.position < this.length;
  225. }
  226. /**
  227. * Returns a slice of the stream up to the current position.
  228. *
  229. * @returns {Uint8Array}
  230. */
  231. carve() {
  232. if (this.bitPos > 0) this.position++;
  233. return this.bytes.slice(0, this.position);
  234. }
  235. }