import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const catalog = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"), "utf8")) as Record; const identity = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), "utf8")) as Record; const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); test("M12-02F quarantines corrupt preview bytes while preserving catalog metadata", async ({ page }) => { await page.goto("/"); const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => { const commit = await import("/src/storage/asset-preview-opfs-commit.ts"); const quarantine = await import("/src/storage/asset-preview-quarantine.ts"); const encoded = new Uint8Array(bytes).buffer; const base = await commit.parseAssetCatalogManifestV2({ ...sourceCatalog, assets: (sourceCatalog.assets as Array>).map((asset) => ({ ...asset, preview: null })), }); const target = base.assets[0]; const identityInput = { ...sourceIdentity, assetId: target.assetId } as Record; delete identityInput.identitySha256; const boundIdentity = await commit.createAssetPreviewIdentity(identityInput as never); const databaseName = `m12-02f-${crypto.randomUUID()}`; const projectId = `m12-02f-${crypto.randomUUID()}`; const open = (): Promise => new Promise((resolve, reject) => { const request = indexedDB.open(databaseName, 1); request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error); }); let database = await open(); const seed = database.transaction("setting", "readwrite"); seed.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); await complete(seed); const committed = await commit.commitAssetPreviewToOPFS(database, projectId, 7, boundIdentity, encoded.slice(0)); const ready = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId); const root = await navigator.storage.getDirectory(); let contentDirectory = root; for (const segment of ["projects", projectId, "assets", "sha256", boundIdentity.content.sha256.slice(0, 2)]) { contentDirectory = await contentDirectory.getDirectoryHandle(segment); } const fileHandle = await contentDirectory.getFileHandle(boundIdentity.content.sha256); const corrupt = new Uint8Array(await (await fileHandle.getFile()).arrayBuffer()); corrupt[corrupt.byteLength - 1] ^= 0xff; const writer = await fileHandle.createWritable(); await writer.write(corrupt); await writer.close(); const quarantined = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId); let contentExists = true; try { await contentDirectory.getFileHandle(boundIdentity.content.sha256); } catch { contentExists = false; } database.close(); database = await open(); const reopened = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId); const catalogTransaction = database.transaction("setting", "readonly"); const catalogRow = await new Promise>((resolve, reject) => { const request = catalogTransaction.objectStore("setting").get("asset-catalog:index:v2"); request.onsuccess = () => resolve(request.result as Record); request.onerror = () => reject(request.error); }); await complete(catalogTransaction); const storedCatalog = await commit.parseAssetCatalogManifestV2(catalogRow.value); const quarantinePath = quarantined.status === "QUARANTINED" ? quarantined.receipt.quarantinePath : null; let quarantineFileBytes = 0; if (quarantinePath) { let current = root; const segments = quarantinePath.split("/"); for (const segment of segments.slice(0, -1)) current = await current.getDirectoryHandle(segment); quarantineFileBytes = (await (await current.getFileHandle(segments.at(-1)!)).getFile()).size; } const value = { baseMetadata: target, ready: { status: ready.status, bytes: ready.status === "READY" ? ready.data.byteLength : 0 }, quarantined, reopened, contentExists, quarantineFileBytes, storedAsset: storedCatalog.assets[0], storedRevision: storedCatalog.revision, committedRevision: committed.manifest.revision, otherAsset: storedCatalog.assets[1], baseOtherAsset: base.assets[1], }; database.close(); indexedDB.deleteDatabase(databaseName); const projects = await root.getDirectoryHandle("projects"); await projects.removeEntry(projectId, { recursive: true }); return value; }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); expect(result.ready).toEqual({ status: "READY", bytes: 513 }); expect(result.quarantined.status).toBe("QUARANTINED"); expect(result.quarantined.preview).toBeNull(); expect(result.quarantined.data).toBeNull(); expect(result.quarantined.receipt).toMatchObject({ task: "M12-02F", status: "QUARANTINED", catalogRevision: 8, actualByteLength: 513, code: "ASSET_SOURCE_HASH_MISMATCH", }); expect(result.quarantined.receipt.actualSha256).not.toBe(result.quarantined.receipt.expected.sha256); expect(result.quarantined.receipt.quarantinePath).toContain("/assets/quarantine/"); expect(result.reopened).toEqual(result.quarantined); expect(result.contentExists).toBe(false); expect(result.quarantineFileBytes).toBe(513); expect(result.storedRevision).toBe(result.committedRevision); expect(result.storedAsset).toEqual(result.quarantined.asset); expect({ ...result.storedAsset, preview: null }).toEqual(result.baseMetadata); expect(result.otherAsset).toEqual(result.baseOtherAsset); });