use std::{io, sync::Once};
use cap_std::{ambient_authority, fs_utf8::Dir};
use camino::{Utf8Path, Utf8PathBuf};
use metrics::{counter, describe_counter};
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<F>(
raw: &str,
home_directory: &HomeDirectory,
read_env: F,
) -> Result<String, Error>
where
F: Fn(&str) -> Option<String>,
{
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, read_env)?;
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<F>(home_directory: &HomeDirectory, read_env: F) -> Result<String, Error>
where
F: Fn(&str) -> Option<String>,
{
describe_home_metrics();
let resolved = match home_directory {
HomeDirectory::Ambient => home_from_env(read_env),
HomeDirectory::Missing => None,
HomeDirectory::Explicit(home) => Some((home.clone(), HOME_SOURCE_EXPLICIT)),
};
let source = resolved
.as_ref()
.map_or(HOME_SOURCE_MISSING, |(_, source)| *source);
let outcome = if resolved.is_some() {
HOME_OUTCOME_FOUND
} else {
HOME_OUTCOME_UNAVAILABLE
};
tracing::debug!(
event = EXPANDUSER_HOME_EVENT,
source,
found = resolved.is_some(),
"resolved the home directory for expanduser",
);
counter!(
EXPANDUSER_HOME_TOTAL,
"outcome" => outcome,
"source" => source,
)
.increment(1);
resolved.map(|(home, _)| home).ok_or_else(|| {
tracing::debug!(
event = EXPANDUSER_HOME_EVENT,
source,
outcome = HOME_OUTCOME_UNAVAILABLE,
"expanduser found no home directory",
);
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("."))
}
pub(super) const EXPANDUSER_HOME_EVENT: &str = "stdlib.expanduser.home";
pub(super) const EXPANDUSER_HOME_TOTAL: &str = "netsuke_stdlib_expanduser_home_total";
pub(super) const HOME_OUTCOME_FOUND: &str = "found";
pub(super) const HOME_OUTCOME_UNAVAILABLE: &str = "home_unavailable";
fn describe_home_metrics() {
static DESCRIBE: Once = Once::new();
DESCRIBE.call_once(|| {
describe_counter!(
EXPANDUSER_HOME_TOTAL,
"Counts expanduser home resolutions labelled by outcome (found or \
home_unavailable) and by the bounded source that supplied the home."
);
});
}
pub(super) const HOME_SOURCE_HOME: &str = "home";
pub(super) const HOME_SOURCE_USERPROFILE: &str = "userprofile";
#[cfg(any(windows, test))]
pub(super) const HOME_SOURCE_DRIVE_PATH: &str = "drive_path";
#[cfg(any(windows, test))]
pub(super) const HOME_SOURCE_HOMESHARE: &str = "homeshare";
pub(super) const HOME_SOURCE_EXPLICIT: &str = "explicit";
pub(super) const HOME_SOURCE_MISSING: &str = "missing";
pub(super) type HomeSource = (String, &'static str);
fn home_from_env<F>(read_env: F) -> Option<HomeSource>
where
F: Fn(&str) -> Option<String>,
{
#[cfg(windows)]
{
windows_home_from(read_env)
}
#[cfg(not(windows))]
{
posix_home_from(read_env)
}
}
#[cfg(any(not(windows), test))]
pub(super) fn posix_home_from<F>(read_env: F) -> Option<HomeSource>
where
F: Fn(&str) -> Option<String>,
{
read_env("HOME")
.map(|home| (home, HOME_SOURCE_HOME))
.or_else(|| read_env("USERPROFILE").map(|home| (home, HOME_SOURCE_USERPROFILE)))
}
#[cfg(any(windows, test))]
pub(super) fn windows_home_from<F>(read_env: F) -> Option<HomeSource>
where
F: Fn(&str) -> Option<String>,
{
read_env("HOME")
.map(|home| (home, HOME_SOURCE_HOME))
.or_else(|| read_env("USERPROFILE").map(|home| (home, HOME_SOURCE_USERPROFILE)))
.or_else(|| match (read_env("HOMEDRIVE"), read_env("HOMEPATH")) {
(Some(drive), Some(path)) if !drive.is_empty() && !path.is_empty() => {
Some((format!("{drive}{path}"), HOME_SOURCE_DRIVE_PATH))
}
_ => read_env("HOMESHARE").map(|home| (home, HOME_SOURCE_HOMESHARE)),
})
}