Skip to main content

codex_mobile_bridge/
directory.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result, bail};
5
6pub fn canonicalize_directory(path: &Path) -> Result<PathBuf> {
7    let canonical = fs::canonicalize(path)
8        .with_context(|| format!("目录不存在或不可访问: {}", path.display()))?;
9
10    if !canonical.is_dir() {
11        bail!("目标必须是目录: {}", canonical.display());
12    }
13
14    Ok(trim_trailing_separator(&canonical))
15}
16
17pub fn normalize_absolute_directory(path: &Path) -> Result<PathBuf> {
18    if !path.is_absolute() {
19        bail!("目录路径必须为绝对路径: {}", path.display());
20    }
21
22    Ok(trim_trailing_separator(path))
23}
24
25pub fn default_display_name(path: &Path) -> String {
26    path.file_name()
27        .and_then(|name| name.to_str())
28        .map(ToOwned::to_owned)
29        .filter(|value| !value.is_empty())
30        .unwrap_or_else(|| "directory".to_string())
31}
32
33pub fn directory_contains(base: &Path, candidate: &Path) -> bool {
34    candidate.starts_with(base)
35}
36
37pub fn parent_directory(path: &Path) -> Option<PathBuf> {
38    path.parent()
39        .filter(|parent| parent != &path)
40        .map(trim_trailing_separator)
41}
42
43fn trim_trailing_separator(path: &Path) -> PathBuf {
44    if path == Path::new("/") {
45        return PathBuf::from("/");
46    }
47
48    let display = path.to_string_lossy();
49    let trimmed = display.trim_end_matches('/');
50    if trimmed.is_empty() {
51        PathBuf::from("/")
52    } else {
53        PathBuf::from(trimmed)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use std::fs;
60
61    use super::{canonicalize_directory, directory_contains, parent_directory};
62
63    #[test]
64    fn canonicalize_directory_accepts_existing_dir() {
65        let temp = tempfile_dir("directory-ok");
66        fs::create_dir_all(&temp).unwrap();
67
68        let result = canonicalize_directory(&temp).unwrap();
69        assert!(result.is_dir());
70    }
71
72    #[test]
73    fn directory_contains_respects_path_boundaries() {
74        assert!(directory_contains(
75            std::path::Path::new("/srv/work"),
76            std::path::Path::new("/srv/work/project"),
77        ));
78        assert!(!directory_contains(
79            std::path::Path::new("/srv/work"),
80            std::path::Path::new("/srv/workspace"),
81        ));
82    }
83
84    #[test]
85    fn parent_directory_returns_none_for_root() {
86        assert!(parent_directory(std::path::Path::new("/")).is_none());
87    }
88
89    fn tempfile_dir(name: &str) -> std::path::PathBuf {
90        std::env::temp_dir().join(format!("{}-{}", name, uuid::Uuid::new_v4().simple()))
91    }
92}