Skip to main content

zagens_runtime_adapters/tools/
path.rs

1//! Path containment helpers shared by tool execution.
2
3use std::path::{Component, Path, PathBuf};
4
5/// Compare paths for prefix containment, normalizing Windows `\\?\` verbatim prefixes.
6#[must_use]
7pub fn path_has_prefix(path: &Path, prefix: &Path) -> bool {
8    strip_verbatim_prefix(path).starts_with(strip_verbatim_prefix(prefix))
9}
10
11#[must_use]
12fn strip_verbatim_prefix(path: &Path) -> PathBuf {
13    #[cfg(windows)]
14    {
15        let s = path.display().to_string();
16        if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
17            return PathBuf::from(format!(r"\\{rest}"));
18        }
19        if let Some(rest) = s.strip_prefix(r"\\?\") {
20            return PathBuf::from(rest);
21        }
22    }
23    path.to_path_buf()
24}
25
26/// Normalize a path by resolving `.` / `..` without touching the filesystem.
27#[must_use]
28pub fn normalize_path(path: &Path) -> PathBuf {
29    let mut prefix: Option<std::ffi::OsString> = None;
30    let mut is_root = false;
31    let mut stack: Vec<std::ffi::OsString> = Vec::new();
32
33    for component in path.components() {
34        match component {
35            Component::Prefix(prefix_component) => {
36                prefix = Some(prefix_component.as_os_str().to_owned());
37            }
38            Component::RootDir => {
39                is_root = true;
40            }
41            Component::CurDir => {}
42            Component::ParentDir => {
43                let parent = Component::ParentDir.as_os_str();
44                if let Some(last) = stack.pop() {
45                    if last == parent {
46                        stack.push(last);
47                        stack.push(parent.to_owned());
48                    }
49                } else if !is_root {
50                    stack.push(parent.to_owned());
51                }
52            }
53            Component::Normal(part) => {
54                stack.push(part.to_owned());
55            }
56        }
57    }
58
59    let mut normalized = PathBuf::new();
60    if let Some(prefix) = prefix {
61        normalized.push(prefix);
62    }
63    if is_root {
64        normalized.push(Path::new(std::path::MAIN_SEPARATOR_STR));
65    }
66    for part in stack {
67        normalized.push(part);
68    }
69    normalized
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn normalize_path_resolves_parent() {
78        let normalized = normalize_path(Path::new("new/../safe.txt"));
79        assert!(normalized.ends_with("safe.txt"));
80    }
81}