AnyPS5/demo/web/tests/archive.test.mjs

103 lines
6.3 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import path from "node:path";
import { register } from "node:module";
register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } });
const site = name => pathToFileURL(path.join(process.argv[2], name)).href;
const { Archive, memoryEstimate, MEMORY_LIMIT, packageArchive } = await import(site("archive.mjs"));
const { BlobReader, BlobWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL);
const { default: createRelinker } = await import(site("native/relinker_wasm.mjs"));
const native = await createRelinker();
const executable = new Uint8Array(await readFile(path.join(process.argv[3], "main.elf")));
const module = new Uint8Array(await readFile(path.join(process.argv[3], "module.elf")));
const resource = new Uint8Array(2 * 1024 * 1024).fill(65);
async function zip(entries) {
const output = new BlobWriter("application/zip");
const writer = new ZipWriter(output, { zip64: true, level: 9, useWebWorkers: false, useCompressionStream: false });
for (const [name, bytes, options] of entries) await writer.add(name, bytes ? new Uint8ArrayReader(bytes) : undefined, options);
return writer.close();
}
const source = await zip([["title/", null, { directory: true }], ["title/program.dat", executable], ["title/sce_module/library.prx.esbak", module], ["title/sce_module/library.prx", new Uint8Array([79, 21, 61, 29])], ["title/sce_module/library.prx.esbak.guest.prx", new Uint8Array([77, 90, 0, 0])], ["title/sce_sys/param.json", new TextEncoder().encode("{}")], ["title/shaders/example.frag", executable], ["title/content/resource.bin", resource]]);
const folder = [
{ path: "title/program.dat", file: new Blob([executable]) },
{ path: "title/sce_module/library.prx.esbak", file: new Blob([module]) },
{ path: "title/sce_module/library.prx", file: new Blob([new Uint8Array([79, 21, 61, 29])]) },
{ path: "title/sce_module/library.prx.esbak.guest.prx", file: new Blob([new Uint8Array([77, 90, 0, 0])]) },
{ path: "title/sce_sys/param.json", file: new Blob(["{}"]) },
{ path: "title/shaders/example.frag", file: new Blob([executable]) },
{ path: "title/content/resource.bin", file: new Blob([resource]) }
];
for (const [kind, selected] of [["ZIP", source], ["folder", folder]]) {
for (const target of ["windows", "linux"]) {
const archive = new Archive(selected);
const summary = await archive.inspect(() => {});
assert.equal(summary.name, "program.dat");
assert.equal(summary.root, "title/");
assert.equal(summary.modules, 1);
assert.equal(summary.selectedBytes, executable.length + module.length);
const input = await archive.conversionInput(target, false, () => {});
assert.equal(input.modules.length, 1);
assert.deepEqual(input.executable, executable);
assert.equal(input.modules[0].name, "sce_module/library.prx.esbak");
const events = [];
const converted = native.Convert(input, event => events.push(event));
assert.equal(converted.ok, true, converted.error);
assert.equal(converted.executable[0], target === "windows" ? 77 : 127);
if (target === "windows") {
const image = new DataView(converted.executable.buffer, converted.executable.byteOffset, converted.executable.byteLength);
assert.equal(image.getUint16(image.getUint32(60, true) + 24 + 68, true), 3);
}
assert(events.some(event => event.stage === 6 && event.completed > 0));
const libraries = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]);
const chunks = [];
let maxChunk = 0;
const destination = new WritableStream({ async write(chunk) {
maxChunk = Math.max(maxChunk, chunk.length);
chunks.push(chunk.slice());
await new Promise(resolve => setTimeout(resolve, 1));
} });
await packageArchive(archive, converted, input.outputName, libraries, destination, () => {});
await destination.close();
const reader = new ZipReader(new BlobReader(new Blob(chunks)));
const entries = await reader.getEntries();
const byName = new Map(entries.map(entry => [entry.filename, entry]));
assert(byName.has(input.outputName));
assert(byName.has("libs/system.prx"));
assert.deepEqual(await byName.get("app0/shaders/example.frag").getData(new Uint8ArrayWriter(), { checkSignature: true }), executable);
assert.deepEqual(await byName.get("app0/sce_module/library.prx.esbak.guest.prx").getData(new Uint8ArrayWriter(), { checkSignature: true }), converted.modules[0].bytes);
assert.deepEqual(await byName.get("app0/sce_module/library.prx").getData(new Uint8ArrayWriter(), { checkSignature: true }), new Uint8Array([79, 21, 61, 29]));
assert.deepEqual(await byName.get("app0/program.dat").getData(new Uint8ArrayWriter(), { checkSignature: true }), executable);
assert.deepEqual(await byName.get("app0/content/resource.bin").getData(new Uint8ArrayWriter(), { checkSignature: true }), resource);
assert.equal((byName.get(input.outputName).externalFileAttributes >>> 16) & 0o777, 0o755);
assert(maxChunk < resource.length);
await reader.close();
await archive.close();
console.log(`${kind}, ${target}: conversion, original files, modules, permissions and streamed ZIP verified`);
}
}
for (const [entries, error] of [
[[["one", executable], ["two", executable]], /exactly one/u],
[[["readme", new Uint8Array([1])]], /exactly one/u],
[[["app", executable], ["..\/escape", resource]], /Unsafe ZIP/u],
[[["app", executable], ["APP", resource]], /Duplicate ZIP/u],
[[["app", executable], ["folder", resource], ["folder/file", resource]], /also used as a folder/u],
[[["app", executable], ["link", resource, { versionMadeBy: 0x31e, externalFileAttributes: (0o120777 << 16) >>> 0 }]], /Links and special/u]
]) {
const archive = new Archive(await zip(entries));
await assert.rejects(archive.inspect(() => {}), error);
await archive.close();
}
const missing = new Archive(await zip([["any-name", executable]]));
assert.equal((await missing.inspect(() => {})).warnings.length, 2);
await missing.close();
assert(memoryEstimate(200 * 1024 ** 3, 1024, 1024, 100) > MEMORY_LIMIT);
const invalid = native.Convert({ target: "unsupported" }, () => {});
assert.equal(invalid.ok, false);
console.log("Archive validation, missing-folder warnings and large-download budget verified");