xbp 10.28.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
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);
    }
}