aether-azathoth 0.5.3

A lightweight, embeddable domain-specific language (DSL) interpreter with rich standard library
Documentation
//! 路径安全验证器
//!
//! 提供统一的路径安全检查,防止路径遍历攻击(`..`)和越权访问。

use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};

/// 路径验证错误
#[derive(Debug, Clone, PartialEq)]
pub enum PathValidationError {
    /// 路径超出根目录限制
    OutsideRoot { path: PathBuf, root: PathBuf },
    /// 绝对路径被禁止
    AbsolutePathNotAllowed(PathBuf),
    /// 父目录遍历 `..` 被禁止
    ParentTraversalNotAllowed(PathBuf),
    /// 文件扩展名不在白名单中
    ExtensionNotAllowed { path: PathBuf, extension: String },
    /// 路径解析失败
    InvalidPath(PathBuf),
}

impl std::fmt::Display for PathValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PathValidationError::OutsideRoot { path, root } => {
                write!(
                    f,
                    "Path '{}' is outside allowed root '{}'",
                    path.display(),
                    root.display()
                )
            }
            PathValidationError::AbsolutePathNotAllowed(path) => {
                write!(f, "Absolute path '{}' not allowed", path.display())
            }
            PathValidationError::ParentTraversalNotAllowed(path) => {
                write!(
                    f,
                    "Parent traversal '..' not allowed in path '{}'",
                    path.display()
                )
            }
            PathValidationError::ExtensionNotAllowed { path, extension } => {
                write!(
                    f,
                    "File extension '{}' not allowed for path '{}'",
                    extension,
                    path.display()
                )
            }
            PathValidationError::InvalidPath(path) => {
                write!(f, "Invalid path '{}'", path.display())
            }
        }
    }
}

impl std::error::Error for PathValidationError {}

/// 路径限制规则
#[derive(Debug, Clone)]
pub struct PathRestriction {
    /// 根目录(路径必须在此之下)
    pub root_dir: PathBuf,
    /// 是否允许绝对路径
    pub allow_absolute: bool,
    /// 是否允许 `..` 路径遍历
    pub allow_parent_traversal: bool,
    /// 允许的文件扩展名白名单(None 表示不限制)
    pub allowed_extensions: Option<HashSet<String>>,
}

impl Default for PathRestriction {
    fn default() -> Self {
        Self {
            root_dir: PathBuf::from("."),
            allow_absolute: false,
            allow_parent_traversal: false,
            allowed_extensions: None,
        }
    }
}

/// 路径验证器
#[derive(Clone)]
pub struct PathValidator {
    restriction: PathRestriction,
}

impl PathValidator {
    /// 创建新的路径验证器
    pub fn new(restriction: PathRestriction) -> Self {
        Self { restriction }
    }

    /// 从根目录创建验证器(默认严格配置)
    pub fn with_root_dir(root_dir: PathBuf) -> Self {
        Self::new(PathRestriction {
            root_dir,
            allow_absolute: false,
            allow_parent_traversal: false,
            allowed_extensions: None,
        })
    }

    /// 验证并规范化路径
    ///
    /// 返回规范化的绝对路径,如果验证失败则返回错误
    pub fn validate_and_normalize(&self, path: &Path) -> Result<PathBuf, PathValidationError> {
        // 1. 检查绝对路径
        if path.is_absolute() && !self.restriction.allow_absolute {
            return Err(PathValidationError::AbsolutePathNotAllowed(
                path.to_path_buf(),
            ));
        }

        // 2. 检查父目录遍历(快速路径检查)
        if !self.restriction.allow_parent_traversal {
            let path_str = path.to_string_lossy();
            if path_str.contains("..") {
                return Err(PathValidationError::ParentTraversalNotAllowed(
                    path.to_path_buf(),
                ));
            }
        }

        // 3. 规范化路径(解析 . 和 ..)
        let normalized = self.canonicalize_safe(path)?;

        // 4. 检查是否在根目录下
        if let Ok(root) = self.restriction.root_dir.canonicalize()
            && !normalized.starts_with(&root)
        {
            return Err(PathValidationError::OutsideRoot {
                path: normalized.clone(),
                root,
            });
        }

        // 5. 检查文件扩展名
        if let Some(allowed) = &self.restriction.allowed_extensions
            && let Some(ext) = normalized.extension()
        {
            let ext_str = ext.to_string_lossy().to_lowercase();
            if !allowed.contains(&ext_str) {
                return Err(PathValidationError::ExtensionNotAllowed {
                    path: normalized.clone(),
                    extension: ext_str,
                });
            }
        }

        Ok(normalized)
    }

    /// 安全地规范化路径(避免 IO 错误导致的 panic)
    fn canonicalize_safe(&self, path: &Path) -> Result<PathBuf, PathValidationError> {
        // 对于相对路径,基于 root_dir 解析
        let full_path = if path.is_absolute() {
            path.to_path_buf()
        } else {
            self.restriction.root_dir.join(path)
        };

        // 尝试 canonicalize,如果失败则手动清理路径组件
        match full_path.canonicalize() {
            Ok(canon) => Ok(canon),
            Err(_) => {
                // 文件不存在时,手动规范化路径组件
                let mut result = PathBuf::new();
                for component in full_path.components() {
                    match component {
                        Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
                            result.push(component);
                        }
                        Component::CurDir => {
                            // 忽略 .
                        }
                        Component::ParentDir => {
                            if !self.restriction.allow_parent_traversal {
                                return Err(PathValidationError::ParentTraversalNotAllowed(
                                    full_path,
                                ));
                            }
                            // 尝试弹出父目录
                            if !result.pop() {
                                return Err(PathValidationError::InvalidPath(full_path));
                            }
                        }
                    }
                }
                Ok(result)
            }
        }
    }

    /// 检查路径是否有效(不进行规范化,仅快速检查)
    pub fn is_valid(&self, path: &Path) -> bool {
        self.validate_and_normalize(path).is_ok()
    }

    /// 获取路径限制配置
    pub fn restriction(&self) -> &PathRestriction {
        &self.restriction
    }
}

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

    #[test]
    fn test_path_validator_blocks_parent_traversal() {
        let restriction = PathRestriction {
            root_dir: PathBuf::from("/safe"),
            allow_absolute: false,
            allow_parent_traversal: false,
            allowed_extensions: None,
        };

        let validator = PathValidator::new(restriction);

        // 应该被阻止
        assert!(
            validator
                .validate_and_normalize(Path::new("../etc/passwd"))
                .is_err()
        );
        assert!(
            validator
                .validate_and_normalize(Path::new("safe/../../etc/passwd"))
                .is_err()
        );
    }

    #[test]
    fn test_path_validator_blocks_absolute_paths() {
        let restriction = PathRestriction {
            root_dir: PathBuf::from("/safe"),
            allow_absolute: false,
            allow_parent_traversal: false,
            allowed_extensions: None,
        };

        let validator = PathValidator::new(restriction);

        // 应该被阻止
        assert!(
            validator
                .validate_and_normalize(Path::new("/etc/passwd"))
                .is_err()
        );
    }

    #[test]
    fn test_path_validator_extension_whitelist() {
        let mut allowed = HashSet::new();
        allowed.insert("aether".to_string());
        allowed.insert("txt".to_string());

        let restriction = PathRestriction {
            root_dir: PathBuf::from("/safe"),
            allow_absolute: false,
            allow_parent_traversal: false,
            allowed_extensions: Some(allowed),
        };

        let _validator = PathValidator::new(restriction);

        // 创建临时文件进行测试
        use std::fs;
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test.aether");
        fs::write(&test_file, "test").unwrap();

        // 应该被允许(基于当前目录的相对路径)
        // 注意:这个测试可能需要根据实际文件系统调整
    }

    #[test]
    fn test_path_validator_with_root_dir() {
        let validator = PathValidator::with_root_dir(PathBuf::from("/tmp/test"));

        // 相对路径应该基于 root_dir
        let result = validator.validate_and_normalize(Path::new("subdir/file.txt"));
        // 由于 /tmp/test/subdir/file.txt 不存在,会手动规范化
        assert!(result.is_ok() || result.is_err()); // 取决于文件系统
    }

    #[test]
    fn test_path_error_display() {
        let err = PathValidationError::OutsideRoot {
            path: PathBuf::from("/etc/passwd"),
            root: PathBuf::from("/safe"),
        };
        assert!(err.to_string().contains("outside allowed root"));

        let err = PathValidationError::AbsolutePathNotAllowed(PathBuf::from("/etc/passwd"));
        assert!(err.to_string().contains("not allowed"));

        let err = PathValidationError::ParentTraversalNotAllowed(PathBuf::from("../file"));
        assert!(err.to_string().contains("Parent traversal"));
    }
}