fix(firmware): derive extraction failure from producer severity
This commit is contained in:
1 parent
a732bd0656
commit
243790adc9
5 files changed
+198
-7
No files matched your search
@@ -139,6 +139,11 @@ build's source revision, so that field remains unknown.
|
||||
share recovery prose without losing any region identity. Instructions use
|
||||
already configured capabilities where one exists. Every extraction rerun
|
||||
requires a fresh absent `output_directory`, preserving the prior evidence.
|
||||
Unblob's declared `ERROR` severity establishes an extraction failure,
|
||||
including standalone `UnknownError` task exceptions. Warnings and resolved
|
||||
extraction problems remain diagnostics with partial coverage; they do not
|
||||
establish failed extractor stages. Standalone errors do not acquire a nearby
|
||||
chunk's handler or byte range.
|
||||
Nested unknown chunks use `inspect_firmware_regions` on the actual published
|
||||
child rather than repeating the same extraction with unchanged handlers.
|
||||
Inspection requires separately configured Binwalk (`REA_BINWALK_COMMAND`);
|
||||
|
||||
@@ -382,6 +382,116 @@ describe("firmware producer chunk failures", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("firmware producer error severity", () => {
|
||||
const outcomes = [
|
||||
{
|
||||
name: "task exception",
|
||||
report: {
|
||||
__typename__: "UnknownError",
|
||||
severity: "ERROR",
|
||||
exception: "inert fixture",
|
||||
},
|
||||
failed: true,
|
||||
},
|
||||
{
|
||||
name: "future declared error",
|
||||
report: { __typename__: "FutureProducerError", severity: "ERROR" },
|
||||
failed: true,
|
||||
},
|
||||
{
|
||||
name: "command warning",
|
||||
report: {
|
||||
__typename__: "ExtractCommandFailedReport",
|
||||
severity: "WARNING",
|
||||
command: "fixture",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exit_code: 1,
|
||||
},
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
name: "resolved extraction problem",
|
||||
report: {
|
||||
__typename__: "ExtractionProblem",
|
||||
problem: "duplicate path",
|
||||
resolution: "kept first entry",
|
||||
path: "config",
|
||||
},
|
||||
failed: false,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(["task", "chunk", "multi-file"] as const)(
|
||||
"preserves diagnostic outcomes at the %s owner",
|
||||
(owner) => {
|
||||
for (const { report, failed } of outcomes) {
|
||||
const record = root();
|
||||
const observation =
|
||||
owner === "task"
|
||||
? report
|
||||
: owner === "chunk"
|
||||
? {
|
||||
__typename__: "ChunkReport",
|
||||
id: "stage-id",
|
||||
handler_name: "gzip",
|
||||
start_offset: 0,
|
||||
end_offset: 20,
|
||||
size: 20,
|
||||
is_encrypted: false,
|
||||
extraction_reports: [report],
|
||||
}
|
||||
: {
|
||||
__typename__: "MultiFileReport",
|
||||
id: "stage-id",
|
||||
handler_name: "gzip",
|
||||
name: "rootfs",
|
||||
paths: [],
|
||||
extraction_reports: [report],
|
||||
};
|
||||
const normalized = normalizeUnblobReport(
|
||||
[
|
||||
{
|
||||
...record,
|
||||
reports: [
|
||||
...record.reports.filter(
|
||||
(item) => item.__typename__ !== "UnknownChunkReport",
|
||||
),
|
||||
observation,
|
||||
],
|
||||
},
|
||||
],
|
||||
context,
|
||||
);
|
||||
expect(normalized.coverage).toBe("partial");
|
||||
expect(normalized.diagnostics).toHaveLength(1);
|
||||
expect(normalized.diagnostics[0]).toMatchObject({
|
||||
input_path: "$input",
|
||||
report: owner === "multi-file" ? observation : report,
|
||||
});
|
||||
if (owner === "chunk") {
|
||||
expect(normalized.chunks[0]?.extraction_failed).toBe(failed);
|
||||
expect(normalized.failed_extraction_stages).toEqual([]);
|
||||
} else {
|
||||
expect(normalized.failed_extraction_stages).toEqual(
|
||||
failed
|
||||
? [
|
||||
{
|
||||
input_path: "$input",
|
||||
provider_id: owner === "task" ? null : "stage-id",
|
||||
handler: owner === "task" ? null : "gzip",
|
||||
dependencies: [],
|
||||
kind: "extractor-failure",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("firmware task dependency failures", () => {
|
||||
it.each([{ dependencies: [] }, { dependencies: ["", ""] }])(
|
||||
"retains unnamed dependency failures ($dependencies)",
|
||||
@@ -686,9 +796,9 @@ describe("failed multi-file extraction stages", () => {
|
||||
is_multi_file: true,
|
||||
};
|
||||
const stageFailure = {
|
||||
__typename__: "ExtractorExecutionErrorReport",
|
||||
__typename__: "UnknownError",
|
||||
severity: "ERROR",
|
||||
handler: "multipart-firmware",
|
||||
exception: "inert multi-file extraction failure",
|
||||
};
|
||||
const normalized = normalizeUnblobReport(
|
||||
[
|
||||
|
||||
@@ -61,6 +61,12 @@ const missingDependency = z.object({
|
||||
__typename__: z.literal("ExtractorDependencyNotFoundReport"),
|
||||
dependencies: z.array(z.string()),
|
||||
});
|
||||
// Unblob reports resolved extraction problems and warnings alongside errors.
|
||||
// The declared severity owns failure; a diagnostic's presence or class name
|
||||
// alone does not establish that extraction failed.
|
||||
const extractionError = z.object({ severity: z.literal("ERROR") });
|
||||
const hasExtractionError = (reports: readonly unknown[]): boolean =>
|
||||
reports.some((item) => extractionError.safeParse(item).success);
|
||||
const region = z.object({
|
||||
id: z.string(),
|
||||
start_offset: integer,
|
||||
@@ -359,7 +365,8 @@ export const normalizeUnblobReport = (
|
||||
);
|
||||
noteMissingDependencies(path, [item]);
|
||||
if (
|
||||
item.__typename__ === "ExtractorExecutionErrorReport" &&
|
||||
hasExtractionError([item]) &&
|
||||
!missingDependency.safeParse(item).success &&
|
||||
!taskFailures.has(path)
|
||||
) {
|
||||
taskFailures.add(path);
|
||||
@@ -396,7 +403,7 @@ export const normalizeUnblobReport = (
|
||||
.parse(item)
|
||||
: null;
|
||||
const extractionFailed =
|
||||
detail !== null && detail.extraction_reports.length > 0;
|
||||
detail !== null && hasExtractionError(detail.extraction_reports);
|
||||
if (detail !== null)
|
||||
for (const problem of detail.extraction_reports) {
|
||||
diagnostics.push(
|
||||
@@ -446,6 +453,11 @@ export const normalizeUnblobReport = (
|
||||
// The report owns its failure and dependencies. A produced subtask
|
||||
// is a derivation, not a new owner for the parent's report.
|
||||
const dependencies = dependencyNames(parsed.extraction_reports);
|
||||
if (
|
||||
dependencies === null &&
|
||||
!hasExtractionError(parsed.extraction_reports)
|
||||
)
|
||||
continue;
|
||||
const rows =
|
||||
dependencies === null
|
||||
? [{ dependencies: [] as readonly string[] }]
|
||||
|
||||
@@ -524,6 +524,49 @@ it("reports a failed multi-file stage as an extractor-failure gap", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["task-error", "task-warning"])(
|
||||
"preserves standalone diagnostic recovery without inventing stage ownership: %s",
|
||||
async (mode) => {
|
||||
const fixture = await firmwareFixture(mode);
|
||||
const result = await fixture.service.execute("extract_firmware", {
|
||||
path: fixture.path,
|
||||
output_directory: fixture.output,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const normalized = firmwareResultSchemas.extract_firmware.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
const failures = gapsWithContinuations(normalized).filter(
|
||||
(gap) => gap.kind === "extractor-failure",
|
||||
);
|
||||
expect(normalized.diagnostics).toContainEqual(
|
||||
expect.objectContaining({
|
||||
input_path: "$input",
|
||||
report: expect.objectContaining({
|
||||
__typename__:
|
||||
mode === "task-error"
|
||||
? "UnknownError"
|
||||
: "ExtractCommandFailedReport",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
if (mode === "task-error") {
|
||||
expect(failures).toEqual([
|
||||
expect.objectContaining({
|
||||
input_path: "$input",
|
||||
handler: null,
|
||||
provider_id: null,
|
||||
range: null,
|
||||
root_file_range: null,
|
||||
dependencies: [],
|
||||
continuation: expect.stringContaining("diagnostics"),
|
||||
}),
|
||||
]);
|
||||
} else expect(failures).toEqual([]);
|
||||
await assertFirmwareCleanup(fixture.launches);
|
||||
},
|
||||
);
|
||||
|
||||
it("carries the owning multi-file stage on a task-level dependency gap", async () => {
|
||||
const fixture = await firmwareFixture("multi-file-dependency");
|
||||
const result = await fixture.service.execute("extract_firmware", {
|
||||
|
||||
Vendored
+24
-3
@@ -325,9 +325,9 @@ if (args.includes("--version")) {
|
||||
dependencies: ["sasquatch"],
|
||||
}
|
||||
: {
|
||||
__typename__: "ExtractorExecutionErrorReport",
|
||||
__typename__: "UnknownError",
|
||||
severity: "ERROR",
|
||||
handler: "tar",
|
||||
exception: "inert multi-file extraction failure",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -381,6 +381,23 @@ if (args.includes("--version")) {
|
||||
},
|
||||
];
|
||||
}
|
||||
if (mode === "task-error" || mode === "task-warning")
|
||||
records[1].reports.push(
|
||||
mode === "task-error"
|
||||
? {
|
||||
__typename__: "UnknownError",
|
||||
severity: "ERROR",
|
||||
exception: "inert task exception",
|
||||
}
|
||||
: {
|
||||
__typename__: "ExtractCommandFailedReport",
|
||||
severity: "WARNING",
|
||||
command: "fixture",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exit_code: 1,
|
||||
},
|
||||
);
|
||||
if (
|
||||
mode !== "depth-reentry" &&
|
||||
mode !== "depth-orphan" &&
|
||||
@@ -393,7 +410,11 @@ if (args.includes("--version")) {
|
||||
? "["
|
||||
: JSON.stringify(records),
|
||||
);
|
||||
if (mode === "dependency" || mode === "multi-file-dependency")
|
||||
if (
|
||||
mode === "dependency" ||
|
||||
mode === "multi-file-dependency" ||
|
||||
mode === "task-error"
|
||||
)
|
||||
process.exitCode = 1;
|
||||
if (
|
||||
mode === "workspace-cleanup-failure" ||
|
||||
|
||||
Reference in new issue
Block a user