mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(artifacts): preserve failures while unwinding nested traversals
Finish every remaining iterator from inner to outer, retaining cleanup observations without replacing the original traversal failure. A real nested ASAR regression reproduces the previous integrity-error masking.
This commit is contained in:
1 parent
5502ca9c97
commit
a17e257f43
2 files changed
+103
-3
No files matched your search
@@ -120,6 +120,7 @@ const visitArtifactEntries = async (
|
||||
iterator: currentReader.entries(context.signal)[Symbol.asyncIterator](),
|
||||
},
|
||||
];
|
||||
let failure: { readonly cause: unknown } | undefined;
|
||||
try {
|
||||
while (stack.length > 0) {
|
||||
const frame = stack.at(-1);
|
||||
@@ -194,10 +195,27 @@ const visitArtifactEntries = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
failure = { cause };
|
||||
} finally {
|
||||
// Readers remain owned by the inventory until its complete observation is built.
|
||||
for (const { iterator } of stack) await iterator.return?.();
|
||||
for (const { iterator, prefix, reader } of stack.reverse()) {
|
||||
try {
|
||||
await iterator.return?.();
|
||||
} catch (cause: unknown) {
|
||||
failure = {
|
||||
cause: ArtifactReaderFailure.withCleanup(
|
||||
failure === undefined ? cause : failure.cause,
|
||||
ArtifactReaderFailure.cleanupObservation(
|
||||
cause,
|
||||
`artifact traversal for ${prefix || reader.format}`,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure !== undefined) throw failure.cause;
|
||||
};
|
||||
|
||||
const digestArtifactEntry = async (
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
import { createPackage } from "@electron/asar";
|
||||
import { describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { translateArtifactFailure } from "../../../src/artifacts/ArtifactProvider.js";
|
||||
import {
|
||||
ArtifactReaderFailure,
|
||||
type ArtifactEntry,
|
||||
type ArtifactReader,
|
||||
} from "../../../src/artifacts/ArtifactReader.js";
|
||||
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
||||
import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js";
|
||||
import { scanCanonicalArtifactInventory as scanCanonicalArtifactInventoryOwned } from "../../../src/artifacts/inventory/scanCanonical.js";
|
||||
import { scanArtifactInventory } from "../../fixtures/artifactInventory.js";
|
||||
@@ -89,3 +92,82 @@ describe("artifact inventory snapshot", () => {
|
||||
expect(closeAttempts).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("unwinds every nested traversal and preserves its primary failure", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-inventory-traversal-cleanup-",
|
||||
);
|
||||
const contents = join(root, "contents");
|
||||
const bundle = join(root, "bundle");
|
||||
await Promise.all([mkdir(contents), mkdir(bundle)]);
|
||||
await writeFile(join(contents, "member.js"), "module.exports = 1;");
|
||||
await createPackage(contents, join(bundle, "app.asar"));
|
||||
const directory = new DirectoryArtifactReader(bundle);
|
||||
const resourceScope = new ArtifactResourceScope();
|
||||
const returned: string[] = [];
|
||||
onTestFinished(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await resourceScope.close();
|
||||
});
|
||||
const nestedEntries = AsarArtifactReader.prototype.entries;
|
||||
vi.spyOn(AsarArtifactReader.prototype, "entries").mockImplementation(
|
||||
function (
|
||||
this: AsarArtifactReader,
|
||||
signal,
|
||||
): AsyncIterableIterator<ArtifactEntry> {
|
||||
const iterator = nestedEntries.call(this, signal)[Symbol.asyncIterator]();
|
||||
return {
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
async next() {
|
||||
await iterator.next();
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
"primary nested member failure",
|
||||
);
|
||||
},
|
||||
async return() {
|
||||
returned.push("nested");
|
||||
await iterator.return?.();
|
||||
return { done: true as const, value: undefined };
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const reader: ArtifactReader = {
|
||||
format: "directory",
|
||||
entries(signal): AsyncIterableIterator<ArtifactEntry> {
|
||||
const iterator = directory.entries(signal)[Symbol.asyncIterator]();
|
||||
return {
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
next: () => iterator.next(),
|
||||
async return() {
|
||||
returned.push("root");
|
||||
await iterator.return?.();
|
||||
throw new Error("root iterator cleanup failed");
|
||||
},
|
||||
};
|
||||
},
|
||||
open: (entry, signal) => directory.open(entry, signal),
|
||||
provenance: () => [],
|
||||
close: () => directory.close(),
|
||||
};
|
||||
|
||||
const failure = await scanCanonicalArtifactInventoryOwned(
|
||||
bundle,
|
||||
{ resourceScope },
|
||||
() => reader,
|
||||
).catch((cause: unknown) => cause);
|
||||
expect(failure).toMatchObject({
|
||||
reason: "integrity",
|
||||
message: "primary nested member failure",
|
||||
cleanup: {
|
||||
reason: "root iterator cleanup failed",
|
||||
resources: ["artifact traversal for directory"],
|
||||
},
|
||||
});
|
||||
expect(returned).toEqual(["nested", "root"]);
|
||||
});
|
||||
Reference in new issue
Block a user