import { createHash, randomBytes } from "node:crypto"; import { once } from "node:events"; import { createWriteStream } from "node:fs"; import { mkdir, readFile, rm } from "node:fs/promises"; import path from "node:path"; import type { ParsedSshInvocation, ParsedSshRoute, SshCaptureResult } from "./ssh"; export interface SshRemoteCommandStreamHandlers { onStdout(chunk: Buffer): Promise | void; onStderr?(chunk: Buffer): Promise | void; } export interface SshRemoteCommandExecutor { runRemoteCommand(remoteCommand: string, input?: string): Promise; streamRemoteCommand?( remoteCommand: string, handlers: SshRemoteCommandStreamHandlers, input?: string, options?: { inactivityTimeoutMs?: number }, ): Promise; } export interface SshFileTransferCommandBuilders { buildRouteCommand(route: ParsedSshRoute, command: string[], options?: { stdin?: boolean }): string; buildWindowsPowerShellCommand(script: string): string; } type SshFileTransferOperation = | "stat" | "write-b64-argv" | "write-b64-stdin" | "write-b64-begin" | "write-b64-append-stdin" | "write-b64-commit"; interface SshFileTransferCliOptions { action: "upload" | "download"; localPath: string; remotePath: string; inactivityTimeoutMs?: number; } interface SshFileTransferStat { bytes: number; sha256: string; } interface SshFileTransferEndpointStat extends SshFileTransferStat { side: "local" | "remote"; path: string; } interface SshFileTransferWriteResult { strategy: "argv" | "stdin" | "chunked-stdin"; chunks: number; } interface SshFileTransferDownloadResult { remote: SshFileTransferStat; local: SshFileTransferStat; strategy: "tcp-pool-stdout-stream"; transport: "host.ssh.tcp-pool"; chunks: number; elapsedMs: number; throughputBytesPerSecond: number; remainingBytes: number; } export interface SshVerifiedDownloadResult { remotePath: string; localPath: string; bytes: number; sha256: string; verified: true; verification: Record; transfer: { strategy: SshFileTransferDownloadResult["strategy"]; transport: SshFileTransferDownloadResult["transport"]; chunks: number; elapsedMs: number; throughputBytesPerSecond: number; remainingBytes: number; }; } class SshFileTransferError extends Error { constructor(message: string, public readonly details: Record = {}) { super(message); this.name = "SshFileTransferError"; } } const fileTransferWriteB64ArgvLimit = 48_000; const fileTransferWriteRawChunkBytes = 1_048_576; const fileTransferWriteB64ChunkChars = 1_398_104; const fileTransferProgressEveryChunks = 16; export function isSshFileTransferOperation(args: string[]): boolean { const subcommand = args[0] ?? ""; return subcommand === "upload" || subcommand === "download"; } export async function runSshFileTransferOperation( invocation: ParsedSshInvocation, args: string[], executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, ): Promise { const options = parseSshFileTransferCliOptions(args); const localPath = path.resolve(options.localPath); if (options.action === "upload") { const content = await readFile(localPath); const expected = { bytes: content.length, sha256: sha256HexBuffer(content) }; const write = await writeRemoteFileVerified(invocation, executor, builders, options.remotePath, content); const remote = await statRemoteFile(invocation, executor, builders, options.remotePath); assertTransferStat("upload final remote verification", options.remotePath, expected, remote); const verification = buildTransferVerification( { side: "local", path: localPath, ...expected }, { side: "remote", path: options.remotePath, ...remote }, ); process.stdout.write(`${JSON.stringify({ ok: true, command: "ssh upload", route: invocation.route.raw, providerId: invocation.providerId, localPath, remotePath: options.remotePath, bytes: expected.bytes, sha256: expected.sha256, verified: true, verification, transfer: write, }, null, 2)}\n`); return 0; } const download = await downloadSshFileVerified(invocation, executor, builders, options.remotePath, localPath, options.inactivityTimeoutMs); process.stdout.write(`${JSON.stringify({ ok: true, command: "ssh download", route: invocation.route.raw, providerId: invocation.providerId, remotePath: options.remotePath, localPath, bytes: download.bytes, sha256: download.sha256, verified: true, verification: download.verification, transfer: download.transfer, }, null, 2)}\n`); return 0; } export async function downloadSshFileVerified( invocation: ParsedSshInvocation, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, remotePath: string, localPath: string, inactivityTimeoutMs?: number, ): Promise { const read = await downloadRemoteFileVerified(invocation, executor, builders, remotePath, localPath, inactivityTimeoutMs); const verification = buildTransferVerification( { side: "remote", path: remotePath, ...read.remote }, { side: "local", path: localPath, ...read.local }, ); return { remotePath, localPath, bytes: read.remote.bytes, sha256: read.remote.sha256, verified: true, verification, transfer: { strategy: read.strategy, transport: read.transport, chunks: read.chunks, elapsedMs: read.elapsedMs, throughputBytesPerSecond: read.throughputBytesPerSecond, remainingBytes: read.remainingBytes, }, }; } function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptions { const action = args[0]; if (action !== "upload" && action !== "download") throw new Error("ssh file transfer requires upload or download"); const positionals: string[] = []; let inactivityTimeoutMs: number | undefined; for (let index = 1; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "--") { positionals.push(...args.slice(index + 1)); break; } if (arg === "--chunk-bytes" || arg === "--block-bytes") { throw new Error(`unsupported ssh ${action} option: ${arg}; downloads use host.ssh.tcp-pool stdout streaming and no longer support the legacy base64 block reader`); } if (arg === "--inactivity-timeout-ms" || arg === "--runtime-timeout-ms") { const value = args[index + 1]; if (value === undefined) throw new Error(`ssh ${action} ${arg} requires a positive integer value`); index += 1; inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(value, `ssh ${action} ${arg}`); continue; } if (arg.startsWith("--inactivity-timeout-ms=")) { inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(arg.slice("--inactivity-timeout-ms=".length), `ssh ${action} --inactivity-timeout-ms`); continue; } if (arg.startsWith("--runtime-timeout-ms=")) { inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(arg.slice("--runtime-timeout-ms=".length), `ssh ${action} --runtime-timeout-ms`); continue; } if (arg === "--help" || arg === "-h" || arg === "help") { throw new Error(`ssh ${action} usage: trans ${action} ${action === "upload" ? " " : " "}`); } if (arg.startsWith("-")) throw new Error(`unsupported ssh ${action} option: ${arg}`); positionals.push(arg); } if (positionals.length !== 2) { throw new Error(`ssh ${action} requires exactly two paths: ${action === "upload" ? " " : " "}`); } const [first, second] = positionals; if (!first || !second) throw new Error(`ssh ${action} paths must be non-empty`); return action === "upload" ? { action, localPath: first, remotePath: second, inactivityTimeoutMs } : { action, remotePath: first, localPath: second, inactivityTimeoutMs }; } function parsePositiveRuntimeTimeoutMs(value: string, label: string): number { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) { throw new Error(`${label} must be a positive integer number of milliseconds`); } return parsed; } async function writeRemoteFileVerified( invocation: ParsedSshInvocation, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, remotePath: string, content: Buffer, ): Promise { const expectedBytes = String(content.length); const expectedSha256 = sha256HexBuffer(content); const encoded = content.length <= fileTransferWriteB64ArgvLimit ? content.toString("base64") : ""; if (invocation.route.plane !== "win" && encoded.length <= fileTransferWriteB64ArgvLimit) { await checkedFileTransfer(invocation, executor, builders, "write-b64-argv", [remotePath, expectedBytes, expectedSha256, ...chunkString(encoded, fileTransferWriteB64ChunkChars)]); return { strategy: "argv", chunks: encoded.length === 0 ? 0 : Math.ceil(encoded.length / fileTransferWriteB64ChunkChars) }; } if (content.length <= fileTransferWriteRawChunkBytes) { try { await checkedFileTransfer(invocation, executor, builders, "write-b64-stdin", [remotePath, expectedBytes, expectedSha256], content.toString("base64")); return { strategy: "stdin", chunks: 1 }; } catch { // Fall through to chunked upload with per-block base64 encoding. } } const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`; let chunks = 0; await checkedFileTransfer(invocation, executor, builders, "write-b64-begin", [remotePath, token]); for (let offset = 0; offset < content.length; offset += fileTransferWriteRawChunkBytes) { const encodedChunk = content.subarray(offset, Math.min(content.length, offset + fileTransferWriteRawChunkBytes)).toString("base64"); await checkedFileTransfer(invocation, executor, builders, "write-b64-append-stdin", [remotePath, token], encodedChunk); chunks += 1; } await checkedFileTransfer(invocation, executor, builders, "write-b64-commit", [remotePath, token, expectedBytes, expectedSha256]); return { strategy: "chunked-stdin", chunks }; } async function downloadRemoteFileVerified( invocation: ParsedSshInvocation, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, remotePath: string, localPath: string, inactivityTimeoutMs?: number, ): Promise { if (executor.streamRemoteCommand === undefined) { throw new SshFileTransferError("ssh download requires a tcp-pool stdout stream sink", { route: invocation.route.raw, providerId: invocation.providerId, remotePath, localPath, requiredTransport: "host.ssh.tcp-pool", blocker: "streamRemoteCommand unavailable", }); } if (invocation.route.plane === "win") { throw new SshFileTransferError("ssh download requires tcp-pool stdout streaming; win routes are not supported by the safe binary stream path", { route: invocation.route.raw, providerId: invocation.providerId, remotePath, localPath, requiredTransport: "host.ssh.tcp-pool", blocker: "win route has no verified binary stdout stream path", }); } const remote = await statRemoteFile(invocation, executor, builders, remotePath); await mkdir(path.dirname(localPath), { recursive: true }); const remoteCommand = buildStreamDownloadRemoteCommand(invocation.route, builders, remotePath); const startedAtMs = Date.now(); const hash = createHash("sha256"); const output = createWriteStream(localPath, { flags: "w", mode: 0o600 }); let actualBytes = 0; let chunkCount = 0; try { const result = await executor.streamRemoteCommand(remoteCommand, { onStdout: async (chunk) => { await writeStreamChunk(output, chunk); actualBytes += chunk.length; chunkCount += 1; hash.update(chunk); emitDownloadProgress(invocation, remotePath, chunkCount, actualBytes, remote.bytes, chunk.length, startedAtMs); }, }, undefined, { inactivityTimeoutMs }); await closeWriteStream(output); if (result.exitCode !== 0) { throw new SshFileTransferError("remote ssh download stream failed", { route: invocation.route.raw, providerId: invocation.providerId, remotePath, localPath, requiredTransport: "host.ssh.tcp-pool", exitCode: result.exitCode, stdout: transferTextSnapshot(result.stdout, { headChars: 120, tailChars: 500 }), stderr: transferTextSnapshot(result.stderr, { headChars: 120, tailChars: 1000 }), }); } const elapsedMs = Math.max(1, Date.now() - startedAtMs); const local = { bytes: actualBytes, sha256: hash.digest("hex") }; assertTransferStat("download tcp-pool stdout stream verification", localPath, remote, local); emitDownloadProgress(invocation, remotePath, Math.max(1, chunkCount), actualBytes, remote.bytes, 0, startedAtMs, true); return { remote, local, strategy: "tcp-pool-stdout-stream", transport: "host.ssh.tcp-pool", chunks: chunkCount, elapsedMs, throughputBytesPerSecond: Math.round((actualBytes * 1000) / elapsedMs), remainingBytes: Math.max(0, remote.bytes - actualBytes), }; } catch (error) { output.destroy(); await rm(localPath, { force: true }).catch(() => undefined); throw error; } } function emitDownloadProgress( invocation: ParsedSshInvocation, remotePath: string, chunkCount: number, actualBytes: number, expectedBytes: number, lastChunkBytes: number, startedAtMs: number, force = false, ): void { if (!force && chunkCount !== 1 && actualBytes < expectedBytes && chunkCount % fileTransferProgressEveryChunks !== 0) return; const elapsedMs = Math.max(1, Date.now() - startedAtMs); process.stderr.write(`${JSON.stringify({ event: "unidesk.ssh.download.progress", at: new Date().toISOString(), route: invocation.route.raw, providerId: invocation.providerId, remotePath, strategy: "tcp-pool-stdout-stream", transport: "host.ssh.tcp-pool", chunks: chunkCount, bytes: actualBytes, totalBytes: expectedBytes, actualBytes, expectedBytes, lastChunkBytes, remainingBytes: Math.max(0, expectedBytes - actualBytes), elapsedMs, throughputBytesPerSecond: Math.round((actualBytes * 1000) / elapsedMs), })}\n`); } async function statRemoteFile( invocation: ParsedSshInvocation, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, remotePath: string, ): Promise { const stat = await checkedFileTransfer(invocation, executor, builders, "stat", [remotePath]); return parseFileTransferStat(stat.stdout, stat.stderr, remotePath); } function buildStreamDownloadRemoteCommand( route: ParsedSshRoute, builders: SshFileTransferCommandBuilders, remotePath: string, ): string { return builders.buildRouteCommand(route, ["sh", "-c", "cat -- \"$1\"", "unidesk-download", remotePath]); } async function writeStreamChunk(stream: ReturnType, chunk: Buffer): Promise { if (chunk.length === 0) return; await new Promise((resolve, reject) => { stream.write(chunk, (error) => { if (error) reject(error); else resolve(); }); }); } async function closeWriteStream(stream: ReturnType): Promise { if (stream.closed) return; const closed = once(stream, "close"); stream.end(); await closed; } async function checkedFileTransfer( invocation: ParsedSshInvocation, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, operation: SshFileTransferOperation, args: string[], input?: string, ): Promise { const remoteCommand = buildFileTransferRemoteCommand(invocation.route, builders, operation, args, input !== undefined); const result = await executor.runRemoteCommand(remoteCommand, input); if (result.exitCode === 0) return result; throw new SshFileTransferError("remote ssh file transfer operation failed", { route: invocation.route.raw, operation, args: args.slice(0, 4), exitCode: result.exitCode, stdout: transferTextSnapshot(result.stdout, { headChars: operation === "stat" ? 500 : 120, tailChars: 500 }), stderr: transferTextSnapshot(result.stderr, { headChars: 120, tailChars: 1000 }), }); } function buildFileTransferRemoteCommand( route: ParsedSshRoute, builders: SshFileTransferCommandBuilders, operation: SshFileTransferOperation, args: string[], hasInput: boolean, ): string { if (route.plane === "win") return buildWindowsFileTransferCommand(route, builders, operation, args); return builders.buildRouteCommand(route, ["sh", "-c", posixFileTransferScript(), "unidesk-file-transfer", operation, ...args], { stdin: hasInput }); } function parseFileTransferStat(stdout: string, stderr: string, remotePath: string): SshFileTransferStat { const lines = stdout.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0); for (const line of lines) { const [bytesText, sha256, extra] = line.split(/\s+/u); const bytes = Number(bytesText); if (extra === undefined && Number.isSafeInteger(bytes) && bytes >= 0 && /^[0-9a-f]{64}$/u.test(sha256 ?? "")) { return { bytes, sha256: sha256! }; } } { throw new SshFileTransferError("remote file transfer stat returned invalid metadata", { remotePath, expected: " ", stdout: transferTextSnapshot(stdout, { headChars: 500, tailChars: 500 }), stderr: transferTextSnapshot(stderr, { headChars: 120, tailChars: 1000 }), candidateLines: lines.slice(0, 8).map((line) => line.slice(0, 200)), candidateLineCount: lines.length, }); } } function transferTextSnapshot(value: string, limits: { headChars: number; tailChars: number }): Record { return { bytes: Buffer.byteLength(value, "utf8"), chars: value.length, head: value.slice(0, limits.headChars), tail: value.slice(Math.max(0, value.length - limits.tailChars)), truncated: value.length > limits.headChars + limits.tailChars, }; } function assertTransferStat(label: string, pathName: string, expected: SshFileTransferStat, actual: SshFileTransferStat): void { if (expected.bytes === actual.bytes && expected.sha256 === actual.sha256) return; throw new SshFileTransferError(`${label} failed`, { path: pathName, expected, actual, }); } function buildTransferVerification(source: SshFileTransferEndpointStat, target: SshFileTransferEndpointStat): Record { return { automatic: true, algorithm: "sha256", checked: ["bytes", "sha256"], verified: source.bytes === target.bytes && source.sha256 === target.sha256, source, target, match: { bytes: source.bytes === target.bytes, sha256: source.sha256 === target.sha256, }, }; } function sha256HexBuffer(value: Buffer): string { return createHash("sha256").update(value).digest("hex"); } function chunkString(value: string, chunkSize: number): string[] { const chunks: string[] = []; for (let index = 0; index < value.length; index += chunkSize) { chunks.push(value.slice(index, index + chunkSize)); } return chunks.length > 0 ? chunks : [""]; } function posixFileTransferScript(): string { return [ "set -eu", "sha256_file() {", " if command -v sha256sum >/dev/null 2>&1; then sha256sum -- \"$1\" | awk '{print $1}'; return; fi", " if command -v shasum >/dev/null 2>&1; then shasum -a 256 -- \"$1\" | awk '{print $1}'; return; fi", " if command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 -- \"$1\" | awk '{print $NF}'; return; fi", " printf 'missing sha256 tool\\n' >&2; return 127", "}", "ensure_parent() { case \"$1\" in */*) parent=${1%/*}; mkdir -p -- \"$parent\";; esac; }", "verify_tmp() {", " target=$1; tmp=$2; expected_bytes=$3; expected_sha256=$4", " actual_bytes=$(wc -c < \"$tmp\" | tr -d '[:space:]')", " if [ \"$actual_bytes\" != \"$expected_bytes\" ]; then rm -f -- \"$tmp\"; printf 'transfer byte count mismatch for %s: expected=%s actual=%s\\n' \"$target\" \"$expected_bytes\" \"$actual_bytes\" >&2; exit 23; fi", " actual_sha256=$(sha256_file \"$tmp\")", " if [ \"$actual_sha256\" != \"$expected_sha256\" ]; then rm -f -- \"$tmp\"; printf 'transfer sha256 mismatch for %s: expected=%s actual=%s\\n' \"$target\" \"$expected_sha256\" \"$actual_sha256\" >&2; exit 24; fi", "}", "set_tmp_paths() {", " target=$1; token=$2", " case \"$token\" in ''|*[!a-zA-Z0-9_.-]*) printf 'invalid transfer temp token\\n' >&2; exit 2;; esac", " base=${target##*/}; dir=.", " case \"$target\" in */*) dir=${target%/*};; esac", " tmp=\"$dir/.${base}.unidesk-transfer-${token}.tmp\"", " tmp_b64=\"$tmp.b64\"", "}", "op=$1; shift", "case \"$op\" in", " stat)", " target=$1; bytes=$(wc -c < \"$target\" | tr -d '[:space:]'); digest=$(sha256_file \"$target\"); printf '%s %s\\n' \"$bytes\" \"$digest\"", " ;;", " write-b64-argv)", " target=$1; expected_bytes=$2; expected_sha256=$3; shift 3", " ensure_parent \"$target\"; base=${target##*/}; dir=.; case \"$target\" in */*) dir=${target%/*};; esac", " tmp=\"$dir/.${base}.unidesk-transfer-$$.tmp\"; tmp_b64=\"$tmp.b64\"; : > \"$tmp_b64\"", " for chunk in \"$@\"; do printf '%s' \"$chunk\" >> \"$tmp_b64\"; done", " if ! base64 -d < \"$tmp_b64\" > \"$tmp\"; then rm -f -- \"$tmp\" \"$tmp_b64\"; printf 'transfer base64 decode failed for %s\\n' \"$target\" >&2; exit 22; fi", " rm -f -- \"$tmp_b64\"; verify_tmp \"$target\" \"$tmp\" \"$expected_bytes\" \"$expected_sha256\"; mv -f -- \"$tmp\" \"$target\"", " actual_sha256=$(sha256_file \"$target\"); if [ \"$actual_sha256\" != \"$expected_sha256\" ]; then printf 'transfer final sha256 mismatch for %s\\n' \"$target\" >&2; exit 25; fi", " ;;", " write-b64-stdin)", " target=$1; expected_bytes=$2; expected_sha256=$3", " ensure_parent \"$target\"; base=${target##*/}; dir=.; case \"$target\" in */*) dir=${target%/*};; esac", " tmp=\"$dir/.${base}.unidesk-transfer-$$.tmp\"", " if ! base64 -d > \"$tmp\"; then rm -f -- \"$tmp\"; printf 'transfer base64 decode failed for %s\\n' \"$target\" >&2; exit 22; fi", " verify_tmp \"$target\" \"$tmp\" \"$expected_bytes\" \"$expected_sha256\"; mv -f -- \"$tmp\" \"$target\"", " actual_sha256=$(sha256_file \"$target\"); if [ \"$actual_sha256\" != \"$expected_sha256\" ]; then printf 'transfer final sha256 mismatch for %s\\n' \"$target\" >&2; exit 25; fi", " ;;", " write-b64-begin)", " target=$1; token=$2; ensure_parent \"$target\"; set_tmp_paths \"$target\" \"$token\"; : > \"$tmp_b64\"", " ;;", " write-b64-append-stdin)", " target=$1; token=$2; set_tmp_paths \"$target\" \"$token\"; cat >> \"$tmp_b64\"", " ;;", " write-b64-commit)", " target=$1; token=$2; expected_bytes=$3; expected_sha256=$4; set_tmp_paths \"$target\" \"$token\"", " if ! base64 -d < \"$tmp_b64\" > \"$tmp\"; then rm -f -- \"$tmp\" \"$tmp_b64\"; printf 'transfer base64 decode failed for %s\\n' \"$target\" >&2; exit 22; fi", " rm -f -- \"$tmp_b64\"; verify_tmp \"$target\" \"$tmp\" \"$expected_bytes\" \"$expected_sha256\"; mv -f -- \"$tmp\" \"$target\"", " actual_sha256=$(sha256_file \"$target\"); if [ \"$actual_sha256\" != \"$expected_sha256\" ]; then printf 'transfer final sha256 mismatch for %s\\n' \"$target\" >&2; exit 25; fi", " ;;", " *) printf 'unsupported transfer op: %s\\n' \"$op\" >&2; exit 2;;", "esac", ].join("\n"); } function buildWindowsFileTransferCommand( route: ParsedSshRoute, builders: SshFileTransferCommandBuilders, operation: SshFileTransferOperation, args: string[], ): string { return builders.buildWindowsPowerShellCommand(windowsFileTransferScript(route.workspace, operation, args)); } function windowsFileTransferScript(basePath: string | null, operation: SshFileTransferOperation, args: string[]): string { const target = args[0] ?? ""; const tokenOrBlock = args[1] ?? ""; const third = args[2] ?? ""; const fourth = args[3] ?? ""; const argvChunks = args.slice(3).map(powerShellSingleQuote).join(", "); return [ "$ErrorActionPreference = 'Stop';", "$ProgressPreference = 'SilentlyContinue';", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();", "$OutputEncoding = [System.Text.UTF8Encoding]::new();", `$basePath = ${powerShellSingleQuote(basePath ?? "")};`, `$operation = ${powerShellSingleQuote(operation)};`, `$targetArg = ${powerShellSingleQuote(target)};`, `$arg1 = ${powerShellSingleQuote(tokenOrBlock)};`, `$arg2 = ${powerShellSingleQuote(third)};`, `$arg3 = ${powerShellSingleQuote(fourth)};`, `$argvChunks = @(${argvChunks});`, "function Fail([string]$Message, [int]$Code) { [Console]::Error.WriteLine($Message); exit $Code }", "function Resolve-UnideskPath([string]$Raw) { if ([string]::IsNullOrWhiteSpace($Raw)) { Fail 'empty transfer path' 2 }; if ([System.IO.Path]::IsPathRooted($Raw)) { return [System.IO.Path]::GetFullPath($Raw) }; if (-not [string]::IsNullOrWhiteSpace($basePath)) { return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($basePath, $Raw)) }; return [System.IO.Path]::GetFullPath($Raw) }", "function Ensure-Parent([string]$Target) { $parent = [System.IO.Path]::GetDirectoryName($Target); if (-not [string]::IsNullOrWhiteSpace($parent)) { [System.IO.Directory]::CreateDirectory($parent) | Out-Null } }", "function Get-Sha256([string]$Path) { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }", "function Set-TmpPaths([string]$Target, [string]$Token) { if ($Token -notmatch '^[A-Za-z0-9_.-]+$') { Fail 'invalid transfer temp token' 2 }; $dir = [System.IO.Path]::GetDirectoryName($Target); if ([string]::IsNullOrWhiteSpace($dir)) { $dir = (Get-Location).ProviderPath }; $base = [System.IO.Path]::GetFileName($Target); $script:tmp = [System.IO.Path]::Combine($dir, '.' + $base + '.unidesk-transfer-' + $Token + '.tmp'); $script:tmpB64 = $script:tmp + '.b64' }", "function Verify-Temp([string]$Target, [string]$Tmp, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { $actualBytes = ([System.IO.FileInfo]$Tmp).Length; if ($actualBytes -ne $ExpectedBytes) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('transfer byte count mismatch for ' + $Target + ': expected=' + $ExpectedBytes + ' actual=' + $actualBytes) 23 }; $actualSha256 = Get-Sha256 $Tmp; if ($actualSha256 -ne $ExpectedSha256) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('transfer sha256 mismatch for ' + $Target + ': expected=' + $ExpectedSha256 + ' actual=' + $actualSha256) 24 } }", "function Decode-ToTarget([string]$Target, [string]$Encoded, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { Ensure-Parent $Target; Set-TmpPaths $Target ([guid]::NewGuid().ToString('N')); try { $bytes = [Convert]::FromBase64String(($Encoded -replace '\\s','')) } catch { Fail ('transfer base64 decode failed for ' + $Target + ': ' + $_.Exception.Message) 22 }; [System.IO.File]::WriteAllBytes($script:tmp, $bytes); Verify-Temp $Target $script:tmp $ExpectedBytes $ExpectedSha256; Move-Item -LiteralPath $script:tmp -Destination $Target -Force; $actualSha256 = Get-Sha256 $Target; if ($actualSha256 -ne $ExpectedSha256) { Fail ('transfer final sha256 mismatch for ' + $Target) 25 } }", "$target = Resolve-UnideskPath $targetArg;", "switch ($operation) {", " 'stat' { if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('file not found: ' + $target) 1 }; $bytes = ([System.IO.FileInfo]$target).Length; $digest = Get-Sha256 $target; [Console]::Out.WriteLine(([string]$bytes) + ' ' + $digest); break }", " 'write-b64-argv' { Decode-ToTarget $target ([string]::Concat($argvChunks)) ([Int64]$arg1) $arg2; break }", " 'write-b64-stdin' { Decode-ToTarget $target ([Console]::In.ReadToEnd()) ([Int64]$arg1) $arg2; break }", " 'write-b64-begin' { Ensure-Parent $target; Set-TmpPaths $target $arg1; [System.IO.File]::WriteAllText($script:tmpB64, '', [System.Text.Encoding]::ASCII); break }", " 'write-b64-append-stdin' { Set-TmpPaths $target $arg1; $chunk = ([Console]::In.ReadToEnd()) -replace '\\s',''; [System.IO.File]::AppendAllText($script:tmpB64, $chunk, [System.Text.Encoding]::ASCII); break }", " 'write-b64-commit' { Set-TmpPaths $target $arg1; $encoded = [System.IO.File]::ReadAllText($script:tmpB64, [System.Text.Encoding]::ASCII); Remove-Item -LiteralPath $script:tmpB64 -Force -ErrorAction SilentlyContinue; Decode-ToTarget $target $encoded ([Int64]$arg2) $arg3; break }", " default { Fail ('unsupported transfer op: ' + $operation) 2 }", "}", ].join(" "); } function powerShellSingleQuote(value: string): string { return `'${value.replace(/'/g, "''")}'`; }