mirror of
https://github.com/yt-dlp/ejs.git
synced 2026-07-04 03:50:57 +00:00
Initial POC
This commit is contained in:
44
src/main.ts
Normal file
44
src/main.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getSolvers } from "./solvers.ts";
|
||||
import { isOneOf } from "./utils.ts";
|
||||
|
||||
if (Deno.stdin.isTerminal()) {
|
||||
console.error("Expected player content on stdin");
|
||||
Deno.exit(9);
|
||||
}
|
||||
const stdin = await new Response(Deno.stdin.readable).text();
|
||||
if (!stdin) {
|
||||
console.error("Expected player content on stdin");
|
||||
Deno.exit(9);
|
||||
}
|
||||
if (Deno.args.length < 1) {
|
||||
console.error("Expected one argument, `solver nsig:... nsig:... sig:...`");
|
||||
Deno.exit(9);
|
||||
}
|
||||
|
||||
const solveList: {
|
||||
mode: "nsig" | "sig";
|
||||
value: string;
|
||||
}[] = [];
|
||||
for (const arg of Deno.args) {
|
||||
const split = arg.split(":", 2);
|
||||
if (split.length === 1) {
|
||||
console.error(`Missing mode: ${arg}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
const [mode, value] = split;
|
||||
if (!isOneOf(mode, "sig", "nsig")) {
|
||||
console.error(`Invalid mode, expected "nsig:..." or "sig:...": ${mode}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
solveList.push({ mode, value });
|
||||
}
|
||||
|
||||
const solvers = getSolvers(stdin);
|
||||
for (const solve of solveList) {
|
||||
const solver = solvers[solve.mode];
|
||||
if (!solver) {
|
||||
console.error(`Did not set ${solve.mode} function`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
console.log(solver(solve.value));
|
||||
}
|
||||
113
src/nsig.ts
Normal file
113
src/nsig.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
type ArrowFunctionExpression,
|
||||
type Node,
|
||||
} from "npm:@babel/types@7.28.2";
|
||||
import { matchesStructure } from "./utils.ts";
|
||||
import { type DeepPartial } from "./types.ts";
|
||||
|
||||
const identifier: DeepPartial<Node> = {
|
||||
type: "AssignmentExpression",
|
||||
operator: "=",
|
||||
left: {
|
||||
type: "Identifier",
|
||||
},
|
||||
right: {
|
||||
type: "FunctionExpression",
|
||||
params: [{}],
|
||||
body: {
|
||||
type: "BlockStatement",
|
||||
body: [
|
||||
{
|
||||
type: "ReturnStatement",
|
||||
// {
|
||||
argument:
|
||||
// or: [
|
||||
{
|
||||
type: "CallExpression",
|
||||
callee: {
|
||||
type: "MemberExpression",
|
||||
object: {
|
||||
type: "Identifier",
|
||||
// XXX: get switch function identifier, use here
|
||||
// name: "Lb",
|
||||
},
|
||||
property: {
|
||||
type: "MemberExpression",
|
||||
object: {
|
||||
type: "Identifier",
|
||||
// XXX: get global string store identifier, use here
|
||||
// name: "Y",
|
||||
},
|
||||
property: {
|
||||
type: "NumericLiteral",
|
||||
},
|
||||
},
|
||||
},
|
||||
arguments: [
|
||||
{ type: "ThisExpression" },
|
||||
{ type: "NumericLiteral" },
|
||||
{
|
||||
type: "Identifier",
|
||||
// XXX: get parameter name, use here
|
||||
// name: "Y",
|
||||
},
|
||||
],
|
||||
},
|
||||
// XXX: possible to be a direct call, ignore that for now
|
||||
// {
|
||||
// type: "CallExpression",
|
||||
// callee: {
|
||||
// type: "Identifier",
|
||||
// // XXX: get global string store identifier, use here
|
||||
// // name: "Y",
|
||||
// },
|
||||
// arguments: [
|
||||
// { type: "NumericLiteral" },
|
||||
// {
|
||||
// type: "Identifier",
|
||||
// // XXX: get parameter name, use here
|
||||
// // name: "Y",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function extract(node: Node): ArrowFunctionExpression | null {
|
||||
if (!matchesStructure(node, identifier)) {
|
||||
return null;
|
||||
}
|
||||
if (node.type !== "AssignmentExpression" || node.left.type !== "Identifier") {
|
||||
return null;
|
||||
}
|
||||
// TODO: verify identifiers here
|
||||
return {
|
||||
type: "ArrowFunctionExpression",
|
||||
params: [
|
||||
{
|
||||
type: "Identifier",
|
||||
name: "nsig",
|
||||
},
|
||||
],
|
||||
async: false,
|
||||
expression: true,
|
||||
body: {
|
||||
type: "CallExpression",
|
||||
callee: {
|
||||
type: "Identifier",
|
||||
name: node.left.name,
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: "Identifier",
|
||||
name: "nsig",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
122
src/sig.ts
Normal file
122
src/sig.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
type ArrowFunctionExpression,
|
||||
type Node,
|
||||
} from "npm:@babel/types@7.28.2";
|
||||
import { matchesStructure } from "./utils.ts";
|
||||
import { type DeepPartial } from "./types.ts";
|
||||
|
||||
const identifier: DeepPartial<Node> = {
|
||||
type: "AssignmentExpression",
|
||||
operator: "=",
|
||||
left: {
|
||||
type: "Identifier",
|
||||
},
|
||||
right: {
|
||||
type: "FunctionExpression",
|
||||
params: [{}, {}, {}],
|
||||
body: {
|
||||
type: "BlockStatement",
|
||||
body: [
|
||||
{ type: "ExpressionStatement" },
|
||||
{ type: "ExpressionStatement" },
|
||||
{ type: "ExpressionStatement" },
|
||||
{ type: "ExpressionStatement" },
|
||||
{
|
||||
type: "ExpressionStatement",
|
||||
expression: {
|
||||
type: "LogicalExpression",
|
||||
left: {
|
||||
type: "Identifier",
|
||||
// name: "M",
|
||||
},
|
||||
operator: "&&",
|
||||
right: {
|
||||
type: "SequenceExpression",
|
||||
expressions: [
|
||||
{
|
||||
type: "AssignmentExpression",
|
||||
operator: "=",
|
||||
left: {
|
||||
type: "Identifier",
|
||||
},
|
||||
right: {
|
||||
type: "CallExpression",
|
||||
callee: {
|
||||
type: "Identifier",
|
||||
},
|
||||
arguments: [
|
||||
{ type: "NumericLiteral" },
|
||||
{
|
||||
type: "CallExpression",
|
||||
callee: {
|
||||
type: "Identifier",
|
||||
name: "decodeURIComponent",
|
||||
},
|
||||
arguments: [{ type: "Identifier" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "CallExpression",
|
||||
},
|
||||
],
|
||||
extra: {
|
||||
parenthesized: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ type: "ReturnStatement" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function extract(node: Node): ArrowFunctionExpression | null {
|
||||
if (!matchesStructure(node, identifier)) {
|
||||
return null;
|
||||
}
|
||||
// shut the type checker up
|
||||
if (
|
||||
node.type !== "AssignmentExpression" ||
|
||||
node.right.type !== "FunctionExpression" ||
|
||||
node.right.body.body[4].type !== "ExpressionStatement" ||
|
||||
node.right.body.body[4].expression.type !== "LogicalExpression" ||
|
||||
node.right.body.body[4].expression.right.type !== "SequenceExpression" ||
|
||||
node.right.body.body[4].expression.right.expressions[0].type !==
|
||||
"AssignmentExpression"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const call = node.right.body.body[4].expression.right.expressions[0].right;
|
||||
if (call.type !== "CallExpression" || call.callee.type !== "Identifier") {
|
||||
return null;
|
||||
}
|
||||
// TODO: verify identifiers here
|
||||
return {
|
||||
type: "ArrowFunctionExpression",
|
||||
params: [
|
||||
{
|
||||
type: "Identifier",
|
||||
name: "sig",
|
||||
},
|
||||
],
|
||||
async: false,
|
||||
expression: true,
|
||||
body: {
|
||||
type: "CallExpression",
|
||||
callee: {
|
||||
type: "Identifier",
|
||||
name: call.callee.name,
|
||||
},
|
||||
arguments: [
|
||||
call.arguments[0],
|
||||
{
|
||||
type: "Identifier",
|
||||
name: "sig",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
103
src/solvers.ts
Executable file
103
src/solvers.ts
Executable file
@@ -0,0 +1,103 @@
|
||||
import { parse } from "npm:@babel/parser@7.28.3";
|
||||
import { generate } from "npm:@babel/generator@7.28.3";
|
||||
import { type ArrowFunctionExpression } from "npm:@babel/types@7.28.2";
|
||||
import { getFunctionNodes } from "./utils.ts";
|
||||
import { extract as extractSig } from "./sig.ts";
|
||||
import { extract as extractNsig } from "./nsig.ts";
|
||||
|
||||
function setup() {
|
||||
// @ts-ignore: This is used in the babel generated js
|
||||
globalThis.XMLHttpRequest = { prototype: {} };
|
||||
// deno-lint-ignore no-unused-vars
|
||||
const window = Object.assign(Object.create(null), globalThis);
|
||||
// deno-lint-ignore no-unused-vars
|
||||
const document = {};
|
||||
}
|
||||
|
||||
// helper functions
|
||||
export function getSolvers(data: string): {
|
||||
nsig: ((val: string) => string) | null;
|
||||
sig: ((val: string) => string) | null;
|
||||
} {
|
||||
const ast = parse(data, {
|
||||
attachComment: false,
|
||||
});
|
||||
const body = ast.program.body;
|
||||
if (body.length !== 2 || body[1].type !== "ExpressionStatement") {
|
||||
throw "unexpected structure";
|
||||
}
|
||||
const func = body[1];
|
||||
if (
|
||||
func.expression.type !== "CallExpression" ||
|
||||
func.expression.callee.type !== "FunctionExpression"
|
||||
) {
|
||||
throw "unexpected structure";
|
||||
}
|
||||
const found = {
|
||||
nsig: [] as ArrowFunctionExpression[],
|
||||
sig: [] as ArrowFunctionExpression[],
|
||||
};
|
||||
const plainExpressions = func.expression.callee.body.body.filter(
|
||||
(node, idx) => {
|
||||
if (idx === 0) {
|
||||
// Ignore `var window = this;`
|
||||
return false;
|
||||
}
|
||||
if (node.type === "ExpressionStatement") {
|
||||
if (node.expression.type === "AssignmentExpression") {
|
||||
const nsig = extractNsig(node.expression);
|
||||
if (nsig) {
|
||||
found.nsig.push(nsig);
|
||||
}
|
||||
const sig = extractSig(node.expression);
|
||||
if (sig) {
|
||||
found.sig.push(sig);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return node.expression.type === "StringLiteral";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
func.expression.callee.body.body = plainExpressions;
|
||||
|
||||
for (const [name, options] of Object.entries(found)) {
|
||||
if (options.length !== 1) {
|
||||
continue;
|
||||
}
|
||||
plainExpressions.push({
|
||||
type: "ExpressionStatement",
|
||||
expression: {
|
||||
type: "AssignmentExpression",
|
||||
operator: "=",
|
||||
left: {
|
||||
type: "MemberExpression",
|
||||
computed: false,
|
||||
object: {
|
||||
type: "Identifier",
|
||||
name: "_result",
|
||||
},
|
||||
property: {
|
||||
type: "Identifier",
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
right: options[0],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ast.program.body.splice(0, 0, ...getFunctionNodes(setup));
|
||||
|
||||
const { code } = generate(ast, {
|
||||
comments: false,
|
||||
compact: false,
|
||||
concise: false,
|
||||
});
|
||||
|
||||
// evil eval!!?!
|
||||
const resultObj = { nsig: null, sig: null };
|
||||
Function("_result", code)(resultObj);
|
||||
return resultObj;
|
||||
}
|
||||
22
src/solvers_test.ts
Normal file
22
src/solvers_test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { assertStrictEquals } from "jsr:@std/assert@1";
|
||||
import { getSolvers } from "./solvers.ts";
|
||||
import { players, tests } from "../tests/tests.ts";
|
||||
import { getCachePath } from "../tests/utils.ts";
|
||||
|
||||
for (const test of tests) {
|
||||
for (const variant of test.variants ?? players.keys()) {
|
||||
const path = getCachePath(test.player, variant);
|
||||
Deno.test(`${test.player} ${variant}`, async (t) => {
|
||||
const content = await Deno.readTextFile(path);
|
||||
const solvers = getSolvers(content);
|
||||
for (const mode of ["nsig", "sig"] as const) {
|
||||
for (const step of test[mode] || []) {
|
||||
await t.step(`${step.input} (${mode})`, () => {
|
||||
const got = solvers[mode]?.(step.input);
|
||||
assertStrictEquals(got, step.expected);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
4
src/types.ts
Normal file
4
src/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type DeepPartial<T> = T extends object ? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
49
src/utils.ts
Normal file
49
src/utils.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { parse } from "npm:@babel/parser@7.28.3";
|
||||
import { type Node, type Statement } from "npm:@babel/types@7.28.2";
|
||||
import { type DeepPartial } from "./types.ts";
|
||||
|
||||
export function matchesStructure<T extends Node>(
|
||||
obj: Node | Node[],
|
||||
structure: DeepPartial<T> | readonly DeepPartial<T>[],
|
||||
): obj is T {
|
||||
if (Array.isArray(structure)) {
|
||||
if (!Array.isArray(obj)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
structure.length === obj.length &&
|
||||
structure.every((value, index) => matchesStructure(obj[index], value))
|
||||
);
|
||||
}
|
||||
if (typeof structure === "object") {
|
||||
if (!obj) {
|
||||
return !structure;
|
||||
}
|
||||
if ("or" in structure) {
|
||||
// Allow `{ or: [a, b] }` so we can handle some special cases
|
||||
return (structure.or! as DeepPartial<Node>[]).some((node) =>
|
||||
matchesStructure(obj, node)
|
||||
);
|
||||
}
|
||||
for (const [key, value] of Object.entries(structure)) {
|
||||
if (!matchesStructure(obj[key as keyof typeof obj], value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return structure === obj;
|
||||
}
|
||||
|
||||
export function getFunctionNodes(f: (...a: unknown[]) => void): Statement[] {
|
||||
const func = parse(f.toString()).program.body[0];
|
||||
if (func.type === "FunctionDeclaration") {
|
||||
return func.body.body;
|
||||
}
|
||||
console.error("failed to parse function into nodes");
|
||||
return [];
|
||||
}
|
||||
|
||||
export function isOneOf<T>(value: unknown, ...of: readonly T[]): value is T {
|
||||
return of.includes(value as T);
|
||||
}
|
||||
Reference in New Issue
Block a user