From fadca8778c1e29a8776bdb6549026e1a8d8334f5 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Wed, 7 Oct 2026 17:21:29 +0300 Subject: [PATCH] fix(parser): parse digit-led atoms that are not sequence sets as ATOM A token made of digits, ":", "," and "*" that is not a valid sequence set, like "10:" or "12:30:00", is a valid atom (RFC 3501 section 9), so it is returned as an ATOM instead of throwing. Commands that take a sequence set validate ATOM values themselves. Fixes #11 Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- lib/parser.js | 10 +++++----- test/parser.js | 38 ++++++++++++++++++++------------------ 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ece9b78..751ef1d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Where If section or partial values are not specified in the command, the values are also missing from the ATOM element -**NB!** Sequence numbers are identified as ATOM values if the value contains only numbers or is a single `*`. +**NB!** Sequence numbers are identified as ATOM values if the value contains only numbers or is a single `*`. A value made of digits, `*`, `:` and `,` that is not a valid sequence set (like `10:` or `12:30:00`) is an ATOM too, so commands that take a sequence set must validate ATOM values. **NB!** NIL atoms are always identified as `null` values, even though in some cases it might be an ATOM with value `"NIL"` Syntax errors throw an `Error` with `code` set to `"ParserError"` (or `"MaxNestingReached"` when lists and sections are nested too deeply) and `pos` set to the position of the error in the input. diff --git a/lib/parser.js b/lib/parser.js index 418efbb..5307579 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -329,7 +329,7 @@ TokenParser.prototype.processString = function () { /** * Reads an atom or a sequence set starting at i. Both are made of the same chars, so the type is * decided once the whole token is known: a token of only digits, ":", "," and "*" is a sequence - * set (a lone number or "*" stays an ATOM), anything else is an atom. + * set (a lone number or "*" stays an ATOM) when it is a valid one, anything else is an atom. */ TokenParser.prototype.readAtom = function (start) { const str = this.str; @@ -377,10 +377,10 @@ TokenParser.prototype.readAtom = function (start) { const value = str.slice(start, i); let type = 'ATOM'; - if (seqOnly && !digitsOnly && value !== '*') { - if (!isSequenceSet(value)) { - throw parserError('Invalid sequence set', this.pos + start); - } + // A token like "10:" or "12:30:00" is not a sequence set but still a valid atom (":" and "," + // are ATOM-CHARs, RFC 3501 9), so it stays an ATOM and the commands that take a sequence set + // refuse it + if (seqOnly && !digitsOnly && value !== '*' && isSequenceSet(value)) { type = 'SEQUENCE'; } diff --git a/test/parser.js b/test/parser.js index 5d3605b..2940466 100644 --- a/test/parser.js +++ b/test/parser.js @@ -426,17 +426,14 @@ describe('ATTRIBUTE', () => { } ]); - assert.throws(() => { - parser('TAG1 CMD *:4,5:'); - }); + // not a sequence set, but a valid atom + assert.deepEqual(parser('TAG1 CMD *:4,5:').attributes[0], { type: 'ATOM', value: '*:4,5:' }); - assert.throws(() => { - parser('TAG1 CMD *:4,5: TEST'); - }); + // not a sequence set, but a valid atom + assert.deepEqual(parser('TAG1 CMD *:4,5: TEST').attributes[0], { type: 'ATOM', value: '*:4,5:' }); - assert.throws(() => { - parser('TAG1 CMD *4,5 TEST'); - }); + // not a sequence set, but a valid atom + assert.deepEqual(parser('TAG1 CMD *4,5 TEST').attributes[0], { type: 'ATOM', value: '*4,5' }); // RFC 3501 9: seq-number = nz-number / "*", so "*" may appear anywhere in a set assert.deepEqual(parser('TAG1 CMD *,5 TEST').attributes, [ @@ -461,9 +458,7 @@ describe('ATTRIBUTE', () => { } ]); - assert.throws(() => { - parser('TAG1 CMD 5, TEST'); - }); + assert.deepEqual(parser('TAG1 CMD 5, TEST').attributes[0], { type: 'ATOM', value: '5,' }); }); }); @@ -518,12 +513,19 @@ describe('Sequence sets', () => { ]); }); - it('rejects malformed sequence sets', () => { - assert.throws(() => parser('A1 FETCH 5: FLAGS'), { code: 'ParserError' }); - assert.throws(() => parser('A1 FETCH 5,')); - assert.throws(() => parser('A1 FETCH 1:2:3 FLAGS')); - assert.throws(() => parser('A1 FETCH *4 FLAGS')); - assert.throws(() => parser('A1 SEARCH (1:5,)')); + it('parses malformed sequence sets as atoms', () => { + // RFC 3501 9: ":" and "," are ATOM-CHARs, the commands that take a sequence set check it + assert.deepEqual(parser('A1 FETCH 5: FLAGS').attributes[0], { type: 'ATOM', value: '5:' }); + assert.deepEqual(parser('A1 FETCH 5,').attributes[0], { type: 'ATOM', value: '5,' }); + assert.deepEqual(parser('A1 FETCH 1:2:3 FLAGS').attributes[0], { type: 'ATOM', value: '1:2:3' }); + assert.deepEqual(parser('A1 FETCH *4 FLAGS').attributes[0], { type: 'ATOM', value: '*4' }); + assert.deepEqual(parser('A1 SEARCH (1:5,)').attributes[0], [{ type: 'ATOM', value: '1:5,' }]); + }); + + it('parses digit-led atoms that are not sequence sets', () => { + assert.deepEqual(parser('A CREATE 10:').attributes, [{ type: 'ATOM', value: '10:' }]); + assert.deepEqual(parser('A CREATE 12:30:00').attributes, [{ type: 'ATOM', value: '12:30:00' }]); + assert.deepEqual(parser('A STORE 1 +FLAGS (10:)').attributes[2], [{ type: 'ATOM', value: '10:' }]); }); it('parses a large sequence set in linear time', () => {