diff --git a/src-tauri/src/commands/analysis.rs b/src-tauri/src/commands/analysis.rs index fcc97cf4..24a24031 100644 --- a/src-tauri/src/commands/analysis.rs +++ b/src-tauri/src/commands/analysis.rs @@ -60,10 +60,12 @@ const SAFE_TEXT_EXTENSIONS: &[&str] = &[ const SAFE_BINARY_EXTENSIONS: &[&str] = &["pdf", "docx", "doc", "xlsx", "xls"]; -fn compress_text(text: &str) -> Vec { +fn compress_text(text: &str) -> Result, String> { let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(text.as_bytes()).unwrap_or_default(); - encoder.finish().unwrap_or_default() + encoder + .write_all(text.as_bytes()) + .map_err(|e| format!("Compression write error: {e}"))?; + encoder.finish().map_err(|e| format!("Compression finish error: {e}")) } /// 100 MB cap — prevents decompression-bomb attacks on crafted DB entries. @@ -256,7 +258,8 @@ pub async fn upload_log_file( ..log_file }; - let compressed = compress_text(&extracted_text); + let compressed = compress_text(&extracted_text) + .map_err(|e| format!("Failed to compress log content: {e}"))?; let db = state.db.lock().map_err(|e| e.to_string())?; db.execute( @@ -349,7 +352,8 @@ pub async fn upload_log_file_by_content( ..log_file }; - let compressed = compress_text(&content); + let compressed = compress_text(&content) + .map_err(|e| format!("Failed to compress log content: {e}"))?; let db = state.db.lock().map_err(|e| e.to_string())?; db.execute( @@ -682,17 +686,27 @@ mod tests { #[test] fn test_compress_decompress_roundtrip() { let original = "Hello, World! This is a log line with some content."; - let compressed = compress_text(original); + let compressed = compress_text(original).unwrap(); assert!(!compressed.is_empty()); assert!(compressed.len() < original.len() * 3); let decompressed = decompress_text(&compressed).unwrap(); assert_eq!(decompressed, original); } + #[test] + fn test_compress_returns_error_not_empty_on_failure() { + // compress_text returns Result — callers must propagate, not silently discard. + // For in-memory gzip this essentially never fails, but the API now allows + // callers to surface the error rather than storing empty bytes. + let result = compress_text("normal log line"); + assert!(result.is_ok(), "compress_text should succeed for normal input"); + assert!(!result.unwrap().is_empty()); + } + #[test] fn test_compress_large_text_is_smaller() { let original = "INFO server started\n".repeat(1000); - let compressed = compress_text(&original); + let compressed = compress_text(&original).unwrap(); assert!( compressed.len() < original.len(), "gzip should compress repetitive text" @@ -707,9 +721,6 @@ mod tests { #[test] fn test_decompress_size_limit_enforced() { - // Create a compressible payload that when decompressed exceeds the limit. - // We mock the limit by creating a large repetitive string and checking the - // round-trip succeeds, then verify the limit constant is set correctly. assert_eq!( MAX_DECOMPRESSED_BYTES, 100 * 1024 * 1024, @@ -718,7 +729,7 @@ mod tests { // A valid small payload must still decompress fine after the guard is in place. let text = "hello world decompression guard test\n".repeat(100); - let compressed = compress_text(&text); + let compressed = compress_text(&text).unwrap(); let result = decompress_text(&compressed); assert!(result.is_ok()); assert_eq!(result.unwrap(), text); diff --git a/src-tauri/src/commands/image.rs b/src-tauri/src/commands/image.rs index 79a5e1bc..60cec096 100644 --- a/src-tauri/src/commands/image.rs +++ b/src-tauri/src/commands/image.rs @@ -140,6 +140,13 @@ pub async fn upload_image_attachment_by_content( .decode(data_part) .map_err(|_| "Failed to decode base64 image data")?; + if decoded.len() as u64 > MAX_IMAGE_FILE_BYTES { + return Err(format!( + "Image content exceeds maximum supported size ({} MB)", + MAX_IMAGE_FILE_BYTES / 1024 / 1024 + )); + } + let content_hash = format!("{:x}", sha2::Sha256::digest(&decoded)); let file_size = decoded.len() as i64; @@ -231,6 +238,13 @@ pub async fn upload_paste_image( .decode(data_part) .map_err(|_| "Failed to decode base64 image data")?; + if decoded.len() as u64 > MAX_IMAGE_FILE_BYTES { + return Err(format!( + "Pasted image exceeds maximum supported size ({} MB)", + MAX_IMAGE_FILE_BYTES / 1024 / 1024 + )); + } + let content_hash = format!("{:x}", sha2::Sha256::digest(&decoded)); let file_size = decoded.len() as i64; let file_name = format!("pasted-image-{}.png", uuid::Uuid::now_v7()); diff --git a/src/pages/History/index.tsx b/src/pages/History/index.tsx index 17cbe7ec..be81a9c3 100644 --- a/src/pages/History/index.tsx +++ b/src/pages/History/index.tsx @@ -259,6 +259,7 @@ function AttachmentsTab({ navigate }: { navigate: ReturnType const [viewModal, setViewModal] = useState<{ type: "log" | "image"; id: string; title: string } | null>(null); const [modalContent, setModalContent] = useState(null); + const [modalError, setModalError] = useState(null); const [modalLoading, setModalLoading] = useState(false); useEffect(() => { @@ -290,12 +291,13 @@ function AttachmentsTab({ navigate }: { navigate: ReturnType const openImageModal = async (id: string, fileName: string) => { setViewModal({ type: "image", id, title: fileName }); setModalContent(null); + setModalError(null); setModalLoading(true); try { const dataUrl = await getImageAttachmentDataCmd(id); setModalContent(dataUrl); } catch (e) { - setModalContent(null); + setModalError(String(e)); } finally { setModalLoading(false); } @@ -304,6 +306,7 @@ function AttachmentsTab({ navigate }: { navigate: ReturnType const closeModal = () => { setViewModal(null); setModalContent(null); + setModalError(null); }; const formatBytes = (bytes: number) => { @@ -510,7 +513,12 @@ function AttachmentsTab({ navigate }: { navigate: ReturnType /> )} {!modalLoading && viewModal.type === "image" && !modalContent && ( -
Image could not be loaded.
+
+
Image could not be loaded.
+ {modalError && ( +
{modalError}
+ )} +
)}