use std::path::{Path, PathBuf};
use super::{collapse_home_to_env, expand_home_in_string};
pub fn collapse_project_path(project_root: &Path, input: &str) -> String {
let project_root = normalize_path(project_root);
let candidate = resolve_absolute_candidate(&project_root, input);
if candidate == project_root {
return "./".to_string();
}
if let Ok(relative) = candidate.strip_prefix(&project_root) {
let rendered = relative.to_string_lossy().replace('\\', "/");
return if rendered.is_empty() {
"./".to_string()
} else {
rendered
};
}
let rendered = candidate.to_string_lossy().to_string();
collapse_home_to_env(strip_windows_verbatim_prefix(&rendered))
}
pub fn resolve_project_path(project_root: &Path, input: &str) -> String {
let project_root = normalize_path(project_root);
let candidate = resolve_absolute_candidate(&project_root, input);
strip_windows_verbatim_prefix(&candidate.to_string_lossy()).to_string()
}
fn resolve_absolute_candidate(project_root: &Path, input: &str) -> PathBuf {
let expanded = expand_home_in_string(strip_windows_verbatim_prefix(input));
let candidate = PathBuf::from(&expanded);
if candidate.is_absolute() {
normalize_path(candidate)
} else {
normalize_path(project_root.join(candidate))
}
}
fn normalize_path(path: impl AsRef<Path>) -> PathBuf {
PathBuf::from(strip_windows_verbatim_prefix(
&path.as_ref().to_string_lossy(),
))
}
fn strip_windows_verbatim_prefix(input: &str) -> &str {
input.strip_prefix(r"\\?\").unwrap_or(input)
}
#[cfg(test)]
mod tests {
use super::{collapse_project_path, resolve_project_path};
use std::fs;
use std::path::PathBuf;
fn make_temp_dir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!("xbp-project-paths-{label}-{nanos}"));
fs::create_dir_all(&dir).expect("temp dir should be created");
dir
}
#[test]
fn collapse_project_path_uses_root_relative_output() {
let project_root = make_temp_dir("collapse-project-root");
let nested = project_root.join("apps").join("web");
assert_eq!(
collapse_project_path(&project_root, &project_root.to_string_lossy()),
"./"
);
assert_eq!(
collapse_project_path(&project_root, &nested.to_string_lossy()),
"apps/web"
);
let _ = fs::remove_dir_all(project_root);
}
#[test]
fn resolve_project_path_expands_relative_paths_against_project_root() {
let project_root = make_temp_dir("resolve-project-root");
let nested = project_root.join("apps").join("web");
assert_eq!(
PathBuf::from(resolve_project_path(&project_root, "./")),
project_root
);
assert_eq!(
PathBuf::from(resolve_project_path(&project_root, "apps/web")),
nested
);
let _ = fs::remove_dir_all(project_root);
}
}