fix(javascript): accept the Wakaru 1.x release line and verify 1.14.0 (#1540)

* fix(javascript): accept the Wakaru 1.x release line and verify 1.14.0

Exact --version equality rejected every Wakaru build except 1.13.0, and
strict report schemas rejected any added field, so each upstream release
broke recovery until REA re-pinned. Wakaru now documents that --version,
--json and provenance.json change only by addition within a major version.

Admit ^1.13.0 with the verified/compatible split used for Binwalk and
Unblob, report the observed version instead of the pinned constant, and
add a limitation for unverified releases. Parse reports with loose
objects and keep the reported module status as a string; path, identity,
range and file-set checks stay exact.

Move the audited pin, submodule, CI binary and verifier to 1.14.0, and
document where to get the binary from the refusal message.

* fix(javascript): project known Wakaru warning fields

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Pionxzhandmorluto authored and GitHub committed 2026-10-10 23:02:15 +08:00
1 parent 0363e5a8e1
commit ded59689ce
16 files changed
+272 -66

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ import { RECOVERY_LIMITS } from "./WakaruRelease.js";
const OPERATION = "recover_javascript_sources";
const WAKARU_REQUIREMENT =
"Provide an absolute REA_WAKARU_COMMAND pointing to Wakaru 1.13.0 on Linux x64; no tool is installed by REA";
"Provide an absolute REA_WAKARU_COMMAND pointing to a Wakaru ^1.13.0 Linux x64 binary (verified with 1.14.0); no tool is installed by REA. See docs/javascript-recovery.md#install-wakaru.";
/** Inject the owned process launcher while retaining production protocol parsing. */
export type WakaruLauncher = (
+36 -32
View File
@@ -24,11 +24,8 @@ import {
} from "./WakaruCommand.js";
import { parseWakaruReports } from "./WakaruReport.js";
import { publishWakaruArtifacts } from "./WakaruPublication.js";
import {
RECOVERY_LIMITS,
WAKARU_RELEASE,
WAKARU_PROVIDER_IDENTITY,
} from "./WakaruRelease.js";
import { RECOVERY_LIMITS, WAKARU_PROVIDER_IDENTITY } from "./WakaruRelease.js";
import { admitWakaruVersion } from "./WakaruVersion.js";
const OPERATION = "recover_javascript_sources";
const MODES = {
@@ -66,12 +63,13 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
...commandContext,
arguments: ["--version"],
});
if (
version.exit_code !== 0 ||
version.stdout.trim() !== `wakaru ${WAKARU_RELEASE.version}`
) {
const admitted =
version.exit_code === 0
? admitWakaruVersion(version.stdout.trim())
: undefined;
if (admitted === undefined || admitted.status === "unsupported") {
// The message names the mismatch; the reason also keeps the tool's stderr.
const mismatch = `This adapter requires Wakaru ${WAKARU_RELEASE.version}; reported version: ${version.stdout.trim()}; exit: ${String(version.exit_code)}`;
const mismatch = `${admitted?.message ?? `Wakaru --version failed; reported version: ${version.stdout.trim()}`}; exit: ${String(version.exit_code)}`;
throw new AnalysisCapabilityUnavailableError(
"wakaru",
OPERATION,
@@ -106,20 +104,22 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
sourceBytes: snapshot,
files,
});
return { version, run, parsed };
return { version, admitted, run, parsed };
};
/** Publish validated recovery and retain exact source and engine bindings. */
export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
const { root, tree, input, source, engine, options } = context;
const staging = join(root, "modules");
const { version, run, parsed } = await executeWakaruRecovery(context);
const { version, admitted, run, parsed } =
await executeWakaruRecovery(context);
const result = await publishWakaruArtifacts({
tree,
staging,
input,
source,
engine,
admitted,
parsed,
execution: run,
...(options?.signal === undefined ? {} : { signal: options.signal }),
@@ -150,25 +150,29 @@ export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
OPERATION,
"Configured engine bytes changed during recovery",
);
return createAnalysisExecution(result, WAKARU_PROVIDER_IDENTITY, {
rawResult: jsonValueSchema.parse({
version,
execution: run,
report: parsed.report,
provenance: parsed.provenance,
}),
subject: {
path: source.path,
sha256: source.sha256,
format: "javascript",
return createAnalysisExecution(
result,
{ ...WAKARU_PROVIDER_IDENTITY, version: admitted.version },
{
rawResult: jsonValueSchema.parse({
version,
execution: run,
report: parsed.report,
provenance: parsed.provenance,
}),
subject: {
path: source.path,
sha256: source.sha256,
format: "javascript",
},
locations: [
{ kind: "artifact-path", path: source.path },
...result.modules.map((module) => ({
kind: "artifact-path" as const,
path: module.artifact.path,
})),
],
limitations: result.limitations,
},
locations: [
{ kind: "artifact-path", path: source.path },
...result.modules.map((module) => ({
kind: "artifact-path" as const,
path: module.artifact.path,
})),
],
limitations: result.limitations,
});
);
};
+17 -3
View File
@@ -22,6 +22,7 @@ import type {
resolveWakaruCommand,
runWakaruCommand,
} from "./WakaruCommand.js";
import type { WakaruVersionAdmission } from "./WakaruVersion.js";
type PublicationContext = {
tree: SafeOutputTree;
@@ -34,6 +35,7 @@ type PublicationContext = {
bytes: number;
};
engine: Awaited<ReturnType<typeof resolveWakaruCommand>>;
admitted: Exclude<WakaruVersionAdmission, { status: "unsupported" }>;
parsed: Awaited<ReturnType<typeof parseWakaruReports>>;
execution: Awaited<ReturnType<typeof runWakaruCommand>>;
signal?: AbortSignal;
@@ -86,7 +88,7 @@ export const publishWakaruArtifacts = async (
source: { ...source, published_copy: publishedCopy },
engine: {
id: "wakaru",
version: WAKARU_RELEASE.version,
version: context.admitted.version,
executable: {
path: context.engine.command,
sha256: context.engine.sha256,
@@ -107,7 +109,14 @@ export const publishWakaruArtifacts = async (
total: parsed.report.total,
failed: parsed.report.failed,
modules,
warnings: parsed.report.warnings,
warnings: parsed.report.warnings.map(
({ filename, kind, is_error, message }) => ({
filename,
kind,
is_error,
message,
}),
),
report,
provenance,
analysis_input:
@@ -118,7 +127,12 @@ export const publishWakaruArtifacts = async (
format: "directory" as const,
},
runtime_equivalence: "unknown" as const,
limitations: [...RECOVERY_LIMITATIONS],
limitations: [
...RECOVERY_LIMITATIONS,
...(context.admitted.status === "compatible"
? [context.admitted.limitation]
: []),
],
};
const manifest = await write(
"manifest.json",
+11 -3
View File
@@ -1,11 +1,19 @@
/** Unchanged upstream source pin; executed bytes are identified separately. */
export const WAKARU_RELEASE = {
version: "1.13.0",
revision: "6070266d24b32951fa2fe1d2dad5b4313bd5c81b",
version: "1.14.0",
revision: "8219bf5016a063d7fb215101a8c45d5959ec733f",
repository: "https://github.com/pionxzh/wakaru",
} as const;
/** Exact provider identity shared by executions and the generated catalog. */
/**
* Releases the report parser accepts. Wakaru changes `--version`, `--json`
* and `provenance.json` only by addition within a major version
* (https://github.com/pionxzh/wakaru/blob/v1.14.0/docs/cli.md#machine-readable-output-compatibility);
* 1.13.0 is the first release this adapter verified.
*/
export const WAKARU_ACCEPTED_RANGE = "^1.13.0";
/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */
export const WAKARU_PROVIDER_IDENTITY = {
id: "wakaru",
name: "Wakaru",
+12 -7
View File
@@ -10,20 +10,20 @@ import { readRecoveryFile, recoveryFailureMessage } from "./RecoveryFiles.js";
import { RECOVERY_LIMITS } from "./WakaruRelease.js";
import type { runWakaruCommand } from "./WakaruCommand.js";
const warningSchema = z.strictObject({
const warningSchema = z.looseObject({
filename: z.string(),
kind: z.string(),
is_error: z.boolean(),
message: z.string(),
});
const reportSchema = z.strictObject({
const reportSchema = z.looseObject({
detected_formats: z.array(z.string()),
safety: z.string(),
modules: z.array(
z.strictObject({
z.looseObject({
filename: z.string().min(1),
kind: z.literal("javascript"),
status: z.literal("decompiled"),
status: z.string().min(1),
}),
),
warnings: z.array(warningSchema),
@@ -35,12 +35,12 @@ const rangeSchema = z.tuple([
z.number().int().nonnegative(),
z.number().int().nonnegative(),
]);
const provenanceSchema = z.strictObject({
const provenanceSchema = z.looseObject({
format: z.string(),
strategy: z.string(),
modules: z.record(
z.string(),
z.strictObject({
z.looseObject({
input: z.string(),
ranges: z.array(rangeSchema),
extraction: z.string(),
@@ -59,7 +59,12 @@ type ReportContext = {
files: readonly { relativePath: string }[];
};
/** Parse exact pinned producer JSON before projecting provider-neutral artifacts. */
/**
* Parse producer JSON before projecting provider-neutral artifacts. Wakaru adds
* fields and category values within a major version, so unknown keys are kept
* and `status` stays the reported string; identity, path, range and file-set
* checks below remain exact.
*/
export const parseWakaruReports = async (context: ReportContext) => {
const { execution, provenancePath } = context;
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { admitWakaruVersion } from "./WakaruVersion.js";
import { WAKARU_RELEASE } from "./WakaruRelease.js";
describe("Wakaru release range", () => {
it("accepts the verified banner without an extra limitation", () => {
expect(admitWakaruVersion(`wakaru ${WAKARU_RELEASE.version}`)).toEqual({
status: "verified",
version: WAKARU_RELEASE.version,
});
});
it.each([
["wakaru 1.13.0", "1.13.0"],
["wakaru 1.15.2", "1.15.2"],
])("accepts %s as compatible", (banner, version) => {
const admitted = admitWakaruVersion(banner);
expect(admitted).toMatchObject({ status: "compatible", version });
if (admitted.status !== "compatible")
throw new Error("expected a compatible release");
expect(admitted.limitation).toContain(version);
expect(admitted.limitation).toContain(WAKARU_RELEASE.version);
});
it.each([
["wakaru 1.12.0", "wakaru 1.12.0"],
["wakaru 2.0.0", "wakaru 2.0.0"],
["wakaru 1.15.0-rc.1", "wakaru 1.15.0-rc.1"],
["wakaru v1.14.0", "wakaru v1.14.0"],
["1.14.0", "1.14.0"],
["", "missing"],
])("rejects banner %j", (banner, detail) => {
const admitted = admitWakaruVersion(banner);
if (admitted.status !== "unsupported")
throw new Error("expected a rejected banner");
expect(admitted.message).toContain(detail);
expect(admitted.message).toContain("^1.13.0");
});
});
+39
View File
@@ -0,0 +1,39 @@
import { satisfies, valid } from "semver";
import { WAKARU_ACCEPTED_RANGE, WAKARU_RELEASE } from "./WakaruRelease.js";
/** Admission of one observed `wakaru --version` banner. */
export type WakaruVersionAdmission =
| { readonly status: "verified"; readonly version: string }
| {
readonly status: "compatible";
readonly version: string;
readonly limitation: string;
}
| { readonly status: "unsupported"; readonly message: string };
/** Caller-facing accepted releases, naming the verified build. */
export const wakaruReleaseLine = (): string =>
`Wakaru ${WAKARU_ACCEPTED_RANGE} (verified with ${WAKARU_RELEASE.version})`;
/** Classify a `wakaru <version>` banner against the accepted release range. */
export const admitWakaruVersion = (banner: string): WakaruVersionAdmission => {
const token = banner.startsWith("wakaru ")
? banner.slice("wakaru ".length)
: undefined;
if (
token === undefined ||
valid(token) !== token ||
!satisfies(token, WAKARU_ACCEPTED_RANGE)
)
return {
status: "unsupported",
message: `Unsupported Wakaru version ${banner.length === 0 ? "missing" : banner}; accepted releases are ${wakaruReleaseLine()}`,
};
if (token === WAKARU_RELEASE.version)
return { status: "verified", version: token };
return {
status: "compatible",
version: token,
limitation: `This session uses Wakaru ${token}. The report parser is verified against ${WAKARU_RELEASE.version}; other ${WAKARU_ACCEPTED_RANGE} releases are accepted and the observed version is reported.`,
};
};