diff --git a/src/artifacts/ArtifactReader.ts b/src/artifacts/ArtifactReader.ts index 06ea3719d2..1c48da5350 100644 --- a/src/artifacts/ArtifactReader.ts +++ b/src/artifacts/ArtifactReader.ts @@ -155,6 +155,8 @@ export interface ArtifactTraversalOmission { export interface ArtifactReaderTraversal { readonly status: ArtifactInventoryCoverage["status"]; readonly limitations: readonly string[]; + /** Number of directory entries examined, including names omitted before decoding. */ + readonly examinedEntries?: number; } /** Per-iterator observation sink; adapters report intentional omissions without shared state. */ diff --git a/src/artifacts/DesktopApplicationReader.ts b/src/artifacts/DesktopApplicationReader.ts index a0a5cce4eb..2aa26bbea0 100644 --- a/src/artifacts/DesktopApplicationReader.ts +++ b/src/artifacts/DesktopApplicationReader.ts @@ -271,6 +271,12 @@ const scanDirectory = async ( kind: omission.kind, }); }, + onCoverage: (coverage) => { + if (coverage.examinedEntries !== undefined) + entries = coverage.examinedEntries; + if (coverage.status !== "complete") partial = true; + limitations.push(...coverage.limitations); + }, })) { entries++; abortIfNeeded(signal); diff --git a/src/artifacts/DirectoryArtifactReader.ts b/src/artifacts/DirectoryArtifactReader.ts index f7531244c8..3127cebba5 100644 --- a/src/artifacts/DirectoryArtifactReader.ts +++ b/src/artifacts/DirectoryArtifactReader.ts @@ -132,6 +132,7 @@ export class DirectoryArtifactReader implements ArtifactReader { if (unrepresentableNameCount > 0) options?.onCoverage?.({ status: "unknown", + examinedEntries: examined, limitations: [ `Directory contains ${String(unrepresentableNameCount)} entry name(s) that are not valid UTF-8; no filesystem operations were performed for them. First raw basename bytes: ${firstUnrepresentableNameHex ?? "unknown"}.`, ], diff --git a/tests/boundary/filesystem/desktopApplication.test.ts b/tests/boundary/filesystem/desktopApplication.test.ts index 58f881eb5d..a8621d8066 100644 --- a/tests/boundary/filesystem/desktopApplication.test.ts +++ b/tests/boundary/filesystem/desktopApplication.test.ts @@ -53,6 +53,35 @@ const put = async (root: string, path: string, bytes: Buffer | string) => { await writeFile(destination, bytes); }; +it.skipIf(process.platform !== "linux")( + "keeps desktop sibling files with partial coverage for invalid UTF-8 names", + async () => { + const root = await createTestTempDirectory("rea-desktop-filename-bytes-"); + const rawName = Buffer.from([ + 0x62, 0x61, 0x64, 0xff, 0x2e, 0x74, 0x78, 0x74, + ]); + const rawPath = Buffer.concat([Buffer.from(`${root}/`), rawName]); + await Promise.all([ + writeFile(rawPath, "RAW_FILE"), + writeFile(join(root, "bad\ufffd.txt"), "UNICODE_FILE"), + writeFile(join(root, "good.txt"), "GOOD_FILE"), + ]); + + const { report } = await inspect(root); + expect(report.coverage).toMatchObject({ + status: "partial", + entries_examined: 3, + }); + expect( + report.files.filter(({ path }) => path === "bad\ufffd.txt"), + ).toHaveLength(1); + expect(report.files.map(({ path }) => path)).toContain("good.txt"); + expect(report.limitations.join(" ")).toContain( + `First raw basename bytes: ${rawName.toString("hex")}`, + ); + }, +); + it("joins real PE/CLI reference facts and independent engine markers without picking a winner", async () => { const root = await createTestTempDirectory("rea-desktop-mixed-"); await put(