rsshub
Version:
Make RSS Great Again!
275 lines (274 loc) • 13.1 kB
JavaScript
import { parse } from "acorn";
//#region lib/utils/parse-script-data.ts
const globalNames = /* @__PURE__ */ new Set([
"window",
"globalThis",
"self"
]);
const unsafeKeys = /* @__PURE__ */ new Set([
"__proto__",
"prototype",
"constructor"
]);
const maxScriptLength = 2e6;
const maxSteps = 1e5;
const maxDepth = 100;
var ScriptDataError = class extends Error {};
var UnsafePropertyError = class extends ScriptDataError {};
const propertyKey = (value) => {
if (typeof value !== "string" && typeof value !== "number") throw new ScriptDataError("Script data property keys must be strings or numbers");
const key = String(value);
if (unsafeKeys.has(key)) throw new UnsafePropertyError(`Unsafe script data property: ${key}`);
return key;
};
const staticPath = (node) => {
if (node.type === "Identifier") return [propertyKey(node.name)];
if (node.type === "MemberExpression" && !node.optional) {
const key = !node.computed && node.property.type === "Identifier" ? node.property.name : node.property.type === "Literal" ? node.property.value : void 0;
return [...staticPath(node.object), propertyKey(key)];
}
throw new ScriptDataError("Script data targets must be static property paths");
};
const normalizePath = (path) => globalNames.has(path[0]) ? path.slice(1) : path;
const targetPath = (target) => {
const program = parse(target, { ecmaVersion: "latest" });
if (program.body.length !== 1 || program.body[0].type !== "ExpressionStatement") throw new ScriptDataError("Script data targets must be a single static property path");
const path = normalizePath(staticPath(program.body[0].expression));
if (!path.length) throw new ScriptDataError("Script data targets must name a property");
return path;
};
const isObject = (value) => typeof value === "object" && value !== null && !(value instanceof ScriptDataError);
const readProperty = (object, key) => {
if (object instanceof ScriptDataError) throw object;
if (!isObject(object)) throw new ScriptDataError("Cannot read a property of non-object script data");
return Object.hasOwn(object, key) ? object[key] : void 0;
};
/** Reads serialized data with a limited AST vocabulary; it never executes JavaScript. */
var ScriptDataReader = class {
target;
argumentIndex;
root = { values: Object.create(null) };
steps = 0;
poisonedAssignments = /* @__PURE__ */ new WeakSet();
captured = false;
callbackValue;
constructor(target, argumentIndex) {
this.target = target;
this.argumentIndex = argumentIndex;
}
step(depth) {
if (++this.steps > maxSteps || depth > maxDepth) throw new UnsafePropertyError("Script data exceeds the parsing complexity limit");
}
identifier(name, scope) {
propertyKey(name);
for (let current = scope; current; current = current.parent) if (Object.hasOwn(current.values, name)) {
const value = current.values[name];
if (value instanceof ScriptDataError) throw value;
return value;
}
if (globalNames.has(name)) return this.root.values;
if (name === "undefined") return;
throw new ScriptDataError(`Unknown script data variable: ${name}`);
}
reference(node, scope, depth) {
if (node.type === "Identifier") {
const key = propertyKey(node.name);
if (globalNames.has(key)) throw new ScriptDataError("Cannot replace script data global aliases");
let current = scope;
while (current.parent && !Object.hasOwn(current.values, key)) current = current.parent;
return {
object: current.values,
key
};
}
if (node.type !== "MemberExpression" || node.optional) throw new ScriptDataError("Unsupported script data assignment target");
const object = this.evaluate(node.object, scope, depth + 1);
const key = propertyKey(!node.computed && node.property.type === "Identifier" ? node.property.name : this.evaluate(node.property, scope, depth + 1));
if (!isObject(object)) throw new ScriptDataError("Cannot assign a property of non-object script data");
if (Array.isArray(object) && (!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= maxSteps)) throw new ScriptDataError("Script data arrays require bounded numeric indexes");
return {
object,
key
};
}
evaluate(node, scope, depth) {
this.step(depth);
switch (node.type) {
case "Literal":
if (node.regex || node.bigint) break;
return node.value;
case "Identifier": return this.identifier(node.name, scope);
case "ArrayExpression": return node.elements.map((element) => element ? this.evaluate(element, scope, depth + 1) : void 0);
case "ObjectExpression": {
const value = Object.create(null);
for (const property of node.properties) {
if (property.type !== "Property" || property.kind !== "init" || property.method) throw new ScriptDataError("Only ordinary script data object properties are supported");
const key = propertyKey(!property.computed && property.key.type === "Identifier" ? property.key.name : this.evaluate(property.key, scope, depth + 1));
value[key] = this.evaluate(property.value, scope, depth + 1);
}
return value;
}
case "MemberExpression": {
const key = propertyKey(!node.computed && node.property.type === "Identifier" ? node.property.name : this.evaluate(node.property, scope, depth + 1));
return readProperty(this.evaluate(node.object, scope, depth + 1), key);
}
case "UnaryExpression": {
const value = this.evaluate(node.argument, scope, depth + 1);
if (node.operator === "void") return;
if (node.operator === "!") return !value;
if (typeof value === "number" && (node.operator === "-" || node.operator === "+")) return node.operator === "-" ? -value : value;
break;
}
case "LogicalExpression": {
const left = this.evaluate(node.left, scope, depth + 1);
if (node.operator === "||" && left || node.operator === "&&" && !left || node.operator === "??" && left !== void 0 && left !== null) return left;
return this.evaluate(node.right, scope, depth + 1);
}
case "AssignmentExpression": {
const { object, key } = this.reference(node.left, scope, depth + 1);
try {
if (node.operator !== "=") throw new ScriptDataError("Only simple script data assignments are supported");
const value = this.evaluate(node.right, scope, depth + 1);
object[key] = value;
return value;
} catch (error) {
if (error instanceof ScriptDataError) {
object[key] = error;
this.poisonedAssignments.add(node);
}
throw error;
}
}
case "SequenceExpression": {
let value;
for (const expression of node.expressions) value = this.evaluate(expression, scope, depth + 1);
return value;
}
case "CallExpression": {
if (this.argumentIndex !== void 0 && this.matchesCallback(node.callee)) {
this.captured = false;
this.callbackValue = void 0;
const args = node.arguments.map((argument) => this.evaluate(argument, scope, depth + 1));
if (this.argumentIndex >= args.length) throw new ScriptDataError("Script data callback is missing its data argument");
this.callbackValue = args[this.argumentIndex];
this.captured = true;
return;
}
const fn = node.callee;
if (fn.type !== "FunctionExpression" && fn.type !== "ArrowFunctionExpression" || fn.async || fn.generator) break;
const args = node.arguments.map((argument) => this.evaluate(argument, scope, depth + 1));
const local = {
values: Object.create(null),
parent: scope
};
for (const [index, parameter] of fn.params.entries()) {
if (parameter.type !== "Identifier" || globalNames.has(parameter.name)) throw new ScriptDataError("Script data IIFEs require simple parameters");
const key = propertyKey(parameter.name);
local.values[key] = args[index];
}
return fn.body.type === "BlockStatement" ? this.statements(fn.body.body, local, depth + 1, true)?.value : this.evaluate(fn.body, local, depth + 1);
}
}
throw new ScriptDataError(`Unsupported script data expression: ${node.type}`);
}
matchesCallback(node) {
try {
const path = normalizePath(staticPath(node));
return path.length === this.target.length && path.every((key, index) => key === this.target[index]);
} catch (error) {
if (error instanceof UnsafePropertyError) throw error;
return false;
}
}
isCallbackGuard(node) {
if (this.argumentIndex === void 0 || node.type !== "IfStatement" || node.alternate || node.test.type !== "UnaryExpression" || node.test.operator !== "!") return false;
const consequent = node.consequent.type === "BlockStatement" && node.consequent.body.length === 1 ? node.consequent.body[0] : node.consequent;
if (consequent.type !== "ReturnStatement" || consequent.argument) return false;
const path = normalizePath(staticPath(node.test.argument));
return path.length > 0 && path.length < this.target.length && path.every((key, index) => key === this.target[index]);
}
referencesKnownData(node, scope, depth) {
this.step(depth);
if (node.type === "Identifier" || node.type === "MemberExpression") try {
const path = staticPath(node);
let value = this.identifier(path[0], scope);
for (const key of path.slice(1)) {
if (!isObject(value) || !Object.hasOwn(value, key)) return value !== this.root.values;
value = readProperty(value, key);
}
return true;
} catch (error) {
if (error instanceof UnsafePropertyError) throw error;
if (node.type === "Identifier") return false;
}
return Object.values(node).some((value) => {
return (Array.isArray(value) ? value : [value]).some((child) => child && typeof child === "object" && typeof child.type === "string" && this.referencesKnownData(child, scope, depth + 1));
});
}
statements(nodes, scope, depth, strict) {
for (const node of nodes) {
this.step(depth);
try {
switch (node.type) {
case "VariableDeclaration":
for (const declaration of node.declarations) {
if (declaration.id.type !== "Identifier" || globalNames.has(declaration.id.name)) throw new ScriptDataError("Script data declarations require simple variable names");
const key = propertyKey(declaration.id.name);
try {
if (declaration.init || !Object.hasOwn(scope.values, key)) scope.values[key] = declaration.init ? this.evaluate(declaration.init, scope, depth + 1) : void 0;
} catch (error) {
if (strict || !(error instanceof ScriptDataError) || error instanceof UnsafePropertyError || declaration.init && this.referencesKnownData(declaration.init, scope, depth + 1)) throw error;
scope.values[key] = error;
}
}
break;
case "ExpressionStatement":
this.evaluate(node.expression, scope, depth + 1);
break;
case "ReturnStatement": return { value: node.argument ? this.evaluate(node.argument, scope, depth + 1) : void 0 };
case "EmptyStatement": break;
default: if (!this.isCallbackGuard(node)) throw new ScriptDataError(`Unsupported script data statement: ${node.type}`);
}
} catch (error) {
if (strict || !(error instanceof ScriptDataError) || error instanceof UnsafePropertyError) throw error;
if (!(node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && this.poisonedAssignments.has(node.expression) && !this.referencesKnownData(node.expression.right, scope, depth + 1)) && this.referencesKnownData(node, scope, depth + 1)) throw error;
}
}
}
validate(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
this.step(depth);
if (value instanceof ScriptDataError) throw value;
if (isObject(value)) {
if (ancestors.has(value)) throw new ScriptDataError("Cyclic script data is not supported");
ancestors.add(value);
for (const child of Object.values(value)) this.validate(child, ancestors, depth + 1);
ancestors.delete(value);
}
}
read(source) {
if (source.length > maxScriptLength) throw new ScriptDataError("Script data source exceeds the size limit");
this.statements(parse(source, { ecmaVersion: "latest" }).body, this.root, 0, false);
let value = this.root.values;
if (this.argumentIndex === void 0) for (const key of this.target) {
if (!isObject(value) || !Object.hasOwn(value, key)) {
if (value instanceof ScriptDataError) throw value;
throw new ScriptDataError("Script data target was not found");
}
value = readProperty(value, key);
}
else {
if (!this.captured) throw new ScriptDataError("Script data callback was not found or could not be parsed");
value = this.callbackValue;
}
this.validate(value);
return value;
}
};
const parseScriptData = (source, target) => new ScriptDataReader(targetPath(target)).read(source);
/** Extracts a serialized callback argument without invoking the callback or any external function. */
const parseScriptCallback = (source, callbackPath, argumentIndex = 2) => {
if (!Number.isSafeInteger(argumentIndex) || argumentIndex < 0) throw new ScriptDataError("Script data callback argument index must be a non-negative integer");
return new ScriptDataReader(targetPath(callbackPath), argumentIndex).read(source);
};
//#endregion
export { parseScriptData as n, parseScriptCallback as t };