From 21f7726316cbb0405b57c9589f83e934041c583d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:02:11 +0000 Subject: [PATCH] fix: replace write_text_file with backend dialog Replaces `write_text_file` and `write_image_file` with backend commands `save_text_file_dialog` and `save_image_file_dialog`. Instead of allowing the frontend to pass an arbitrary absolute path, the Tauri backend now invokes the OS save dialog directly, eliminating the risk of arbitrary file write if the frontend is compromised. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> --- package-lock.json | 10 ++++++++++ package.json | 1 + src-tauri/src/main.rs | 45 +++++++++++++++++++++++++++++++------------ src/main.ts | 12 +++--------- src/storage.ts | 6 +++--- src/ui/export.ts | 12 ++++-------- 6 files changed, 54 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cdd7de..c5428af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-autostart": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-global-shortcut": "^2.2.0", "@tauri-apps/plugin-opener": "^2.2.0", "@tauri-apps/plugin-process": "^2.3.1", @@ -1218,6 +1219,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.1.tgz", + "integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tauri-apps/plugin-global-shortcut": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.2.tgz", diff --git a/package.json b/package.json index 4df5651..239414a 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-autostart": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-global-shortcut": "^2.2.0", "@tauri-apps/plugin-opener": "^2.2.0", "@tauri-apps/plugin-process": "^2.3.1", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4fd3982..d57ce83 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -38,20 +38,41 @@ fn safe_name(name: &str) -> bool { } #[tauri::command] -fn write_text_file(path: String, contents: String) -> Result<(), String> { - write_atomic(std::path::Path::new(&path), &contents) +async fn save_text_file_dialog(app: tauri::AppHandle, contents: String, is_sum: bool) -> Result { + use tauri_plugin_dialog::DialogExt; + let dialog = app.dialog().file(); + let dialog = if is_sum { + dialog.add_filter("Summarum Sheet", &["sum"]) + } else { + dialog.add_filter("Text file", &["txt"]) + }; + + if let Some(path) = dialog.blocking_save_file() { + let path = path.into_path().map_err(|_| "invalid path".to_string())?; + write_atomic(&path, &contents)?; + Ok(true) + } else { + Ok(false) + } } #[tauri::command] -fn write_image_file(path: String, data_base64: String) -> Result<(), String> { +async fn save_image_file_dialog(app: tauri::AppHandle, data_base64: String) -> Result { + use tauri_plugin_dialog::DialogExt; use std::io::Write; - let bytes = base64_decode(&data_base64)?; - let p = std::path::Path::new(&path); - let tmp = p.with_extension("tmp"); - std::fs::File::create(&tmp) - .and_then(|mut f| f.write_all(&bytes)) - .map_err(|e| e.to_string())?; - fs::rename(&tmp, p).map_err(|e| e.to_string()) + + if let Some(path) = app.dialog().file().add_filter("PNG Image", &["png"]).blocking_save_file() { + let path = path.into_path().map_err(|_| "invalid path".to_string())?; + let bytes = base64_decode(&data_base64)?; + let tmp = path.with_extension("tmp"); + std::fs::File::create(&tmp) + .and_then(|mut f| f.write_all(&bytes)) + .map_err(|e| e.to_string())?; + fs::rename(&tmp, &path).map_err(|e| e.to_string())?; + Ok(true) + } else { + Ok(false) + } } fn base64_decode(s: &str) -> Result, String> { @@ -712,8 +733,8 @@ fn main() { read_text_file, exit_app, is_hidden_launch, - write_text_file, - write_image_file + save_text_file_dialog, + save_image_file_dialog ]) .setup(|app| { let handle = app.handle().clone(); diff --git a/src/main.ts b/src/main.ts index a0d754b..7798d1a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -907,17 +907,11 @@ async function boot(): Promise { } else if (action === "print") { window.print(); } else if (isTauri()) { - const { save } = await import("@tauri-apps/plugin-dialog"); const { invoke } = await import("@tauri-apps/api/core"); const isSum = action === "save-sum"; - const path = await save({ - filters: isSum - ? [{ name: "Summarum Sheet", extensions: ["sum"] }] - : [{ name: "Text file", extensions: ["txt"] }], - }); - if (path) { - const content = isSum ? editor.getText() : editor.getSheetWithResults(); - await invoke("write_text_file", { path, contents: content }); + const content = isSum ? editor.getText() : editor.getSheetWithResults(); + const saved = await invoke("save_text_file_dialog", { contents: content, isSum }); + if (saved) { toast(t("saved")); } } diff --git a/src/storage.ts b/src/storage.ts index ce99608..70ef897 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -234,9 +234,9 @@ export async function fetchHistoricalRates(date: string): Promise { - if (!isTauri()) return; - await invoke("write_image_file", { path, dataBase64 }); +export async function writeImageFile(dataBase64: string): Promise { + if (!isTauri()) return false; + return await invoke("save_image_file_dialog", { dataBase64 }); } // ---------- market data diff --git a/src/ui/export.ts b/src/ui/export.ts index b94e6ab..7b2b456 100644 --- a/src/ui/export.ts +++ b/src/ui/export.ts @@ -58,14 +58,10 @@ export async function exportSheetImage(deps: ExportImageDeps): Promise { } catch { // clipboard API failed — save to file try { - const { save } = await import("@tauri-apps/plugin-dialog"); - const path = await save({ filters: [{ name: "PNG Image", extensions: ["png"] }] }); - if (path) { - const buf = await blob.arrayBuffer(); - const b64 = bytesToBase64(new Uint8Array(buf)); - await writeImageFile(path, b64); - toast(t("saved")); - } + const buf = await blob.arrayBuffer(); + const b64 = bytesToBase64(new Uint8Array(buf)); + const saved = await writeImageFile(b64); + if (saved) toast(t("saved")); } catch { toast(t("imageFailed")); }