Add support for `match-case` option; fine-tune behavior of `redirect=`

`match-case`
------------

Related issue:
- https://github.com/uBlockOrigin/uAssets/issues/8280#issuecomment-735245452

The new filter option `match-case` can be used only for
regex-based filters. Using `match-case` with any other
sort of filters will cause uBO to discard the filter.

`redirect=`
-----------

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/1366

`redirect=` filters with unresolvable resource token at
runtime will be discarded.

Additionally, the implicit priority is now set to 1
(was 0). The idea is to allow custom `redirect=` filters
to be used strictly as fallback `redirect=` filters in case
another `redirect=` filter is not picked up.

For example, one might create a `redirect=click2load.html:0`
filter, to be taken if and only if the blocked resource is
not already being redirected by another "official" filter
in one of the enabled filter lists.
This commit is contained in:
Raymond Hill 2020-11-28 11:26:28 -05:00
parent c6d0204b23
commit eae7cd58fe
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
4 changed files with 95 additions and 57 deletions

View File

@ -140,8 +140,8 @@ const µBlock = (( ) => { // jshint ignore:line
// Read-only
systemSettings: {
compiledMagic: 35, // Increase when compiled format changes
selfieMagic: 35, // Increase when selfie format changes
compiledMagic: 36, // Increase when compiled format changes
selfieMagic: 36, // Increase when selfie format changes
},
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501

View File

@ -304,6 +304,16 @@ RedirectEngine.prototype.tokenToURL = function(fctxt, token) {
/******************************************************************************/
RedirectEngine.prototype.hasToken = function(token) {
const asDataURI = token.charCodeAt(0) === 0x25 /* '%' */;
if ( asDataURI ) {
token = token.slice(1);
}
return this.resources.get(this.aliases.get(token) || token) !== undefined;
};
/******************************************************************************/
RedirectEngine.prototype.toSelfie = async function() {
};

View File

@ -1923,22 +1923,23 @@ const OPTTokenImage = 20;
const OPTTokenImportant = 21;
const OPTTokenInlineFont = 22;
const OPTTokenInlineScript = 23;
const OPTTokenMedia = 24;
const OPTTokenMp4 = 25;
const OPTTokenObject = 26;
const OPTTokenOther = 27;
const OPTTokenPing = 28;
const OPTTokenPopunder = 29;
const OPTTokenPopup = 30;
const OPTTokenRedirect = 31;
const OPTTokenRedirectRule = 32;
const OPTTokenQueryprune = 33;
const OPTTokenScript = 34;
const OPTTokenShide = 35;
const OPTTokenXhr = 36;
const OPTTokenWebrtc = 37;
const OPTTokenWebsocket = 38;
const OPTTokenCount = 39;
const OPTTokenMatchCase = 24;
const OPTTokenMedia = 25;
const OPTTokenMp4 = 26;
const OPTTokenObject = 27;
const OPTTokenOther = 28;
const OPTTokenPing = 29;
const OPTTokenPopunder = 30;
const OPTTokenPopup = 31;
const OPTTokenRedirect = 32;
const OPTTokenRedirectRule = 33;
const OPTTokenQueryprune = 34;
const OPTTokenScript = 35;
const OPTTokenShide = 36;
const OPTTokenXhr = 37;
const OPTTokenWebrtc = 38;
const OPTTokenWebsocket = 39;
const OPTTokenCount = 40;
//const OPTPerOptionMask = 0x0000ff00;
const OPTCanNegate = 1 << 8;
@ -2021,6 +2022,7 @@ Parser.prototype.OPTTokenImportant = OPTTokenImportant;
Parser.prototype.OPTTokenInlineFont = OPTTokenInlineFont;
Parser.prototype.OPTTokenInlineScript = OPTTokenInlineScript;
Parser.prototype.OPTTokenInvalid = OPTTokenInvalid;
Parser.prototype.OPTTokenMatchCase = OPTTokenMatchCase;
Parser.prototype.OPTTokenMedia = OPTTokenMedia;
Parser.prototype.OPTTokenMp4 = OPTTokenMp4;
Parser.prototype.OPTTokenObject = OPTTokenObject;
@ -2082,6 +2084,7 @@ const netOptionTokenDescriptors = new Map([
[ 'important', OPTTokenImportant | OPTBlockOnly ],
[ 'inline-font', OPTTokenInlineFont | OPTNonNetworkType | OPTCanNegate | OPTNonCspableType | OPTNonRedirectableType ],
[ 'inline-script', OPTTokenInlineScript | OPTNonNetworkType | OPTCanNegate | OPTNonCspableType | OPTNonRedirectableType ],
[ 'match-case', OPTTokenMatchCase ],
[ 'media', OPTTokenMedia | OPTCanNegate | OPTNetworkType | OPTModifiableType | OPTRedirectableType | OPTNonCspableType ],
[ 'mp4', OPTTokenMp4 | OPTNetworkType | OPTBlockOnly | OPTModifierType ],
[ 'object', OPTTokenObject | OPTCanNegate | OPTNetworkType | OPTModifiableType | OPTRedirectableType | OPTNonCspableType ],
@ -2138,6 +2141,7 @@ Parser.netOptionTokenIds = new Map([
[ 'important', OPTTokenImportant ],
[ 'inline-font', OPTTokenInlineFont ],
[ 'inline-script', OPTTokenInlineScript ],
[ 'match-case', OPTTokenMatchCase ],
[ 'media', OPTTokenMedia ],
[ 'mp4', OPTTokenMp4 ],
[ 'object', OPTTokenObject ],
@ -2184,6 +2188,7 @@ Parser.netOptionTokenNames = new Map([
[ OPTTokenImportant, 'important' ],
[ OPTTokenInlineFont, 'inline-font' ],
[ OPTTokenInlineScript, 'inline-script' ],
[ OPTTokenMatchCase, 'match-case' ],
[ OPTTokenMedia, 'media' ],
[ OPTTokenMp4, 'mp4' ],
[ OPTTokenObject, 'object' ],
@ -2462,6 +2467,16 @@ const NetOptionsIterator = class {
}
}
}
// `match-case`: valid only for regex-based filters
{
const i = this.tokenPos[OPTTokenMatchCase];
if ( i !== -1 && this.parser.patternIsRegex() === false ) {
optSlices[i] = OPTTokenInvalid;
if ( this.interactive ) {
this.parser.errorSlices(optSlices[i+1], optSlices[i+5]);
}
}
}
return this;
}
next() {

View File

@ -146,6 +146,7 @@ const typeValueToTypeName = [
// valid until the next evaluation.
let $requestURL = '';
let $requestURLRaw = '';
let $requestHostname = '';
let $docHostname = '';
let $docDomain = '';
@ -867,9 +868,13 @@ const FilterPatternGeneric = class {
}
static compile(details) {
const anchor = details.anchor;
const out = [
FilterPatternGeneric.fid,
details.pattern,
details.anchor,
];
details.anchor = 0;
return [ FilterPatternGeneric.fid, details.pattern, anchor ];
return out;
}
static fromCompiled(args) {
@ -1107,20 +1112,22 @@ registerFilterClass(FilterTrailingSeparator);
/******************************************************************************/
const FilterRegex = class {
constructor(s) {
constructor(s, matchCase = false) {
this.s = s;
if ( matchCase ) {
this.matchCase = true;
}
}
match() {
if ( this.re === null ) {
this.re = FilterRegex.dict.get(this.s);
if ( this.re === undefined ) {
this.re = new RegExp(this.s, 'i');
FilterRegex.dict.set(this.s, this.re);
}
this.re = new RegExp(
this.s,
this.matchCase ? '' : 'i'
);
}
if ( this.re.test($requestURL) === false ) { return false; }
$patternMatchLeft = $requestURL.search(this.re);
if ( this.re.test($requestURLRaw) === false ) { return false; }
$patternMatchLeft = $requestURLRaw.search(this.re);
return true;
}
@ -1128,33 +1135,36 @@ const FilterRegex = class {
details.pattern.push('/', this.s, '/');
details.regex.push(this.s);
details.isRegex = true;
if ( this.matchCase ) {
details.options.push('match-case');
}
}
toSelfie() {
return [ this.fid, this.s ];
return [ this.fid, this.s, this.matchCase ];
}
static compile(details) {
return [ FilterRegex.fid, details.pattern ];
return [ FilterRegex.fid, details.pattern, details.patternMatchCase ];
}
static fromCompiled(args) {
return new FilterRegex(args[1]);
return new FilterRegex(args[1], args[2]);
}
static fromSelfie(args) {
return new FilterRegex(args[1]);
return new FilterRegex(args[1], args[2]);
}
static keyFromArgs(args) {
return args[1];
return `${args[1]}\t${args[2]}`;
}
};
FilterRegex.prototype.re = null;
FilterRegex.prototype.matchCase = false;
FilterRegex.isSlow = true;
FilterRegex.dict = new Map();
registerFilterClass(FilterRegex);
@ -2783,6 +2793,7 @@ const FilterParser = class {
this.modifyValue = undefined;
this.invalid = false;
this.pattern = '';
this.patternMatchCase = false;
this.party = AnyParty;
this.optionUnitBits = 0;
this.domainOpt = '';
@ -2944,6 +2955,9 @@ const FilterParser = class {
}
this.optionUnitBits |= this.REDIRECT_BIT;
break;
case this.parser.OPTTokenMatchCase:
this.patternMatchCase = true;
break;
case this.parser.OPTTokenMp4:
id = this.action === AllowAction
? this.parser.OPTTokenRedirectRule
@ -3833,6 +3847,7 @@ FilterContainer.prototype.matchAndFetchModifiers = function(
modifierType
) {
$requestURL = urlTokenizer.setURL(fctxt.url);
$requestURLRaw = fctxt.url;
$docHostname = fctxt.getDocHostname();
$docDomain = fctxt.getDocDomain();
$docEntity.reset();
@ -4126,6 +4141,7 @@ FilterContainer.prototype.matchStringReverse = function(type, url) {
// Prime tokenizer: we get a normalized URL in return.
$requestURL = urlTokenizer.setURL(url);
$requestURLRaw = url;
this.$filterUnit = 0;
// These registers will be used by various filters
@ -4172,6 +4188,7 @@ FilterContainer.prototype.matchString = function(fctxt, modifiers = 0) {
// Prime tokenizer: we get a normalized URL in return.
$requestURL = urlTokenizer.setURL(fctxt.url);
$requestURLRaw = fctxt.url;
this.$filterUnit = 0;
// These registers will be used by various filters
@ -4203,6 +4220,7 @@ FilterContainer.prototype.matchHeaders = function(fctxt, headers) {
// Prime tokenizer: we get a normalized URL in return.
$requestURL = urlTokenizer.setURL(fctxt.url);
$requestURLRaw = fctxt.url;
this.$filterUnit = 0;
// These registers will be used by various filters
@ -4239,13 +4257,9 @@ FilterContainer.prototype.redirectRequest = function(fctxt) {
const directive = directives[0];
if ( (directive.bits & AllowAction) !== 0 ) { return directive; }
const modifier = directive.modifier;
if ( modifier.cache === undefined ) {
modifier.cache = this.parseRedirectRequestValue(modifier.value);
}
fctxt.redirectURL = µb.redirectEngine.tokenToURL(
fctxt,
modifier.cache.token
);
const { token } = this.parseRedirectRequestValue(modifier);
fctxt.redirectURL = µb.redirectEngine.tokenToURL(fctxt, token);
if ( fctxt.redirectURL === undefined ) { return; }
return directive;
}
// Multiple directives mean more work to do.
@ -4258,15 +4272,11 @@ FilterContainer.prototype.redirectRequest = function(fctxt) {
winningDirective = directive;
break;
}
if ( modifier.cache === undefined ) {
modifier.cache = this.parseRedirectRequestValue(modifier.value);
}
if (
winningDirective === undefined ||
modifier.cache.priority > winningPriority
) {
const { token, priority } = this.parseRedirectRequestValue(modifier);
if ( µb.redirectEngine.hasToken(token) === false ) { continue; }
if ( winningDirective === undefined || priority > winningPriority ) {
winningDirective = directive;
winningPriority = modifier.cache.priority;
winningPriority = priority;
}
}
if ( winningDirective === undefined ) { return; }
@ -4279,15 +4289,18 @@ FilterContainer.prototype.redirectRequest = function(fctxt) {
return winningDirective;
};
FilterContainer.prototype.parseRedirectRequestValue = function(rawValue) {
let token = rawValue;
let priority = 0;
const match = /:(\d+)$/.exec(rawValue);
if ( match !== null ) {
token = rawValue.slice(0, match.index);
priority = parseInt(match[1], 10);
FilterContainer.prototype.parseRedirectRequestValue = function(modifier) {
if ( modifier.cache === undefined ) {
let token = modifier.value;
let priority = 1;
const match = /:(\d+)$/.exec(token);
if ( match !== null ) {
token = token.slice(0, match.index);
priority = parseInt(match[1], 10);
}
modifier.cache = { token, priority };
}
return { token, priority };
return modifier.cache;
};
/******************************************************************************/