Index: lib/matcher.js |
=================================================================== |
--- a/lib/matcher.js |
+++ b/lib/matcher.js |
@@ -32,26 +32,111 @@ |
/** |
* Regular expression for matching all keywords in a filter. |
* @type {RegExp} |
*/ |
const allKeywordsRegExp = new RegExp(keywordRegExp, "g"); |
/** |
+ * Bitmask for content types that are implied by default in a filter, like |
+ * <code>$script</code>, <code>$image</code>, <code>$stylesheet</code>, and so |
+ * on. |
+ * @type {number} |
+ */ |
+const DEFAULT_TYPES = RegExpFilter.prototype.contentType; |
+ |
+/** |
+ * Bitmask for "types" that must always be specified in a filter explicitly, |
+ * like <code>$csp</code>, <code>$popup</code>, <code>$elemhide</code>, and so |
+ * on. |
+ * @type {number} |
+ */ |
+const NON_DEFAULT_TYPES = ~DEFAULT_TYPES; |
+ |
+/** |
* Bitmask for "types" that are for exception rules only, like |
* <code>$document</code>, <code>$elemhide</code>, and so on. |
* @type {number} |
*/ |
const WHITELIST_ONLY_TYPES = RegExpFilter.typeMap.DOCUMENT | |
RegExpFilter.typeMap.ELEMHIDE | |
RegExpFilter.typeMap.GENERICHIDE | |
RegExpFilter.typeMap.GENERICBLOCK; |
/** |
+ * Yields individual non-default types from a filter's type mask. |
+ * @param {number} contentType A filter's type mask. |
+ * @yields {number} |
+ */ |
+function* nonDefaultTypes(contentType) |
Manish Jethani
2018/11/02 00:33:47
For a filter like `foo$script,image,popup`, this f
|
+{ |
+ for (let mask = contentType & NON_DEFAULT_TYPES, bitIndex = 0; |
+ mask != 0; mask >>>= 1, bitIndex++) |
+ { |
+ if ((mask & 1) != 0) |
+ { |
+ // Note: The zero-fill right shift by zero is necessary for dropping the |
+ // sign. |
+ yield 1 << bitIndex >>> 0; |
+ } |
+ } |
+} |
+ |
+/** |
+ * Adds a filter by a given keyword to a map. |
+ * @param {RegExpFilter} filter |
+ * @param {string} keyword |
+ * @param {Map.<string,(RegExpFilter|Set.<RegExpFilter>)>} map |
+ */ |
+function addFilterByKeyword(filter, keyword, map) |
+{ |
+ let set = map.get(keyword); |
Manish Jethani
2018/11/02 00:33:47
This has been copied and pasted from the add and r
|
+ if (typeof set == "undefined") |
+ { |
+ map.set(keyword, filter); |
+ } |
+ else if (set.size == 1) |
+ { |
+ if (filter != set) |
+ map.set(keyword, new Set([set, filter])); |
+ } |
+ else |
+ { |
+ set.add(filter); |
+ } |
+} |
+ |
+/** |
+ * Removes a filter by a given keyword from a map. |
+ * @param {RegExpFilter} filter |
+ * @param {string} keyword |
+ * @param {Map.<string,(RegExpFilter|Set.<RegExpFilter>)>} map |
+ */ |
+function removeFilterByKeyword(filter, keyword, map) |
+{ |
+ let set = map.get(keyword); |
+ if (typeof set == "undefined") |
+ return; |
+ |
+ if (set.size == 1) |
+ { |
+ if (filter == set) |
+ map.delete(keyword); |
+ } |
+ else |
+ { |
+ set.delete(filter); |
+ |
+ if (set.size == 1) |
+ map.set(keyword, [...set][0]); |
+ } |
+} |
+ |
+/** |
* Checks whether a particular filter is slow. |
* @param {RegExpFilter} filter |
* @returns {boolean} |
*/ |
function isSlowFilter(filter) |
{ |
return !filter.pattern || !keywordRegExp.test(filter.pattern); |
} |
@@ -73,79 +158,84 @@ |
this._simpleFiltersByKeyword = new Map(); |
/** |
* Lookup table for complex filters by their associated keyword |
* @type {Map.<string,(RegExpFilter|Set.<RegExpFilter>)>} |
* @private |
*/ |
this._complexFiltersByKeyword = new Map(); |
+ |
+ /** |
+ * Lookup table of type-specific lookup tables for complex filters by their |
+ * associated keyword |
+ * @type {Map.<string,Map.<string,(RegExpFilter|Set.<RegExpFilter>)>>} |
+ * @private |
+ */ |
+ this._filterMapsByType = new Map(); |
} |
/** |
* Removes all known filters |
*/ |
clear() |
{ |
this._simpleFiltersByKeyword.clear(); |
this._complexFiltersByKeyword.clear(); |
+ this._filterMapsByType.clear(); |
} |
/** |
* Adds a filter to the matcher |
* @param {RegExpFilter} filter |
*/ |
add(filter) |
{ |
- let filtersByKeyword = filter.isLocationOnly() ? |
- this._simpleFiltersByKeyword : |
- this._complexFiltersByKeyword; |
// Look for a suitable keyword |
let keyword = this.findKeyword(filter); |
- let set = filtersByKeyword.get(keyword); |
- if (typeof set == "undefined") |
- { |
- filtersByKeyword.set(keyword, filter); |
- } |
- else if (set.size == 1) |
+ let locationOnly = filter.isLocationOnly(); |
+ |
+ addFilterByKeyword(filter, keyword, |
+ locationOnly ? this._simpleFiltersByKeyword : |
+ this._complexFiltersByKeyword); |
+ |
+ if (locationOnly) |
+ return; |
+ |
+ for (let type of nonDefaultTypes(filter.contentType)) |
{ |
- if (filter != set) |
- filtersByKeyword.set(keyword, new Set([set, filter])); |
- } |
- else |
- { |
- set.add(filter); |
+ let map = this._filterMapsByType.get(type); |
+ if (!map) |
+ this._filterMapsByType.set(type, map = new Map()); |
+ |
+ addFilterByKeyword(filter, keyword, map); |
} |
} |
/** |
* Removes a filter from the matcher |
* @param {RegExpFilter} filter |
*/ |
remove(filter) |
{ |
- let filtersByKeyword = filter.isLocationOnly() ? |
- this._simpleFiltersByKeyword : |
- this._complexFiltersByKeyword; |
let keyword = this.findKeyword(filter); |
- let set = filtersByKeyword.get(keyword); |
- if (typeof set == "undefined") |
+ let locationOnly = filter.isLocationOnly(); |
+ |
+ removeFilterByKeyword(filter, keyword, |
+ locationOnly ? this._simpleFiltersByKeyword : |
+ this._complexFiltersByKeyword); |
+ |
+ if (locationOnly) |
return; |
- if (set.size == 1) |
+ for (let type of nonDefaultTypes(filter.contentType)) |
{ |
- if (filter == set) |
- filtersByKeyword.delete(keyword); |
- } |
- else |
- { |
- set.delete(filter); |
- |
- if (set.size == 1) |
- filtersByKeyword.set(keyword, [...set][0]); |
+ let map = this._filterMapsByType.get(type); |
+ if (map) |
+ removeFilterByKeyword(filter, keyword, map); |
} |
} |
/** |
* Chooses a keyword to be associated with the filter |
* @param {Filter} filter |
* @returns {string} keyword or an empty string if no keyword could be found |
* @protected |
@@ -195,33 +285,50 @@ |
* @returns {?Filter} |
* @protected |
*/ |
checkEntryMatch(keyword, location, typeMask, docDomain, thirdParty, sitekey, |
specificOnly) |
{ |
// We need to skip the simple (location-only) filters if the type mask does |
// not contain any default content types. |
- if ((typeMask & RegExpFilter.prototype.contentType) != 0) |
+ if ((typeMask & DEFAULT_TYPES) != 0) |
{ |
let simpleSet = this._simpleFiltersByKeyword.get(keyword); |
if (simpleSet) |
{ |
for (let filter of simpleSet) |
{ |
if (specificOnly && !(filter instanceof WhitelistFilter)) |
continue; |
if (filter.matchesLocation(location)) |
return filter; |
} |
} |
} |
- let complexSet = this._complexFiltersByKeyword.get(keyword); |
+ let complexSet = null; |
+ |
+ // If the type mask contains a non-default type (first condition) and it is |
+ // the only type in the mask (second condition), we can use the |
+ // type-specific map, which typically contains a lot fewer filters. This |
+ // enables faster lookups for whitelisting types like $document, $elemhide, |
+ // and so on, as well as other special types like $csp. |
+ if ((typeMask & NON_DEFAULT_TYPES) != 0 && (typeMask & typeMask - 1) == 0) |
Manish Jethani
2018/11/02 00:33:47
The second condition here basically checks if the
hub
2018/12/11 15:34:59
What throw me off here is the evaluation order of
|
+ { |
+ let map = this._filterMapsByType.get(typeMask); |
+ if (map) |
+ complexSet = map.get(keyword); |
+ } |
+ else |
+ { |
+ complexSet = this._complexFiltersByKeyword.get(keyword); |
+ } |
+ |
if (complexSet) |
{ |
for (let filter of complexSet) |
{ |
if (specificOnly && filter.isGeneric() && |
!(filter instanceof WhitelistFilter)) |
continue; |