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

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

/// TAR / TAR.GZ / TAR.BZ2 / TAR.XZ 引擎
pub struct TarEngine;

impl TarEngine {
    /// 列出 tar 归档内容
    pub fn list(path: &str, format: ArchiveFormat) -> Result<ArchiveInfo, String> {
        let file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;

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

        match format {
            ArchiveFormat::TarGz => {
                let gz = flate2::read::GzDecoder::new(file);
                let mut archive = tar::Archive::new(gz);
                list_tar_entries(&mut archive, &mut entries, &mut total_size, &mut total_compressed_size)?;
            }
            ArchiveFormat::TarBz2 => {
                let bz2 = bzip2::read::BzDecoder::new(file);
                let mut archive = tar::Archive::new(bz2);
                list_tar_entries(&mut archive, &mut entries, &mut total_size, &mut total_compressed_size)?;
            }
            ArchiveFormat::TarXz => {
                let xz = xz2::read::XzDecoder::new(file);
                let mut archive = tar::Archive::new(xz);
                list_tar_entries(&mut archive, &mut entries, &mut total_size, &mut total_compressed_size)?;
            }
            ArchiveFormat::Tar => {
                let mut archive = tar::Archive::new(file);
                list_tar_entries(&mut archive, &mut entries, &mut total_size, &mut total_compressed_size)?;
            }
            _ => return Err("不支持的格式".to_string()),
        }

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

    /// 解压 tar 归档
    pub fn extract(path: &str, format: ArchiveFormat, 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 file = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
        let task_id = uuid::Uuid::new_v4().to_string();

        match format {
            ArchiveFormat::TarGz => {
                let gz = flate2::read::GzDecoder::new(file);
                let mut archive = tar::Archive::new(gz);
                extract_tar_entries(&mut archive, output_dir, &task_id, &options.selected_entries, progress_cb)?;
            }
            ArchiveFormat::TarBz2 => {
                let bz2 = bzip2::read::BzDecoder::new(file);
                let mut archive = tar::Archive::new(bz2);
                extract_tar_entries(&mut archive, output_dir, &task_id, &options.selected_entries, progress_cb)?;
            }
            ArchiveFormat::TarXz => {
                let xz = xz2::read::XzDecoder::new(file);
                let mut archive = tar::Archive::new(xz);
                extract_tar_entries(&mut archive, output_dir, &task_id, &options.selected_entries, progress_cb)?;
            }
            ArchiveFormat::Tar => {
                let mut archive = tar::Archive::new(file);
                extract_tar_entries(&mut archive, output_dir, &task_id, &options.selected_entries, progress_cb)?;
            }
            _ => return Err("不支持的格式".to_string()),
        }

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

    /// 压缩文件为 tar.gz
    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 task_id = uuid::Uuid::new_v4().to_string();
        let file = File::create(&options.output_path).map_err(|e| format!("创建文件失败: {}", e))?;

        // 先收集所有文件条目(用于计算总数)
        let mut all_entries: Vec<(std::path::PathBuf, String, bool)> = 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_tar_entries(path, &dir_name, &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;

        match options.format {
            ArchiveFormat::TarGz => {
                let gz = flate2::write::GzEncoder::new(file, flate2::Compression::default());
                let mut archive = tar::Builder::new(gz);
                add_entries_to_tar(&mut archive, &all_entries, &task_id, total, progress_cb)?;
                let gz = archive.into_inner().map_err(|e| format!("完成压缩失败: {}", e))?;
                gz.finish().map_err(|e| format!("完成压缩失败: {}", e))?;
            }
            ArchiveFormat::TarBz2 => {
                let bz2 = bzip2::write::BzEncoder::new(file, bzip2::Compression::default());
                let mut archive = tar::Builder::new(bz2);
                add_entries_to_tar(&mut archive, &all_entries, &task_id, total, progress_cb)?;
                let bz2 = archive.into_inner().map_err(|e| format!("完成压缩失败: {}", e))?;
                bz2.finish().map_err(|e| format!("完成压缩失败: {}", e))?;
            }
            ArchiveFormat::TarXz => {
                let xz = xz2::write::XzEncoder::new(file, 6);
                let mut archive = tar::Builder::new(xz);
                add_entries_to_tar(&mut archive, &all_entries, &task_id, total, progress_cb)?;
                let xz = archive.into_inner().map_err(|e| format!("完成压缩失败: {}", e))?;
                xz.finish().map_err(|e| format!("完成压缩失败: {}", e))?;
            }
            ArchiveFormat::Tar => {
                let mut archive = tar::Builder::new(file);
                add_entries_to_tar(&mut archive, &all_entries, &task_id, total, progress_cb)?;
                archive.finish().map_err(|e| format!("完成压缩失败: {}", e))?;
            }
            _ => return Err("不支持的格式".to_string()),
        }

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

fn list_tar_entries<R: std::io::Read>(
    archive: &mut tar::Archive<R>,
    entries: &mut Vec<ArchiveEntry>,
    total_size: &mut u64,
    total_compressed_size: &mut u64,
) -> Result<(), String> {
    for entry_result in archive.entries().map_err(|e| format!("读取条目失败: {}", e))? {
        let entry = entry_result.map_err(|e| format!("读取条目失败: {}", e))?;
        let path = entry.path().map_err(|e| format!("读取路径失败: {}", e))?;
        let name = path.to_string_lossy().to_string();
        let size = entry.size();
        let is_dir = entry.header().entry_type().is_dir();

        *total_size += size;
        *total_compressed_size += size; // tar 没有单独的压缩大小

        entries.push(ArchiveEntry {
            name: name.clone(),
            path: name,
            size,
            compressed_size: size,
            is_dir,
            modified: entry.header().mtime().ok().map(|t| t.to_string()),
            is_encrypted: false,
        });
    }
    Ok(())
}

/// 逐文件解压 TAR 并推送进度
fn extract_tar_entries<R: std::io::Read>(
    archive: &mut tar::Archive<R>,
    output_dir: &Path,
    task_id: &str,
    selected_entries: &Option<Vec<String>>,
    progress_cb: &dyn Fn(ProgressInfo),
) -> Result<(), String> {
    let entries: Vec<tar::Entry<'_, R>> = archive.entries().map_err(|e| format!("读取条目失败: {}", e))?.collect::<Result<Vec<_>, _>>().map_err(|e| format!("读取条目失败: {}", e))?;

    // 计算实际要处理的条目数
    let total = if let Some(ref selected) = selected_entries {
        entries.iter().filter(|e| {
            let name = e.path().map(|p| p.to_string_lossy().to_string()).unwrap_or_default();
            selected.contains(&name)
        }).count() as u64
    } else {
        entries.len() as u64
    };

    let mut processed: u64 = 0;

    for mut entry in entries.into_iter() {
        let name = entry.path().map_err(|e| format!("读取路径失败: {}", e))?.to_string_lossy().to_string();

        // 过滤选定条目
        if let Some(ref selected) = selected_entries {
            if !selected.contains(&name) {
                continue;
            }
        }

        // 路径安全检查
        let out_path = ensure_path_within_dir(output_dir, &name)?;

        if entry.header().entry_type().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 out_file = std::fs::File::create(&out_path).map_err(|e| format!("创建文件失败: {}", e))?;
            std::io::copy(&mut entry, &mut out_file).map_err(|e| format!("写入文件失败: {}", e))?;
        }

        processed += 1;
        progress_cb(ProgressInfo {
            task_id: task_id.to_string(),
            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(())
}

/// 递归收集目录下所有条目
fn collect_tar_entries(
    dir: &Path,
    prefix: &str,
    result: &mut Vec<(std::path::PathBuf, String, bool)>,
) -> Result<(), String> {
    result.push((dir.to_path_buf(), prefix.to_string(), true));

    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_tar_entries(&path, &archive_path, result)?;
        } else {
            result.push((path, archive_path, false));
        }
    }
    Ok(())
}

/// 逐文件添加到 TAR 并推送进度
fn add_entries_to_tar<W: std::io::Write>(
    archive: &mut tar::Builder<W>,
    entries: &[(std::path::PathBuf, String, bool)],
    task_id: &str,
    total: u64,
    progress_cb: &dyn Fn(ProgressInfo),
) -> Result<(), String> {
    for (i, (path, name, is_dir)) in entries.iter().enumerate() {
        if *is_dir {
            archive.append_dir(name, path)
                .map_err(|e| format!("添加目录失败: {}", e))?;
        } else {
            let mut f = File::open(path).map_err(|e| format!("打开文件失败: {}", e))?;
            archive.append_file(name, &mut f)
                .map_err(|e| format!("添加文件失败: {}", e))?;
        }

        progress_cb(ProgressInfo {
            task_id: task_id.to_string(),
            current_file: name.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,
        });
    }
    Ok(())
}