From 177e783f92f9053cd900e84a206130a4395e2af0 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 2 Jan 2026 00:32:22 -0600 Subject: [PATCH] fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --- src/backend/ssh/file-manager.ts | 54 +++++++++++++++++-- .../file-manager/components/FileViewer.tsx | 8 +-- .../file-manager/components/FileWindow.tsx | 30 ++++++++++- src/ui/main-axios.ts | 6 ++- 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 2769ed8c..97f6a88b 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -343,6 +343,27 @@ function getMimeType(fileName: string): string { return mimeTypes[ext || ""] || "application/octet-stream"; } +function detectBinary(buffer: Buffer): boolean { + if (buffer.length === 0) return false; + + const sampleSize = Math.min(buffer.length, 8192); + let nullBytes = 0; + + for (let i = 0; i < sampleSize; i++) { + const byte = buffer[i]; + + if (byte === 0) { + nullBytes++; + } + + if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) { + if (++nullBytes > 1) return true; + } + } + + return nullBytes / sampleSize > 0.01; +} + app.post("/ssh/file_manager/ssh/connect", async (req, res) => { const { sessionId, @@ -1368,11 +1389,11 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { return res.status(500).json({ error: err.message }); } - let data = ""; + let binaryData = Buffer.alloc(0); let errorData = ""; stream.on("data", (chunk: Buffer) => { - data += chunk.toString(); + binaryData = Buffer.concat([binaryData, chunk]); }); stream.stderr.on("data", (chunk: Buffer) => { @@ -1396,7 +1417,23 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { }); } - res.json({ content: data, path: filePath }); + const isBinary = detectBinary(binaryData); + + if (isBinary) { + const base64Content = binaryData.toString("base64"); + res.json({ + content: base64Content, + path: filePath, + encoding: "base64", + }); + } else { + const textContent = binaryData.toString("utf8"); + res.json({ + content: textContent, + path: filePath, + encoding: "utf8", + }); + } }); }); }); @@ -1440,7 +1477,16 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { let fileBuffer; try { if (typeof content === "string") { - fileBuffer = Buffer.from(content, "base64"); + try { + const testBuffer = Buffer.from(content, "base64"); + if (testBuffer.toString("base64") === content) { + fileBuffer = testBuffer; + } else { + fileBuffer = Buffer.from(content, "utf8"); + } + } catch { + fileBuffer = Buffer.from(content, "utf8"); + } } else if (Buffer.isBuffer(content)) { fileBuffer = content; } else { diff --git a/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx b/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx index d3f8455f..e09b5cee 100644 --- a/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/FileViewer.tsx @@ -333,13 +333,7 @@ export function FileViewer({ const ext = fileName.split(".").pop()?.toLowerCase() || ""; if (ext === "svg") { - try { - const base64 = btoa(unescape(encodeURIComponent(content))); - return `data:image/svg+xml;base64,${base64}`; - } catch (e) { - console.error("Failed to encode SVG:", e); - return ""; - } + return `data:image/svg+xml;base64,${content}`; } return `data:image/*;base64,${content}`; diff --git a/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx b/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx index 5768a949..9cf7f25c 100644 --- a/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/FileWindow.tsx @@ -47,6 +47,22 @@ interface FileWindowProps { onFileNotFound?: (file: FileItem) => void; } +function isDisplayableText(str: string): boolean { + let printable = 0; + for (let i = 0; i < Math.min(str.length, 1000); i++) { + const code = str.charCodeAt(i); + if ( + (code >= 32 && code <= 126) || + code === 9 || + code === 10 || + code === 13 + ) { + printable++; + } + } + return printable / Math.min(str.length, 1000) > 0.85; +} + export function FileWindow({ windowId, file, @@ -106,7 +122,19 @@ export function FileWindow({ await ensureSSHConnection(); const response = await readSSHFile(sshSessionId, file.path); - const fileContent = response.content || ""; + let fileContent = response.content || ""; + + if (response.encoding === "base64") { + try { + const decoded = atob(fileContent); + if (isDisplayableText(decoded)) { + fileContent = decoded; + } + } catch (err) { + console.error("Failed to decode base64 content:", err); + } + } + setContent(fileContent); setPendingContent(fileContent); diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 9e85d92c..afe82295 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1421,7 +1421,11 @@ export async function identifySSHSymlink( export async function readSSHFile( sessionId: string, path: string, -): Promise<{ content: string; path: string }> { +): Promise<{ + content: string; + path: string; + encoding?: "base64" | "utf8"; +}> { try { const response = await fileManagerApi.get("/ssh/readFile", { params: { sessionId, path },