/*******************************************************************************

    uBlock Origin - a browser extension to block requests.
    Copyright (C) 2014-2017 Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

/* jshint bitwise: false */
/* global punycode */

'use strict';

/******************************************************************************/

µBlock.staticNetFilteringEngine = (function(){

/******************************************************************************/

var µb = µBlock;

// fedcba9876543210
// |     |    | |||
// |     |    | |||
// |     |    | |||
// |     |    | |||
// |     |    | ||+---- bit    0: [BlockAction | AllowAction]
// |     |    | |+----- bit    1: `important`
// |     |    | +------ bit 2- 3: party [0 - 3]
// |     |    +-------- bit 4- 8: type [0 - 31]
// |     +------------- bit 9-14: unused
// +------------------- bit   15: bad filter

var BlockAction = 0 << 0;
var AllowAction = 1 << 0;
var Important   = 1 << 1;
var AnyParty    = 0 << 2;
var FirstParty  = 1 << 2;
var ThirdParty  = 2 << 2;
var BadFilter   = 1 << 15;

var AnyType = 0 << 4;
var typeNameToTypeValue = {
           'no_type':  0 << 4,
        'stylesheet':  1 << 4,
             'image':  2 << 4,
            'object':  3 << 4,
            'script':  4 << 4,
    'xmlhttprequest':  5 << 4,
         'sub_frame':  6 << 4,
              'font':  7 << 4,
             'media':  8 << 4,
         'websocket':  9 << 4,
             'other': 10 << 4,
             'popup': 11 << 4,  // start of behavorial filtering
          'popunder': 12 << 4,
        'main_frame': 13 << 4,  // start of 1st-party-only behavorial filtering
       'generichide': 14 << 4,
     'inline-script': 15 << 4,
              'data': 16 << 4,  // special: a generic data holder
          'redirect': 17 << 4,
            'webrtc': 18 << 4
};
var otherTypeBitValue = typeNameToTypeValue.other;

var typeValueToTypeName = {
     1: 'stylesheet',
     2: 'image',
     3: 'object',
     4: 'script',
     5: 'xmlhttprequest',
     6: 'subdocument',
     7: 'font',
     8: 'media',
     9: 'websocket',
    10: 'other',
    11: 'popup',
    12: 'popunder',
    13: 'document',
    14: 'generichide',
    15: 'inline-script',
    16: 'data',
    17: 'redirect',
    18: 'webrtc'
};

// All network request types to bitmap
//   bring origin to 0 (from 4 -- see typeNameToTypeValue)
//   left-shift 1 by the above-calculated value
//   subtract 1 to set all type bits
var allNetRequestTypesBitmap = (1 << (otherTypeBitValue >>> 4)) - 1;

var BlockAnyTypeAnyParty = BlockAction | AnyType | AnyParty;
var BlockAnyType = BlockAction | AnyType;
var BlockAnyParty = BlockAction | AnyParty;

var AllowAnyTypeAnyParty = AllowAction | AnyType | AnyParty;
var AllowAnyType = AllowAction | AnyType;
var AllowAnyParty = AllowAction | AnyParty;

var genericHideException = AllowAction | AnyParty | typeNameToTypeValue.generichide,
    genericHideImportant = BlockAction | AnyParty | typeNameToTypeValue.generichide | Important;

// ABP filters: https://adblockplus.org/en/filters
// regex tester: http://regex101.com/

/******************************************************************************/

// See the following as short-lived registers, used during evaluation. They are
// valid until the next evaluation.

var pageHostnameRegister = '',
    requestHostnameRegister = '';
//var filterRegister = null;
//var categoryRegister = '';

/******************************************************************************/

var histogram = function() {};
/*
histogram = function(label, categories) {
    var h = [],
        categoryBucket;
    for ( var k in categories ) {
        // No need for hasOwnProperty() here: there is no prototype chain.
        categoryBucket = categories[k];
        for ( var kk in categoryBucket ) {
            // No need for hasOwnProperty() here: there is no prototype chain.
            filterBucket = categoryBucket[kk];
            h.push({
                k: k.charCodeAt(0).toString(2) + ' ' + kk,
                n: filterBucket instanceof FilterBucket ? filterBucket.filters.length : 1
            });
        }
    }

    console.log('Histogram %s', label);

    var total = h.length;
    h.sort(function(a, b) { return b.n - a.n; });

    // Find indices of entries of interest
    var target = 2;
    for ( var i = 0; i < total; i++ ) {
        if ( h[i].n === target ) {
            console.log('\tEntries with only %d filter(s) start at index %s (key = "%s")', target, i, h[i].k);
            target -= 1;
        }
    }

    h = h.slice(0, 50);

    h.forEach(function(v) {
        console.log('\tkey=%s  count=%d', v.k, v.n);
    });
    console.log('\tTotal buckets count: %d', total);
};
*/
/******************************************************************************/

// Local helpers

// Be sure to not confuse 'example.com' with 'anotherexample.com'
var isFirstParty = function(domain, hostname) {
    return hostname.endsWith(domain) &&
          (hostname.length === domain.length ||
           hostname.charCodeAt(hostname.length - domain.length - 1) === 0x2E /* '.' */);
};

var normalizeRegexSource = function(s) {
    try {
        var re = new RegExp(s);
        return re.source;
    } catch (ex) {
        normalizeRegexSource.message = ex.toString();
    }
    return '';
};

var rawToRegexStr = function(s, anchor) {
    var me = rawToRegexStr;
    // https://www.loggly.com/blog/five-invaluable-techniques-to-improve-regex-performance/
    // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
    // Also: remove leading/trailing wildcards -- there is no point.
    var reStr = s.replace(me.escape1, '\\$&')
                 .replace(me.escape2, '(?:[^%.0-9a-z_-]|$)')
                 .replace(me.escape3, '')
                 .replace(me.escape4, '[^ ]*?');
    if ( anchor & 0x4 ) {
        reStr = '[0-9a-z.-]*?' + reStr;
    } else if ( anchor & 0x2 ) {
        reStr = '^' + reStr;
    }
    if ( anchor & 0x1 ) {
        reStr += '$';
    }
    return reStr;
};
rawToRegexStr.escape1 = /[.+?${}()|[\]\\]/g;
rawToRegexStr.escape2 = /\^/g;
rawToRegexStr.escape3 = /^\*|\*$/g;
rawToRegexStr.escape4 = /\*/g;

var filterFingerprinter = µb.CompiledLineWriter.fingerprint;

var toLogDataInternal = function(categoryBits, tokenHash, filter) {
    if ( filter === null ) { return undefined; }
    var logData = filter.logData();
    logData.compiled = filterFingerprinter([ categoryBits, tokenHash, logData.compiled ]);
    if ( categoryBits & 0x001 ) {
        logData.raw = '@@' + logData.raw;
    }
    var opts = [];
    if ( categoryBits & 0x002 ) {
        opts.push('important');
    }
    if ( categoryBits & 0x008 ) {
        opts.push('third-party');
    } else if ( categoryBits & 0x004 ) {
        opts.push('first-party');
    }
    var type = (categoryBits >>> 4) & 0x1F;
    if ( type !== 0 && type !== 16 /* data */ ) {
        opts.push(typeValueToTypeName[type]);
    }
    if ( logData.opts !== undefined ) {
        opts.push(logData.opts);
    }
    if ( opts.length !== 0 ) {
        logData.raw += '$' + opts.join(',');
    }
    return logData;
};

// First character of match must be within the hostname part of the url.
var isHnAnchored = function(url, matchStart) {
    var hnStart = url.indexOf('://');
    if ( hnStart === -1 ) { return false; }
    hnStart += 3;
    if ( matchStart <= hnStart ) { return true; }
    if ( reURLPostHostnameAnchors.test(url.slice(hnStart, matchStart)) ) {
        return false;
    }
    // https://github.com/gorhill/uBlock/issues/1929
    // Match only hostname label boundaries.
    return url.charCodeAt(matchStart - 1) === 0x2E;
};

var reURLPostHostnameAnchors = /[\/?#]/;

var arrayStrictEquals = function(a, b) {
    var n = a.length;
    if ( n !== b.length ) { return false; }
    var isArray, x, y;
    for ( var i = 0; i < n; i++ ) {
        x = a[i]; y = b[i];
        isArray = Array.isArray(x);
        if ( isArray !== Array.isArray(y) ) { return false; }
        if ( isArray === true ) {
            if ( arrayStrictEquals(x, y) === false ) { return false; }
        } else {
            if ( x !== y ) { return false; }
        }
    }
    return true;
};

/*******************************************************************************

    Each filter class will register itself in the map. A filter class
    id MUST always stringify to ONE single character.

    IMPORTANT: any change which modifies the mapping will have to be
    reflected with µBlock.systemSettings.compiledMagic.

**/

var filterClasses = [],
    filterClassIdGenerator = 0;

var registerFilterClass = function(ctor) {
    var fid = filterClassIdGenerator++;
    ctor.fid = ctor.prototype.fid = fid;
    filterClasses[fid] = ctor;
    //console.log(ctor.name, fid);
};

var filterFromCompiledData = function(args) {
    //filterClassHistogram.set(fid, (filterClassHistogram.get(fid) || 0) + 1);
    return filterClasses[args[0]].load(args);
};

//var filterClassHistogram = new Map();

/******************************************************************************/

var FilterTrue = function() {
};

FilterTrue.prototype.match = function() {
    return true;
};

FilterTrue.prototype.logData = function() {
    return {
        raw: '*',
        regex: '^',
        compiled: this.compile(),
    };
};

FilterTrue.prototype.compile = function() {
    return [ this.fid ];
};

FilterTrue.compile = function() {
    return [ FilterTrue.fid ];
};

FilterTrue.load = function() {
    return new FilterTrue();
};

registerFilterClass(FilterTrue);

/******************************************************************************/

var FilterPlain = function(s, tokenBeg) {
    this.s = s;
    this.tokenBeg = tokenBeg;
};

FilterPlain.prototype.match = function(url, tokenBeg) {
    return url.startsWith(this.s, tokenBeg - this.tokenBeg);
};

FilterPlain.prototype.logData = function() {
    return {
        raw: this.s,
        regex: rawToRegexStr(this.s),
        compiled: this.compile()
    };
};

FilterPlain.prototype.compile = function() {
    return [ this.fid, this.s, this.tokenBeg ];
};

FilterPlain.compile = function(details) {
    return [ FilterPlain.fid, details.f, details.tokenBeg ];
};

FilterPlain.load = function(args) {
    return new FilterPlain(args[1], args[2]);
};

registerFilterClass(FilterPlain);

/******************************************************************************/

var FilterPlainPrefix0 = function(s) {
    this.s = s;
};

FilterPlainPrefix0.prototype.match = function(url, tokenBeg) {
    return url.startsWith(this.s, tokenBeg);
};

FilterPlainPrefix0.prototype.logData = function() {
    return {
        raw: this.s,
        regex: rawToRegexStr(this.s),
        compiled: this.compile()
    };
};

FilterPlainPrefix0.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterPlainPrefix0.compile = function(details) {
    return [ FilterPlainPrefix0.fid, details.f ];
};

FilterPlainPrefix0.load = function(args) {
    return new FilterPlainPrefix0(args[1]);
};

registerFilterClass(FilterPlainPrefix0);

/******************************************************************************/

var FilterPlainPrefix1 = function(s) {
    this.s = s;
};

FilterPlainPrefix1.prototype.match = function(url, tokenBeg) {
    return url.startsWith(this.s, tokenBeg - 1);
};

FilterPlainPrefix1.prototype.logData = function() {
    return {
        raw: this.s,
        regex: rawToRegexStr(this.s),
        compiled: this.compile()
    };
};

FilterPlainPrefix1.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterPlainPrefix1.compile = function(details) {
    return [ FilterPlainPrefix1.fid, details.f ];
};

FilterPlainPrefix1.load = function(args) {
    return new FilterPlainPrefix1(args[1]);
};

registerFilterClass(FilterPlainPrefix1);

/******************************************************************************/

var FilterPlainHostname = function(s) {
    this.s = s;
};

FilterPlainHostname.prototype.match = function() {
    var haystack = requestHostnameRegister, needle = this.s;
    if ( haystack.endsWith(needle) === false ) { return false; }
    var offset = haystack.length - needle.length;
    return offset === 0 || haystack.charCodeAt(offset - 1) === 0x2E /* '.' */;
};

FilterPlainHostname.prototype.logData = function() {
    return {
        raw: '||' + this.s + '^',
        regex: rawToRegexStr(this.s, 0x4),
        compiled: this.compile()
    };
};

FilterPlainHostname.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterPlainHostname.compile = function(details) {
    return [ FilterPlainHostname.fid, details.f ];
};

FilterPlainHostname.load = function(args) {
    return new FilterPlainHostname(args[1]);
};

registerFilterClass(FilterPlainHostname);

/******************************************************************************/

var FilterPlainLeftAnchored = function(s) {
    this.s = s;
};

FilterPlainLeftAnchored.prototype.match = function(url) {
    return url.startsWith(this.s);
};

FilterPlainLeftAnchored.prototype.logData = function() {
    return {
        raw: '|' + this.s,
        regex: rawToRegexStr(this.s, 0x2),
        compiled: this.compile()
    };
};

FilterPlainLeftAnchored.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterPlainLeftAnchored.compile = function(details) {
    return [ FilterPlainLeftAnchored.fid, details.f ];
};

FilterPlainLeftAnchored.load = function(args) {
    return new FilterPlainLeftAnchored(args[1]);
};

registerFilterClass(FilterPlainLeftAnchored);

/******************************************************************************/

var FilterPlainRightAnchored = function(s) {
    this.s = s;
};

FilterPlainRightAnchored.prototype.match = function(url) {
    return url.endsWith(this.s);
};

FilterPlainRightAnchored.prototype.logData = function() {
    return {
        raw: this.s + '|',
        regex: rawToRegexStr(this.s, 0x1),
        compiled: this.compile()
    };
};

FilterPlainRightAnchored.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterPlainRightAnchored.compile = function(details) {
    return [ FilterPlainRightAnchored.fid, details.f ];
};

FilterPlainRightAnchored.load = function(args) {
    return new FilterPlainRightAnchored(args[1]);
};

registerFilterClass(FilterPlainRightAnchored);

/******************************************************************************/

var FilterPlainHnAnchored = function(s) {
    this.s = s;
};

FilterPlainHnAnchored.prototype.match = function(url, tokenBeg) {
    return url.startsWith(this.s, tokenBeg) &&
           isHnAnchored(url, tokenBeg);
};

FilterPlainHnAnchored.prototype.logData = function() {
    return {
        raw: '||' + this.s,
        regex: rawToRegexStr(this.s),
        compiled: this.compile()
    };
};

FilterPlainHnAnchored.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterPlainHnAnchored.compile = function(details) {
    return [ FilterPlainHnAnchored.fid, details.f ];
};

FilterPlainHnAnchored.load = function(args) {
    return new FilterPlainHnAnchored(args[1]);
};

registerFilterClass(FilterPlainHnAnchored);

/******************************************************************************/

var FilterGeneric = function(s, anchor) {
    this.s = s;
    this.anchor = anchor;
};

FilterGeneric.prototype.re = null;

FilterGeneric.prototype.match = function(url) {
    if ( this.re === null ) {
        this.re = new RegExp(rawToRegexStr(this.s, this.anchor));
    }
    return this.re.test(url);
};

FilterGeneric.prototype.logData = function() {
    var out = {
        raw: this.s,
        regex: this.re.source,
        compiled: this.compile()
    };
    if ( this.anchor & 0x2 ) {
        out.raw = '|' + out.raw;
    }
    if ( this.anchor & 0x1 ) {
        out.raw += '|';
    }
    return out;
};

FilterGeneric.prototype.compile = function() {
    return [ this.fid, this.s, this.anchor ];
};

FilterGeneric.compile = function(details) {
    return [ FilterGeneric.fid, details.f, details.anchor ];
};

FilterGeneric.load = function(args) {
    return new FilterGeneric(args[1], args[2]);
};

registerFilterClass(FilterGeneric);

/******************************************************************************/

var FilterGenericHnAnchored = function(s) {
    this.s = s;
};

FilterGenericHnAnchored.prototype.re = null;
FilterGenericHnAnchored.prototype.anchor = 0x4;

FilterGenericHnAnchored.prototype.match = function(url) {
    if ( this.re === null ) {
        this.re = new RegExp(rawToRegexStr(this.s, this.anchor));
    }
    var matchStart = url.search(this.re);
    return matchStart !== -1 && isHnAnchored(url, matchStart);
};

FilterGenericHnAnchored.prototype.logData = function() {
    var out = {
        raw: '||' + this.s,
        regex: this.re.source,
        compiled: this.compile()
    };
    return out;
};

FilterGenericHnAnchored.prototype.compile = function() {
    return [ this.fid, this.s ];
};

FilterGenericHnAnchored.compile = function(details) {
    return [ FilterGenericHnAnchored.fid, details.f ];
};

FilterGenericHnAnchored.load = function(args) {
    return new FilterGenericHnAnchored(args[1]);
};

registerFilterClass(FilterGenericHnAnchored);

/******************************************************************************/

var FilterGenericHnAndRightAnchored = function(s) {
    FilterGenericHnAnchored.call(this, s);
};

FilterGenericHnAndRightAnchored.prototype = Object.create(
    FilterGenericHnAnchored.prototype,
    {
        constructor: {
            value: FilterGenericHnAndRightAnchored
        },
        anchor: {
            value: 0x5
        },
        logData: {
            value: function() {
                var out = FilterGenericHnAnchored.prototype.logData.call(this);
                out.raw += '|';
                return out;
            }
        },
        compile: {
            value: function() {
                return [ this.fid, this.s ];
            }
        }
    }
);

FilterGenericHnAndRightAnchored.compile = function(details) {
    return [ FilterGenericHnAndRightAnchored.fid, details.f ];
};

FilterGenericHnAndRightAnchored.load = function(args) {
    return new FilterGenericHnAndRightAnchored(args[1]);
};

registerFilterClass(FilterGenericHnAndRightAnchored);

/******************************************************************************/

var FilterRegex = function(s) {
    this.re = s;
};

FilterRegex.prototype.match = function(url) {
    if ( typeof this.re === 'string' ) {
        this.re = new RegExp(this.re, 'i');
    }
    return this.re.test(url);
};

FilterRegex.prototype.logData = function() {
    var s = typeof this.re === 'string' ? this.re : this.re.source;
    return {
        raw: '/' + s + '/',
        regex: s,
        compiled: this.compile()
    };
};

FilterRegex.prototype.compile = function() {
    return [
        this.fid,
        typeof this.re === 'string' ? this.re : this.re.source
    ];
};

FilterRegex.compile = function(details) {
    return [ FilterRegex.fid, details.f ];
};

FilterRegex.load = function(args) {
    return new FilterRegex(args[1]);
};

registerFilterClass(FilterRegex);

/******************************************************************************/

// Filtering according to the origin.

var FilterOrigin = function() {
};

FilterOrigin.prototype.wrapped = {
    compile: function() {
        return '';
    },
    logData: function() {
        return {
            compiled: ''
        };
    },
    match: function() {
        return true;
    }
};

FilterOrigin.prototype.matchOrigin = function() {
    return true;
};

FilterOrigin.prototype.match = function(url, tokenBeg) {
    return this.matchOrigin() && this.wrapped.match(url, tokenBeg);
};

FilterOrigin.prototype.logData = function() {
    var out = this.wrapped.logData(),
        domainOpt = this.toDomainOpt();
    out.compiled = [ this.fid, domainOpt, out.compiled ];
    if ( out.opts === undefined ) {
        out.opts = 'domain=' + domainOpt;
    } else {
        out.opts += ',domain=' + domainOpt;
    }
    return out;
};

FilterOrigin.prototype.compile = function() {
    return [ this.fid, this.toDomainOpt(), this.wrapped.compile() ];
};

// *** start of specialized origin matchers

var FilterOriginHit = function(domainOpt) {
    FilterOrigin.call(this);
    this.hostname = domainOpt;
};

FilterOriginHit.prototype = Object.create(FilterOrigin.prototype, {
    constructor: {
        value: FilterOriginHit
    },
    toDomainOpt: {
        value: function() {
            return this.hostname;
        }
    },
    matchOrigin: {
        value: function() {
            var needle = this.hostname, haystack = pageHostnameRegister;
            if ( haystack.endsWith(needle) === false ) { return false; }
            var offset = haystack.length - needle.length;
            return offset === 0 || haystack.charCodeAt(offset - 1) === 0x2E /* '.' */;
        }
    },
});

//

var FilterOriginMiss = function(domainOpt) {
    FilterOrigin.call(this);
    this.hostname = domainOpt.slice(1);
};

FilterOriginMiss.prototype = Object.create(FilterOrigin.prototype, {
    constructor: {
        value: FilterOriginMiss
    },
    toDomainOpt: {
        value: function() {
            return '~' + this.hostname;
        }
    },
    matchOrigin: {
        value: function() {
            var needle = this.hostname, haystack = pageHostnameRegister;
            if ( haystack.endsWith(needle) === false ) { return true; }
            var offset = haystack.length - needle.length;
            return offset !== 0 && haystack.charCodeAt(offset - 1) !== 0x2E /* '.' */;
        }
    },
});

//

var FilterOriginHitSet = function(domainOpt) {
    FilterOrigin.call(this);
    this.domainOpt = domainOpt.length < 128
        ? domainOpt
        : µb.stringDeduplicater.lookup(domainOpt);
};

FilterOriginHitSet.prototype = Object.create(FilterOrigin.prototype, {
    constructor: {
        value: FilterOriginHitSet
    },
    oneOf: {
        value: null,
        writable: true
    },
    toDomainOpt: {
        value: function() {
            return this.domainOpt;
        }
    },
    matchOrigin: {
        value: function() {
            if ( this.oneOf === null ) {
                this.oneOf = new RegExp('(?:^|\\.)(?:' + this.domainOpt.replace(/\./g, '\\.') + ')$');
            }
            return this.oneOf.test(pageHostnameRegister);
        }
    },
});

//

var FilterOriginMissSet = function(domainOpt) {
    FilterOrigin.call(this);
    this.domainOpt = domainOpt.length < 128
        ? domainOpt
        : µb.stringDeduplicater.lookup(domainOpt);
};

FilterOriginMissSet.prototype = Object.create(FilterOrigin.prototype, {
    constructor: {
        value: FilterOriginMissSet
    },
    noneOf: {
        value: null,
        writable: true
    },
    toDomainOpt: {
        value: function() {
            return this.domainOpt;
        }
    },
    matchOrigin: {
        value: function() {
            if ( this.noneOf === null ) {
                this.noneOf = new RegExp('(?:^|\\.)(?:' + this.domainOpt.replace(/~/g, '').replace(/\./g, '\\.') + ')$');
            }
            return this.noneOf.test(pageHostnameRegister) === false;
        }
    },
});

//

var FilterOriginMixedSet = function(domainOpt) {
    FilterOrigin.call(this);
    this.domainOpt = domainOpt.length < 128
        ? domainOpt
        : µb.stringDeduplicater.lookup(domainOpt);
};

FilterOriginMixedSet.prototype = Object.create(FilterOrigin.prototype, {
    constructor: {
        value: FilterOriginMixedSet
    },
    oneOf: {
        value: null,
        writable: true
    },
    noneOf: {
        value: null,
        writable: true
    },
    init: {
        value: function() {
            var oneOf = [], noneOf = [],
                hostnames = this.domainOpt.split('|'),
                i = hostnames.length,
                hostname;
            while ( i-- ) {
                hostname = hostnames[i].replace(/\./g, '\\.');
                if ( hostname.charCodeAt(0) === 0x7E /* '~' */ ) {
                    noneOf.push(hostname.slice(1));
                } else {
                    oneOf.push(hostname);
                }
            }
            this.oneOf = new RegExp('(?:^|\\.)(?:' + oneOf.join('|') + ')$');
            this.noneOf = new RegExp('(?:^|\\.)(?:' + noneOf.join('|') + ')$');
        }
    },
    toDomainOpt: {
        value: function() {
            return this.domainOpt;
        }
    },
    matchOrigin: {
        value: function() {
            if ( this.oneOf === null ) { this.init(); }
            var needle = pageHostnameRegister;
            return this.oneOf.test(needle) && this.noneOf.test(needle) === false;
        }
    },
});

// *** end of specialized origin matchers

// The optimal test function is picked according to the content of the
// `domain=` filter option.
// Re-factored in light of:
// - https://gorhill.github.io/obj-vs-set-vs-map/set-vs-regexp.html
// The re-factoring made possible to reuse instances of a matcher. As of
// writing, I observed that just with EasyList, there were ~1,200 reused
// instances out of ~2,800.

FilterOrigin.matcherFactory = function(domainOpt) {
    // One hostname
    if ( domainOpt.indexOf('|') === -1 ) {
        if ( domainOpt.charCodeAt(0) === 0x7E /* '~' */ ) {
            return new FilterOriginMiss(domainOpt);
        }
        return new FilterOriginHit(domainOpt);
    }
    // Many hostnames.
    // Must be in set (none negated).
    if ( domainOpt.indexOf('~') === -1 ) {
        return new FilterOriginHitSet(domainOpt);
    }
    // Must not be in set (all negated).
    if ( FilterOrigin.reAllNegated.test(domainOpt) ) {
        return new FilterOriginMissSet(domainOpt);
    }
    // Must be in one set, but not in the other.
    return new FilterOriginMixedSet(domainOpt);
};

FilterOrigin.reAllNegated = /^~(?:[^|~]+\|~)+[^|~]+$/;

FilterOrigin.compile = function(details) {
    return [ FilterOrigin.fid, details.domainOpt ];
};

FilterOrigin.load = function(args) {
    var f = FilterOrigin.matcherFactory(args[1]);
    f.wrapped = filterFromCompiledData(args[2]);
    return f;
};

registerFilterClass(FilterOrigin);

/******************************************************************************/

var FilterDataHolder = function(dataType, dataStr) {
    this.dataType = dataType;
    this.dataStr = dataStr;
    this.wrapped = undefined;
};

FilterDataHolder.prototype.match = function(url, tokenBeg) {
    return this.wrapped.match(url, tokenBeg);
};

FilterDataHolder.prototype.logData = function() {
    var out = this.wrapped.logData();
    out.compiled = [ this.fid, this.dataType, this.dataStr, out.compiled ];
    var opt = this.dataType;
    if ( this.dataStr !== '' ) {
        opt += '=' + this.dataStr;
    }
    if ( out.opts === undefined ) {
        out.opts = opt;
    } else {
        out.opts = opt + ',' + out.opts;
    }
    return out;
};

FilterDataHolder.prototype.compile = function() {
    return [ this.fid, this.dataType, this.dataStr, this.wrapped.compile() ];
};

FilterDataHolder.compile = function(details) {
    return [ FilterDataHolder.fid, details.dataType, details.dataStr ];
};

FilterDataHolder.load = function(args) {
    var f = new FilterDataHolder(args[1], args[2]);
    f.wrapped = filterFromCompiledData(args[3]);
    return f;
};

registerFilterClass(FilterDataHolder);

// Helper class for storing instances of FilterDataHolder.

var FilterDataHolderEntry = function(categoryBits, tokenHash, fdata) {
    this.categoryBits = categoryBits;
    this.tokenHash = tokenHash;
    this.filter = filterFromCompiledData(fdata);
    this.next = undefined;
};

FilterDataHolderEntry.prototype.logData = function() {
    return toLogDataInternal(this.categoryBits, this.tokenHash, this.filter);
};

FilterDataHolderEntry.prototype.compile = function() {
    return [ this.categoryBits, this.tokenHash, this.filter.compile() ];
};

FilterDataHolderEntry.load = function(data) {
    return new FilterDataHolderEntry(data[0], data[1], data[2]);
};

/******************************************************************************/

// Dictionary of hostnames
//
var FilterHostnameDict = function() {
    this.h = ''; // short-lived register
    this.dict = new Set();
};

Object.defineProperty(FilterHostnameDict.prototype, 'size', {
    get: function() {
        return this.dict.size;
    }
});

FilterHostnameDict.prototype.add = function(hn) {
    if ( this.dict.has(hn) === true ) { return false; }
    this.dict.add(hn);
    return true;
};

FilterHostnameDict.prototype.remove = function(hn) {
    return this.dict.delete(hn);
};

FilterHostnameDict.prototype.match = function() {
    // TODO: mind IP addresses
    var pos,
        hostname = requestHostnameRegister;
    while ( this.dict.has(hostname) === false ) {
        pos = hostname.indexOf('.');
        if ( pos === -1 ) {
            this.h = '';
            return false;
        }
        hostname = hostname.slice(pos + 1);
    }
    this.h = hostname;
    return true;
};

FilterHostnameDict.prototype.logData = function() {
    return {
        raw: '||' + this.h + '^',
        regex: rawToRegexStr(this.h) + '(?:[^%.0-9a-z_-]|$)',
        compiled: this.h
    };
};

FilterHostnameDict.prototype.compile = function() {
    return [ this.fid, µb.setToArray(this.dict) ];
};

FilterHostnameDict.load = function(args) {
    var f = new FilterHostnameDict();
    f.dict = µb.setFromArray(args[1]);
    return f;
};

registerFilterClass(FilterHostnameDict);

/******************************************************************************/

// Some buckets can grow quite large, and finding a hit in these buckets
// may end up being expensive. After considering various solutions, the one
// retained is to promote hit filters to a smaller index, so that next time
// they can be looked-up faster.

// key=  10000 ad           count=660
// key=  10000 ads          count=433
// key=  10001 google       count=277
// key=1000000 2mdn         count=267
// key=  10000 social       count=240
// key=  10001 pagead2      count=166
// key=  10000 twitter      count=122
// key=  10000 doubleclick  count=118
// key=  10000 facebook     count=114
// key=  10000 share        count=113
// key=  10000 google       count=106
// key=  10001 code         count=103
// key=  11000 doubleclick  count=100
// key=1010001 g            count=100
// key=  10001 js           count= 89
// key=  10000 adv          count= 88
// key=  10000 youtube      count= 61
// key=  10000 plugins      count= 60
// key=  10001 partner      count= 59
// key=  10000 ico          count= 57
// key= 110001 ssl          count= 57
// key=  10000 banner       count= 53
// key=  10000 footer       count= 51
// key=  10000 rss          count= 51

/******************************************************************************/

var FilterPair = function(a, b) {
    this.f1 = a;
    this.f2 = b;
    this.f = null;
};

Object.defineProperty(FilterPair.prototype, 'size', {
    get: function() {
        if ( this.f1 === undefined && this.f2 === undefined ) { return 0; }
        if ( this.f1 === undefined || this.f2 === undefined ) { return 1; }
        return 2;
    }
});

FilterPair.prototype.remove = function(fdata) {
    if ( arrayStrictEquals(this.f2.compile(), fdata) === true ) {
        this.f2 = undefined;
    }
    if ( arrayStrictEquals(this.f1.compile(), fdata) === true ) {
        this.f1 = this.f2;
    }
};

FilterPair.prototype.match = function(url, tokenBeg) {
    if ( this.f1.match(url, tokenBeg) === true ) {
        this.f = this.f1;
        return true;
    }
    if ( this.f2.match(url, tokenBeg) === true ) {
        this.f = this.f2;
        return true;
    }
    return false;
};

FilterPair.prototype.logData = function() {
    return this.f.logData();
};

FilterPair.prototype.compile = function() {
    return [ this.fid, this.f1.compile(), this.f2.compile() ];
};

FilterPair.prototype.upgrade = function(a) {
    var bucket = new FilterBucket(this.f1, this.f2, a);
    this.f1 = this.f2 = this.f = null;
    FilterPair.available = this;
    return bucket;
};

FilterPair.load = function(args) {
    var f1 = filterFromCompiledData(args[1]),
        f2 = filterFromCompiledData(args[2]),
        pair = FilterPair.available;
    if ( pair === null ) {
        return new FilterPair(f1, f2);
    }
    FilterPair.available = null;
    pair.f1 = f1;
    pair.f2 = f2;
    return pair;
};

FilterPair.available = null;

registerFilterClass(FilterPair);

/******************************************************************************/

var FilterBucket = function(a, b, c) {
    this.filters = [];
    this.f = null;
    if ( a !== undefined ) {
        this.filters[0] = a;
        this.filters[1] = b;
        this.filters[2] = c;
    }
};

Object.defineProperty(FilterBucket.prototype, 'size', {
    get: function() {
        return this.filters.length;
    }
});

FilterBucket.prototype.promoted = 0;

FilterBucket.prototype.add = function(fdata) {
    this.filters[this.filters.length] = filterFromCompiledData(fdata);
};

FilterBucket.prototype.remove = function(fdata) {
    var i = this.filters.length,
        filter;
    while ( i-- ) {
        filter = this.filters[i];
        if ( arrayStrictEquals(filter.compile(), fdata) === true ) {
            this.filters.splice(i, 1);
        }
    }
};

// Promote hit filters so they can be found faster next time.
FilterBucket.prototype.promote = function(i) {
    var filters = this.filters,
        pivot = filters.length >>> 1;
    while ( i < pivot ) {
        pivot >>>= 1;
        if ( pivot < 16 ) { break; }
    }
    if ( i <= pivot ) { return; }
    var j = this.promoted % pivot;
    //console.debug('FilterBucket.promote(): promoted %d to %d', i, j);
    var f = filters[j];
    filters[j] = filters[i];
    filters[i] = f;
    this.promoted += 1;
};

FilterBucket.prototype.match = function(url, tokenBeg) {
    var filters = this.filters;
    for ( var i = 0, n = filters.length; i < n; i++ ) {
        if ( filters[i].match(url, tokenBeg) === true ) {
            this.f = filters[i];
            if ( i >= 16 ) { this.promote(i); }
            return true;
        }
    }
    return false;
};

FilterBucket.prototype.logData = function() {
    return this.f.logData();
};

FilterBucket.prototype.compile = function() {
    var compiled = [],
        filters = this.filters;
    for ( var i = 0, n = filters.length; i < n; i++ ) {
        compiled[i] = filters[i].compile();
    }
    return [ this.fid, compiled ];
};

FilterBucket.prototype.downgrade = function() {
    return new FilterPair(this.filters[0], this.filters[1]);
};

FilterBucket.load = function(args) {
    var bucket = new FilterBucket(),
        compiledFilters = args[1],
        filters = bucket.filters;
    for ( var i = 0, n = compiledFilters.length; i < n; i++ ) {
        filters[i] = filterFromCompiledData(compiledFilters[i]);
    }
    return bucket;
};

registerFilterClass(FilterBucket);

/******************************************************************************/
/******************************************************************************/

var FilterParser = function() {
    this.cantWebsocket = vAPI.cantWebsocket;
    this.reBadDomainOptChars = /[*+?^${}()[\]\\]/;
    this.reHostnameRule1 = /^[0-9a-z][0-9a-z.-]*[0-9a-z]$/i;
    this.reHostnameRule2 = /^[0-9a-z][0-9a-z.-]*[0-9a-z]\^?$/i;
    this.reCleanupHostnameRule2 = /\^$/g;
    this.reCanTrimCarets1 = /^[^*]*$/;
    this.reCanTrimCarets2 = /^\^?[^^]+[^^][^^]+\^?$/;
    this.reHasUppercase = /[A-Z]/;
    this.reIsolateHostname = /^(\*?\.)?([^\x00-\x24\x26-\x2C\x2F\x3A-\x5E\x60\x7B-\x7F]+)(.*)/;
    this.reHasUnicode = /[^\x00-\x7F]/;
    this.reWebsocketAny = /^ws[s*]?(?::\/?\/?)?\*?$/;
    this.reBadCSP = /(?:^|;)\s*report-(?:to|uri)\b/;
    this.domainOpt = '';
    this.noTokenHash = µb.urlTokenizer.tokenHashFromString('*');
    this.reset();
};

/******************************************************************************/

// https://github.com/gorhill/uBlock/issues/1493
//   Transpose `ping` into `other` for now.

FilterParser.prototype.toNormalizedType = {
            'beacon': 'other',
              'data': 'data',
          'document': 'main_frame',
          'elemhide': 'generichide',
              'font': 'font',
       'generichide': 'generichide',
             'image': 'image',
     'inline-script': 'inline-script',
             'media': 'media',
            'object': 'object',
             'other': 'other',
 'object-subrequest': 'object',
              'ping': 'other',
          'popunder': 'popunder',
             'popup': 'popup',
            'script': 'script',
        'stylesheet': 'stylesheet',
       'subdocument': 'sub_frame',
    'xmlhttprequest': 'xmlhttprequest',
            'webrtc': 'webrtc',
         'websocket': 'websocket'
};

/******************************************************************************/

FilterParser.prototype.reset = function() {
    this.action = BlockAction;
    this.anchor = 0;
    this.badFilter = 0;
    this.dataType = undefined;
    this.dataStr = undefined;
    this.elemHiding = false;
    this.f = '';
    this.firstParty = false;
    this.thirdParty = false;
    this.party = AnyParty;
    this.fopts = '';
    this.hostnamePure = false;
    this.domainOpt = '';
    this.isRegex = false;
    this.raw = '';
    this.redirect = false;
    this.token = '*';
    this.tokenHash = this.noTokenHash;
    this.tokenBeg = 0;
    this.types = 0;
    this.important = 0;
    this.unsupported = false;
    return this;
};

/******************************************************************************/

FilterParser.prototype.bitFromType = function(type) {
    return 1 << ((typeNameToTypeValue[type] >>> 4) - 1);
};

/******************************************************************************/

// https://github.com/chrisaljoudi/uBlock/issues/589
// Be ready to handle multiple negated types

FilterParser.prototype.parseTypeOption = function(raw, not) {
    var typeBit = this.bitFromType(this.toNormalizedType[raw]);

    if ( !not ) {
        this.types |= typeBit;
        return;
    }

    // Non-discrete network types can't be negated.
    if ( (typeBit & allNetRequestTypesBitmap) === 0 ) {
        return;
    }

    // Negated type: set all valid network request type bits to 1
    if (
        (typeBit & allNetRequestTypesBitmap) !== 0 &&
        (this.types & allNetRequestTypesBitmap) === 0
    ) {
        this.types |= allNetRequestTypesBitmap;
    }
    this.types &= ~typeBit;
};

/******************************************************************************/

FilterParser.prototype.parsePartyOption = function(firstParty, not) {
    if ( firstParty ) {
        not = !not;
    }
    if ( not ) {
        this.firstParty = true;
        this.party = this.thirdParty ? AnyParty : FirstParty;
    } else {
        this.thirdParty = true;
        this.party = this.firstParty ? AnyParty : ThirdParty;
    }
};

/******************************************************************************/

FilterParser.prototype.parseDomainOption = function(s) {
    if ( this.reHasUnicode.test(s) ) {
        var hostnames = s.split('|'),
            i = hostnames.length;
        while ( i-- ) {
            if ( this.reHasUnicode.test(hostnames[i]) ) {
                hostnames[i] = punycode.toASCII(hostnames[i]);
            }
        }
        s = hostnames.join('|');
    }
    if ( this.reBadDomainOptChars.test(s) ) {
        return '';
    }
    return s;
};

/******************************************************************************/

FilterParser.prototype.parseOptions = function(s) {
    this.fopts = s;
    var opts = s.split(',');
    var opt, not;
    for ( var i = 0; i < opts.length; i++ ) {
        opt = opts[i];
        not = opt.startsWith('~');
        if ( not ) {
            opt = opt.slice(1);
        }
        if ( opt === 'third-party' ) {
            this.parsePartyOption(false, not);
            continue;
        }
        // https://issues.adblockplus.org/ticket/616
        // `generichide` concept already supported, just a matter of
        // adding support for the new keyword.
        if ( opt === 'elemhide' || opt === 'generichide' ) {
            if ( not === false ) {
                this.parseTypeOption('generichide', false);
                continue;
            }
            this.unsupported = true;
            break;
        }
        // Test before handling all other types.
        if ( opt.startsWith('redirect=') ) {
            if ( this.action === BlockAction ) {
                this.redirect = true;
                continue;
            }
            this.unsupported = true;
            break;
        }
        if ( this.toNormalizedType.hasOwnProperty(opt) ) {
            this.parseTypeOption(opt, not);
            continue;
        }
        // https://github.com/gorhill/uBlock/issues/2294
        // Detect and discard filter if domain option contains nonsensical
        // characters.
        if ( opt.startsWith('domain=') ) {
            this.domainOpt = this.parseDomainOption(opt.slice(7));
            if ( this.domainOpt === '' ) {
                this.unsupported = true;
                break;
            }
            continue;
        }
        if ( opt === 'important' ) {
            this.important = Important;
            continue;
        }
        if ( opt === 'first-party' ) {
            this.parsePartyOption(true, not);
            continue;
        }
        if ( opt.startsWith('csp=') ) {
            if ( opt.length > 4 && this.reBadCSP.test(opt) === false ) {
                this.parseTypeOption('data', not);
                this.dataType = 'csp';
                this.dataStr = opt.slice(4).trim();
            }
            continue;
        }
        if ( opt === 'csp' && this.action === AllowAction ) {
            this.parseTypeOption('data', not);
            this.dataType = 'csp';
            this.dataStr = '';
            continue;
        }
        // Used by Adguard, purpose is unclear -- just ignore for now.
        if ( opt === 'empty' ) {
            continue;
        }
        // https://github.com/uBlockOrigin/uAssets/issues/192
        if ( opt === 'badfilter' ) {
            this.badFilter = BadFilter;
            continue;
        }
        // Unrecognized filter option: ignore whole filter.
        this.unsupported = true;
        break;
    }
};

/******************************************************************************/

// https://github.com/gorhill/uBlock/issues/1943#issuecomment-243188946
//   Convert websocket-related filter where possible to a format which
//   can be handled using CSP injection.

FilterParser.prototype.translate = function() {
    var dataTypeBit = this.bitFromType('data');

    if ( this.cantWebsocket && this.reWebsocketAny.test(this.f) ) {
        this.f = '*';
        this.types = dataTypeBit;
        this.dataType = 'csp';
        this.dataStr = "connect-src https: http:";
        // https://bugs.chromium.org/p/chromium/issues/detail?id=669086
        // TODO: remove when most users are beyond Chromium v56
        if ( vAPI.chromiumVersion < 57 ) {
            this.dataStr += '; frame-src *';
        }
        return;
    }

    // Broad |data:-based filters.
    if ( this.f === 'data:' ) {
        switch ( this.types ) {
        case 0:
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "default-src 'self' * blob: 'unsafe-inline' 'unsafe-eval'";
            break;
        case this.bitFromType('script'):
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "script-src 'self' * blob: 'unsafe-inline' 'unsafe-eval'";
            break;
        case this.bitFromType('sub_frame'):
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "frame-src 'self' * blob:";
            break;
        case this.bitFromType('script') | this.bitFromType('sub_frame'):
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "frame-src 'self' * blob:; script-src 'self' * blob: 'unsafe-inline' 'unsafe-eval';";
            break;
        default:
            break;
        }
    }

    // Broad |blob:-based filters.
    if ( this.f === 'blob:' ) {
        switch ( this.types ) {
        case 0:
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "default-src 'self' * data: 'unsafe-inline' 'unsafe-eval'";
            break;
        case this.bitFromType('script'):
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "script-src 'self' * data: 'unsafe-inline' 'unsafe-eval'";
            break;
        case this.bitFromType('sub_frame'):
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "frame-src 'self' * data:";
            break;
        case this.bitFromType('script') | this.bitFromType('sub_frame'):
            this.f = '*';
            this.types = dataTypeBit;
            this.dataType = 'csp';
            this.dataStr = "frame-src 'self' * data:; script-src 'self' * data: 'unsafe-inline' 'unsafe-eval';";
            break;
        default:
            break;
        }
    }
};

/*******************************************************************************

    anchor: bit vector
        0000 (0x0): no anchoring
        0001 (0x1): anchored to the end of the URL.
        0010 (0x2): anchored to the start of the URL.
        0011 (0x3): anchored to the start and end of the URL.
        0100 (0x4): anchored to the hostname of the URL.
        0101 (0x5): anchored to the hostname and end of the URL.

**/

FilterParser.prototype.parse = function(raw) {
    // important!
    this.reset();

    var s = this.raw = raw;

    // plain hostname? (from HOSTS file)
    if ( this.reHostnameRule1.test(s) ) {
        this.f = s;
        this.hostnamePure = true;
        this.anchor |= 0x4;
        return this;
    }

    // element hiding filter?
    var pos = s.indexOf('#');
    if ( pos !== -1 ) {
        var c = s.charAt(pos + 1);
        if ( c === '#' || c === '@' ) {
            console.error('static-net-filtering.js > unexpected cosmetic filters');
            this.elemHiding = true;
            return this;
        }
    }

    // block or allow filter?
    // Important: this must be executed before parsing options
    if ( s.startsWith('@@') ) {
        this.action = AllowAction;
        s = s.slice(2);
    }

    // options
    // https://github.com/gorhill/uBlock/issues/842
    // - ensure sure we are not dealing with a regex-based filter.
    // - lookup the last occurrence of `$`.
    if ( s.startsWith('/') === false || s.endsWith('/') === false ) {
        pos = s.lastIndexOf('$');
        if ( pos !== -1 ) {
            // https://github.com/gorhill/uBlock/issues/952
            // Discard Adguard-specific `$$` filters.
            if ( s.indexOf('$$') !== -1 ) {
                this.unsupported = true;
                return this;
            }
            this.parseOptions(s.slice(pos + 1));
            s = s.slice(0, pos);
        }
    }

    // regex?
    if ( s.startsWith('/') && s.endsWith('/') && s.length > 2 ) {
        this.isRegex = true;
        this.f = s.slice(1, -1);
        // https://github.com/gorhill/uBlock/issues/1246
        // If the filter is valid, use the corrected version of the source
        // string -- this ensure reverse-lookup will work fine.
        this.f = normalizeRegexSource(this.f);
        if ( this.f === '' ) {
            console.error(
                "uBlock Origin> discarding bad regular expression-based network filter '%s': '%s'",
                raw,
                normalizeRegexSource.message
            );
            this.unsupported = true;
        }
        return this;
    }

    // hostname-anchored
    if ( s.startsWith('||') ) {
        this.anchor |= 0x4;
        s = s.slice(2);

        // convert hostname to punycode if needed
        // https://github.com/gorhill/uBlock/issues/2599
        if ( this.reHasUnicode.test(s) ) {
            var matches = this.reIsolateHostname.exec(s);
            if ( matches ) {
                s = (matches[1] !== undefined ? matches[1] : '') +
                    punycode.toASCII(matches[2]) +
                    matches[3];
                //console.debug('µBlock.staticNetFilteringEngine/FilterParser.parse():', raw, '=', s);
            }
        }

        // https://github.com/chrisaljoudi/uBlock/issues/1096
        if ( s.startsWith('^') ) {
            this.unsupported = true;
            return this;
        }

        // plain hostname? (from ABP filter list)
        // https://github.com/gorhill/uBlock/issues/1757
        // A filter can't be a pure-hostname one if there is a domain or csp
        // option present.
        if ( this.reHostnameRule2.test(s) ) {
            this.f = s.replace(this.reCleanupHostnameRule2, '');
            this.hostnamePure = true;
            return this;
        }
    }
    // left-anchored
    else if ( s.startsWith('|') ) {
        this.anchor |= 0x2;
        s = s.slice(1);
    }

    // right-anchored
    if ( s.endsWith('|') ) {
        this.anchor |= 0x1;
        s = s.slice(0, -1);
    }

    // https://github.com/gorhill/uBlock/issues/1669#issuecomment-224822448
    // remove pointless leading *.
    if ( s.startsWith('*') ) {
        s = s.replace(/^\*+([^%0-9a-z])/, '$1');
    }
    // remove pointless trailing *
    if ( s.endsWith('*') ) {
        s = s.replace(/([^%0-9a-z])\*+$/, '$1');
    }

    // nothing left?
    if ( s === '' ) {
        s = '*';
    }

    // https://github.com/gorhill/uBlock/issues/1047
    // Hostname-anchored makes no sense if matching all requests.
    if ( s === '*' ) {
        this.anchor = 0;
    }

    // This might look weird but we gain memory footprint by not going through
    // toLowerCase(), at least on Chromium. Because copy-on-write?

    this.f = this.reHasUppercase.test(s) ? s.toLowerCase() : s;

    // Convenience:
    //   Convert special broad filters for non-webRequest aware types into
    //   `csp` filters wherever possible.
    if ( this.anchor & 0x2 && this.party === 0 ) {
        this.translate();
    }

    return this;
};

/******************************************************************************/

// Given a string, find a good token. Tokens which are too generic, i.e. very
// common with a high probability of ending up as a miss, are not
// good. Avoid if possible. This has a *significant* positive impact on
// performance.
// These "bad tokens" are collated manually.

// Hostname-anchored with no wildcard always have a token index of 0.
var reHostnameToken = /^[0-9a-z]+/;
var reGoodToken = /[%0-9a-z]{2,}/g;
var reRegexToken = /[%0-9A-Za-z]{2,}/g;
var reRegexTokenAbort = /[([]/;
var reRegexBadPrefix = /(^|[^\\]\.|[*?{}\\])$/;
var reRegexBadSuffix = /^([^\\]\.|\\[dw]|[([{}?*]|$)/;

var badTokens = new Set([
    'com',
    'http',
    'https',
    'icon',
    'images',
    'img',
    'js',
    'net',
    'news',
    'www'
]);

FilterParser.prototype.findFirstGoodToken = function() {
    reGoodToken.lastIndex = 0;
    var s = this.f,
        matches, lpos,
        badTokenMatch = null;
    while ( (matches = reGoodToken.exec(s)) !== null ) {
        // https://github.com/gorhill/uBlock/issues/997
        // Ignore token if preceded by wildcard.
        lpos = matches.index;
        if ( lpos !== 0 && s.charCodeAt(lpos - 1) === 0x2A /* '*' */ ) {
            continue;
        }
        if ( s.charCodeAt(reGoodToken.lastIndex) === 0x2A /* '*' */ ) {
            continue;
        }
        if ( badTokens.has(matches[0]) ) {
            if ( badTokenMatch === null ) {
                badTokenMatch = matches;
            }
            continue;
        }
        return matches;
    }
    return badTokenMatch;
};

FilterParser.prototype.extractTokenFromRegex = function() {
    reRegexToken.lastIndex = 0;
    var s = this.f,
        matches, prefix;
    while ( (matches = reRegexToken.exec(s)) !== null ) {
        prefix = s.slice(0, matches.index);
        if ( reRegexTokenAbort.test(prefix) ) { return; }
        if (
            reRegexBadPrefix.test(prefix) ||
            reRegexBadSuffix.test(s.slice(reRegexToken.lastIndex))
        ) {
            continue;
        }
        this.token = matches[0].toLowerCase();
        this.tokenHash = µb.urlTokenizer.tokenHashFromString(this.token);
        this.tokenBeg = matches.index;
        if ( badTokens.has(this.token) === false ) { break; }
    }
};

/******************************************************************************/

// https://github.com/chrisaljoudi/uBlock/issues/1038
// Single asterisk will match any URL.

// https://github.com/gorhill/uBlock/issues/2781
//   For efficiency purpose, try to extract a token from a regex-based filter.

FilterParser.prototype.makeToken = function() {
    if ( this.isRegex ) {
        this.extractTokenFromRegex();
        return;
    }

    if ( this.f === '*' ) { return; }

    var matches = null;
    if ( (this.anchor & 0x4) !== 0 && this.f.indexOf('*') === -1 ) {
        matches = reHostnameToken.exec(this.f);
    }
    if ( matches === null ) {
        matches = this.findFirstGoodToken();
    }
    if ( matches !== null ) {
        this.token = matches[0];
        this.tokenHash = µb.urlTokenizer.tokenHashFromString(this.token);
        this.tokenBeg = matches.index;
    }
};

/******************************************************************************/
/******************************************************************************/

var FilterContainer = function() {
    this.reIsGeneric = /[\^\*]/;
    this.filterParser = new FilterParser();
    this.urlTokenizer = µb.urlTokenizer;
    this.noTokenHash = this.urlTokenizer.tokenHashFromString('*');
    this.dotTokenHash = this.urlTokenizer.tokenHashFromString('.');
    this.reset();
};

/******************************************************************************/

// Reset all, thus reducing to a minimum memory footprint of the context.

FilterContainer.prototype.reset = function() {
    this.frozen = false;
    this.processedFilterCount = 0;
    this.acceptedCount = 0;
    this.rejectedCount = 0;
    this.allowFilterCount = 0;
    this.blockFilterCount = 0;
    this.discardedCount = 0;
    this.badFilters = new Set();
    this.duplicateBuster = new Set();
    this.categories = new Map();
    this.dataFilters = new Map();
    this.filterParser.reset();

    // Reuse filter instances whenever possible at load time.
    this.fclassLast = null;
    this.fdataLast = null;
    this.filterLast = null;

    // Runtime registers
    this.cbRegister = undefined;
    this.thRegister = undefined;
    this.fRegister = null;
};

/******************************************************************************/

FilterContainer.prototype.freeze = function() {
    histogram('allFilters', this.categories);
    this.removeBadFilters();
    this.duplicateBuster = new Set();
    this.filterParser.reset();
    this.fclassLast = null;
    this.fdataLast = null;
    this.filterLast = null;
    this.frozen = true;
    //console.log(JSON.stringify(Array.from(filterClassHistogram)));
    //this.tokenHistogram = new Map(Array.from(this.tokenHistogram).sort(function(a, b) {
    //    return a[0].localeCompare(b[0]) || (b[1] - a[1]);
    //}));
};

/******************************************************************************/

FilterContainer.prototype.toSelfie = function() {
    var categoriesToSelfie = function(categoryMap) {
        var categoryEntries = [];
        for ( var categoryEntry of categoryMap ) {
            var tokenEntries = [];
            for ( var tokenEntry of categoryEntry[1] ) {
                tokenEntries.push([ tokenEntry[0], tokenEntry[1].compile() ]);
            }
            categoryEntries.push([ categoryEntry[0], tokenEntries ]);
        }
        return JSON.stringify(categoryEntries);
    };

    var dataFiltersToSelfie = function(dataFilters) {
        var selfie = [];
        for ( var entry of dataFilters.values() ) {
            do {
                selfie.push(entry.compile());
                entry = entry.next;
            } while ( entry !== undefined );
        }
        return JSON.stringify(selfie);
    };

    return {
        processedFilterCount: this.processedFilterCount,
        acceptedCount: this.acceptedCount,
        rejectedCount: this.rejectedCount,
        allowFilterCount: this.allowFilterCount,
        blockFilterCount: this.blockFilterCount,
        discardedCount: this.discardedCount,
        categories: categoriesToSelfie(this.categories),
        dataFilters: dataFiltersToSelfie(this.dataFilters)
    };
};

/******************************************************************************/

FilterContainer.prototype.fromSelfie = function(selfie) {
    this.frozen = true;
    this.processedFilterCount = selfie.processedFilterCount;
    this.acceptedCount = selfie.acceptedCount;
    this.rejectedCount = selfie.rejectedCount;
    this.allowFilterCount = selfie.allowFilterCount;
    this.blockFilterCount = selfie.blockFilterCount;
    this.discardedCount = selfie.discardedCount;

    var entries;

    var categoryMap = new Map();
    entries = JSON.parse(selfie.categories);
    for ( var i = 0, ni = entries.length; i < ni; i++ ) {
        var categoryEntry = entries[i],
            tokenMap = new Map();
        var tokenEntries = categoryEntry[1];
        for ( var j = 0, nj = tokenEntries.length; j < nj; j++ ) {
            var tokenEntry = tokenEntries[j];
            tokenMap.set(tokenEntry[0], filterFromCompiledData(tokenEntry[1]));
        }
        categoryMap.set(categoryEntry[0], tokenMap);
    }
    this.categories = categoryMap;

    entries = JSON.parse(selfie.dataFilters);
    var entry, bucket;
    i = entries.length;
    while ( i-- ) {
        entry = FilterDataHolderEntry.load(entries[i]);
        bucket = this.dataFilters.get(entry.tokenHash);
        if ( bucket !== undefined ) {
            entry.next = bucket;
        }
        this.dataFilters.set(entry.tokenHash, entry);
    }
};

/******************************************************************************/

FilterContainer.prototype.compile = function(raw, writer) {
    // ORDER OF TESTS IS IMPORTANT!

    // Ignore empty lines
    var s = raw.trim();
    if ( s.length === 0 ) {
        return false;
    }

    var parsed = this.filterParser.parse(s);

    // Ignore element-hiding filters
    if ( parsed.elemHiding ) {
        return false;
    }

    // Ignore filters with unsupported options
    if ( parsed.unsupported ) {
        µb.logger.writeOne('', 'error', 'Network filtering – invalid filter: ' + raw);
        return false;
    }

    // Pure hostnames, use more efficient dictionary lookup
    // https://github.com/chrisaljoudi/uBlock/issues/665
    // Create a dict keyed on request type etc.
    if (
        parsed.hostnamePure &&
        parsed.domainOpt === '' &&
        parsed.dataType === undefined &&
        this.compileHostnameOnlyFilter(parsed, writer)
    ) {
        return true;
    }

    parsed.makeToken();

    var fdata;
    if ( parsed.isRegex ) {
        fdata = FilterRegex.compile(parsed);
    } else if ( parsed.hostnamePure ) {
        fdata = FilterPlainHostname.compile(parsed);
    } else if ( parsed.f === '*' ) {
        fdata = FilterTrue.compile();
    } else if ( parsed.anchor === 0x5 ) {
        // https://github.com/gorhill/uBlock/issues/1669
        fdata = FilterGenericHnAndRightAnchored.compile(parsed);
    } else if ( parsed.anchor === 0x4 ) {
        if (
            this.reIsGeneric.test(parsed.f) === false &&
            parsed.tokenHash !== parsed.noTokenHash &&
            parsed.tokenBeg === 0
        ) {
            fdata = FilterPlainHnAnchored.compile(parsed);
        } else {
            fdata = FilterGenericHnAnchored.compile(parsed);
        }
    } else if (
        this.reIsGeneric.test(parsed.f) ||
        parsed.tokenHash === parsed.noTokenHash
    ) {
        fdata = FilterGeneric.compile(parsed);
    } else if ( parsed.anchor === 0x2 ) {
        fdata = FilterPlainLeftAnchored.compile(parsed);
    } else if ( parsed.anchor === 0x1 ) {
        fdata = FilterPlainRightAnchored.compile(parsed);
    } else if ( parsed.tokenBeg === 0 ) {
        fdata = FilterPlainPrefix0.compile(parsed);
    } else if ( parsed.tokenBeg === 1 ) {
        fdata = FilterPlainPrefix1.compile(parsed);
    } else {
        fdata = FilterPlain.compile(parsed);
    }

    var fwrapped;
    if ( parsed.domainOpt !== '' ) {
        fwrapped = fdata;
        fdata = FilterOrigin.compile(parsed);
        fdata.push(fwrapped);
    }

    if ( parsed.dataType !== undefined ) {
        fwrapped = fdata;
        fdata = FilterDataHolder.compile(parsed);
        fdata.push(fwrapped);
    }

    this.compileToAtomicFilter(fdata, parsed, writer);

    return true;
};

/******************************************************************************/

// Using fast/compact dictionary when filter is a pure hostname.

FilterContainer.prototype.compileHostnameOnlyFilter = function(parsed, writer) {
    // Can't fit the filter in a pure hostname dictionary.
    // https://github.com/gorhill/uBlock/issues/1757
    // This should no longer happen with fix to above issue.
    //if ( parsed.domainOpt.length !== 0 ) {
    //    return;
    //}

    var descBits = parsed.action | parsed.important | parsed.party | parsed.badFilter;

    var type = parsed.types;
    if ( type === 0 ) {
        writer.push([ descBits, this.dotTokenHash, parsed.f ]);
        return true;
    }

    var bitOffset = 1;
    do {
        if ( type & 1 ) {
            writer.push([ descBits | (bitOffset << 4), this.dotTokenHash, parsed.f ]);
        }
        bitOffset += 1;
        type >>>= 1;
    } while ( type !== 0 );
    return true;
};

/******************************************************************************/

FilterContainer.prototype.compileToAtomicFilter = function(fdata, parsed, writer) {
    var descBits = parsed.action | parsed.important | parsed.party | parsed.badFilter,
        type = parsed.types;
    if ( type === 0 ) {
        writer.push([ descBits, parsed.tokenHash, fdata ]);
        return;
    }
    var bitOffset = 1;
    do {
        if ( type & 1 ) {
            writer.push([ descBits | (bitOffset << 4), parsed.tokenHash, fdata ]);
        }
        bitOffset += 1;
        type >>>= 1;
    } while ( type !== 0 );

    // Only static filter with an explicit type can be redirected. If we reach
    // this point, it's because there is one or more explicit type.
    if ( !parsed.redirect ) {
        return;
    }

    if ( parsed.badFilter ) {
        return;
    }

    var redirects = µb.redirectEngine.compileRuleFromStaticFilter(parsed.raw);
    if ( Array.isArray(redirects) === false ) {
        return;
    }
    descBits = typeNameToTypeValue.redirect;
    var i = redirects.length;
    while ( i-- ) {
        writer.push([ descBits, redirects[i] ]);
    }
};

/******************************************************************************/

FilterContainer.prototype.fromCompiledContent = function(reader) {
    var badFilterBit = BadFilter,
        filterPairId = FilterPair.fid,
        filterBucketId = FilterBucket.fid,
        filterDataHolderId = FilterDataHolder.fid,
        args, bits, bucket, entry,
        tokenHash, fdata, fingerprint;

    while ( reader.next() === true ) {
        args = reader.args();
        bits = args[0];

        if ( (bits & badFilterBit) !== 0 ) {
            this.badFilters.add(args);
            continue;
        }

        // Special cases: delegate to more specialized engines.
        // Redirect engine.
        if ( (bits >>> 4) === 17 ) {
            µb.redirectEngine.fromCompiledRule(args[1]);
            continue;
        }

        this.acceptedCount += 1;

        // Plain static filters.
        fingerprint = reader.fingerprint();
        tokenHash = args[1];
        fdata = args[2];

        // Special treatment: data-holding filters are stored separately
        // because they require special matching algorithm (unlike other
        // filters, ALL hits must be reported).
        if ( fdata[0] === filterDataHolderId ) {
            if ( this.duplicateBuster.has(fingerprint) ) {
                this.discardedCount += 1;
                continue;
            }
            this.duplicateBuster.add(fingerprint);
            entry = new FilterDataHolderEntry(bits, tokenHash, fdata);
            bucket = this.dataFilters.get(tokenHash);
            if ( bucket !== undefined ) {
                entry.next = bucket;
            }
            this.dataFilters.set(tokenHash, entry);
            continue;
        }

        bucket = this.categories.get(bits);
        if ( bucket === undefined ) {
            bucket = new Map();
            this.categories.set(bits, bucket);
        }
        entry = bucket.get(tokenHash);

        if ( tokenHash === this.dotTokenHash ) {
            if ( entry === undefined ) {
                entry = new FilterHostnameDict();
                bucket.set(this.dotTokenHash, entry);
            }
            if ( entry.add(fdata) === false ) {
                this.discardedCount += 1;
            }
            continue;
        }

        if ( this.duplicateBuster.has(fingerprint) ) {
            this.discardedCount += 1;
            continue;
        }
        this.duplicateBuster.add(fingerprint);

        if ( entry === undefined ) {
            bucket.set(tokenHash, filterFromCompiledData(fdata));
            continue;
        }
        if ( entry.fid === filterBucketId ) {
            entry.add(fdata);
            continue;
        }
        if ( entry.fid === filterPairId ) {
            bucket.set(
                tokenHash,
                entry.upgrade(filterFromCompiledData(fdata))
            );
            continue;
        }
        bucket.set(
            tokenHash,
            new FilterPair(entry, filterFromCompiledData(fdata))
        );
    }
};

/******************************************************************************/

FilterContainer.prototype.removeBadFilters = function() {
    var filterPairId = FilterPair.fid,
        filterBucketId = FilterBucket.fid,
        filterHostnameDictId = FilterHostnameDict.fid,
        bits, tokenHash, fdata, bucket, entry;
    for ( var args of this.badFilters ) {
        bits = args[0] & ~BadFilter;
        bucket = this.categories.get(bits);
        if ( bucket === undefined ) { continue; }
        tokenHash = args[1];
        entry = bucket.get(tokenHash);
        if ( entry === undefined ) { continue; }
        fdata = args[2];
        if ( entry.fid === filterPairId ) {
            entry.remove(fdata);
            if ( entry.size === 1 ) {
                bucket.set(tokenHash, entry.f1);
            }
            continue;
        }
        if ( entry.fid === filterBucketId ) {
            entry.remove(fdata);
            if ( entry.size === 2 ) {
                bucket.set(tokenHash, entry.downgrade());
            }
            continue;
        }
        if ( entry.fid === filterHostnameDictId ) {
            entry.remove(fdata);
            if ( entry.size === 0 ) {
                bucket.delete(tokenHash);
                if ( bucket.size === 0 ) {
                    this.categories.delete(bits);
                }
            }
            continue;
        }
        if ( arrayStrictEquals(entry.compile(), fdata) === true ) {
            bucket.delete(tokenHash);
            if ( bucket.size === 0 ) {
                this.categories.delete(bits);
            }
            continue;
        }
    }
};

/******************************************************************************/

FilterContainer.prototype.matchAndFetchData = function(dataType, requestURL, out, outlog) {
    if ( this.dataFilters.length === 0 ) { return; }

    var url = this.urlTokenizer.setURL(requestURL);

    requestHostnameRegister = µb.URI.hostnameFromURI(url);

    // We need to visit ALL the matching filters.
    var toAddImportant = new Map(),
        toAdd = new Map(),
        toRemove = new Map();

    var entry, f,
        tokenHashes = this.urlTokenizer.getTokens(),
        tokenHash, tokenOffset,
        i = 0;
    while ( i < 32 ) {
        tokenHash = tokenHashes[i++];
        if ( tokenHash === 0 ) { break; }
        tokenOffset = tokenHashes[i++];
        entry = this.dataFilters.get(tokenHash);
        while ( entry !== undefined ) {
            f = entry.filter;
            if ( f.match(url, tokenOffset) === true ) {
                if ( entry.categoryBits & 0x001 ) {
                    toRemove.set(f.dataStr, entry);
                } else if ( entry.categoryBits & 0x002 ) {
                    toAddImportant.set(f.dataStr, entry);
                } else {
                    toAdd.set(f.dataStr, entry);
                }
            }
            entry = entry.next;
        }
    }
    entry = this.dataFilters.get(this.noTokenHash);
    while ( entry !== undefined ) {
        f = entry.filter;
        if ( f.match(url) === true ) {
            if ( entry.categoryBits & 0x001 ) {
                toRemove.set(f.dataStr, entry);
            } else if ( entry.categoryBits & 0x002 ) {
                toAddImportant.set(f.dataStr, entry);
            } else {
                toAdd.set(f.dataStr, entry);
            }
        }
        entry = entry.next;
    }

    if ( toAddImportant.size === 0 && toAdd.size === 0 ) { return; }

    // Remove entries overriden by other filters.
    var key;
    for ( key of toAddImportant.keys() ) {
        toAdd.delete(key);
        toRemove.delete(key);
    }
    for ( key of toRemove.keys() ) {
        if ( key === '' ) {
            toAdd.clear();
            break;
        }
        toAdd.delete(key);
    }

    var logData;
    for ( entry of toAddImportant ) {
        out.push(entry[0]);
        if ( outlog === undefined ) { continue; }
        logData = entry[1].logData();
        logData.source = 'static';
        logData.result = 1;
        outlog.push(logData);
    }
    for ( entry of toAdd ) {
        out.push(entry[0]);
        if ( outlog === undefined ) { continue; }
        logData = entry[1].logData();
        logData.source = 'static';
        logData.result = 1;
        outlog.push(logData);
    }
    if ( outlog !== undefined ) {
        for ( entry of toRemove.values()) {
            logData = entry.logData();
            logData.source = 'static';
            logData.result = 2;
            outlog.push(logData);
        }
    }
};

/******************************************************************************/

// bucket: Map
// url: string

FilterContainer.prototype.matchTokens = function(bucket, url) {
    // Hostname-only filters
    var f = bucket.get(this.dotTokenHash);
    if ( f !== undefined && f.match() === true ) {
        this.thRegister = this.dotTokenHash;
        this.fRegister = f;
        return true;
    }

    var tokenHashes = this.urlTokenizer.getTokens(),
        tokenHash, tokenOffset,
        i = 0;
    for (;;) {
        tokenHash = tokenHashes[i++];
        if ( tokenHash === 0 ) { break; }
        tokenOffset = tokenHashes[i++];
        f = bucket.get(tokenHash);
        if ( f !== undefined && f.match(url, tokenOffset) === true ) {
            this.thRegister = tokenHash;
            this.fRegister = f;
            return true;
        }
    }

    // Untokenizable filters
    f = bucket.get(this.noTokenHash);
    if ( f !== undefined && f.match(url) === true ) {
        this.thRegister = this.noTokenHash;
        this.fRegister = f;
        return true;
    }

    return false;
};

/******************************************************************************/

// Specialized handlers

// https://github.com/gorhill/uBlock/issues/1477
//   Special case: blocking-generichide filter ALWAYS exists, it is implicit --
//   thus we always first check for exception filters, then for important block
//   filter if and only if there was a hit on an exception filter.
// https://github.com/gorhill/uBlock/issues/2103
//   User may want to override `generichide` exception filters.

FilterContainer.prototype.matchStringGenericHide = function(context, requestURL) {
    var url = this.urlTokenizer.setURL(requestURL);

    // https://github.com/gorhill/uBlock/issues/2225
    //   Important: this is used by FilterHostnameDict.match().
    requestHostnameRegister = µb.URI.hostnameFromURI(url);

    var bucket = this.categories.get(genericHideException);
    if ( !bucket || this.matchTokens(bucket, url) === false ) {
        this.fRegister = null;
        return 0;
    }

    bucket = this.categories.get(genericHideImportant);
    if ( bucket && this.matchTokens(bucket, url) ) {
        this.cbRegister = genericHideImportant;
        return 1;
    }

    this.cbRegister = genericHideException;
    return 2;
};

/******************************************************************************/

// https://github.com/chrisaljoudi/uBlock/issues/116
//   Some type of requests are exceptional, they need custom handling,
//   not the generic handling.

FilterContainer.prototype.matchStringExactType = function(context, requestURL, requestType) {
    // Special cases.
    if ( requestType === 'generichide' ) {
        return this.matchStringGenericHide(context, requestURL);
    }
    var type = typeNameToTypeValue[requestType];
    if ( type === undefined ) {
        return 0;
    }

    // Prime tokenizer: we get a normalized URL in return.
    var url = this.urlTokenizer.setURL(requestURL);

    // These registers will be used by various filters
    pageHostnameRegister = context.pageHostname || '';
    requestHostnameRegister = µb.URI.hostnameFromURI(url);

    var party = isFirstParty(context.pageDomain, requestHostnameRegister) ? FirstParty : ThirdParty,
        categories = this.categories,
        catBits, bucket;

    this.fRegister = null;

    // https://github.com/chrisaljoudi/uBlock/issues/139
    //   Test against important block filters
    catBits = BlockAnyParty | Important | type;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 1;
        }
    }
    catBits = BlockAction | Important | type | party;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 1;
        }
    }

    // Test against block filters
    catBits = BlockAnyParty | type;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
        }
    }
    if ( this.fRegister === null ) {
        catBits = BlockAction | type | party;
        if ( (bucket = categories.get(catBits)) ) {
            if ( this.matchTokens(bucket, url) ) {
                this.cbRegister = catBits;
            }
        }
    }

    // If there is no block filter, no need to test against allow filters
    if ( this.fRegister === null ) {
        return 0;
    }

    // Test against allow filters
    catBits = AllowAnyParty | type;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 2;
        }
    }
    catBits = AllowAction | type | party;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 2;
        }
    }

    return 1;
};

/******************************************************************************/

FilterContainer.prototype.matchString = function(context) {
    // https://github.com/chrisaljoudi/uBlock/issues/519
    // Use exact type match for anything beyond `other`
    // Also, be prepared to support unknown types
    var type = typeNameToTypeValue[context.requestType];
    if ( type === undefined ) {
         type = otherTypeBitValue;
    } else if ( type === 0 || type > otherTypeBitValue ) {
        return this.matchStringExactType(context, context.requestURL, context.requestType);
    }

    // The logic here is simple:
    //
    // block = !whitelisted &&  blacklisted
    //   or equivalent
    // allow =  whitelisted || !blacklisted

    // Statistically, hits on a URL in order of likelihood:
    // 1. No hit
    // 2. Hit on a block filter
    // 3. Hit on an allow filter
    //
    // High likelihood of "no hit" means to optimize we need to reduce as much
    // as possible the number of filters to test.
    //
    // Then, because of the order of probabilities, we should test only
    // block filters first, and test allow filters if and only if there is a
    // hit on a block filter. Since there is a high likelihood of no hit,
    // testing allow filter by default is likely wasted work, hence allow
    // filters are tested *only* if there is a (unlikely) hit on a block
    // filter.

    // Prime tokenizer: we get a normalized URL in return.
    var url = this.urlTokenizer.setURL(context.requestURL);

    // These registers will be used by various filters
    pageHostnameRegister = context.pageHostname || '';
    requestHostnameRegister = context.requestHostname;

    this.fRegister = null;

    var party = isFirstParty(context.pageDomain, context.requestHostname)
        ? FirstParty
        : ThirdParty;
    var categories = this.categories,
        catBits, bucket;

    // https://github.com/chrisaljoudi/uBlock/issues/139
    // Test against important block filters.
    // The purpose of the `important` option is to reverse the order of
    // evaluation. Normally, it is "evaluate block then evaluate allow", with
    // the `important` property it is "evaluate allow then evaluate block".
    catBits = BlockAnyTypeAnyParty | Important;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 1;
        }
    }
    catBits = BlockAnyType | Important | party;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 1;
        }
    }
    catBits = BlockAnyParty | Important | type;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 1;
        }
    }
    catBits = BlockAction | Important | type | party;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 1;
        }
    }

    // Test against block filters
    catBits = BlockAnyTypeAnyParty;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
        }
    }
    if ( this.fRegister === null ) {
        catBits = BlockAnyType | party;
        if ( (bucket = categories.get(catBits)) ) {
            if ( this.matchTokens(bucket, url) ) {
                this.cbRegister = catBits;
            }
        }
        if ( this.fRegister === null ) {
            catBits = BlockAnyParty | type;
            if ( (bucket = categories.get(catBits)) ) {
                if ( this.matchTokens(bucket, url) ) {
                    this.cbRegister = catBits;
                }
            }
            if ( this.fRegister === null ) {
                catBits = BlockAction | type | party;
                if ( (bucket = categories.get(catBits)) ) {
                    if ( this.matchTokens(bucket, url) ) {
                        this.cbRegister = catBits;
                    }
                }
            }
        }
    }

    // If there is no block filter, no need to test against allow filters
    if ( this.fRegister === null ) {
        return 0;
    }

    // Test against allow filters
    catBits = AllowAnyTypeAnyParty;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 2;
        }
    }
    catBits = AllowAnyType | party;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 2;
        }
    }
    catBits = AllowAnyParty | type;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 2;
        }
    }
    catBits = AllowAction | type | party;
    if ( (bucket = categories.get(catBits)) ) {
        if ( this.matchTokens(bucket, url) ) {
            this.cbRegister = catBits;
            return 2;
        }
    }

    return 1;
};

/******************************************************************************/

FilterContainer.prototype.toLogData = function() {
    if ( this.fRegister === null ) { return; }
    var logData = toLogDataInternal(this.cbRegister, this.thRegister, this.fRegister);
    logData.source = 'static';
    logData.tokenHash = this.thRegister;
    logData.result = this.fRegister === null ? 0 : (this.cbRegister & 1 ? 2 : 1);
    return logData;
};

/******************************************************************************/

FilterContainer.prototype.getFilterCount = function() {
    return this.acceptedCount - this.discardedCount;
};

/******************************************************************************/

return new FilterContainer();

/******************************************************************************/

})();
