mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
fix(artifacts): preserve physical ZIP member identities (#1865)
Bind each ZIP occurrence independently of its filename so engine metadata inspection can read repeated resources without replacing retained sources. Keep logical-path collision checks in artifact inventory. Closes #1860 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
3 files changed
+64
-20
No files matched your search
@@ -147,22 +147,23 @@ export class ZipArtifactReader implements ArtifactReader {
|
||||
}
|
||||
|
||||
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
|
||||
const filenames = new Set<string>();
|
||||
let index = 0;
|
||||
for await (const entry of this.#reader.getEntriesGenerator()) {
|
||||
abortIfNeeded(signal);
|
||||
// Each enumeration can replay valid entries, but a second record with
|
||||
// the same name must not replace an already emitted source identity.
|
||||
if (filenames.has(entry.filename))
|
||||
// ZIP member identity is its directory occurrence, not its filename.
|
||||
// Logical path uniqueness belongs to the artifact inventory boundary.
|
||||
const metadata = projectZipEntry(entry, index++);
|
||||
const stored = this.#entries.get(metadata.adapterKey);
|
||||
if (stored !== undefined && !sameArtifactEntry(metadata, stored.metadata))
|
||||
throw new ArtifactReaderFailure(
|
||||
"path",
|
||||
`Artifact path collision: ${entry.filename}`,
|
||||
"integrity",
|
||||
`ZIP member identity changed during enumeration: ${entry.filename}`,
|
||||
);
|
||||
filenames.add(entry.filename);
|
||||
const metadata = projectZipEntry(entry);
|
||||
this.#entries.set(entry.filename, {
|
||||
source: entry,
|
||||
metadata: copyArtifactEntry(metadata),
|
||||
});
|
||||
if (stored === undefined)
|
||||
this.#entries.set(metadata.adapterKey, {
|
||||
source: entry,
|
||||
metadata: copyArtifactEntry(metadata),
|
||||
});
|
||||
yield metadata;
|
||||
}
|
||||
}
|
||||
@@ -203,7 +204,7 @@ export class ZipArtifactReader implements ArtifactReader {
|
||||
}
|
||||
}
|
||||
|
||||
const projectZipEntry = (entry: Entry): ArtifactEntry => {
|
||||
const projectZipEntry = (entry: Entry, index: number): ArtifactEntry => {
|
||||
const symlink = isSymlink(entry);
|
||||
return {
|
||||
path: entry.filename,
|
||||
@@ -216,7 +217,7 @@ const projectZipEntry = (entry: Entry): ArtifactEntry => {
|
||||
declaredSha256: null,
|
||||
unpacked: false,
|
||||
limitations: symlink ? ["Archive symlink target was not read."] : [],
|
||||
adapterKey: entry.filename,
|
||||
adapterKey: `zip-entry:${index}`,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -86,6 +86,40 @@ it("accepts a full JDK and canonicalizes the selected JAR", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("admits engine classes past repeated ZIP resource filenames", async () => {
|
||||
const root = await createTestTempDirectory("rea-jadx-duplicate-resource-");
|
||||
const jar = join(root, "engine.jar");
|
||||
const javaHome = await createFakeJdk(root);
|
||||
await writeOrderedZip(jar, [
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/LICENCE.txt",
|
||||
"com/atxx/jhmcp/JadxSession.class",
|
||||
"com/atxx/jhmcp/SessionHolder.class",
|
||||
"jadx/api/JadxDecompiler.class",
|
||||
"com/google/gson/Gson.class",
|
||||
"io/modelcontextprotocol/kotlin/sdk/server/Server.class",
|
||||
]);
|
||||
const bytes = await readFile(jar);
|
||||
const original = Buffer.from("META-INF/LICENCE.txt");
|
||||
let renamed = 0;
|
||||
for (
|
||||
let offset = bytes.indexOf(original);
|
||||
offset >= 0;
|
||||
offset = bytes.indexOf(original, offset + original.length)
|
||||
) {
|
||||
bytes.write("META-INF/LICENSE.txt", offset);
|
||||
renamed++;
|
||||
}
|
||||
expect(renamed).toBe(2);
|
||||
await writeFile(jar, bytes);
|
||||
await expect(
|
||||
resolveConfiguration(
|
||||
{ REA_JADX_MCP_JAR: jar, JAVA_HOME: javaHome },
|
||||
"inspect_android_package",
|
||||
),
|
||||
).resolves.toMatchObject({ jar: await realpath(jar) });
|
||||
});
|
||||
|
||||
it("configures heap and visible processors independently without forcing either", async () => {
|
||||
const root = await createTestTempDirectory("rea-jadx-config-");
|
||||
const jar = join(root, "engine.jar");
|
||||
|
||||
@@ -77,7 +77,7 @@ it("binds ZIP opens to the emitted entry metadata", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects duplicate ZIP names before replacing an emitted entry", async () => {
|
||||
it("retains duplicate ZIP members without replacing their source identities", async () => {
|
||||
const root = await createTestTempDirectory("rea-zip-duplicate-identity-");
|
||||
const path = join(root, "duplicates.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
@@ -106,11 +106,20 @@ it("rejects duplicate ZIP names before replacing an emitted entry", async () =>
|
||||
const iterator = reader.entries()[Symbol.asyncIterator]();
|
||||
const first = await iterator.next();
|
||||
if (first.done) throw new Error("Expected the first ZIP member");
|
||||
await expect(iterator.next()).rejects.toMatchObject({ reason: "path" });
|
||||
const stream = await reader.open({ ...first.value });
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
||||
expect(Buffer.concat(chunks).toString()).toBe("first bytes\n");
|
||||
const second = await iterator.next();
|
||||
if (second.done) throw new Error("Expected the second ZIP member");
|
||||
expect(first.value.path).toBe(second.value.path);
|
||||
expect(first.value.adapterKey).not.toBe(second.value.adapterKey);
|
||||
expect((await iterator.next()).done).toBe(true);
|
||||
for (const [entry, expected] of [
|
||||
[first.value, "first bytes\n"],
|
||||
[second.value, "later bytes\n"],
|
||||
] as const) {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of await reader.open({ ...entry }))
|
||||
chunks.push(Buffer.from(chunk));
|
||||
expect(Buffer.concat(chunks).toString()).toBe(expected);
|
||||
}
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user