fix(artifacts): preserve iNES source disappearance as integrity failure

This commit is contained in:
64579525 2026-10-12 15:50:02 +08:00
parent 414307c9d5
commit 574f6bb3bd
2 changed files with 56 additions and 2 deletions

View File

@ -178,7 +178,19 @@ export class InesArtifactReader implements ArtifactReader {
throw new ArtifactReaderFailure("unavailable", "iNES reader is closed");
const [opened, selected] = await Promise.all([
this.source.handle.stat(),
lstat(this.path),
lstat(this.path).catch((cause: unknown) => {
if (
cause instanceof Error &&
"code" in cause &&
["ENOENT", "ENOTDIR", "ELOOP"].includes(String(cause.code))
)
throw new ArtifactReaderFailure(
"integrity",
"iNES source changed during reading",
{ cause },
);
throw cause;
}),
]);
if (
!sameRegularFileState(this.source.initial, opened) ||

View File

@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { lstat, open, readFile, writeFile } from "node:fs/promises";
import { lstat, open, readFile, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
@ -191,6 +191,48 @@ it("rejects forged region ranges, cancellation and a changed retained source", a
}
});
it.each(["before", "during"])(
"reports source removal %s a region read as integrity failure",
async (when) => {
const root = await createTestTempDirectory("rea-ines-removed-source-");
const path = join(root, "source.nes");
await writeFile(path, inesFixture());
const handle = await open(path, "r");
const reader = new InesArtifactReader(path, {
handle,
initial: await handle.stat(),
});
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries[0];
if (entry === undefined) throw new Error("No header region");
if (when === "before") {
await unlink(path);
await expect(reader.open(entry)).rejects.toMatchObject({
reason: "integrity",
cause: { code: "ENOENT" },
});
} else {
const stream = await reader.open(entry);
await unlink(path);
await expect(
(async () => {
for await (const chunk of stream)
expect(Buffer.isBuffer(chunk)).toBe(true);
})(),
).rejects.toMatchObject({
reason: "integrity",
cause: { code: "ENOENT" },
});
}
} finally {
await reader.close();
await handle.close();
}
},
);
it("releases an extraction-owned source descriptor on repeated close", async () => {
const root = await createTestTempDirectory("rea-ines-owned-source-");
const path = join(root, "source.nes");