mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
+10
-2
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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] },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user