import { randomBytes } from "node:crypto"; import { posix as posixPath } from "node:path"; import type { RemoteTransport } from "./ssh2-transport.ts"; const PROBE_TIMEOUT_SECONDS = 20; const MAX_PROBE_OUTPUT_BYTES = 64 * 1024; export type RemotePathProbe = "home" | "cwd"; function probeCommand(kind: RemotePathProbe, token: string): { command: string; start: string; end: string } { const start = `__PI_SSH_PROBE_${token}_START__`; const end = `__PI_SSH_PROBE_${token}_END__`; const assign = kind === "home" ? "pi_ssh_probe_value=$HOME" : "pi_ssh_probe_value=$(pwd -P) || exit $?"; return { command: `${assign}\nprintf '%s%s%s' '${start}' "$pi_ssh_probe_value" '${end}'`, start, end, }; } export function parseRemotePathProbe(output: Buffer, start: string, end: string, kind: RemotePathProbe): string { if (output.length > MAX_PROBE_OUTPUT_BYTES) { throw new Error(`remote ${kind} probe output exceeded ${MAX_PROBE_OUTPUT_BYTES} bytes`); } const text = output.toString("utf8"); const startIndex = text.indexOf(start); const endIndex = startIndex < 0 ? -1 : text.indexOf(end, startIndex + start.length); if (startIndex < 0 || endIndex < 0 || text.indexOf(start, startIndex + start.length) >= 0) { throw new Error(`remote ${kind} probe returned an invalid framed response`); } const value = text.slice(startIndex + start.length, endIndex); if (!value.startsWith("/") || /[\0\r\n]/u.test(value)) { throw new Error(`remote ${kind} probe did not return one absolute POSIX path`); } return posixPath.normalize(value); } export async function probeRemotePath( transport: RemoteTransport, kind: RemotePathProbe, cwd = ".", signal?: AbortSignal, ): Promise { const token = randomBytes(12).toString("hex"); const probe = probeCommand(kind, token); const chunks: Buffer[] = []; let captured = 0; let overflow = false; const result = await transport.exec(probe.command, cwd, { signal, timeout: PROBE_TIMEOUT_SECONDS, onData(data) { if (captured + data.length > MAX_PROBE_OUTPUT_BYTES) { overflow = true; return; } chunks.push(data); captured += data.length; }, }); const output = Buffer.concat(chunks); if (overflow) throw new Error(`remote ${kind} probe output exceeded ${MAX_PROBE_OUTPUT_BYTES} bytes`); if (result.exitCode !== 0) { const detail = output.toString("utf8").replace(/\s+/gu, " ").trim().slice(0, 300); throw new Error(`remote ${kind} probe failed${result.exitCode === null ? "" : ` with exit code ${result.exitCode}`}${detail ? `: ${detail}` : ""}`); } return parseRemotePathProbe(output, probe.start, probe.end, kind); }