use std::{env, io};
use cap_std::{ambient_authority, fs_utf8::Dir};
use camino::{Utf8Path, Utf8PathBuf};
use minijinja::{Error, ErrorKind};
use super::fs_utils::{ParentDir, open_parent_dir};
use crate::localization::{self, keys};
use crate::stdlib::config_types::HomeDirectory;
use crate::stdlib::io_helpers::io_to_error;
pub(super) fn basename(path: &Utf8Path) -> String {
path.file_name().unwrap_or(path.as_str()).to_owned()
}
pub(super) fn dirname(path: &Utf8Path) -> String {
normalise_parent(path.parent()).into_string()
}
pub(super) fn with_suffix(
path: &Utf8Path,
suffix: &str,
count: usize,
sep: &str,
) -> Result<Utf8PathBuf, Error> {
if sep.is_empty() {
return Err(Error::new(
ErrorKind::InvalidOperation,
localization::message(keys::STDLIB_PATH_WITH_SUFFIX_EMPTY_SEPARATOR).to_string(),
));
}
let mut base = path.to_path_buf();
let name = base.file_name().map(str::to_owned).unwrap_or_default();
if !name.is_empty() {
base.pop();
}
let mut stem = name;
let mut removed = 0;
while removed < count {
if let Some(idx) = stem.rfind(sep) {
stem.truncate(idx);
removed += 1;
} else {
break;
}
}
stem.push_str(suffix);
let replacement = Utf8PathBuf::from(stem);
base.push(&replacement);
Ok(base)
}
pub(super) fn relative_to(path: &Utf8Path, root: &Utf8Path) -> Result<String, Error> {
path.strip_prefix(root)
.map(|p| p.as_str().to_owned())
.map_err(|_| {
Error::new(
ErrorKind::InvalidOperation,
localization::message(keys::STDLIB_PATH_RELATIVE_TO_MISMATCH)
.with_arg("path", path.as_str())
.with_arg("root", root.as_str())
.to_string(),
)
})
}
pub(super) fn canonicalize_any(path: &Utf8Path) -> Result<Utf8PathBuf, Error> {
if path.as_str().is_empty() || path == Utf8Path::new(".") {
return current_dir_utf8().map_err(|err| {
io_to_error(
Utf8Path::new("."),
&localization::message(keys::STDLIB_PATH_ACTION_CANONICALIZE),
err,
)
});
}
if is_root(path) {
return Ok(path.to_path_buf());
}
let ParentDir {
handle,
entry,
dir_path,
} = open_parent_dir(path)?;
handle
.canonicalize(Utf8Path::new(&entry))
.map(|resolved| {
if resolved.is_absolute() {
resolved
} else {
let mut absolute = dir_path;
absolute.push(&resolved);
absolute
}
})
.map_err(|err| {
io_to_error(
path,
&localization::message(keys::STDLIB_PATH_ACTION_CANONICALIZE),
err,
)
})
}
pub(super) fn is_user_specific_expansion(stripped: &str) -> bool {
matches!(
stripped.chars().next(),
Some(first) if first != '/' && first != std::path::MAIN_SEPARATOR
)
}
pub(super) fn expanduser(raw: &str, home_directory: &HomeDirectory) -> Result<String, Error> {
if let Some(stripped) = raw.strip_prefix('~') {
if is_user_specific_expansion(stripped) {
return Err(Error::new(
ErrorKind::InvalidOperation,
localization::message(keys::STDLIB_PATH_EXPANDUSER_UNSUPPORTED).to_string(),
));
}
let home = resolve_home(home_directory)?;
Ok(format!("{home}{stripped}"))
} else {
Ok(raw.to_owned())
}
}
pub(super) fn normalise_parent(parent: Option<&Utf8Path>) -> Utf8PathBuf {
parent
.filter(|p| !p.as_str().is_empty())
.map_or_else(|| Utf8PathBuf::from("."), Utf8Path::to_path_buf)
}
fn resolve_home(home_directory: &HomeDirectory) -> Result<String, Error> {
let home = match home_directory {
HomeDirectory::Ambient => home_from_env(),
HomeDirectory::Missing => None,
HomeDirectory::Explicit(home) => Some(home.clone()),
};
home.ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
localization::message(keys::STDLIB_PATH_EXPANDUSER_NO_HOME).to_string(),
)
})
}
fn is_root(path: &Utf8Path) -> bool {
path.parent().is_none() && path.file_name().is_none() && !path.as_str().is_empty()
}
fn current_dir_utf8() -> Result<Utf8PathBuf, io::Error> {
let dir = Dir::open_ambient_dir(".", ambient_authority())?;
dir.canonicalize(Utf8Path::new("."))
}
#[cfg(windows)]
#[expect(
clippy::disallowed_methods,
reason = "composition root: home resolution is the path stdlib's ambient boundary; the injected ladders are tracked in the environment meta issue"
)]
fn home_from_env() -> Option<String> {
env::var("HOME")
.or_else(|_| env::var("USERPROFILE"))
.ok()
.or_else(
|| match (env::var("HOMEDRIVE").ok(), env::var("HOMEPATH").ok()) {
(Some(drive), Some(path)) if !path.is_empty() => Some(format!("{drive}{path}")),
_ => env::var("HOMESHARE").ok(),
},
)
}
#[cfg(not(windows))]
#[expect(
clippy::disallowed_methods,
reason = "composition root: home resolution is the path stdlib's ambient boundary; the injected ladders are tracked in the environment meta issue"
)]
fn home_from_env() -> Option<String> {
env::var("HOME").or_else(|_| env::var("USERPROFILE")).ok()
}