mirror of
https://github.com/SukkaW/Surge.git
synced 2025-12-21 13:50:29 +08:00
540 lines
21 KiB
JavaScript
540 lines
21 KiB
JavaScript
'use strict';const misc=require('./misc.BpcQjda1.cjs'),require$$1=require('node:util'),require$$2=require('foxts/noop'),require$$3=require('foxts/fast-string-array-join'),require$$4=require('foxts/bitwise');var trie = {};/**
|
|
* Hostbane-Optimized Trie based on Mnemonist Trie
|
|
*/
|
|
|
|
var hasRequiredTrie;
|
|
|
|
function requireTrie () {
|
|
if (hasRequiredTrie) return trie;
|
|
hasRequiredTrie = 1;
|
|
(function (exports) {
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
function _export(target, all) {
|
|
for(var name in all)Object.defineProperty(target, name, {
|
|
enumerable: true,
|
|
get: all[name]
|
|
});
|
|
}
|
|
_export(exports, {
|
|
HostnameSmolTrie: function() {
|
|
return HostnameSmolTrie;
|
|
},
|
|
HostnameTrie: function() {
|
|
return HostnameTrie;
|
|
}
|
|
});
|
|
const _misc = /*@__PURE__*/ misc.r();
|
|
const _nodeutil = /*#__PURE__*/ _interop_require_default(require$$1);
|
|
const _noop = require$$2;
|
|
const _faststringarrayjoin = require$$3;
|
|
const _bitwise = require$$4;
|
|
function _interop_require_default(obj) {
|
|
return obj && obj.__esModule ? obj : {
|
|
default: obj
|
|
};
|
|
}
|
|
var _computedKey;
|
|
const START = 1 << 1;
|
|
const INCLUDE_ALL_SUBDOMAIN = 1 << 2;
|
|
function deepTrieNodeToJSON(node, unpackMeta) {
|
|
const obj = {};
|
|
obj['[start]'] = (0, _bitwise.getBit)(node[0], START);
|
|
obj['[subdomain]'] = (0, _bitwise.getBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
if (node[3] != null) {
|
|
if (unpackMeta) {
|
|
obj['[meta]'] = unpackMeta(node[3]);
|
|
} else {
|
|
obj['[meta]'] = node[3];
|
|
}
|
|
}
|
|
node[2].forEach((value, key)=>{
|
|
obj[key] = deepTrieNodeToJSON(value, unpackMeta);
|
|
});
|
|
return obj;
|
|
}
|
|
const createNode = (parent = null)=>[
|
|
1,
|
|
parent,
|
|
new Map(),
|
|
null
|
|
];
|
|
function hostnameToTokens(hostname, hostnameFromIndex) {
|
|
const tokens = hostname.split('.');
|
|
const results = [];
|
|
let token = '';
|
|
for(let i = hostnameFromIndex, l = tokens.length; i < l; i++){
|
|
token = tokens[i];
|
|
if (token.length > 0) {
|
|
results.push(token);
|
|
} else {
|
|
throw new TypeError(JSON.stringify({
|
|
hostname,
|
|
hostnameFromIndex
|
|
}, null, 2));
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
function walkHostnameTokens(hostname, onToken, hostnameFromIndex) {
|
|
const tokens = hostname.split('.');
|
|
const l = tokens.length - 1;
|
|
// we are at the first of hostname, no splitor there
|
|
let token = '';
|
|
for(let i = l; i >= hostnameFromIndex; i--){
|
|
token = tokens[i];
|
|
if (token.length > 0) {
|
|
const t = onToken(token);
|
|
if (t === null) {
|
|
return null;
|
|
}
|
|
// if the callback returns true, we should skip the rest
|
|
if (t) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
_computedKey = _nodeutil.default.inspect.custom;
|
|
class Triebase {
|
|
$root = createNode();
|
|
$size = 0;
|
|
get root() {
|
|
return this.$root;
|
|
}
|
|
constructor(from){
|
|
// Actually build trie
|
|
if (Array.isArray(from)) {
|
|
for(let i = 0, l = from.length; i < l; i++){
|
|
this.add(from[i]);
|
|
}
|
|
} else if (from) {
|
|
from.forEach((value)=>this.add(value));
|
|
}
|
|
}
|
|
walkIntoLeafWithTokens(tokens, onLoop = _noop.noop) {
|
|
let node = this.$root;
|
|
let parent = node;
|
|
let token;
|
|
let child = node[2];
|
|
// reverse lookup from end to start
|
|
for(let i = tokens.length - 1; i >= 0; i--){
|
|
token = tokens[i];
|
|
// if (token === '') {
|
|
// break;
|
|
// }
|
|
parent = node;
|
|
child = node[2];
|
|
// cache node index access is 20% faster than direct access when doing twice
|
|
if (child.has(token)) {
|
|
node = child.get(token);
|
|
} else {
|
|
return null;
|
|
}
|
|
onLoop(node, parent, token);
|
|
}
|
|
return {
|
|
node,
|
|
parent
|
|
};
|
|
}
|
|
walkIntoLeafWithSuffix(suffix, hostnameFromIndex, onLoop = _noop.noop) {
|
|
let node = this.$root;
|
|
let parent = node;
|
|
let child = node[2];
|
|
const onToken = (token)=>{
|
|
// if (token === '') {
|
|
// return true;
|
|
// }
|
|
parent = node;
|
|
child = node[2];
|
|
if (child.has(token)) {
|
|
node = child.get(token);
|
|
} else {
|
|
return null;
|
|
}
|
|
onLoop(node, parent, token);
|
|
return false;
|
|
};
|
|
if (walkHostnameTokens(suffix, onToken, hostnameFromIndex) === null) {
|
|
return null;
|
|
}
|
|
return {
|
|
node,
|
|
parent
|
|
};
|
|
}
|
|
contains(suffix, includeAllSubdomain = suffix[0] === '.') {
|
|
const hostnameFromIndex = suffix[0] === '.' ? 1 : 0;
|
|
const res = this.walkIntoLeafWithSuffix(suffix, hostnameFromIndex);
|
|
if (!res) return false;
|
|
if (includeAllSubdomain) return (0, _bitwise.getBit)(res.node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
return true;
|
|
}
|
|
static bfsResults = [
|
|
null,
|
|
[]
|
|
];
|
|
static dfs(nodeStack, suffixStack) {
|
|
const node = nodeStack.pop();
|
|
const suffix = suffixStack.pop();
|
|
node[2].forEach((childNode, k)=>{
|
|
// Pushing the child node to the stack for next iteration of DFS
|
|
nodeStack.push(childNode);
|
|
suffixStack.push([
|
|
k,
|
|
...suffix
|
|
]);
|
|
});
|
|
Triebase.bfsResults[0] = node;
|
|
Triebase.bfsResults[1] = suffix;
|
|
return Triebase.bfsResults;
|
|
}
|
|
static dfsWithSort(nodeStack, suffixStack) {
|
|
const node = nodeStack.pop();
|
|
const suffix = suffixStack.pop();
|
|
const child = node[2];
|
|
if (child.size) {
|
|
const keys = Array.from(child.keys()).sort(Triebase.compare);
|
|
for(let i = 0, l = keys.length; i < l; i++){
|
|
const key = keys[i];
|
|
const childNode = child.get(key);
|
|
// Pushing the child node to the stack for next iteration of DFS
|
|
nodeStack.push(childNode);
|
|
suffixStack.push([
|
|
key,
|
|
...suffix
|
|
]);
|
|
}
|
|
}
|
|
Triebase.bfsResults[0] = node;
|
|
Triebase.bfsResults[1] = suffix;
|
|
return Triebase.bfsResults;
|
|
}
|
|
walk(onMatches, initialNode = this.$root, initialSuffix = [], withSort = false) {
|
|
const bfsImpl = withSort ? Triebase.dfsWithSort : Triebase.dfs;
|
|
const nodeStack = [];
|
|
nodeStack.push(initialNode);
|
|
// Resolving initial string (begin the start of the stack)
|
|
const suffixStack = [];
|
|
suffixStack.push(initialSuffix);
|
|
let node = initialNode;
|
|
let r;
|
|
do {
|
|
r = bfsImpl(nodeStack, suffixStack);
|
|
node = r[0];
|
|
const suffix = r[1];
|
|
// If the node is a sentinel, we push the suffix to the results
|
|
if ((0, _bitwise.getBit)(node[0], START)) {
|
|
onMatches(suffix, (0, _bitwise.getBit)(node[0], INCLUDE_ALL_SUBDOMAIN), node[3]);
|
|
}
|
|
}while (nodeStack.length)
|
|
}
|
|
static compare(a, b) {
|
|
if (a === b) return 0;
|
|
return a.length - b.length || (0, _misc.fastStringCompare)(a, b);
|
|
}
|
|
walkWithSort(onMatches, initialNode = this.$root, initialSuffix = []) {
|
|
const nodeStack = [];
|
|
nodeStack.push(initialNode);
|
|
// Resolving initial string (begin the start of the stack)
|
|
const suffixStack = [];
|
|
suffixStack.push(initialSuffix);
|
|
let node = initialNode;
|
|
let child = node[2];
|
|
do {
|
|
node = nodeStack.pop();
|
|
const suffix = suffixStack.pop();
|
|
child = node[2];
|
|
if (child.size) {
|
|
const keys = Array.from(child.keys()).sort(Triebase.compare);
|
|
for(let i = 0, l = keys.length; i < l; i++){
|
|
const key = keys[i];
|
|
const childNode = child.get(key);
|
|
// Pushing the child node to the stack for next iteration of DFS
|
|
nodeStack.push(childNode);
|
|
suffixStack.push([
|
|
key,
|
|
...suffix
|
|
]);
|
|
}
|
|
}
|
|
// If the node is a sentinel, we push the suffix to the results
|
|
if ((0, _bitwise.getBit)(node[0], START)) {
|
|
onMatches(suffix, (0, _bitwise.getBit)(node[0], INCLUDE_ALL_SUBDOMAIN), node[3]);
|
|
}
|
|
}while (nodeStack.length)
|
|
}
|
|
getSingleChildLeaf(tokens) {
|
|
let toPrune = null;
|
|
let tokenToPrune = null;
|
|
const onLoop = (node, parent, token)=>{
|
|
// Keeping track of a potential branch to prune
|
|
const child = node[2];
|
|
// console.log({
|
|
// child, parent, token
|
|
// });
|
|
// console.log(this.inspect(0));
|
|
if (toPrune !== null) {
|
|
if (child.size > 1) {
|
|
// The branch has some children, the branch need retain.
|
|
// And we need to abort prune that parent branch, so we set it to null
|
|
toPrune = null;
|
|
tokenToPrune = null;
|
|
}
|
|
} else if (child.size < 1) {
|
|
// There is only one token child, or no child at all, we can prune it safely
|
|
// It is now the top-est branch that could potentially being pruned
|
|
toPrune = parent;
|
|
tokenToPrune = token;
|
|
}
|
|
};
|
|
const res = this.walkIntoLeafWithTokens(tokens, onLoop);
|
|
if (res === null) return null;
|
|
return {
|
|
node: res.node,
|
|
toPrune,
|
|
tokenToPrune,
|
|
parent: res.parent
|
|
};
|
|
}
|
|
/**
|
|
* Method used to retrieve every item in the trie with the given prefix.
|
|
*/ find(inputSuffix, subdomainOnly = inputSuffix[0] === '.', hostnameFromIndex = inputSuffix[0] === '.' ? 1 : 0) {
|
|
const inputTokens = hostnameToTokens(inputSuffix, hostnameFromIndex);
|
|
const res = this.walkIntoLeafWithTokens(inputTokens);
|
|
if (res === null) return [];
|
|
const results = [];
|
|
const onMatches = subdomainOnly ? (suffix, subdomain)=>{
|
|
const d = (0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.');
|
|
if (!subdomain && subStringEqual(inputSuffix, d, 1)) return;
|
|
results.push(subdomain ? '.' + d : d);
|
|
} : (suffix, subdomain)=>{
|
|
const d = (0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.');
|
|
results.push(subdomain ? '.' + d : d);
|
|
};
|
|
this.walk(onMatches, res.node, inputTokens);
|
|
return results;
|
|
}
|
|
/**
|
|
* Method used to delete a prefix from the trie.
|
|
*/ remove(suffix) {
|
|
const res = this.getSingleChildLeaf(hostnameToTokens(suffix, 0));
|
|
if (res === null) return false;
|
|
if ((0, _bitwise.missingBit)(res.node[0], START)) return false;
|
|
this.$size--;
|
|
const { node, toPrune, tokenToPrune } = res;
|
|
if (tokenToPrune && toPrune) {
|
|
toPrune[2].delete(tokenToPrune);
|
|
} else {
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], START);
|
|
}
|
|
return true;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/unbound-method -- safe
|
|
delete = this.remove;
|
|
/**
|
|
* Method used to assert whether the given prefix exists in the Trie.
|
|
*/ has(suffix, includeAllSubdomain = suffix[0] === '.') {
|
|
const hostnameFromIndex = suffix[0] === '.' ? 1 : 0;
|
|
const res = this.walkIntoLeafWithSuffix(suffix, hostnameFromIndex);
|
|
if (res === null) return false;
|
|
if ((0, _bitwise.missingBit)(res.node[0], START)) return false;
|
|
if (includeAllSubdomain) return (0, _bitwise.getBit)(res.node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
return true;
|
|
}
|
|
dumpWithoutDot(onSuffix, withSort = false) {
|
|
const handleSuffix = (suffix, subdomain)=>{
|
|
onSuffix((0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.'), subdomain);
|
|
};
|
|
if (withSort) {
|
|
this.walkWithSort(handleSuffix);
|
|
} else {
|
|
this.walk(handleSuffix);
|
|
}
|
|
}
|
|
dump(onSuffix, withSort = false) {
|
|
const results = [];
|
|
const handleSuffix = onSuffix ? (suffix, subdomain)=>{
|
|
const d = (0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.');
|
|
onSuffix(subdomain ? '.' + d : d);
|
|
} : (suffix, subdomain)=>{
|
|
const d = (0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.');
|
|
results.push(subdomain ? '.' + d : d);
|
|
};
|
|
if (withSort) {
|
|
this.walkWithSort(handleSuffix);
|
|
} else {
|
|
this.walk(handleSuffix);
|
|
}
|
|
return results;
|
|
}
|
|
dumpMeta(onMeta, withSort = false) {
|
|
const results = [];
|
|
const handleMeta = onMeta ? (_suffix, _subdomain, meta)=>onMeta(meta) : (_suffix, _subdomain, meta)=>results.push(meta);
|
|
if (withSort) {
|
|
this.walkWithSort(handleMeta);
|
|
} else {
|
|
this.walk(handleMeta);
|
|
}
|
|
return results;
|
|
}
|
|
dumpWithMeta(onSuffix, withSort = false) {
|
|
const results = [];
|
|
const handleSuffix = onSuffix ? (suffix, subdomain, meta)=>{
|
|
const d = (0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.');
|
|
return onSuffix(subdomain ? '.' + d : d, meta);
|
|
} : (suffix, subdomain, meta)=>{
|
|
const d = (0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.');
|
|
results.push([
|
|
subdomain ? '.' + d : d,
|
|
meta
|
|
]);
|
|
};
|
|
if (withSort) {
|
|
this.walkWithSort(handleSuffix);
|
|
} else {
|
|
this.walk(handleSuffix);
|
|
}
|
|
return results;
|
|
}
|
|
inspect(depth, unpackMeta) {
|
|
return (0, _faststringarrayjoin.fastStringArrayJoin)(JSON.stringify(deepTrieNodeToJSON(this.$root, unpackMeta), null, 2).split('\n').map((line)=>' '.repeat(depth) + line), '\n');
|
|
}
|
|
[_computedKey](depth) {
|
|
return this.inspect(depth);
|
|
}
|
|
merge(trie) {
|
|
const handleSuffix = (suffix, subdomain, meta)=>{
|
|
this.add((0, _faststringarrayjoin.fastStringArrayJoin)(suffix, '.'), subdomain, meta);
|
|
};
|
|
trie.walk(handleSuffix);
|
|
return this;
|
|
}
|
|
}
|
|
class HostnameSmolTrie extends Triebase {
|
|
smolTree = true;
|
|
add(suffix, includeAllSubdomain = suffix[0] === '.', meta, hostnameFromIndex = suffix[0] === '.' ? 1 : 0) {
|
|
let node = this.$root;
|
|
let curNodeChildren = node[2];
|
|
const onToken = (token)=>{
|
|
curNodeChildren = node[2];
|
|
if (curNodeChildren.has(token)) {
|
|
node = curNodeChildren.get(token);
|
|
// During the adding of `[start]blog|.skk.moe` and find out that there is a `[start].skk.moe` in the trie, skip adding the rest of the node
|
|
if ((0, _bitwise.getBit)(node[0], INCLUDE_ALL_SUBDOMAIN)) {
|
|
return true;
|
|
}
|
|
} else {
|
|
const newNode = createNode(node);
|
|
curNodeChildren.set(token, newNode);
|
|
node = newNode;
|
|
}
|
|
return false;
|
|
};
|
|
// When walkHostnameTokens returns true, we should skip the rest
|
|
if (walkHostnameTokens(suffix, onToken, hostnameFromIndex)) {
|
|
return;
|
|
}
|
|
// If we are in smolTree mode, we need to do something at the end of the loop
|
|
if (includeAllSubdomain) {
|
|
// Trying to add `[.]sub.example.com` where there is already a `blog.sub.example.com` in the trie
|
|
// Make sure parent `[start]sub.example.com` (without dot) is removed (SETINEL to false)
|
|
// (/** parent */ node[2]!)[0] = false;
|
|
// Removing the rest of the parent's child nodes
|
|
node[2].clear();
|
|
// The SENTINEL of this node will be set to true at the end of the function, so we don't need to set it here
|
|
// we can use else-if here, because the children is now empty, we don't need to check the leading "."
|
|
} else if ((0, _bitwise.getBit)(node[0], INCLUDE_ALL_SUBDOMAIN)) {
|
|
// Trying to add `example.com` when there is already a `.example.com` in the trie
|
|
// No need to increment size and set SENTINEL to true (skip this "new" item)
|
|
return;
|
|
}
|
|
node[0] = (0, _bitwise.setBit)(node[0], START);
|
|
if (includeAllSubdomain) {
|
|
node[0] = (0, _bitwise.setBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
} else {
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
}
|
|
node[3] = meta;
|
|
}
|
|
whitelist(suffix, includeAllSubdomain = suffix[0] === '.', hostnameFromIndex = suffix[0] === '.' ? 1 : 0) {
|
|
const tokens = hostnameToTokens(suffix, hostnameFromIndex);
|
|
const res = this.getSingleChildLeaf(tokens);
|
|
if (res === null) return;
|
|
const { node, toPrune, tokenToPrune } = res;
|
|
// Trying to whitelist `[start].sub.example.com` where there might already be a `[start]blog.sub.example.com` in the trie
|
|
if (includeAllSubdomain) {
|
|
// If there is a `[start]sub.example.com` here, remove it
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], START);
|
|
// Removing all the child nodes by empty the children
|
|
node[2].clear();
|
|
} else {
|
|
// Trying to whitelist `example.com` when there is already a `.example.com` in the trie
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
}
|
|
// return early if not found
|
|
if ((0, _bitwise.missingBit)(node[0], START)) return;
|
|
if (toPrune && tokenToPrune) {
|
|
toPrune[2].delete(tokenToPrune);
|
|
} else {
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], START);
|
|
}
|
|
}
|
|
}
|
|
class HostnameTrie extends Triebase {
|
|
get size() {
|
|
return this.$size;
|
|
}
|
|
add(suffix, includeAllSubdomain = suffix[0] === '.', meta, hostnameFromIndex = suffix[0] === '.' ? 1 : 0) {
|
|
let node = this.$root;
|
|
let child = node[2];
|
|
const onToken = (token)=>{
|
|
child = node[2];
|
|
if (child.has(token)) {
|
|
node = child.get(token);
|
|
} else {
|
|
const newNode = createNode(node);
|
|
child.set(token, newNode);
|
|
node = newNode;
|
|
}
|
|
return false;
|
|
};
|
|
// When walkHostnameTokens returns true, we should skip the rest
|
|
if (walkHostnameTokens(suffix, onToken, hostnameFromIndex)) {
|
|
return;
|
|
}
|
|
// if same entry has been added before, skip
|
|
if ((0, _bitwise.getBit)(node[0], START)) {
|
|
return;
|
|
}
|
|
this.$size++;
|
|
node[0] = (0, _bitwise.setBit)(node[0], START);
|
|
if (includeAllSubdomain) {
|
|
node[0] = (0, _bitwise.setBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
} else {
|
|
node[0] = (0, _bitwise.deleteBit)(node[0], INCLUDE_ALL_SUBDOMAIN);
|
|
}
|
|
node[3] = meta;
|
|
}
|
|
}
|
|
// function deepEqualArray(a: string[], b: string[]) {
|
|
// let len = a.length;
|
|
// if (len !== b.length) return false;
|
|
// while (len--) {
|
|
// if (a[len] !== b[len]) return false;
|
|
// }
|
|
// return true;
|
|
// };
|
|
function subStringEqual(needle, haystack, needleIndex = 0) {
|
|
for(let i = 0, l = haystack.length; i < l; i++){
|
|
if (needle[i + needleIndex] !== haystack[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
} (trie));
|
|
return trie;
|
|
}exports.r=requireTrie; |