feat(02-01): format types, crypto pipeline, and compression module

- Implement Header and TocEntry structs matching FORMAT.md byte layout
- Add write_header (40 bytes) and write_toc_entry (101+name_len bytes) serialization
- Add read_header, read_toc_entry, read_toc deserialization with validation
- Implement AES-256-CBC encrypt/decrypt with PKCS7 padding via cbc crate
- Add HMAC-SHA-256 compute/verify over IV||ciphertext (encrypt-then-MAC)
- Add SHA-256 hash for original file integrity
- Implement gzip compress/decompress with deterministic mtime(0)
- Add should_compress heuristic for known compressed file extensions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
NikitolProject
2026-02-24 23:58:08 +03:00
parent c647f3a90e
commit 6292b41159
3 changed files with 340 additions and 6 deletions

View File

@@ -1,2 +1,51 @@
// Gzip compression/decompression and compression heuristic.
// Will be implemented in Task 2.
use flate2::read::GzDecoder;
use flate2::{Compression, GzBuilder};
use std::io::{Read, Write};
/// Gzip-compress data with reproducible output (mtime zeroed).
///
/// Uses `GzBuilder::new().mtime(0)` to zero the gzip timestamp,
/// ensuring reproducible compressed output for testing.
pub fn compress(data: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut encoder = GzBuilder::new()
.mtime(0)
.write(Vec::new(), Compression::default());
encoder.write_all(data)?;
let compressed = encoder.finish()?;
Ok(compressed)
}
/// Gzip-decompress data.
pub fn decompress(data: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
/// Determine if a file should be compressed based on filename and exclusion list.
///
/// Returns false for:
/// - Files matching any entry in `no_compress_list` (by suffix or exact match)
/// - Files with known compressed extensions (apk, zip, gz, etc.)
///
/// Returns true otherwise.
pub fn should_compress(filename: &str, no_compress_list: &[String]) -> bool {
// Check explicit exclusion list
if no_compress_list
.iter()
.any(|nc| filename.ends_with(nc) || filename == nc)
{
return false;
}
// Check known compressed extensions
let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase();
!matches!(
ext.as_str(),
"apk" | "zip" | "gz" | "bz2" | "xz" | "zst"
| "png" | "jpg" | "jpeg" | "gif" | "webp"
| "mp4" | "mp3" | "aac" | "ogg" | "flac"
| "7z" | "rar" | "jar"
)
}