win-rar 0.1.0

A Windows archive manager supporting ZIP, 7z, RAR, TAR with encryption, shell integration, and drag-and-drop
// Copyright (c) 北京锋通科技有限公司 — 郭玉峰、吴琼
// SPDX-License-Identifier: MIT

use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;

use zip::unstable::write::FileOptionsExt;

use crate::archive::types::*;
use crate::archive::types::ensure_path_within_dir;

/// ZIP 引擎:列出、解压、压缩
pub struct ZipEngine;

impl ZipEngine {
    /// 列出 ZIP 归档内容
    pub fn list(path: &str) -> Result<ArchiveInfo, String> {
        let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
        let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取ZIP失败: {}", e))?;

        let mut entries = Vec::new();
        let mut total_size = 0u64;
        let mut total_compressed_size = 0u64;
        let mut has_password = false;

        for i in 0..archive.len() {
            let entry = archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?;
            let name = entry.name().to_string();
            let is_dir = entry.is_dir();
            let size = entry.size();
            let compressed_size = entry.compressed_size();
            let is_encrypted = entry.encrypted();

            if is_encrypted {
                has_password = true;
            }
            total_size += size;
            total_compressed_size += compressed_size;

            entries.push(ArchiveEntry {
                name: name.clone(),
                path: name,
                size,
                compressed_size,
                is_dir,
                modified: None,
                is_encrypted,
            });
        }

        Ok(ArchiveInfo {
            format: ArchiveFormat::Zip,
            path: path.to_string(),
            file_count: entries.iter().filter(|e| !e.is_dir).count(),
            entries,
            total_size,
            total_compressed_size,
            has_password,
        })
    }

    /// 解压 ZIP 归档
    pub fn extract(path: &str, options: &ExtractOptions, progress_cb: &dyn Fn(ProgressInfo)) -> Result<TaskResult, String> {
        let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
        let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("读取ZIP失败: {}", e))?;

        let output_dir = Path::new(&options.output_dir);
        std::fs::create_dir_all(output_dir).map_err(|e| format!("创建目录失败: {}", e))?;

        let total = if let Some(ref selected) = options.selected_entries {
            // 只计算选定条目的数量
            let mut count = 0u64;
            for i in 0..archive.len() {
                let entry = archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?;
                if selected.contains(&entry.name().to_string()) {
                    count += 1;
                }
            }
            count
        } else {
            archive.len() as u64
        };
        let task_id = uuid::Uuid::new_v4().to_string();

        let mut processed: u64 = 0;

        for i in 0..archive.len() {
            let name = {
                // 先获取文件名(不需要密码)
                let entry = archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?;
                entry.name().to_string()
            };

            // 如果指定了选定条目,跳过不在列表中的
            if let Some(ref selected) = options.selected_entries {
                if !selected.contains(&name) {
                    continue;
                }
            }

            // 如果有密码,用密码解密
            let mut entry = if let Some(ref password) = options.password {
                archive.by_index_decrypt(i, password.as_bytes())
                    .map_err(|e| format!("解密失败(密码可能不正确): {}", e))?
            } else {
                archive.by_index(i).map_err(|e| format!("读取条目失败: {}", e))?
            };

            let out_path = ensure_path_within_dir(output_dir, &name)?;

            if entry.is_dir() {
                std::fs::create_dir_all(&out_path).map_err(|e| format!("创建目录失败: {}", e))?;
            } else {
                if let Some(parent) = out_path.parent() {
                    std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
                }

                let mut outfile = File::create(&out_path).map_err(|e| format!("创建文件失败: {}", e))?;
                std::io::copy(&mut entry, &mut outfile).map_err(|e| format!("写入文件失败: {}", e))?;
            }

            processed += 1;
            progress_cb(ProgressInfo {
                task_id: task_id.clone(),
                current_file: name,
                processed_bytes: processed,
                total_bytes: total,
                percent: processed as f64 / total.max(1) as f64 * 100.0,
                speed: 0.0,
                elapsed_secs: 0.0,
                remaining_secs: 0.0,
            });
        }

        Ok(TaskResult {
            task_id,
            success: true,
            message: "解压完成".to_string(),
            output_path: Some(options.output_dir.clone()),
        })
    }

    /// 压缩文件为 ZIP
    pub fn compress(files: &[String], options: &CompressOptions, progress_cb: &dyn Fn(ProgressInfo)) -> Result<TaskResult, String> {
        // 确保输出路径的父目录存在
        if let Some(parent) = Path::new(&options.output_path).parent() {
            std::fs::create_dir_all(parent).map_err(|e| format!("创建输出目录失败: {}", e))?;
        }

        let file = File::create(&options.output_path).map_err(|e| format!("创建文件失败: {}", e))?;

        let compression_method = match options.compression_level {
            CompressionLevel::Store => zip::CompressionMethod::Stored,
            _ => zip::CompressionMethod::Deflated,
        };

        let mut zip = zip::ZipWriter::new(file);
        let task_id = uuid::Uuid::new_v4().to_string();

        // 收集所有要压缩的文件(递归展开目录)
        let mut all_files: Vec<(String, String)> = Vec::new(); // (文件系统路径, 归档内路径)
        for file_path in files {
            let path = Path::new(file_path);
            if !path.exists() {
                continue;
            }
            if path.is_dir() {
                let dir_name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
                collect_dir_files(path, &dir_name, &mut all_files)?;
            } else {
                let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
                all_files.push((file_path.clone(), name));
            }
        }

        let total = all_files.len() as u64;

        for (i, (fs_path, archive_path)) in all_files.iter().enumerate() {
            let path = Path::new(fs_path);

            let mut file_options = zip::write::SimpleFileOptions::default()
                .compression_method(compression_method);

            // 如果指定了密码,启用加密
            if let Some(ref password) = options.password {
                file_options = file_options
                    .with_deprecated_encryption(password.as_bytes());
            }

            if path.is_dir() {
                zip.add_directory(&format!("{}/", archive_path), file_options)
                    .map_err(|e| format!("添加目录失败: {}", e))?;
            } else {
                let mut f = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
                let mut data = Vec::new();
                f.read_to_end(&mut data).map_err(|e| format!("读取文件失败: {}", e))?;

                zip.start_file(archive_path, file_options)
                    .map_err(|e| format!("写入条目失败: {}", e))?;
                zip.write_all(&data).map_err(|e| format!("写入数据失败: {}", e))?;
            }

            progress_cb(ProgressInfo {
                task_id: task_id.clone(),
                current_file: archive_path.clone(),
                processed_bytes: i as u64 + 1,
                total_bytes: total,
                percent: (i as f64 + 1.0) / total as f64 * 100.0,
                speed: 0.0,
                elapsed_secs: 0.0,
                remaining_secs: 0.0,
            });
        }

        zip.finish().map_err(|e| format!("完成压缩失败: {}", e))?;

        Ok(TaskResult {
            task_id,
            success: true,
            message: "压缩完成".to_string(),
            output_path: Some(options.output_path.clone()),
        })
    }
}

/// 递归收集目录下的所有文件和子目录
fn collect_dir_files(dir: &Path, prefix: &str, result: &mut Vec<(String, String)>) -> Result<(), String> {
    // 先添加目录本身
    result.push((dir.to_string_lossy().to_string(), prefix.to_string()));

    let entries = std::fs::read_dir(dir).map_err(|e| format!("读取目录失败: {}", e))?;
    for entry in entries {
        let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?;
        let path = entry.path();
        let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
        let archive_path = format!("{}/{}", prefix, name);

        if path.is_dir() {
            collect_dir_files(&path, &archive_path, result)?;
        } else {
            result.push((path.to_string_lossy().to_string(), archive_path));
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_folder_compress_recursive() {
        // 创建临时目录结构
        let temp_dir = std::env::temp_dir().join("winrar_test_folder");
        let sub_dir = temp_dir.join("subdir");
        let _ = std::fs::remove_dir_all(&temp_dir);
        std::fs::create_dir_all(&sub_dir).unwrap();

        // 创建文件
        std::fs::write(temp_dir.join("file1.txt"), b"Hello World 1").unwrap();
        std::fs::write(sub_dir.join("file2.txt"), b"Hello World 2").unwrap();
        std::fs::write(sub_dir.join("file3.txt"), b"Hello World 3").unwrap();

        // 压缩
        let output_path = std::env::temp_dir().join("winrar_test_folder.zip");
        let options = CompressOptions {
            format: ArchiveFormat::Zip,
            output_path: output_path.to_string_lossy().to_string(),
            password: None,
            compression_level: CompressionLevel::Normal,
            split_volume: None,
        };

        let result = ZipEngine::compress(
            &[temp_dir.to_string_lossy().to_string()],
            &options,
            &|_| {},
        );

        assert!(result.is_ok(), "压缩失败: {:?}", result);

        // 列出归档内容
        let info = ZipEngine::list(&output_path.to_string_lossy().to_string()).unwrap();

        println!("归档条目 (共 {} 个):", info.entries.len());
        for entry in &info.entries {
            println!("  {} (is_dir={}, size={})", entry.path, entry.is_dir, entry.size);
        }

        // 验证文件存在
        let paths: Vec<&str> = info.entries.iter().map(|e| e.path.as_str()).collect();
        assert!(paths.iter().any(|p| p.contains("file1.txt")), "file1.txt 不在归档中! paths: {:?}", paths);
        assert!(paths.iter().any(|p| p.contains("file2.txt")), "file2.txt 不在归档中! paths: {:?}", paths);
        assert!(paths.iter().any(|p| p.contains("file3.txt")), "file3.txt 不在归档中! paths: {:?}", paths);
        assert!(info.file_count >= 3, "应该至少有3个文件,实际有 {}", info.file_count);

        // 清理
        let _ = std::fs::remove_dir_all(&temp_dir);
        let _ = std::fs::remove_file(&output_path);
    }
}