diff --git a/Cargo.lock b/Cargo.lock index 3db006a..54f4697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,9 +339,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex-literal" -version = "0.4.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hmac" diff --git a/Cargo.toml b/Cargo.toml index 6bc7e33..6cce685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,5 @@ anyhow = "1.0" [dev-dependencies] tempfile = "3.16" assert_cmd = "2.0" -hex-literal = "0.4" +hex-literal = "1.1" predicates = "3.1" diff --git a/src/compression.rs b/src/compression.rs index 1022ee2..445d277 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -49,3 +49,51 @@ pub fn should_compress(filename: &str, no_compress_list: &[String]) -> bool { | "7z" | "rar" | "jar" ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compress_decompress_roundtrip() { + let data = b"Hello, World! This is test data for compression."; + let compressed = compress(data).unwrap(); + let decompressed = decompress(&compressed).unwrap(); + assert_eq!(decompressed, data); + } + + #[test] + fn test_compress_decompress_empty() { + let data = b""; + let compressed = compress(data).unwrap(); + let decompressed = decompress(&compressed).unwrap(); + assert_eq!(decompressed, data.as_slice()); + } + + #[test] + fn test_compress_decompress_large() { + // 10000 bytes of pattern data + let data: Vec = (0..10000).map(|i| (i % 256) as u8).collect(); + let compressed = compress(&data).unwrap(); + let decompressed = decompress(&compressed).unwrap(); + assert_eq!(decompressed, data); + } + + #[test] + fn test_should_compress_text() { + assert!(should_compress("readme.txt", &[])); + assert!(should_compress("data.json", &[])); + } + + #[test] + fn test_should_not_compress_known_extensions() { + assert!(!should_compress("app.apk", &[])); + assert!(!should_compress("photo.jpg", &[])); + assert!(!should_compress("archive.zip", &[])); + } + + #[test] + fn test_should_not_compress_excluded() { + assert!(!should_compress("special.dat", &["special.dat".into()])); + } +} diff --git a/src/crypto.rs b/src/crypto.rs index 2e4cf8d..2fed43b 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -76,3 +76,66 @@ pub fn sha256_hash(data: &[u8]) -> [u8; 32] { use sha2::Digest; sha2::Sha256::digest(data).into() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::KEY; + use hex_literal::hex; + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let plaintext = b"Hello, World!"; + let iv = [0u8; 16]; + let ciphertext = encrypt_data(plaintext, &KEY, &iv); + let decrypted = decrypt_data(&ciphertext, &KEY, &iv).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_encrypt_decrypt_empty() { + let plaintext = b""; + let iv = [0u8; 16]; + let ciphertext = encrypt_data(plaintext, &KEY, &iv); + let decrypted = decrypt_data(&ciphertext, &KEY, &iv).unwrap(); + assert_eq!(decrypted, plaintext.as_slice()); + } + + #[test] + fn test_encrypted_size_formula() { + let iv = [0u8; 16]; + // 5 bytes -> ((5/16)+1)*16 = 16 + assert_eq!(encrypt_data(b"Hello", &KEY, &iv).len(), 16); + // 16 bytes -> ((16/16)+1)*16 = 32 (full padding block) + assert_eq!(encrypt_data(&[0u8; 16], &KEY, &iv).len(), 32); + // 0 bytes -> ((0/16)+1)*16 = 16 + assert_eq!(encrypt_data(b"", &KEY, &iv).len(), 16); + } + + #[test] + fn test_hmac_compute_verify() { + let iv = [0xAA; 16]; + let ciphertext = b"some ciphertext data here!!12345"; + let hmac_tag = compute_hmac(&KEY, &iv, ciphertext); + // Verify with correct tag + assert!(verify_hmac(&KEY, &iv, ciphertext, &hmac_tag)); + // Verify with wrong tag + let wrong_tag = [0u8; 32]; + assert!(!verify_hmac(&KEY, &iv, ciphertext, &wrong_tag)); + } + + #[test] + fn test_sha256_known_value() { + // SHA-256("Hello") from FORMAT.md Section 12.3 + let expected = hex!("185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"); + let result = sha256_hash(b"Hello"); + assert_eq!(result, expected); + } + + #[test] + fn test_sha256_empty() { + let expected = hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + let result = sha256_hash(b""); + assert_eq!(result, expected); + } +} diff --git a/src/format.rs b/src/format.rs index 544e5a5..61e5caa 100644 --- a/src/format.rs +++ b/src/format.rs @@ -209,3 +209,191 @@ pub fn entry_size(entry: &TocEntry) -> u32 { pub fn compute_toc_size(entries: &[TocEntry]) -> u32 { entries.iter().map(entry_size).sum() } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_header_write_read_roundtrip() { + let header = Header { + version: 1, + flags: 0x01, + file_count: 3, + toc_offset: HEADER_SIZE, + toc_size: 330, + toc_iv: [0u8; 16], + reserved: [0u8; 8], + }; + + let mut buf = Vec::new(); + write_header(&mut buf, &header).unwrap(); + assert_eq!(buf.len(), HEADER_SIZE as usize); + + let mut cursor = Cursor::new(&buf); + let read_back = read_header(&mut cursor).unwrap(); + + assert_eq!(read_back.version, header.version); + assert_eq!(read_back.flags, header.flags); + assert_eq!(read_back.file_count, header.file_count); + assert_eq!(read_back.toc_offset, header.toc_offset); + assert_eq!(read_back.toc_size, header.toc_size); + assert_eq!(read_back.toc_iv, header.toc_iv); + assert_eq!(read_back.reserved, header.reserved); + } + + #[test] + fn test_toc_entry_roundtrip_ascii() { + let entry = TocEntry { + name: "hello.txt".to_string(), + original_size: 5, + compressed_size: 25, + encrypted_size: 32, + data_offset: 259, + iv: [0xAA; 16], + hmac: [0xBB; 32], + sha256: [0xCC; 32], + compression_flag: 1, + padding_after: 0, + }; + + let mut buf = Vec::new(); + write_toc_entry(&mut buf, &entry).unwrap(); + assert_eq!(buf.len(), 101 + 9); // 101 + "hello.txt".len() + + let mut cursor = Cursor::new(&buf); + let read_back = read_toc_entry(&mut cursor).unwrap(); + + assert_eq!(read_back.name, entry.name); + assert_eq!(read_back.original_size, entry.original_size); + assert_eq!(read_back.compressed_size, entry.compressed_size); + assert_eq!(read_back.encrypted_size, entry.encrypted_size); + assert_eq!(read_back.data_offset, entry.data_offset); + assert_eq!(read_back.iv, entry.iv); + assert_eq!(read_back.hmac, entry.hmac); + assert_eq!(read_back.sha256, entry.sha256); + assert_eq!(read_back.compression_flag, entry.compression_flag); + assert_eq!(read_back.padding_after, entry.padding_after); + } + + #[test] + fn test_toc_entry_roundtrip_cyrillic() { + let name = "\u{0442}\u{0435}\u{0441}\u{0442}\u{043e}\u{0432}\u{044b}\u{0439}_\u{0444}\u{0430}\u{0439}\u{043b}.txt"; + let entry = TocEntry { + name: name.to_string(), + original_size: 100, + compressed_size: 80, + encrypted_size: 96, + data_offset: 500, + iv: [0x11; 16], + hmac: [0x22; 32], + sha256: [0x33; 32], + compression_flag: 1, + padding_after: 0, + }; + + let mut buf = Vec::new(); + write_toc_entry(&mut buf, &entry).unwrap(); + // "тестовый_файл.txt" UTF-8 length + let expected_name_len = name.len(); + assert_eq!(buf.len(), 101 + expected_name_len); + + let mut cursor = Cursor::new(&buf); + let read_back = read_toc_entry(&mut cursor).unwrap(); + + assert_eq!(read_back.name, name); + assert_eq!(read_back.original_size, entry.original_size); + assert_eq!(read_back.compressed_size, entry.compressed_size); + assert_eq!(read_back.encrypted_size, entry.encrypted_size); + assert_eq!(read_back.data_offset, entry.data_offset); + } + + #[test] + fn test_toc_entry_roundtrip_empty_name() { + let entry = TocEntry { + name: "".to_string(), + original_size: 0, + compressed_size: 0, + encrypted_size: 16, + data_offset: 40, + iv: [0u8; 16], + hmac: [0u8; 32], + sha256: [0u8; 32], + compression_flag: 0, + padding_after: 0, + }; + + let mut buf = Vec::new(); + write_toc_entry(&mut buf, &entry).unwrap(); + + let mut cursor = Cursor::new(&buf); + let read_back = read_toc_entry(&mut cursor).unwrap(); + + assert_eq!(read_back.name, ""); + } + + #[test] + fn test_header_rejects_bad_magic() { + let mut buf = vec![0u8; 40]; + // Wrong magic bytes + buf[0] = 0xFF; + buf[1] = 0xFF; + buf[2] = 0xFF; + buf[3] = 0xFF; + buf[4] = 1; // version + + let mut cursor = Cursor::new(&buf); + let result = read_header(&mut cursor); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("magic")); + } + + #[test] + fn test_header_rejects_bad_version() { + let mut buf = vec![0u8; 40]; + // Correct magic + buf[0..4].copy_from_slice(&MAGIC); + // Wrong version + buf[4] = 2; + + let mut cursor = Cursor::new(&buf); + let result = read_header(&mut cursor); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("version")); + } + + #[test] + fn test_entry_size_calculation() { + let entry_hello = TocEntry { + name: "hello.txt".to_string(), // 9 bytes + original_size: 5, + compressed_size: 25, + encrypted_size: 32, + data_offset: 259, + iv: [0u8; 16], + hmac: [0u8; 32], + sha256: [0u8; 32], + compression_flag: 1, + padding_after: 0, + }; + assert_eq!(entry_size(&entry_hello), 110); // 101 + 9 + + let entry_data = TocEntry { + name: "data.bin".to_string(), // 8 bytes + original_size: 32, + compressed_size: 22, + encrypted_size: 32, + data_offset: 291, + iv: [0u8; 16], + hmac: [0u8; 32], + sha256: [0u8; 32], + compression_flag: 1, + padding_after: 0, + }; + assert_eq!(entry_size(&entry_data), 109); // 101 + 8 + + // FORMAT.md worked example: 110 + 109 = 219 + assert_eq!(compute_toc_size(&[entry_hello, entry_data]), 219); + } +}