cargo-port 0.4.3

A TUI for inspecting and managing Rust projects
use std::borrow::Borrow;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::ops::Deref;
#[cfg(windows)]
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
#[cfg(windows)]
use std::path::Prefix;

/// An absolute filesystem path. Used as `HashMap` keys and for filesystem operations.
/// Wraps `PathBuf`. Created from absolute paths only.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub(crate) struct AbsolutePath(PathBuf);

impl AbsolutePath {
    pub fn as_path(&self) -> &Path { &self.0 }

    pub fn to_path_buf(&self) -> PathBuf { self.0.clone() }

    pub fn display_path(&self) -> DisplayPath { DisplayPath::new(home_relative_path(&self.0)) }

    /// Resolve a raw path string that may be relative to a base directory.
    /// If the raw path is absolute, use it directly. Otherwise, join it with
    /// `base`. Canonicalizes the result when possible.
    pub fn resolve(raw: &str, base: &Path) -> Self {
        let raw_path = Path::new(raw);
        let resolved = if raw_path.is_absolute() {
            PathBuf::from(raw)
        } else {
            base.join(raw)
        };
        Self::from(resolved.canonicalize().unwrap_or(resolved))
    }

    /// Resolve a raw path string that may be relative to a base directory.
    /// Does not canonicalize.
    pub fn resolve_no_canonicalize(raw: &str, base: &Path) -> Self {
        let raw_path = Path::new(raw);
        if raw_path.is_absolute() {
            Self::from(raw)
        } else {
            Self::from(base.join(raw))
        }
    }
}

impl PartialEq<Path> for AbsolutePath {
    fn eq(&self, other: &Path) -> bool { self.0.as_path() == other }
}

impl PartialEq<AbsolutePath> for Path {
    fn eq(&self, other: &AbsolutePath) -> bool { self == other.0.as_path() }
}

impl PartialEq<AbsolutePath> for PathBuf {
    fn eq(&self, other: &AbsolutePath) -> bool { self.as_path() == other.0.as_path() }
}

impl PartialEq<PathBuf> for AbsolutePath {
    fn eq(&self, other: &PathBuf) -> bool { self.0.as_path() == other.as_path() }
}

impl Deref for AbsolutePath {
    type Target = Path;

    fn deref(&self) -> &Path { &self.0 }
}

impl AsRef<Path> for AbsolutePath {
    fn as_ref(&self) -> &Path { &self.0 }
}

impl Borrow<Path> for AbsolutePath {
    fn borrow(&self) -> &Path { &self.0 }
}

impl Display for AbsolutePath {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.display().fmt(f) }
}

impl From<PathBuf> for AbsolutePath {
    fn from(path: PathBuf) -> Self {
        #[cfg(windows)]
        let path = strip_verbatim_disk_prefix(path);
        #[cfg(all(test, windows))]
        let path = normalize_test_path(&path);
        debug_assert!(
            path.is_absolute(),
            "AbsolutePath requires an absolute path: {}",
            path.display()
        );
        Self(path)
    }
}

impl From<String> for AbsolutePath {
    fn from(path: String) -> Self { Self::from(PathBuf::from(path)) }
}

impl From<&str> for AbsolutePath {
    fn from(path: &str) -> Self { Self::from(PathBuf::from(path)) }
}

impl From<&Path> for AbsolutePath {
    fn from(path: &Path) -> Self { Self::from(path.to_path_buf()) }
}

/// Rewrite the verbatim `\\?\C:\…` form that [`std::fs::canonicalize`] returns
/// on Windows back into the conventional `C:\…` form.
///
/// [`Prefix::VerbatimDisk`] never compares equal to [`Prefix::Disk`], so a
/// verbatim path fails every prefix test against the conventional paths the
/// rest of the program meets: the `C:\` mount points `Disks::mount_point`
/// reports (`scan::project_storage` returns
/// [`Unknown`](crate::scan::ProjectStorage::Unknown), and the project pane
/// drops its Available row), the `dirs::home_dir` prefix
/// [`home_relative_path`] strips, and the manifest paths `cargo metadata`
/// prints. Storing the conventional form makes those comparisons work.
///
/// `std`'s filesystem calls re-apply the prefix internally for paths past
/// `MAX_PATH`, so dropping it does not shorten the paths we can open.
/// Identity on the other verbatim forms (`\\?\UNC\…`, `\\?\pipe\…`), which
/// have no conventional spelling to rewrite to.
#[cfg(windows)]
fn strip_verbatim_disk_prefix(path: PathBuf) -> PathBuf {
    let mut components = path.components();
    let Some(Component::Prefix(prefix)) = components.next() else {
        return path;
    };
    let Prefix::VerbatimDisk(drive) = prefix.kind() else {
        return path;
    };
    let mut conventional = PathBuf::from(format!("{}:\\", char::from(drive)));
    conventional.extend(components.filter(|component| !matches!(component, Component::RootDir)));
    conventional
}

/// Rewrite a Unix-style absolute test path so it is absolute on the host
/// platform. Fixtures spell paths as `/tmp/...`, but on Windows a leading slash
/// carries no drive letter, so [`Path::is_absolute`] is `false` and the
/// assertion in `From<PathBuf>` would fire. Prefixing a drive keeps a single
/// fixture string valid on every platform. Identity on Unix.
#[cfg(test)]
pub(crate) fn normalize_test_path(path: &Path) -> PathBuf {
    #[cfg(windows)]
    if let Ok(rest) = path.strip_prefix("/") {
        return Path::new("C:/").join(rest);
    }
    path.to_path_buf()
}

/// A display path for the UI (e.g. `~/rust/bevy`). Never used as a `HashMap` key.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct DisplayPath(String);

impl DisplayPath {
    pub const fn new(s: String) -> Self { Self(s) }

    pub fn as_str(&self) -> &str { &self.0 }

    pub fn into_string(self) -> String { self.0 }
}

impl Display for DisplayPath {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) }
}

impl AsRef<str> for DisplayPath {
    fn as_ref(&self) -> &str { self.as_str() }
}

/// The last directory component of a project's root checkout path.
/// Used for top-level root labels, disambiguation, and worktree label fallback.
/// Never derived from Cargo metadata.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RootDirectoryName(pub(super) String);

impl RootDirectoryName {
    pub(crate) fn as_str(&self) -> &str { &self.0 }

    pub(crate) fn into_string(self) -> String { self.0 }
}

impl Display for RootDirectoryName {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) }
}

impl AsRef<str> for RootDirectoryName {
    fn as_ref(&self) -> &str { self.as_str() }
}

/// The Cargo package name when present, otherwise the directory leaf.
/// Used for workspace member rows, vendored rows, detail title bars, and
/// finder parent labels. Only available on `RustProject<Kind>`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PackageName(pub(super) String);

impl PackageName {
    pub(crate) fn as_str(&self) -> &str { &self.0 }

    pub(crate) fn into_string(self) -> String { self.0 }
}

impl Display for PackageName {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) }
}

impl AsRef<str> for PackageName {
    fn as_ref(&self) -> &str { self.as_str() }
}

/// Returns a `~/`-prefixed path if under the home directory, otherwise the absolute path.
pub(crate) fn home_relative_path(path: &Path) -> String {
    if let Some(home) = dirs::home_dir()
        && let Ok(rel) = path.strip_prefix(&home)
    {
        return format!("~/{}", rel.display());
    }
    path.display().to_string()
}

/// Extract the last path component as a `String`, returning an empty string
/// if the path has no file name or is not valid UTF-8.
pub(super) fn directory_leaf(path: &Path) -> String {
    path.file_name()
        .and_then(std::ffi::OsStr::to_str)
        .unwrap_or("")
        .to_string()
}

/// Windows-only: these assert on `std::path::Prefix` behavior that has no Unix
/// counterpart. The `windows` CI job runs them with
/// `cargo nextest run -E 'test(/^project::paths::/)'`.
#[cfg(all(test, windows))]
mod tests {
    use super::*;

    #[test]
    fn verbatim_disk_prefix_is_stored_in_conventional_form() {
        let canonicalized = AbsolutePath::from(PathBuf::from(r"\\?\C:\Users\dev\rust\bevy"));

        assert_eq!(
            canonicalized.as_path(),
            Path::new(r"C:\Users\dev\rust\bevy")
        );
        assert!(
            canonicalized.starts_with(Path::new(r"C:\")),
            "a volume whose mount point reads `C:\\` must prefix-match the stored path"
        );
    }

    #[test]
    fn conventional_path_is_stored_unchanged() {
        let path = AbsolutePath::from(PathBuf::from(r"C:\Users\dev\rust"));

        assert_eq!(path.as_path(), Path::new(r"C:\Users\dev\rust"));
    }
}