Skip to main content

xbp_cli/utils/
project_paths.rs

1use std::path::{Path, PathBuf};
2
3use super::{collapse_home_to_env, expand_home_in_string};
4
5pub fn collapse_project_path(project_root: &Path, input: &str) -> String {
6    let project_root = normalize_path(project_root);
7    let candidate = resolve_absolute_candidate(&project_root, input);
8
9    if candidate == project_root {
10        return "./".to_string();
11    }
12
13    if let Ok(relative) = candidate.strip_prefix(&project_root) {
14        let rendered = relative.to_string_lossy().replace('\\', "/");
15        return if rendered.is_empty() {
16            "./".to_string()
17        } else {
18            rendered
19        };
20    }
21
22    let rendered = candidate.to_string_lossy().to_string();
23    collapse_home_to_env(strip_windows_verbatim_prefix(&rendered))
24}
25
26pub fn resolve_project_path(project_root: &Path, input: &str) -> String {
27    let project_root = normalize_path(project_root);
28    let candidate = resolve_absolute_candidate(&project_root, input);
29    strip_windows_verbatim_prefix(&candidate.to_string_lossy()).to_string()
30}
31
32fn resolve_absolute_candidate(project_root: &Path, input: &str) -> PathBuf {
33    let expanded = expand_home_in_string(strip_windows_verbatim_prefix(input));
34    let candidate = PathBuf::from(&expanded);
35    if candidate.is_absolute() {
36        normalize_path(candidate)
37    } else {
38        normalize_path(project_root.join(candidate))
39    }
40}
41
42fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
43    PathBuf::from(strip_windows_verbatim_prefix(
44        &path.as_ref().to_string_lossy(),
45    ))
46}
47
48fn strip_windows_verbatim_prefix(input: &str) -> &str {
49    input.strip_prefix(r"\\?\").unwrap_or(input)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::{collapse_project_path, resolve_project_path};
55    use std::fs;
56    use std::path::PathBuf;
57
58    fn make_temp_dir(label: &str) -> PathBuf {
59        let nanos = std::time::SystemTime::now()
60            .duration_since(std::time::UNIX_EPOCH)
61            .expect("system clock should be after epoch")
62            .as_nanos();
63        let dir = std::env::temp_dir().join(format!("xbp-project-paths-{label}-{nanos}"));
64        fs::create_dir_all(&dir).expect("temp dir should be created");
65        dir
66    }
67
68    #[test]
69    fn collapse_project_path_uses_root_relative_output() {
70        let project_root = make_temp_dir("collapse-project-root");
71        let nested = project_root.join("apps").join("web");
72
73        assert_eq!(
74            collapse_project_path(&project_root, &project_root.to_string_lossy()),
75            "./"
76        );
77        assert_eq!(
78            collapse_project_path(&project_root, &nested.to_string_lossy()),
79            "apps/web"
80        );
81
82        let _ = fs::remove_dir_all(project_root);
83    }
84
85    #[test]
86    fn resolve_project_path_expands_relative_paths_against_project_root() {
87        let project_root = make_temp_dir("resolve-project-root");
88        let nested = project_root.join("apps").join("web");
89
90        assert_eq!(
91            PathBuf::from(resolve_project_path(&project_root, "./")),
92            project_root
93        );
94        assert_eq!(
95            PathBuf::from(resolve_project_path(&project_root, "apps/web")),
96            nested
97        );
98
99        let _ = fs::remove_dir_all(project_root);
100    }
101}