* feat(harmony): project HarmonyOS package inventory evidence First HarmonyOS increment from #1431: HAP, HSP, and App Pack roots are classified through the hardened ZIP reader and projected into an execution-free application graph with packaging-model hints, exact component paths and hashes, App Pack children by path, runtime-family hints, and path-based N-API bridge hypotheses. HarmonyOS .har stays unclassified by suffix because .har also names HTTP Archive JSON. Adds the project_harmony_application_graph tool (CLI and MCP), synthetic composition coverage, a pinned real-package verify lane, and docs. * refactor(domain): share application graph projection scaffolding The HarmonyOS projection mirrors the Android and Apple projections by design, which pushed jscpd duplication over its 2.5 percent budget. Extract the identical pieces (inventory input schema, projected component schema and occurrence flattening, bridge candidate schema factory, coverage assembly, comparator, canonical digest) into domain/mobileApplicationGraphProjection.ts and route all three projections plus the composition fixtures through it. Duplication drops from 2.52 to 2.49 percent with no behavior change: projection ids, classifications, and limitations are byte-identical. * fix(harmony): address review, stable app-pack roots, and rotated fixture - rebased onto main; the shared projection scaffolding now keeps main's nativeRuntimeConvention loop and the harmony runtime families adopt the same react-native/flutter/unity path conventions - the .app App Pack suffix is now a root-input rule only (verified ZIP magic in classifyRootFormat and BinaryTargetResolver); nested .app ZIP members inside other packages keep the generic zip classification, with a regression test, so existing inventories do not change - the signing limitation now states that an empty signing list does not mean unsigned because packaging-tool signatures live in the ZIP signing block, and the app-pack root rule is disclosed as suffix-based - repinned the real-package fixture to the current VHome 2.6.14-beta asset after the upstream deleted v5.0.111-beta and replaced the v2.6.14-beta asset within hours; the pin records both observed digests --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import canonicalize from "canonicalize";
|
|
import { z } from "zod";
|
|
|
|
import type { ParsedInventorySet } from "./artifactInventoryEvidence.js";
|
|
import { evidenceSchema } from "./evidence.js";
|
|
import { digestSchema } from "./digests.js";
|
|
import { prefixedDigestSchema } from "./digests.js";
|
|
import {
|
|
bridgeCandidateCoverageSchema,
|
|
projectCartesianCandidates,
|
|
} from "./bridgeCandidateProjection.js";
|
|
|
|
/** Evidence-scoped identifier schemas shared by application graph projections. */
|
|
export const projectionEvidenceIdSchema = prefixedDigestSchema("ev");
|
|
export const projectionPathSchema = z.string().min(1);
|
|
|
|
/** Inventory pages accepted by every application graph projection. */
|
|
export const applicationInventoryProjectionInputSchema = z.strictObject({
|
|
inventory_evidence: z.array(evidenceSchema).min(1),
|
|
});
|
|
|
|
/** One authenticated inventory entry retained by application graph projections. */
|
|
export const projectedComponentSchema = z.strictObject({
|
|
path: projectionPathSchema,
|
|
artifact_id: prefixedDigestSchema("art"),
|
|
sha256: digestSchema,
|
|
format: z.string().min(1),
|
|
});
|
|
|
|
export type ProjectedComponent = z.infer<typeof projectedComponentSchema>;
|
|
|
|
/** Path-pair hypotheses connecting managed content to a native library. */
|
|
export const projectedBridgeCandidateSchema = <
|
|
Basis extends readonly [string, ...string[]],
|
|
>(
|
|
basis: Basis,
|
|
) =>
|
|
z.strictObject({
|
|
managed_path: projectionPathSchema,
|
|
native_path: projectionPathSchema,
|
|
basis: z.enum(basis),
|
|
});
|
|
|
|
/** Coverage status combining inventory completeness with a projection budget. */
|
|
export const projectionCoverage = (
|
|
inventory: ParsedInventorySet["inventory"],
|
|
budgetComplete: boolean,
|
|
): {
|
|
status: "complete-within-inventory" | "partial";
|
|
inventory_complete: boolean;
|
|
} => ({
|
|
status:
|
|
inventory.complete && budgetComplete
|
|
? ("complete-within-inventory" as const)
|
|
: ("partial" as const),
|
|
inventory_complete: inventory.complete,
|
|
});
|
|
|
|
/** Flatten every inventoried occurrence into authenticated path components. */
|
|
export const projectedComponents = (
|
|
inventory: ParsedInventorySet["inventory"],
|
|
): ProjectedComponent[] => {
|
|
const nodes = new Map(
|
|
inventory.nodes.map((node) => [node.artifact_id, node]),
|
|
);
|
|
return inventory.occurrences
|
|
.filter(
|
|
(occurrence) =>
|
|
occurrence.artifact_id !== null && occurrence.logical_path !== ".",
|
|
)
|
|
.map((occurrence) => {
|
|
const node = nodes.get(occurrence.artifact_id ?? "");
|
|
if (node === undefined)
|
|
throw new TypeError(
|
|
"Application projection occurrence has no artifact node",
|
|
);
|
|
return {
|
|
path: occurrence.logical_path,
|
|
artifact_id: node.artifact_id,
|
|
sha256: node.sha256,
|
|
format: occurrence.artifact_format,
|
|
} satisfies ProjectedComponent;
|
|
});
|
|
};
|
|
|
|
export { bridgeCandidateCoverageSchema, projectCartesianCandidates };
|
|
|
|
/** Unicode code point ordering so projection arrays sort deterministically. */
|
|
export const compareProjectionStrings = (
|
|
left: string,
|
|
right: string,
|
|
): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
|
|
/** Digest a canonical JSON projection payload for its stable identifier. */
|
|
export const projectionDigest = (value: unknown, label: string): string => {
|
|
const encoded = canonicalize(value);
|
|
if (encoded === undefined)
|
|
throw new TypeError(`${label} is not canonical JSON`);
|
|
return createHash("sha256").update(encoded).digest("hex");
|
|
};
|