From 20115697e5e7265b6045b11055ef45d0470e157e Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 8 Sep 2024 10:01:13 -0400 Subject: [PATCH] Add ability to quote static network option values For the sake of convenience for filter list maintainers, this commit add ability to quote static network option values, so as to avoid the need to escape commas when parser ambiguity arises. The quotes can be `"`, `'`, or backticks. Example, the following filter requires escaping commas: example.com$xhr,replace=/"loremIpsum.*?([A-Z]"\}|"\}{2\,4})\}\]\,//,1p Can be now rewritten with no need to escape when using quotes: example.com$xhr,replace='/"loremIpsum.*?([A-Z]"\}|"\}{2,4})\}\],//',1p --- src/js/codemirror/ubo-static-filtering.js | 1 + src/js/static-filtering-parser.js | 105 ++++++++++++++-------- src/js/static-net-filtering.js | 12 ++- 3 files changed, 81 insertions(+), 37 deletions(-) diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index 2aaf85b3d..386ed52f3 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -189,6 +189,7 @@ const uBOStaticFilteringMode = (( ) => { mode.lastNetOptionType = nodeType; return 'def'; case sfp.NODE_TYPE_NET_OPTION_ASSIGN: + case sfp.NODE_TYPE_NET_OPTION_QUOTE: return 'def'; case sfp.NODE_TYPE_NET_OPTION_VALUE: if ( mode.astWalker.canGoDown() ) { break; } diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index e89f7b3a3..b0a4a15d1 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -195,6 +195,7 @@ export const NODE_TYPE_NET_OPTION_NAME_XHR = iota++; export const NODE_TYPE_NET_OPTION_NAME_WEBRTC = iota++; export const NODE_TYPE_NET_OPTION_NAME_WEBSOCKET = iota++; export const NODE_TYPE_NET_OPTION_ASSIGN = iota++; +export const NODE_TYPE_NET_OPTION_QUOTE = iota++; export const NODE_TYPE_NET_OPTION_VALUE = iota++; export const NODE_TYPE_OPTION_VALUE_DOMAIN_LIST = iota++; export const NODE_TYPE_OPTION_VALUE_DOMAIN_RAW = iota++; @@ -896,7 +897,9 @@ export class AstFilterParser { this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/; this.reBadCSP = /(?:^|[;,])\s*report-(?:to|uri)\b/i; this.reBadPP = /(?:^|[;,])\s*report-to\b/i; + this.reNetOption = /^(~?)([13a-z_-]+)(=?)/; this.reNoopOption = /^_+$/; + this.netOptionValueParser = new ArgListParser(','); this.scriptletArgListParser = new ArgListParser(','); } @@ -1959,16 +1962,17 @@ export class AstFilterParser { const head = this.allocHeadNode(); let prev = head, next = 0; let optionBeg = 0, optionEnd = 0; - let emptyOption = false, badComma = false; while ( optionBeg !== optionsEnd ) { - optionEnd = this.endOfNetOption(s, optionBeg); next = this.allocTypedNode( NODE_TYPE_NET_OPTION_RAW, parentBeg + optionBeg, - parentBeg + optionEnd + parentBeg + optionsEnd // open ended ); - emptyOption = optionEnd === optionBeg; - this.linkDown(next, this.parseNetOption(next)); + const { node: down, len: optionLen } = this.parseNetOption(next); + // set next's end to down's end + optionEnd += optionLen; + this.nodes[next+NODE_END_INDEX] = parentBeg + optionEnd; + this.linkDown(next, down); prev = this.linkRight(prev, next); if ( optionEnd === optionsEnd ) { break; } optionBeg = optionEnd + 1; @@ -1977,12 +1981,12 @@ export class AstFilterParser { parentBeg + optionEnd, parentBeg + optionBeg ); - badComma = optionBeg === optionsEnd; - prev = this.linkRight(prev, next); - if ( emptyOption || badComma ) { + if ( optionLen === 0 || optionBeg === optionsEnd ) { this.addNodeFlags(next, NODE_FLAG_ERROR); this.addFlags(AST_FLAG_HAS_ERROR); } + prev = this.linkRight(prev, next); + optionEnd = optionBeg; } this.linkRight(prev, this.allocSentinelNode(NODE_TYPE_NET_OPTION_SENTINEL, parentEnd) @@ -1990,19 +1994,21 @@ export class AstFilterParser { return this.throwHeadNode(head); } - endOfNetOption(s, beg) { - const match = this.reNetOptionComma.exec(s.slice(beg)); - return match !== null ? beg + match.index : s.length; - } - parseNetOption(parent) { const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; const s = this.getNodeString(parent); - const optionEnd = s.length; + const match = this.reNetOption.exec(s) || []; + if ( match.length === 0 ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_OPTION_UNKNOWN; + return { node: 0, len: s.length }; + } const head = this.allocHeadNode(); let prev = head, next = 0; - let nameBeg = 0; - if ( s.charCodeAt(0) === 0x7E ) { + const matchEnd = match && match[0].length || 0; + const negated = match[1] === '~'; + if ( negated ) { this.addNodeFlags(parent, NODE_FLAG_IS_NEGATED); next = this.allocTypedNode( NODE_TYPE_NET_OPTION_NAME_NOT, @@ -2010,11 +2016,11 @@ export class AstFilterParser { parentBeg+1 ); prev = this.linkRight(prev, next); - nameBeg += 1; } - const equalPos = s.indexOf('='); - const nameEnd = equalPos !== -1 ? equalPos : s.length; - const name = s.slice(nameBeg, nameEnd); + const nameBeg = negated ? 1 : 0; + const assigned = match[3] === '='; + const nameEnd = matchEnd - (assigned ? 1 : 0); + const name = match[2] || ''; let nodeOptionType = nodeTypeFromOptionName.get(name); if ( nodeOptionType === undefined ) { nodeOptionType = this.reNoopOption.test(name) @@ -2037,27 +2043,43 @@ export class AstFilterParser { this.addNodeToRegister(nodeOptionType, parent); } prev = this.linkRight(prev, next); - if ( equalPos === -1 ) { - return this.throwHeadNode(head); + if ( assigned === false ) { + return { node: this.throwHeadNode(head), len: matchEnd }; } - const valueBeg = equalPos + 1; next = this.allocTypedNode( NODE_TYPE_NET_OPTION_ASSIGN, - parentBeg + equalPos, - parentBeg + valueBeg + parentBeg + matchEnd - 1, + parentBeg + matchEnd ); prev = this.linkRight(prev, next); - if ( (equalPos+1) === optionEnd ) { - this.addNodeFlags(parent, NODE_FLAG_ERROR); - this.addFlags(AST_FLAG_HAS_ERROR); - return this.throwHeadNode(head); - } this.addNodeFlags(parent, NODE_FLAG_OPTION_HAS_VALUE); + const details = this.netOptionValueParser.nextArg(s, matchEnd); + if ( details.quoteBeg !== details.argBeg ) { + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_QUOTE, + parentBeg + details.quoteBeg, + parentBeg + details.argBeg + ); + prev = this.linkRight(prev, next); + } else { + const argEnd = this.endOfNetOption(s, matchEnd); + if ( argEnd !== details.argEnd ) { + details.argEnd = details.quoteEnd = argEnd; + } + } next = this.allocTypedNode( NODE_TYPE_NET_OPTION_VALUE, - parentBeg + valueBeg, - parentBeg + optionEnd + parentBeg + details.argBeg, + parentBeg + details.argEnd ); + if ( details.argBeg === details.argEnd ) { + this.addNodeFlags(parent, NODE_FLAG_ERROR); + this.addFlags(AST_FLAG_HAS_ERROR); + this.astError = AST_ERROR_OPTION_BADVALUE; + } else if ( details.transform ) { + const arg = s.slice(details.argBeg, details.argEnd); + this.setNodeTransform(next, this.netOptionValueParser.normalizeArg(arg)); + } switch ( nodeOptionType ) { case NODE_TYPE_NET_OPTION_NAME_DENYALLOW: this.linkDown(next, this.parseDomainList(next, '|'), 0b00000); @@ -2069,8 +2091,21 @@ export class AstFilterParser { default: break; } - this.linkRight(prev, next); - return this.throwHeadNode(head); + prev = this.linkRight(prev, next); + if ( details.quoteEnd !== details.argEnd ) { + next = this.allocTypedNode( + NODE_TYPE_NET_OPTION_QUOTE, + parentBeg + details.argEnd, + parentBeg + details.quoteEnd + ); + this.linkRight(prev, next); + } + return { node: this.throwHeadNode(head), len: details.quoteEnd }; + } + + endOfNetOption(s, beg) { + const match = this.reNetOptionComma.exec(s.slice(beg)); + return match !== null ? beg + match.index : s.length; } getNetOptionValue(type) { @@ -3086,8 +3121,8 @@ export const netOptionTokenDescriptors = new Map([ /* synonym */ [ 'rewrite', { mustAssign: true } ], [ 'redirect-rule', { mustAssign: true } ], [ 'removeparam', { } ], - [ 'replace', { mustAssign: true } ], /* synonym */ [ 'queryprune', { } ], + [ 'replace', { mustAssign: true } ], [ 'script', { canNegate: true } ], [ 'shide', { } ], /* synonym */ [ 'specifichide', { } ], diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index f30af31be..eaa2c7131 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -409,6 +409,14 @@ class LogData { isPureHostname() { return this.tokenHash === DOT_TOKEN_HASH; } + + static requote(s) { + if ( /^(["'`]).+\1$|,/.test(s) === false ) { return s; } + if ( s.includes("'") === false ) { return `'${s}'`; } + if ( s.includes('"') === false ) { return `"${s}"`; } + if ( s.includes('`') === false ) { return `\`${s}\``; } + return `'${s.replace(/'/g, "\\'")}'`; + } } /******************************************************************************/ @@ -2128,7 +2136,7 @@ class FilterModifier { let opt = modifierNameFromType.get(filterData[idata+2]); const refs = filterRefs[filterData[idata+3]]; if ( refs.value !== '' ) { - opt += `=${refs.value}`; + opt += `=${LogData.requote(refs.value)}`; } details.options.push(opt); } @@ -2947,7 +2955,7 @@ class FilterOnHeaders { const headerOpt = filterRefs[irefs].headerOpt; let opt = 'header'; if ( headerOpt !== '' ) { - opt += `=${headerOpt}`; + opt += `=${LogData.requote(headerOpt)}`; } details.options.push(opt); }