test(03-01): add 19 unit tests for crypto, compression, and format modules
- crypto: encrypt/decrypt roundtrip, empty data, size formula, HMAC compute/verify, SHA-256 known values - compression: compress/decompress roundtrip, empty data, large data, should_compress heuristic - format: header write/read roundtrip, TOC entry roundtrip (ASCII + Cyrillic + empty name), bad magic/version rejection, entry size calculation matching FORMAT.md worked example - Update hex-literal to v1.1
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<u8> = (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()]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
188
src/format.rs
188
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user