source: imaps-frontend/node_modules/@discoveryjs/json-ext/cjs/parse-chunked.cjs

main
Last change on this file was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 4 days ago

F4 Finalna Verzija

  • Property mode set to 100644
File size: 12.0 KB
Line 
1'use strict';
2
3const utils = require('./utils.cjs');
4
5const STACK_OBJECT = 1;
6const STACK_ARRAY = 2;
7const decoder = new TextDecoder();
8
9function adjustPosition(error, parser) {
10 if (error.name === 'SyntaxError' && parser.jsonParseOffset) {
11 error.message = error.message.replace(/at position (\d+)/, (_, pos) =>
12 'at position ' + (Number(pos) + parser.jsonParseOffset)
13 );
14 }
15
16 return error;
17}
18
19function append(array, elements) {
20 // Note: Avoid to use array.push(...elements) since it may lead to
21 // "RangeError: Maximum call stack size exceeded" for a long arrays
22 const initialLength = array.length;
23 array.length += elements.length;
24
25 for (let i = 0; i < elements.length; i++) {
26 array[initialLength + i] = elements[i];
27 }
28}
29
30async function parseChunked(chunkEmitter) {
31 const iterable = typeof chunkEmitter === 'function'
32 ? chunkEmitter()
33 : chunkEmitter;
34
35 if (utils.isIterable(iterable)) {
36 let parser = new ChunkParser();
37
38 try {
39 for await (const chunk of iterable) {
40 if (typeof chunk !== 'string' && !ArrayBuffer.isView(chunk)) {
41 throw new TypeError('Invalid chunk: Expected string, TypedArray or Buffer');
42 }
43
44 parser.push(chunk);
45 }
46
47 return parser.finish();
48 } catch (e) {
49 throw adjustPosition(e, parser);
50 }
51 }
52
53 throw new TypeError(
54 'Invalid chunk emitter: Expected an Iterable, AsyncIterable, generator, ' +
55 'async generator, or a function returning an Iterable or AsyncIterable'
56 );
57}
58class ChunkParser {
59 constructor() {
60 this.value = undefined;
61 this.valueStack = null;
62
63 this.stack = new Array(100);
64 this.lastFlushDepth = 0;
65 this.flushDepth = 0;
66 this.stateString = false;
67 this.stateStringEscape = false;
68 this.pendingByteSeq = null;
69 this.pendingChunk = null;
70 this.chunkOffset = 0;
71 this.jsonParseOffset = 0;
72 }
73
74 parseAndAppend(fragment, wrap) {
75 // Append new entries or elements
76 if (this.stack[this.lastFlushDepth - 1] === STACK_OBJECT) {
77 if (wrap) {
78 this.jsonParseOffset--;
79 fragment = '{' + fragment + '}';
80 }
81
82 Object.assign(this.valueStack.value, JSON.parse(fragment));
83 } else {
84 if (wrap) {
85 this.jsonParseOffset--;
86 fragment = '[' + fragment + ']';
87 }
88
89 append(this.valueStack.value, JSON.parse(fragment));
90 }
91 }
92
93 prepareAddition(fragment) {
94 const { value } = this.valueStack;
95 const expectComma = Array.isArray(value)
96 ? value.length !== 0
97 : Object.keys(value).length !== 0;
98
99 if (expectComma) {
100 // Skip a comma at the beginning of fragment, otherwise it would
101 // fail to parse
102 if (fragment[0] === ',') {
103 this.jsonParseOffset++;
104 return fragment.slice(1);
105 }
106
107 // When value (an object or array) is not empty and a fragment
108 // doesn't start with a comma, a single valid fragment starting
109 // is a closing bracket. If it's not, a prefix is adding to fail
110 // parsing. Otherwise, the sequence of chunks can be successfully
111 // parsed, although it should not, e.g. ["[{}", "{}]"]
112 if (fragment[0] !== '}' && fragment[0] !== ']') {
113 this.jsonParseOffset -= 3;
114 return '[[]' + fragment;
115 }
116 }
117
118 return fragment;
119 }
120
121 flush(chunk, start, end) {
122 let fragment = chunk.slice(start, end);
123
124 // Save position correction an error in JSON.parse() if any
125 this.jsonParseOffset = this.chunkOffset + start;
126
127 // Prepend pending chunk if any
128 if (this.pendingChunk !== null) {
129 fragment = this.pendingChunk + fragment;
130 this.jsonParseOffset -= this.pendingChunk.length;
131 this.pendingChunk = null;
132 }
133
134 if (this.flushDepth === this.lastFlushDepth) {
135 // Depth didn't changed, so it's a root value or entry/element set
136 if (this.flushDepth > 0) {
137 this.parseAndAppend(this.prepareAddition(fragment), true);
138 } else {
139 // That's an entire value on a top level
140 this.value = JSON.parse(fragment);
141 this.valueStack = {
142 value: this.value,
143 prev: null
144 };
145 }
146 } else if (this.flushDepth > this.lastFlushDepth) {
147 // Add missed closing brackets/parentheses
148 for (let i = this.flushDepth - 1; i >= this.lastFlushDepth; i--) {
149 fragment += this.stack[i] === STACK_OBJECT ? '}' : ']';
150 }
151
152 if (this.lastFlushDepth === 0) {
153 // That's a root value
154 this.value = JSON.parse(fragment);
155 this.valueStack = {
156 value: this.value,
157 prev: null
158 };
159 } else {
160 this.parseAndAppend(this.prepareAddition(fragment), true);
161 }
162
163 // Move down to the depths to the last object/array, which is current now
164 for (let i = this.lastFlushDepth || 1; i < this.flushDepth; i++) {
165 let value = this.valueStack.value;
166
167 if (this.stack[i - 1] === STACK_OBJECT) {
168 // find last entry
169 let key;
170 // eslint-disable-next-line curly
171 for (key in value);
172 value = value[key];
173 } else {
174 // last element
175 value = value[value.length - 1];
176 }
177
178 this.valueStack = {
179 value,
180 prev: this.valueStack
181 };
182 }
183 } else /* this.flushDepth < this.lastFlushDepth */ {
184 fragment = this.prepareAddition(fragment);
185
186 // Add missed opening brackets/parentheses
187 for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
188 this.jsonParseOffset--;
189 fragment = (this.stack[i] === STACK_OBJECT ? '{' : '[') + fragment;
190 }
191
192 this.parseAndAppend(fragment, false);
193
194 for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
195 this.valueStack = this.valueStack.prev;
196 }
197 }
198
199 this.lastFlushDepth = this.flushDepth;
200 }
201
202 push(chunk) {
203 if (typeof chunk !== 'string') {
204 // Suppose chunk is Buffer or Uint8Array
205
206 // Prepend uncompleted byte sequence if any
207 if (this.pendingByteSeq !== null) {
208 const origRawChunk = chunk;
209 chunk = new Uint8Array(this.pendingByteSeq.length + origRawChunk.length);
210 chunk.set(this.pendingByteSeq);
211 chunk.set(origRawChunk, this.pendingByteSeq.length);
212 this.pendingByteSeq = null;
213 }
214
215 // In case Buffer/Uint8Array, an input is encoded in UTF8
216 // Seek for parts of uncompleted UTF8 symbol on the ending
217 // This makes sense only if we expect more chunks and last char is not multi-bytes
218 if (chunk[chunk.length - 1] > 127) {
219 for (let seqLength = 0; seqLength < chunk.length; seqLength++) {
220 const byte = chunk[chunk.length - 1 - seqLength];
221
222 // 10xxxxxx - 2nd, 3rd or 4th byte
223 // 110xxxxx – first byte of 2-byte sequence
224 // 1110xxxx - first byte of 3-byte sequence
225 // 11110xxx - first byte of 4-byte sequence
226 if (byte >> 6 === 3) {
227 seqLength++;
228
229 // If the sequence is really incomplete, then preserve it
230 // for the future chunk and cut off it from the current chunk
231 if ((seqLength !== 4 && byte >> 3 === 0b11110) ||
232 (seqLength !== 3 && byte >> 4 === 0b1110) ||
233 (seqLength !== 2 && byte >> 5 === 0b110)) {
234 this.pendingByteSeq = chunk.slice(chunk.length - seqLength);
235 chunk = chunk.slice(0, -seqLength);
236 }
237
238 break;
239 }
240 }
241 }
242
243 // Convert chunk to a string, since single decode per chunk
244 // is much effective than decode multiple small substrings
245 chunk = decoder.decode(chunk);
246 }
247
248 const chunkLength = chunk.length;
249 let lastFlushPoint = 0;
250 let flushPoint = 0;
251
252 // Main scan loop
253 scan: for (let i = 0; i < chunkLength; i++) {
254 if (this.stateString) {
255 for (; i < chunkLength; i++) {
256 if (this.stateStringEscape) {
257 this.stateStringEscape = false;
258 } else {
259 switch (chunk.charCodeAt(i)) {
260 case 0x22: /* " */
261 this.stateString = false;
262 continue scan;
263
264 case 0x5C: /* \ */
265 this.stateStringEscape = true;
266 }
267 }
268 }
269
270 break;
271 }
272
273 switch (chunk.charCodeAt(i)) {
274 case 0x22: /* " */
275 this.stateString = true;
276 this.stateStringEscape = false;
277 break;
278
279 case 0x2C: /* , */
280 flushPoint = i;
281 break;
282
283 case 0x7B: /* { */
284 // Open an object
285 flushPoint = i + 1;
286 this.stack[this.flushDepth++] = STACK_OBJECT;
287 break;
288
289 case 0x5B: /* [ */
290 // Open an array
291 flushPoint = i + 1;
292 this.stack[this.flushDepth++] = STACK_ARRAY;
293 break;
294
295 case 0x5D: /* ] */
296 case 0x7D: /* } */
297 // Close an object or array
298 flushPoint = i + 1;
299 this.flushDepth--;
300
301 if (this.flushDepth < this.lastFlushDepth) {
302 this.flush(chunk, lastFlushPoint, flushPoint);
303 lastFlushPoint = flushPoint;
304 }
305
306 break;
307
308 case 0x09: /* \t */
309 case 0x0A: /* \n */
310 case 0x0D: /* \r */
311 case 0x20: /* space */
312 // Move points forward when they points on current position and it's a whitespace
313 if (lastFlushPoint === i) {
314 lastFlushPoint++;
315 }
316
317 if (flushPoint === i) {
318 flushPoint++;
319 }
320
321 break;
322 }
323 }
324
325 if (flushPoint > lastFlushPoint) {
326 this.flush(chunk, lastFlushPoint, flushPoint);
327 }
328
329 // Produce pendingChunk if something left
330 if (flushPoint < chunkLength) {
331 if (this.pendingChunk !== null) {
332 // When there is already a pending chunk then no flush happened,
333 // appending entire chunk to pending one
334 this.pendingChunk += chunk;
335 } else {
336 // Create a pending chunk, it will start with non-whitespace since
337 // flushPoint was moved forward away from whitespaces on scan
338 this.pendingChunk = chunk.slice(flushPoint, chunkLength);
339 }
340 }
341
342 this.chunkOffset += chunkLength;
343 }
344
345 finish() {
346 if (this.pendingChunk !== null) {
347 this.flush('', 0, 0);
348 this.pendingChunk = null;
349 }
350
351 return this.value;
352 }
353}
354
355exports.parseChunked = parseChunked;
Note: See TracBrowser for help on using the repository browser.