fix(rust): preserve historical cargo-auditable feature facts

Accept observed feature arrays, preserve omitted values as unknown across producer revisions, and keep older retained reports compatible. Reuse bounded JSON sizing before graph expansion.

Fixes #6622
This commit is contained in:
morluto
2026-10-23 18:49:19 +08:00
parent 1ece9630e6
commit 9c4dd7eb23
11 changed files with 288 additions and 13 deletions
+10 -2
View File
@@ -91,14 +91,22 @@ section is present, `auditable_metadata` contains:
- `compressed_bytes` and `uncompressed_bytes`;
- `format_version`, the embedded integer, or `null` when the field is omitted;
- `packages`, in the section's original order, with `name`, `version`, `source`,
`kind`, `dependencies`, and `root`;
`kind`, `dependencies`, `features`, and `root`;
- `root_package_index`, the single package marked `root`, or `null` when none is marked;
- `dependency_edges`, pairs of package indices in package-then-dependency order;
- `cyclic_dependency_indices`, sorted unique indices that participate in a cycle.
An omitted `kind` stays `null`. The embedded format treats that omission as
runtime, and that default is not stored in the section. An omitted dependency
list is an empty array. An omitted `root` flag is false.
list is an empty array. An omitted `features` field is `null`, because it does
not establish whether features were empty or the producer did not record them;
an explicit empty array establishes an empty list. An omitted `root` flag is
false.
Historical [cargo-auditable 0.4 package records](https://github.com/rust-secure-code/cargo-auditable/blob/v0.4.0/auditable-serde/src/lib.rs)
may include the package `features` list; REA preserves those strings. That
version omitted empty feature lists during serialization, while version 0.5
removed the field entirely, as recorded in its
[changelog](https://github.com/rust-secure-code/cargo-auditable/blob/v0.5.0/CHANGELOG.md).
`rustc_commit` is present only when a `/rustc/<40-hex>/` path is found in
read-only data. Its `evidence_kind` is `inferred`. The hash is not a claim that
@@ -34,6 +34,7 @@ const report: RustBinary = {
source: "local",
kind: null,
dependencies: [],
features: null,
root: true,
},
],
@@ -27,6 +27,7 @@ const report: RustBinary = {
kind: null,
root: true,
dependencies: [0],
features: [],
},
],
root_package_index: 0,
@@ -76,6 +77,30 @@ it("revalidates mutable and merely frozen Rust imports despite a claimed seal",
).toBe(false);
});
it("pages retained Rust rows from before feature metadata was added as unknown", () => {
const previous = structuredClone(report);
const metadata = previous.auditable_metadata;
if (metadata === null) throw new Error("Expected Rust metadata");
const legacy = {
...previous,
auditable_metadata: {
...metadata,
packages: metadata.packages.map(({ features: _features, ...pkg }) => pkg),
},
} as unknown as RustBinary;
const result = projectAnalysisView(parent(legacy), {
kind: "rust",
collection: "packages",
fields: ["features"],
offset: 0,
limit: 10,
});
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
page: { rows: [{ index: 0, features: null }] },
});
});
it("checks exact artifact identity and subject architecture for every Rust view", () => {
for (const changed of [
{ local_path: "/different" },
@@ -121,6 +146,7 @@ it("bounds inherited warnings and retains cycle counts without walking a cyclic
it("pages matching original indices and omits dependency vectors unless selected", () => {
const value = structuredClone(report);
expect(value.auditable_metadata?.packages[0]?.features).toEqual([]);
value.auditable_metadata!.packages.push({
name: "other",
version: "1",
@@ -128,6 +154,7 @@ it("pages matching original indices and omits dependency vectors unless selected
kind: null,
root: false,
dependencies: [],
features: [],
});
const source = parent(value);
const first = projectAnalysisView(source, {
@@ -229,6 +229,9 @@ export const projectRustBinaryView = (
...(fields?.has("dependencies") === true && {
dependencies: row.dependencies,
}),
...(fields?.has("features") === true && {
features: row.features,
}),
}),
);
return ok({
+2
View File
@@ -31,6 +31,7 @@ export const RUST_AUDITABLE_FORMAT_VERSIONS = [0, 1, 8] as const;
const index = z.int().nonnegative();
const rustcHash = z.string().regex(/^[0-9a-f]{40}$/u);
export const rustPackageFeaturesSchema = z.array(z.string().min(1)).nullable();
/** One embedded package, with omitted kind and root preserved as null and false. */
export const rustPackageSchema = z.strictObject({
@@ -39,6 +40,7 @@ export const rustPackageSchema = z.strictObject({
source: z.string().min(1),
kind: z.enum(["runtime", "build"]).nullable(),
dependencies: z.array(index),
features: rustPackageFeaturesSchema.default(null),
root: z.boolean(),
});
+7 -1
View File
@@ -2,6 +2,7 @@ import { z } from "zod";
import {
inspectRustBinaryInputSchema,
rustPackageSchema,
rustPackageFeaturesSchema,
rustAuditableMetadataSchema,
} from "./rustBinary.js";
@@ -44,7 +45,12 @@ export const rustBinaryViewRequestSchema = z.discriminatedUnion("collection", [
export const rustBinaryViewPageSchema = z.discriminatedUnion("collection", [
z.strictObject({
collection: z.literal("packages"),
rows: z.array(rustPackageSchema.partial().extend({ index })),
rows: z.array(
rustPackageSchema.partial().extend({
features: rustPackageFeaturesSchema.optional(),
index,
}),
),
}),
z.strictObject({
collection: z.literal("edges"),
+52
View File
@@ -58,6 +58,58 @@ it("preserves package order, omitted kind, and the root index", () => {
expect(parsed.uncompressed_bytes).toBeGreaterThan(parsed.compressed_bytes);
});
it("preserves historical cargo-auditable feature lists and rejects bad values", () => {
const parsed = section({
format: 0,
packages: [
{
name: "feature_dep",
version: "1.2.3",
source: "crates.io",
features: ["default", "serde"],
},
],
});
expect(parsed.packages).toEqual([
{
name: "feature_dep",
version: "1.2.3",
source: "crates.io",
kind: null,
dependencies: [],
features: ["default", "serde"],
root: false,
},
]);
const omitted = section({
packages: [{ name: "no_features", version: "1", source: "crates.io" }],
});
const explicitEmpty = section({
packages: [
{
name: "no_features",
version: "1",
source: "crates.io",
features: [],
},
],
});
expect(omitted.packages[0]?.features).toBeNull();
expect(explicitEmpty.packages[0]?.features).toEqual([]);
expect(() =>
section({
packages: [
{
name: "feature_dep",
version: "1.2.3",
source: "crates.io",
features: ["default", 7],
},
],
}),
).toThrow(RustAuditableFormatFailure);
});
it("reports dependency cycles and keeps an omitted format version unknown", () => {
const parsed = section({
packages: [
+36 -6
View File
@@ -5,6 +5,7 @@ import {
type RustAuditableMetadata,
type RustPackage,
} from "../domain/rust/rustBinary.js";
import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
/** Selected-file admission limit shared with the stable artifact reader. */
export const RUST_BINARY_INPUT_BYTES = 256 * 1024 * 1024;
@@ -24,6 +25,7 @@ const packageSchema = z.strictObject({
source: z.string().min(1),
kind: z.enum(["runtime", "build"]).optional(),
dependencies: z.array(z.int().nonnegative()).optional(),
features: z.array(z.string().min(1)).optional(),
root: z.boolean().optional(),
});
const documentVersionSchema = z.object({
@@ -259,6 +261,7 @@ const MIN_PACKAGE_OUTPUT_BYTES = JSON.stringify({
source: "a",
kind: null,
dependencies: [],
features: null,
root: true,
}).length;
const MIN_EDGE_OUTPUT_BYTES = JSON.stringify({
@@ -289,9 +292,27 @@ const admitGraphExpansion = (value: unknown): void => {
"Dependency graph exceeds the complete report byte budget before expansion",
);
};
const currentEdgeBytes = () =>
edges * MIN_EDGE_OUTPUT_BYTES + Math.max(0, edges - 1);
admit();
for (const pkg of packages) {
// Shape and index validity remain owned by documentSchema/rootIndex.
if (
typeof pkg === "object" &&
pkg !== null &&
"features" in pkg &&
Array.isArray(pkg.features) &&
pkg.features.every((feature) => typeof feature === "string")
) {
const featureBudget =
RUST_OUTPUT_BYTES - minimumBytes - currentEdgeBytes() + 4;
const featureBytes = jsonValueByteLength(
pkg.features as string[],
featureBudget,
);
minimumBytes += featureBytes - 4;
admit();
}
if (
typeof pkg !== "object" ||
pkg === null ||
@@ -312,6 +333,7 @@ const PACKAGE_FIELDS = new Set([
"source",
"kind",
"dependencies",
"features",
"root",
]);
@@ -336,6 +358,7 @@ const packageRecord = (
pkg: unknown,
): pkg is {
readonly dependencies?: readonly unknown[];
readonly features?: readonly unknown[];
} => {
if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
return false;
@@ -358,13 +381,19 @@ const packageRecord = (
if (record.root !== undefined && typeof record.root !== "boolean")
return false;
const dependencies = record.dependencies;
if (dependencies === undefined) return true;
if (
dependencies !== undefined &&
(!Array.isArray(dependencies) ||
!dependencies.every(
(entry) =>
typeof entry === "number" && Number.isInteger(entry) && entry >= 0,
))
)
return false;
const features = record.features;
return (
Array.isArray(dependencies) &&
dependencies.every(
(entry) =>
typeof entry === "number" && Number.isInteger(entry) && entry >= 0,
)
features === undefined ||
(Array.isArray(features) && features.every(nonEmptyString))
);
};
@@ -406,6 +435,7 @@ const normalizePackages = (
source: pkg.source,
kind: pkg.kind ?? null,
dependencies: pkg.dependencies ?? [],
features: pkg.features ?? null,
root: pkg.root ?? false,
}));
+139
View File
@@ -101,6 +101,145 @@ cliTest(
},
);
cliTest(
"preserves historical cargo-auditable features through CLI and stdio MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-features-legacy-");
const { client, call } = await connectLocalToolsMcp();
const validCases = [
{
name: "features",
document: {
packages: [
{
name: "legacy_dep",
version: "1.2.3",
source: "crates.io",
features: ["default", "serde"],
},
{
name: "app",
version: "0.1.0",
source: "local",
dependencies: [0],
root: true,
},
],
},
features: ["default", "serde"],
},
{
name: "omitted-features-unknown",
document: {
packages: [
{
name: "legacy_dep",
version: "1.2.3",
source: "crates.io",
},
],
},
features: null,
},
{
name: "explicit-empty-features",
document: {
packages: [
{
name: "legacy_dep",
version: "1.2.3",
source: "crates.io",
features: [],
},
],
},
features: [],
},
];
const malformedPath = join(root, "invalid-features.elf");
await writeFile(
malformedPath,
createRustImage({
format: "elf",
payload: rustDependencyPayload({
packages: [
{
name: "legacy_dep",
version: "1.2.3",
source: "crates.io",
features: ["default", 7],
},
],
}),
}),
);
for (const surface of ["cli", "mcp"] as const) {
for (const fixture of validCases) {
const path = join(root, `${fixture.name}.elf`);
await writeFile(
path,
createRustImage({
format: "elf",
payload: rustDependencyPayload(fixture.document),
}),
);
if (surface === "cli") {
const response = await cli.run({
arguments: ["inspect-rust-binary", path, "--json"],
});
expect(response.exitCode, response.stderr).toBe(0);
const evidence = toolContract(
"inspect_rust_binary",
).outputSchema.parse(response.json);
expect(
rustBinarySchema.parse(evidence.normalized_result)
.auditable_metadata?.packages[0]?.features,
).toEqual(fixture.features);
} else {
const response = await call("inspect_rust_binary", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const evidence = toolContract(
"inspect_rust_binary",
).outputSchema.parse(response.structuredContent);
expect(
rustBinarySchema.parse(evidence.normalized_result)
.auditable_metadata?.packages[0]?.features,
).toEqual(fixture.features);
}
}
if (surface === "cli") {
const response = await cli.run({
arguments: ["inspect-rust-binary", malformedPath, "--json"],
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "invalid_request",
details: {
operation: "inspect_rust_binary",
issues: [{ path: ["path"], reason: "invalid_format" }],
},
});
} else {
const response = await call("inspect_rust_binary", {
path: malformedPath,
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
details: {
operation: "inspect_rust_binary",
issues: [{ path: ["path"], reason: "invalid_format" }],
},
});
}
}
await client.ping();
},
30000,
);
cliTest("returns null metadata when .dep-v0 is absent", async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-cli-missing-");
const path = join(root, "plain");
+9 -2
View File
@@ -123,6 +123,7 @@ cliTest(
version: "1",
source: "crates.io",
dependencies: [2],
features: ["default"],
},
{
name: "same",
@@ -153,7 +154,7 @@ cliTest(
const selection = {
collection: "packages",
names: ["same"],
fields: ["name", "version", "dependencies"],
fields: ["name", "version", "dependencies", "features"],
};
const direct = await call("inspect_rust_binary", {
path,
@@ -169,7 +170,13 @@ cliTest(
page: {
collection: "packages",
rows: [
{ index: 0, name: "same", version: "1", dependencies: [2] },
{
index: 0,
name: "same",
version: "1",
dependencies: [2],
features: ["default"],
},
{ index: 1, name: "same", version: "2", dependencies: [0, 2] },
],
},
+2 -2
View File
@@ -19,7 +19,7 @@ const packages = [
source: "crates.io",
kind: null,
dependencies: [],
features: [],
features: null,
root: false,
},
{
@@ -28,7 +28,7 @@ const packages = [
source: "local",
kind: null,
dependencies: [0],
features: [],
features: null,
root: true,
},
];