From 0d018fb0f2afc1f43f376eb8e73f8f3d9e9250a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Sun, 15 Dec 2024 14:38:04 +0100 Subject: [PATCH] feat(cookie): introduce lenient parsing mode (#3) --- README.md | 44 +++- src/cookie/parse/index.js | 20 +- src/grammar.bnf | 17 ++ src/grammar.js | 308 ++++++++++++++++------- test/cookie/parse.js | 508 ++++++++++++++++++++++++++++++++++---- types/index.d.ts | 7 +- 6 files changed, 750 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 59876eb..2eaaa74 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,32 @@ const parseResult = parseCookie('foo=bar'); parseResult.result.success; // => true ``` -**parseResult** variable has the following shape: +The **lenient** mode for cookie parsing is designed to handle and extract valid +cookie-pairs from potentially malformed or non-standard cookie strings. +It focuses on maintaining compatibility with real-world scenarios where cookie +headers may deviate from strict compliance with RFC 6265. + +```js +import { parseCookie } from '@swaggerexpert/cookie'; + +/** + * All of the following parse successfully. + */ + +parseCookie('foo1=bar; foo2=baz', { strict: false }); +parseCookie('foo1=bar;foo2=baz', { strict: false }); +parseCookie('FOO = bar; baz = raz', { strict: false }); +parseCookie('foo="bar=123456789&name=Magic+Mouse"', { strict: false }); +parseCookie('foo = "bar"', { strict: false }); +parseCookie('foo = bar ; fizz = buzz', { strict: false }); +parseCookie('foo =', { strict: false }); +parseCookie('\tfoo\t=\tbar\t', { strict: false }); +parseCookie('foo1=bar;foo2=baz', { strict: false }); +parseCookie('foo1=bar; foo2=baz', { strict: false }); +parseCookie('foo=bar; fizz; buzz', { strict: false }); +``` + +**ParseResult** returned by the parser has the following shape: ``` { @@ -177,6 +202,15 @@ String(grammar); The cookie is defined by the following [ABNF](https://tools.ietf.org/html/rfc5234) syntax ```abnf +; Lenient version of https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1 +lenient-cookie-string = lenient-cookie-pair *( ";" OWS ( lenient-cookie-pair / lenient-cookie-pair-invalid ) ) +lenient-cookie-pair = OWS cookie-name OWS "=" OWS lenient-cookie-value OWS +lenient-cookie-pair-invalid = OWS *tchar OWS ; Allow for standalone entries like "fizz" to be ignored +lenient-cookie-value = lenient-quoted-value / *lenient-cookie-octet +lenient-quoted-value = DQUOTE *( %x20-21 / %x23-7E ) DQUOTE ; Allow all printable US-ASCII except DQUOTE +lenient-cookie-octet = %x20-2B / %x2D-3A / %x3C-7E + ; Allow all printable characters except control chars and DQUOTE, except for semicolon + ; https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1 cookie-string = cookie-pair *( ";" SP cookie-pair ) cookie-pair = cookie-name "=" cookie-value @@ -187,6 +221,9 @@ cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E ; whitespace DQUOTE, comma, semicolon, ; and backslash +; https://datatracker.ietf.org/doc/html/rfc6265#section-2.2 +OWS = *( [ CRLF ] WSP ) ; "optional" whitespace + ; https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 token = 1*(tchar) tchar = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7A / %x7C / %x7E @@ -199,6 +236,11 @@ HT = %x09 ; US-ASCII HT, horizontal-tab (9) ; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 DQUOTE = %x22 ; " (Double Quote) +WSP = SP / HTAB ; white space +HTAB = %x09 ; horizontal tab +CRLF = CR LF ; Internet standard newline +CR = %x0D ; carriage return +LF = %x0A ; linefeed ``` ## License diff --git a/src/cookie/parse/index.js b/src/cookie/parse/index.js index a28bba1..0a55218 100644 --- a/src/cookie/parse/index.js +++ b/src/cookie/parse/index.js @@ -8,16 +8,24 @@ import cookieValueCallback from './callbacks/cookie-value.js'; const grammar = new Grammar(); -const parse = (cookieString) => { +const parse = (cookieString, { strict = true } = {}) => { const parser = new Parser(); parser.ast = new AST(); - parser.ast.callbacks['cookie-string'] = cookieStringCallback; - parser.ast.callbacks['cookie-pair'] = cookiePairCallback; - parser.ast.callbacks['cookie-name'] = cookieNameCallback; - parser.ast.callbacks['cookie-value'] = cookieValueCallback; + if (strict) { + parser.ast.callbacks['cookie-string'] = cookieStringCallback; + parser.ast.callbacks['cookie-pair'] = cookiePairCallback; + parser.ast.callbacks['cookie-name'] = cookieNameCallback; + parser.ast.callbacks['cookie-value'] = cookieValueCallback; + } else { + parser.ast.callbacks['lenient-cookie-string'] = cookieStringCallback; + parser.ast.callbacks['lenient-cookie-pair'] = cookiePairCallback; + parser.ast.callbacks['cookie-name'] = cookieNameCallback; + parser.ast.callbacks['lenient-cookie-value'] = cookieValueCallback; + } - const result = parser.parse(grammar, 'cookie-string', cookieString); + const startRule = strict ? 'cookie-string' : 'lenient-cookie-string'; + const result = parser.parse(grammar, startRule, cookieString); return { result, ast: parser.ast }; }; diff --git a/src/grammar.bnf b/src/grammar.bnf index b04600b..0f48247 100644 --- a/src/grammar.bnf +++ b/src/grammar.bnf @@ -1,3 +1,12 @@ +; Lenient version of https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1 +lenient-cookie-string = lenient-cookie-pair *( ";" OWS ( lenient-cookie-pair / lenient-cookie-pair-invalid ) ) +lenient-cookie-pair = OWS cookie-name OWS "=" OWS lenient-cookie-value OWS +lenient-cookie-pair-invalid = OWS *tchar OWS ; Allow for standalone entries like "fizz" to be ignored +lenient-cookie-value = lenient-quoted-value / *lenient-cookie-octet +lenient-quoted-value = DQUOTE *( %x20-21 / %x23-7E ) DQUOTE ; Allow all printable US-ASCII except DQUOTE +lenient-cookie-octet = %x20-2B / %x2D-3A / %x3C-7E + ; Allow all printable characters except control chars and DQUOTE, except for semicolon + ; https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1 cookie-string = cookie-pair *( ";" SP cookie-pair ) cookie-pair = cookie-name "=" cookie-value @@ -8,6 +17,9 @@ cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E ; whitespace DQUOTE, comma, semicolon, ; and backslash +; https://datatracker.ietf.org/doc/html/rfc6265#section-2.2 +OWS = *( [ CRLF ] WSP ) ; "optional" whitespace + ; https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 token = 1*(tchar) tchar = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7A / %x7C / %x7E @@ -20,3 +32,8 @@ HT = %x09 ; US-ASCII HT, horizontal-tab (9) ; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1 DQUOTE = %x22 ; " (Double Quote) +WSP = SP / HTAB ; white space +HTAB = %x09 ; horizontal tab +CRLF = CR LF ; Internet standard newline +CR = %x0D ; carriage return +LF = %x0A ; linefeed diff --git a/src/grammar.js b/src/grammar.js index ae52b26..0f3b074 100644 --- a/src/grammar.js +++ b/src/grammar.js @@ -5,17 +5,17 @@ export default function grammar(){ // ``` // SUMMARY - // rules = 13 + // rules = 25 // udts = 0 - // opcodes = 65 + // opcodes = 116 // --- ABNF original opcodes - // ALT = 5 - // CAT = 4 - // REP = 4 - // RNM = 13 - // TLS = 18 - // TBS = 9 - // TRG = 12 + // ALT = 10 + // CAT = 11 + // REP = 10 + // RNM = 36 + // TLS = 20 + // TBS = 12 + // TRG = 17 // --- SABNF superset opcodes // UDT = 0 // AND = 0 @@ -27,131 +27,239 @@ export default function grammar(){ /* RULES */ this.rules = []; - this.rules[0] = { name: 'cookie-string', lower: 'cookie-string', index: 0, isBkr: false }; - this.rules[1] = { name: 'cookie-pair', lower: 'cookie-pair', index: 1, isBkr: false }; - this.rules[2] = { name: 'cookie-name', lower: 'cookie-name', index: 2, isBkr: false }; - this.rules[3] = { name: 'cookie-value', lower: 'cookie-value', index: 3, isBkr: false }; - this.rules[4] = { name: 'cookie-octet', lower: 'cookie-octet', index: 4, isBkr: false }; - this.rules[5] = { name: 'token', lower: 'token', index: 5, isBkr: false }; - this.rules[6] = { name: 'tchar', lower: 'tchar', index: 6, isBkr: false }; - this.rules[7] = { name: 'CHAR', lower: 'char', index: 7, isBkr: false }; - this.rules[8] = { name: 'CTL', lower: 'ctl', index: 8, isBkr: false }; - this.rules[9] = { name: 'separators', lower: 'separators', index: 9, isBkr: false }; - this.rules[10] = { name: 'SP', lower: 'sp', index: 10, isBkr: false }; - this.rules[11] = { name: 'HT', lower: 'ht', index: 11, isBkr: false }; - this.rules[12] = { name: 'DQUOTE', lower: 'dquote', index: 12, isBkr: false }; + this.rules[0] = { name: 'lenient-cookie-string', lower: 'lenient-cookie-string', index: 0, isBkr: false }; + this.rules[1] = { name: 'lenient-cookie-pair', lower: 'lenient-cookie-pair', index: 1, isBkr: false }; + this.rules[2] = { name: 'lenient-cookie-pair-invalid', lower: 'lenient-cookie-pair-invalid', index: 2, isBkr: false }; + this.rules[3] = { name: 'lenient-cookie-value', lower: 'lenient-cookie-value', index: 3, isBkr: false }; + this.rules[4] = { name: 'lenient-quoted-value', lower: 'lenient-quoted-value', index: 4, isBkr: false }; + this.rules[5] = { name: 'lenient-cookie-octet', lower: 'lenient-cookie-octet', index: 5, isBkr: false }; + this.rules[6] = { name: 'cookie-string', lower: 'cookie-string', index: 6, isBkr: false }; + this.rules[7] = { name: 'cookie-pair', lower: 'cookie-pair', index: 7, isBkr: false }; + this.rules[8] = { name: 'cookie-name', lower: 'cookie-name', index: 8, isBkr: false }; + this.rules[9] = { name: 'cookie-value', lower: 'cookie-value', index: 9, isBkr: false }; + this.rules[10] = { name: 'cookie-octet', lower: 'cookie-octet', index: 10, isBkr: false }; + this.rules[11] = { name: 'OWS', lower: 'ows', index: 11, isBkr: false }; + this.rules[12] = { name: 'token', lower: 'token', index: 12, isBkr: false }; + this.rules[13] = { name: 'tchar', lower: 'tchar', index: 13, isBkr: false }; + this.rules[14] = { name: 'CHAR', lower: 'char', index: 14, isBkr: false }; + this.rules[15] = { name: 'CTL', lower: 'ctl', index: 15, isBkr: false }; + this.rules[16] = { name: 'separators', lower: 'separators', index: 16, isBkr: false }; + this.rules[17] = { name: 'SP', lower: 'sp', index: 17, isBkr: false }; + this.rules[18] = { name: 'HT', lower: 'ht', index: 18, isBkr: false }; + this.rules[19] = { name: 'DQUOTE', lower: 'dquote', index: 19, isBkr: false }; + this.rules[20] = { name: 'WSP', lower: 'wsp', index: 20, isBkr: false }; + this.rules[21] = { name: 'HTAB', lower: 'htab', index: 21, isBkr: false }; + this.rules[22] = { name: 'CRLF', lower: 'crlf', index: 22, isBkr: false }; + this.rules[23] = { name: 'CR', lower: 'cr', index: 23, isBkr: false }; + this.rules[24] = { name: 'LF', lower: 'lf', index: 24, isBkr: false }; /* UDTS */ this.udts = []; /* OPCODES */ - /* cookie-string */ + /* lenient-cookie-string */ this.rules[0].opcodes = []; this.rules[0].opcodes[0] = { type: 2, children: [1,2] };// CAT - this.rules[0].opcodes[1] = { type: 4, index: 1 };// RNM(cookie-pair) + this.rules[0].opcodes[1] = { type: 4, index: 1 };// RNM(lenient-cookie-pair) this.rules[0].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP this.rules[0].opcodes[3] = { type: 2, children: [4,5,6] };// CAT this.rules[0].opcodes[4] = { type: 7, string: [59] };// TLS - this.rules[0].opcodes[5] = { type: 4, index: 10 };// RNM(SP) - this.rules[0].opcodes[6] = { type: 4, index: 1 };// RNM(cookie-pair) + this.rules[0].opcodes[5] = { type: 4, index: 11 };// RNM(OWS) + this.rules[0].opcodes[6] = { type: 1, children: [7,8] };// ALT + this.rules[0].opcodes[7] = { type: 4, index: 1 };// RNM(lenient-cookie-pair) + this.rules[0].opcodes[8] = { type: 4, index: 2 };// RNM(lenient-cookie-pair-invalid) - /* cookie-pair */ + /* lenient-cookie-pair */ this.rules[1].opcodes = []; - this.rules[1].opcodes[0] = { type: 2, children: [1,2,3] };// CAT - this.rules[1].opcodes[1] = { type: 4, index: 2 };// RNM(cookie-name) - this.rules[1].opcodes[2] = { type: 7, string: [61] };// TLS - this.rules[1].opcodes[3] = { type: 4, index: 3 };// RNM(cookie-value) + this.rules[1].opcodes[0] = { type: 2, children: [1,2,3,4,5,6,7] };// CAT + this.rules[1].opcodes[1] = { type: 4, index: 11 };// RNM(OWS) + this.rules[1].opcodes[2] = { type: 4, index: 8 };// RNM(cookie-name) + this.rules[1].opcodes[3] = { type: 4, index: 11 };// RNM(OWS) + this.rules[1].opcodes[4] = { type: 7, string: [61] };// TLS + this.rules[1].opcodes[5] = { type: 4, index: 11 };// RNM(OWS) + this.rules[1].opcodes[6] = { type: 4, index: 3 };// RNM(lenient-cookie-value) + this.rules[1].opcodes[7] = { type: 4, index: 11 };// RNM(OWS) - /* cookie-name */ + /* lenient-cookie-pair-invalid */ this.rules[2].opcodes = []; - this.rules[2].opcodes[0] = { type: 4, index: 5 };// RNM(token) + this.rules[2].opcodes[0] = { type: 2, children: [1,2,4] };// CAT + this.rules[2].opcodes[1] = { type: 4, index: 11 };// RNM(OWS) + this.rules[2].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP + this.rules[2].opcodes[3] = { type: 4, index: 13 };// RNM(tchar) + this.rules[2].opcodes[4] = { type: 4, index: 11 };// RNM(OWS) - /* cookie-value */ + /* lenient-cookie-value */ this.rules[3].opcodes = []; - this.rules[3].opcodes[0] = { type: 1, children: [1,3] };// ALT - this.rules[3].opcodes[1] = { type: 3, min: 0, max: Infinity };// REP - this.rules[3].opcodes[2] = { type: 4, index: 4 };// RNM(cookie-octet) - this.rules[3].opcodes[3] = { type: 2, children: [4,5,7] };// CAT - this.rules[3].opcodes[4] = { type: 4, index: 12 };// RNM(DQUOTE) - this.rules[3].opcodes[5] = { type: 3, min: 0, max: Infinity };// REP - this.rules[3].opcodes[6] = { type: 4, index: 4 };// RNM(cookie-octet) - this.rules[3].opcodes[7] = { type: 4, index: 12 };// RNM(DQUOTE) + this.rules[3].opcodes[0] = { type: 1, children: [1,2] };// ALT + this.rules[3].opcodes[1] = { type: 4, index: 4 };// RNM(lenient-quoted-value) + this.rules[3].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP + this.rules[3].opcodes[3] = { type: 4, index: 5 };// RNM(lenient-cookie-octet) - /* cookie-octet */ + /* lenient-quoted-value */ this.rules[4].opcodes = []; - this.rules[4].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT - this.rules[4].opcodes[1] = { type: 6, string: [33] };// TBS - this.rules[4].opcodes[2] = { type: 5, min: 35, max: 43 };// TRG - this.rules[4].opcodes[3] = { type: 5, min: 45, max: 58 };// TRG - this.rules[4].opcodes[4] = { type: 5, min: 60, max: 91 };// TRG - this.rules[4].opcodes[5] = { type: 5, min: 93, max: 126 };// TRG + this.rules[4].opcodes[0] = { type: 2, children: [1,2,6] };// CAT + this.rules[4].opcodes[1] = { type: 4, index: 19 };// RNM(DQUOTE) + this.rules[4].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP + this.rules[4].opcodes[3] = { type: 1, children: [4,5] };// ALT + this.rules[4].opcodes[4] = { type: 5, min: 32, max: 33 };// TRG + this.rules[4].opcodes[5] = { type: 5, min: 35, max: 126 };// TRG + this.rules[4].opcodes[6] = { type: 4, index: 19 };// RNM(DQUOTE) - /* token */ + /* lenient-cookie-octet */ this.rules[5].opcodes = []; - this.rules[5].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP - this.rules[5].opcodes[1] = { type: 4, index: 6 };// RNM(tchar) + this.rules[5].opcodes[0] = { type: 1, children: [1,2,3] };// ALT + this.rules[5].opcodes[1] = { type: 5, min: 32, max: 43 };// TRG + this.rules[5].opcodes[2] = { type: 5, min: 45, max: 58 };// TRG + this.rules[5].opcodes[3] = { type: 5, min: 60, max: 126 };// TRG - /* tchar */ + /* cookie-string */ this.rules[6].opcodes = []; - this.rules[6].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9] };// ALT - this.rules[6].opcodes[1] = { type: 6, string: [33] };// TBS - this.rules[6].opcodes[2] = { type: 5, min: 35, max: 39 };// TRG - this.rules[6].opcodes[3] = { type: 5, min: 42, max: 43 };// TRG - this.rules[6].opcodes[4] = { type: 5, min: 45, max: 46 };// TRG - this.rules[6].opcodes[5] = { type: 5, min: 48, max: 57 };// TRG - this.rules[6].opcodes[6] = { type: 5, min: 65, max: 90 };// TRG - this.rules[6].opcodes[7] = { type: 5, min: 94, max: 122 };// TRG - this.rules[6].opcodes[8] = { type: 6, string: [124] };// TBS - this.rules[6].opcodes[9] = { type: 6, string: [126] };// TBS + this.rules[6].opcodes[0] = { type: 2, children: [1,2] };// CAT + this.rules[6].opcodes[1] = { type: 4, index: 7 };// RNM(cookie-pair) + this.rules[6].opcodes[2] = { type: 3, min: 0, max: Infinity };// REP + this.rules[6].opcodes[3] = { type: 2, children: [4,5,6] };// CAT + this.rules[6].opcodes[4] = { type: 7, string: [59] };// TLS + this.rules[6].opcodes[5] = { type: 4, index: 17 };// RNM(SP) + this.rules[6].opcodes[6] = { type: 4, index: 7 };// RNM(cookie-pair) - /* CHAR */ + /* cookie-pair */ this.rules[7].opcodes = []; - this.rules[7].opcodes[0] = { type: 5, min: 1, max: 127 };// TRG + this.rules[7].opcodes[0] = { type: 2, children: [1,2,3] };// CAT + this.rules[7].opcodes[1] = { type: 4, index: 8 };// RNM(cookie-name) + this.rules[7].opcodes[2] = { type: 7, string: [61] };// TLS + this.rules[7].opcodes[3] = { type: 4, index: 9 };// RNM(cookie-value) - /* CTL */ + /* cookie-name */ this.rules[8].opcodes = []; - this.rules[8].opcodes[0] = { type: 1, children: [1,2] };// ALT - this.rules[8].opcodes[1] = { type: 5, min: 0, max: 31 };// TRG - this.rules[8].opcodes[2] = { type: 6, string: [127] };// TBS + this.rules[8].opcodes[0] = { type: 4, index: 12 };// RNM(token) - /* separators */ + /* cookie-value */ this.rules[9].opcodes = []; - this.rules[9].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] };// ALT - this.rules[9].opcodes[1] = { type: 7, string: [40] };// TLS - this.rules[9].opcodes[2] = { type: 7, string: [41] };// TLS - this.rules[9].opcodes[3] = { type: 7, string: [60] };// TLS - this.rules[9].opcodes[4] = { type: 7, string: [62] };// TLS - this.rules[9].opcodes[5] = { type: 7, string: [64] };// TLS - this.rules[9].opcodes[6] = { type: 7, string: [44] };// TLS - this.rules[9].opcodes[7] = { type: 7, string: [59] };// TLS - this.rules[9].opcodes[8] = { type: 7, string: [58] };// TLS - this.rules[9].opcodes[9] = { type: 7, string: [92] };// TLS - this.rules[9].opcodes[10] = { type: 6, string: [34] };// TBS - this.rules[9].opcodes[11] = { type: 7, string: [47] };// TLS - this.rules[9].opcodes[12] = { type: 7, string: [91] };// TLS - this.rules[9].opcodes[13] = { type: 7, string: [93] };// TLS - this.rules[9].opcodes[14] = { type: 7, string: [63] };// TLS - this.rules[9].opcodes[15] = { type: 7, string: [61] };// TLS - this.rules[9].opcodes[16] = { type: 7, string: [123] };// TLS - this.rules[9].opcodes[17] = { type: 7, string: [125] };// TLS - this.rules[9].opcodes[18] = { type: 4, index: 10 };// RNM(SP) - this.rules[9].opcodes[19] = { type: 4, index: 11 };// RNM(HT) + this.rules[9].opcodes[0] = { type: 1, children: [1,3] };// ALT + this.rules[9].opcodes[1] = { type: 3, min: 0, max: Infinity };// REP + this.rules[9].opcodes[2] = { type: 4, index: 10 };// RNM(cookie-octet) + this.rules[9].opcodes[3] = { type: 2, children: [4,5,7] };// CAT + this.rules[9].opcodes[4] = { type: 4, index: 19 };// RNM(DQUOTE) + this.rules[9].opcodes[5] = { type: 3, min: 0, max: Infinity };// REP + this.rules[9].opcodes[6] = { type: 4, index: 10 };// RNM(cookie-octet) + this.rules[9].opcodes[7] = { type: 4, index: 19 };// RNM(DQUOTE) - /* SP */ + /* cookie-octet */ this.rules[10].opcodes = []; - this.rules[10].opcodes[0] = { type: 6, string: [32] };// TBS + this.rules[10].opcodes[0] = { type: 1, children: [1,2,3,4,5] };// ALT + this.rules[10].opcodes[1] = { type: 6, string: [33] };// TBS + this.rules[10].opcodes[2] = { type: 5, min: 35, max: 43 };// TRG + this.rules[10].opcodes[3] = { type: 5, min: 45, max: 58 };// TRG + this.rules[10].opcodes[4] = { type: 5, min: 60, max: 91 };// TRG + this.rules[10].opcodes[5] = { type: 5, min: 93, max: 126 };// TRG - /* HT */ + /* OWS */ this.rules[11].opcodes = []; - this.rules[11].opcodes[0] = { type: 6, string: [9] };// TBS + this.rules[11].opcodes[0] = { type: 3, min: 0, max: Infinity };// REP + this.rules[11].opcodes[1] = { type: 2, children: [2,4] };// CAT + this.rules[11].opcodes[2] = { type: 3, min: 0, max: 1 };// REP + this.rules[11].opcodes[3] = { type: 4, index: 22 };// RNM(CRLF) + this.rules[11].opcodes[4] = { type: 4, index: 20 };// RNM(WSP) - /* DQUOTE */ + /* token */ this.rules[12].opcodes = []; - this.rules[12].opcodes[0] = { type: 6, string: [34] };// TBS + this.rules[12].opcodes[0] = { type: 3, min: 1, max: Infinity };// REP + this.rules[12].opcodes[1] = { type: 4, index: 13 };// RNM(tchar) + + /* tchar */ + this.rules[13].opcodes = []; + this.rules[13].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9] };// ALT + this.rules[13].opcodes[1] = { type: 6, string: [33] };// TBS + this.rules[13].opcodes[2] = { type: 5, min: 35, max: 39 };// TRG + this.rules[13].opcodes[3] = { type: 5, min: 42, max: 43 };// TRG + this.rules[13].opcodes[4] = { type: 5, min: 45, max: 46 };// TRG + this.rules[13].opcodes[5] = { type: 5, min: 48, max: 57 };// TRG + this.rules[13].opcodes[6] = { type: 5, min: 65, max: 90 };// TRG + this.rules[13].opcodes[7] = { type: 5, min: 94, max: 122 };// TRG + this.rules[13].opcodes[8] = { type: 6, string: [124] };// TBS + this.rules[13].opcodes[9] = { type: 6, string: [126] };// TBS + + /* CHAR */ + this.rules[14].opcodes = []; + this.rules[14].opcodes[0] = { type: 5, min: 1, max: 127 };// TRG + + /* CTL */ + this.rules[15].opcodes = []; + this.rules[15].opcodes[0] = { type: 1, children: [1,2] };// ALT + this.rules[15].opcodes[1] = { type: 5, min: 0, max: 31 };// TRG + this.rules[15].opcodes[2] = { type: 6, string: [127] };// TBS + + /* separators */ + this.rules[16].opcodes = []; + this.rules[16].opcodes[0] = { type: 1, children: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] };// ALT + this.rules[16].opcodes[1] = { type: 7, string: [40] };// TLS + this.rules[16].opcodes[2] = { type: 7, string: [41] };// TLS + this.rules[16].opcodes[3] = { type: 7, string: [60] };// TLS + this.rules[16].opcodes[4] = { type: 7, string: [62] };// TLS + this.rules[16].opcodes[5] = { type: 7, string: [64] };// TLS + this.rules[16].opcodes[6] = { type: 7, string: [44] };// TLS + this.rules[16].opcodes[7] = { type: 7, string: [59] };// TLS + this.rules[16].opcodes[8] = { type: 7, string: [58] };// TLS + this.rules[16].opcodes[9] = { type: 7, string: [92] };// TLS + this.rules[16].opcodes[10] = { type: 6, string: [34] };// TBS + this.rules[16].opcodes[11] = { type: 7, string: [47] };// TLS + this.rules[16].opcodes[12] = { type: 7, string: [91] };// TLS + this.rules[16].opcodes[13] = { type: 7, string: [93] };// TLS + this.rules[16].opcodes[14] = { type: 7, string: [63] };// TLS + this.rules[16].opcodes[15] = { type: 7, string: [61] };// TLS + this.rules[16].opcodes[16] = { type: 7, string: [123] };// TLS + this.rules[16].opcodes[17] = { type: 7, string: [125] };// TLS + this.rules[16].opcodes[18] = { type: 4, index: 17 };// RNM(SP) + this.rules[16].opcodes[19] = { type: 4, index: 18 };// RNM(HT) + + /* SP */ + this.rules[17].opcodes = []; + this.rules[17].opcodes[0] = { type: 6, string: [32] };// TBS + + /* HT */ + this.rules[18].opcodes = []; + this.rules[18].opcodes[0] = { type: 6, string: [9] };// TBS + + /* DQUOTE */ + this.rules[19].opcodes = []; + this.rules[19].opcodes[0] = { type: 6, string: [34] };// TBS + + /* WSP */ + this.rules[20].opcodes = []; + this.rules[20].opcodes[0] = { type: 1, children: [1,2] };// ALT + this.rules[20].opcodes[1] = { type: 4, index: 17 };// RNM(SP) + this.rules[20].opcodes[2] = { type: 4, index: 21 };// RNM(HTAB) + + /* HTAB */ + this.rules[21].opcodes = []; + this.rules[21].opcodes[0] = { type: 6, string: [9] };// TBS + + /* CRLF */ + this.rules[22].opcodes = []; + this.rules[22].opcodes[0] = { type: 2, children: [1,2] };// CAT + this.rules[22].opcodes[1] = { type: 4, index: 23 };// RNM(CR) + this.rules[22].opcodes[2] = { type: 4, index: 24 };// RNM(LF) + + /* CR */ + this.rules[23].opcodes = []; + this.rules[23].opcodes[0] = { type: 6, string: [13] };// TBS + + /* LF */ + this.rules[24].opcodes = []; + this.rules[24].opcodes[0] = { type: 6, string: [10] };// TBS // The `toString()` function will display the original grammar file(s) that produced these opcodes. this.toString = function toString(){ let str = ""; + str += "; Lenient version of https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1\n"; + str += "lenient-cookie-string = lenient-cookie-pair *( \";\" OWS ( lenient-cookie-pair / lenient-cookie-pair-invalid ) )\n"; + str += "lenient-cookie-pair = OWS cookie-name OWS \"=\" OWS lenient-cookie-value OWS\n"; + str += "lenient-cookie-pair-invalid = OWS *tchar OWS ; Allow for standalone entries like \"fizz\" to be ignored\n"; + str += "lenient-cookie-value = lenient-quoted-value / *lenient-cookie-octet\n"; + str += "lenient-quoted-value = DQUOTE *( %x20-21 / %x23-7E ) DQUOTE ; Allow all printable US-ASCII except DQUOTE\n"; + str += "lenient-cookie-octet = %x20-2B / %x2D-3A / %x3C-7E\n"; + str += " ; Allow all printable characters except control chars and DQUOTE, except for semicolon\n"; + str += "\n"; str += "; https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1\n"; str += "cookie-string = cookie-pair *( \";\" SP cookie-pair )\n"; str += "cookie-pair = cookie-name \"=\" cookie-value\n"; @@ -162,6 +270,9 @@ export default function grammar(){ str += " ; whitespace DQUOTE, comma, semicolon,\n"; str += " ; and backslash\n"; str += "\n"; + str += "; https://datatracker.ietf.org/doc/html/rfc6265#section-2.2\n"; + str += "OWS = *( [ CRLF ] WSP ) ; \"optional\" whitespace\n"; + str += "\n"; str += "; https://datatracker.ietf.org/doc/html/rfc2616#section-2.2\n"; str += "token = 1*(tchar)\n"; str += "tchar = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7A / %x7C / %x7E\n"; @@ -174,6 +285,11 @@ export default function grammar(){ str += "\n"; str += "; https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1\n"; str += "DQUOTE = %x22 ; \" (Double Quote)\n"; + str += "WSP = SP / HTAB ; white space\n"; + str += "HTAB = %x09 ; horizontal tab\n"; + str += "CRLF = CR LF ; Internet standard newline\n"; + str += "CR = %x0D ; carriage return\n"; + str += "LF = %x0A ; linefeed\n"; return str; } } diff --git a/test/cookie/parse.js b/test/cookie/parse.js index 24464c2..f94cf2f 100644 --- a/test/cookie/parse.js +++ b/test/cookie/parse.js @@ -4,7 +4,7 @@ import { parseCookie } from '../../src/index.js'; describe('parseCookie', function () { context('given valid cookie string', function () { - context('single cookie pair', function () { + context('single cookie pair #1', function () { specify('should parse and translate', function () { const parseResult = parseCookie('foo=bar'); @@ -21,6 +21,31 @@ describe('parseCookie', function () { }); }); + context('single cookie pair #2', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo=123'); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=123'], + ['cookie-pair', 'foo=123'], + ['cookie-name', 'foo'], + ['cookie-value', '123'], + ]); + }); + }); + + context('cookie with whitespaces', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('FOO = bar; baz = raz'); + + assert.isFalse(parseResult.result.success); + }); + }); + context('multiple cookie pairs', function () { specify('should parse and translate', function () { const parseResult = parseCookie('foo1=bar1; foo2=123'); @@ -98,7 +123,15 @@ describe('parseCookie', function () { }); }); - context('percent encoded cookie value', function () { + context('percent encoded cookie value #1', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('foo="bar=123456789&name=Magic+Mouse"'); + + assert.isFalse(parseResult.result.success); + }); + }); + + context('percent encoded cookie value #2 ', function () { specify('should parse and translate', function () { const parseResult = parseCookie('email=%20%22%2c%3b%2f'); @@ -115,6 +148,44 @@ describe('parseCookie', function () { }); }); + context('cookie with whitespaces around key and value', function () { + specify('should fail parsing', function () { + const parseResult1 = parseCookie(' foo = "bar" '); + const parseResult2 = parseCookie(' foo = bar ; fizz = buzz '); + const parseResult3 = parseCookie(' = bar '); + const parseResult4 = parseCookie(' foo = '); + const parseResult5 = parseCookie(' = '); + const parseResult6 = parseCookie('\tfoo\t=\tbar\t'); + + assert.isFalse(parseResult1.result.success); + assert.isFalse(parseResult2.result.success); + assert.isFalse(parseResult3.result.success); + assert.isFalse(parseResult4.result.success); + assert.isFalse(parseResult5.result.success); + assert.isFalse(parseResult6.result.success); + }); + }); + + context('damaged escaping', function () { + specify('should parse all and translate', function () { + const parseResult = parseCookie('foo=%1; bar=bar'); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=%1; bar=bar'], + ['cookie-pair', 'foo=%1'], + ['cookie-name', 'foo'], + ['cookie-value', '%1'], + ['cookie-pair', 'bar=bar'], + ['cookie-name', 'bar'], + ['cookie-value', 'bar'], + ]); + }); + }); + context('duplicate cookies', function () { specify('should parse all and translate', function () { const parseResult = parseCookie('foo=%1; bar=bar; foo=boo'); @@ -159,74 +230,411 @@ describe('parseCookie', function () { }); }); - context('given invalid cookie string', function () { - context('cookie with whitespaces', function () { - specify('should fail parsing', function () { - const parseResult = parseCookie('FOO = bar'); + context('cookie with no separator space', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('foo1=bar;foo2=baz'); - assert.isFalse(parseResult.result.success); - }); + assert.isFalse(parseResult.result.success); }); + }); - context('cookie with whitespaces around key and value', function () { - specify('should fail parsing', function () { - const parseResult1 = parseCookie(' foo = "bar" '); - const parseResult2 = parseCookie(' foo = bar ; fizz = buzz '); - const parseResult3 = parseCookie(' = bar '); - const parseResult4 = parseCookie(' foo = '); - const parseResult5 = parseCookie(' = '); - const parseResult6 = parseCookie('\tfoo\t=\tbar\t'); + context('cookie with multiple separators', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('foo1=bar; foo2=baz'); - assert.isFalse(parseResult1.result.success); - assert.isFalse(parseResult2.result.success); - assert.isFalse(parseResult3.result.success); - assert.isFalse(parseResult4.result.success); - assert.isFalse(parseResult5.result.success); - assert.isFalse(parseResult6.result.success); - }); + assert.isFalse(parseResult.result.success); }); + }); - context('cookie with no separator', function () { - specify('should fail parsing', function () { - const parseResult = parseCookie('foo1=bar;foo2=baz'); + context('cookie with empty name', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('=value'); - assert.isFalse(parseResult.result.success); - }); + assert.isFalse(parseResult.result.success); + }); + }); + + context('cookies without value', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('foo=bar; fizz; buzz'); + + assert.isFalse(parseResult.result.success); }); + }); +}); - context('cookie with multiple separators', function () { - specify('should fail parsing', function () { - const parseResult = parseCookie('foo1=bar; foo2=baz'); +context('parseCookie in lenient mode', function () { + context('single cookie pair #1', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo=bar', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=bar'], + ['cookie-pair', 'foo=bar'], + ['cookie-name', 'foo'], + ['cookie-value', 'bar'], + ]); + }); + }); - assert.isFalse(parseResult.result.success); - }); + context('single cookie pair #2', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo=123', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=123'], + ['cookie-pair', 'foo=123'], + ['cookie-name', 'foo'], + ['cookie-value', '123'], + ]); }); + }); - context('percent encoded cookie value', function () { - specify('should fail parsing', function () { - const parseResult = parseCookie('foo="bar=123456789&name=Magic+Mouse"'); + context('cookie with OWS', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('FOO = bar; baz = raz', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'FOO = bar; baz = raz'], + ['cookie-pair', 'FOO = bar'], + ['cookie-name', 'FOO'], + ['cookie-value', 'bar'], + ['cookie-pair', 'baz = raz'], + ['cookie-name', 'baz'], + ['cookie-value', 'raz'], + ]); + }); + }); - const parts = []; - parseResult.ast.translate(parts); + context('multiple cookie pairs', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo1=bar1; foo2=123', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo1=bar1; foo2=123'], + ['cookie-pair', 'foo1=bar1'], + ['cookie-name', 'foo1'], + ['cookie-value', 'bar1'], + ['cookie-pair', 'foo2=123'], + ['cookie-name', 'foo2'], + ['cookie-value', '123'], + ]); + }); + }); - assert.isFalse(parseResult.result.success); - }); + context('cookie with empty value', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo=; bar=', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=; bar='], + ['cookie-pair', 'foo='], + ['cookie-name', 'foo'], + ['cookie-value', ''], + ['cookie-pair', 'bar='], + ['cookie-name', 'bar'], + ['cookie-value', ''], + ]); }); + }); - context('cookie with empty name', function () { - specify('should fail parsing', function () { - const parseResult = parseCookie('=value'); + context('cookie with minimum length #1', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('f=', { strict: false }); - assert.isFalse(parseResult.result.success); - }); + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'f='], + ['cookie-pair', 'f='], + ['cookie-name', 'f'], + ['cookie-value', ''], + ]); }); + }); - context('cookies without value', function () { - specify('should fail parsing', function () { - const parseResult = parseCookie('foo=bar; fizz; buzz'); + context('cookie with minimum length #2', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('f=; b=', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'f=; b='], + ['cookie-pair', 'f='], + ['cookie-name', 'f'], + ['cookie-value', ''], + ['cookie-pair', 'b='], + ['cookie-name', 'b'], + ['cookie-value', ''], + ]); + }); + }); - assert.isFalse(parseResult.result.success); - }); + context('percent encoded cookie value #1', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo="bar=123456789&name=Magic+Mouse"', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo="bar=123456789&name=Magic+Mouse"'], + ['cookie-pair', 'foo="bar=123456789&name=Magic+Mouse"'], + ['cookie-name', 'foo'], + ['cookie-value', '"bar=123456789&name=Magic+Mouse"'], + ]); + }); + }); + + context('percent encoded cookie value #2 ', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('email=%20%22%2c%3b%2f', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'email=%20%22%2c%3b%2f'], + ['cookie-pair', 'email=%20%22%2c%3b%2f'], + ['cookie-name', 'email'], + ['cookie-value', '%20%22%2c%3b%2f'], + ]); + }); + }); + + context('cookie with whitespaces around key and value #1', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie(' foo = "bar" ', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', ' foo = "bar" '], + ['cookie-pair', ' foo = "bar" '], + ['cookie-name', 'foo'], + ['cookie-value', '"bar"'], + ]); + }); + }); + + context('cookie with whitespaces around key and value #2', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie(' foo = bar ; fizz = buzz ', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', ' foo = bar ; fizz = buzz '], + ['cookie-pair', ' foo = bar '], + ['cookie-name', 'foo'], + ['cookie-value', 'bar '], + ['cookie-pair', 'fizz = buzz '], + ['cookie-name', 'fizz'], + ['cookie-value', 'buzz '], + ]); + }); + }); + + context('cookie with whitespaces around key and value #3', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie(' foo = ', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', ' foo = '], + ['cookie-pair', ' foo = '], + ['cookie-name', 'foo'], + ['cookie-value', ''], + ]); + }); + }); + + context('cookie with whitespaces around key and value #4', function () { + specify('should fail parsing', function () { + const parseResult1 = parseCookie(' = bar ', { strict: false }); + const parseResult2 = parseCookie(' = ', { strict: false }); + + assert.isFalse(parseResult1.result.success); + assert.isFalse(parseResult2.result.success); + }); + }); + + context('cookie with whitespaces around key and value #5', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('\tfoo\t=\tbar\t', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', '\tfoo\t=\tbar\t'], + ['cookie-pair', '\tfoo\t=\tbar\t'], + ['cookie-name', 'foo'], + ['cookie-value', 'bar'], + ]); + }); + }); + + context('damaged escaping', function () { + specify('should parse all and translate', function () { + const parseResult = parseCookie('foo=%1; bar=bar', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=%1; bar=bar'], + ['cookie-pair', 'foo=%1'], + ['cookie-name', 'foo'], + ['cookie-value', '%1'], + ['cookie-pair', 'bar=bar'], + ['cookie-name', 'bar'], + ['cookie-value', 'bar'], + ]); + }); + }); + + context('duplicate cookies', function () { + specify('should parse all and translate', function () { + const parseResult = parseCookie('foo=%1; bar=bar; foo=boo', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=%1; bar=bar; foo=boo'], + ['cookie-pair', 'foo=%1'], + ['cookie-name', 'foo'], + ['cookie-value', '%1'], + ['cookie-pair', 'bar=bar'], + ['cookie-name', 'bar'], + ['cookie-value', 'bar'], + ['cookie-pair', 'foo=boo'], + ['cookie-name', 'foo'], + ['cookie-value', 'boo'], + ]); + }); + }); + + context('native properties', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('toString=foo; valueOf=bar', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'toString=foo; valueOf=bar'], + ['cookie-pair', 'toString=foo'], + ['cookie-name', 'toString'], + ['cookie-value', 'foo'], + ['cookie-pair', 'valueOf=bar'], + ['cookie-name', 'valueOf'], + ['cookie-value', 'bar'], + ]); + }); + }); + + context('cookie with no separator space', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo1=bar;foo2=baz', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo1=bar;foo2=baz'], + ['cookie-pair', 'foo1=bar'], + ['cookie-name', 'foo1'], + ['cookie-value', 'bar'], + ['cookie-pair', 'foo2=baz'], + ['cookie-name', 'foo2'], + ['cookie-value', 'baz'], + ]); + }); + }); + + context('cookie with multiple separators', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo1=bar; foo2=baz', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo1=bar; foo2=baz'], + ['cookie-pair', 'foo1=bar'], + ['cookie-name', 'foo1'], + ['cookie-value', 'bar'], + ['cookie-pair', 'foo2=baz'], + ['cookie-name', 'foo2'], + ['cookie-value', 'baz'], + ]); + }); + }); + + context('cookie with empty name', function () { + specify('should fail parsing', function () { + const parseResult = parseCookie('=value', { strict: false }); + + assert.isFalse(parseResult.result.success); + }); + }); + + context('cookies without value', function () { + specify('should parse and translate', function () { + const parseResult = parseCookie('foo=bar; fizz; buzz', { strict: false }); + + const parts = []; + parseResult.ast.translate(parts); + + assert.isTrue(parseResult.result.success); + assert.deepEqual(parts, [ + ['cookie-string', 'foo=bar; fizz; buzz'], + ['cookie-pair', 'foo=bar'], + ['cookie-name', 'foo'], + ['cookie-value', 'bar'], + ]); }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 1dfafc9..bac7528 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -8,6 +8,11 @@ interface ParseResult { }; } +interface ParseOptions { + readonly strict?: boolean; +} + + export interface Grammar { grammarObject: string; // Internal identifier rules: Rule[]; // List of grammar rules @@ -33,5 +38,5 @@ export type Opcode = export type UDT = {}; // User-defined terminals (empty in this grammar) -export function parseCookie(cookieString: string): ParseResult; +export function parseCookie(cookieString: string, options?: ParseOptions): ParseResult; export function Grammar(): Grammar;