claude_agent/security/path/
mod.rs

1//! TOCTOU-safe path handling with symlink protection.
2
3mod resolver;
4
5pub use resolver::SafePath;
6
7use std::ffi::OsString;
8use std::path::{Component, Path, PathBuf};
9
10pub const DEFAULT_MAX_SYMLINK_DEPTH: u8 = 10;
11
12pub fn normalize_path(path: &Path) -> PathBuf {
13    let mut components = Vec::new();
14
15    for component in path.components() {
16        match component {
17            Component::ParentDir => {
18                if !components.is_empty()
19                    && !matches!(
20                        components.last(),
21                        Some(Component::RootDir) | Some(Component::Prefix(_))
22                    )
23                {
24                    components.pop();
25                }
26            }
27            Component::CurDir => {}
28            c => components.push(c),
29        }
30    }
31
32    if components.is_empty() {
33        PathBuf::from(".")
34    } else {
35        components.iter().collect()
36    }
37}
38
39pub fn extract_relative_components(path: &Path) -> Vec<OsString> {
40    path.components()
41        .filter_map(|c| match c {
42            Component::Normal(s) => Some(s.to_os_string()),
43            _ => None,
44        })
45        .collect()
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn test_normalize_path() {
54        assert_eq!(
55            normalize_path(Path::new("/a/b/../c")),
56            PathBuf::from("/a/c")
57        );
58        assert_eq!(normalize_path(Path::new("/a/./b")), PathBuf::from("/a/b"));
59        assert_eq!(
60            normalize_path(Path::new("/a/b/../../c")),
61            PathBuf::from("/c")
62        );
63    }
64
65    #[test]
66    fn test_extract_relative_components() {
67        let components = extract_relative_components(Path::new("/a/b/c"));
68        assert_eq!(components.len(), 3);
69        assert_eq!(components[0], "a");
70        assert_eq!(components[1], "b");
71        assert_eq!(components[2], "c");
72    }
73}