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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Options
- **multiWords** (Array) commands that are joined with the next word, default value is `["UID", "AUTHENTICATE"]`
- **literalPlus** (Boolean) accept non-synchronizing literals `{n+}` (RFC 7888), and `~{n+}` when `literal8` is set as well (RFC 4466)
- **literal8** (Boolean) accept `~{n}` literals (RFC 3516, RFC 9051), returned as `{type: "LITERAL8", value}` nodes. Unlike a `LITERAL`, the value may contain NUL
- **number64** (Boolean) accept literal sizes and partial ranges (`<offset.length>`) up to `Number.MAX_SAFE_INTEGER` as the RFC 9051 number64 rule allows. Without it both are limited to 32 bits (RFC 3501 number)
- **utf8** (Boolean) accept valid UTF-8 in quoted strings (RFC 9051 and RFC 9755 UTF8=ACCEPT). The value is still returned as a binary string (one char per octet). Invalid UTF-8 (overlong forms, surrogates, truncated sequences, values above U+10FFFF) still throws

The function returns an object in the following form:
Expand Down
14 changes: 11 additions & 3 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const formalSyntax = require('./formal');
// RFC 3501 9: number is an unsigned 32-bit integer
const MAX_NUMBER = 0xffffffff;

// RFC 9051 9: number64 is a 63-bit integer for literal sizes and partial ranges. Larger values than
// Number.MAX_SAFE_INTEGER can not be represented exactly, so with the number64 option that is the limit
const MAX_NUMBER64 = Number.MAX_SAFE_INTEGER;

// Deeper input (lists, sections and the values in them) is refused instead of risking the stack
const MAX_NODE_DEPTH = 25;

Expand Down Expand Up @@ -128,6 +132,9 @@ function TokenParser(startPos, str, options) {
// the longest value that may open a section, longer atoms are not compared at all
this.maxSectionName = Math.max(0, ...this.options.allowSection.map(name => name.length));

// literal sizes and partial ranges are number64 values in IMAP4rev2 (RFC 9051 section 9)
this.maxNumber = this.options.number64 ? MAX_NUMBER64 : MAX_NUMBER;

this.tree = this.currentNode = this.createNode();
this.currentNode.type = 'TREE';

Expand Down Expand Up @@ -471,7 +478,7 @@ TokenParser.prototype.readLiteral = function (start, isLiteral8) {
throw this.unexpected(i);
}

if (size > MAX_NUMBER || i + size > len) {
if (size > this.maxNumber || i + size > len) {
throw parserError('Unexpected end of input', this.pos + len);
}

Expand All @@ -489,7 +496,8 @@ TokenParser.prototype.readLiteral = function (start, isLiteral8) {
return this.afterValue(i + size);
};

// RFC 3501 9: partial = "<" number "." nz-number ">", a single number is also accepted
// RFC 3501 9: partial = "<" number "." nz-number ">", a single number is also accepted. RFC 9051 9 uses
// number64 and nz-number64, allowed with the number64 option
TokenParser.prototype.readPartial = function (start) {
const str = this.str;
const len = str.length;
Expand All @@ -506,7 +514,7 @@ TokenParser.prototype.readPartial = function (start) {
if (nonZero && str.charAt(numStart) === '0') {
throw parserError('Invalid partial', this.pos + numStart);
}
if (Number(str.slice(numStart, i)) > MAX_NUMBER) {
if (Number(str.slice(numStart, i)) > this.maxNumber) {
throw parserError('Invalid partial', this.pos + numStart);
}
};
Expand Down
20 changes: 20 additions & 0 deletions test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,26 @@ describe('Literals', () => {
});
});

describe('number64 option', () => {
// RFC 9051 section 9: partial-range = number64 ["." nz-number64], literal = "{" number64 ["+"] "}"
it('accepts partial ranges above 32 bits', () => {
assert.deepEqual(parser('A1 FETCH 1 BODY[]<4294967296.4294967297>', { number64: true }).attributes[1].partial, [4294967296, 4294967297]);
assert.deepEqual(parser('A1 FETCH 1 BODY[]<9007199254740991>', { number64: true }).attributes[1].partial, [9007199254740991]);
assert.throws(() => parser('A1 FETCH 1 BODY[]<4294967296.1>'));
});

it('refuses values that can not be represented exactly', () => {
assert.throws(() => parser('A1 FETCH 1 BODY[]<9007199254740992>', { number64: true }), /Invalid partial/);
assert.throws(() => parser('A1 FETCH 1 BODY[]<0.0>', { number64: true }), /Invalid partial/);
});

it('checks literal sizes against the 64-bit limit', () => {
// the size is valid, there is just not enough input
assert.throws(() => parser('A1 CMD {4294967296}\r\nabc', { number64: true }), /Unexpected end of input/);
assert.deepEqual(parser('A1 CMD {3}\r\nabc', { number64: true }).attributes, [{ type: 'LITERAL', value: 'abc' }]);
});
});

describe('Partials', () => {
it('validates the partial range', () => {
assert.deepEqual(parser('A1 FETCH 1 BODY[]<5>').attributes[1].partial, [5]);
Expand Down