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::{self, File};
use std::path::Path;

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

/// 7z 引擎
pub struct SevenZEngine;

impl SevenZEngine {
    /// 列出 7z 归档内容
    pub fn list(path: &str) -> Result<ArchiveInfo, String> {
        let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
        let len = file.metadata().map_err(|e| format!("读取元数据失败: {}", e))?.len();

        let archive = sevenz_rust::SevenZReader::new(file, len, sevenz_rust::Password::empty())
            .map_err(|e| format!("读取7z失败: {}", e))?;

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

        for entry in files {
            let name = entry.name().to_string();
            let is_dir = entry.is_directory();
            let size = entry.size();
            let compressed_size = entry.compressed_size;

            total_size += size;
            total_compressed_size += compressed_size;

            // sevenz-rust 没有直接的 is_encrypted 字段
            // 加密条目通常 has_stream() 为 false 且 size > 0
            // 注意:此推断不完全可靠,如果误判,前端有密码重试机制兜底
            let is_encrypted = !entry.has_stream() && size > 0;
            if is_encrypted {
                has_password = true;
            }

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

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

    /// 解压 7z 归档
    pub fn extract(path: &str, options: &ExtractOptions, progress_cb: &dyn Fn(ProgressInfo)) -> Result<TaskResult, String> {
        let output_dir = Path::new(&options.output_dir);
        fs::create_dir_all(output_dir).map_err(|e| format!("创建目录失败: {}", e))?;

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

        // 先获取文件总数用于进度计算
        let file_count = {
            let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
            let len = file.metadata().map_err(|e| format!("读取元数据失败: {}", e))?.len();
            let archive = sevenz_rust::SevenZReader::new(file, len, sevenz_rust::Password::empty())
                .map_err(|e| format!("读取7z失败: {}", e))?;
            let all_files: Vec<_> = archive.archive().files.iter().filter(|f| !f.is_directory()).collect();
            if let Some(ref selected) = options.selected_entries {
                all_files.into_iter().filter(|f| selected.contains(&f.name().to_string())).count()
            } else {
                all_files.len()
            }
        };

        // 构造密码
        let pwd = match options.password {
            Some(ref password) => sevenz_rust::Password::from(password.as_str()),
            None => sevenz_rust::Password::empty(),
        };

        let mut processed: usize = 0;
        let selected = &options.selected_entries;
        sevenz_rust::SevenZReader::new(
            File::open(path).map_err(|e| format!("打开文件失败: {}", e))?,
            std::fs::metadata(path).map_err(|e| format!("读取元数据失败: {}", e))?.len(),
            pwd,
        ).map_err(|e| format!("读取7z失败: {}", e))?
        .for_each_entries(|entry, reader| {
            let name = entry.name().to_string();
            if !entry.is_directory() {
                // 过滤选定条目
                if let Some(ref selected_list) = selected {
                    if !selected_list.contains(&name) {
                        return Ok(true); // 跳过
                    }
                }

                let out_path = match ensure_path_within_dir(output_dir, &name) {
                    Ok(p) => p,
                    Err(e) => return Err(sevenz_rust::Error::other(e)),
                };
                if let Some(parent) = out_path.parent() {
                    fs::create_dir_all(parent)?;
                }
                let mut out_file = File::create(&out_path)?;
                std::io::copy(reader, &mut out_file)?;
                processed += 1;
                progress_cb(ProgressInfo {
                    task_id: task_id.clone(),
                    current_file: name,
                    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,
                });
            }
            Ok(true) // continue
        }).map_err(|e| format!("解压7z失败: {}", e))?;

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

    /// 压缩文件为 7z
    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() {
            fs::create_dir_all(parent).map_err(|e| format!("创建输出目录失败: {}", e))?;
        }

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

        let mut writer = sevenz_rust::SevenZWriter::create(&options.output_path)
            .map_err(|e| format!("创建7z文件失败: {}", e))?;

        // 如果指定了密码,设置AES256加密
        if let Some(ref password) = options.password {
            let pwd = sevenz_rust::Password::from(password.as_str());
            writer.set_content_methods(vec![
                sevenz_rust::AesEncoderOptions::new(pwd).into(),
                sevenz_rust::SevenZMethod::LZMA2.into(),
            ]);
        }

        // 先收集所有文件条目(用于计算总数)
        let mut all_entries: Vec<(std::path::PathBuf, String, bool)> = Vec::new(); // (path, name, is_dir)
        for file_path in files {
            let path = Path::new(file_path);
            if !path.exists() {
                continue;
            }
            if path.is_dir() {
                collect_sevenz_entries(path, path, &mut all_entries)?;
            } else {
                let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
                all_entries.push((path.to_path_buf(), name, false));
            }
        }

        let total = all_entries.len() as u64;

        // 入队阶段占 0~50%,finish() 占 50~100%
        for (i, (path, name, is_dir)) in all_entries.iter().enumerate() {
            if *is_dir {
                let entry = sevenz_rust::SevenZArchiveEntry::from_path(path, name.clone());
                writer.push_archive_entry(entry, None::<File>)
                    .map_err(|e| format!("添加目录失败: {}", e))?;
            } else {
                let entry = sevenz_rust::SevenZArchiveEntry::from_path(path, name.clone());
                let f = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
                writer.push_archive_entry(entry, Some(f))
                    .map_err(|e| format!("添加文件失败: {}", e))?;
            }

            progress_cb(ProgressInfo {
                task_id: task_id.clone(),
                current_file: format!("入队: {}", name),
                processed_bytes: i as u64 + 1,
                total_bytes: total,
                percent: (i as f64 + 1.0) / total as f64 * 50.0, // 0~50%
                speed: 0.0,
                elapsed_secs: 0.0,
                remaining_secs: 0.0,
            });
        }

        progress_cb(ProgressInfo {
            task_id: task_id.clone(),
            current_file: "正在压缩...".to_string(),
            processed_bytes: total,
            total_bytes: total,
            percent: 50.0,
            speed: 0.0,
            elapsed_secs: 0.0,
            remaining_secs: 0.0,
        });

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

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

/// 递归收集目录下所有条目
fn collect_sevenz_entries(
    base: &Path,
    dir: &Path,
    result: &mut Vec<(std::path::PathBuf, String, bool)>,
) -> Result<(), String> {
    let entries = 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 rel = path.strip_prefix(base).unwrap_or(&path);
        let name = rel.to_string_lossy().to_string();

        if path.is_dir() {
            result.push((path.clone(), name, true));
            collect_sevenz_entries(base, &path, result)?;
        } else {
            result.push((path, name, false));
        }
    }
    Ok(())
}