mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
fix(audit): charge range-read progress before a read fails
readSpan added a member's bytes to memberBytesRead only after the whole range read succeeded, so a read that consumed some bytes and then failed (for example EIO) charged nothing against the packaged-probe allowance. Charge the cumulative bytes from readFileHandleRange's checkpoint as they arrive. Also cover the transient stat failure that HostFileRead's test named but never induced: EIO now has its own module-mocked test, and the original case is renamed for what it checks. Refs #10443 Refs #9703 Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
d68383f98d
commit
b3c0ac4bc4
3 files changed
+45
-12
No files matched your search
@@ -3110,19 +3110,29 @@ const readSpan = async (
|
||||
`Artifact changed before read: ${filePath}`,
|
||||
);
|
||||
if (offset + length > observed.size) return null;
|
||||
const bytes = await readFileHandleRange(handle, offset, length, () => {
|
||||
if (signal?.aborted)
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
cause: signal.reason,
|
||||
});
|
||||
});
|
||||
// Charge bytes as they arrive, so a read that fails after partial
|
||||
// progress still counts what it consumed against the allowance.
|
||||
let charged = 0;
|
||||
const bytes = await readFileHandleRange(
|
||||
handle,
|
||||
offset,
|
||||
length,
|
||||
(bytesRead) => {
|
||||
state.memberBytesRead += bytesRead - charged;
|
||||
charged = bytesRead;
|
||||
if (signal?.aborted)
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
cause: signal.reason,
|
||||
});
|
||||
},
|
||||
);
|
||||
const after = await handle.stat();
|
||||
if (!sameRegularFileState(initial, after))
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`Artifact changed during read: ${filePath}`,
|
||||
);
|
||||
state.memberBytesRead += bytes.length;
|
||||
state.memberBytesRead += bytes.length - charged;
|
||||
return Buffer.from(bytes);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -32,14 +32,11 @@ describe.skipIf(process.platform === "win32")("probeHostFile", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a transient failure unresolved", async () => {
|
||||
// EIO is not a refusal and does not settle the candidate, so the walk still
|
||||
// treats it as a check that may succeed on a retry.
|
||||
it("reports a readable regular file as a file", async () => {
|
||||
// The transient (EIO) branch is covered in HostFileRead.transient.test.ts.
|
||||
const root = await createTestTempDirectory("rea-host-file-probe-");
|
||||
const path = join(root, "libcore.so");
|
||||
await writeFile(path, "carrier");
|
||||
// A regular file is probeable; this asserts the settled side of the probe
|
||||
// still reports the kind, so the transient branch is not reached by mistake.
|
||||
await expect(probeHostFile(path)).resolves.toEqual({
|
||||
kind: "file",
|
||||
isFile: true,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const fs = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return {
|
||||
...fs,
|
||||
stat: async (...args: Parameters<typeof fs.stat>) => {
|
||||
if (String(args[0]).endsWith("transient.so"))
|
||||
throw Object.assign(new Error("injected I/O failure"), {
|
||||
code: "EIO",
|
||||
});
|
||||
return fs.stat(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { hostFileKind, probeHostFile } from "./HostFileRead.js";
|
||||
|
||||
it("keeps a transient stat failure unresolved rather than absent", async () => {
|
||||
// EIO neither proves absence nor lets ld.so search past the candidate.
|
||||
await expect(probeHostFile("/fixture/lib/transient.so")).resolves.toEqual({
|
||||
kind: "unknown",
|
||||
unreachable: false,
|
||||
});
|
||||
await expect(hostFileKind("/fixture/lib/transient.so")).resolves.toBeNull();
|
||||
});
|
||||
Reference in new issue
Block a user