UNPKG

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