diff options
Diffstat (limited to 'src/js/hnswitches.js')
-rw-r--r-- | src/js/hnswitches.js | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/src/js/hnswitches.js b/src/js/hnswitches.js new file mode 100644 index 0000000..9e94a8e --- /dev/null +++ b/src/js/hnswitches.js @@ -0,0 +1,289 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2015-present 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 */ + +'use strict'; + +/******************************************************************************/ + +import punycode from '../lib/punycode.js'; + +import { decomposeHostname } from './uri-utils.js'; +import { LineIterator } from './text-utils.js'; + +/******************************************************************************/ + +const decomposedSource = []; + +// Object.create(null) is used below to eliminate worries about unexpected +// property names in prototype chain -- and this way we don't have to use +// hasOwnProperty() to avoid this. + +const switchBitOffsets = Object.create(null); +Object.assign(switchBitOffsets, { + 'no-strict-blocking': 0, + 'no-popups': 2, + 'no-cosmetic-filtering': 4, + 'no-remote-fonts': 6, + 'no-large-media': 8, + 'no-csp-reports': 10, + 'no-scripting': 12, +}); + +const switchStateToNameMap = Object.create(null); +Object.assign(switchStateToNameMap, { + '1': 'true', + '2': 'false', +}); + +const nameToSwitchStateMap = Object.create(null); +Object.assign(nameToSwitchStateMap, { + 'true': 1, + 'false': 2, + 'on': 1, + 'off': 2, +}); + +/******************************************************************************/ + +// For performance purpose, as simple test as possible +const reNotASCII = /[^\x20-\x7F]/; + +// http://tools.ietf.org/html/rfc5952 +// 4.3: "MUST be represented in lowercase" +// Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers + +/******************************************************************************/ + +class DynamicSwitchRuleFiltering { + constructor() { + this.reset(); + } + + reset() { + this.switches = new Map(); + this.n = ''; + this.z = ''; + this.r = 0; + this.changed = true; + } + + assign(from) { + // Remove rules not in other + for ( const hn of this.switches.keys() ) { + if ( from.switches.has(hn) === false ) { + this.switches.delete(hn); + this.changed = true; + } + } + // Add/change rules in other + for ( const [hn, bits] of from.switches ) { + if ( this.switches.get(hn) !== bits ) { + this.switches.set(hn, bits); + this.changed = true; + } + } + } + + copyRules(from, srcHostname) { + const thisBits = this.switches.get(srcHostname); + const fromBits = from.switches.get(srcHostname); + if ( fromBits !== thisBits ) { + if ( fromBits !== undefined ) { + this.switches.set(srcHostname, fromBits); + } else { + this.switches.delete(srcHostname); + } + this.changed = true; + } + return this.changed; + } + + hasSameRules(other, srcHostname) { + return this.switches.get(srcHostname) === other.switches.get(srcHostname); + } + + toggle(switchName, hostname, newVal) { + const bitOffset = switchBitOffsets[switchName]; + if ( bitOffset === undefined ) { return false; } + if ( newVal === this.evaluate(switchName, hostname) ) { return false; } + let bits = this.switches.get(hostname) || 0; + bits &= ~(3 << bitOffset); + bits |= newVal << bitOffset; + if ( bits === 0 ) { + this.switches.delete(hostname); + } else { + this.switches.set(hostname, bits); + } + this.changed = true; + return true; + } + + toggleOneZ(switchName, hostname, newState) { + const bitOffset = switchBitOffsets[switchName]; + if ( bitOffset === undefined ) { return false; } + let state = this.evaluateZ(switchName, hostname); + if ( newState === state ) { return false; } + if ( newState === undefined ) { + newState = !state; + } + let bits = this.switches.get(hostname) || 0; + bits &= ~(3 << bitOffset); + if ( bits === 0 ) { + this.switches.delete(hostname); + } else { + this.switches.set(hostname, bits); + } + state = this.evaluateZ(switchName, hostname); + if ( state !== newState ) { + this.switches.set(hostname, bits | ((newState ? 1 : 2) << bitOffset)); + } + this.changed = true; + return true; + } + + toggleBranchZ(switchName, targetHostname, newState) { + this.toggleOneZ(switchName, targetHostname, newState); + + // Turn off all descendant switches, they will inherit the state of the + // branch's origin. + const targetLen = targetHostname.length; + for ( const hostname of this.switches.keys() ) { + if ( hostname === targetHostname ) { continue; } + if ( hostname.length <= targetLen ) { continue; } + if ( hostname.endsWith(targetHostname) === false ) { continue; } + if ( hostname.charAt(hostname.length - targetLen - 1) !== '.' ) { + continue; + } + this.toggle(switchName, hostname, 0); + } + + return this.changed; + } + + toggleZ(switchName, hostname, deep, newState) { + if ( deep === true ) { + return this.toggleBranchZ(switchName, hostname, newState); + } + return this.toggleOneZ(switchName, hostname, newState); + } + + // 0 = inherit from broader scope, up to default state + // 1 = non-default state + // 2 = forced default state (to override a broader non-default state) + + evaluate(switchName, hostname) { + const bits = this.switches.get(hostname); + if ( bits === undefined ) { return 0; } + let bitOffset = switchBitOffsets[switchName]; + if ( bitOffset === undefined ) { return 0; } + return (bits >>> bitOffset) & 3; + } + + evaluateZ(switchName, hostname) { + const bitOffset = switchBitOffsets[switchName]; + if ( bitOffset === undefined ) { + this.r = 0; + return false; + } + this.n = switchName; + for ( const shn of decomposeHostname(hostname, decomposedSource) ) { + let bits = this.switches.get(shn); + if ( bits === undefined ) { continue; } + bits = bits >>> bitOffset & 3; + if ( bits === 0 ) { continue; } + this.z = shn; + this.r = bits; + return bits === 1; + } + this.r = 0; + return false; + } + + toLogData() { + return { + source: 'switch', + result: this.r, + raw: `${this.n}: ${this.z} true` + }; + } + + toArray() { + const out = []; + for ( const hostname of this.switches.keys() ) { + const prettyHn = hostname.includes('xn--') && punycode + ? punycode.toUnicode(hostname) + : hostname; + for ( const switchName in switchBitOffsets ) { + if ( switchBitOffsets[switchName] === undefined ) { continue; } + const val = this.evaluate(switchName, hostname); + if ( val === 0 ) { continue; } + out.push(`${switchName}: ${prettyHn} ${switchStateToNameMap[val]}`); + } + } + return out; + } + + toString() { + return this.toArray().join('\n'); + } + + fromString(text, append) { + const lineIter = new LineIterator(text); + if ( append !== true ) { this.reset(); } + while ( lineIter.eot() === false ) { + this.addFromRuleParts(lineIter.next().trim().split(/\s+/)); + } + } + + validateRuleParts(parts) { + if ( parts.length < 3 ) { return; } + if ( parts[0].endsWith(':') === false ) { return; } + if ( nameToSwitchStateMap[parts[2]] === undefined ) { return; } + if ( reNotASCII.test(parts[1]) && punycode !== undefined ) { + parts[1] = punycode.toASCII(parts[1]); + } + return parts; + } + + addFromRuleParts(parts) { + if ( this.validateRuleParts(parts) === undefined ) { return false; } + const switchName = parts[0].slice(0, -1); + if ( switchBitOffsets[switchName] === undefined ) { return false; } + this.toggle(switchName, parts[1], nameToSwitchStateMap[parts[2]]); + return true; + } + + removeFromRuleParts(parts) { + if ( this.validateRuleParts(parts) !== undefined ) { + this.toggle(parts[0].slice(0, -1), parts[1], 0); + return true; + } + return false; + } +} + +/******************************************************************************/ + +export default DynamicSwitchRuleFiltering; + +/******************************************************************************/ |