xbp_cli/utils/
project_paths.rs1use 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}