parser.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /**
  2. * @internal
  3. */
  4. const NULL = 0;
  5. /**
  6. * @internal
  7. */
  8. const LF = 10;
  9. /**
  10. * @internal
  11. */
  12. const CR = 13;
  13. /**
  14. * @internal
  15. */
  16. const COLON = 58;
  17. /**
  18. * This is an evented, rec descent parser.
  19. * A stream of Octets can be passed and whenever it recognizes
  20. * a complete Frame or an incoming ping it will invoke the registered callbacks.
  21. *
  22. * All incoming Octets are fed into _onByte function.
  23. * Depending on current state the _onByte function keeps changing.
  24. * Depending on the state it keeps accumulating into _token and _results.
  25. * State is indicated by current value of _onByte, all states are named as _collect.
  26. *
  27. * STOMP standards https://stomp.github.io/stomp-specification-1.2.html
  28. * imply that all lengths are considered in bytes (instead of string lengths).
  29. * So, before actual parsing, if the incoming data is String it is converted to Octets.
  30. * This allows faithful implementation of the protocol and allows NULL Octets to be present in the body.
  31. *
  32. * There is no peek function on the incoming data.
  33. * When a state change occurs based on an Octet without consuming the Octet,
  34. * the Octet, after state change, is fed again (_reinjectByte).
  35. * This became possible as the state change can be determined by inspecting just one Octet.
  36. *
  37. * There are two modes to collect the body, if content-length header is there then it by counting Octets
  38. * otherwise it is determined by NULL terminator.
  39. *
  40. * Following the standards, the command and headers are converted to Strings
  41. * and the body is returned as Octets.
  42. * Headers are returned as an array and not as Hash - to allow multiple occurrence of an header.
  43. *
  44. * This parser does not use Regular Expressions as that can only operate on Strings.
  45. *
  46. * It handles if multiple STOMP frames are given as one chunk, a frame is split into multiple chunks, or
  47. * any combination there of. The parser remembers its state (any partial frame) and continues when a new chunk
  48. * is pushed.
  49. *
  50. * Typically the higher level function will convert headers to Hash, handle unescaping of header values
  51. * (which is protocol version specific), and convert body to text.
  52. *
  53. * Check the parser.spec.js to understand cases that this parser is supposed to handle.
  54. *
  55. * Part of `@stomp/stompjs`.
  56. *
  57. * @internal
  58. */
  59. export class Parser {
  60. constructor(onFrame, onIncomingPing) {
  61. this.onFrame = onFrame;
  62. this.onIncomingPing = onIncomingPing;
  63. this._encoder = new TextEncoder();
  64. this._decoder = new TextDecoder();
  65. this._token = [];
  66. this._initState();
  67. }
  68. parseChunk(segment, appendMissingNULLonIncoming = false) {
  69. let chunk;
  70. if (typeof segment === 'string') {
  71. chunk = this._encoder.encode(segment);
  72. }
  73. else {
  74. chunk = new Uint8Array(segment);
  75. }
  76. // See https://github.com/stomp-js/stompjs/issues/89
  77. // Remove when underlying issue is fixed.
  78. //
  79. // Send a NULL byte, if the last byte of a Text frame was not NULL.F
  80. if (appendMissingNULLonIncoming && chunk[chunk.length - 1] !== 0) {
  81. const chunkWithNull = new Uint8Array(chunk.length + 1);
  82. chunkWithNull.set(chunk, 0);
  83. chunkWithNull[chunk.length] = 0;
  84. chunk = chunkWithNull;
  85. }
  86. // tslint:disable-next-line:prefer-for-of
  87. for (let i = 0; i < chunk.length; i++) {
  88. const byte = chunk[i];
  89. this._onByte(byte);
  90. }
  91. }
  92. // The following implements a simple Rec Descent Parser.
  93. // The grammar is simple and just one byte tells what should be the next state
  94. _collectFrame(byte) {
  95. if (byte === NULL) {
  96. // Ignore
  97. return;
  98. }
  99. if (byte === CR) {
  100. // Ignore CR
  101. return;
  102. }
  103. if (byte === LF) {
  104. // Incoming Ping
  105. this.onIncomingPing();
  106. return;
  107. }
  108. this._onByte = this._collectCommand;
  109. this._reinjectByte(byte);
  110. }
  111. _collectCommand(byte) {
  112. if (byte === CR) {
  113. // Ignore CR
  114. return;
  115. }
  116. if (byte === LF) {
  117. this._results.command = this._consumeTokenAsUTF8();
  118. this._onByte = this._collectHeaders;
  119. return;
  120. }
  121. this._consumeByte(byte);
  122. }
  123. _collectHeaders(byte) {
  124. if (byte === CR) {
  125. // Ignore CR
  126. return;
  127. }
  128. if (byte === LF) {
  129. this._setupCollectBody();
  130. return;
  131. }
  132. this._onByte = this._collectHeaderKey;
  133. this._reinjectByte(byte);
  134. }
  135. _reinjectByte(byte) {
  136. this._onByte(byte);
  137. }
  138. _collectHeaderKey(byte) {
  139. if (byte === COLON) {
  140. this._headerKey = this._consumeTokenAsUTF8();
  141. this._onByte = this._collectHeaderValue;
  142. return;
  143. }
  144. this._consumeByte(byte);
  145. }
  146. _collectHeaderValue(byte) {
  147. if (byte === CR) {
  148. // Ignore CR
  149. return;
  150. }
  151. if (byte === LF) {
  152. this._results.headers.push([
  153. this._headerKey,
  154. this._consumeTokenAsUTF8(),
  155. ]);
  156. this._headerKey = undefined;
  157. this._onByte = this._collectHeaders;
  158. return;
  159. }
  160. this._consumeByte(byte);
  161. }
  162. _setupCollectBody() {
  163. const contentLengthHeader = this._results.headers.filter((header) => {
  164. return header[0] === 'content-length';
  165. })[0];
  166. if (contentLengthHeader) {
  167. this._bodyBytesRemaining = parseInt(contentLengthHeader[1], 10);
  168. this._onByte = this._collectBodyFixedSize;
  169. }
  170. else {
  171. this._onByte = this._collectBodyNullTerminated;
  172. }
  173. }
  174. _collectBodyNullTerminated(byte) {
  175. if (byte === NULL) {
  176. this._retrievedBody();
  177. return;
  178. }
  179. this._consumeByte(byte);
  180. }
  181. _collectBodyFixedSize(byte) {
  182. // It is post decrement, so that we discard the trailing NULL octet
  183. if (this._bodyBytesRemaining-- === 0) {
  184. this._retrievedBody();
  185. return;
  186. }
  187. this._consumeByte(byte);
  188. }
  189. _retrievedBody() {
  190. this._results.binaryBody = this._consumeTokenAsRaw();
  191. try {
  192. this.onFrame(this._results);
  193. }
  194. catch (e) {
  195. console.log(`Ignoring an exception thrown by a frame handler. Original exception: `, e);
  196. }
  197. this._initState();
  198. }
  199. // Rec Descent Parser helpers
  200. _consumeByte(byte) {
  201. this._token.push(byte);
  202. }
  203. _consumeTokenAsUTF8() {
  204. return this._decoder.decode(this._consumeTokenAsRaw());
  205. }
  206. _consumeTokenAsRaw() {
  207. const rawResult = new Uint8Array(this._token);
  208. this._token = [];
  209. return rawResult;
  210. }
  211. _initState() {
  212. this._results = {
  213. command: undefined,
  214. headers: [],
  215. binaryBody: undefined,
  216. };
  217. this._token = [];
  218. this._headerKey = undefined;
  219. this._onByte = this._collectFrame;
  220. }
  221. }
  222. //# sourceMappingURL=parser.js.map