//! Helpers for commands that take their payload as a raw IPC body (no base64). //! //! The webview sends `invoke(cmd, Uint8Array, { headers })`; Tauri hands the bytes over as //! `InvokeBody::Raw` and the headers as an `http::HeaderMap`. Header values must be visible //! ASCII (`HeaderValue::from_str` rejects anything else), so non-ASCII data such as file //! paths is percent-encoded by the caller. use percent_encoding::percent_decode_str; use std::collections::HashMap; use tauri::ipc::{InvokeBody, Request}; /// Lower-cased header name to value; values that are not valid UTF-8 are dropped. pub type Headers = HashMap; pub fn raw_body<'a>(request: &'a Request<'_>) -> Result<&'a [u8], String> { match request.body() { InvokeBody::Raw(bytes) => Ok(bytes), InvokeBody::Json(_) => Err("Expected a raw binary request body".to_string()), } } pub fn header_map(request: &Request<'_>) -> Headers { request .headers() .iter() .filter_map(|(name, value)| { let value = value.to_str().ok()?; Some((name.as_str().to_ascii_lowercase(), value.to_string())) }) .collect() } pub fn required_header<'a>(headers: &'a Headers, name: &str) -> Result<&'a str, String> { headers .get(name) .map(String::as_str) .ok_or_else(|| format!("Missing {name} header")) } pub fn decode_header_path(value: &str) -> Result { percent_decode_str(value) .decode_utf8() .map(|s| s.into_owned()) .map_err(|_| "The path header is not valid UTF-8".to_string()) } /// Write `bytes` to `path` via a sibling temp file and a rename, so a crash never leaves a /// half-written file under the final name. pub fn write_atomic(path: &std::path::Path, bytes: &[u8]) -> Result<(), String> { use std::io::Write; let mut tmp_name = path .file_name() .ok_or_else(|| "The path has no file name".to_string())? .to_os_string(); tmp_name.push(".tmp"); let tmp = path.with_file_name(tmp_name); let result = (|| { let mut file = std::fs::File::create(&tmp)?; file.write_all(bytes)?; file.sync_all()?; std::fs::rename(&tmp, path) })(); if let Err(e) = result { let _ = std::fs::remove_file(&tmp); return Err(format!("Could not write {}: {e}", path.display())); } Ok(()) } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; #[test] fn decodes_percent_encoded_paths() { assert_eq!( decode_header_path("%2Ftmp%2Fa%20b%2F%E0%A6%AC.pdf").unwrap(), "/tmp/a b/\u{9ac}.pdf" ); assert!(decode_header_path("%FF").is_err()); } #[test] fn atomic_write_replaces_and_leaves_no_temp_file() { let dir = tempdir().unwrap(); let target = dir.path().join("out.pdf"); write_atomic(&target, b"one").unwrap(); write_atomic(&target, b"two").unwrap(); assert_eq!(std::fs::read(&target).unwrap(), b"two"); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1); } }