Files
reverse-engineering-anything/src/cli/sqliteCommands.ts
T
Kaoru0822andClaude Opus 5.5 fc791bb606 fix(paths): select caller paths through symlinks and .. as the filesystem does (#4067)
* fix: select caller paths through symlinks and .. as the filesystem does

CLI adapters, binary target resolution, the reverse-engineering service
and the objdump and Rizin command builders made caller-selected paths
absolute with path.resolve, which collapses `link/..` lexically. On
POSIX the filesystem follows `link` first, so these commands inspected,
hashed or launched a different file and returned internally consistent
Evidence for it.

absoluteCallerPath now owns that step: on POSIX it prefixes a relative
path with the working directory and keeps an absolute path as spelled;
on Windows, where `..` resolves lexically, it keeps win32.resolve.

Fixes #3864
Fixes #3911
Fixes #3912

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep firmware, WASM list and output-root paths on the selected file

Review follow-up. The CLI already passed firmware and WASM paths through
absoluteCallerPath, but snapshotFirmware and SafeOutputTree.create
re-resolved them lexically, and the WASM glue and candidate lists still
used resolve, so those inputs could still select a sibling file or
create output beside the intended directory.

absoluteCallerPath now also drops `.` segments and repeated or trailing
separators on POSIX, which never change the named file, so selected
paths such as `./module.wasm` keep matching their glue references
exactly. Every `..` is still left for the filesystem.

SafeOutputTree keeps host path syntax; its platform argument selects only
the Windows name policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep directory requirements and relative cwd in caller paths

Review follow-up. absoluteCallerPath stripped a trailing `/` or `/.`, so
`file/` selected the regular file instead of failing with ENOTDIR; it
now keeps one trailing separator. A relative cwd option was appended to
the root; it is now made absolute without collapsing `..`. WASM glue
matching resolved local literals lexically, so a module selected through
`link/..` matched only by basename; the adapter now compares both sides
through absoluteCallerPath and still reports each candidate as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 05:19:43 -07:00

112 lines
3.9 KiB
TypeScript

import { z } from "incur";
import { createSqliteDatabaseService } from "../composition/sqlite.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
/** Resolve an operator-selected SQLite snapshot through the shared inspection workflow. */
export const registerSqliteCommands = (
cli: CliInstance,
logger: Logger,
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
const service = createSqliteDatabaseService(environment);
cli.command(CLI_COMMANDS.inspectSqliteDatabase, {
description:
"Inspect SQLite schema and optional ordinary-table rows from a local snapshot",
args: z.object({
path: z.string().describe("Local SQLite database snapshot"),
}),
options: z.object({
table: z
.string()
.optional()
.describe("Exact ordinary table name to read"),
column: z
.array(z.string())
.optional()
.describe(
"Repeat to select exact row columns in the requested order; requires table",
),
schemaScope: z
.enum(["inventory", "selected_table", "database"])
.optional()
.describe(
"Compact inventory by default, or selected-table details when table is supplied; database requests full schema",
),
rowLimit: z
.number()
.int()
.min(1)
.optional()
.describe(
"Positive safe integer row count; default 20; requires table",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectSqliteDatabase, async () => {
const inspection = await service
.inspect(
{
path: absoluteCallerPath(args.path),
...(options.table === undefined
? {}
: { table: options.table }),
...(options.rowLimit === undefined
? {}
: { row_limit: options.rowLimit }),
...(options.column === undefined
? {}
: { columns: options.column }),
...(options.schemaScope === undefined
? {}
: { schema_scope: options.schemaScope }),
},
{ signal },
)
.then(
(result) => ({ result }),
(cause: unknown) => ({ cause }),
);
try {
await service.close();
} catch (cause: unknown) {
const primary =
"cause" in inspection
? inspection.cause
: inspection.result.ok
? undefined
: inspection.result.error;
if (
primary instanceof AnalysisError &&
cause instanceof AnalysisError
)
throw analysisErrorWithCleanupFailure(
primary,
cause,
"inspect_sqlite_database",
);
if ("cause" in inspection)
throw new AggregateError(
[inspection.cause, cause],
"SQLite inspection and provider cleanup both failed",
{ cause: inspection.cause },
);
throw cause;
}
if ("cause" in inspection) throw inspection.cause;
return inspection.result.ok
? inspection.result.value
: projectAnalysisError(inspection.result.error);
}),
),
});
};