diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs index 8a77539c39..1fd8b56d96 100644 --- a/demo/web/tests/archive-root.test.mjs +++ b/demo/web/tests/archive-root.test.mjs @@ -150,6 +150,56 @@ for (const directories of [[], parents, [...parents].reverse()]) { } 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(); } diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index d2cbdb4377..fcd816ffe8 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -21,7 +21,7 @@ export async function downloadLibraries(target) { if (!response.ok) throw new Error(`Cannot load release manifest (${response.status})`); const release = await response.json(); const asset = release.platforms[target]; - if (!asset || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); + if (!asset || !Number.isSafeInteger(asset.size) || asset.size < 0 || asset.size > 512 * 1024 * 1024 || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); const latestResponse = await fetch("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest", { headers: { Accept: "application/vnd.github+json" } }); if (!latestResponse.ok) { const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); @@ -33,8 +33,27 @@ export async function downloadLibraries(target) { } const download = await fetch(asset.path); if (!download.ok) throw new Error(`Cannot download system libraries (${download.status})`); - const bytes = await download.arrayBuffer(); - if (bytes.byteLength !== asset.size) throw new Error("System library download is incomplete"); + const reader = download.body.getReader(); + const chunks = []; + let size = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > asset.size) throw new Error("System library download is incomplete"); + chunks.push(value); + } + if (size !== asset.size) throw new Error("System library download is incomplete"); + } catch (error) { + await reader.cancel(error).catch(() => {}); + throw error; + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); if (digest !== asset.sha256) throw new Error("System library checksum mismatch"); return { file: new Blob([bytes]), release, expandedBytes: asset.expandedBytes };