Files
rea/src/application/sqlite/SqliteDatabaseService.ts
T
morluto bd4f5e9c19 fix(parsers): preserve format evidence and caller input identity
Retain bounded FAT and offset-tagged PE observations through archive hashing,
classification, and content nodes. Preserve PyInstaller member paths according
to the observed carrier dialect and share APK v1 signing-name semantics.

Read SQLite's implicit sqlite_schema table without inventing a stored schema
declaration. Distinguish unsupported ELF numbering and decoded-name admission
from malformed data and fixed scan allowances. Preserve Sigrok basenames and
classify matching explicit input-format refusals at their owning boundary.

Validation: 554 distinct tests passed, 3 skipped across serial focused runs;
build, typecheck/lint, docs, formatting, and discovery (2141 files) passed.
Sigrok command-boundary simulators do not establish real-tool decoding.
Small repeated public PyInstaller CLI runs had equivalent inventory facts;
warm median elapsed 0.5431s before / 0.5426s after, with no speed claim.

Fixes #6733
Fixes #6741
Fixes #6722
Fixes #6732
Fixes #6736
Fixes #6734
Fixes #6764
Fixes #6766
Refs #6739
2026-10-23 22:54:28 +08:00

129 lines
4.6 KiB
TypeScript

import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { SqliteDatabasePort } from "./SqliteDatabasePort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import {
createEvidence,
type Evidence,
type EvidenceSubjectTarget,
} from "../../domain/evidence.js";
import {
inspectSqliteDatabaseInputSchema,
sqliteDatabaseSchema,
type InspectSqliteDatabaseInput,
type SqliteDatabase,
SQLITE_ROW_LIMIT_DEFAULT,
DEFAULT_SQLITE_WORKER_TIMEOUT_MS,
sqliteSchemaHasObjectDeclaration,
sqliteSchemaScope,
sqliteInspectionParameters,
} from "../../domain/sqlite/sqliteDatabase.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
const OPERATION = "inspect_sqlite_database";
const matchesSelectedSchema = (
schema: SqliteDatabase["schema"],
table: string,
): boolean =>
sqliteSchemaHasObjectDeclaration(table)
? schema.tables.length === 1 &&
schema.tables[0]?.name === table &&
schema.indexes.every((index) => index.table === table) &&
schema.triggers.every((trigger) => trigger.table === table) &&
schema.views.length === 0
: schema.tables.length === 0 &&
schema.indexes.length === 0 &&
schema.views.length === 0 &&
schema.triggers.length === 0;
const matchesSelectedDatabase = (
report: SqliteDatabase,
input: InspectSqliteDatabaseInput,
subject: EvidenceSubjectTarget,
): boolean =>
report.artifact.path === input.path &&
subject.path === input.path &&
subject.sha256 === report.artifact.sha256 &&
subject.format === "file" &&
// Fresh executions must report known settings; only historical parsing permits omission.
report.worker_timeout_ms ===
(input.worker_timeout_ms ?? DEFAULT_SQLITE_WORKER_TIMEOUT_MS) &&
(report.wal === null || report.wal.path === `${input.path}-wal`) &&
report.schema.scope === sqliteSchemaScope(input) &&
(report.schema.scope !== "selected_table" ||
(input.table !== undefined &&
matchesSelectedSchema(report.schema, input.table))) &&
(input.table === undefined
? report.rows === null
: report.rows?.table === input.table &&
report.rows.row_limit === (input.row_limit ?? SQLITE_ROW_LIMIT_DEFAULT) &&
(input.columns === undefined ||
(report.rows.columns.length === input.columns.length &&
report.rows.columns.every(
(name, index) => name === input.columns?.[index],
))));
/** Shared snapshot inspection and caller-selection validation for CLI and MCP. */
export class SqliteDatabaseService {
constructor(readonly provider: SqliteDatabasePort) {}
/** Await provider shutdown, retaining failed cleanup owners for retry. */
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
/** Inspect an explicit database snapshot and preserve its original database/WAL identity. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
}),
);
const input = inspectSqliteDatabaseInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected;
const execution = inspected.value;
const report = sqliteDatabaseSchema.safeParse(execution.result);
if (
!report.success ||
execution.subject == null ||
!matchesSelectedDatabase(report.data, input.data, execution.subject)
)
return err(
new AnalysisOutputError(
OPERATION,
"SQLite adapter returned malformed data or changed the selected database identity/table/row limit/worker budget.",
),
);
return ok(
createEvidence(execution.subject, execution.provider, {
operation: OPERATION,
parameters: sqliteInspectionParameters(input.data),
result: jsonValueSchema.parse(report.data),
...(execution.rawResult === null
? {}
: { rawResult: execution.rawResult }),
confidence: "observed",
locations: execution.locations,
limitations: execution.limitations,
}),
);
}
}