UNPKG

6.88 kBPlain TextView Raw
1import { IRawFrameType } from './types';
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 private _results: IRawFrameType;
67
68 private _token: number[] = [];
69 private _headerKey: string;
70 private _bodyBytesRemaining: number;
71
72 private _onByte: (byte: number) => void;
73
74 public constructor(
75 public onFrame: (rawFrame: IRawFrameType) => void,
76 public onIncomingPing: () => void
77 ) {
78 this._initState();
79 }
80
81 public parseChunk(
82 segment: string | ArrayBuffer,
83 appendMissingNULLonIncoming: boolean = false
84 ) {
85 let chunk: Uint8Array;
86
87 if (segment instanceof ArrayBuffer) {
88 chunk = new Uint8Array(segment);
89 } else {
90 chunk = this._encoder.encode(segment);
91 }
92
93 // See https://github.com/stomp-js/stompjs/issues/89
94 // Remove when underlying issue is fixed.
95 //
96 // Send a NULL byte, if the last byte of a Text frame was not NULL.F
97 if (appendMissingNULLonIncoming && chunk[chunk.length - 1] !== 0) {
98 const chunkWithNull = new Uint8Array(chunk.length + 1);
99 chunkWithNull.set(chunk, 0);
100 chunkWithNull[chunk.length] = 0;
101 chunk = chunkWithNull;
102 }
103
104 // tslint:disable-next-line:prefer-for-of
105 for (let i = 0; i < chunk.length; i++) {
106 const byte = chunk[i];
107 this._onByte(byte);
108 }
109 }
110
111 // The following implements a simple Rec Descent Parser.
112 // The grammar is simple and just one byte tells what should be the next state
113
114 private _collectFrame(byte: number): void {
115 if (byte === NULL) {
116 // Ignore
117 return;
118 }
119 if (byte === CR) {
120 // Ignore CR
121 return;
122 }
123 if (byte === LF) {
124 // Incoming Ping
125 this.onIncomingPing();
126 return;
127 }
128
129 this._onByte = this._collectCommand;
130 this._reinjectByte(byte);
131 }
132
133 private _collectCommand(byte: number): void {
134 if (byte === CR) {
135 // Ignore CR
136 return;
137 }
138 if (byte === LF) {
139 this._results.command = this._consumeTokenAsUTF8();
140 this._onByte = this._collectHeaders;
141 return;
142 }
143
144 this._consumeByte(byte);
145 }
146
147 private _collectHeaders(byte: number): void {
148 if (byte === CR) {
149 // Ignore CR
150 return;
151 }
152 if (byte === LF) {
153 this._setupCollectBody();
154 return;
155 }
156 this._onByte = this._collectHeaderKey;
157 this._reinjectByte(byte);
158 }
159
160 private _reinjectByte(byte: number) {
161 this._onByte(byte);
162 }
163
164 private _collectHeaderKey(byte: number): void {
165 if (byte === COLON) {
166 this._headerKey = this._consumeTokenAsUTF8();
167 this._onByte = this._collectHeaderValue;
168 return;
169 }
170 this._consumeByte(byte);
171 }
172
173 private _collectHeaderValue(byte: number): void {
174 if (byte === CR) {
175 // Ignore CR
176 return;
177 }
178 if (byte === LF) {
179 this._results.headers.push([this._headerKey, this._consumeTokenAsUTF8()]);
180 this._headerKey = undefined;
181 this._onByte = this._collectHeaders;
182 return;
183 }
184 this._consumeByte(byte);
185 }
186
187 private _setupCollectBody() {
188 const contentLengthHeader = this._results.headers.filter(
189 (header: [string, string]) => {
190 return header[0] === 'content-length';
191 }
192 )[0];
193
194 if (contentLengthHeader) {
195 this._bodyBytesRemaining = parseInt(contentLengthHeader[1], 10);
196 this._onByte = this._collectBodyFixedSize;
197 } else {
198 this._onByte = this._collectBodyNullTerminated;
199 }
200 }
201
202 private _collectBodyNullTerminated(byte: number): void {
203 if (byte === NULL) {
204 this._retrievedBody();
205 return;
206 }
207 this._consumeByte(byte);
208 }
209
210 private _collectBodyFixedSize(byte: number): void {
211 // It is post decrement, so that we discard the trailing NULL octet
212 if (this._bodyBytesRemaining-- === 0) {
213 this._retrievedBody();
214 return;
215 }
216 this._consumeByte(byte);
217 }
218
219 private _retrievedBody() {
220 this._results.binaryBody = this._consumeTokenAsRaw();
221
222 this.onFrame(this._results);
223
224 this._initState();
225 }
226
227 // Rec Descent Parser helpers
228
229 private _consumeByte(byte: number) {
230 this._token.push(byte);
231 }
232
233 private _consumeTokenAsUTF8() {
234 return this._decoder.decode(this._consumeTokenAsRaw());
235 }
236
237 private _consumeTokenAsRaw() {
238 const rawResult = new Uint8Array(this._token);
239 this._token = [];
240 return rawResult;
241 }
242
243 private _initState() {
244 this._results = {
245 command: undefined,
246 headers: [],
247 binaryBody: undefined,
248 };
249
250 this._token = [];
251 this._headerKey = undefined;
252
253 this._onByte = this._collectFrame;
254 }
255}