hallouminate 0.2.2

A markdown corpus indexer for LLMs to build and query their own per-repo wikis.
Documentation
use std::path::{Path, PathBuf};

use super::FileRef;

pub fn expand_tilde(path: &str) -> PathBuf {
    PathBuf::from(shellexpand::tilde(path).into_owned())
}

/// Resolve symlinks and collapse `..` via [`std::fs::canonicalize`], falling
/// back to the original path on failure (typically: path does not exist, is
/// on a non-traversable mount, or the process lacks permission).
///
/// The error is intentionally swallowed: callers (the walker, the indexer)
/// need a stable [`FileRef`] for paths that may legitimately fail to
/// canonicalize, and there is no recovery the caller could perform with the
/// `io::Error` here. The unit tests pin both branches: `canonicalize_existing_dir_resolves`
/// exercises the success path, `canonicalize_nonexistent_passes_through_unchanged`
/// exercises the fallback.
pub fn canonicalize_or_passthrough(path: &Path) -> FileRef {
    match std::fs::canonicalize(path) {
        Ok(canonical) => FileRef::new(canonical),
        Err(_) => FileRef::new(path.to_path_buf()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn expand_tilde_resolves_home() {
        let expanded = expand_tilde("~/foo");
        let base_dirs = directories::BaseDirs::new().expect("home directory not available");
        let home = base_dirs.home_dir();
        assert!(
            !expanded.to_string_lossy().starts_with('~'),
            "tilde should be replaced: {}",
            expanded.display()
        );
        assert!(
            expanded.starts_with(home),
            "{} should start with home ({})",
            expanded.display(),
            home.display()
        );
        assert!(
            expanded.ends_with("foo"),
            "{} should end with foo",
            expanded.display()
        );
    }

    #[test]
    fn expand_tilde_leaves_absolute_path_unchanged() {
        let expanded = expand_tilde("/etc/hosts");
        assert_eq!(expanded, PathBuf::from("/etc/hosts"));
    }

    #[test]
    fn expand_tilde_leaves_relative_path_unchanged() {
        let expanded = expand_tilde("foo/bar");
        assert_eq!(expanded, PathBuf::from("foo/bar"));
    }

    #[test]
    fn canonicalize_existing_dir_resolves() {
        let tmp = std::env::temp_dir();
        let resolved = canonicalize_or_passthrough(&tmp);
        let expected = std::fs::canonicalize(&tmp).expect("temp dir canonicalizes");
        assert_eq!(resolved.as_path(), expected.as_path());
        assert!(
            resolved.as_path().is_absolute(),
            "canonical path must be absolute: {}",
            resolved.as_path().display()
        );
    }

    #[cfg(unix)]
    #[test]
    fn canonicalize_resolves_symlink() {
        use std::os::unix::fs::symlink;
        let temp = std::env::temp_dir();
        let pid = std::process::id();
        let target = temp.join(format!("hallouminate-target-{}", pid));
        let link = temp.join(format!("hallouminate-link-{}", pid));
        let _ = std::fs::remove_file(&link);
        let _ = std::fs::remove_dir(&target);
        std::fs::create_dir(&target).expect("create target dir");
        symlink(&target, &link).expect("create symlink");

        let resolved = canonicalize_or_passthrough(&link);
        let expected = std::fs::canonicalize(&target).expect("canonicalize target");
        assert_eq!(resolved.as_path(), expected.as_path());
        assert_ne!(
            resolved.as_path(),
            link.as_path(),
            "symlink path should be resolved away"
        );

        let _ = std::fs::remove_file(&link);
        let _ = std::fs::remove_dir(&target);
    }

    #[test]
    fn canonicalize_nonexistent_passes_through_unchanged() {
        let path =
            std::env::temp_dir().join(format!("hallouminate-nonexistent-{}", std::process::id()));
        let resolved = canonicalize_or_passthrough(&path);
        assert_eq!(resolved.as_path(), path.as_path());
    }
}