mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
fix(artifacts): preserve archive entry types and host paths (#581)
This commit is contained in:
1 parent
f73bb0fe11
commit
28e0f4e9ec
5 files changed
+153
-5
No files matched your search
@@ -339,6 +339,8 @@ jobs:
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
tests/boundary/filesystem/asarInventoryFreshness.test.ts
|
||||
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
|
||||
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
|
||||
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
|
||||
src/ghidra/GhidraInstallation.test.ts
|
||||
|
||||
@@ -255,6 +255,10 @@ const expectedInventory = (
|
||||
"integrity",
|
||||
`Artifact inventory node is missing: ${occurrence.logical_path}`,
|
||||
);
|
||||
// Inventory occurrences can have content identities for directories too.
|
||||
// Suffixes only describe file candidates; a directory such as
|
||||
// node_modules/@zip.js remains a graph node and must never be opened.
|
||||
if (occurrence.entry_kind !== "file") continue;
|
||||
if (occurrence.logical_path.toLowerCase().endsWith(".asar"))
|
||||
containers.set(occurrence.logical_path, {
|
||||
path: occurrence.logical_path,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sep } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
import { extractFile, listPackage, statFile, uncache } from "@electron/asar";
|
||||
@@ -33,11 +34,18 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
}
|
||||
for (const listed of paths) {
|
||||
abortIfNeeded(signal);
|
||||
const path = listed.replace(/^\/+|\/+$/gu, "");
|
||||
// @electron/asar returns paths assembled with the host path module.
|
||||
// Keep that spelling for its API calls, but expose archive paths with
|
||||
// portable separators. On POSIX, backslashes can be literal filename
|
||||
// characters and must remain untouched.
|
||||
const path = toArtifactPath(listed);
|
||||
if (path.length === 0) continue;
|
||||
const providerPath = listed.startsWith(sep)
|
||||
? listed.slice(sep.length)
|
||||
: listed;
|
||||
let metadata: ReturnType<typeof statFile>;
|
||||
try {
|
||||
metadata = statFile(this.path, path, false);
|
||||
metadata = statFile(this.path, providerPath, false);
|
||||
} catch (cause: unknown) {
|
||||
throw asarFailure(this.path, `stat ${path}`, cause);
|
||||
}
|
||||
@@ -68,7 +76,7 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
: [
|
||||
"Official ASAR extraction buffers each individually bounded file.",
|
||||
],
|
||||
adapterKey: path,
|
||||
adapterKey: providerPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -119,6 +127,11 @@ const abortIfNeeded = (signal?: AbortSignal): void => {
|
||||
throw new ArtifactReaderFailure("cancelled", "ASAR operation cancelled");
|
||||
};
|
||||
|
||||
const toArtifactPath = (listed: string): string => {
|
||||
const portable = sep === "\\" ? listed.replaceAll("\\", "/") : listed;
|
||||
return portable.replace(/^\/+|\/+$/gu, "");
|
||||
};
|
||||
|
||||
const asarFailure = (
|
||||
path: string,
|
||||
operation: string,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { buffer } from "node:stream/consumers";
|
||||
|
||||
@@ -31,6 +31,84 @@ const archiveFixture = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
describe("ASAR path boundary", () => {
|
||||
it("keeps inventory paths portable and opens nested entries from a packed archive", async () => {
|
||||
const root = await createTestTempDirectory("rea-asar-paths-");
|
||||
const source = join(root, "source");
|
||||
await mkdir(join(source, "nested"), { recursive: true });
|
||||
await writeFile(join(source, "nested", "main.js"), CONTENT);
|
||||
const archive = join(root, "paths.asar");
|
||||
await createPackage(source, archive);
|
||||
const reader = new AsarArtifactReader(archive);
|
||||
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
expect(entries.map(({ path, kind }) => [path, kind])).toEqual([
|
||||
["nested", "directory"],
|
||||
["nested/main.js", "file"],
|
||||
]);
|
||||
const nestedFile = entries.find(({ path }) => path === "nested/main.js");
|
||||
if (!nestedFile) throw new Error("Expected packed nested ASAR file");
|
||||
expect((await buffer(await reader.open(nestedFile))).toString()).toBe(
|
||||
CONTENT,
|
||||
);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"reports packed ASAR symlinks without following them",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-asar-symlink-");
|
||||
const source = join(root, "source");
|
||||
await mkdir(join(source, "nested"), { recursive: true });
|
||||
await writeFile(join(source, "nested", "main.js"), CONTENT);
|
||||
await symlink("nested/main.js", join(source, "alias.js"));
|
||||
const archive = join(root, "symlink.asar");
|
||||
await createPackage(source, archive);
|
||||
const reader = new AsarArtifactReader(archive);
|
||||
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
expect(entries.find(({ path }) => path === "alias.js")).toMatchObject({
|
||||
kind: "symlink",
|
||||
path: "alias.js",
|
||||
});
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"preserves literal backslashes in POSIX ASAR filenames",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-asar-backslash-");
|
||||
const source = join(root, "source");
|
||||
await mkdir(source);
|
||||
await writeFile(join(source, "literal\\name.js"), CONTENT);
|
||||
const archive = join(root, "backslash.asar");
|
||||
await createPackage(source, archive);
|
||||
const reader = new AsarArtifactReader(archive);
|
||||
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
const file = entries.find(({ path }) => path === "literal\\name.js");
|
||||
if (!file) throw new Error("Expected literal-backslash ASAR entry");
|
||||
expect((await buffer(await reader.open(file))).toString()).toBe(
|
||||
CONTENT,
|
||||
);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("ASAR inventory freshness", () => {
|
||||
it.each(["archive", "bundle"] as const)(
|
||||
"refreshes a replaced archive when inventorying a %s target",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createPackageWithOptions } from "@electron/asar";
|
||||
@@ -155,6 +155,57 @@ it("produces deterministic ASAR graphs with unpacked native linkage and complete
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps suffix-named ASAR directories in the graph and analyzes their files", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-javascript-asar-suffix-directory-",
|
||||
);
|
||||
const source = join(root, "source");
|
||||
const zipDirectory = join(source, "node_modules", "@zip.js");
|
||||
await mkdir(zipDirectory, { recursive: true });
|
||||
await writeJavaScriptArtifactFixture(source);
|
||||
await writeFile(
|
||||
join(zipDirectory, "entry.js"),
|
||||
"export const packed = true;\n",
|
||||
);
|
||||
const archive = join(root, "app.asar");
|
||||
await createPackageWithOptions(source, archive, {});
|
||||
|
||||
const snapshot = await scanArtifactInventory(archive);
|
||||
const zipDirectoryOccurrence = snapshot.occurrences.find(
|
||||
({ logical_path }) => logical_path === "node_modules/@zip.js",
|
||||
);
|
||||
expect(zipDirectoryOccurrence).toMatchObject({
|
||||
entry_kind: "directory",
|
||||
artifact_id: expect.stringMatching(/^art_[a-f0-9]{64}$/u),
|
||||
});
|
||||
expect(
|
||||
snapshot.nodes.some(
|
||||
({ artifact_id }) => artifact_id === zipDirectoryOccurrence?.artifact_id,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const result = await reconstructJavaScriptArtifact({
|
||||
input_path: archive,
|
||||
format: "asar",
|
||||
});
|
||||
|
||||
expect(result.statistics.parsed_javascript_files).toBeGreaterThan(0);
|
||||
expect(result.graph.nodes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "javascript-asset",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
path: "node_modules/@zip.js/entry.js",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps ASAR analysis usable when unpacked native companion bytes are absent", async () => {
|
||||
const root = await createTestTempDirectory("rea-javascript-asar-missing-");
|
||||
const source = join(root, "source");
|
||||
|
||||
Reference in new issue
Block a user