From 3560971c0fc7ec5fa8a946b07d66f60ace6ddceb Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Wed, 7 Oct 2026 14:25:36 +0300 Subject: [PATCH] fix(parser): accept atom chars in command names RFC 3501 section 9 defines x-command as "X" atom and auth-type as atom, so names like X-FOO.BAR or AUTHENTICATE PLAIN-CLIENTTOKEN are valid. Closes #1 Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- lib/formal.js | 4 +++- test/parser.js | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9f7997c..ece9b78 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ The function returns an object in the following form: Where - **tag** is a string containing the tag -- **command** is the first element after tag +- **command** is the first element after tag. It is an atom (RFC 3501 section 9, `x-command = "X" atom`), so besides letters and digits it can hold chars like `-`, `.`, `/` or `=`. Do not use it to build file paths without checking it first - **attributes** (if present) is an array of next elements If section or partial values are not specified in the command, the values are also missing from the ATOM element diff --git a/lib/formal.js b/lib/formal.js index 6920be5..4ad31fb 100644 --- a/lib/formal.js +++ b/lib/formal.js @@ -88,8 +88,10 @@ module.exports = { return this._excludeChars(this['ASTRING-CHAR'](), '+'); }, + // RFC 3501 9: command names are atoms, e.g. x-command = "X" atom, and the second word of + // UID and AUTHENTICATE is an atom too (auth-type = atom) command: function () { - const value = this.ALPHA() + this.DIGIT(); + const value = this['ATOM-CHAR'](); this.command = function () { return value; }; diff --git a/test/parser.js b/test/parser.js index 83b3f0b..5d3605b 100644 --- a/test/parser.js +++ b/test/parser.js @@ -65,9 +65,18 @@ describe('COMMAND', () => { }); it('Get command fail (invalid char)', () => { - assert.throws(() => { - parser('TAG1 CM=D'); - }); + // RFC 3501 9: atom-specials and 8-bit octets are not ATOM-CHARs + for (const command of ['CM(D', 'CM"D', 'CM%D', 'CM*D', 'CM]D', 'CM{D', 'CM\\D', 'CM\x7fD', 'CM\xe9D']) { + assert.throws(() => parser('TAG1 ' + command), /Unexpected char/, command); + } + }); + + // RFC 3501 9: x-command = "X" atom, auth-type = atom + it('Get command with atom chars', () => { + assert.equal(parser('TAG1 X-FOO.BAR').command, 'X-FOO.BAR'); + assert.equal(parser('TAG1 CM=D').command, 'CM=D'); + assert.equal(parser('TAG1 AUTHENTICATE PLAIN-CLIENTTOKEN').command, 'AUTHENTICATE PLAIN-CLIENTTOKEN'); + assert.equal(parser('TAG1 AUTHENTICATE SCRAM-SHA-256 =').command, 'AUTHENTICATE SCRAM-SHA-256'); }); it('Get multi word command', () => { assert.equal(parser('TAG1 UID FETCH').command, 'UID FETCH');