diff --git a/scripts/src/ssh-file-transfer.test.ts b/scripts/src/ssh-file-transfer.test.ts index 28e84f55..ce947d88 100644 --- a/scripts/src/ssh-file-transfer.test.ts +++ b/scripts/src/ssh-file-transfer.test.ts @@ -87,6 +87,32 @@ describe("ssh text transfer preflight", () => { } }); + test("uploads files above one chunk without concatenating padded base64 blocks", async () => { + const dir = mkdtempSync(join(tmpdir(), "unidesk-chunked-transfer-")); + const source = join(dir, "payload.bin"); + const content = Buffer.alloc((3 * 1024 * 1024) + 17, 0xa5); + writeFileSync(source, content); + const fake = fakeTransfer(content); + const invocation = parseSshInvocation("D601:/tmp", ["upload", source, "/tmp/payload.bin"]); + try { + expect(await runSshFileTransferOperation( + invocation, + ["upload", source, "/tmp/payload.bin"], + fake.executor, + fake.builders, + )).toBe(0); + expect(fake.calls).toEqual([ + "write-b64-begin", + "write-b64-append-stdin", + "write-b64-append-stdin", + "write-b64-commit", + "stat", + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + test("allows an explicit generated-text override without bypassing verification", async () => { const dir = mkdtempSync(join(tmpdir(), "unidesk-generated-transfer-")); const source = join(dir, "result.json"); diff --git a/scripts/src/ssh-file-transfer.ts b/scripts/src/ssh-file-transfer.ts index 4f0583ca..3e90e3b9 100644 --- a/scripts/src/ssh-file-transfer.ts +++ b/scripts/src/ssh-file-transfer.ts @@ -96,7 +96,7 @@ class SshFileTransferError extends Error { } const fileTransferWriteB64ArgvLimit = 48_000; -const fileTransferWriteRawChunkBytes = 131_072; +const fileTransferWriteRawChunkBytes = 3 * 1024 * 1024; const fileTransferWriteB64ChunkChars = 1_398_104; const fileTransferProgressEveryChunks = 16; @@ -763,15 +763,15 @@ function posixFileTransferScript(): string { " 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\"", + " target=$1; token=$2; ensure_parent \"$target\"; set_tmp_paths \"$target\" \"$token\"; : > \"$tmp\"", " ;;", " write-b64-append-stdin)", - " target=$1; token=$2; set_tmp_paths \"$target\" \"$token\"; cat >> \"$tmp_b64\"", + " target=$1; token=$2; set_tmp_paths \"$target\" \"$token\"", + " if ! base64 -d >> \"$tmp\"; then rm -f -- \"$tmp\"; printf 'transfer base64 chunk decode failed for %s\\n' \"$target\" >&2; exit 22; fi", " ;;", " 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\"", + " 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;;", @@ -819,9 +819,9 @@ function windowsFileTransferScript(basePath: string | null, operation: SshFileTr " '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 }", + " 'write-b64-begin' { Ensure-Parent $target; Set-TmpPaths $target $arg1; [System.IO.File]::WriteAllBytes($script:tmp, [byte[]]::new(0)); break }", + " 'write-b64-append-stdin' { Set-TmpPaths $target $arg1; try { $bytes = [Convert]::FromBase64String((([Console]::In.ReadToEnd()) -replace '\\s','')) } catch { Remove-Item -LiteralPath $script:tmp -Force -ErrorAction SilentlyContinue; Fail ('transfer base64 chunk decode failed for ' + $target + ': ' + $_.Exception.Message) 22 }; $stream = [System.IO.File]::Open($script:tmp, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None); try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() }; break }", + " 'write-b64-commit' { Set-TmpPaths $target $arg1; Verify-Temp $target $script:tmp ([Int64]$arg2) $arg3; Move-Item -LiteralPath $script:tmp -Destination $target -Force; $actualSha256 = Get-Sha256 $target; if ($actualSha256 -ne $arg3) { Fail ('transfer final sha256 mismatch for ' + $target) 25 }; break }", " default { Fail ('unsupported transfer op: ' + $operation) 2 }", "}", ].join(" ");