From ebe4e879e8d108f7d235f05f96d0cdc2d07ec7d4 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:39:34 +0800 Subject: [PATCH] fix(harmony): retain path-only package and signing observations --- docs/harmony-analysis.md | 9 +++ src/domain/harmony/harmonyApplication.ts | 30 ++++++--- .../filesystem/harmony/application.test.ts | 62 +++++++++++++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/docs/harmony-analysis.md b/docs/harmony-analysis.md index b5817b7236..468cdd347f 100644 --- a/docs/harmony-analysis.md +++ b/docs/harmony-analysis.md @@ -84,3 +84,12 @@ not been independently established. This lane does not complete the pinned toolchain acceptance in #1431 and does not verify Ark decoding or signatures. Set `REA_HARMONY_ENTRYPOINT` to an installed package's `scripts/rea.mjs` to run the same checks against that package; the default lane uses the repository build. + +Packaging-model and App Pack child presence use inventoried file/slice paths even +when encrypted content cannot be authenticated. Unavailable App Pack children +retain their path, format, encryption and limitations with null artifact ID and +digest; they do not establish decoded package semantics. Missing source nodes +outside supplied pages retain the inventory limitation and partial coverage. +The signing list includes inventoried META-INF RSA, DSA and EC certificate-block +paths alongside the existing suffixes; opaque content does not establish a valid +signature or certificate. diff --git a/src/domain/harmony/harmonyApplication.ts b/src/domain/harmony/harmonyApplication.ts index de48b6e265..91f3562104 100644 --- a/src/domain/harmony/harmonyApplication.ts +++ b/src/domain/harmony/harmonyApplication.ts @@ -44,7 +44,9 @@ export const harmonyApplicationProjectionResultSchema = z.strictObject({ signing: z.array(projectedComponentSchema), }), unavailable_components: z.array(unavailableComponentSchema), - app_pack_children: z.array(projectedComponentSchema), + app_pack_children: z.array( + z.union([projectedComponentSchema, unavailableComponentSchema]), + ), runtime_families: z.array( z.enum(["ark", "native", "javascript", "react-native", "flutter", "unity"]), ), @@ -82,6 +84,17 @@ export const projectHarmonyInventory = ( const unavailable = entries.flatMap(({ unavailable }) => unavailable === undefined ? [] : [unavailable], ); + const fileEntries = entries.filter( + ({ kind }) => kind === "file" || kind === "slice", + ); + const children: HarmonyApplicationProjectionResult["app_pack_children"] = []; + if (rootFormat === "app-pack") { + for (const entry of fileEntries) { + if (!/\.(?:hap|hsp)$/iu.test(entry.path)) continue; + const child = entry.component ?? entry.unavailable; + if (child !== undefined) children.push(child); + } + } const classified = classify(all); const bridgeProjection = bridgeCandidates( classified.bytecode, @@ -122,13 +135,12 @@ export const projectHarmonyInventory = ( source_evidence_ids: evidence .map(({ evidence_id: id }) => id) .sort(compareUnicodeCodePoints), - packaging_model: packagingModel(classified.manifests), + packaging_model: packagingModel( + fileEntries.filter(({ path }) => !isResourcePath(path)), + ), components: classified, unavailable_components: unavailable, - app_pack_children: - rootFormat === "app-pack" - ? all.filter(({ path }) => /\.(?:hap|hsp)$/iu.test(path)) - : [], + app_pack_children: children, runtime_families: runtimeFamilies(all, classified), bridge_candidates: bridgeProjection.candidates, bridge_candidate_coverage: bridgeProjection.coverage, @@ -171,7 +183,7 @@ const harmonyRootFormat = ( }; const packagingModel = ( - manifests: readonly Component[], + manifests: readonly { readonly path: string }[], ): HarmonyApplicationProjectionResult["packaging_model"] => { const stage = manifests.some(({ path }) => /(?:^|\/)module\.json$/iu.test(path), @@ -199,7 +211,9 @@ const classify = (all: readonly Component[]) => ({ format === "javascript-bundle" || /\.(?:m?js|cjs)$/iu.test(path), ), signing: all.filter(({ path }) => - /^META-INF\/[^/]+\.(?:p7b|sig|cer|crt|pem|provision)$/iu.test(path), + /^META-INF\/[^/]+\.(?:p7b|sig|cer|crt|pem|provision|rsa|dsa|ec)$/iu.test( + path, + ), ), }); diff --git a/tests/boundary/filesystem/harmony/application.test.ts b/tests/boundary/filesystem/harmony/application.test.ts index b1ee89bb59..6b4625608f 100644 --- a/tests/boundary/filesystem/harmony/application.test.ts +++ b/tests/boundary/filesystem/harmony/application.test.ts @@ -36,6 +36,68 @@ const stageHapEntries = [ { path: "META-INF/FIXTURE.p7b", content: "opaque signing" }, ] as const; +it.each(["hap", "app"] as const)( + "retains encrypted path-only packaging facts for a %s inventory", + async (suffix) => { + const root = await createTestTempDirectory("rea-harmony-encrypted-paths-"); + const path = join(root, `Fixture.${suffix}`); + const writer = new ZipWriter(new Uint8ArrayWriter()); + const member = suffix === "app" ? "entry.hap" : "module.json"; + await writer.add(member, new TextReader("opaque inaccessible content"), { + password: "fixture-only", + }); + await writeFile(path, await writer.close()); + const inventory = await inventoryApplicationPackage(path); + const graph = requireDeterministicProjection( + projectHarmonyApplicationEvidence, + inventory, + harmonyApplicationProjectionResultSchema, + ); + expect(graph.unavailable_components).toContainEqual( + expect.objectContaining({ + path: member, + artifact_id: null, + sha256: null, + encrypted: true, + }), + ); + expect(graph.coverage.status).toBe("partial"); + if (suffix === "app") + expect(graph.app_pack_children).toContainEqual( + expect.objectContaining({ + path: member, + artifact_id: null, + sha256: null, + }), + ); + else expect(graph.packaging_model).toBe("stage"); + }, +); + +it("retains inventoried certificate-block suffixes without claiming verified signatures", async () => { + const names = ["CERT.RSA", "CERT.DSA", "CERT.EC", "FIXTURE.p7b"]; + const path = await writeApplicationZip( + "rea-harmony-certificate-paths-", + "Fixture.hap", + [ + ...names.map((name) => ({ + path: `META-INF/${name}`, + content: "opaque bytes", + })), + { path: "META-INF/MANIFEST.MF", content: "manifest" }, + { path: "META-INF/CERT.SF", content: "signature file" }, + ], + ); + const graph = requireDeterministicProjection( + projectHarmonyApplicationEvidence, + await inventoryApplicationPackage(path), + harmonyApplicationProjectionResultSchema, + ); + expect(graph.components.signing.map(({ path }) => path).sort()).toEqual( + names.map((name) => `META-INF/${name}`).sort(), + ); +}); + describe("HarmonyOS application projection", () => { it.each(["js", "mjs", "cjs", "jsx", "ts", "tsx", "mts", "cts"])( "retains the JavaScript runtime hint for an authenticated .%s component",