1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-09-29 06:07:11 +02:00

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
This commit is contained in:
Raymond Hill 2024-09-08 10:01:13 -04:00
parent 1b464f75cc
commit 20115697e5
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 81 additions and 37 deletions

View File

@ -189,6 +189,7 @@ const uBOStaticFilteringMode = (( ) => {
mode.lastNetOptionType = nodeType; mode.lastNetOptionType = nodeType;
return 'def'; return 'def';
case sfp.NODE_TYPE_NET_OPTION_ASSIGN: case sfp.NODE_TYPE_NET_OPTION_ASSIGN:
case sfp.NODE_TYPE_NET_OPTION_QUOTE:
return 'def'; return 'def';
case sfp.NODE_TYPE_NET_OPTION_VALUE: case sfp.NODE_TYPE_NET_OPTION_VALUE:
if ( mode.astWalker.canGoDown() ) { break; } if ( mode.astWalker.canGoDown() ) { break; }

View File

@ -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_WEBRTC = iota++;
export const NODE_TYPE_NET_OPTION_NAME_WEBSOCKET = iota++; export const NODE_TYPE_NET_OPTION_NAME_WEBSOCKET = iota++;
export const NODE_TYPE_NET_OPTION_ASSIGN = 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_NET_OPTION_VALUE = iota++;
export const NODE_TYPE_OPTION_VALUE_DOMAIN_LIST = iota++; export const NODE_TYPE_OPTION_VALUE_DOMAIN_LIST = iota++;
export const NODE_TYPE_OPTION_VALUE_DOMAIN_RAW = 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.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.reBadCSP = /(?:^|[;,])\s*report-(?:to|uri)\b/i;
this.reBadPP = /(?:^|[;,])\s*report-to\b/i; this.reBadPP = /(?:^|[;,])\s*report-to\b/i;
this.reNetOption = /^(~?)([13a-z_-]+)(=?)/;
this.reNoopOption = /^_+$/; this.reNoopOption = /^_+$/;
this.netOptionValueParser = new ArgListParser(',');
this.scriptletArgListParser = new ArgListParser(','); this.scriptletArgListParser = new ArgListParser(',');
} }
@ -1959,16 +1962,17 @@ export class AstFilterParser {
const head = this.allocHeadNode(); const head = this.allocHeadNode();
let prev = head, next = 0; let prev = head, next = 0;
let optionBeg = 0, optionEnd = 0; let optionBeg = 0, optionEnd = 0;
let emptyOption = false, badComma = false;
while ( optionBeg !== optionsEnd ) { while ( optionBeg !== optionsEnd ) {
optionEnd = this.endOfNetOption(s, optionBeg);
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_NET_OPTION_RAW, NODE_TYPE_NET_OPTION_RAW,
parentBeg + optionBeg, parentBeg + optionBeg,
parentBeg + optionEnd parentBeg + optionsEnd // open ended
); );
emptyOption = optionEnd === optionBeg; const { node: down, len: optionLen } = this.parseNetOption(next);
this.linkDown(next, 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); prev = this.linkRight(prev, next);
if ( optionEnd === optionsEnd ) { break; } if ( optionEnd === optionsEnd ) { break; }
optionBeg = optionEnd + 1; optionBeg = optionEnd + 1;
@ -1977,12 +1981,12 @@ export class AstFilterParser {
parentBeg + optionEnd, parentBeg + optionEnd,
parentBeg + optionBeg parentBeg + optionBeg
); );
badComma = optionBeg === optionsEnd; if ( optionLen === 0 || optionBeg === optionsEnd ) {
prev = this.linkRight(prev, next);
if ( emptyOption || badComma ) {
this.addNodeFlags(next, NODE_FLAG_ERROR); this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR); this.addFlags(AST_FLAG_HAS_ERROR);
} }
prev = this.linkRight(prev, next);
optionEnd = optionBeg;
} }
this.linkRight(prev, this.linkRight(prev,
this.allocSentinelNode(NODE_TYPE_NET_OPTION_SENTINEL, parentEnd) this.allocSentinelNode(NODE_TYPE_NET_OPTION_SENTINEL, parentEnd)
@ -1990,19 +1994,21 @@ export class AstFilterParser {
return this.throwHeadNode(head); 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) { parseNetOption(parent) {
const parentBeg = this.nodes[parent+NODE_BEG_INDEX]; const parentBeg = this.nodes[parent+NODE_BEG_INDEX];
const s = this.getNodeString(parent); 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(); const head = this.allocHeadNode();
let prev = head, next = 0; let prev = head, next = 0;
let nameBeg = 0; const matchEnd = match && match[0].length || 0;
if ( s.charCodeAt(0) === 0x7E ) { const negated = match[1] === '~';
if ( negated ) {
this.addNodeFlags(parent, NODE_FLAG_IS_NEGATED); this.addNodeFlags(parent, NODE_FLAG_IS_NEGATED);
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_NET_OPTION_NAME_NOT, NODE_TYPE_NET_OPTION_NAME_NOT,
@ -2010,11 +2016,11 @@ export class AstFilterParser {
parentBeg+1 parentBeg+1
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
nameBeg += 1;
} }
const equalPos = s.indexOf('='); const nameBeg = negated ? 1 : 0;
const nameEnd = equalPos !== -1 ? equalPos : s.length; const assigned = match[3] === '=';
const name = s.slice(nameBeg, nameEnd); const nameEnd = matchEnd - (assigned ? 1 : 0);
const name = match[2] || '';
let nodeOptionType = nodeTypeFromOptionName.get(name); let nodeOptionType = nodeTypeFromOptionName.get(name);
if ( nodeOptionType === undefined ) { if ( nodeOptionType === undefined ) {
nodeOptionType = this.reNoopOption.test(name) nodeOptionType = this.reNoopOption.test(name)
@ -2037,27 +2043,43 @@ export class AstFilterParser {
this.addNodeToRegister(nodeOptionType, parent); this.addNodeToRegister(nodeOptionType, parent);
} }
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
if ( equalPos === -1 ) { if ( assigned === false ) {
return this.throwHeadNode(head); return { node: this.throwHeadNode(head), len: matchEnd };
} }
const valueBeg = equalPos + 1;
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_NET_OPTION_ASSIGN, NODE_TYPE_NET_OPTION_ASSIGN,
parentBeg + equalPos, parentBeg + matchEnd - 1,
parentBeg + valueBeg parentBeg + matchEnd
); );
prev = this.linkRight(prev, next); 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); 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( next = this.allocTypedNode(
NODE_TYPE_NET_OPTION_VALUE, NODE_TYPE_NET_OPTION_VALUE,
parentBeg + valueBeg, parentBeg + details.argBeg,
parentBeg + optionEnd 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 ) { switch ( nodeOptionType ) {
case NODE_TYPE_NET_OPTION_NAME_DENYALLOW: case NODE_TYPE_NET_OPTION_NAME_DENYALLOW:
this.linkDown(next, this.parseDomainList(next, '|'), 0b00000); this.linkDown(next, this.parseDomainList(next, '|'), 0b00000);
@ -2069,8 +2091,21 @@ export class AstFilterParser {
default: default:
break; break;
} }
this.linkRight(prev, next); prev = this.linkRight(prev, next);
return this.throwHeadNode(head); 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) { getNetOptionValue(type) {
@ -3086,8 +3121,8 @@ export const netOptionTokenDescriptors = new Map([
/* synonym */ [ 'rewrite', { mustAssign: true } ], /* synonym */ [ 'rewrite', { mustAssign: true } ],
[ 'redirect-rule', { mustAssign: true } ], [ 'redirect-rule', { mustAssign: true } ],
[ 'removeparam', { } ], [ 'removeparam', { } ],
[ 'replace', { mustAssign: true } ],
/* synonym */ [ 'queryprune', { } ], /* synonym */ [ 'queryprune', { } ],
[ 'replace', { mustAssign: true } ],
[ 'script', { canNegate: true } ], [ 'script', { canNegate: true } ],
[ 'shide', { } ], [ 'shide', { } ],
/* synonym */ [ 'specifichide', { } ], /* synonym */ [ 'specifichide', { } ],

View File

@ -409,6 +409,14 @@ class LogData {
isPureHostname() { isPureHostname() {
return this.tokenHash === DOT_TOKEN_HASH; 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]); let opt = modifierNameFromType.get(filterData[idata+2]);
const refs = filterRefs[filterData[idata+3]]; const refs = filterRefs[filterData[idata+3]];
if ( refs.value !== '' ) { if ( refs.value !== '' ) {
opt += `=${refs.value}`; opt += `=${LogData.requote(refs.value)}`;
} }
details.options.push(opt); details.options.push(opt);
} }
@ -2947,7 +2955,7 @@ class FilterOnHeaders {
const headerOpt = filterRefs[irefs].headerOpt; const headerOpt = filterRefs[irefs].headerOpt;
let opt = 'header'; let opt = 'header';
if ( headerOpt !== '' ) { if ( headerOpt !== '' ) {
opt += `=${headerOpt}`; opt += `=${LogData.requote(headerOpt)}`;
} }
details.options.push(opt); details.options.push(opt);
} }