Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
}

Expand Down
38 changes: 20 additions & 18 deletions test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand All @@ -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,' });
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down