mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(firmware): name missing extractor dependencies in partial extraction (#1879)
Unblob reports a missing external extractor as an
ExtractorDependencyNotFoundReport nested inside a chunk or multi-file
report, and normalizeUnblobReport forwarded that value verbatim into
diagnostics as { input_path, report }. A caller that reached partial
coverage therefore had to decode the raw producer report to learn which
extractor was absent and at which stage, even though the report already
names the dependencies.
Extraction diagnostics now normalize those entries: the named
dependencies are collected from the report and any nested value, the
producing handler is recorded as the stage, and the original report stays
retained for provenance. Entries without a dependency report keep their
previous shape.
Validation: FirmwareReports unit cases cover the chunk-nested and
multi-file-nested dependency reports and the retained raw report;
firmware suite passes; typecheck, oxlint and oxfmt on changed files. Real
Unblob extraction is unverified here because it needs the external
extractor stack.
This commit is contained in:
@@ -77,6 +77,47 @@ describe("firmware producer boundaries", () => {
|
||||
});
|
||||
|
||||
describe("Unblob multi-file extraction diagnostics", () => {
|
||||
it("names a missing extractor dependency and stage while retaining the raw report", () => {
|
||||
const dependencyFailure = {
|
||||
__typename__: "ExtractorDependencyNotFoundReport",
|
||||
severity: "ERROR",
|
||||
dependencies: ["sasquatch"],
|
||||
};
|
||||
const record = root();
|
||||
const normalized = normalizeUnblobReport(
|
||||
[
|
||||
{
|
||||
...record,
|
||||
reports: [
|
||||
...record.reports.filter(
|
||||
(item) => item.__typename__ !== "UnknownChunkReport",
|
||||
),
|
||||
{
|
||||
__typename__: "ChunkReport",
|
||||
id: "gzip-id",
|
||||
start_offset: 0,
|
||||
end_offset: 20,
|
||||
size: 20,
|
||||
handler_name: "gzip",
|
||||
is_encrypted: false,
|
||||
extraction_reports: [dependencyFailure],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
context,
|
||||
);
|
||||
expect(normalized.coverage).toBe("partial");
|
||||
expect(normalized.diagnostics).toEqual([
|
||||
{
|
||||
input_path: "$input",
|
||||
stage: "gzip",
|
||||
missing_dependencies: ["sasquatch"],
|
||||
report: dependencyFailure,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains nested MultiFile extraction failures in partial coverage", () => {
|
||||
const outputPath = `${context.outputRoot}/multi`;
|
||||
const childTask = {
|
||||
@@ -130,7 +171,12 @@ describe("Unblob multi-file extraction diagnostics", () => {
|
||||
|
||||
expect(normalized.coverage).toBe("partial");
|
||||
expect(normalized.diagnostics).toEqual([
|
||||
{ input_path: "$output/multi", report: multiFileReport },
|
||||
{
|
||||
input_path: "$output/multi",
|
||||
stage: "multipart-firmware",
|
||||
missing_dependencies: ["sasquatch"],
|
||||
report: multiFileReport,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,6 +68,53 @@ const known = new Set([
|
||||
"MultiFileReport",
|
||||
]);
|
||||
|
||||
const dependencyFailure = z.object({
|
||||
__typename__: z.literal("ExtractorDependencyNotFoundReport"),
|
||||
dependencies: z.array(z.string().min(1)).min(1),
|
||||
});
|
||||
|
||||
/** Collect the named extractors from any dependency report nested in one value. */
|
||||
const missingDependencies = (value: unknown): string[] => {
|
||||
const found: string[] = [];
|
||||
const visit = (node: unknown): void => {
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) visit(item);
|
||||
return;
|
||||
}
|
||||
if (typeof node !== "object" || node === null) return;
|
||||
const failure = dependencyFailure.safeParse(node);
|
||||
if (failure.success) {
|
||||
for (const dependency of failure.data.dependencies)
|
||||
if (!found.includes(dependency)) found.push(dependency);
|
||||
return;
|
||||
}
|
||||
for (const child of Object.values(node)) visit(child);
|
||||
};
|
||||
visit(value);
|
||||
return found;
|
||||
};
|
||||
|
||||
/**
|
||||
* Name the missing extractors and the producing stage for one extraction
|
||||
* problem so a caller can act without decoding the raw report. The original
|
||||
* report stays retained alongside the normalized fields.
|
||||
*/
|
||||
const extractionDiagnostic = (
|
||||
inputPath: string,
|
||||
stage: string | null,
|
||||
report: JsonValue,
|
||||
): JsonValue => {
|
||||
const dependencies = missingDependencies(report);
|
||||
return {
|
||||
input_path: inputPath,
|
||||
...(stage === null ? {} : { stage }),
|
||||
...(dependencies.length === 0
|
||||
? {}
|
||||
: { missing_dependencies: dependencies }),
|
||||
report,
|
||||
};
|
||||
};
|
||||
|
||||
/** Interpret the pinned Binwalk report without inventing validated region lengths. */
|
||||
export const normalizeBinwalkReport = (
|
||||
raw: unknown,
|
||||
@@ -234,7 +281,13 @@ export const normalizeUnblobReport = (
|
||||
: null;
|
||||
if (detail !== null)
|
||||
for (const problem of detail.extraction_reports)
|
||||
diagnostics.push({ input_path: path, report: problem });
|
||||
diagnostics.push(
|
||||
extractionDiagnostic(
|
||||
path,
|
||||
detail.handler_name,
|
||||
jsonValueSchema.parse(problem),
|
||||
),
|
||||
);
|
||||
chunks.push({
|
||||
input_path: path,
|
||||
provider_id: parsed.id,
|
||||
@@ -253,7 +306,13 @@ export const normalizeUnblobReport = (
|
||||
if (item.__typename__ === "MultiFileReport") {
|
||||
const parsed = multiFile.parse(item);
|
||||
if (parsed.extraction_reports.length > 0)
|
||||
diagnostics.push({ input_path: path, report: item });
|
||||
diagnostics.push(
|
||||
extractionDiagnostic(
|
||||
path,
|
||||
parsed.handler_name,
|
||||
jsonValueSchema.parse(item),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +308,8 @@ it.each(["depth", "dependency", "link"])(
|
||||
expect(normalized.diagnostics).toEqual([
|
||||
{
|
||||
input_path: "$input",
|
||||
stage: "gzip",
|
||||
missing_dependencies: ["sasquatch"],
|
||||
report: {
|
||||
__typename__: "ExtractorDependencyNotFoundReport",
|
||||
severity: "ERROR",
|
||||
|
||||
Reference in New Issue
Block a user