diff --git a/src/background.html b/src/background.html index 29b9a2970..104b94131 100644 --- a/src/background.html +++ b/src/background.html @@ -7,7 +7,7 @@ - + diff --git a/src/js/storage.js b/src/js/storage.js index 52b3eef6f..bd281decc 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -1009,6 +1009,10 @@ /******************************************************************************/ µBlock.loadPublicSuffixList = function() { + if ( this.hiddenSettings.disableWebAssembly === false ) { + publicSuffixList.enableWASM(); + } + return new Promise(resolve => { // start of executor this.assets.get('compiled/' + this.pslAssetKey, details => { diff --git a/src/js/uritools.js b/src/js/uritools.js index 6d5a12742..0bb59c8d0 100644 --- a/src/js/uritools.js +++ b/src/js/uritools.js @@ -307,7 +307,7 @@ URI.domain = function() { // It is expected that there is higher-scoped `publicSuffixList` lingering // somewhere. Cache it. See . -var psl = publicSuffixList; +const psl = publicSuffixList; /******************************************************************************/ @@ -391,7 +391,7 @@ const domainCachePrune = function() { } }; -window.addEventListener('publicSuffixList', function() { +window.addEventListener('publicSuffixListChanged', function() { domainCache.clear(); }); diff --git a/src/lib/publicsuffixlist.js b/src/lib/publicsuffixlist.js deleted file mode 100644 index cc7ae2dfc..000000000 --- a/src/lib/publicsuffixlist.js +++ /dev/null @@ -1,328 +0,0 @@ -/******************************************************************************* - - publicsuffixlist.js - an efficient javascript implementation to deal with - Mozilla Foundation's Public Suffix List - Copyright (C) 2013-2018 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/publicsuffixlist.js */ - -/* - This code is mostly dumb: I consider this to be lower-level code, thus - in order to ensure efficiency, the caller is responsible for sanitizing - the inputs. -*/ - -/******************************************************************************/ - -// A single instance of PublicSuffixList is enough. - -;(function(root) { - -'use strict'; - -/******************************************************************************/ - -let exceptions = new Map(); -let rules = new Map(); - -// This value dictate how the search will be performed: -// < this.cutoffLength = indexOf() -// >= this.cutoffLength = binary search -const cutoffLength = 256; -const mustPunycode = /[^a-z0-9.-]/; - -/******************************************************************************/ - -// In the context of this code, a domain is defined as: -// "{label}.{public suffix}". -// A single standalone label is a public suffix as per -// http://publicsuffix.org/list/: -// "If no rules match, the prevailing rule is '*' " -// This means 'localhost' is not deemed a domain by this -// code, since according to the definition above, it would be -// evaluated as a public suffix. The caller is therefore responsible to -// decide how to further interpret such public suffix. -// -// `hostname` must be a valid ascii-based hostname. - -function getDomain(hostname) { - // A hostname starting with a dot is not a valid hostname. - if ( !hostname || hostname.charAt(0) === '.' ) { - return ''; - } - hostname = hostname.toLowerCase(); - var suffix = getPublicSuffix(hostname); - if ( suffix === hostname ) { - return ''; - } - var pos = hostname.lastIndexOf('.', hostname.lastIndexOf('.', hostname.length - suffix.length) - 1); - if ( pos <= 0 ) { - return hostname; - } - return hostname.slice(pos + 1); -} - -/******************************************************************************/ - -// Return longest public suffix. -// -// `hostname` must be a valid ascii-based string which respect hostname naming. - -function getPublicSuffix(hostname) { - if ( !hostname ) { - return ''; - } - // Since we slice down the hostname with each pass, the first match - // is the longest, so no need to find all the matching rules. - var pos; - while ( true ) { - pos = hostname.indexOf('.'); - if ( pos < 0 ) { - return hostname; - } - if ( search(exceptions, hostname) ) { - return hostname.slice(pos + 1); - } - if ( search(rules, hostname) ) { - return hostname; - } - if ( search(rules, '*' + hostname.slice(pos)) ) { - return hostname; - } - hostname = hostname.slice(pos + 1); - } - // unreachable -} - -/******************************************************************************/ - -// Look up a specific hostname. - -function search(store, hostname) { - // Extract TLD - var pos = hostname.lastIndexOf('.'); - var tld, remainder; - if ( pos < 0 ) { - tld = hostname; - remainder = hostname; - } else { - tld = hostname.slice(pos + 1); - remainder = hostname.slice(0, pos); - } - var substore = store.get(tld); - if ( substore === undefined ) { return false; } - // If substore is a string, use indexOf() - if ( typeof substore === 'string' ) { - return substore.indexOf(' ' + remainder + ' ') >= 0; - } - // It is an array: use binary search. - var l = remainder.length; - if ( l >= substore.length ) { return false; } - var haystack = substore[l]; - if ( haystack.length === 0 ) { return false; } - var left = 0; - var right = Math.floor(haystack.length / l + 0.5); - var i, needle; - while ( left < right ) { - i = left + right >> 1; - needle = haystack.substr( l * i, l ); - if ( remainder < needle ) { - right = i; - } else if ( remainder > needle ) { - left = i + 1; - } else { - return true; - } - } - return false; -} - -/******************************************************************************/ - -// Parse and set a UTF-8 text-based suffix list. Format is same as found at: -// http://publicsuffix.org/list/ -// -// `toAscii` is a converter from unicode to punycode. Required since the -// Public Suffix List contains unicode characters. -// Suggestion: use it's quite good. - -function parse(text, toAscii) { - exceptions = new Map(); - rules = new Map(); - - // http://publicsuffix.org/list/: - // "... all rules must be canonicalized in the normal way - // for hostnames - lower-case, Punycode ..." - text = text.toLowerCase(); - - var lineBeg = 0, lineEnd; - var textEnd = text.length; - var line, store, pos, tld; - - while ( lineBeg < textEnd ) { - lineEnd = text.indexOf('\n', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = text.indexOf('\r', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = textEnd; - } - } - line = text.slice(lineBeg, lineEnd).trim(); - lineBeg = lineEnd + 1; - - if ( line.length === 0 ) { - continue; - } - - // Ignore comments - pos = line.indexOf('//'); - if ( pos >= 0 ) { - line = line.slice(0, pos); - } - - // Ignore surrounding whitespaces - line = line.trim(); - if ( !line ) { - continue; - } - - if ( mustPunycode.test(line) ) { - line = toAscii(line); - } - - // Is this an exception rule? - if ( line.charAt(0) === '!' ) { - store = exceptions; - line = line.slice(1); - } else { - store = rules; - } - - // Extract TLD - pos = line.lastIndexOf('.'); - if ( pos < 0 ) { - tld = line; - } else { - tld = line.slice(pos + 1); - line = line.slice(0, pos); - } - - // Store suffix using tld as key - var substore = store.get(tld); - if ( substore === undefined ) { - store.set(tld, (substore = [])); - } - if ( line ) { - substore.push(line); - } - } - crystallize(exceptions); - crystallize(rules); - - window.dispatchEvent(new CustomEvent('publicSuffixList')); -} - -/******************************************************************************/ - -// Cristallize the storage of suffixes using optimal internal representation -// for future look up. - -function crystallize(store) { - for ( var entry of store ) { - var tld = entry[0]; - var suffixes = entry[1]; - // No suffix - if ( suffixes.length === 0 ) { - store.set(tld, ''); - continue; - } - // Concatenated list of suffixes less than cutoff length: - // Store as string, lookup using indexOf() - var s = suffixes.join(' '); - if ( s.length < cutoffLength ) { - store.set(tld, ' ' + s + ' '); - continue; - } - // Concatenated list of suffixes greater or equal to cutoff length - // Store as array keyed on suffix length, lookup using binary search. - // I borrowed the idea to key on string length here: - // http://ejohn.org/blog/dictionary-lookups-in-javascript/#comment-392072 - var i = suffixes.length, l; - var aa = []; - while ( i-- ) { - var suffix = suffixes[i]; - var j = aa.length; - l = suffix.length; - while ( j <= l ) { - aa[j] = []; j += 1; - } - aa[l].push(suffix); - } - l = aa.length; - while ( l-- ) { - aa[l] = aa[l].sort().join(''); - } - store.set(tld, aa); - } - return store; -} - -/******************************************************************************/ - -const selfieMagic = 1; - -function toSelfie() { - return { - magic: selfieMagic, - rules: Array.from(rules), - exceptions: Array.from(exceptions) - }; -} - -function fromSelfie(selfie) { - if ( typeof selfie !== 'object' || selfie.magic !== selfieMagic ) { - return false; - } - rules = new Map(selfie.rules); - exceptions = new Map(selfie.exceptions); - window.dispatchEvent(new CustomEvent('publicSuffixList')); - return true; -} - -/******************************************************************************/ - -// Public API - -root = root || window; - -root.publicSuffixList = { - version: '1.0', - parse: parse, - getDomain: getDomain, - getPublicSuffix: getPublicSuffix, - toSelfie: toSelfie, - fromSelfie: fromSelfie, - get empty() { - return rules.size === 0; - } -}; - -/******************************************************************************/ - -})(this); - diff --git a/src/lib/publicsuffixlist/publicsuffixlist.js b/src/lib/publicsuffixlist/publicsuffixlist.js new file mode 100644 index 000000000..34473cd6e --- /dev/null +++ b/src/lib/publicsuffixlist/publicsuffixlist.js @@ -0,0 +1,627 @@ +/******************************************************************************* + + publicsuffixlist.js - an efficient javascript implementation to deal with + Mozilla Foundation's Public Suffix List + + Copyright (C) 2013-present Raymond Hill + + License: pick the one which suits you: + GPL v3 see + APL v2 see + +*/ + +/*! Home: https://github.com/gorhill/publicsuffixlist.js -- GPLv3 APLv2 */ + +/* jshint browser:true, esversion:6, laxbreak:true, undef:true, unused:true */ +/* globals WebAssembly, console, exports:true, module */ + +/******************************************************************************* + + Reference: + https://publicsuffix.org/list/ + + Excerpt: + + > Algorithm + > + > 1. Match domain against all rules and take note of the matching ones. + > 2. If no rules match, the prevailing rule is "*". + > 3. If more than one rule matches, the prevailing rule is the one which + is an exception rule. + > 4. If there is no matching exception rule, the prevailing rule is the + one with the most labels. + > 5. If the prevailing rule is a exception rule, modify it by removing + the leftmost label. + > 6. The public suffix is the set of labels from the domain which match + the labels of the prevailing rule, using the matching algorithm above. + > 7. The registered or registrable domain is the public suffix plus one + additional label. + +*/ + +/******************************************************************************/ + +(function(context) { +// >>>>>>>> start of anonymous namespace + +'use strict'; + +/******************************************************************************* + + Tree encoding in array buffer: + + Node: + + u16: length of array of children + + u8: flags => bit 0: is_publicsuffix, bit 1: is_exception + + u8: length of char data + + u32: char data or offset to char data + + u32: offset to array of children + = 12 bytes + + More bits in flags could be used; for example: + - to distinguish private suffixes + +*/ + + // i32 / i8 +const HOSTNAME_SLOT = 0; // jshint ignore:line +const LABEL_INDICES_SLOT = 256; // -- / 256 +const RULES_PTR_SLOT = 100; // 100 / 400 +const CHARDATA_PTR_SLOT = 101; // 101 / 404 +const EMPTY_STRING = ''; +const SELFIE_MAGIC = 2; + +let wasmMemory; +let pslBuffer32; +let pslBuffer8; +let pslByteLength = 0; +let hostnameArg = EMPTY_STRING; + +/******************************************************************************/ + +const fireChangedEvent = function() { + if ( + window instanceof Object && + window.dispatchEvent instanceof Function && + window.CustomEvent instanceof Function + ) { + window.dispatchEvent(new CustomEvent('publicSuffixListChanged')); + } +}; + +/******************************************************************************/ + +const allocateBuffers = function(byteLength) { + pslByteLength = byteLength + 3 & ~3; + if ( + pslBuffer32 !== undefined && + pslBuffer32.byteLength >= pslByteLength + ) { + return; + } + if ( wasmMemory !== undefined ) { + const newPageCount = pslByteLength + 0xFFFF >>> 16; + const curPageCount = wasmMemory.buffer.byteLength >>> 16; + const delta = newPageCount - curPageCount; + if ( delta > 0 ) { + wasmMemory.grow(delta); + pslBuffer32 = new Uint32Array(wasmMemory.buffer); + pslBuffer8 = new Uint8Array(wasmMemory.buffer); + } + } else { + pslBuffer8 = new Uint8Array(pslByteLength); + pslBuffer32 = new Uint32Array(pslBuffer8.buffer); + } + hostnameArg = ''; + pslBuffer8[LABEL_INDICES_SLOT] = 0; +}; + +/******************************************************************************/ + +// Parse and set a UTF-8 text-based suffix list. Format is same as found at: +// http://publicsuffix.org/list/ +// +// `toAscii` is a converter from unicode to punycode. Required since the +// Public Suffix List contains unicode characters. +// Suggestion: use + +const parse = function(text, toAscii) { + // Use short property names for better minifying results + const rootRule = { + l: EMPTY_STRING, // l => label + f: 0, // f => flags + c: undefined // c => children + }; + + // Tree building + { + const compareLabels = function(a, b) { + let n = a.length; + let d = n - b.length; + if ( d !== 0 ) { return d; } + for ( let i = 0; i < n; i++ ) { + d = a.charCodeAt(i) - b.charCodeAt(i); + if ( d !== 0 ) { return d; } + } + return 0; + }; + + const addToTree = function(rule, exception) { + let node = rootRule; + let end = rule.length; + while ( end > 0 ) { + const beg = rule.lastIndexOf('.', end - 1); + const label = rule.slice(beg + 1, end); + end = beg; + + if ( Array.isArray(node.c) === false ) { + const child = { l: label, f: 0, c: undefined }; + node.c = [ child ]; + node = child; + continue; + } + + let left = 0; + let right = node.c.length; + while ( left < right ) { + const i = left + right >>> 1; + const d = compareLabels(label, node.c[i].l); + if ( d < 0 ) { + right = i; + if ( right === left ) { + const child = { + l: label, + f: 0, + c: undefined + }; + node.c.splice(left, 0, child); + node = child; + break; + } + continue; + } + if ( d > 0 ) { + left = i + 1; + if ( left === right ) { + const child = { + l: label, + f: 0, + c: undefined + }; + node.c.splice(right, 0, child); + node = child; + break; + } + continue; + } + /* d === 0 */ + node = node.c[i]; + break; + } + } + node.f |= 0b01; + if ( exception ) { + node.f |= 0b10; + } + }; + + // 2. If no rules match, the prevailing rule is "*". + addToTree('*', false); + + const mustPunycode = /[^a-z0-9.-]/; + const textEnd = text.length; + let lineBeg = 0; + + while ( lineBeg < textEnd ) { + let lineEnd = text.indexOf('\n', lineBeg); + if ( lineEnd === -1 ) { + lineEnd = text.indexOf('\r', lineBeg); + if ( lineEnd === -1 ) { + lineEnd = textEnd; + } + } + let line = text.slice(lineBeg, lineEnd).trim(); + lineBeg = lineEnd + 1; + + // Ignore comments + const pos = line.indexOf('//'); + if ( pos !== -1 ) { + line = line.slice(0, pos); + } + + // Ignore surrounding whitespaces + line = line.trim(); + if ( line.length === 0 ) { continue; } + + const exception = line.charCodeAt(0) === 0x21 /* '!' */; + if ( exception ) { + line = line.slice(1); + } + + if ( mustPunycode.test(line) ) { + line = toAscii(line.toLowerCase()); + } + + addToTree(line, exception); + } + } + + { + const labelToOffsetMap = new Map(); + const treeData = []; + const charData = []; + + const allocate = function(n) { + const ibuf = treeData.length; + for ( let i = 0; i < n; i++ ) { + treeData.push(0); + } + return ibuf; + }; + + const storeNode = function(ibuf, node) { + const nChars = node.l.length; + const nChildren = node.c !== undefined + ? node.c.length + : 0; + treeData[ibuf+0] = nChildren << 16 | node.f << 8 | nChars; + // char data + if ( nChars <= 4 ) { + let v = 0; + if ( nChars > 0 ) { + v |= node.l.charCodeAt(0); + if ( nChars > 1 ) { + v |= node.l.charCodeAt(1) << 8; + if ( nChars > 2 ) { + v |= node.l.charCodeAt(2) << 16; + if ( nChars > 3 ) { + v |= node.l.charCodeAt(3) << 24; + } + } + } + } + treeData[ibuf+1] = v; + } else { + let offset = labelToOffsetMap.get(node.l); + if ( offset === undefined ) { + offset = charData.length; + for ( let i = 0; i < nChars; i++ ) { + charData.push(node.l.charCodeAt(i)); + } + labelToOffsetMap.set(node.l, offset); + } + treeData[ibuf+1] = offset; + } + // child nodes + if ( Array.isArray(node.c) === false ) { + treeData[ibuf+2] = 0; + return; + } + + const iarray = allocate(nChildren * 3); + treeData[ibuf+2] = iarray; + for ( let i = 0; i < nChildren; i++ ) { + storeNode(iarray + i * 3, node.c[i]); + } + }; + + // First 512 bytes are reserved for internal use + allocate(512 >> 2); + + const iRootRule = allocate(3); + storeNode(iRootRule, rootRule); + treeData[RULES_PTR_SLOT] = iRootRule; + + const iCharData = treeData.length << 2; + treeData[CHARDATA_PTR_SLOT] = iCharData; + + const byteLength = (treeData.length << 2) + (charData.length + 3 & ~3); + allocateBuffers(byteLength); + pslBuffer32.set(treeData); + pslBuffer8.set(charData, treeData.length << 2); + } + + fireChangedEvent(); +}; + +/******************************************************************************/ + +const setHostnameArg = function(hostname) { + const buf = pslBuffer8; + if ( hostname === hostnameArg ) { return buf[LABEL_INDICES_SLOT]; } + if ( hostname === null || hostname.length === 0 ) { + return (buf[LABEL_INDICES_SLOT] = 0); + } + hostname = hostname.toLowerCase(); + hostnameArg = hostname; + let n = hostname.length; + if ( n > 255 ) { n = 255; } + buf[LABEL_INDICES_SLOT] = n; + let i = n; + let j = LABEL_INDICES_SLOT + 1; + while ( i-- ) { + const c = hostname.charCodeAt(i); + if ( c === 0x2E /* '.' */ ) { + buf[j+0] = i + 1; + buf[j+1] = i; + j += 2; + } + buf[i] = c; + } + buf[j] = 0; + return n; +}; + +/******************************************************************************/ + +// Returns an offset to the start of the public suffix. +// +// WASM-able, because no information outside the buffer content is required. + +const getPublicSuffixPosJS = function() { + const buf8 = pslBuffer8; + const buf32 = pslBuffer32; + const iCharData = buf32[CHARDATA_PTR_SLOT]; + + let iNode = pslBuffer32[RULES_PTR_SLOT]; + let cursorPos = -1; + let iLabel = LABEL_INDICES_SLOT; + + // Label-lookup loop + for (;;) { + // Extract label indices + const labelBeg = buf8[iLabel+1]; + const labelLen = buf8[iLabel+0] - labelBeg; + // Match-lookup loop: binary search + let r = buf32[iNode+0] >>> 16; + if ( r === 0 ) { break; } + const iCandidates = buf32[iNode+2]; + let l = 0; + let iFound = 0; + while ( l < r ) { + const iCandidate = l + r >>> 1; + const iCandidateNode = iCandidates + iCandidate + (iCandidate << 1); + const candidateLen = buf32[iCandidateNode+0] & 0x000000FF; + let d = labelLen - candidateLen; + if ( d === 0 ) { + const iCandidateChar = candidateLen <= 4 + ? iCandidateNode + 1 << 2 + : iCharData + buf32[iCandidateNode+1]; + for ( let i = 0; i < labelLen; i++ ) { + d = buf8[labelBeg+i] - buf8[iCandidateChar+i]; + if ( d !== 0 ) { break; } + } + } + if ( d < 0 ) { + r = iCandidate; + } else if ( d > 0 ) { + l = iCandidate + 1; + } else /* if ( d === 0 ) */ { + iFound = iCandidateNode; + break; + } + } + // 2. If no rules match, the prevailing rule is "*". + if ( iFound === 0 ) { + if ( buf8[iCandidates + 1 << 2] !== 0x2A /* '*' */ ) { break; } + iFound = iCandidates; + } + iNode = iFound; + // 5. If the prevailing rule is a exception rule, modify it by + // removing the leftmost label. + if ( (buf32[iNode+0] & 0x00000200) !== 0 ) { + if ( iLabel > LABEL_INDICES_SLOT ) { + return iLabel - 2; + } + break; + } + if ( (buf32[iNode+0] & 0x00000100) !== 0 ) { + cursorPos = iLabel; + } + if ( labelBeg === 0 ) { break; } + iLabel += 2; + } + + return cursorPos; +}; + +let getPublicSuffixPosWASM; +let getPublicSuffixPos = getPublicSuffixPosJS; + +/******************************************************************************/ + +const getPublicSuffix = function(hostname) { + if ( pslBuffer32 === undefined ) { return EMPTY_STRING; } + + const hostnameLen = setHostnameArg(hostname); + const buf8 = pslBuffer8; + if ( hostnameLen === 0 || buf8[0] === 0x2E /* '.' */ ) { + return EMPTY_STRING; + } + + const cursorPos = getPublicSuffixPos(); + if ( cursorPos === -1 ) { + return EMPTY_STRING; + } + + const beg = buf8[cursorPos + 1]; + return beg === 0 ? hostnameArg : hostnameArg.slice(beg); +}; + +/******************************************************************************/ + +const getDomain = function(hostname) { + if ( pslBuffer32 === undefined ) { return EMPTY_STRING; } + + const hostnameLen = setHostnameArg(hostname); + const buf8 = pslBuffer8; + if ( hostnameLen === 0 || buf8[0] === 0x2E /* '.' */ ) { + return EMPTY_STRING; + } + + const cursorPos = getPublicSuffixPos(); + if ( cursorPos === -1 || buf8[cursorPos + 1] === 0 ) { + return EMPTY_STRING; + } + + // 7. The registered or registrable domain is the public suffix plus one + // additional label. + const beg = buf8[cursorPos + 3]; + return beg === 0 ? hostnameArg : hostnameArg.slice(beg); +}; + +/******************************************************************************/ + +const toSelfie = function() { + const selfie = { + magic: SELFIE_MAGIC, + byteLength: pslByteLength, + buffer: pslBuffer32 !== undefined + ? Array.from(new Uint32Array(pslBuffer32.buffer, 0, pslByteLength >>> 2)) + : null, + }; + return selfie; +}; + +const fromSelfie = function(selfie) { + if ( + selfie instanceof Object === false || + selfie.magic !== SELFIE_MAGIC || + typeof selfie.byteLength !== 'number' || + Array.isArray(selfie.buffer) === false + ) { + return false; + } + + allocateBuffers(selfie.byteLength); + pslBuffer32.set(selfie.buffer); + + // Important! + hostnameArg = ''; + pslBuffer8[LABEL_INDICES_SLOT] = 0; + + fireChangedEvent(); + + return true; +}; + +/******************************************************************************/ + +// The WASM module is entirely optional, the JS implementation will be +// used should the WASM module be unavailable for whatever reason. + +const enableWASM = (function() { + // The directory from which the current script was fetched should also + // contain the related WASM file. The script is fetched from a trusted + // location, and consequently so will be the related WASM file. + let workingDir; + { + const url = new URL(document.currentScript.src); + const match = /[^\/]+$/.exec(url.pathname); + if ( match !== null ) { + url.pathname = url.pathname.slice(0, match.index); + } + workingDir = url.href; + } + + let memory; + + return function() { + if ( getPublicSuffixPosWASM instanceof Function ) { + return Promise.resolve(true); + } + + if ( + typeof WebAssembly !== 'object' || + typeof WebAssembly.instantiateStreaming !== 'function' + ) { + return Promise.resolve(false); + } + + // The wasm code will work only if CPU is natively little-endian, + // as we use native uint32 array in our js code. + const uint32s = new Uint32Array(1); + const uint8s = new Uint8Array(uint32s.buffer); + uint32s[0] = 1; + if ( uint8s[0] !== 1 ) { + return Promise.resolve(false); + } + + return fetch( + workingDir + 'wasm/publicsuffixlist.wasm', + { mode: 'same-origin' } + ).then(response => { + const pageCount = pslBuffer8 !== undefined + ? pslBuffer8.byteLength + 0xFFFF >>> 16 + : 1; + memory = new WebAssembly.Memory({ initial: pageCount }); + return WebAssembly.instantiateStreaming( + response, + { imports: { memory: memory } } + ); + }).then(({ instance }) => { + const curPageCount = memory.buffer.byteLength; + const newPageCount = pslBuffer8 !== undefined + ? pslBuffer8.byteLength + 0xFFFF >>> 16 + : 0; + if ( newPageCount > curPageCount ) { + memory.grow(newPageCount - curPageCount); + } + const buf8 = new Uint8Array(memory.buffer); + const buf32 = new Uint32Array(memory.buffer); + if ( pslBuffer32 !== undefined ) { + buf32.set(pslBuffer32); + } + pslBuffer8 = buf8; + pslBuffer32 = buf32; + wasmMemory = memory; + getPublicSuffixPosWASM = instance.exports.getPublicSuffixPos; + getPublicSuffixPos = getPublicSuffixPosWASM; + memory = undefined; + return true; + }).catch(reason => { + console.info(reason); + return false; + }); + }; +})(); + +const disableWASM = function() { + if ( getPublicSuffixPosWASM instanceof Function ) { + getPublicSuffixPos = getPublicSuffixPosJS; + getPublicSuffixPosWASM = undefined; + } + if ( wasmMemory !== undefined ) { + const buf8 = new Uint8Array(pslByteLength); + const buf32 = new Uint32Array(buf8.buffer); + buf32.set(pslBuffer32); + pslBuffer8 = buf8; + pslBuffer32 = buf32; + wasmMemory = undefined; + } +}; + +/******************************************************************************/ + +context = context || window; + +context.publicSuffixList = { + version: '2.0', + parse, + getDomain, + getPublicSuffix, + toSelfie, fromSelfie, + disableWASM, enableWASM, +}; + +if ( typeof module !== 'undefined' ) { + module.exports = context.publicSuffixList; +} else if ( typeof exports !== 'undefined' ) { + exports = context.publicSuffixList; +} + +/******************************************************************************/ + +// <<<<<<<< end of anonymous namespace +})(this); diff --git a/src/lib/publicsuffixlist/wasm/README.md b/src/lib/publicsuffixlist/wasm/README.md new file mode 100644 index 000000000..5c1c4839f --- /dev/null +++ b/src/lib/publicsuffixlist/wasm/README.md @@ -0,0 +1,29 @@ +### For code reviewers + +All `wasm` files in that directory where created by compiling the +corresponding `wat` file using the command (using +`publicsuffixlist.wat`/`publicsuffixlist.wasm` as example): + + wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm + +Assuming: + +- The command is executed from within the present directory. + +### `wat2wasm` tool + +The `wat2wasm` tool can be downloaded from an official WebAssembly project: +. + +### `wat2wasm` tool online + +You can also use the following online `wat2wasm` tool: +. + +Just paste the whole content of the `wat` file to compile into the WAT pane. +Click "Download" button to retrieve the resulting `wasm` file. + +### See also + +For the curious, the following online tool allows you to find out the machine +code as a result from the WASM code: https://mbebenita.github.io/WasmExplorer/ diff --git a/src/lib/publicsuffixlist/wasm/publicsuffixlist.wasm b/src/lib/publicsuffixlist/wasm/publicsuffixlist.wasm new file mode 100644 index 000000000..40e76e650 Binary files /dev/null and b/src/lib/publicsuffixlist/wasm/publicsuffixlist.wasm differ diff --git a/src/lib/publicsuffixlist/wasm/publicsuffixlist.wat b/src/lib/publicsuffixlist/wasm/publicsuffixlist.wat new file mode 100644 index 000000000..7ae2942ba --- /dev/null +++ b/src/lib/publicsuffixlist/wasm/publicsuffixlist.wat @@ -0,0 +1,317 @@ +;; +;; uBlock Origin - a browser extension to block requests. +;; Copyright (C) 2019-present Raymond Hill +;; +;; License: pick the one which suits you: +;; GPL v3 see +;; APL v2 see +;; +;; Home: https://github.com/gorhill/publicsuffixlist.js +;; File: publicsuffixlist.wat +;; +;; Description: WebAssembly implementation for core lookup method in +;; publicsuffixlist.js +;; +;; How to compile: +;; +;; wat2wasm publicsuffixlist.wat -o publicsuffixlist.wasm +;; +;; The `wat2wasm` tool can be downloaded from an official WebAssembly +;; project: +;; https://github.com/WebAssembly/wabt/releases + + +(module +;; +;; module start +;; + +(memory (import "imports" "memory") 1) + +;; +;; Tree encoding in array buffer: +;; +;; Node: +;; + u8: length of char data +;; + u8: flags => bit 0: is_publicsuffix, bit 1: is_exception +;; + u16: length of array of children +;; + u32: char data or offset to char data +;; + u32: offset to array of children +;; = 12 bytes +;; +;; // i32 / i8 +;; const HOSTNAME_SLOT = 0; // jshint ignore:line +;; const LABEL_INDICES_SLOT = 256; // -- / 256 +;; const RULES_PTR_SLOT = 100; // 100 / 400 +;; const CHARDATA_PTR_SLOT = 101; // 101 / 404 +;; const EMPTY_STRING = ''; +;; const SELFIE_MAGIC = 2; +;; + +;; +;; Public functions +;; + +;; +;; unsigned int getPublicSuffixPos() +;; +;; Returns an offset to the start of the public suffix. +;; +(func (export "getPublicSuffixPos") + (result i32) ;; result = match index, -1 = miss + (local $iCharData i32) ;; offset to start of character data + (local $iNode i32) ;; offset to current node + (local $iLabel i32) ;; offset to label indices + (local $cursorPos i32) ;; position of cursor within hostname argument + (local $labelBeg i32) + (local $labelLen i32) + (local $nCandidates i32) + (local $iCandidates i32) + (local $iFound i32) + (local $l i32) + (local $r i32) + (local $d i32) + (local $iCandidate i32) + (local $iCandidateNode i32) + (local $candidateLen i32) + (local $iCandidateChar i32) + (local $_1 i32) + (local $_2 i32) + (local $_3 i32) + ;; + ;; const iCharData = buf32[CHARDATA_PTR_SLOT]; + i32.const 404 + i32.load + set_local $iCharData + ;; let iNode = pslBuffer32[RULES_PTR_SLOT]; + i32.const 400 + i32.load + i32.const 2 + i32.shl + set_local $iNode + ;; let iLabel = LABEL_INDICES_SLOT; + i32.const 256 + set_local $iLabel + ;; let cursorPos = -1; + i32.const -1 + set_local $cursorPos + ;; label-lookup loop + ;; for (;;) { + block $labelLookupDone loop $labelLookup + ;; // Extract label indices + ;; const labelBeg = buf8[iLabel+1]; + ;; const labelLen = buf8[iLabel+0] - labelBeg; + get_local $iLabel + i32.load8_u + get_local $iLabel + i32.load8_u offset=1 + tee_local $labelBeg + i32.sub + set_local $labelLen + ;; // Match-lookup loop: binary search + ;; let r = buf32[iNode+0] >>> 16; + ;; if ( r === 0 ) { break; } + get_local $iNode + i32.load16_u offset=2 + tee_local $r + i32.eqz + br_if $labelLookupDone + ;; const iCandidates = buf32[iNode+2]; + get_local $iNode + i32.load offset=8 + i32.const 2 + i32.shl + set_local $iCandidates + ;; let l = 0; + ;; let iFound = 0; + i32.const 0 + tee_local $l + set_local $iFound + ;; while ( l < r ) { + block $binarySearchDone loop $binarySearch + get_local $l + get_local $r + i32.ge_u + br_if $binarySearchDone + ;; const iCandidate = l + r >>> 1; + get_local $l + get_local $r + i32.add + i32.const 1 + i32.shr_u + tee_local $iCandidate + ;; const iCandidateNode = iCandidates + iCandidate + (iCandidate << 1); + i32.const 2 + i32.shl + tee_local $_1 + get_local $_1 + i32.const 1 + i32.shl + i32.add + get_local $iCandidates + i32.add + tee_local $iCandidateNode + ;; const candidateLen = buf32[iCandidateNode+0] & 0x000000FF; + i32.load8_u + set_local $candidateLen + ;; let d = labelLen - candidateLen; + get_local $labelLen + get_local $candidateLen + i32.sub + tee_local $d + ;; if ( d === 0 ) { + i32.eqz + if + ;; const iCandidateChar = candidateLen <= 4 + get_local $candidateLen + i32.const 4 + i32.le_u + if + ;; ? iCandidateNode + 1 << 2 + get_local $iCandidateNode + i32.const 4 + i32.add + set_local $iCandidateChar + else + ;; : buf32[CHARDATA_PTR_SLOT] + buf32[iCandidateNode+1]; + get_local $iCharData + get_local $iCandidateNode + i32.load offset=4 + i32.add + set_local $iCandidateChar + end + ;; for ( let i = 0; i < labelLen; i++ ) { + get_local $labelBeg + tee_local $_1 + get_local $labelLen + i32.add + set_local $_3 + get_local $iCandidateChar + set_local $_2 + block $findDiffDone loop $findDiff + ;; d = buf8[labelBeg+i] - buf8[iCandidateChar+i]; + ;; if ( d !== 0 ) { break; } + get_local $_1 + i32.load8_u + get_local $_2 + i32.load8_u + i32.sub + tee_local $d + br_if $findDiffDone + get_local $_1 + i32.const 1 + i32.add + tee_local $_1 + get_local $_3 + i32.eq + br_if $findDiffDone + get_local $_2 + i32.const 1 + i32.add + set_local $_2 + br $findDiff + ;; } + end end + ;; } + end + ;; if ( d < 0 ) { + ;; r = iCandidate; + get_local $d + i32.const 0 + i32.lt_s + if + get_local $iCandidate + set_local $r + br $binarySearch + end + ;; } else if ( d > 0 ) { + ;; l = iCandidate + 1; + get_local $d + i32.const 0 + i32.gt_s + if + get_local $iCandidate + i32.const 1 + i32.add + set_local $l + br $binarySearch + end + ;; } else /* if ( d === 0 ) */ { + ;; iFound = iCandidateNode; + ;; break; + ;; } + get_local $iCandidateNode + set_local $iFound + end end + ;; } + ;; // 2. If no rules match, the prevailing rule is "*". + ;; if ( iFound === 0 ) { + ;; if ( buf8[iCandidates + 1 << 2] !== 0x2A /* '*' */ ) { break; } + ;; iFound = iCandidates; + ;; } + get_local $iFound + i32.eqz + if + get_local $iCandidates + i32.load8_u offset=4 + i32.const 0x2A + i32.ne + br_if $labelLookupDone + get_local $iCandidates + set_local $iFound + end + ;; iNode = iFound; + get_local $iFound + tee_local $iNode + ;; // 5. If the prevailing rule is a exception rule, modify it by + ;; // removing the leftmost label. + ;; if ( (buf32[iNode+0] & 0x00000200) !== 0 ) { + ;; if ( iLabel > LABEL_INDICES_SLOT ) { + ;; return iLabel - 2; + ;; } + ;; break; + ;; } + i32.load8_u offset=1 + tee_local $_1 + i32.const 0x02 + i32.and + if + get_local $iLabel + i32.const 256 + i32.gt_u + if + get_local $iLabel + i32.const -2 + i32.add + return + end + br $labelLookupDone + end + ;; if ( (buf32[iNode+0] & 0x00000100) !== 0 ) { + ;; cursorPos = labelBeg; + ;; } + get_local $_1 + i32.const 0x01 + i32.and + if + get_local $iLabel + set_local $cursorPos + end + ;; if ( labelBeg === 0 ) { break; } + get_local $labelBeg + i32.eqz + br_if $labelLookupDone + ;; iLabel += 2; + get_local $iLabel + i32.const 2 + i32.add + set_local $iLabel + br $labelLookup + end end + get_local $cursorPos +) + +;; +;; module end +;; +)