import assert from "node:assert/strict"; import { register } from "node:module"; import { pathToFileURL } from "node:url"; import path from "node:path"; register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); const { Archive, DirectoryWriter } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); const { BlobWriter, Uint8ArrayReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); const header = new Uint8Array([127, 69, 76, 70]); async function zipped(entries) { const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); for (const [name, directory, bytes = header, options = {}] of entries) await writer.add(name, directory ? undefined : new Uint8ArrayReader(bytes), { directory, ...options }); return writer.close(); } const { packageArchive } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); const libraryBundle = await zipped([["libs/provider.prx", false]]); for (const conflict of ["sce_module/provider.guest.prx/child", "sce_module/PROVIDER.GUEST.PRX/child", "sce_module/provider.guest.prx/"]) { const input = new Archive(await zipped([["main.elf", false], ["sce_module/provider.prx", false], [conflict, conflict.endsWith("/")]])); try { await input.inspect(() => {}); const written = []; const destination = { async directory(name) { written.push(name); }, async file(name) { written.push(name); return new WritableStream(); } }; const converted = { executable: header, modules: [{ name: "app0/sce_module/provider.guest.prx", bytes: header }] }; await assert.rejects(packageArchive(input, converted, "main", libraryBundle, destination, () => {}, true), /Output path collision/); assert.deepEqual(written, []); } finally { await input.close(); } } const distinctOutput = new Archive(await zipped([["main.elf", false], ["sce_module/provider.prx", false], ["assets/", true]])); try { await distinctOutput.inspect(() => {}); const written = []; await packageArchive(distinctOutput, { executable: header, modules: [{ name: "app0/sce_module/provider.guest.prx", bytes: header }] }, "main", libraryBundle, { async directory(name) { written.push(name); }, async file(name) { written.push(name); return new WritableStream(); } }, () => {}, true); assert(written.includes("app0/assets/")); assert(written.includes("app0/sce_module/provider.guest.prx")); assert(written.includes("libs/provider.prx")); } finally { await distinctOutput.close(); } const metadata = new Uint8Array(86); const metadataView = new DataView(metadata.buffer); metadataView.setUint32(0, 0x00051607); metadataView.setUint32(4, 0x00020000); metadataView.setUint16(24, 2); metadataView.setUint32(26, 9); metadataView.setUint32(30, 50); metadataView.setUint32(34, 32); metadataView.setUint32(38, 2); metadataView.setUint32(42, 82); metadataView.setUint32(46, 4); const app = [["Title/main.elf", false], ["Title/sce_module/provider.prx", false]]; const sidecars = [["__MACOSX/Title/._main.elf", false, metadata], ["__MACOSX/Title/sce_module/._provider.prx", false, metadata]]; for (const directories of [[], ["Title/", "Title/sce_module/", "__MACOSX/", "__MACOSX/Title/", "__MACOSX/Title/sce_module/"]]) { const archive = new Archive(await zipped([...directories.map(name => [name, true]), ...app, ...sidecars, ["__MACOSX/._Title", false, metadata]])); try { const summary = await archive.inspect(() => {}); assert.equal(summary.root, "Title/"); assert.equal(summary.files, 2); assert.equal(summary.modules, 1); assert.equal(summary.selectedBytes, 8); assert.equal(summary.totalBytes, 8 + metadata.length * 3); assert(!archive.files.some(item => item.name.includes("__MACOSX"))); for (const target of ["linux", "windows"]) { const input = await archive.conversionInput(target, false, () => {}); assert.deepEqual(input.executable, header); assert.equal(input.modules[0].name, "sce_module/provider.prx"); assert.deepEqual(input.modules[0].bytes, header); } } finally { await archive.close(); } } const wrongMagic = metadata.slice(); wrongMagic[3] = 0; const wrongVersion = metadata.slice(); wrongVersion[5] = 1; const oversizedTable = metadata.slice(); new DataView(oversizedTable.buffer).setUint16(24, 100); const invalidDescriptors = []; for (const [position, value] of [ [26, 0], [26, 1], [38, 9], [30, 0], [30, 49], [30, 0xffffffff], [34, 0xffffffff], [42, 83], [42, 87] ]) { const invalid = metadata.slice(); new DataView(invalid.buffer).setUint32(position, value); invalidDescriptors.push(invalid); } const emptyBeforeTable = metadata.slice(); new DataView(emptyBeforeTable.buffer).setUint32(30, 25); new DataView(emptyBeforeTable.buffer).setUint32(34, 0); invalidDescriptors.push(emptyBeforeTable); const emptyPastEnd = metadata.slice(); new DataView(emptyPastEnd.buffer).setUint32(42, 87); new DataView(emptyPastEnd.buffer).setUint32(46, 0); invalidDescriptors.push(emptyPastEnd); for (const extra of [ [["__MACOSX/Title/._main.elf", false, wrongMagic]], [["__MACOSX/Title/._main.elf", false, wrongVersion]], [["__MACOSX/Title/._main.elf", false, metadata.subarray(0, 25)]], [["__MACOSX/Title/._main.elf", false, oversizedTable]], ...invalidDescriptors.map(bytes => [["__MACOSX/Title/._main.elf", false, bytes]]), [["__MACOSX/Title/._absent.elf", false, metadata]], [...sidecars, ["__MACOSX/Title/readme", false, new Uint8Array([1])]], [...sidecars, ["__MACOSX/unrelated/", true]], [...sidecars, ["other/resource", false, new Uint8Array([1])]] ]) { const archive = new Archive(await zipped([...app, ...extra])); try { await assert.rejects(archive.inspect(() => {}), /exactly one executable ELF/); } finally { await archive.close(); } } for (const [offset, length] of [[0, 0], [50, 0], [86, 0]]) { const emptyEntry = metadata.slice(); new DataView(emptyEntry.buffer).setUint32(30, offset); new DataView(emptyEntry.buffer).setUint32(34, length); const archive = new Archive(await zipped([["main.elf", false], ["__MACOSX/._main.elf", false, emptyEntry]])); try { assert.equal((await archive.inspect(() => {})).files, 1); assert.deepEqual(archive.files.map(item => item.name), ["main.elf"]); } finally { await archive.close(); } } for (const bytes of [wrongMagic, wrongVersion, metadata.subarray(0, 25), oversizedTable, ...invalidDescriptors]) { const name = "__MACOSX/._main.elf"; const archive = new Archive(await zipped([["main.elf", false], [name, false, bytes]])); try { assert.equal((await archive.inspect(() => {})).files, 2); assert.deepEqual(archive.files.map(item => item.name), ["main.elf", name]); } finally { await archive.close(); } } for (const unrelated of [ ["__MACOSX/readme", false, new Uint8Array([1])], ["__MACOSX/unrelated/", true], ["__MACOSX/__MACOSX/._._main.elf", false, metadata], ["__MACOSX/__MACOSX/", true] ]) { const archive = new Archive(await zipped([["main.elf", false], ["__MACOSX/._main.elf", false, metadata], unrelated])); try { await archive.inspect(() => {}); assert.deepEqual(archive.files.map(item => item.name), ["main.elf", "__MACOSX/._main.elf", unrelated[0]]); } finally { await archive.close(); } } const knownDirectory = new Archive(await zipped([ ["main.elf", false], ["assets/", true], ["__MACOSX/._main.elf", false, metadata], ["__MACOSX/assets/", true] ])); try { await knownDirectory.inspect(() => {}); assert.deepEqual(knownDirectory.files.map(item => item.name), ["main.elf", "assets/"]); } finally { await knownDirectory.close(); } for (const [entries, message] of [ [[...app, ["__MACOSX/../._main.elf", false, metadata]], /Unsafe ZIP path/], [[...app, ...sidecars, ["__MACOSX/title/._MAIN.ELF", false, metadata]], /Duplicate ZIP path/], [[...app, ["__MACOSX/Title/._main.elf", false, metadata, { password: "metadata-password" }]], /Encrypted ZIP entries/], [[...app, ["__MACOSX/Title/._main.elf", false, metadata, { versionMadeBy: 0x31e, externalFileAttributes: (0o120777 << 16) >>> 0 }]], /Links and special files/], [[["Title/", true], ["__MACOSX/._Title", false, metadata]], /No files were selected/] ]) { const archive = new Archive(await zipped(entries)); try { await assert.rejects(archive.inspect(() => {}), message); } finally { await archive.close(); } } const parents = ["outer/", "outer/inner/", "outer/inner/title/"]; for (const directories of [[], parents, [...parents].reverse()]) { const archive = new Archive(await zipped([ ...directories.map(name => [name, true]), ["outer/inner/title/main.elf", false] ])); try { const summary = await archive.inspect(() => {}); assert.equal(summary.root, "outer/inner/title/"); assert.equal(summary.name, "main.elf"); assert.equal(summary.files, 1); assert.deepEqual((await archive.conversionInput("linux", false, () => {})).executable, header); } finally { await archive.close(); } } { const originalFetch = globalThis.fetch; const originalSelf = globalThis.self; globalThis.self = { postMessage() {} }; try { const { downloadLibraries } = await import(pathToFileURL(path.join(process.argv[2], "worker.mjs"))); const bytes = Uint8Array.from({ length: 32 }, (_, index) => index); const sha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); const asset = { path: "libraries/linux-1234.zip", size: bytes.length, expandedBytes: 100, sha256 }; let cancelled = false; let pulled = 0; const configureDownload = (chunks, size = bytes.length) => { cancelled = false; pulled = 0; globalThis.fetch = async url => { if (url === "./release.json") return Response.json({ tag: "v1", platforms: { linux: { ...asset, size } } }); if (url.startsWith("https://api.github.com/")) return Response.json({ tag_name: "v1" }); return new Response(new ReadableStream({ pull(controller) { if (pulled < chunks.length) controller.enqueue(chunks[pulled++]); else controller.close(); }, cancel() { cancelled = true; } })); }; }; configureDownload(Array.from({ length: 8 }, () => bytes)); await assert.rejects(downloadLibraries("linux"), /System library download is incomplete/); assert.equal(cancelled, true); assert(pulled < 8); for (const chunks of [[], [bytes.subarray(0, 31)]]) { configureDownload(chunks); await assert.rejects(downloadLibraries("linux"), /System library download is incomplete/); } configureDownload([bytes.subarray(0, 3), bytes.subarray(3)]); const downloaded = await downloadLibraries("linux"); assert.deepEqual(new Uint8Array(await downloaded.file.arrayBuffer()), bytes); assert.equal(downloaded.expandedBytes, 100); for (const size of [-1, 1.5, 512 * 1024 * 1024 + 1]) { configureDownload([bytes], size); await assert.rejects(downloadLibraries("linux"), /Invalid release manifest/); assert.equal(pulled, 0); } } finally { globalThis.fetch = originalFetch; if (originalSelf === undefined) delete globalThis.self; else globalThis.self = originalSelf; } } const folder = new Archive([{ path: "outer/inner/title/main.elf", file: new Blob([header]) }]); try { assert.equal((await folder.inspect(() => {})).root, "outer/inner/title/"); } finally { await folder.close(); } const siblings = new Archive(await zipped([["outer/", true], ["outer/main.elf", false], ["other/resource", false]])); try { await assert.rejects(siblings.inspect(() => {}), /exactly one executable ELF/); } finally { await siblings.close(); } for (const cleanupRejects of [false, true]) { const failure = new Error("input read failed"); let aborted = false; const output = new WritableStream({ abort(reason) { aborted = true; assert.equal(reason, failure); if (cleanupRejects) throw new Error("abort failed"); } }); const source = new ReadableStream({ start(controller) { controller.error(failure); } }); const writer = new DirectoryWriter({ async file() { return output; } }); await assert.rejects(writer.add("example.bin", source), error => error === failure); assert.equal(aborted, true); assert.equal(output.locked, false); assert.equal(source.locked, false); } { const failure = new Error("progress failed"); let cancelled = false; let aborted = false; const source = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array([1])); }, cancel(reason) { cancelled = true; assert.equal(reason, failure); throw new Error("cancel failed"); } }); const output = new WritableStream({ abort(reason) { aborted = true; assert.equal(reason, failure); } }); const writer = new DirectoryWriter({ async file() { return output; } }); await assert.rejects(writer.add("example.bin", source, { onprogress() { throw failure; } }), error => error === failure); assert.equal(cancelled, true); assert.equal(aborted, true); assert.equal(output.locked, false); assert.equal(source.locked, false); } { const failure = new Error("blob read failed"); let aborted = false; const output = new WritableStream({ abort(reason) { aborted = true; assert.equal(reason, failure); throw new Error("abort failed"); } }); const source = { size: 1, async init() {}, async readUint8Array() { throw failure; } }; const writer = new DirectoryWriter({ async file() { return output; } }); await assert.rejects(writer.add("example.bin", source), error => error === failure); assert.equal(aborted, true); assert.equal(output.locked, false); }