fix(javascript): keep ASAR-named files without an ASAR header opaque (#2347)

Inventory expands an ASAR-named filesystem file only when its bytes are
an ASAR. JavaScript reconstruction expanded it on the suffix alone, so
a directory holding any other file named *.asar failed the whole
analysis with an invalid ASAR header.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Jin 2026-10-13 09:23:58 -04:00 committed by GitHub
parent 84ec40f1af
commit dd4bfed614
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 0 deletions

View File

@ -37,6 +37,7 @@ interface ExpectedFile {
interface ExpectedContainer {
readonly hash_status: ArtifactOccurrence["hash_status"];
readonly format: ArtifactOccurrence["artifact_format"];
readonly inventory: JavaScriptArtifactContainer | null;
}
@ -219,6 +220,9 @@ const visitReader = async (
"integrity",
`Nested ASAR was not integrity-verified: ${path}`,
);
// Inventory expands an ASAR-named file only when its bytes are an
// ASAR. One it recorded as ordinary bytes has no members to read.
if (container.format !== "asar") continue;
const inventory = container.inventory;
if (inventory === null)
throw new ArtifactReaderFailure(
@ -395,6 +399,7 @@ const expectedInventory = (
if (isNestedAsar)
containers.set(occurrence.logical_path, {
hash_status: occurrence.hash_status,
format: occurrence.artifact_format,
inventory: null,
});
continue;
@ -412,6 +417,7 @@ const expectedInventory = (
if (isNestedAsar)
containers.set(occurrence.logical_path, {
hash_status: occurrence.hash_status,
format: occurrence.artifact_format,
inventory: {
path: occurrence.logical_path,
sha256: node.sha256,

View File

@ -105,6 +105,34 @@ it("keeps a contradicted nested ASAR opaque during JavaScript reconstruction", a
expect(result.graph.coverage.omitted_count).toBeNull();
});
it("keeps an ASAR-named file without an ASAR header opaque during JavaScript reconstruction", async () => {
const root = await createTestTempDirectory("rea-opaque-asar-name-");
await writeFile(join(root, "main.js"), "export const outer = true;\n");
// Inventory records this file as ordinary bytes and does not expand it.
await writeFile(join(root, "ordinary.asar"), "ordinary bytes");
const snapshot = await scanCanonicalArtifactInventory(root, {});
const reader = createJavaScriptArtifactReader(root, "directory");
try {
const read = await readJavaScriptArtifactFiles(reader, snapshot);
expect(read.files.map(({ path }) => path)).toEqual(["main.js"]);
expect(read.containers).toEqual([]);
} finally {
await reader.close();
}
const analyzed = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!analyzed.ok) throw analyzed.error;
const result = javascriptApplicationAnalysisResultSchema.parse(
analyzed.value.normalized_result,
);
expect(result.statistics.nested_asar_containers).toBe(0);
expect(result.integrity_contradictions).toEqual([]);
});
it("retains semantic resource limits alongside recorded integrity contradictions", async () => {
const { archive } = await createStrippedAsarAddon([
{ path: "app.js", contents: primitiveCandidateExpansionSource() },