SukkaW d354c5e988
Some checks are pending
Build / Build (push) Waiting to run
Build / Diff output (push) Blocked by required conditions
Build / Deploy to Cloudflare Pages (push) Blocked by required conditions
Build / Deploy to GitHub and GitLab (push) Blocked by required conditions
Chore: maintainance
2025-02-07 17:52:36 +08:00

540 lines
21 KiB
JavaScript

'use strict';Object.defineProperty(exports,Symbol.toStringTag,{value:'Module'});const trie=require('../../_virtual/trie.cjs'),misc=require('./misc.cjs'),require$$1=require('node:util'),require$$2=require('foxts/noop'),require$$3=require('foxts/fast-string-array-join'),require$$4=require('foxts/bitwise');/**
* Hostbane-Optimized Trie based on Mnemonist Trie
*/
var hasRequiredTrie;
function requireTrie () {
if (hasRequiredTrie) return trie.__exports;
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.__require();
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.__exports));
return trie.__exports;
}exports.__require=requireTrie;