feat(harmony): disclose the unread pkgContextInfo.json declaration (#9096)

* feat(harmony): disclose the unread pkgContextInfo.json declaration

A package that ships pkgContextInfo.json shows the entry in
unclassified_components, yet nothing said its isSO declarations were
skipped: bridge_candidates came only from the libs/<abi>/lib<name>.so
path rule. State that gap in limitations when the entry is inventoried,
so a path-convention hypothesis is not read as a package declaration.

The declaration-parsing half of #4489 needs a decision about who owns
reading inventoried file content; this projection receives identity and
digests only, and the shared inventory carrier guard rejects other
Evidence. No content is parsed here.

* test(harmony): assert the disclosure by its names, not its sentence

docs/testing.md requires asserting structured facts instead of freezing an
explanatory sentence, and says a replaced prose assertion must still pass on a
harmless wording change while failing when the fact is lost. The first version of
this test compared the whole limitation string verbatim, so any rewording of the
disclosure would have failed CI.

The assertions now require exactly one limitation naming pkgContextInfo.json,
carrying bridge_candidates, isSO, and the not-read/not-narrowed fact, and keep the
structural claim that the same package without the declaration file yields
identical bridge candidates and no such limitation.
This commit is contained in:
Ace authored and GitHub committed 2026-11-06 14:26:41 +08:00
1 parent 8766bcef59
commit c7b0e0dec6
3 files changed
+75 -1

No files matched your search

+6
View File
@@ -82,6 +82,12 @@ boundaries are out of scope for this increment and stay in the projection's
`limitations` array until a dedicated HarmonyOS provider exists. REA fails
closed on packages whose inventory cannot be authenticated.
`pkgContextInfo.json` is inventoried and listed as an unclassified component but
never read, so its `isSO` declarations do not narrow `bridge_candidates`. When
that entry is present the projection adds a limitation naming the unread
declaration, so a path-convention hypothesis is not mistaken for a package
declaration.
## Verification lane
`npm run fixtures:harmony` downloads the SHA-256-pinned 1.0.1 HAP release
+10
View File
@@ -71,6 +71,15 @@ export type HarmonyApplicationProjectionResult = z.infer<
>;
type Component = z.infer<typeof projectedComponentSchema>;
const PKG_CONTEXT_NOT_READ =
"pkgContextInfo.json is inventoried but not read, so bridge_candidates are not narrowed by its isSO declarations; narrowing requires parsed file content that this projection does not receive.";
/** Whether the package carries the build-output declaration this projection cannot read. */
const pkgContextDeclared = (
files: readonly ProjectedInventoryEntry[],
): boolean =>
files.some(({ path }) => /(?:^|\/)pkgContextInfo\.json$/iu.test(path));
/** Project exact HarmonyOS package paths and hashes without decoding or executing target code. */
export const projectHarmonyInventory = (
source: ParsedInventorySet,
@@ -122,6 +131,7 @@ export const projectHarmonyInventory = (
"Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid Ark bytecode.",
"The signing component lists META-INF certificate-entry suffixes only; signatures applied by the packaging tool live in the ZIP signing block rather than archive entries, so an empty signing list does not mean the package is unsigned.",
"Bridge candidates are path-based hypotheses, not decoded N-API declarations or observed runtime calls.",
...(pkgContextDeclared(fileEntries) ? [PKG_CONTEXT_NOT_READ] : []),
...(rootFormat === "app-pack"
? [
"App Pack child packages are listed by inventory path only; their contents are not recursively inventoried by this projection.",
@@ -17,7 +17,10 @@ import {
import { projectHarmonyApplicationEvidence } from "../../../../src/application/harmony/HarmonyApplicationService.js";
import { createDirectAnalysis } from "../../../../src/composition/directAnalysis.js";
import { harmonyApplicationProjectionResultSchema } from "../../../../src/domain/harmony/harmonyApplication.js";
import {
harmonyApplicationProjectionResultSchema,
type HarmonyApplicationProjectionResult,
} from "../../../../src/domain/harmony/harmonyApplication.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import { createEvidence } from "../../../../src/domain/evidence.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
@@ -39,6 +42,61 @@ const stageHapEntries = [
{ path: "META-INF/FIXTURE.p7b", content: "opaque signing" },
] as const;
const PKG_CONTEXT_NAME = "pkgContextInfo.json";
const unreadDeclarationLimitations = (
limitations: readonly string[],
): string[] => limitations.filter((text) => text.includes(PKG_CONTEXT_NAME));
const projectStageHap = async (
label: string,
entries: readonly { path: string; content: string | Uint8Array }[],
): Promise<HarmonyApplicationProjectionResult> => {
const path = await writeApplicationZip(label, "Fixture.hap", entries);
return requireDeterministicProjection(
projectHarmonyApplicationEvidence,
await inventoryApplicationPackage(path),
harmonyApplicationProjectionResultSchema,
);
};
const stageHapWithDeclaration = stageHapEntries.map(({ path, content }) => ({
path,
content,
}));
describe("HarmonyOS N-API declaration disclosure", () => {
it("discloses the unread declaration by its names, not its wording", async () => {
const projection = await projectStageHap("rea-harmony-pkg-context-", [
...stageHapWithDeclaration,
{
path: PKG_CONTEXT_NAME,
content: '{"libentry.so":{"isSO":true}}',
},
]);
const control = await projectStageHap(
"rea-harmony-pkg-context-control-",
stageHapWithDeclaration,
);
const disclosed = unreadDeclarationLimitations(projection.limitations);
expect(disclosed).toHaveLength(1);
expect(disclosed[0]).toContain("bridge_candidates");
expect(disclosed[0]).toMatch(/\bisSO\b/);
expect(disclosed[0]).toMatch(/not read|not narrowed/iu);
expect(projection.bridge_candidates).toEqual(control.bridge_candidates);
expect(unreadDeclarationLimitations(control.limitations)).toEqual([]);
});
it("does not disclose a declaration the package does not carry", async () => {
const projection = await projectStageHap(
"rea-harmony-no-pkg-context-",
stageHapWithDeclaration,
);
expect(unreadDeclarationLimitations(projection.limitations)).toEqual([]);
});
});
describe("HarmonyOS unclassified inventory", () => {
it("retains distinct unclassified paths sharing content with a classified file", async () => {
const path = await writeApplicationZip(