Fix code quality: trailing whitespace, unused imports, clippy warnings

- Fix trailing whitespace in kex.rs and s3.rs
- Add missing KexProposal import in kex_complete.rs
- Auto-fix clippy warnings across all crates
- All 153 tests pass
This commit is contained in:
Warren
2026-06-19 05:21:38 +08:00
parent 4b37e524cf
commit d94cb2df4c
135 changed files with 7256 additions and 4321 deletions

View File

@@ -1,22 +1,21 @@
// Archive Configuration - User Configurable Options
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use log::warn;
use serde::{Deserialize, Serialize};
/// Archive Configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveConfig {
// Optional formats (controversial)
pub enable_rar: bool, // ⚠️ Legal risk (RARLAB patent)
pub enable_xz: bool, // ⚠️ External dependency (liblzma)
pub enable_7z: bool, // ⚠️ Unstable library
pub enable_rar: bool, // ⚠️ Legal risk (RARLAB patent)
pub enable_xz: bool, // ⚠️ External dependency (liblzma)
pub enable_7z: bool, // ⚠️ Unstable library
// Performance settings
pub cache_size_mb: u64,
pub max_concurrent_extractions: usize,
// Security settings
pub max_decompression_ratio: u64,
pub max_file_size_mb: u64,
@@ -29,11 +28,11 @@ impl Default for ArchiveConfig {
enable_rar: false,
enable_xz: false,
enable_7z: false,
// Performance
cache_size_mb: 100,
max_concurrent_extractions: 4,
// Security
max_decompression_ratio: 1000,
max_file_size_mb: 1024,
@@ -46,45 +45,46 @@ impl ArchiveConfig {
pub fn load(path: &str) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: ArchiveConfig = toml::from_str(&content)?;
// Validate configuration
config.validate()?;
Ok(config)
}
/// Save configuration to TOML file
pub fn save(&self, path: &str) -> Result<()> {
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
/// Validate configuration
pub fn validate(&self) -> Result<()> {
if self.cache_size_mb > 1000 {
warn!("Cache size > 1GB may cause memory pressure");
}
if self.max_concurrent_extractions > 10 {
warn!("Concurrent extractions > 10 may cause resource exhaustion");
}
if self.max_decompression_ratio < 10 {
return Err(anyhow::anyhow!("Max decompression ratio too low (min 10)"));
}
if self.max_file_size_mb > 10_000 { // 10GB
if self.max_file_size_mb > 10_000 {
// 10GB
warn!("Max file size > 10GB may cause disk space issues");
}
Ok(())
}
/// Generate default config file template
pub fn generate_template() -> String {
let config = Self::default();
format!(
"# === Archive Configuration ===
# MarkBase Universal Compression Format Support
@@ -138,33 +138,33 @@ max_file_size_mb = {} # File size limit (MB)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ArchiveConfig::default();
assert_eq!(config.enable_rar, false);
assert_eq!(config.enable_xz, false);
assert_eq!(config.enable_7z, false);
assert_eq!(config.cache_size_mb, 100);
assert_eq!(config.max_decompression_ratio, 1000);
}
#[test]
fn test_config_validation() {
let config = ArchiveConfig {
max_decompression_ratio: 5,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_config_template() {
let template = ArchiveConfig::generate_template();
assert!(template.contains("enable_rar = false"));
assert!(template.contains("⚠️ RAR Format Legal Risk Warning"));
}
}
}

View File

@@ -1,9 +1,9 @@
// Format Detector - Automatic Detection Based on Magic Numbers
use anyhow::Result;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use anyhow::Result;
use crate::archive::processor::ArchiveFormat;
@@ -18,64 +18,61 @@ impl FormatDetector {
// ZIP: 50 4B 03 04 or 50 4B 05 06 (empty) or 50 4B 07 08 (spanned)
(vec![0x50, 0x4B, 0x03, 0x04], ArchiveFormat::Zip, 4),
(vec![0x50, 0x4B, 0x05, 0x06], ArchiveFormat::Zip, 4),
// GZIP: 1F 8B
(vec![0x1F, 0x8B], ArchiveFormat::Gzip, 2),
];
Self { magic_table }
}
/// Detect file format based on Magic Number
pub fn detect(&self, path: &Path) -> Result<ArchiveFormat> {
let mut file = File::open(path)?;
let mut buffer = vec![0u8; 512];
let bytes_read = file.read(&mut buffer)?;
if bytes_read < 2 {
return Ok(ArchiveFormat::Unknown);
}
// Match Magic Numbers
for (magic, format, offset) in &self.magic_table {
if buffer.len() >= *offset && buffer[0..magic.len()] == *magic {
return Ok(*format);
}
}
// Special detection: TAR format (check ustar magic at offset 257)
if buffer.len() >= 262 {
if &buffer[257..262] == b"ustar" {
if buffer.len() >= 262
&& &buffer[257..262] == b"ustar" {
return Ok(ArchiveFormat::Tar);
}
}
Ok(ArchiveFormat::Unknown)
}
/// Detect composite format (e.g., TAR.GZ)
pub fn detect_composite(&self, path: &Path) -> Result<ArchiveFormat> {
let format = self.detect(path)?;
// If GZIP, check if it's TAR.GZ (by extension for now)
if format == ArchiveFormat::Gzip {
let ext = path.extension()
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if ext == "tgz" || ext == "gz" {
// Check if filename contains .tar
let filename = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename.contains(".tar") {
return Ok(ArchiveFormat::TarGzip);
}
}
}
Ok(format)
}
}
@@ -89,51 +86,51 @@ impl Default for FormatDetector {
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_detect_zip() {
let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip");
// Create minimal ZIP file header
let mut file = File::create(&zip_path).unwrap();
file.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap();
let detector = FormatDetector::new();
let format = detector.detect(&zip_path).unwrap();
assert_eq!(format, ArchiveFormat::Zip);
}
#[test]
fn test_detect_gzip() {
let temp_dir = TempDir::new().unwrap();
let gz_path = temp_dir.path().join("test.gz");
// Create minimal GZIP file header
let mut file = File::create(&gz_path).unwrap();
file.write_all(&[0x1F, 0x8B]).unwrap();
let detector = FormatDetector::new();
let format = detector.detect(&gz_path).unwrap();
assert_eq!(format, ArchiveFormat::Gzip);
}
#[test]
fn test_detect_unknown() {
let temp_dir = TempDir::new().unwrap();
let unknown_path = temp_dir.path().join("test.bin");
// Create unknown file
let mut file = File::create(&unknown_path).unwrap();
file.write_all(b"unknown data").unwrap();
let detector = FormatDetector::new();
let format = detector.detect(&unknown_path).unwrap();
assert_eq!(format, ArchiveFormat::Unknown);
}
}
}

View File

@@ -1,8 +1,8 @@
// Metadata Module - Archive Entry Metadata Management
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::archive::processor::ArchiveFormat;
@@ -29,7 +29,7 @@ impl ArchiveMetadata {
self.total_size as f64 / self.compressed_size as f64
}
}
/// Check if compression ratio exceeds limit (Zip Bomb detection)
pub fn check_zip_bomb(&self, max_ratio: u64) -> bool {
self.actual_ratio() > max_ratio as f64
@@ -65,7 +65,7 @@ impl ArchiveEntry {
checksum: None,
}
}
/// Create file entry
pub fn file(path: PathBuf, size: u64, compressed_size: u64) -> Self {
Self {
@@ -104,7 +104,7 @@ impl ExtractResult {
warnings: Vec::new(),
}
}
pub fn success_rate(&self) -> f64 {
if self.total_files == 0 {
100.0
@@ -113,11 +113,11 @@ impl ExtractResult {
(success_count as f64 / self.total_files as f64) * 100.0
}
}
pub fn has_failures(&self) -> bool {
!self.failed_files.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
@@ -126,7 +126,7 @@ impl ExtractResult {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_archive_metadata() {
let metadata = ArchiveMetadata {
@@ -140,37 +140,37 @@ mod tests {
created_time: None,
modified_time: None,
};
assert_eq!(metadata.actual_ratio(), 2.0);
assert!(!metadata.check_zip_bomb(1000));
assert!(metadata.check_zip_bomb(1)); // Should detect as bomb
assert!(metadata.check_zip_bomb(1)); // Should detect as bomb
}
#[test]
fn test_archive_entry() {
let dir_entry = ArchiveEntry::directory(PathBuf::from("test_dir"));
assert!(dir_entry.is_dir);
assert!(!dir_entry.is_file);
let file_entry = ArchiveEntry::file(PathBuf::from("test.txt"), 100, 50);
assert!(!file_entry.is_dir);
assert!(file_entry.is_file);
assert_eq!(file_entry.size, 100);
}
#[test]
fn test_extract_result() {
let result = ExtractResult::new();
assert_eq!(result.success_rate(), 100.0);
let result_with_failure = ExtractResult {
total_files: 10,
success_files: 8,
failed_files: vec![PathBuf::from("failed.txt")],
..Default::default()
};
assert_eq!(result_with_failure.success_rate(), 80.0);
assert!(result_with_failure.has_failures());
}
}
}

View File

@@ -25,9 +25,9 @@ pub use metadata::{ArchiveEntry, ArchiveMetadata, ExtractResult};
pub use processor::{ArchiveFormat, ArchiveProcessor};
use anyhow::Result;
use log::info;
use std::collections::HashMap;
use std::path::Path;
use log::{info, warn};
/// Processor Registry - Plugin Architecture
pub struct ProcessorRegistry {
@@ -43,93 +43,108 @@ impl ProcessorRegistry {
config,
}
}
/// Initialize all processors (based on config)
pub fn initialize(&mut self) -> Result<()> {
// Core formats (always registered)
self.register_core_processors()?;
// Optional formats (based on config)
self.register_optional_processors()?;
Ok(())
}
/// Register core format processors (9 formats)
fn register_core_processors(&mut self) -> Result<()> {
use crate::archive::processors::core::*;
self.processors.insert(ArchiveFormat::Zip, Box::new(ZipProcessor::new()));
self.processors.insert(ArchiveFormat::Tar, Box::new(TarProcessor::new()));
self.processors.insert(ArchiveFormat::Gzip, Box::new(GzipProcessor::new()));
self.processors.insert(ArchiveFormat::Zstd, Box::new(ZstdProcessor::new()));
self.processors.insert(ArchiveFormat::Bzip2, Box::new(Bzip2Processor::new()));
self.processors.insert(ArchiveFormat::Lz4, Box::new(Lz4Processor::new()));
self.processors.insert(ArchiveFormat::TarGzip, Box::new(TarGzipProcessor::new()));
self.processors.insert(ArchiveFormat::TarBzip2, Box::new(TarBzip2Processor::new()));
self.processors.insert(ArchiveFormat::TarZstd, Box::new(TarZstdProcessor::new()));
self.processors
.insert(ArchiveFormat::Zip, Box::new(ZipProcessor::new()));
self.processors
.insert(ArchiveFormat::Tar, Box::new(TarProcessor::new()));
self.processors
.insert(ArchiveFormat::Gzip, Box::new(GzipProcessor::new()));
self.processors
.insert(ArchiveFormat::Zstd, Box::new(ZstdProcessor::new()));
self.processors
.insert(ArchiveFormat::Bzip2, Box::new(Bzip2Processor::new()));
self.processors
.insert(ArchiveFormat::Lz4, Box::new(Lz4Processor::new()));
self.processors
.insert(ArchiveFormat::TarGzip, Box::new(TarGzipProcessor::new()));
self.processors
.insert(ArchiveFormat::TarBzip2, Box::new(TarBzip2Processor::new()));
self.processors
.insert(ArchiveFormat::TarZstd, Box::new(TarZstdProcessor::new()));
info!("✅ Core formats registered: 9 formats");
Ok(())
}
/// Register optional format processors (3 formats, based on config)
fn register_optional_processors(&mut self) -> Result<()> {
#[cfg(feature = "optional-formats")]
{
use crate::archive::processors::optional::*;
// RAR format (legal risk)
if self.config.enable_rar {
crate::archive::warning::show_rar_legal_warning();
self.processors.insert(ArchiveFormat::Rar, Box::new(RarProcessor::new()));
self.processors
.insert(ArchiveFormat::Rar, Box::new(RarProcessor::new()));
warn!("⚠️ RAR format enabled (legal risk)");
}
// XZ format (external dependency)
if self.config.enable_xz {
if check_liblzma_available() {
self.processors.insert(ArchiveFormat::Xz, Box::new(XzProcessor::new()));
self.processors
.insert(ArchiveFormat::Xz, Box::new(XzProcessor::new()));
info!("✅ XZ format enabled");
} else {
crate::archive::warning::show_xz_dependency_warning();
warn!("⚠️ XZ format disabled (liblzma not found)");
}
}
// 7z format (unstable library)
if self.config.enable_7z {
crate::archive::warning::show_7z_stability_warning();
self.processors.insert(ArchiveFormat::SevenZ, Box::new(SevenZProcessor::new()));
self.processors
.insert(ArchiveFormat::SevenZ, Box::new(SevenZProcessor::new()));
warn!("⚠️ 7z format enabled (stability warning)");
}
}
Ok(())
}
/// Get processor for detected format (mutable version for open/extraction)
pub fn get_processor_mut(&mut self, path: &Path) -> Result<&mut (dyn ArchiveProcessor + '_)> {
let detector = FormatDetector::new();
let format = detector.detect(path)?;
match self.processors.get_mut(&format) {
Some(p) => Ok(p.as_mut()),
None => Err(anyhow::anyhow!("Format {} not supported or not enabled", format)),
None => Err(anyhow::anyhow!(
"Format {} not supported or not enabled",
format
)),
}
}
/// Get processor for detected format (immutable version for listing)
pub fn get_processor(&self, path: &Path) -> Result<&dyn ArchiveProcessor> {
let detector = FormatDetector::new();
let format = detector.detect(path)?;
self.processors
.get(&format)
.map(|p| p.as_ref())
.ok_or_else(|| anyhow::anyhow!("Format {} not supported or not enabled", format))
}
/// List all enabled formats
pub fn enabled_formats(&self) -> Vec<ArchiveFormat> {
self.processors.keys().cloned().collect()
@@ -141,7 +156,7 @@ impl ProcessorRegistry {
fn check_liblzma_available() -> bool {
// Try to load xz2 library
// Simplified check: try to create XzProcessor
true // Simplified for now, actual implementation needs better detection
true // Simplified for now, actual implementation needs better detection
}
#[cfg(not(feature = "optional-formats"))]
@@ -156,13 +171,16 @@ pub fn init_archive_system(config_path: Option<&str>) -> Result<ProcessorRegistr
} else {
ArchiveConfig::default()
};
// Show startup warnings for optional formats
crate::archive::warning::show_startup_warnings(&config);
let mut registry = ProcessorRegistry::new(config);
registry.initialize()?;
info!("Archive system initialized with {} formats", registry.enabled_formats().len());
info!(
"Archive system initialized with {} formats",
registry.enabled_formats().len()
);
Ok(registry)
}
}

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use std::path::{Path, PathBuf};
// Re-export types from metadata.rs
pub use crate::archive::metadata::{ArchiveMetadata, ArchiveEntry, ExtractResult};
pub use crate::archive::metadata::{ArchiveEntry, ArchiveMetadata, ExtractResult};
/// Archive Format Type Enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
@@ -19,12 +19,12 @@ pub enum ArchiveFormat {
TarGzip,
TarBzip2,
TarZstd,
// Optional formats (controversial)
Rar, // ⚠️ Legal risk (RARLAB patent)
Xz, // ⚠️ External dependency (liblzma)
SevenZ, // ⚠️ Unstable library (sevenz-rust 0.21.0)
Unknown,
}
@@ -53,30 +53,34 @@ impl std::fmt::Display for ArchiveFormat {
pub trait ArchiveProcessor: Send + Sync {
/// Format type supported by this processor
fn format(&self) -> ArchiveFormat;
/// Open archive file and read metadata
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata>;
/// List all file entries in archive
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>>;
/// Extract single file (on-demand decompression)
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64>;
/// Extract all files to directory (batch extraction)
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult>;
/// Check if this processor can handle the format
fn can_process(format: ArchiveFormat) -> bool where Self: Sized;
fn can_process(format: ArchiveFormat) -> bool
where
Self: Sized;
/// Create new processor instance
fn new() -> Self where Self: Sized;
fn new() -> Self
where
Self: Sized;
}
/// Security Validation - Zip Slip Protection
pub fn validate_extraction_path(entry_path: &Path, base_dir: &Path) -> Result<PathBuf> {
use std::path::Component;
// 1. Check path components
for component in entry_path.components() {
match component {
@@ -92,51 +96,62 @@ pub fn validate_extraction_path(entry_path: &Path, base_dir: &Path) -> Result<Pa
Component::Normal(_) | Component::CurDir => {}
}
}
// 2. Build full path
let full_path = base_dir.join(entry_path);
// 3. Canonicalize and validate (ensure within base_dir)
let canonical_base = base_dir.canonicalize()
let canonical_base = base_dir
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot canonicalize base dir: {}", e))?;
// Create parent directories first
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)?;
}
// 4. Verify extraction path is within base_dir
// Note: full_path may not exist yet, so we check parent directory
if full_path.exists() {
let canonical_full = full_path.canonicalize()
let canonical_full = full_path
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot canonicalize full path: {}", e))?;
if !canonical_full.starts_with(&canonical_base) {
return Err(anyhow::anyhow!("Zip Slip detected: path escapes base directory"));
return Err(anyhow::anyhow!(
"Zip Slip detected: path escapes base directory"
));
}
} else {
// Check parent directory instead
if let Some(parent) = full_path.parent() {
let canonical_parent = parent.canonicalize()
let canonical_parent = parent
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot canonicalize parent: {}", e))?;
if !canonical_parent.starts_with(&canonical_base) {
return Err(anyhow::anyhow!("Zip Slip detected: path escapes base directory"));
return Err(anyhow::anyhow!(
"Zip Slip detected: path escapes base directory"
));
}
}
}
Ok(full_path)
}
/// Security Validation - Zip Bomb Protection
pub fn check_decompression_ratio(compressed_size: u64, decompressed_size: u64, max_ratio: u64) -> Result<()> {
pub fn check_decompression_ratio(
compressed_size: u64,
decompressed_size: u64,
max_ratio: u64,
) -> Result<()> {
if compressed_size == 0 {
return Ok(()); // Empty file, allow
return Ok(()); // Empty file, allow
}
let ratio = decompressed_size / compressed_size;
if ratio > max_ratio {
return Err(anyhow::anyhow!(
"Zip Bomb detected: compression ratio {} exceeds limit {}",
@@ -144,7 +159,7 @@ pub fn check_decompression_ratio(compressed_size: u64, decompressed_size: u64, m
max_ratio
));
}
Ok(())
}
@@ -157,7 +172,7 @@ pub fn check_file_size_limit(file_size: u64, max_size: u64) -> Result<()> {
max_size / 1024 / 1024
));
}
Ok(())
}
@@ -165,34 +180,34 @@ pub fn check_file_size_limit(file_size: u64, max_size: u64) -> Result<()> {
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_zip_slip_protection() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
// Safe path: should pass
let safe_path = Path::new("safe/file.txt");
assert!(validate_extraction_path(safe_path, base).is_ok());
// Evil path: should be rejected
let evil_path = Path::new("../../etc/passwd");
assert!(validate_extraction_path(evil_path, base).is_err());
// Absolute path: should be rejected
let abs_path = Path::new("/etc/passwd");
assert!(validate_extraction_path(abs_path, base).is_err());
}
#[test]
fn test_zip_bomb_detection() {
// Normal ratio: should pass
assert!(check_decompression_ratio(1000, 5000, 1000).is_ok());
// Zip Bomb ratio: should be rejected
assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err());
}
#[test]
fn test_compression_ratio_calculation() {
let metadata = ArchiveMetadata {
@@ -206,7 +221,7 @@ mod tests {
created_time: None,
modified_time: None,
};
assert_eq!(metadata.actual_ratio(), 2.0);
}
}
}

View File

@@ -1,16 +1,16 @@
// Core Format Processors - ZIP, TAR, GZIP, TAR.GZ Full Implementation
use crate::archive::{
ArchiveProcessor, ArchiveFormat, ArchiveMetadata, ArchiveEntry, ExtractResult,
processor::{validate_extraction_path, check_decompression_ratio, check_file_size_limit},
};
use crate::archive::config::ArchiveConfig;
use anyhow::{Result, anyhow};
use crate::archive::{
processor::{check_decompression_ratio, check_file_size_limit, validate_extraction_path},
ArchiveEntry, ArchiveFormat, ArchiveMetadata, ArchiveProcessor, ExtractResult,
};
use anyhow::{anyhow, Result};
use log::{debug, info, warn};
use std::fs::{create_dir_all, File};
use std::io::{BufWriter, Read};
use std::path::{Path, PathBuf};
use std::fs::{File, create_dir_all};
use std::io::{Read, Write, BufReader, BufWriter};
use std::time::SystemTime;
use log::{info, warn, debug};
// ==================== ZIP Processor ====================
@@ -21,6 +21,12 @@ pub struct ZipProcessor {
config: ArchiveConfig,
}
impl Default for ZipProcessor {
fn default() -> Self {
Self::new()
}
}
impl ZipProcessor {
pub fn new() -> Self {
Self {
@@ -29,7 +35,7 @@ impl ZipProcessor {
config: ArchiveConfig::default(),
}
}
pub fn with_config(config: ArchiveConfig) -> Self {
Self {
archive: None,
@@ -43,7 +49,7 @@ impl ArchiveProcessor for ZipProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Zip
}
fn new() -> Self {
Self {
archive: None,
@@ -51,64 +57,72 @@ impl ArchiveProcessor for ZipProcessor {
config: ArchiveConfig::default(),
}
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening ZIP archive: {}", path.display());
let file = File::open(path)?;
let archive = zip::ZipArchive::new(file)?;
self.archive = Some(archive);
self.path = path.to_path_buf();
// Extract metadata (need mutable reference for by_index)
let archive_ref = self.archive.as_mut().unwrap();
let total_files = archive_ref.len() as u64;
let mut total_size = 0u64;
let mut compressed_size = 0u64;
for i in 0..archive_ref.len() {
let file = archive_ref.by_index(i)?;
total_size += file.size();
compressed_size += file.compressed_size();
}
let compression_ratio = if compressed_size > 0 {
total_size as f64 / compressed_size as f64
} else {
0.0
};
// Check for Zip Bomb
if compression_ratio > self.config.max_decompression_ratio as f64 {
warn!("Potential Zip Bomb detected: ratio {:.1}:1", compression_ratio);
return Err(anyhow!("Zip Bomb detected: compression ratio {:.1} exceeds limit {}",
compression_ratio, self.config.max_decompression_ratio));
warn!(
"Potential Zip Bomb detected: ratio {:.1}:1",
compression_ratio
);
return Err(anyhow!(
"Zip Bomb detected: compression ratio {:.1} exceeds limit {}",
compression_ratio,
self.config.max_decompression_ratio
));
}
Ok(ArchiveMetadata {
format: ArchiveFormat::Zip,
total_files,
total_size,
compressed_size,
compression_ratio,
is_encrypted: false, // TODO: Check encryption
is_encrypted: false, // TODO: Check encryption
is_multi_volume: false,
created_time: Some(SystemTime::now()),
modified_time: Some(SystemTime::now()),
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
let archive = self.archive.as_mut()
let archive = self
.archive
.as_mut()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let mut entries = Vec::new();
for i in 0..archive.len() {
let file = archive.by_index(i)?;
let entry = ArchiveEntry {
path: PathBuf::from(file.name()),
size: file.size(),
@@ -116,61 +130,64 @@ impl ArchiveProcessor for ZipProcessor {
is_dir: file.name().ends_with('/'),
is_file: !file.name().ends_with('/'),
is_encrypted: false,
modified: SystemTime::UNIX_EPOCH, // TODO: Get actual time
modified: SystemTime::UNIX_EPOCH, // TODO: Get actual time
permissions: Some(0o644),
checksum: None,
};
entries.push(entry);
}
info!("Listed {} entries in ZIP archive", entries.len());
Ok(entries)
}
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
let archive = self.archive.as_mut()
let archive = self
.archive
.as_mut()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let entry_name = entry_path.to_str()
let entry_name = entry_path
.to_str()
.ok_or_else(|| anyhow!("Invalid entry path"))?;
let mut file = archive.by_name(entry_name)?;
// Check file size limit
check_file_size_limit(file.size(), self.config.max_file_size_mb * 1024 * 1024)?;
output.clear();
output.reserve(file.size() as usize);
file.read_to_end(output)?;
info!("Extracted file: {} ({} bytes)", entry_name, output.len());
Ok(output.len() as u64)
}
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
create_dir_all(output_dir)?;
let mut result = ExtractResult::new();
// Open archive if not already open
if self.archive.is_none() {
let file = File::open(&self.path)?;
let archive = zip::ZipArchive::new(file)?;
self.archive = Some(archive);
}
let archive = self.archive.as_mut().unwrap();
result.total_files = archive.len() as u64;
// Use archive iteration to extract files
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let entry_name = file.name().to_string();
let file_size = file.size();
let is_dir = entry_name.ends_with('/');
// Zip Slip protection
match validate_extraction_path(&PathBuf::from(&entry_name), output_dir) {
Ok(safe_path) => {
@@ -181,21 +198,24 @@ impl ArchiveProcessor for ZipProcessor {
result.success_files += 1;
} else {
// File
check_file_size_limit(file_size, self.config.max_file_size_mb * 1024 * 1024)?;
check_file_size_limit(
file_size,
self.config.max_file_size_mb * 1024 * 1024,
)?;
if let Some(parent) = safe_path.parent() {
create_dir_all(parent)?;
}
// Extract file content
let mut outfile = BufWriter::new(File::create(&safe_path)?);
std::io::copy(&mut file, &mut outfile)?;
result.success_files += 1;
result.total_bytes += file_size;
debug!("Extracted: {} ({} bytes)", entry_name, file_size);
}
},
}
Err(e) => {
warn!("Zip Slip detected: {} - {}", entry_name, e);
result.failed_files.push(PathBuf::from(&entry_name));
@@ -203,13 +223,17 @@ impl ArchiveProcessor for ZipProcessor {
}
}
}
info!("Extracted {} files ({} bytes) to {}",
result.success_files, result.total_bytes, output_dir.display());
info!(
"Extracted {} files ({} bytes) to {}",
result.success_files,
result.total_bytes,
output_dir.display()
);
Ok(result)
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Zip
}
@@ -224,6 +248,12 @@ pub struct TarProcessor {
config: ArchiveConfig,
}
impl Default for TarProcessor {
fn default() -> Self {
Self::new()
}
}
impl TarProcessor {
pub fn new() -> Self {
Self {
@@ -232,7 +262,7 @@ impl TarProcessor {
config: ArchiveConfig::default(),
}
}
pub fn with_config(config: ArchiveConfig) -> Self {
Self {
path: PathBuf::new(),
@@ -246,7 +276,7 @@ impl ArchiveProcessor for TarProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Tar
}
fn new() -> Self {
Self {
path: PathBuf::new(),
@@ -254,30 +284,30 @@ impl ArchiveProcessor for TarProcessor {
config: ArchiveConfig::default(),
}
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening TAR archive: {}", path.display());
self.path = path.to_path_buf();
self.entries.clear();
let file = File::open(path)?;
let mut archive = tar::Archive::new(file);
let mut total_size = 0u64;
// Iterate entries to collect metadata
for entry in archive.entries()? {
let entry = entry?;
let path = entry.path()?.to_path_buf();
let size = entry.size();
total_size += size;
self.entries.push(ArchiveEntry {
path,
size,
compressed_size: size, // TAR has no compression
compressed_size: size, // TAR has no compression
is_dir: entry.header().entry_type().is_dir(),
is_file: entry.header().entry_type().is_file(),
is_encrypted: false,
@@ -286,78 +316,87 @@ impl ArchiveProcessor for TarProcessor {
checksum: None,
});
}
let total_files = self.entries.len() as u64;
Ok(ArchiveMetadata {
format: ArchiveFormat::Tar,
total_files,
total_size,
compressed_size: total_size, // TAR has no compression
compression_ratio: 1.0, // No compression
compressed_size: total_size, // TAR has no compression
compression_ratio: 1.0, // No compression
is_encrypted: false,
is_multi_volume: false,
created_time: Some(SystemTime::now()),
modified_time: Some(SystemTime::now()),
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(self.entries.clone())
}
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
// TAR doesn't support random access, need to unpack entire archive
// This is a limitation - for single file extraction, we unpack everything
warn!("TAR format doesn't support random access - extracting entire archive");
let temp_dir = tempfile::tempdir()?;
self.extract_all(temp_dir.path())?;
let file_path = temp_dir.path().join(entry_path);
let mut file = File::open(&file_path)?;
output.clear();
file.read_to_end(output)?;
Ok(output.len() as u64)
}
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
create_dir_all(output_dir)?;
let file = File::open(&self.path)?;
let mut archive = tar::Archive::new(file);
let mut result = ExtractResult::new();
result.total_files = self.entries.len() as u64;
for entry in archive.entries()? {
let mut entry = entry?;
let entry_path = entry.path()?.to_path_buf();
let entry_path_str = entry_path.display().to_string(); // Save for warning
let entry_path_str = entry_path.display().to_string(); // Save for warning
// Zip Slip protection
match validate_extraction_path(&entry_path, output_dir) {
Ok(safe_path) => {
check_file_size_limit(entry.size(), self.config.max_file_size_mb * 1024 * 1024)?;
check_file_size_limit(
entry.size(),
self.config.max_file_size_mb * 1024 * 1024,
)?;
entry.unpack(&safe_path)?;
result.success_files += 1;
result.total_bytes += entry.size();
},
}
Err(e) => {
warn!("Zip Slip detected: {} - {}", entry_path_str, e);
result.failed_files.push(entry_path);
result.warnings.push(format!("Zip Slip: {}", entry_path_str));
result
.warnings
.push(format!("Zip Slip: {}", entry_path_str));
}
}
}
info!("Extracted {} TAR entries to {}", result.success_files, output_dir.display());
info!(
"Extracted {} TAR entries to {}",
result.success_files,
output_dir.display()
);
Ok(result)
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Tar
}
@@ -372,6 +411,12 @@ pub struct GzipProcessor {
config: ArchiveConfig,
}
impl Default for GzipProcessor {
fn default() -> Self {
Self::new()
}
}
impl GzipProcessor {
pub fn new() -> Self {
Self {
@@ -380,7 +425,7 @@ impl GzipProcessor {
config: ArchiveConfig::default(),
}
}
pub fn with_config(config: ArchiveConfig) -> Self {
Self {
path: PathBuf::new(),
@@ -394,7 +439,7 @@ impl ArchiveProcessor for GzipProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Gzip
}
fn new() -> Self {
Self {
path: PathBuf::new(),
@@ -402,27 +447,31 @@ impl ArchiveProcessor for GzipProcessor {
config: ArchiveConfig::default(),
}
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening GZIP archive: {}", path.display());
self.path = path.to_path_buf();
let file = File::open(path)?;
let compressed_size = file.metadata()?.len();
let mut decoder = flate2::read::GzDecoder::new(file);
let mut buffer = Vec::new();
decoder.read_to_end(&mut buffer)?;
self.decompressed_size = buffer.len() as u64;
// Check Zip Bomb
check_decompression_ratio(compressed_size, self.decompressed_size, self.config.max_decompression_ratio)?;
check_decompression_ratio(
compressed_size,
self.decompressed_size,
self.config.max_decompression_ratio,
)?;
Ok(ArchiveMetadata {
format: ArchiveFormat::Gzip,
total_files: 1, // GZIP is single file
total_files: 1, // GZIP is single file
total_size: self.decompressed_size,
compressed_size,
compression_ratio: if compressed_size > 0 {
@@ -436,58 +485,64 @@ impl ArchiveProcessor for GzipProcessor {
modified_time: Some(SystemTime::now()),
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// GZIP is single file - infer name from archive name
let name = self.path.file_name()
let name = self
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.replace(".gz", "")
.replace(".gzip", "");
Ok(vec![ArchiveEntry::file(
PathBuf::from(name),
self.decompressed_size,
0, // GZIP doesn't preserve compressed size per file
0, // GZIP doesn't preserve compressed size per file
)])
}
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
fn extract_file(&mut self, _entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
// GZIP is single file - just decompress it
let file = File::open(&self.path)?;
let mut decoder = flate2::read::GzDecoder::new(file);
output.clear();
decoder.read_to_end(output)?;
check_file_size_limit(output.len() as u64, self.config.max_file_size_mb * 1024 * 1024)?;
check_file_size_limit(
output.len() as u64,
self.config.max_file_size_mb * 1024 * 1024,
)?;
info!("Decompressed GZIP file: {} bytes", output.len());
Ok(output.len() as u64)
}
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
create_dir_all(output_dir)?;
let entries = self.list_entries()?;
let entry = entries.first()
let entry = entries
.first()
.ok_or_else(|| anyhow!("No entry in GZIP archive"))?;
let outpath = output_dir.join(&entry.path);
// Zip Slip protection
validate_extraction_path(&entry.path, output_dir)?;
if let Some(parent) = outpath.parent() {
create_dir_all(parent)?;
}
let file = File::open(&self.path)?;
let mut decoder = flate2::read::GzDecoder::new(file);
let mut outfile = BufWriter::new(File::create(&outpath)?);
std::io::copy(&mut decoder, &mut outfile)?;
let result = ExtractResult {
total_files: 1,
total_bytes: self.decompressed_size,
@@ -496,11 +551,11 @@ impl ArchiveProcessor for GzipProcessor {
skipped_files: Vec::new(),
warnings: Vec::new(),
};
info!("Decompressed GZIP to: {}", outpath.display());
Ok(result)
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Gzip
}
@@ -514,6 +569,12 @@ pub struct TarGzipProcessor {
config: ArchiveConfig,
}
impl Default for TarGzipProcessor {
fn default() -> Self {
Self::new()
}
}
impl TarGzipProcessor {
pub fn new() -> Self {
Self {
@@ -521,7 +582,7 @@ impl TarGzipProcessor {
config: ArchiveConfig::default(),
}
}
pub fn with_config(config: ArchiveConfig) -> Self {
Self {
gzip_processor: GzipProcessor::with_config(config.clone()),
@@ -534,32 +595,33 @@ impl ArchiveProcessor for TarGzipProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::TarGzip
}
fn new() -> Self {
Self {
gzip_processor: GzipProcessor::new(),
config: ArchiveConfig::default(),
}
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
info!("Opening TAR.GZ archive: {}", path.display());
// Step 1: Decompress GZIP
let temp_dir = tempfile::tempdir()?;
self.gzip_processor.open(path)?;
self.gzip_processor.extract_all(temp_dir.path())?;
// Step 2: Open TAR
let tar_entries = self.gzip_processor.list_entries()?;
let tar_file = tar_entries.first()
let tar_file = tar_entries
.first()
.ok_or_else(|| anyhow!("No TAR file in GZIP"))?;
let tar_path = temp_dir.path().join(&tar_file.path);
let mut tar_processor = TarProcessor::with_config(self.config.clone());
let tar_metadata = tar_processor.open(&tar_path)?;
Ok(ArchiveMetadata {
format: ArchiveFormat::TarGzip,
total_files: tar_metadata.total_files,
@@ -576,46 +638,47 @@ impl ArchiveProcessor for TarGzipProcessor {
modified_time: Some(SystemTime::now()),
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// Need to implement properly - this requires decompressing first
warn!("TAR.GZ list_entries requires full decompression - consider extract_all instead");
Ok(Vec::new())
}
fn extract_file(&mut self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
warn!("TAR.GZ extract_file requires full unpacking - inefficient for single file");
let temp_dir = tempfile::tempdir()?;
self.extract_all(temp_dir.path())?;
let file_path = temp_dir.path().join(entry_path);
let mut file = File::open(&file_path)?;
output.clear();
file.read_to_end(output)?;
Ok(output.len() as u64)
}
fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractResult> {
info!("Extracting TAR.GZ to: {}", output_dir.display());
// Step 1: Decompress GZIP to temp
let temp_dir = tempfile::tempdir()?;
self.gzip_processor.extract_all(temp_dir.path())?;
// Step 2: Extract TAR
let tar_entries = self.gzip_processor.list_entries()?;
let tar_file = tar_entries.first()
let tar_file = tar_entries
.first()
.ok_or_else(|| anyhow!("No TAR file found"))?;
let tar_path = temp_dir.path().join(&tar_file.path);
let mut tar_processor = TarProcessor::with_config(self.config.clone());
tar_processor.open(&tar_path)?;
tar_processor.extract_all(output_dir)
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::TarGzip
}
@@ -627,73 +690,133 @@ impl ArchiveProcessor for TarGzipProcessor {
pub struct ZstdProcessor;
impl ArchiveProcessor for ZstdProcessor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::Zstd }
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Zstd
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("ZSTD processor not yet implemented"))
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) }
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) }
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::Zstd }
fn new() -> Self { Self }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(Vec::new())
}
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Zstd
}
fn new() -> Self {
Self
}
}
/// BZIP2 Processor Stub (Phase 2/3)
pub struct Bzip2Processor;
impl ArchiveProcessor for Bzip2Processor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::Bzip2 }
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Bzip2
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("BZIP2 processor not yet implemented"))
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) }
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) }
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::Bzip2 }
fn new() -> Self { Self }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(Vec::new())
}
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Bzip2
}
fn new() -> Self {
Self
}
}
/// LZ4 Processor Stub (Phase 2/3)
pub struct Lz4Processor;
impl ArchiveProcessor for Lz4Processor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::Lz4 }
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Lz4
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("LZ4 processor not yet implemented"))
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) }
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) }
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::Lz4 }
fn new() -> Self { Self }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(Vec::new())
}
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Lz4
}
fn new() -> Self {
Self
}
}
/// TAR.BZ2 Composite Processor Stub (Phase 2/3)
pub struct TarBzip2Processor;
impl ArchiveProcessor for TarBzip2Processor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::TarBzip2 }
fn format(&self) -> ArchiveFormat {
ArchiveFormat::TarBzip2
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("TAR.BZ2 processor not yet implemented"))
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) }
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) }
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::TarBzip2 }
fn new() -> Self { Self }
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(Vec::new())
}
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::TarBzip2
}
fn new() -> Self {
Self
}
}
/// TAR.ZST Composite Processor Stub (Phase 2/3)
pub struct TarZstdProcessor;
impl ArchiveProcessor for TarZstdProcessor {
fn format(&self) -> ArchiveFormat { ArchiveFormat::TarZstd }
fn format(&self) -> ArchiveFormat {
ArchiveFormat::TarZstd
}
fn open(&mut self, _path: &Path) -> Result<ArchiveMetadata> {
Err(anyhow!("TAR.ZST processor not yet implemented"))
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> { Ok(Vec::new()) }
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> { Ok(0) }
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> { Ok(ExtractResult::new()) }
fn can_process(format: ArchiveFormat) -> bool { format == ArchiveFormat::TarZstd }
fn new() -> Self { Self }
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
Ok(Vec::new())
}
fn extract_file(&mut self, _entry: &Path, _output: &mut Vec<u8>) -> Result<u64> {
Ok(0)
}
fn extract_all(&mut self, _dir: &Path) -> Result<ExtractResult> {
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::TarZstd
}
fn new() -> Self {
Self
}
}

View File

@@ -1,13 +1,15 @@
// Optional Format Processors - RAR, XZ, 7z
// All optional formats have warnings displayed when enabled
use crate::archive::{ArchiveFormat, ArchiveProcessor, ArchiveMetadata, ArchiveEntry, ExtractResult};
use crate::archive::processor::{check_decompression_ratio, validate_extraction_path};
use crate::archive::warning;
use crate::archive::processor::{validate_extraction_path, check_decompression_ratio};
use anyhow::{Result, anyhow};
use std::path::Path;
use crate::archive::{
ArchiveEntry, ArchiveFormat, ArchiveMetadata, ArchiveProcessor, ExtractResult,
};
use anyhow::{anyhow, Result};
use log::{info, warn};
use std::fs;
use log::{warn, info};
use std::path::Path;
/// RAR Processor - Only Decompression
/// ⚠️ Legal Warning: RARLAB patent, commercial use requires license
@@ -28,54 +30,65 @@ impl ArchiveProcessor for RarProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Rar
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
// Show legal warning when RAR is used
warning::show_rar_legal_warning();
self.archive_path = Some(path.to_path_buf());
// Use unrar library to open RAR
// Note: unrar only supports decompression, no compression
use unrar::Archive;
let archive = Archive::new(path)?;
let entries: Vec<_> = archive.list()?.collect();
let total_files = entries.len() as u64;
let total_size = entries.iter()
let total_size = entries
.iter()
.filter_map(|e| e.ok())
.map(|e| e.uncompressed_size)
.sum();
let compressed_size = fs::metadata(path)?.len();
Ok(ArchiveMetadata {
format: ArchiveFormat::Rar,
total_files,
total_size,
compressed_size,
compression_ratio: if compressed_size > 0 { total_size as f64 / compressed_size as f64 } else { 0.0 },
is_encrypted: entries.iter().any(|e| e.ok().map_or(false, |e| e.is_encrypted())),
is_multi_volume: false, // unrar library limitation
compression_ratio: if compressed_size > 0 {
total_size as f64 / compressed_size as f64
} else {
0.0
},
is_encrypted: entries
.iter()
.any(|e| e.ok().map_or(false, |e| e.is_encrypted())),
is_multi_volume: false, // unrar library limitation
created_time: None,
modified_time: None,
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
use unrar::Archive;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let archive = Archive::new(path)?;
let entries: Vec<ArchiveEntry> = archive.list()?
let entries: Vec<ArchiveEntry> = archive
.list()?
.filter_map(|e| e.ok())
.map(|e| ArchiveEntry {
path: PathBuf::from(e.filename),
size: e.uncompressed_size,
compressed_size: 0, // unrar doesn't provide this
compressed_size: 0, // unrar doesn't provide this
is_dir: e.is_directory(),
is_file: !e.is_directory(),
is_encrypted: e.is_encrypted(),
@@ -83,45 +96,49 @@ impl ArchiveProcessor for RarProcessor {
permissions: None,
})
.collect();
Ok(entries)
}
fn extract_file(&self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
// RAR doesn't support random access efficiently
// Need to extract entire archive
warn!("RAR extract_file requires full extraction (no random access)");
let entries = self.list_entries()?;
let entry = entries.iter()
let entry = entries
.iter()
.find(|e| e.path == entry_path)
.ok_or_else(|| anyhow!("Entry not found: {}", entry_path.display()))?;
// Extract to temp dir, then read
let temp_dir = tempfile::tempdir()?;
self.extract_all(temp_dir.path())?;
let extracted_file = temp_dir.path().join(entry_path);
let content = fs::read(&extracted_file)?;
output.extend_from_slice(&content);
Ok(content.len() as u64)
}
fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> {
use unrar::Archive;
use unrar::ExtractOption;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
// Validate output_dir path
validate_extraction_path(output_dir, output_dir)?;
let mut result = ExtractResult::new();
result.total_files = self.list_entries()?.len() as u64;
let archive = Archive::new(path)?;
for entry_result in archive.extract_all(output_dir, ExtractOption::Recurse)? {
match entry_result {
Ok(entry) => {
@@ -135,10 +152,10 @@ impl ArchiveProcessor for RarProcessor {
}
}
}
Ok(result)
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Rar
}
@@ -163,57 +180,65 @@ impl ArchiveProcessor for XzProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::Xz
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
// Check if liblzma is available
if !check_liblzma_available() {
warning::show_xz_dependency_warning();
return Err(anyhow!("liblzma library not found, XZ format disabled"));
}
self.archive_path = Some(path.to_path_buf());
use xz2::read::XzDecoder;
use std::io::Read;
use xz2::read::XzDecoder;
let file = fs::File::open(path)?;
let mut decoder = XzDecoder::new(file);
// Read decompressed size (estimate)
let mut buffer = Vec::new();
decoder.read_to_end(&mut buffer)?;
let decompressed_size = buffer.len() as u64;
let compressed_size = fs::metadata(path)?.len();
// Check decompression ratio
check_decompression_ratio(compressed_size, decompressed_size, 1000)?;
Ok(ArchiveMetadata {
format: ArchiveFormat::Xz,
total_files: 1, // XZ is single-file format
total_files: 1, // XZ is single-file format
total_size: decompressed_size,
compressed_size,
compression_ratio: if compressed_size > 0 { decompressed_size as f64 / compressed_size as f64 } else { 0.0 },
compression_ratio: if compressed_size > 0 {
decompressed_size as f64 / compressed_size as f64
} else {
0.0
},
is_encrypted: false,
is_multi_volume: false,
created_time: None,
modified_time: None,
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// XZ is single-file, infer filename from archive name
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
let filename = path.file_name()
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let filename = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".xz").unwrap_or(s))
.unwrap_or("output");
Ok(vec![ArchiveEntry {
path: PathBuf::from(filename),
size: 0, // Will be determined during extraction
size: 0, // Will be determined during extraction
compressed_size: 0,
is_dir: false,
is_file: true,
@@ -222,48 +247,54 @@ impl ArchiveProcessor for XzProcessor {
permissions: None,
}])
}
fn extract_file(&self, _entry_path: &Path, output: &mut Vec<u8>) -> Result<u64> {
use xz2::read::XzDecoder;
use std::io::Read;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
use xz2::read::XzDecoder;
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
let file = fs::File::open(path)?;
let mut decoder = XzDecoder::new(file);
decoder.read_to_end(output)?;
Ok(output.len() as u64)
}
fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> {
use xz2::read::XzDecoder;
use std::io::Read;
let path = self.archive_path.as_ref().ok_or_else(|| anyhow!("Archive not opened"))?;
use xz2::read::XzDecoder;
let path = self
.archive_path
.as_ref()
.ok_or_else(|| anyhow!("Archive not opened"))?;
// Infer output filename
let entries = self.list_entries()?;
let output_path = output_dir.join(&entries[0].path);
// Validate path
validate_extraction_path(&entries[0].path, output_dir)?;
let file = fs::File::open(path)?;
let mut decoder = XzDecoder::new(file);
let mut output_file = fs::File::create(&output_path)?;
std::io::copy(&mut decoder, &mut output_file)?;
let mut result = ExtractResult::new();
result.success_files = 1;
result.total_files = 1;
result.total_bytes = fs::metadata(&output_path)?.len();
Ok(result)
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::Xz && check_liblzma_available()
}
@@ -286,59 +317,61 @@ impl ArchiveProcessor for SevenZProcessor {
fn format(&self) -> ArchiveFormat {
ArchiveFormat::SevenZ
}
fn open(&mut self, path: &Path) -> Result<ArchiveMetadata> {
// Show stability warning
warning::show_7z_stability_warning();
use sevenz_rust::SevenZReader;
let reader = SevenZReader::new(path)?;
let entries = reader.entries()?;
let total_files = entries.len() as u64;
let total_size = entries.iter()
.map(|e| e.uncompressed_size as u64)
.sum();
let total_size = entries.iter().map(|e| e.uncompressed_size as u64).sum();
let compressed_size = fs::metadata(path)?.len();
Ok(ArchiveMetadata {
format: ArchiveFormat::SevenZ,
total_files,
total_size,
compressed_size,
compression_ratio: if compressed_size > 0 { total_size as f64 / compressed_size as f64 } else { 0.0 },
compression_ratio: if compressed_size > 0 {
total_size as f64 / compressed_size as f64
} else {
0.0
},
is_encrypted: entries.iter().any(|e| e.is_encrypted),
is_multi_volume: false,
created_time: None,
modified_time: None,
})
}
fn list_entries(&mut self) -> Result<Vec<ArchiveEntry>> {
// Note: sevenz-rust doesn't have full entry listing yet
// This is a stub returning empty list
warn!("7z list_entries not fully implemented (library limitation)");
Ok(Vec::new())
}
fn extract_file(&self, _entry_path: &Path, _output: &mut Vec<u8>) -> Result<u64> {
warn!("7z extract_file not implemented (library limitation)");
Err(anyhow!("7z library doesn't support random access"))
}
fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult> {
use sevenz_rust::SevenZReader;
// Note: sevenz-rust doesn't have full extraction yet
// This is a stub
warn!("7z extract_all limited (library under development)");
Ok(ExtractResult::new())
}
fn can_process(format: ArchiveFormat) -> bool {
format == ArchiveFormat::SevenZ
}
@@ -369,15 +402,21 @@ pub struct SevenZProcessor;
#[cfg(not(feature = "optional-formats"))]
impl RarProcessor {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
#[cfg(not(feature = "optional-formats"))]
impl XzProcessor {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
#[cfg(not(feature = "optional-formats"))]
impl SevenZProcessor {
pub fn new() -> Self { Self }
}
pub fn new() -> Self {
Self
}
}

View File

@@ -1,31 +1,31 @@
use crate::archive::{
ArchiveProcessor, ArchiveFormat, ArchiveMetadata, ArchiveEntry, ExtractResult,
processors::core::{ZipProcessor, TarProcessor, GzipProcessor, TarGzipProcessor},
processor::{validate_extraction_path, check_decompression_ratio},
config::ArchiveConfig,
processor::{check_decompression_ratio, validate_extraction_path},
processors::core::{GzipProcessor, TarGzipProcessor, TarProcessor, ZipProcessor},
ArchiveEntry, ArchiveFormat, ArchiveMetadata, ArchiveProcessor, ExtractResult,
};
use tempfile::TempDir;
use std::fs::{File, create_dir_all};
use anyhow::Result;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use tempfile::TempDir;
#[cfg(test)]
mod helpers {
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
pub fn create_test_zip(path: &PathBuf, files: Vec<(&str, &[u8])>) {
use std::io::Cursor;
let mut buffer = Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buffer);
let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
for (name, content) in files {
if name.ends_with('/') {
zip.add_directory(name, options).unwrap();
@@ -34,31 +34,31 @@ mod helpers {
zip.write_all(content).unwrap();
}
}
zip.finish().unwrap();
}
let zip_data = buffer.into_inner();
File::create(path).unwrap().write_all(&zip_data).unwrap();
}
pub fn create_test_tar(path: &PathBuf, files: Vec<(&str, &[u8])>) {
let file = File::create(path).unwrap();
let mut builder = tar::Builder::new(file);
for (name, content) in files {
let mut header = tar::Header::new_gnu();
header.set_size(content.len() as u64);
header.set_path(name);
header.set_mode(0o644);
header.set_cksum();
builder.append_data(&mut header, name, content).unwrap();
}
builder.finish().unwrap();
}
pub fn create_test_gzip(path: &PathBuf, content: &[u8]) {
let file = File::create(path).unwrap();
let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
@@ -69,74 +69,74 @@ mod helpers {
#[cfg(test)]
mod core_format_tests {
use super::*;
use super::helpers::*;
use super::*;
#[test]
fn test_zip_processor_basic() {
let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip");
create_test_zip(&zip_path, vec![("file1.txt", b"hello")]);
let mut processor = ZipProcessor::new();
let metadata = processor.open(&zip_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Zip);
assert_eq!(metadata.total_files, 1);
}
#[test]
fn test_tar_processor_basic() {
let temp_dir = TempDir::new().unwrap();
let tar_path = temp_dir.path().join("test.tar");
create_test_tar(&tar_path, vec![("file1.txt", b"tar content")]);
let mut processor = TarProcessor::new();
let metadata = processor.open(&tar_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Tar);
}
#[test]
fn test_gzip_processor_basic() {
let temp_dir = TempDir::new().unwrap();
let gz_path = temp_dir.path().join("test.gz");
create_test_gzip(&gz_path, b"gzip content here");
let mut processor = GzipProcessor::new();
let metadata = processor.open(&gz_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Gzip);
assert_eq!(metadata.total_files, 1);
}
#[test]
fn test_validate_extraction_path_safe() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let safe_path = PathBuf::from("safe/file.txt");
let result = validate_extraction_path(&safe_path, base);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.starts_with(base));
}
#[test]
fn test_validate_extraction_path_zip_slip() {
let base = PathBuf::from("/tmp/extract");
let evil_path = PathBuf::from("../../etc/passwd");
let result = validate_extraction_path(&evil_path, &base);
assert!(result.is_err());
}
#[test]
fn test_check_decompression_ratio_ok() {
assert!(check_decompression_ratio(1000, 5000, 1000).is_ok());
}
#[test]
fn test_check_decompression_ratio_zip_bomb() {
assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err());
@@ -145,39 +145,39 @@ mod core_format_tests {
#[cfg(test)]
mod integration_tests {
use super::*;
use super::helpers::*;
use super::*;
use crate::archive::detector::FormatDetector;
use crate::archive::ProcessorRegistry;
#[test]
fn test_format_detection_automation() {
let temp_dir = TempDir::new().unwrap();
let detector = FormatDetector::new();
let zip_path = temp_dir.path().join("test.zip");
create_test_zip(&zip_path, vec![("f.txt", b"z")]);
assert_eq!(detector.detect(&zip_path).unwrap(), ArchiveFormat::Zip);
let tar_path = temp_dir.path().join("test.tar");
create_test_tar(&tar_path, vec![("f.txt", b"t")]);
assert_eq!(detector.detect(&tar_path).unwrap(), ArchiveFormat::Tar);
let gz_path = temp_dir.path().join("test.gz");
create_test_gzip(&gz_path, b"g");
assert_eq!(detector.detect(&gz_path).unwrap(), ArchiveFormat::Gzip);
}
#[test]
fn test_processor_registry_integration() {
let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config);
registry.initialize().unwrap();
let formats = registry.enabled_formats();
assert!(formats.contains(&ArchiveFormat::Zip));
assert!(formats.contains(&ArchiveFormat::Tar));
assert!(formats.contains(&ArchiveFormat::Gzip));
assert!(formats.contains(&ArchiveFormat::TarGzip));
}
}
}

View File

@@ -4,48 +4,46 @@ use std::fs;
use std::io::Read;
use tempfile::TempDir;
use crate::archive::*;
use crate::archive::processor::check_decompression_ratio;
use crate::archive::tests::test_helpers::*;
use crate::archive::*;
#[test]
fn test_zip_processor_full_workflow() {
let temp_dir = TempDir::new().unwrap();
let zip_path = create_test_zip(&temp_dir);
// Initialize processor
let mut processor = processors::core::ZipProcessor::new();
// Test open
let metadata = processor.open(&zip_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Zip);
assert_eq!(metadata.total_files, 3);
// Test list_entries
let entries = processor.list_entries().unwrap();
assert_eq!(entries.len(), 3);
// Verify entry names
let names: Vec<&str> = entries.iter()
.map(|e| e.path.to_str().unwrap())
.collect();
let names: Vec<&str> = entries.iter().map(|e| e.path.to_str().unwrap()).collect();
assert!(names.contains(&"file1.txt"));
assert!(names.contains(&"file2.txt"));
assert!(names.contains(&"subdir/file3.txt"));
// Test extract_all
let extract_dir = temp_dir.path().join("extracted");
fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap();
assert_eq!(result.success_files, 3);
assert_eq!(result.failed_files.len(), 0);
// Verify extracted files
assert!(extract_dir.join("file1.txt").exists());
assert!(extract_dir.join("file2.txt").exists());
assert!(extract_dir.join("subdir/file3.txt").exists());
// Verify content
let content1 = fs::read_to_string(extract_dir.join("file1.txt")).unwrap();
assert_eq!(content1, "content of file 1");
@@ -55,24 +53,24 @@ fn test_zip_processor_full_workflow() {
fn test_tar_processor_full_workflow() {
let temp_dir = TempDir::new().unwrap();
let tar_path = create_test_tar(&temp_dir);
let mut processor = processors::core::TarProcessor::new();
// Test open
let metadata = processor.open(&tar_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Tar);
// Test list_entries
let entries = processor.list_entries().unwrap();
assert!(entries.len() >= 3); // TAR may include directory entries
assert!(entries.len() >= 3); // TAR may include directory entries
// Test extract_all
let extract_dir = temp_dir.path().join("extracted_tar");
fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap();
assert!(result.success_files >= 3);
// Verify extracted files exist
assert!(extract_dir.join("file1.txt").exists());
assert!(extract_dir.join("file2.txt").exists());
@@ -82,25 +80,25 @@ fn test_tar_processor_full_workflow() {
fn test_gzip_processor_full_workflow() {
let temp_dir = TempDir::new().unwrap();
let gz_path = create_test_gzip(&temp_dir);
let mut processor = processors::core::GzipProcessor::new();
// Test open
let metadata = processor.open(&gz_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::Gzip);
assert_eq!(metadata.total_files, 1); // GZIP is single file
assert_eq!(metadata.total_files, 1); // GZIP is single file
// Test extract_all
let extract_dir = temp_dir.path().join("extracted_gz");
fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap();
assert_eq!(result.success_files, 1);
// Verify extracted file (should strip .gz extension)
let extracted_file = extract_dir.join("test.txt");
assert!(extracted_file.exists());
// Verify content
let content = fs::read_to_string(&extracted_file).unwrap();
assert_eq!(content, "test gzip content for validation");
@@ -110,20 +108,20 @@ fn test_gzip_processor_full_workflow() {
fn test_tar_gz_processor_workflow() {
let temp_dir = TempDir::new().unwrap();
let tar_gz_path = create_test_tar_gz(&temp_dir);
let mut processor = processors::core::TarGzipProcessor::new();
// Test open
let metadata = processor.open(&tar_gz_path).unwrap();
assert_eq!(metadata.format, ArchiveFormat::TarGzip);
// Test extract_all
let extract_dir = temp_dir.path().join("extracted_tar_gz");
fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir).unwrap();
assert!(result.success_files >= 2);
// Verify extracted TAR files
assert!(extract_dir.join("file1.txt").exists());
assert!(extract_dir.join("file2.txt").exists());
@@ -132,18 +130,18 @@ fn test_tar_gz_processor_workflow() {
#[test]
fn test_format_detection_auto() {
let temp_dir = TempDir::new().unwrap();
// Test ZIP detection
let zip_path = create_test_zip(&temp_dir);
let detector = FormatDetector::new();
let format = detector.detect(&zip_path).unwrap();
assert_eq!(format, ArchiveFormat::Zip);
// Test TAR detection
let tar_path = create_test_tar(&temp_dir);
let format = detector.detect(&tar_path).unwrap();
assert_eq!(format, ArchiveFormat::Tar);
// Test GZIP detection
let gz_path = create_test_gzip(&temp_dir);
let format = detector.detect(&gz_path).unwrap();
@@ -155,12 +153,12 @@ fn test_processor_registry_core_formats() {
let config = ArchiveConfig::default();
let mut registry = ProcessorRegistry::new(config);
registry.initialize().unwrap();
let formats = registry.enabled_formats();
// Should have 9 core formats
assert!(formats.len() >= 4); // At least the ones we implemented
assert!(formats.len() >= 4); // At least the ones we implemented
// Verify format support
assert!(formats.contains(&ArchiveFormat::Zip));
assert!(formats.contains(&ArchiveFormat::Tar));
@@ -172,20 +170,20 @@ fn test_processor_registry_core_formats() {
fn test_zip_slip_protection() {
let temp_dir = TempDir::new().unwrap();
let zip_bomb_data = create_zip_slip_test();
// Write malicious ZIP to file
let evil_zip_path = temp_dir.path().join("evil.zip");
fs::write(&evil_zip_path, &zip_bomb_data).unwrap();
let mut processor = processors::core::ZipProcessor::new();
processor.open(&evil_zip_path).unwrap();
// Attempt extraction should fail due to Zip Slip protection
let extract_dir = temp_dir.path().join("should_fail");
fs::create_dir_all(&extract_dir).unwrap();
let result = processor.extract_all(&extract_dir);
// Should either fail or have empty extracted files
// (validate_extraction_path prevents malicious paths)
if result.is_ok() {
@@ -199,11 +197,11 @@ fn test_zip_slip_protection() {
fn test_zip_bomb_detection() {
// Test decompression ratio check
let result = check_decompression_ratio(42_000, 5_000_000_000, 1000);
assert!(result.is_err()); // Should detect as Zip Bomb
assert!(result.is_err()); // Should detect as Zip Bomb
// Test normal ratio
let result = check_decompression_ratio(1000, 5000, 1000);
assert!(result.is_ok()); // Normal ratio should pass
assert!(result.is_ok()); // Normal ratio should pass
}
#[test]
@@ -219,21 +217,21 @@ fn test_metadata_compression_ratio() {
created_time: None,
modified_time: None,
};
assert_eq!(metadata.actual_ratio(), 5.0); // 5000/1000 = 5.0
assert!(!metadata.check_zip_bomb(10)); // ratio 5.0 < 10, not a bomb
assert!(metadata.check_zip_bomb(4)); // ratio 5.0 > 4, detected as bomb
assert_eq!(metadata.actual_ratio(), 5.0); // 5000/1000 = 5.0
assert!(!metadata.check_zip_bomb(10)); // ratio 5.0 < 10, not a bomb
assert!(metadata.check_zip_bomb(4)); // ratio 5.0 > 4, detected as bomb
}
#[test]
fn test_config_validation() {
let config = ArchiveConfig {
max_decompression_ratio: 5, // Too low
max_decompression_ratio: 5, // Too low
..Default::default()
};
assert!(config.validate().is_err());
let valid_config = ArchiveConfig::default();
assert!(valid_config.validate().is_ok());
}
}

View File

@@ -7,10 +7,10 @@ pub mod test_helpers;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_module_structure() {
// Test that all test modules exist
assert!(true);
}
}
}

View File

@@ -1,28 +1,27 @@
use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use tempfile::TempDir;
use zip::{ZipWriter, write::FileOptions, CompressionMethod};
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::Builder;
use tempfile::TempDir;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
pub fn create_test_zip(temp_dir: &TempDir) -> PathBuf {
let zip_path = temp_dir.path().join("test.zip");
let file = File::create(&zip_path).unwrap();
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Stored);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("file1.txt", options).unwrap();
zip.write_all(b"content of file 1").unwrap();
zip.start_file("file2.txt", options).unwrap();
zip.write_all(b"content of file 2").unwrap();
zip.start_file("subdir/file3.txt", options).unwrap();
zip.write_all(b"content of file 3 in subdir").unwrap();
zip.finish().unwrap();
zip_path
}
@@ -31,28 +30,38 @@ pub fn create_test_tar(temp_dir: &TempDir) -> PathBuf {
let tar_path = temp_dir.path().join("test.tar");
let file = File::create(&tar_path).unwrap();
let mut builder = Builder::new(file);
let mut header1 = tar::Header::new_gnu();
header1.set_path("file1.txt").unwrap();
header1.set_size(17);
header1.set_mode(0o644);
header1.set_cksum();
builder.append_data(&mut header1, "file1.txt", &b"content of file 1"[..]).unwrap();
builder
.append_data(&mut header1, "file1.txt", &b"content of file 1"[..])
.unwrap();
let mut header2 = tar::Header::new_gnu();
header2.set_path("file2.txt").unwrap();
header2.set_size(17);
header2.set_mode(0o644);
header2.set_cksum();
builder.append_data(&mut header2, "file2.txt", &b"content of file 2"[..]).unwrap();
builder
.append_data(&mut header2, "file2.txt", &b"content of file 2"[..])
.unwrap();
let mut header3 = tar::Header::new_gnu();
header3.set_path("subdir/file3.txt").unwrap();
header3.set_size(27);
header3.set_mode(0o644);
header3.set_cksum();
builder.append_data(&mut header3, "subdir/file3.txt", &b"content of file 3 in subdir"[..]).unwrap();
builder
.append_data(
&mut header3,
"subdir/file3.txt",
&b"content of file 3 in subdir"[..],
)
.unwrap();
builder.finish().unwrap();
tar_path
}
@@ -61,7 +70,9 @@ pub fn create_test_gzip(temp_dir: &TempDir) -> PathBuf {
let gz_path = temp_dir.path().join("test.txt.gz");
let file = File::create(&gz_path).unwrap();
let mut encoder = GzEncoder::new(file, Compression::default());
encoder.write_all(b"test gzip content for validation").unwrap();
encoder
.write_all(b"test gzip content for validation")
.unwrap();
encoder.finish().unwrap();
gz_path
}
@@ -70,33 +81,37 @@ pub fn create_test_tar_gz(temp_dir: &TempDir) -> PathBuf {
let tar_path = temp_dir.path().join("test.tar");
let tar_file = File::create(&tar_path).unwrap();
let mut builder = Builder::new(tar_file);
let mut header1 = tar::Header::new_gnu();
header1.set_path("file1.txt").unwrap();
header1.set_size(10);
header1.set_mode(0o644);
header1.set_cksum();
builder.append_data(&mut header1, "file1.txt", &b"file1 data"[..]).unwrap();
builder
.append_data(&mut header1, "file1.txt", &b"file1 data"[..])
.unwrap();
let mut header2 = tar::Header::new_gnu();
header2.set_path("file2.txt").unwrap();
header2.set_size(10);
header2.set_mode(0o644);
header2.set_cksum();
builder.append_data(&mut header2, "file2.txt", &b"file2 data"[..]).unwrap();
builder
.append_data(&mut header2, "file2.txt", &b"file2 data"[..])
.unwrap();
builder.finish().unwrap();
let tar_gz_path = temp_dir.path().join("test.tar.gz");
let gz_file = File::create(&tar_gz_path).unwrap();
let mut encoder = GzEncoder::new(gz_file, Compression::default());
let tar_content = std::fs::read(&tar_path).unwrap();
encoder.write_all(&tar_content).unwrap();
encoder.finish().unwrap();
std::fs::remove_file(&tar_path).unwrap();
tar_gz_path
}
@@ -105,13 +120,12 @@ pub fn create_zip_bomb_test() -> Vec<u8> {
{
let writer = std::io::Cursor::new(&mut buffer);
let mut zip = ZipWriter::new(writer);
let options = FileOptions::default()
.compression_method(CompressionMethod::Stored);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
zip.start_file("bomb.txt", options).unwrap();
zip.write_all(&[0u8; 100]).unwrap();
zip.finish().unwrap();
}
buffer
@@ -123,11 +137,11 @@ pub fn create_zip_slip_test() -> Vec<u8> {
let writer = std::io::Cursor::new(&mut buffer);
let mut zip = ZipWriter::new(writer);
let options = FileOptions::default();
zip.start_file("../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd", options).unwrap();
zip.write_all(b"malicious content").unwrap();
zip.finish().unwrap();
}
buffer
}
}

View File

@@ -1,6 +1,6 @@
// Warning System - Legal and Technical Warnings for Optional Formats
use log::{warn, info};
use log::{info, warn};
use crate::archive::config::ArchiveConfig;
@@ -63,25 +63,27 @@ pub fn show_startup_warnings(config: &ArchiveConfig) {
if config.enable_rar {
show_rar_legal_warning();
}
if config.enable_xz {
// Dependency check happens in ProcessorRegistry
}
if config.enable_7z {
show_7z_stability_warning();
}
// Show summary of enabled formats
let enabled_optional = [
config.enable_rar,
config.enable_xz,
config.enable_7z,
].iter().filter(|&x| *x).count();
let enabled_optional = [config.enable_rar, config.enable_xz, config.enable_7z]
.iter()
.filter(|&x| *x)
.count();
if enabled_optional > 0 {
info!("");
info!("⚠️ {} optional format(s) enabled with warnings shown above", enabled_optional);
info!(
"⚠️ {} optional format(s) enabled with warnings shown above",
enabled_optional
);
info!("Core formats (9): ZIP, TAR, GZIP, ZSTD, BZIP2, LZ4, TAR.GZ, TAR.BZ2, TAR.ZST");
info!("");
}
@@ -89,8 +91,7 @@ pub fn show_startup_warnings(config: &ArchiveConfig) {
/// Generate user-facing legal disclaimer text
pub fn generate_rar_legal_disclaimer() -> String {
format!(
"RAR FORMAT LEGAL DISCLAIMER
"RAR FORMAT LEGAL DISCLAIMER
IMPORTANT WARNING:
@@ -136,6 +137,5 @@ CONTACT:
Last Updated: 2026-06-10
Version: 1.0
Legal Consultation: [Please consult professional lawyer for commercial use]
"
)
}
".to_string()
}