fix(harmony): retain path-only package and signing observations

This commit is contained in:
morluto
2026-10-24 02:39:34 +08:00
parent 6c4313b93e
commit ebe4e879e8
3 changed files with 93 additions and 8 deletions
+9
View File
@@ -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.
+22 -8
View File
@@ -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,
),
),
});
@@ -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",