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::path::Path;

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

/// RAR 引擎:解压使用 unrar-ng,压缩检测系统 WinRAR
pub struct RarEngine;

impl RarEngine {
    /// 列出 RAR 归档内容
    pub fn list(path: &str) -> Result<ArchiveInfo, String> {
        let archive = unrar_ng::Archive::new(path);

        let open_archive = archive
            .open_for_listing()
            .map_err(|e| format!("打开RAR失败: {}", e))?;

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

        for entry_result in open_archive {
            let entry = entry_result.map_err(|e| format!("读取条目失败: {}", e))?;

            let is_dir = entry.is_directory();
            let size = entry.unpacked_size;
            // unrar-ng 不提供 packed_size,用 0 占位
            let compressed_size = 0u64;
            let is_encrypted = entry.is_encrypted();

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

            let filename = entry.filename.to_string_lossy().to_string();

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

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

    /// 解压 RAR 归档(逐文件推送进度,支持选定条目过滤)
    pub fn extract(path: &str, options: &ExtractOptions, progress_cb: &dyn Fn(ProgressInfo)) -> Result<TaskResult, String> {
        let output_dir = Path::new(&options.output_dir);
        std::fs::create_dir_all(output_dir).map_err(|e| format!("创建目录失败: {}", e))?;

        let task_id = uuid::Uuid::new_v4().to_string();

        // 先获取文件总数
        let file_count = {
            let list_archive = unrar_ng::Archive::new(path);
            let entries: Vec<_> = list_archive.open_for_listing()
                .map_err(|e| format!("列出条目失败: {}", e))?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| format!("读取条目失败: {}", e))?;
            let all_files: Vec<_> = entries.into_iter().filter(|e| !e.is_directory()).collect();
            if let Some(ref selected) = options.selected_entries {
                all_files.into_iter().filter(|e| selected.contains(&e.filename.to_string_lossy().to_string())).count()
            } else {
                all_files.len()
            }
        };

        let archive = if let Some(ref password) = options.password {
            unrar_ng::Archive::with_password(path, password.as_str())
        } else {
            unrar_ng::Archive::new(path)
        };

        let mut open_archive = archive.open_for_processing()
            .map_err(|e| format!("打开RAR失败: {}", e))?;

        let mut processed: usize = 0;

        loop {
            let next = open_archive.read_header()
                .map_err(|e| format!("读取头部失败: {}", e))?;

            match next {
                Some(before_file) => {
                    let filename = before_file.entry().filename.to_string_lossy().to_string();
                    let is_dir = before_file.entry().is_directory();

                    if is_dir {
                        open_archive = before_file.skip().map_err(|e| format!("跳过目录失败: {}", e))?;
                        continue;
                    }

                    // 过滤选定条目
                    if let Some(ref selected) = options.selected_entries {
                        if !selected.contains(&filename) {
                            open_archive = before_file.skip().map_err(|e| format!("跳过条目失败: {}", e))?;
                            continue;
                        }
                    }

                    open_archive = before_file.extract_with_base(&options.output_dir)
                        .map_err(|e| format!("解压 {} 失败: {}", filename, e))?;

                    processed += 1;
                    progress_cb(ProgressInfo {
                        task_id: task_id.clone(),
                        current_file: filename,
                        processed_bytes: processed as u64,
                        total_bytes: file_count as u64,
                        percent: processed as f64 / file_count.max(1) as f64 * 100.0,
                        speed: 0.0,
                        elapsed_secs: 0.0,
                        remaining_secs: 0.0,
                    });
                }
                None => break, // 归档结束
            }
        }

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

    /// 检测系统是否安装了 WinRAR / rar.exe
    pub fn find_winrar() -> RarSupport {
        // 优先使用命令行版 rar.exe,避免 WinRAR.exe 弹出 GUI
        let common_paths = [
            r"C:\Program Files\WinRAR\rar.exe",
            r"C:\Program Files (x86)\WinRAR\rar.exe",
            r"C:\Program Files\WinRAR\WinRAR.exe",
            r"C:\Program Files (x86)\WinRAR\WinRAR.exe",
        ];

        let winrar_path = common_paths.iter().find(|p| Path::new(p).exists());

        RarSupport {
            can_decompress: true, // unrar-ng 始终可用
            can_compress: winrar_path.is_some(),
            winrar_path: winrar_path.map(|p| p.to_string()),
        }
    }

    /// 使用系统 WinRAR 创建 RAR 归档
    pub fn compress_with_winrar(
        files: &[String],
        options: &CompressOptions,
        winrar_path: &str,
        progress_cb: &dyn Fn(ProgressInfo),
    ) -> Result<TaskResult, String> {
        let task_id = uuid::Uuid::new_v4().to_string();

        let mut cmd = std::process::Command::new(winrar_path);
        cmd.arg("a"); // add

        // 压缩级别
        let level = match options.compression_level {
            CompressionLevel::Store => "-m0",
            CompressionLevel::Fastest => "-m1",
            CompressionLevel::Fast => "-m2",
            CompressionLevel::Normal => "-m3",
            CompressionLevel::Good => "-m4",
            CompressionLevel::Best => "-m5",
        };
        cmd.arg(level);

        // 密码
        if let Some(ref password) = options.password {
            cmd.arg(format!("-p{}", password));
        }

        // 分卷
        if let Some(volume_size) = options.split_volume {
            let size_str = if volume_size >= 1_073_741_824 {
                format!("{}g", volume_size / 1_073_741_824)
            } else if volume_size >= 1_048_576 {
                format!("{}m", volume_size / 1_048_576)
            } else if volume_size >= 1024 {
                format!("{}k", volume_size / 1024)
            } else {
                format!("{}b", volume_size)
            };
            cmd.arg(format!("-v{}", size_str));
        }

        cmd.arg(&options.output_path);

        for file in files {
            cmd.arg(file);
        }

        // 先推送开始状态
        progress_cb(ProgressInfo {
            task_id: task_id.clone(),
            current_file: "正在压缩...".to_string(),
            processed_bytes: 0,
            total_bytes: 1,
            percent: 0.0,
            speed: 0.0,
            elapsed_secs: 0.0,
            remaining_secs: 0.0,
        });

        let output = cmd
            .output()
            .map_err(|e| format!("执行WinRAR失败: {}", e))?;

        progress_cb(ProgressInfo {
            task_id: task_id.clone(),
            current_file: "完成".to_string(),
            processed_bytes: 1,
            total_bytes: 1,
            percent: 100.0,
            speed: 0.0,
            elapsed_secs: 0.0,
            remaining_secs: 0.0,
        });

        if output.status.success() {
            Ok(TaskResult {
                task_id,
                success: true,
                message: "压缩完成".to_string(),
                output_path: Some(options.output_path.clone()),
            })
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr);
            Err(format!("WinRAR 压缩失败: {}", stderr))
        }
    }
}