From de43da71ae982dd0f3d7b5ae49c914729db3a6ec Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:23:44 +0000 Subject: [PATCH] fix(artifacts): report encrypted archive entries as unsupported extraction extract_artifact on a ZIP containing a password-protected entry failed the whole extraction with "Artifact is invalid or has changed. Get a fresh copy" and named only an opaque occurrence ID. The archive was intact; REA does not decrypt entries, so complete extraction is unsupported for this target. Raise AnalysisUnsupportedTargetError naming the encrypted entry's path. Other unextractable occurrences keep their format failure but now name the logical path alongside the occurrence ID. Co-Authored-By: Claude Opus 5.5 --- src/artifacts/extraction/ArtifactExtraction.ts | 11 +++++++++-- .../filesystem/artifactZipIntegrity.test.ts | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/artifacts/extraction/ArtifactExtraction.ts b/src/artifacts/extraction/ArtifactExtraction.ts index a309d35f1d..4cd2268319 100644 --- a/src/artifacts/extraction/ArtifactExtraction.ts +++ b/src/artifacts/extraction/ArtifactExtraction.ts @@ -67,15 +67,22 @@ export const extractArtifact = async ( nodes, }; const selected = selectedOccurrences.map((occurrence) => { + // REA never decrypts archive entries, so an encrypted entry makes the + // complete extraction unsupported rather than the archive invalid. + if (occurrence.encrypted) + throw new AnalysisUnsupportedTargetError( + "extract_artifact", + sourcePath, + `Archive entry ${occurrence.logical_path} is encrypted; REA does not decrypt archive entries, so the archive cannot be extracted completely`, + ); if ( (occurrence.entry_kind !== "file" && occurrence.entry_kind !== "slice") || occurrence.artifact_id === null || - occurrence.encrypted || occurrence.logical_path === "." ) throw new ArtifactReaderFailure( "format", - `Selected occurrence is not an extractable regular child file: ${occurrence.occurrence_id}`, + `Selected occurrence is not an extractable regular child file: ${occurrence.logical_path} (${occurrence.occurrence_id})`, ); const node = inventory.nodes.get(occurrence.artifact_id); if (node === undefined) diff --git a/tests/boundary/filesystem/artifactZipIntegrity.test.ts b/tests/boundary/filesystem/artifactZipIntegrity.test.ts index 61b6b47c48..1796f1bc77 100644 --- a/tests/boundary/filesystem/artifactZipIntegrity.test.ts +++ b/tests/boundary/filesystem/artifactZipIntegrity.test.ts @@ -89,6 +89,22 @@ it("preserves encrypted-member and corrupt-CRC observations", async () => { hash_status: "unavailable", }), ); + // REA does not decrypt entries, so complete extraction is unsupported. + const extractionRoot = join(root, "encrypted-output"); + expect( + await new ArtifactProvider() + .createClient(encryptedTarget.value) + .execute("extract_artifact", { output_root: extractionRoot }), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisUnsupportedTargetError", + reason: expect.stringContaining("Archive entry secret.txt is encrypted"), + }, + }); + await expect(readFile(extractionRoot)).rejects.toMatchObject({ + code: "ENOENT", + }); const corruptPath = join(root, "corrupt-crc.zip"); const corruptWriter = new ZipWriter(new Uint8ArrayWriter());