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