mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
5b7fe94710
Derive firmware completeness from examined coverage, retain linked continuation evidence, and preserve reported extraction limits. Keep Android package attribution and Go image fixtures coherent, and classify SQLite busy failures without discarding descriptor cleanup facts. Reviewed fixes include firmware, ADB, SQLite, and recorded-transport bug reports and their focused PRs. Validation: firmware report source tests passed 21 cases; prior focused artifact/provider boundaries passed. Real Android devices and external firmware tools were not rerun.
191 lines
6.7 KiB
TypeScript
191 lines
6.7 KiB
TypeScript
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import {
|
|
AnalysisError,
|
|
type AnalysisCapturedOutput,
|
|
} from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisAccessDeniedError,
|
|
AnalysisArtifactChangedError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisCancelledError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
AnalysisResourceConstraintError,
|
|
AnalysisTimeoutError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
|
|
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
|
|
import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/sqliteDatabase.js";
|
|
import { SQLITE_DATABASE_LIMITS } from "./SqliteDatabaseLimits.js";
|
|
|
|
/** Preserve diagnostics independently of worker exit and private-root cleanup. */
|
|
export const capturedSqliteOutput = (
|
|
snapshot: ProviderProcessSnapshot,
|
|
): AnalysisCapturedOutput => ({
|
|
stdout: snapshot.stdout.text,
|
|
stderr: snapshot.stderr.text,
|
|
truncated: snapshot.diagnosticTruncated === true,
|
|
});
|
|
|
|
/** The file-set member a filesystem error names, if it is the database, its WAL or its journal. */
|
|
const busyMember = (cause: Error, path: string): string => {
|
|
const failed = "path" in cause ? cause.path : undefined;
|
|
return typeof failed === "string" &&
|
|
[path, `${path}-wal`, `${path}-journal`].includes(failed)
|
|
? failed
|
|
: path;
|
|
};
|
|
|
|
/** Preserve artifact, caller, process, resource and output failure meanings. */
|
|
export const sqliteDatabaseFailure = (
|
|
cause: unknown,
|
|
path: string,
|
|
phase: string,
|
|
signal?: AbortSignal,
|
|
): AnalysisError => {
|
|
const operation = "inspect_sqlite_database";
|
|
if (cause instanceof AnalysisError) return cause;
|
|
if (cause instanceof ArtifactReaderFailure) {
|
|
if (cause.cleanup !== undefined)
|
|
return new ProviderCleanupError(
|
|
SQLITE_PROVIDER_IDENTITY.id,
|
|
cause.cleanup.resources,
|
|
{
|
|
reason: cause.cleanup.reason,
|
|
previous_error:
|
|
cause.cause === undefined
|
|
? null
|
|
: projectAnalysisError(
|
|
signal?.aborted
|
|
? new AnalysisCancelledError(operation, {
|
|
cause: cause.cause,
|
|
})
|
|
: sqliteDatabaseFailure(cause.cause, path, phase),
|
|
),
|
|
},
|
|
{ operation, cause },
|
|
);
|
|
if (cause.reason === "io" && cause.cause !== undefined)
|
|
return sqliteDatabaseFailure(cause.cause, path, phase, signal);
|
|
if (cause.reason === "integrity")
|
|
return new AnalysisArtifactChangedError(operation, path, cause.message, {
|
|
cause,
|
|
});
|
|
if (cause.reason === "cancelled")
|
|
return new AnalysisCancelledError(operation, { cause });
|
|
return new AnalysisInputError(operation, { cause }, [
|
|
{
|
|
path: ["path"],
|
|
reason: cause.reason === "limit" ? "out_of_range" : "invalid_format",
|
|
message: cause.message,
|
|
},
|
|
]);
|
|
}
|
|
if (cause instanceof Error && cause.name === "AbortError")
|
|
return new AnalysisCancelledError(operation, { cause });
|
|
if (cause instanceof OwnedCommandFailure) {
|
|
const projected = ownedCommandFailure(cause);
|
|
if (projected !== undefined) return projected;
|
|
}
|
|
if (phase === "artifact-read" && cause instanceof Error && "code" in cause) {
|
|
if (cause.code === "EACCES" || cause.code === "EPERM")
|
|
return new AnalysisAccessDeniedError(operation, path, cause.code, {
|
|
cause,
|
|
});
|
|
// Windows reports a sharing violation as EBUSY: another process holds the file without read
|
|
// sharing. The unchanged request succeeds once that process releases it. Name the member of the
|
|
// file set that was busy (the database, its WAL or its journal) when the error identifies it.
|
|
if (cause.code === "EBUSY")
|
|
return new AnalysisCapabilityUnavailableError(
|
|
SQLITE_PROVIDER_IDENTITY.id,
|
|
operation,
|
|
`Selected database file set is in use by another process (EBUSY): ${busyMember(cause, path)}`,
|
|
{
|
|
cause,
|
|
retryable: true,
|
|
userMessage:
|
|
"The selected SQLite database is held open by another process without read sharing. Close that application or inspect a copy, then retry.",
|
|
},
|
|
);
|
|
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
|
return new AnalysisInputError(operation, { cause }, [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_value",
|
|
message: `Selected database file set could not be read (${String(cause.code)}): ${path}`,
|
|
},
|
|
]);
|
|
}
|
|
return new ProviderAdapterError(SQLITE_PROVIDER_IDENTITY.id, operation, {
|
|
cause,
|
|
diagnostics: {
|
|
phase,
|
|
path,
|
|
reason: cause instanceof Error ? cause.message : String(cause),
|
|
...(cause instanceof OwnedCommandFailure && cause.snapshot !== null
|
|
? {
|
|
failure_kind: cause.reason,
|
|
exit_code: cause.snapshot.exitCode ?? null,
|
|
signal: cause.snapshot.signal ?? null,
|
|
captured_output: { ...capturedSqliteOutput(cause.snapshot) },
|
|
}
|
|
: {}),
|
|
},
|
|
});
|
|
};
|
|
|
|
const ownedCommandFailure = (
|
|
cause: OwnedCommandFailure,
|
|
): AnalysisError | undefined => {
|
|
const operation = "inspect_sqlite_database";
|
|
const capturedOutput =
|
|
cause.snapshot === null ? undefined : capturedSqliteOutput(cause.snapshot);
|
|
const options = {
|
|
cause,
|
|
...(capturedOutput === undefined ? {} : { capturedOutput }),
|
|
};
|
|
if (cause.cleanupFailure !== null)
|
|
return new ProviderCleanupError(
|
|
SQLITE_PROVIDER_IDENTITY.id,
|
|
cause.resources,
|
|
{
|
|
reason: cause.cleanupFailure,
|
|
previous_error: {
|
|
failure_kind: cause.reason,
|
|
message: cause.message,
|
|
},
|
|
...(capturedOutput === undefined
|
|
? {}
|
|
: { captured_output: { ...capturedOutput } }),
|
|
},
|
|
{ operation, cause },
|
|
);
|
|
if (cause.reason === "cancelled")
|
|
return new AnalysisCancelledError(operation, options);
|
|
if (cause.reason === "timeout")
|
|
return new AnalysisTimeoutError(
|
|
operation,
|
|
SQLITE_DATABASE_LIMITS.timeoutMs,
|
|
options,
|
|
);
|
|
if (cause.reason === "output-limit")
|
|
return new AnalysisOutputError(operation, cause.message, options);
|
|
if (
|
|
cause.snapshot?.stderr.text.includes("JavaScript heap out of memory") ===
|
|
true
|
|
)
|
|
return new AnalysisResourceConstraintError(
|
|
operation,
|
|
"memory",
|
|
"Owned SQLite worker reported JavaScript heap exhaustion.",
|
|
{
|
|
javascript_heap_bytes: 256 * 1024 * 1024,
|
|
},
|
|
options,
|
|
);
|
|
return undefined;
|
|
};
|