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 serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

/// 检查解压目标路径是否在输出目录内(防止 Zip Slip 路径遍历攻击)
pub fn ensure_path_within_dir(output_dir: &Path, entry_name: &str) -> Result<PathBuf, String> {
    let target = output_dir.join(entry_name);
    let canonical_dir = output_dir.canonicalize().unwrap_or_else(|_| output_dir.to_path_buf());
    // 对目标路径的父目录做 canonicalize(文件可能还不存在)
    if let Some(parent) = target.parent() {
        if parent.exists() {
            if let Ok(canonical_target) = parent.canonicalize() {
                if !canonical_target.starts_with(&canonical_dir) {
                    return Err(format!("路径遍历攻击检测: \"{}\" 试图写到输出目录之外", entry_name));
                }
            }
        }
    }
    // 额外检查:路径中不应包含 ".."
    if entry_name.contains("..") {
        return Err(format!("路径遍历攻击检测: \"{}\" 包含非法路径组件", entry_name));
    }
    Ok(target)
}

/// 压缩格式枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ArchiveFormat {
    Zip,
    Rar,
    SevenZ,
    Tar,
    TarGz,
    TarBz2,
    TarXz,
    Gz,
    Bz2,
    Xz,
    Unknown,
}

impl ArchiveFormat {
    /// 根据文件扩展名推断格式
    pub fn from_path(path: &str) -> Self {
        let lower = path.to_lowercase();
        if lower.ends_with(".zip") {
            Self::Zip
        } else if lower.ends_with(".rar") {
            Self::Rar
        } else if lower.ends_with(".7z") {
            Self::SevenZ
        } else if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") {
            Self::TarGz
        } else if lower.ends_with(".tar.bz2") || lower.ends_with(".tbz2") {
            Self::TarBz2
        } else if lower.ends_with(".tar.xz") || lower.ends_with(".txz") {
            Self::TarXz
        } else if lower.ends_with(".tar") {
            Self::Tar
        } else if lower.ends_with(".gz") {
            Self::Gz
        } else if lower.ends_with(".bz2") {
            Self::Bz2
        } else if lower.ends_with(".xz") {
            Self::Xz
        } else {
            Self::Unknown
        }
    }

    #[allow(dead_code)]
    pub fn name(&self) -> &str {
        match self {
            Self::Zip => "ZIP",
            Self::Rar => "RAR",
            Self::SevenZ => "7z",
            Self::Tar => "TAR",
            Self::TarGz => "TAR.GZ",
            Self::TarBz2 => "TAR.BZ2",
            Self::TarXz => "TAR.XZ",
            Self::Gz => "GZ",
            Self::Bz2 => "BZ2",
            Self::Xz => "XZ",
            Self::Unknown => "未知",
        }
    }

    /// 是否支持压缩(创建新归档)
    #[allow(dead_code)]
    pub fn can_compress(&self) -> bool {
        matches!(
            self,
            Self::Zip | Self::SevenZ | Self::Tar | Self::TarGz | Self::TarBz2 | Self::TarXz
        )
    }

    /// 是否支持解压
    #[allow(dead_code)]
    pub fn can_decompress(&self) -> bool {
        !matches!(self, Self::Unknown)
    }
}

/// 归档内文件条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveEntry {
    pub name: String,
    pub path: String,
    pub size: u64,
    pub compressed_size: u64,
    pub is_dir: bool,
    pub modified: Option<String>,
    pub is_encrypted: bool,
}

/// 归档信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveInfo {
    pub format: ArchiveFormat,
    pub path: String,
    pub entries: Vec<ArchiveEntry>,
    pub total_size: u64,
    pub total_compressed_size: u64,
    pub file_count: usize,
    pub has_password: bool,
}

/// 压缩选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressOptions {
    pub format: ArchiveFormat,
    pub output_path: String,
    pub password: Option<String>,
    pub compression_level: CompressionLevel,
    pub split_volume: Option<u64>, // 分卷大小(字节),None 表示不分卷
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionLevel {
    Store,
    Fastest,
    Fast,
    Normal,
    Good,
    Best,
}

impl Default for CompressionLevel {
    fn default() -> Self {
        Self::Normal
    }
}

/// 解压选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractOptions {
    pub output_dir: String,
    pub password: Option<String>,
    pub selected_entries: Option<Vec<String>>, // None 表示解压全部
    pub overwrite: bool,
}

/// 操作进度
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgressInfo {
    pub task_id: String,
    pub current_file: String,
    pub processed_bytes: u64,
    pub total_bytes: u64,
    pub percent: f64,
    pub speed: f64,        // 字节/秒
    pub elapsed_secs: f64,
    pub remaining_secs: f64,
}

/// 操作结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskResult {
    pub task_id: String,
    pub success: bool,
    pub message: String,
    pub output_path: Option<String>,
}

/// RAR 支持状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RarSupport {
    pub can_decompress: bool,
    pub can_compress: bool,
    pub winrar_path: Option<String>,
}