diff --git a/src/ipc/adapters/modScan.ts b/src/ipc/adapters/modScan.ts index 75108d3d..1f3b53b0 100644 --- a/src/ipc/adapters/modScan.ts +++ b/src/ipc/adapters/modScan.ts @@ -21,7 +21,7 @@ const MODICON_ENTRY = "modicon.png" const MAX_MODINFO_BYTES = 1 * 1024 * 1024 /** Past this, a mod icon is not an icon any more. */ -const MAX_MOD_IMAGE_BYTES = 8 * 1024 * 1024 +const MAX_MOD_IMAGE_BYTES = 512 * 1024 /** Folder mod icons are cached under, inside the launcher's own user data. */ function modImagesFolder(): string { @@ -118,7 +118,11 @@ function readModArchive(archivePath: string): Promise { } if (entry.fileName === MODICON_ENTRY && content.icon === undefined) { - if (!declaredSizeAllowed(entry, MAX_MOD_IMAGE_BYTES)) return settle({ ok: false, problem: "icon-too-large" }) + if (!declaredSizeAllowed(entry, MAX_MOD_IMAGE_BYTES)) { + // An oversized icon costs the mod its picture, never its place in + // the list: skip the icon and keep extracting metadata. + return advance() + } return collect( entry, @@ -127,7 +131,8 @@ function readModArchive(archivePath: string): Promise { content.icon = bytes advance() }, - () => settle({ ok: false, problem: "icon-too-large" }), + // Runtime size exceeds the limit: skip the icon, keep the mod. + () => advance(), // An icon that will not read costs the mod its picture, never its // place in the list: the metadata may still be perfectly readable. () => advance() diff --git a/tests/domain/mods/scanInstalled.test.ts b/tests/domain/mods/scanInstalled.test.ts index de5ab538..a293a6a5 100644 --- a/tests/domain/mods/scanInstalled.test.ts +++ b/tests/domain/mods/scanInstalled.test.ts @@ -289,7 +289,6 @@ describe("scanInstalledMods problem kinds", () => { fakePorts({ "gone.zip": { problem: "unreadable-archive" }, "huge-info.zip": { problem: "modinfo-too-large" }, - "huge-icon.zip": { problem: "icon-too-large" }, "resources.zip": {}, "broken.zip": { modinfo: "{ not json" }, "nameless.zip": { modinfo: JSON.stringify({ version: "1.0.0" }) } @@ -299,13 +298,28 @@ describe("scanInstalledMods problem kinds", () => { assert.deepEqual( result.errors.map((archive) => `${archive.zipname}:${archive.problem}`), + ["gone.zip:unreadable-archive", "huge-info.zip:modinfo-too-large", "resources.zip:modinfo-missing", "broken.zip:modinfo-invalid", "nameless.zip:modinfo-incomplete"] + ) + }) + + it("lists a mod with an oversized declared icon without an icon, not as an error", async () => { + const result = await scanInstalledMods( + fakePorts({ + "huge-icon.zip": { modinfo: modinfoText({ modid: "hugeicon" }) }, + "normal.zip": { modinfo: modinfoText({ modid: "normal" }), icon: ICON } + }), + { folder: FOLDER } + ) + + assert.deepEqual( + result.errors.map((e) => e.zipname), + [] + ) + assert.deepEqual( + result.mods.map((m) => ({ id: m.modid, hasIcon: m.image !== undefined })), [ - "gone.zip:unreadable-archive", - "huge-info.zip:modinfo-too-large", - "huge-icon.zip:icon-too-large", - "resources.zip:modinfo-missing", - "broken.zip:modinfo-invalid", - "nameless.zip:modinfo-incomplete" + { id: "hugeicon", hasIcon: false }, + { id: "normal", hasIcon: true } ] ) }) diff --git a/tests/fixtures/build-fixtures.ts b/tests/fixtures/build-fixtures.ts index ade71614..8868e10a 100644 --- a/tests/fixtures/build-fixtures.ts +++ b/tests/fixtures/build-fixtures.ts @@ -290,9 +290,10 @@ write( // --- oversized-declared-icon.zip ------------------------------------------- // Same idea as oversized-declared-modinfo.zip, aimed at the icon's own, -// larger cap instead (MAX_MOD_IMAGE_BYTES, 8 MiB in modScan.ts). modinfo.json -// is valid and under its cap, so the archive gets as far as trying the icon -// and declaredSizeAllowed() rejects it there. +// larger cap instead (MAX_MOD_IMAGE_BYTES, 512 KiB in modScan.ts). modinfo.json +// is valid and under its cap, so the archive gets as far as trying the icon. +// The declared size exceeds the limit, so the icon is skipped and the mod +// appears without a picture rather than as an error. write( "oversized-declared-icon.zip", assembleZip([ @@ -301,7 +302,7 @@ write( name: "modicon.png", method: METHOD_DEFLATE, realBytes: Buffer.from("irrelevant", "utf8"), - declaredUncompressedSize: 9 * 1024 * 1024 + declaredUncompressedSize: 1 * 1024 * 1024 } ]) ) diff --git a/tests/ipc/modScan.test.ts b/tests/ipc/modScan.test.ts index 4295f017..366a6059 100644 --- a/tests/ipc/modScan.test.ts +++ b/tests/ipc/modScan.test.ts @@ -96,15 +96,20 @@ describe("readModArchive", () => { assert.deepEqual(result, { ok: false, problem: "modinfo-too-large" }) }) - it("refuses a modicon.png whose declared size already exceeds the 8 MiB cap", async () => { + it("skips the icon when its declared size exceeds the 512 KiB cap", async () => { // modinfo.json is valid and under its own cap here, so the archive reaches - // the icon and declaredSizeAllowed() rejects that entry's header instead. + // the icon. The declared size exceeds the limit, so the icon is skipped and + // the mod appears without a picture rather than failing the archive. const { createModArchiveReaderPort } = await import("../../src/ipc/adapters/modScan") const readModArchive = createModArchiveReaderPort().read const result = await readModArchive(fixturePath("oversized-declared-icon.zip")) - assert.deepEqual(result, { ok: false, problem: "icon-too-large" }) + assert.equal(result.ok, true) + if (result.ok) { + assert.notEqual(result.content.modinfo, undefined) + assert.equal(result.content.icon, undefined) + } }) it("refuses a file with no zip structure in it at all", async () => {