use std::process::{Command, Stdio};
use crate::passwd;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
/// The buffer used for output is not large enough to store the value.
#[error("buffer too small")]
BufferTooSmall,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ExpandError {
#[error("home detection failed")]
HomeDetectionFailed,
#[error("buffer too small")]
BufferTooSmall,
}
/// Equivalent of `std.fs.max_path_bytes` on Linux.
const MAX_PATH_BYTES: usize = 4096;
/// Determine the home directory for the currently executing user. This
/// is generally an expensive process so the value should be cached.
pub fn home() -> Result, Error> {
home_with_capacity(MAX_PATH_BYTES)
}
/// Like [`home`] but emulates the fixed output buffer of the Zig version:
/// errors with `BufferTooSmall` if the result is longer than `cap` bytes.
pub fn home_with_capacity(cap: usize) -> Result , Error> {
let check = |s: String| {
if cap < s.len() {
Err(Error::BufferTooSmall)
} else {
Ok(Some(s))
}
};
// First: if we have a HOME env var, then we use that.
if let Some(result) = std::env::var_os("HOME") {
return check(result.to_string_lossy().into_owned());
}
// We try passwd. This doesn't work on multi-user mac but we try it anyways.
if let Ok(pw) = passwd::get()
&& let Some(result) = pw.home
{
return check(result);
}
// If all else fails, have the shell tell us...
let run = Command::new("/bin/sh")
.args(["-c", "cd && pwd"])
.stdin(Stdio::null())
.stderr(Stdio::null())
.output();
if let Ok(run) = run
&& run.status.success()
{
let out = String::from_utf8_lossy(&run.stdout);
return check(trim_space(&out).to_owned());
}
Ok(None)
}
fn trim_space(input: &str) -> &str {
input.trim_matches([' ', '\n', '\t'])
}
/// Expands a path that starts with a tilde (~) to the home directory of
/// the current user.
///
/// Errors if `home` fails or if the size of the expanded path is larger
/// than the maximum path length.
pub fn expand_home(path: &str) -> Result {
expand_home_with_capacity(path, MAX_PATH_BYTES)
}
/// Like [`expand_home`] with an explicit output capacity (emulating the
/// caller-provided buffer of the Zig version).
pub fn expand_home_with_capacity(path: &str, cap: usize) -> Result {
if !path.starts_with("~/") {
return Ok(path.to_owned());
}
let Ok(Some(home_dir)) = home_with_capacity(cap) else {
return Err(ExpandError::HomeDetectionFailed);
};
let rest = &path[1..]; // Skip the ~
let expanded_len = home_dir.len() + rest.len();
if expanded_len > cap {
return Err(ExpandError::BufferTooSmall);
}
Ok(format!("{home_dir}{rest}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::with_env_lock;
#[test]
fn expand_home_unix() {
with_env_lock(|| {
let cap = MAX_PATH_BYTES;
let home_dir = expand_home_with_capacity("~/", cap).unwrap();
// Joining the home directory `~` with the path `/`
// the result should end with a separator here. (e.g. `/home/user/`)
assert!(home_dir.ends_with('/'));
let downloads = expand_home_with_capacity("~/Downloads/shader.glsl", cap).unwrap();
let expected_downloads = format!("{home_dir}Downloads/shader.glsl");
assert_eq!(expected_downloads, downloads);
assert_eq!("~", expand_home_with_capacity("~", cap).unwrap());
assert_eq!("~abc/", expand_home_with_capacity("~abc/", cap).unwrap());
assert_eq!(
"/home/user",
expand_home_with_capacity("/home/user", cap).unwrap()
);
assert_eq!("", expand_home_with_capacity("", cap).unwrap());
// Expect an error if the buffer is large enough to hold the home directory,
// but not the expanded path
assert_eq!(
Err(ExpandError::BufferTooSmall),
expand_home_with_capacity("~/Downloads", home_dir.len())
);
});
}
#[test]
fn home_is_detected() {
with_env_lock(|| {
let result = home().unwrap();
assert!(result.is_some());
assert!(!result.unwrap().is_empty());
});
}
#[test]
fn home_prefers_env() {
temp_env::with_var("HOME", Some("/some/home"), || {
assert_eq!(home().unwrap().as_deref(), Some("/some/home"));
assert_eq!(expand_home("~/x").unwrap(), "/some/home/x");
assert_eq!(home_with_capacity(3), Err(Error::BufferTooSmall));
});
}
}