diff --git a/src/artifacts/inventory/ArtifactGraphConstruction.ts b/src/artifacts/inventory/ArtifactGraphConstruction.ts index 96770f85e..99d76e9f1 100644 --- a/src/artifacts/inventory/ArtifactGraphConstruction.ts +++ b/src/artifacts/inventory/ArtifactGraphConstruction.ts @@ -325,15 +325,20 @@ export const resolveOccurrenceParents = ( branch.occurrence = occurrence; } for (const occurrence of occurrences) { - const segments = occurrence.logical_path.split("/"); + const path = occurrence.logical_path; let branch = root; let parent: MutableOccurrence | undefined; - // The final segment identifies this occurrence, not one of its parents. - for (const segment of segments.slice(0, -1)) { + // Walk parent segments only. Stop scanning the path once no observed + // directory or expanded container can be an ancestor. + for (let start = 0; ;) { + const slash = path.indexOf("/", start); + if (slash < 0) break; + const segment = path.slice(start, slash); const child = branch.children.get(segment); if (child === undefined) break; branch = child; parent = branch.occurrence ?? parent; + start = slash + 1; } occurrence.parent_occurrence_id = parent?.occurrence_id ?? null; } diff --git a/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts b/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts index ad2830167..04cb7b707 100644 --- a/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts +++ b/tests/boundary/filesystem/artifactInventoryEntryOrder.test.ts @@ -73,25 +73,29 @@ describe("artifact inventory entry order", () => { const root = await createTestTempDirectory("rea-deep-archive-parent-"); const archive = join(root, "deep.zip"); // Fits the ZIP filename field while exceeding the formerly expensive depth. - const member = `observed/${"d/".repeat(30_000)}data.txt`; - await writeOrderedZip(archive, [member, "observed/", "unrelated/"]); + const members = ["data.txt", "second.txt", "third.bin"].map( + (name) => `observed/${"d/".repeat(30_000)}${name}`, + ); + await writeOrderedZip(archive, [...members, "observed/", "unrelated/"]); const inventory = await inventoryArtifact(archive); expect(artifactParentPaths(inventory)).toEqual({ ".": null, observed: ".", unrelated: ".", - [member]: "observed", + ...Object.fromEntries(members.map((member) => [member, "observed"])), }); - const occurrence = artifactOccurrenceAt(inventory, member); - expect(occurrence.hash_status).toBe("verified"); - expect(inventory.edges).toContainEqual( - expect.objectContaining({ - occurrence_id: occurrence.occurrence_id, - parent_artifact_id: artifactOccurrenceAt(inventory, "observed") - .artifact_id, - relation: "contains", - }), - ); + const observedParent = artifactOccurrenceAt(inventory, "observed"); + for (const member of members) { + const occurrence = artifactOccurrenceAt(inventory, member); + expect(occurrence.hash_status).toBe("verified"); + expect(inventory.edges).toContainEqual( + expect.objectContaining({ + occurrence_id: occurrence.occurrence_id, + parent_artifact_id: observedParent.artifact_id, + relation: "contains", + }), + ); + } }); it.each([