UNPKG

rsshub

Version:
294 lines (293 loc) • 13.7 kB
import { parseSync, rawTransferSupported } from "oxc-parser"; //#region lib/utils/parse-js.ts const parseScriptSource = (source) => { const { program, errors } = parseSync("script.js", source, { lang: "js", sourceType: "script", preserveParens: false, experimentalRawTransfer: rawTransferSupported() }); return { program, errors }; }; //#endregion //#region lib/utils/evaluate-script.ts const globalNames = /* @__PURE__ */ new Set([ "window", "globalThis", "self" ]); const unsafeKeys = /* @__PURE__ */ new Set([ "__proto__", "prototype", "constructor" ]); const maxScriptLength = 2e6; const maxSteps = 1e4; const maxDepth = 100; var ScriptDataError = class extends Error {}; var UnsafePropertyError = class extends ScriptDataError {}; const isKey = (value) => typeof value === "string" || typeof value === "number"; const isNumber = (value) => typeof value === "number"; const isObject = (value) => typeof value === "object" && value !== null && !(value instanceof ScriptDataError); const propertyKey = (value) => { if (!isKey(value)) 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" && !("regex" in node.property) && !("bigint" in node.property) ? 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 parseProgram = async (source, errorMessage) => { const { program, errors } = await parseScriptSource(source); if (errors.length) throw new ScriptDataError(errorMessage); return program; }; const targetPath = async (target) => { const program = await parseProgram(target, "Script data targets must be a single static property path"); 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 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. */ const readScriptData = async (source, target, argumentIndex) => { const root = { values: Object.create(null) }; const poisonedAssignments = /* @__PURE__ */ new WeakSet(); let steps = 0; let captured = false; let callbackValue; const step = (depth) => { if (++steps > maxSteps || depth > maxDepth) throw new UnsafePropertyError("Script data exceeds the parsing complexity limit"); }; const 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 root.values; if (name === "undefined") return; throw new ScriptDataError(`Unknown script data variable: ${name}`); }; const 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 = evaluate(node.object, scope, depth + 1); const key = propertyKey(!node.computed && node.property.type === "Identifier" ? node.property.name : 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 }; }; const evaluate = (node, scope, depth) => { step(depth); switch (node.type) { case "Literal": if ("regex" in node || "bigint" in node) break; return node.value; case "Identifier": return identifier(node.name, scope); case "ArrayExpression": return node.elements.map((element) => element ? 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 : evaluate(property.key, scope, depth + 1)); value[key] = evaluate(property.value, scope, depth + 1); } return value; } case "MemberExpression": { const key = propertyKey(!node.computed && node.property.type === "Identifier" ? node.property.name : evaluate(node.property, scope, depth + 1)); return readProperty(evaluate(node.object, scope, depth + 1), key); } case "UnaryExpression": { const value = evaluate(node.argument, scope, depth + 1); if (node.operator === "void") return; if (node.operator === "!") return !value; if (isNumber(value) && (node.operator === "-" || node.operator === "+")) return node.operator === "-" ? -value : value; break; } case "LogicalExpression": { const left = evaluate(node.left, scope, depth + 1); if (node.operator === "||" && left || node.operator === "&&" && !left || node.operator === "??" && left !== void 0 && left !== null) return left; return evaluate(node.right, scope, depth + 1); } case "AssignmentExpression": { const { object, key } = reference(node.left, scope, depth + 1); try { if (node.operator !== "=") throw new ScriptDataError("Only simple script data assignments are supported"); const value = evaluate(node.right, scope, depth + 1); object[key] = value; return value; } catch (error) { if (error instanceof ScriptDataError) { object[key] = error; poisonedAssignments.add(node); } throw error; } } case "SequenceExpression": { let value; for (const expression of node.expressions) value = evaluate(expression, scope, depth + 1); return value; } case "CallExpression": { if (argumentIndex !== void 0 && matchesCallback(node.callee)) { captured = false; callbackValue = void 0; const args = node.arguments.map((argument) => evaluate(argument, scope, depth + 1)); if (argumentIndex >= args.length) throw new ScriptDataError("Script data callback is missing its data argument"); callbackValue = args[argumentIndex]; 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) => 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]; } if (!fn.body) break; return fn.body.type === "BlockStatement" ? statements(fn.body.body, local, depth + 1, true)?.value : evaluate(fn.body, local, depth + 1); } } throw new ScriptDataError(`Unsupported script data expression: ${node.type}`); }; const matchesCallback = (node) => { try { const path = normalizePath(staticPath(node)); return path.length === target.length && path.every((key, index) => key === target[index]); } catch (error) { if (error instanceof UnsafePropertyError) throw error; return false; } }; const isCallbackGuard = (node) => { if (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 < target.length && path.every((key, index) => key === target[index]); }; const referencesKnownData = (node, scope, depth) => { step(depth); if (node.type === "Identifier" || node.type === "MemberExpression") try { const path = staticPath(node); let value = identifier(path[0], scope); for (const key of path.slice(1)) { if (!isObject(value) || !Object.hasOwn(value, key)) return value !== root.values; value = readProperty(value, key); } return true; } catch (error) { if (error instanceof UnsafePropertyError) throw error; if (node.type === "Identifier") return false; } return Object.entries(node).some(([key, value]) => { if (key === "parent") return false; return (Array.isArray(value) ? value : [value]).some((child) => child?.type && referencesKnownData(child, scope, depth + 1)); }); }; const statements = (nodes, scope, depth, strict) => { for (const node of nodes) { 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 ? evaluate(declaration.init, scope, depth + 1) : void 0; } catch (error) { if (strict || !(error instanceof ScriptDataError) || error instanceof UnsafePropertyError || declaration.init && referencesKnownData(declaration.init, scope, depth + 1)) throw error; scope.values[key] = error; } } break; case "ExpressionStatement": evaluate(node.expression, scope, depth + 1); break; case "ReturnStatement": return { value: node.argument ? evaluate(node.argument, scope, depth + 1) : void 0 }; case "EmptyStatement": break; default: if (!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" && poisonedAssignments.has(node.expression) && !referencesKnownData(node.expression.right, scope, depth + 1)) && referencesKnownData(node, scope, depth + 1)) throw error; } } }; const validate = (value, ancestors = /* @__PURE__ */ new Set(), depth = 0) => { 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)) validate(child, ancestors, depth + 1); ancestors.delete(value); } }; if (source.length > maxScriptLength) throw new ScriptDataError("Script data source exceeds the size limit"); statements((await parseProgram(source, "Script data source could not be parsed")).body, root, 0, false); let value = root.values; if (argumentIndex === void 0) for (const key of 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 (!captured) throw new ScriptDataError("Script data callback was not found or could not be parsed"); value = callbackValue; } validate(value); return value; }; const evaluateScriptData = async (source, target) => { const path = await targetPath(target); return readScriptData(source, path); }; /** Extracts a serialized callback argument without invoking the callback or any external function. */ const evaluateScriptCallback = async (source, callbackPath, argumentIndex = 2) => { if (!Number.isSafeInteger(argumentIndex) || argumentIndex < 0) throw new ScriptDataError("Script data callback argument index must be a non-negative integer"); const path = await targetPath(callbackPath); return readScriptData(source, path, argumentIndex); }; //#endregion export { evaluateScriptData as n, evaluateScriptCallback as t };