imapflow 2.2.5 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/commands/append.js +10 -3
  3. package/dist/cjs/commands/authenticate.js +27 -18
  4. package/dist/cjs/commands/close.d.ts +12 -1
  5. package/dist/cjs/commands/close.js +4 -2
  6. package/dist/cjs/commands/copyuid-parser.js +16 -2
  7. package/dist/cjs/commands/delete.js +2 -1
  8. package/dist/cjs/commands/esearch-parser.js +8 -2
  9. package/dist/cjs/commands/fetch.js +35 -9
  10. package/dist/cjs/commands/id.js +8 -1
  11. package/dist/cjs/commands/idle.js +15 -5
  12. package/dist/cjs/commands/list.js +10 -1
  13. package/dist/cjs/commands/login.js +5 -1
  14. package/dist/cjs/commands/logout.js +7 -0
  15. package/dist/cjs/commands/namespace.js +7 -3
  16. package/dist/cjs/commands/quota.js +3 -1
  17. package/dist/cjs/commands/rename.js +2 -1
  18. package/dist/cjs/commands/select.js +5 -0
  19. package/dist/cjs/commands/status.js +6 -1
  20. package/dist/cjs/commands/store.d.ts +1 -1
  21. package/dist/cjs/commands/store.js +1 -2
  22. package/dist/cjs/download.js +82 -91
  23. package/dist/cjs/handler/imap-compiler.js +45 -29
  24. package/dist/cjs/handler/limits.d.ts +11 -0
  25. package/dist/cjs/handler/limits.js +16 -1
  26. package/dist/cjs/handler/parser-instance.d.ts +10 -0
  27. package/dist/cjs/handler/parser-instance.js +25 -10
  28. package/dist/cjs/handler/token-parser.js +162 -78
  29. package/dist/cjs/imap-flow.js +235 -89
  30. package/dist/cjs/package-info.d.ts +1 -1
  31. package/dist/cjs/package-info.js +1 -1
  32. package/dist/cjs/proxy-connection.js +7 -7
  33. package/dist/cjs/search-compiler.js +37 -16
  34. package/dist/cjs/special-use.js +10 -5
  35. package/dist/cjs/tools.d.ts +10 -1
  36. package/dist/cjs/tools.js +30 -3
  37. package/dist/cjs/types.d.ts +19 -4
  38. package/dist/esm/commands/append.js +11 -4
  39. package/dist/esm/commands/authenticate.js +28 -19
  40. package/dist/esm/commands/close.d.ts +12 -1
  41. package/dist/esm/commands/close.js +5 -3
  42. package/dist/esm/commands/copyuid-parser.js +16 -2
  43. package/dist/esm/commands/delete.js +2 -1
  44. package/dist/esm/commands/esearch-parser.js +8 -2
  45. package/dist/esm/commands/fetch.js +35 -9
  46. package/dist/esm/commands/id.js +9 -2
  47. package/dist/esm/commands/idle.js +16 -6
  48. package/dist/esm/commands/list.js +10 -1
  49. package/dist/esm/commands/login.js +6 -2
  50. package/dist/esm/commands/logout.js +7 -0
  51. package/dist/esm/commands/namespace.js +7 -3
  52. package/dist/esm/commands/quota.js +3 -1
  53. package/dist/esm/commands/rename.js +2 -1
  54. package/dist/esm/commands/select.js +5 -0
  55. package/dist/esm/commands/status.js +7 -2
  56. package/dist/esm/commands/store.d.ts +1 -1
  57. package/dist/esm/commands/store.js +1 -2
  58. package/dist/esm/download.js +82 -91
  59. package/dist/esm/handler/imap-compiler.js +45 -29
  60. package/dist/esm/handler/limits.d.ts +11 -0
  61. package/dist/esm/handler/limits.js +14 -0
  62. package/dist/esm/handler/parser-instance.d.ts +10 -0
  63. package/dist/esm/handler/parser-instance.js +25 -10
  64. package/dist/esm/handler/token-parser.js +163 -79
  65. package/dist/esm/imap-flow.js +235 -89
  66. package/dist/esm/package-info.d.ts +1 -1
  67. package/dist/esm/package-info.js +1 -1
  68. package/dist/esm/proxy-connection.js +7 -7
  69. package/dist/esm/search-compiler.js +37 -16
  70. package/dist/esm/special-use.js +10 -5
  71. package/dist/esm/tools.d.ts +10 -1
  72. package/dist/esm/tools.js +29 -3
  73. package/dist/esm/types.d.ts +19 -4
  74. package/package.json +3 -2
@@ -13,7 +13,6 @@ const STATE_NORMAL = 0x003;
13
13
  const STATE_PARTIAL = 0x004;
14
14
  const STATE_SEQUENCE = 0x005;
15
15
  const STATE_STRING = 0x006;
16
- const RE_DIGITS = /^\d+$/;
17
16
  const RE_SINGLE_DIGIT = /^\d$/;
18
17
  // Prevents stack overflow from maliciously crafted deeply-nested IMAP input (e.g., (((((...))))))
19
18
  const MAX_NODE_DEPTH = 25;
@@ -74,7 +73,7 @@ class TokenParser {
74
73
  if (!node.isClosed) {
75
74
  let error = new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`);
76
75
  error.code = 'ParserError9';
77
- error.parserContext = { input: this.str, pos: this.pos + this.str.length - 1 };
76
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + this.str.length - 1 };
78
77
  throw error;
79
78
  }
80
79
  let type = (node.type || '').toString().toUpperCase();
@@ -148,7 +147,8 @@ class TokenParser {
148
147
  if (node.depth > MAX_NODE_DEPTH) {
149
148
  let error = new Error('Too much nesting in IMAP string');
150
149
  error.code = 'MAX_IMAP_NESTING_REACHED';
151
- error._imapStr = this.str;
150
+ // A bounded prefix only, the error travels whole into logs and rejections
151
+ error._imapStr = this.str.slice(0, limits_js_1.ERROR_CONTEXT_LENGTH);
152
152
  throw error;
153
153
  }
154
154
  if (typeof startPos === 'number') {
@@ -175,6 +175,63 @@ class TokenParser {
175
175
  i++;
176
176
  }
177
177
  };
178
+ // Whether chr is the closing delimiter of node: ")" ends a LIST, "]" ends a SECTION
179
+ const closesNode = (c, node) => !!node && ((c === ')' && node.type === 'LIST') || (c === ']' && node.type === 'SECTION'));
180
+ // Facts about the ATOM, SEQUENCE or PARTIAL token being read, kept up to date as its
181
+ // characters are appended. Reading them back from the value instead (value.at(-1),
182
+ // value.includes('*'), a regex test) makes V8 flatten the string built with += on every
183
+ // character, which is quadratic in the token length, and the server decides that length:
184
+ // a 600 KB ESEARCH result took seconds of event loop time
185
+ let tokenLength = 0;
186
+ let tokenLast = '';
187
+ let tokenPrev = '';
188
+ let tokenDigitsOnly = true;
189
+ let tokenHasStar = false;
190
+ let tokenHasDot = false;
191
+ const appendToToken = (c) => {
192
+ this.currentNode.value += c;
193
+ tokenLength++;
194
+ tokenPrev = tokenLast;
195
+ tokenLast = c;
196
+ tokenDigitsOnly = tokenDigitsOnly && RE_SINGLE_DIGIT.test(c);
197
+ tokenHasStar = tokenHasStar || c === '*';
198
+ tokenHasDot = tokenHasDot || c === '.';
199
+ };
200
+ // A digit-led token is only a guess at a sequence set. ":" and "," are ATOM-CHARs, so a
201
+ // server sends a mailbox name, keyword or label such as "2024:Q1" or "1,a" unquoted, and
202
+ // rejecting it would drop the whole response line: the mailbox missing from LIST, the
203
+ // message missing from FETCH. When the sequence grammar breaks, such a token is read on as
204
+ // an atom, and the atom rules decide whether the current character is acceptable. A token
205
+ // holding "*" can not be an atom ("*" is not an ATOM-CHAR), so it keeps the sequence errors
206
+ const continueAsAtom = () => {
207
+ this.currentNode.type = 'ATOM';
208
+ this.state = STATE_ATOM;
209
+ // read the current character again, in STATE_ATOM
210
+ i--;
211
+ };
212
+ // Starts the value of the current node as a new token, with its first character if any
213
+ const startToken = (first) => {
214
+ this.currentNode.value = '';
215
+ tokenLength = 0;
216
+ tokenLast = tokenPrev = '';
217
+ tokenDigitsOnly = true;
218
+ tokenHasStar = tokenHasDot = false;
219
+ if (first) {
220
+ appendToToken(first);
221
+ }
222
+ };
223
+ // ImapStream supplies one buffer per literal marker it framed, {0} included. A marker
224
+ // without a buffer means the line and its literals do not match up, so fail with a parser
225
+ // error instead of reading a missing buffer or shifting every later literal by one
226
+ const takeLiteral = (literals) => {
227
+ if (!literals.length) {
228
+ let error = new Error(`Literal without data at position ${this.pos + i} [E35]`);
229
+ error.code = 'ParserError35';
230
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
231
+ throw error;
232
+ }
233
+ return literals.shift();
234
+ };
178
235
  // Any ATOM supported char starts a new Atom sequence, otherwise throw an error
179
236
  // Allow \ as the first char for atom to support system flags
180
237
  // Allow % to support LIST '' %
@@ -185,12 +242,12 @@ class TokenParser {
185
242
  if (!imap_formal_syntax_js_1.default['ATOM-CHAR']().includes(chr) && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
186
243
  let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
187
244
  error.code = 'ParserError13';
188
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
245
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
189
246
  throw error;
190
247
  }
191
248
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
192
249
  this.currentNode.type = 'ATOM';
193
- this.currentNode.value = chr;
250
+ startToken(chr);
194
251
  this.state = STATE_ATOM;
195
252
  };
196
253
  for (i = 0, len = this.str.length; i < len; i++) {
@@ -216,7 +273,7 @@ class TokenParser {
216
273
  if (this.currentNode.type !== 'LIST') {
217
274
  let error = new Error(`Unexpected list terminator ) at position ${this.pos + i} [E10]`);
218
275
  error.code = 'ParserError10';
219
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
276
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
220
277
  throw error;
221
278
  }
222
279
  this.currentNode.isClosed = true;
@@ -229,7 +286,7 @@ class TokenParser {
229
286
  if (this.currentNode.type !== 'SECTION') {
230
287
  let error = new Error(`Unexpected section terminator ] at position ${this.pos + i} [E11]`);
231
288
  error.code = 'ParserError11';
232
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
289
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
233
290
  throw error;
234
291
  }
235
292
  this.currentNode.isClosed = true;
@@ -245,12 +302,13 @@ class TokenParser {
245
302
  if (this.str.charAt(i - 1) !== ']') {
246
303
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
247
304
  this.currentNode.type = 'ATOM';
248
- this.currentNode.value = chr;
305
+ startToken(chr);
249
306
  this.state = STATE_ATOM;
250
307
  }
251
308
  else {
252
309
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
253
310
  this.currentNode.type = 'PARTIAL';
311
+ startToken();
254
312
  this.state = STATE_PARTIAL;
255
313
  this.currentNode.isClosed = false;
256
314
  }
@@ -260,17 +318,19 @@ class TokenParser {
260
318
  case '~': {
261
319
  let nextChr = this.str.charAt(i + 1);
262
320
  if (nextChr !== '{') {
263
- if (imap_formal_syntax_js_1.default['ATOM-CHAR']().includes(nextChr)) {
264
- // '~' not followed by '{' but followed by an ATOM char: treat as ATOM
265
- this.currentNode = this.createNode(this.currentNode, this.pos + i);
266
- this.currentNode.type = 'ATOM';
267
- this.currentNode.value = chr;
268
- this.state = STATE_ATOM;
321
+ // '~' is an ATOM-CHAR itself, so when it does not start a literal8
322
+ // it starts an atom, including the one-character atom "~" that a
323
+ // space, the end of the line or a closing delimiter ends
324
+ if (imap_formal_syntax_js_1.default['ATOM-CHAR']().includes(nextChr) ||
325
+ nextChr === ' ' ||
326
+ nextChr === '' ||
327
+ closesNode(nextChr, this.currentNode)) {
328
+ startAtom();
269
329
  break;
270
330
  }
271
331
  let error = new Error(`Unexpected literal8 marker at position ${this.pos + i} [E12]`);
272
332
  error.code = 'ParserError12';
273
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
333
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
274
334
  throw error;
275
335
  }
276
336
  // Mark the next literal as literal8 type; consumed when '{' is encountered
@@ -291,7 +351,7 @@ class TokenParser {
291
351
  case '*':
292
352
  this.currentNode = this.createNode(this.currentNode, this.pos + i);
293
353
  this.currentNode.type = 'SEQUENCE';
294
- this.currentNode.value = chr;
354
+ startToken(chr);
295
355
  this.currentNode.isClosed = false;
296
356
  this.state = STATE_SEQUENCE;
297
357
  break;
@@ -372,8 +432,7 @@ class TokenParser {
372
432
  break;
373
433
  }
374
434
  // ')' or ']' terminates the atom AND closes the enclosing LIST or SECTION
375
- if (this.currentNode.parentNode &&
376
- ((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))) {
435
+ if (closesNode(chr, this.currentNode.parentNode)) {
377
436
  this.currentNode.endPos = this.pos + i - 1;
378
437
  this.currentNode = this.currentNode.parentNode;
379
438
  this.currentNode.isClosed = true;
@@ -409,14 +468,16 @@ class TokenParser {
409
468
  }
410
469
  // If the atom so far is all digits and we see ',' or ':', it is actually
411
470
  // a sequence set (e.g., "1:5" or "1,3,5"), so reclassify and switch state
412
- if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
471
+ if ((chr === ',' || chr === ':') && tokenLength && tokenDigitsOnly) {
413
472
  this.currentNode.type = 'SEQUENCE';
414
473
  this.currentNode.isClosed = true;
415
474
  this.state = STATE_SEQUENCE;
416
475
  }
417
476
  // [ starts a section group for this element
418
477
  // Allowed only for selected elements, otherwise falls through to regular ATOM processing
419
- if (chr === '[' && ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].includes(this.currentNode.value.toUpperCase())) {
478
+ if (chr === '[' &&
479
+ tokenLength <= 11 &&
480
+ ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].includes(this.currentNode.value.toUpperCase())) {
420
481
  this.currentNode.endPos = this.pos + i;
421
482
  this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
422
483
  this.currentNode.type = 'SECTION';
@@ -429,19 +490,19 @@ class TokenParser {
429
490
  chr.charCodeAt(0) < 0x80 && // allow 8bit (presumably unicode) bytes
430
491
  chr !== ']' &&
431
492
  !(chr === '*' && this.currentNode.value === '\\') &&
432
- (!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))) {
493
+ (!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command.toUpperCase()))) {
433
494
  let error = new Error(`Unexpected char at position ${this.pos + i} [E16: ${JSON.stringify(chr)}]`);
434
495
  error.code = 'ParserError16';
435
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
496
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
436
497
  throw error;
437
498
  }
438
499
  else if (this.currentNode.value === '\\*') {
439
500
  let error = new Error(`Unexpected char at position ${this.pos + i} [E17: ${JSON.stringify(chr)}]`);
440
501
  error.code = 'ParserError17';
441
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
502
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
442
503
  throw error;
443
504
  }
444
- this.currentNode.value += chr;
505
+ appendToToken(chr);
445
506
  break;
446
507
  case STATE_STRING:
447
508
  // DQUOTE ends the string sequence
@@ -459,7 +520,7 @@ class TokenParser {
459
520
  if (i >= len) {
460
521
  let error = new Error(`Unexpected end of input at position ${this.pos + i} [E18]`);
461
522
  error.code = 'ParserError18';
462
- error.parserContext = { input: this.str, pos: this.pos + i };
523
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i };
463
524
  throw error;
464
525
  }
465
526
  chr = this.str.charAt(i);
@@ -468,10 +529,10 @@ class TokenParser {
468
529
  break;
469
530
  case STATE_PARTIAL:
470
531
  if (chr === '>') {
471
- if (this.currentNode.value.at(-1) === '.') {
532
+ if (tokenLast === '.') {
472
533
  let error = new Error(`Unexpected end of partial at position ${this.pos + i} [E19]`);
473
534
  error.code = 'ParserError19';
474
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
535
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
475
536
  throw error;
476
537
  }
477
538
  this.currentNode.endPos = this.pos + i;
@@ -481,25 +542,25 @@ class TokenParser {
481
542
  checkSP();
482
543
  break;
483
544
  }
484
- if (chr === '.' && (this.currentNode.value === '' || this.currentNode.value.includes('.'))) {
545
+ if (chr === '.' && (!tokenLength || tokenHasDot)) {
485
546
  let error = new Error(`Unexpected partial separator . at position ${this.pos + i} [E20]`);
486
547
  error.code = 'ParserError20';
487
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
548
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
488
549
  throw error;
489
550
  }
490
551
  if (!imap_formal_syntax_js_1.default.DIGIT().includes(chr) && chr !== '.') {
491
552
  let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
492
553
  error.code = 'ParserError21';
493
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
554
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
494
555
  throw error;
495
556
  }
496
- if ((this.currentNode.value === '0' || this.currentNode.value.endsWith('.0')) && chr !== '.') {
557
+ if (tokenLast === '0' && (tokenLength === 1 || tokenPrev === '.') && chr !== '.') {
497
558
  let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
498
559
  error.code = 'ParserError22';
499
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
560
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
500
561
  throw error;
501
562
  }
502
- this.currentNode.value += chr;
563
+ appendToToken(chr);
503
564
  break;
504
565
  case STATE_LITERAL:
505
566
  if (this.currentNode.started) {
@@ -531,7 +592,7 @@ class TokenParser {
531
592
  if (!('literalLength' in this.currentNode)) {
532
593
  let error = new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E23]`);
533
594
  error.code = 'ParserError23';
534
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
595
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
535
596
  throw error;
536
597
  }
537
598
  if (this.str.charAt(i + 1) === '\n') {
@@ -543,19 +604,17 @@ class TokenParser {
543
604
  else {
544
605
  let error = new Error(`Unexpected char at position ${this.pos + i} [E24: ${JSON.stringify(chr)}]`);
545
606
  error.code = 'ParserError24';
546
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
607
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
547
608
  throw error;
548
609
  }
549
610
  this.currentNode.literalLength = Number(this.currentNode.literalLength);
550
611
  if (!this.currentNode.literalLength) {
551
612
  // special case where literal content length is 0
552
613
  // close the node right away, do not wait for additional input
553
- if (this.options.literals && this.options.literals.length) {
554
- // ImapStream queues a Buffer for every literal marker it
555
- // extracts, including {0} - consume the queue entry so
556
- // subsequent literals in the same response stay aligned
557
- // with their markers instead of shifting by one
558
- this.currentNode.value = this.options.literals.shift();
614
+ if (this.options.literals) {
615
+ // consume the queue entry of the {0} marker too, so later
616
+ // literals in the same response stay aligned with their markers
617
+ this.currentNode.value = takeLiteral(this.options.literals);
559
618
  }
560
619
  this.currentNode.endPos = this.pos + i;
561
620
  this.currentNode.isClosed = true;
@@ -565,7 +624,7 @@ class TokenParser {
565
624
  }
566
625
  else if (this.options.literals) {
567
626
  // use the next precached literal values
568
- this.currentNode.value = this.options.literals.shift();
627
+ this.currentNode.value = takeLiteral(this.options.literals);
569
628
  // only APPEND arguments are kept as Buffers
570
629
  /*
571
630
  if ((this.parent.command || '').toString().toUpperCase() !== 'APPEND') {
@@ -595,7 +654,7 @@ class TokenParser {
595
654
  if (literalLength > this.maxLiteralSize || literalLength > available) {
596
655
  let overMax = literalLength > this.maxLiteralSize;
597
656
  let error = (0, limits_js_1.createLiteralTooLargeError)(literalLength, overMax ? this.maxLiteralSize : available, overMax ? null : `the ${available} bytes available in the input`);
598
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
657
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
599
658
  throw error;
600
659
  }
601
660
  this.currentNode.started = true;
@@ -609,96 +668,121 @@ class TokenParser {
609
668
  if (!imap_formal_syntax_js_1.default.DIGIT().includes(chr)) {
610
669
  let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
611
670
  error.code = 'ParserError25';
612
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
671
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
613
672
  throw error;
614
673
  }
615
674
  if (this.currentNode.literalLength === '0') {
616
675
  let error = new Error(`Invalid literal at position ${this.pos + i} [E26]`);
617
676
  error.code = 'ParserError26';
618
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
677
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
619
678
  throw error;
620
679
  }
621
680
  this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
622
681
  break;
623
- case STATE_SEQUENCE:
624
- // space finishes the sequence set
625
- if (chr === ' ') {
626
- if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
627
- let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E27]`);
682
+ case STATE_SEQUENCE: {
683
+ // A space ends the sequence set, and so does the closing delimiter of the
684
+ // enclosing list or section ("PARTIAL (1:100 5,7,9)", "[COPYUID 1 1:3 4,5]")
685
+ let closesParent = closesNode(chr, this.currentNode.parentNode);
686
+ if (chr === ' ' || closesParent) {
687
+ if (!RE_SINGLE_DIGIT.test(tokenLast) && tokenLast !== '*') {
688
+ if (!tokenHasStar) {
689
+ // a dangling separator, "10:" or "1,"
690
+ continueAsAtom();
691
+ break;
692
+ }
693
+ let error = new Error(`Unexpected end of sequence at position ${this.pos + i} [E27: ${JSON.stringify(chr)}]`);
628
694
  error.code = 'ParserError27';
629
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
695
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
630
696
  throw error;
631
697
  }
632
- if (this.currentNode.value !== '*' &&
633
- this.currentNode.value.at(-1) === '*' &&
634
- this.currentNode.value.at(-2) !== ':') {
635
- let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E28]`);
698
+ if (this.currentNode.value !== '*' && tokenLast === '*' && tokenPrev !== ':') {
699
+ let error = new Error(`Unexpected end of sequence at position ${this.pos + i} [E28: ${JSON.stringify(chr)}]`);
636
700
  error.code = 'ParserError28';
637
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
701
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
638
702
  throw error;
639
703
  }
640
704
  this.currentNode.isClosed = true;
641
705
  this.currentNode.endPos = this.pos + i - 1;
642
706
  this.currentNode = this.currentNode.parentNode;
643
707
  this.state = STATE_NORMAL;
644
- break;
645
- }
646
- else if (this.currentNode.parentNode && chr === ']' && this.currentNode.parentNode.type === 'SECTION') {
647
- this.currentNode.endPos = this.pos + i - 1;
648
- this.currentNode = this.currentNode.parentNode;
649
- this.currentNode.isClosed = true;
650
- this.currentNode.endPos = this.pos + i;
651
- this.currentNode = this.currentNode.parentNode;
652
- this.state = STATE_NORMAL;
653
- checkSP();
708
+ if (closesParent) {
709
+ this.currentNode.isClosed = true;
710
+ this.currentNode.endPos = this.pos + i;
711
+ this.currentNode = this.currentNode.parentNode;
712
+ checkSP();
713
+ }
654
714
  break;
655
715
  }
656
716
  if (chr === ':') {
657
- if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
717
+ if (!RE_SINGLE_DIGIT.test(tokenLast) && tokenLast !== '*') {
718
+ if (!tokenHasStar) {
719
+ continueAsAtom();
720
+ break;
721
+ }
658
722
  let error = new Error(`Unexpected range separator : at position ${this.pos + i} [E29]`);
659
723
  error.code = 'ParserError29';
660
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
724
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
661
725
  throw error;
662
726
  }
663
727
  }
664
728
  else if (chr === '*') {
665
- if (![',', ':'].includes(this.currentNode.value.at(-1))) {
729
+ if (![',', ':'].includes(tokenLast)) {
666
730
  let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
667
731
  error.code = 'ParserError30';
668
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
732
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
669
733
  throw error;
670
734
  }
671
735
  }
672
736
  else if (chr === ',') {
673
- if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
737
+ if (!RE_SINGLE_DIGIT.test(tokenLast) && tokenLast !== '*') {
738
+ if (!tokenHasStar) {
739
+ continueAsAtom();
740
+ break;
741
+ }
674
742
  let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E31]`);
675
743
  error.code = 'ParserError31';
676
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
744
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
677
745
  throw error;
678
746
  }
679
- if (this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
747
+ if (tokenLast === '*' && tokenPrev !== ':') {
680
748
  let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E32]`);
681
749
  error.code = 'ParserError32';
682
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
750
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
683
751
  throw error;
684
752
  }
685
753
  }
686
754
  else if (!RE_SINGLE_DIGIT.test(chr)) {
755
+ if (!tokenHasStar) {
756
+ continueAsAtom();
757
+ break;
758
+ }
687
759
  let error = new Error(`Unexpected char at position ${this.pos + i} [E33: ${JSON.stringify(chr)}]`);
688
760
  error.code = 'ParserError33';
689
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
761
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
690
762
  throw error;
691
763
  }
692
- if (RE_SINGLE_DIGIT.test(chr) && this.currentNode.value.at(-1) === '*') {
764
+ if (RE_SINGLE_DIGIT.test(chr) && tokenLast === '*') {
693
765
  let error = new Error(`Unexpected number at position ${this.pos + i} [E34: ${JSON.stringify(chr)}]`);
694
766
  error.code = 'ParserError34';
695
- error.parserContext = { input: this.str, pos: this.pos + i, chr };
767
+ error.parserContext = { ...(0, limits_js_1.boundedInput)(this.str), pos: this.pos + i, chr };
696
768
  throw error;
697
769
  }
698
- this.currentNode.value += chr;
770
+ appendToToken(chr);
699
771
  break;
772
+ }
700
773
  }
701
774
  }
775
+ // the same applies to a digit-led token that ends the input on a dangling separator ("10:")
776
+ if (this.state === STATE_SEQUENCE && (tokenLast === ':' || tokenLast === ',') && !tokenHasStar) {
777
+ this.currentNode.type = 'ATOM';
778
+ }
779
+ // A star-led sequence set ("*:4") is only closed by a space or a closing delimiter, so one
780
+ // that ends the input is closed here, under the same rules a space applies to it. A bare "*"
781
+ // is left to getAttributes(), which reads it as an atom
782
+ if (this.state === STATE_SEQUENCE && !this.currentNode.isClosed && (RE_SINGLE_DIGIT.test(tokenLast) || (tokenLast === '*' && tokenPrev === ':'))) {
783
+ this.currentNode.isClosed = true;
784
+ this.currentNode.endPos = this.pos + this.str.length - 1;
785
+ }
702
786
  }
703
787
  }
704
788
  exports.TokenParser = TokenParser;