Initial POC

This commit is contained in:
Simon Sawicki
2025-08-24 08:01:07 +02:00
commit a54e90cd77
18 changed files with 923 additions and 0 deletions

44
src/main.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}