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:
@@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user