realpath-ext 0.1.3

An implementation of realpath() (with some extensions) for *nix systems.
Documentation
use std::ffi::OsString;
use std::fs;

use realpath_ext::RealpathFlags;

#[cfg(feature = "std")]
use realpath_ext::realpath;

#[cfg(not(feature = "std"))]
fn realpath<P: AsRef<std::path::Path>>(
    path: P,
    flags: RealpathFlags,
) -> std::io::Result<std::path::PathBuf> {
    #[cfg(target_family = "unix")]
    use std::os::unix::prelude::*;
    #[cfg(target_os = "wasi")]
    use std::os::wasi::prelude::*;

    let mut buf = vec![0; libc::PATH_MAX as usize];

    let n = realpath_ext::realpath_raw(path.as_ref().as_os_str().as_bytes(), &mut buf, flags)
        .map_err(std::io::Error::from_raw_os_error)?;

    buf.truncate(n);
    Ok(OsString::from_vec(buf).into())
}

#[test]
fn test_success() {
    let tmpdir = std::env::temp_dir();
    let exe = std::env::current_exe().unwrap();
    let cwd = std::env::current_dir().unwrap();

    let mut alt_cwd;
    if let Some(fname) = cwd.file_name() {
        alt_cwd = OsString::from("../");
        alt_cwd.push(fname);
    } else {
        alt_cwd = cwd.clone().into();
    }

    for &path in [
        "/",
        ".",
        "..",
        "../..",
        "..//..//../",
        "/bin",
        "///usr/./bin/.",
        "src",
        "/etc/passwd",
        tmpdir.to_str().unwrap(),
        exe.to_str().unwrap(),
        cwd.to_str().unwrap(),
        alt_cwd.to_str().unwrap(),
        #[cfg(all(target_os = "linux", any(target_env = "gnu", target_env = "")))]
        &"./".repeat(libc::PATH_MAX as usize),
    ]
    .iter()
    {
        assert_eq!(
            realpath(path, RealpathFlags::empty()).unwrap().as_os_str(),
            fs::canonicalize(path).unwrap().as_os_str()
        );

        realpath(path, RealpathFlags::IGNORE_SYMLINKS).unwrap();
    }
}

#[test]
fn test_leading_slashes() {
    assert_eq!(
        realpath("/", RealpathFlags::empty()).unwrap().as_os_str(),
        "/"
    );
    assert_eq!(
        realpath("//", RealpathFlags::empty()).unwrap().as_os_str(),
        "//"
    );
    assert_eq!(
        realpath("///", RealpathFlags::empty()).unwrap().as_os_str(),
        "/"
    );

    assert!(!realpath("/", RealpathFlags::empty())
        .unwrap()
        .as_os_str()
        .to_str()
        .unwrap()
        .starts_with("//"));
    assert!(realpath("//", RealpathFlags::empty())
        .unwrap()
        .as_os_str()
        .to_str()
        .unwrap()
        .starts_with("//"));
    assert!(!realpath("///", RealpathFlags::empty())
        .unwrap()
        .as_os_str()
        .to_str()
        .unwrap()
        .starts_with("//"));
}

#[test]
fn test_enotdir() {
    let exe = std::env::current_exe().unwrap().into_os_string();
    let mut path = exe.clone();
    path.push("/.");
    assert_eq!(
        realpath(&path, RealpathFlags::empty())
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    assert_eq!(
        realpath(&path, RealpathFlags::IGNORE_SYMLINKS)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    assert_eq!(
        realpath(&path, RealpathFlags::ALLOW_LAST_MISSING)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    let mut path2 = exe;
    path2.push("/a/.");

    assert_eq!(
        realpath(&path2, RealpathFlags::empty())
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    assert_eq!(
        realpath(&path2, RealpathFlags::IGNORE_SYMLINKS)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    realpath(&path2, RealpathFlags::ALLOW_MISSING).unwrap();

    assert_eq!(
        realpath("/etc/passwd/", RealpathFlags::empty())
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );
    assert_eq!(
        realpath("/etc/passwd/abc", RealpathFlags::empty())
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    assert_eq!(
        realpath("/etc/passwd/", RealpathFlags::IGNORE_SYMLINKS)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );
    assert_eq!(
        realpath("/etc/passwd/abc", RealpathFlags::IGNORE_SYMLINKS)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );

    realpath("/etc/passwd/", RealpathFlags::ALLOW_MISSING).unwrap();
    assert_eq!(
        realpath("/etc/passwd/", RealpathFlags::ALLOW_LAST_MISSING)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );
    realpath("/etc/passwd/abc", RealpathFlags::ALLOW_MISSING).unwrap();
    assert_eq!(
        realpath("/etc/passwd/abc", RealpathFlags::ALLOW_LAST_MISSING)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOTDIR)
    );
}

#[test]
fn test_enoent() {
    assert_eq!(
        realpath("NOEXIST", RealpathFlags::empty())
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOENT)
    );

    realpath("NOEXIST", RealpathFlags::ALLOW_MISSING).unwrap();
    realpath("NOEXIST", RealpathFlags::ALLOW_LAST_MISSING).unwrap();

    realpath("NOEXIST/abc", RealpathFlags::ALLOW_MISSING).unwrap();
    assert_eq!(
        realpath("NOEXIST/abc", RealpathFlags::ALLOW_LAST_MISSING)
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENOENT)
    );
}

#[test]
fn test_builders() {
    // Resolving a path with a max length that's too short should fail
    #[cfg(feature = "std")]
    assert_eq!(
        realpath_ext::RealpathBuilder::default()
            .max_len(10)
            .realpath("/etc/passwd")
            .unwrap_err()
            .raw_os_error(),
        Some(libc::ENAMETOOLONG),
    );

    // Resolving a path with a buffer that's too short should fail
    let mut buf = vec![0u8; 10];
    assert_eq!(
        realpath_ext::RealpathRawBuilder::default().realpath_raw(b"/etc/passwd", &mut buf),
        Err(libc::ENAMETOOLONG),
    );

    // Try some paths that are commonly symlinks
    for path in ["/bin", "/lib", "/tmp", "/var/run"].iter() {
        if std::path::Path::new(path).is_symlink() {
            let mut buf = vec![0u8; 1000];
            // This is NOT enough to handle symlinks
            let mut tmp = vec![0u8; 1];
            assert_eq!(
                realpath_ext::RealpathRawBuilder::new()
                    .temp_buffer(Some(&mut tmp))
                    .realpath_raw(path.as_bytes(), &mut buf),
                Err(libc::ENAMETOOLONG),
            );
        }
    }
}