feat: add mod editing, revision merges and secure game storage
Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
This commit is contained in:
1 parent
06ff19ac4a
commit
c8d83b3fe7
65 files changed
+3825
-68
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
import { build } from "esbuild";
|
||||
|
||||
async function load(name) {
|
||||
const result = await build({ entryPoints: [fileURLToPath(new URL(`../src/player/${name}.ts`, import.meta.url))],
|
||||
bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
|
||||
return import(`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString("base64")}`);
|
||||
}
|
||||
const { evaluateNani, naniText } = await load("naninovel");
|
||||
const c = (value) => ["const", value];
|
||||
const n = (name) => ["name", name];
|
||||
const bin = (op, a, b) => ["bin", op, a, b];
|
||||
|
||||
test("Naninovel uses its own truth, concatenation and short-circuit semantics", () => {
|
||||
const evaluate = (ast) => evaluateNani(ast, (key) => ({ points: 3, flag: "false" })[key]);
|
||||
assert.equal(evaluate(bin("+", c("Score: "), n("points"))), "Score: 3");
|
||||
assert.equal(evaluate(["if", n("flag"), c("yes"), c("no")]), "no");
|
||||
assert.equal(evaluate(bin("&&", c(false), ["call", "Missing", []])), false);
|
||||
assert.equal(evaluate(bin("||", c(true), ["call", "Missing", []])), true);
|
||||
assert.throws(() => evaluate(["call", "Missing", []]), /функция/);
|
||||
assert.throws(() => evaluate(bin("/", c(1), c(0))), /ноль/);
|
||||
});
|
||||
|
||||
test("Naninovel translates complete templates before substituting expressions", () => {
|
||||
const expressions = { points: n("points") };
|
||||
assert.equal(naniText("<b>Очки: {points}</b><br>Продолжить", expressions, () => 7), "{b}Очки: 7{/b}\nПродолжить");
|
||||
assert.throws(() => naniText("{unexpected}", expressions, () => 1), /неизвестное выражение/);
|
||||
assert.equal(naniText("{points}", expressions, () => "{b}"), "{{b}");
|
||||
});
|
||||
|
||||
test("Naninovel AST runs through the shared store and survives JSON saves", async () => {
|
||||
const { Python } = await load("python");
|
||||
const store = { score: 2 };
|
||||
const py = new Python({ get: (key) => Object.hasOwn(store, key) ? store[key] : undefined,
|
||||
has: (key) => Object.hasOwn(store, key), set: (key, value) => { store[key] = value; }, del: (key) => { delete store[key]; } });
|
||||
py.exec([{ op: "set", t: { k: "name", id: "score" }, v: { k: "nani", ast: bin("+", n("score"), c(4)) } }]);
|
||||
assert.equal(JSON.parse(JSON.stringify(store)).score, 6);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
import { build } from "esbuild";
|
||||
|
||||
const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/python.ts", import.meta.url))],
|
||||
bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
|
||||
const { Python, PyDict } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`);
|
||||
const c = (v) => ({ k: "const", v });
|
||||
const attr = (o, a) => ({ k: "attr", o, a });
|
||||
const sub = (o, i) => ({ k: "sub", o, i: c(i) });
|
||||
function runtime() {
|
||||
const store = { fn: Math.max, values: [1, 2], config: {}, data: new PyDict() };
|
||||
const py = new Python({ get: (n) => store[n], has: (n) => n in store, set: (n, v) => { store[n] = v; }, del: (n) => { delete store[n]; } });
|
||||
return { py, store };
|
||||
}
|
||||
|
||||
test("PGVN cannot obtain native constructors through attributes or subscripts", () => {
|
||||
const { py } = runtime();
|
||||
for (const value of ["text", [], {}, Math.max]) {
|
||||
for (const key of ["constructor", "__proto__", "prototype", "__defineGetter__"]) {
|
||||
assert.equal(py.eval(attr(c(value), key)), null);
|
||||
assert.equal(py.eval(sub(c(value), key)), null);
|
||||
}
|
||||
}
|
||||
assert.equal(py.eval({ k: "name", id: "constructor" }), null);
|
||||
assert.equal(py.eval(attr({ k: "name", id: "fn" }, "call")), null);
|
||||
const coercedKey = { toString: () => "constructor" };
|
||||
assert.equal(py.eval(sub(c({}), coercedKey)), null);
|
||||
assert.equal(py.eval(attr(c({}), coercedKey)), null);
|
||||
});
|
||||
|
||||
test("PGVN cannot pollute prototypes via assignment", () => {
|
||||
const { py, store } = runtime();
|
||||
for (const target of [attr({ k: "name", id: "config" }, "__proto__"), sub({ k: "name", id: "config" }, "constructor"), { k: "name", id: "__proto__" }]) {
|
||||
assert.throws(() => py.bind(target, { injected: true }), /недоступ/);
|
||||
}
|
||||
assert.equal(Object.getPrototypeOf(store.config), Object.prototype);
|
||||
assert.equal({}.injected, undefined);
|
||||
});
|
||||
|
||||
test("ordinary PGVN list methods, dictionary keys and builtin calls still work", () => {
|
||||
const { py, store } = runtime();
|
||||
const name = (id) => ({ k: "name", id });
|
||||
const call = (f, ...a) => ({ k: "call", f, a: a.map(c), kw: {} });
|
||||
py.eval(call(attr(name("values"), "append"), 3));
|
||||
assert.deepEqual(store.values, [1, 2, 3]);
|
||||
assert.equal(py.eval(sub(name("values"), -1)), 3);
|
||||
assert.equal(py.eval(call(name("fn"), 4, 9)), 9);
|
||||
py.bind(sub(name("data"), "constructor"), "ordinary dictionary value");
|
||||
assert.equal(py.eval(sub(name("data"), "constructor")), "ordinary dictionary value");
|
||||
assert.throws(() => py.eval(sub(name("values"), "map")), /целым/);
|
||||
});
|
||||
Reference in new issue
Block a user