From d45a0436289c40d8e72e9772c9af28fb56a958c1 Mon Sep 17 00:00:00 2001 From: Simon Sawicki Date: Mon, 25 Aug 2025 19:37:57 +0200 Subject: [PATCH] Support all player variants --- src/nsig.ts | 168 ++++++++++++++++++++++++++-------------------- src/sig.ts | 177 +++++++++++++++++++++++++++++-------------------- src/solvers.ts | 91 +++++++++++++++---------- src/types.ts | 12 ++-- src/utils.ts | 8 +-- tests/tests.ts | 19 +++++- 6 files changed, 284 insertions(+), 191 deletions(-) diff --git a/src/nsig.ts b/src/nsig.ts index a93d502..8ce4321 100644 --- a/src/nsig.ts +++ b/src/nsig.ts @@ -1,97 +1,119 @@ import { type ArrowFunctionExpression, type Node, + type BlockStatement, } from "npm:@babel/types@7.28.2"; import { matchesStructure } from "./utils.ts"; import { type DeepPartial } from "./types.ts"; const identifier: DeepPartial = { - 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", - // }, - // ], - // }, - // ], - // }, + type: "VariableDeclaration", + declarations: [ + { + type: "VariableDeclarator", + id: { + type: "Identifier", + }, + init: { + type: "ArrayExpression", + elements: [ + { + type: "Identifier", + }, + ], + }, + }, + ], + kind: "var", +}; +const catchBlockBody = [ + { + "type": "ReturnStatement", + "argument": { + "type": "BinaryExpression", + "operator": "+", + "left": { + "type": "MemberExpression", + "object": { + "type": "Identifier", }, - ], + "property": { + "type": "NumericLiteral", + }, + }, + "right": { + "type": "Identifier", + }, }, }, -}; +] as const; export function extract(node: Node): ArrowFunctionExpression | null { if (!matchesStructure(node, identifier)) { + // Fallback search for try { } catch { return X[12] + Y } + let name: string | undefined | null = null; + let block: BlockStatement | null = null; + switch (node.type) { + case "ExpressionStatement": { + if (node.expression.type === "AssignmentExpression" && + node.expression.left.type === "Identifier" && + node.expression.right.type === "FunctionExpression" && + node.expression.right.params.length === 1) { + name = node.expression.left.name; + block = node.expression.right.body; + } + break; + } + case "FunctionDeclaration": { + if (node.params.length === 1) { + name = node.id?.name; + block = node.body; + } + break; + } + } + if (!block || !name) { + return null; + } + const tryNode = block.body.at(-2); + if ( + tryNode?.type !== "TryStatement" || + tryNode.handler?.type !== "CatchClause" + ) { + return null; + } + const catchBody = tryNode.handler!.body.body; + if (matchesStructure(catchBody, catchBlockBody)) { + return makeSolverFuncFromName(name); + } return null; } - if (node.type !== "AssignmentExpression" || node.left.type !== "Identifier") { + + if (node.type !== "VariableDeclaration") { return null; } - // TODO: verify identifiers here + const declaration = node.declarations[0]; + if ( + declaration.type !== "VariableDeclarator" || !declaration.init || + declaration.init.type !== "ArrayExpression" || + declaration.init.elements.length !== 1 + ) { + return null; + } + const [firstElement] = declaration.init.elements; + if (!firstElement || firstElement.type !== "Identifier") { + return null; + } + return makeSolverFuncFromName(firstElement.name); +} + +function makeSolverFuncFromName(name: string): ArrowFunctionExpression { return { type: "ArrowFunctionExpression", params: [ { type: "Identifier", - name: "nsig", + name: "x", }, ], async: false, @@ -100,12 +122,12 @@ export function extract(node: Node): ArrowFunctionExpression | null { type: "CallExpression", callee: { type: "Identifier", - name: node.left.name, + name: name, }, arguments: [ { type: "Identifier", - name: "nsig", + name: "x", }, ], }, diff --git a/src/sig.ts b/src/sig.ts index f288b8c..8f8d02f 100644 --- a/src/sig.ts +++ b/src/sig.ts @@ -1,95 +1,121 @@ import { type ArrowFunctionExpression, + type FunctionExpression, + type Expression, type Node, + ExpressionStatement, } from "npm:@babel/types@7.28.2"; import { matchesStructure } from "./utils.ts"; import { type DeepPartial } from "./types.ts"; -const identifier: DeepPartial = { - 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: { +const logicalExpression: DeepPartial = { + type: "ExpressionStatement", + expression: { + type: "LogicalExpression", + left: { + type: "Identifier", + }, + operator: "&&", + right: { + type: "SequenceExpression", + expressions: [ + { + type: "AssignmentExpression", + operator: "=", + left: { + type: "Identifier", + }, + right: { + type: "CallExpression", + callee: { type: "Identifier", }, - right: { - type: "CallExpression", - callee: { - type: "Identifier", - }, - arguments: [ - { type: "NumericLiteral" }, - { - type: "CallExpression", - callee: { - type: "Identifier", - name: "decodeURIComponent", + arguments: { + or: [ + [ + { type: "NumericLiteral" }, + { + type: "CallExpression", + callee: { + type: "Identifier", + name: "decodeURIComponent", + }, + arguments: [{ type: "Identifier" }], }, - arguments: [{ type: "Identifier" }], - }, + ], + [ + { + type: "CallExpression", + callee: { + type: "Identifier", + name: "decodeURIComponent", + }, + arguments: [{ type: "Identifier" }], + }, + ], ], }, }, - { - type: "CallExpression", - }, - ], - extra: { - parenthesized: true, }, + { + type: "CallExpression", + }, + ], + extra: { + parenthesized: true, }, }, }, - { type: "ReturnStatement" }, - ], + } + +const identifier = { + or: [{ + type: "ExpressionStatement", + expression: { + type: "AssignmentExpression", + operator: "=", + left: { + type: "Identifier", + }, + right: { + type: "FunctionExpression", + params: [{}, {}, {}], + }, }, - }, -}; + }, { + type: "FunctionDeclaration", + params: [{}, {}, {}], + }], +} as const; export function extract(node: Node): ArrowFunctionExpression | null { - if (!matchesStructure(node, identifier)) { + + if (!matchesStructure(node, identifier as unknown as DeepPartial)) { + return null; + } + const block = (node.type === "ExpressionStatement" && + node.expression.type === "AssignmentExpression" && + node.expression.right.type === "FunctionExpression") + ? node.expression.right.body + : node.type === "FunctionDeclaration" + ? node.body + : null; + const relevantExpression = block?.body.at(-2); + if (!matchesStructure(relevantExpression!, logicalExpression)) { 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 !== + relevantExpression?.type !== "ExpressionStatement" || + relevantExpression.expression.type !== + "LogicalExpression" || + relevantExpression.expression.right.type !== + "SequenceExpression" || + relevantExpression.expression.right.expressions[0].type !== "AssignmentExpression" ) { return null; } - const call = node.right.body.body[4].expression.right.expressions[0].right; + const call = relevantExpression.expression.right.expressions[0].right; if (call.type !== "CallExpression" || call.callee.type !== "Identifier") { return null; } @@ -110,13 +136,20 @@ export function extract(node: Node): ArrowFunctionExpression | null { type: "Identifier", name: call.callee.name, }, - arguments: [ - call.arguments[0], - { - type: "Identifier", - name: "sig", - }, - ], + arguments: call.arguments.length === 1 + ? [ + { + type: "Identifier", + name: "sig", + }, + ] + : [ + call.arguments[0], + { + type: "Identifier", + name: "sig", + }, + ], }, }; } diff --git a/src/solvers.ts b/src/solvers.ts index 4c481eb..86f3206 100755 --- a/src/solvers.ts +++ b/src/solvers.ts @@ -19,48 +19,71 @@ export function preprocessPlayer(data: string): string { attachComment: false, }); const body = ast.program.body; - if (body.length !== 2 || body[1].type !== "ExpressionStatement") { + + const block = (() => { + switch (body.length) { + case 1: { + const func = body[0]; + if ( + func?.type === "ExpressionStatement" && + func.expression.type === "CallExpression" && + func.expression.callee.type === "MemberExpression" && + func.expression.callee.object.type === "FunctionExpression" + ) { + return func.expression.callee.object.body; + } + break; + } + case 2: { + const func = body[1]; + if ( + func?.type === "ExpressionStatement" && + func.expression.type === "CallExpression" && + func.expression.callee.type === "FunctionExpression" + ) { + const block = func.expression.callee.body; + // Skip `var window = this;` + block.body.splice(0, 1); + return block; + } + break; + } + } 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; + const plainExpressions = block.body.filter((node) => { + const nsig = extractNsig(node); + if (nsig) { + found.nsig.push(nsig); } - ); - func.expression.callee.body.body = plainExpressions; + const sig = extractSig(node); + if (sig) { + found.sig.push(sig); + } + if (node.type === "ExpressionStatement") { + if (node.expression.type === "AssignmentExpression") { + return true; + } + return node.expression.type === "StringLiteral"; + } + return true; + }); + block.body = plainExpressions; for (const [name, options] of Object.entries(found)) { - if (options.length !== 1) { - continue; + // TODO: this is cringe fix plz + const unique = new Set(options.map((x) => JSON.stringify(x))); + if (unique.size !== 1) { + const message = `found ${unique.size} ${name} function possibilities`; + throw message + + (unique.size + ? `: ${options.map((x) => generate(x)["code"]).join(", ")}` + : ""); } plainExpressions.push({ type: "ExpressionStatement", diff --git a/src/types.ts b/src/types.ts index 8592029..8c9a5cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,8 @@ -export type DeepPartial = T extends object ? { - [P in keyof T]?: DeepPartial; - } - : T; +export type DeepPartial = T extends object ? Or< + { + [P in keyof T]?: DeepPartial; + } + > + : Or; + +type Or = T | { or: T[] }; diff --git a/src/utils.ts b/src/utils.ts index d95b883..68c5b22 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,7 @@ import { type DeepPartial } from "./types.ts"; export function matchesStructure( obj: Node | Node[], structure: DeepPartial | readonly DeepPartial[], -): obj is T { +): boolean { if (Array.isArray(structure)) { if (!Array.isArray(obj)) { return false; @@ -20,10 +20,8 @@ export function matchesStructure( return !structure; } if ("or" in structure) { - // Allow `{ or: [a, b] }` so we can handle some special cases - return (structure.or! as DeepPartial[]).some((node) => - matchesStructure(obj, node) - ); + // Handle `{ or: [a, b] }` + return structure.or.some((node) => matchesStructure(obj, node)); } for (const [key, value] of Object.entries(structure)) { if (!matchesStructure(obj[key as keyof typeof obj], value)) { diff --git a/tests/tests.ts b/tests/tests.ts index dc87f3f..2a067ac 100644 --- a/tests/tests.ts +++ b/tests/tests.ts @@ -11,7 +11,6 @@ export const tests: { }[] = [ { player: "3d3ba064", - variants: ["tce"], nsig: [ { input: "ZdZIqFPQK-Ty8wId", expected: "qmtUsIz04xxiNW" }, { input: "4GMrWHyKI5cEvhDO", expected: "N9gmEX7YhKTSmw" }, @@ -27,7 +26,6 @@ export const tests: { }, { player: "5ec65609", - variants: ["tce"], nsig: [{ input: "0eRGgQWJGfT5rFHFj", expected: "4SvMpDQH-vBJCw" }], sig: [ { @@ -40,7 +38,6 @@ export const tests: { }, { player: "6742b2b9", - variants: ["tce"], nsig: [ { input: "_HPB-7GFg1VTkn9u", expected: "qUAsPryAO_ByYg" }, { input: "K1t_fcB6phzuq2SF", expected: "Y7PcOt3VE62mog" }, @@ -54,6 +51,22 @@ export const tests: { }, ], }, + { + player: "23ccdd25", + nsig: [ + // Synthetic test + { input: "0eRGgQWJGfT5rFHFj", expected: "orSsTqUaUO-j" }, + ], + sig: [ + // Synthetic test + { + input: + "MMGZJMUucirzS_SnrSPYsc85CJNnTUi6GgR5NKn-znQEICACojE8MHS6S7uYq4TGjQX_D4aPk99hNU6wbTvorvVVMgIARwsSdQfJAA", + expected: + "ZJMUucirzS_SnrSPYsc85CJNnTUi6GgR5NKn-znQEICACojE8MHS6S7uYq4TGjQX_D4aPk99hAU6wbTvorvVVMgIARwsSdQfJAN", + }, + ], + }, ]; export const players = new Map([