From 36d69c8fc20848a4857e443c65d5b5e6fcf8cefe Mon Sep 17 00:00:00 2001 From: forkless Date: Tue, 9 Jun 2026 13:35:04 +0200 Subject: [PATCH] add archive integrity check and tests for corrupted backup detection --- src/ops.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/ops.rs b/src/ops.rs index 7692527..b5d611f 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -176,6 +176,26 @@ fn extract_tar_gz(archive_path: &Path, dest: &Path) -> Result { Ok(count) } +/// Quick integrity check: verify the file is non-empty and starts with gzip +/// magic bytes (`1f 8b`). Returns `true` if the archive looks valid. +pub fn check_tar_gz_integrity(path: &Path) -> bool { + let meta = match std::fs::metadata(path) { + Ok(m) => m, + Err(_) => return false, + }; + if meta.len() < 20 { + return false; // too small to be a valid archive + } + let mut buf = [0u8; 2]; + use std::io::Read; + if let Ok(mut f) = std::fs::File::open(path) { + if f.read_exact(&mut buf).is_ok() { + return buf == [0x1f, 0x8b]; // gzip magic + } + } + false +} + /// List tar.gz files in a directory, sorted by mtime descending. fn list_tar_gz(dir: &Path) -> Vec { if !dir.exists() { @@ -625,4 +645,51 @@ mod tests { let c2 = migrate_backups_from(old_root.clone()).unwrap(); assert_eq!(c2, 1, "second migration (should not duplicate)"); } + + // ── integrity check ──────────────────────────────────────────── + + #[test] + fn integrity_check_valid_gzip() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.tar.gz"); + // Write a minimal valid gzip file (20 bytes of gzip stream) + let valid_gzip = [ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + fs::write(&path, &valid_gzip).unwrap(); + assert!(check_tar_gz_integrity(&path)); + } + + #[test] + fn integrity_check_empty_file() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("empty.tar.gz"); + fs::write(&path, b"").unwrap(); + assert!(!check_tar_gz_integrity(&path)); + } + + #[test] + fn integrity_check_non_gzip() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("not_gzip.tar.gz"); + fs::write(&path, b"this is not a gzip file").unwrap(); + assert!(!check_tar_gz_integrity(&path)); + } + + #[test] + fn integrity_check_too_small() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("tiny.tar.gz"); + // Gzip magic bytes present but file is too small (3 bytes) + fs::write(&path, &[0x1f, 0x8b, 0x08]).unwrap(); + assert!(!check_tar_gz_integrity(&path)); + } + + #[test] + fn integrity_check_nonexistent_file() { + let path = std::path::Path::new("/nonexistent/path.tar.gz"); + assert!(!check_tar_gz_integrity(&path)); + } }