use std::path::{Component, Path, PathBuf};
#[must_use]
pub fn canon_abs(raw: &str) -> String {
if !raw.starts_with('/') {
return raw.to_string();
}
let mut components: Vec<&str> = Vec::new();
for seg in raw.split('/') {
match seg {
"" | "." => {}
".." => {
components.pop();
}
other => components.push(other),
}
}
if components.is_empty() {
"/".to_string()
} else {
let mut out = String::with_capacity(raw.len());
for c in &components {
out.push('/');
out.push_str(c);
}
out
}
}
#[must_use]
pub fn normalize(path: &Path) -> PathBuf {
let mut out = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
out.pop();
}
other => out.push(other),
}
}
if out.is_empty() {
PathBuf::from(".")
} else {
out.iter().collect()
}
}
#[must_use]
pub fn resolve_relative(base: &Path, relative: &str) -> PathBuf {
normalize(&base.join(relative))
}
pub fn resolve_import(base_dir: Option<&Path>, raw: &str) -> Result<PathBuf, String> {
let resolved = if Path::new(raw).is_absolute() {
normalize(Path::new(raw))
} else {
let base = base_dir.ok_or_else(|| {
let mut msg = String::from("relative import '");
msg.push_str(raw);
msg.push_str("' with no base directory");
msg
})?;
resolve_relative(base, raw)
};
if resolved.is_dir() {
Ok(resolved.join("default.nix"))
} else {
Ok(resolved)
}
}
fn resolve_corepkg(sub: &str) -> Option<String> {
use std::sync::OnceLock;
static COREPKGS_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
let dir = COREPKGS_DIR.get_or_init(|| {
let dir = std::env::temp_dir().join("sui-corepkgs");
std::fs::create_dir_all(&dir).ok()?;
std::fs::write(
dir.join("fetchurl.nix"),
include_str!("../../sui-eval/src/corepkgs/fetchurl.nix"),
)
.ok()?;
Some(dir)
});
let dir = dir.as_ref()?;
let path = dir.join(sub);
if path.exists() {
Some(path.to_string_lossy().into_owned())
} else {
None
}
}
#[must_use]
pub fn parse_nix_path(s: &str) -> Vec<(String, String)> {
if s.is_empty() {
return Vec::new();
}
s.split(':')
.filter(|e| !e.is_empty())
.map(|entry| match entry.split_once('=') {
Some((prefix, path)) => (prefix.to_string(), path.to_string()),
None => (String::new(), entry.to_string()),
})
.collect()
}
#[must_use]
pub fn resolve_search_path(name: &str) -> Option<String> {
if let Some(sub) = name.strip_prefix("nix/") {
return resolve_corepkg(sub);
}
let nix_path = std::env::var("NIX_PATH").unwrap_or_default();
if nix_path.is_empty() {
return None;
}
for (prefix, path) in parse_nix_path(&nix_path) {
if !prefix.is_empty() && name == prefix {
if Path::new(&path).exists() {
return Some(path);
}
continue;
}
if !prefix.is_empty() {
let needle = format!("{prefix}/");
if let Some(rest) = name.strip_prefix(&needle) {
let full = format!("{path}/{rest}");
if Path::new(&full).exists() {
return Some(full);
}
continue;
}
}
if prefix.is_empty() {
let full = format!("{path}/{name}");
if Path::new(&full).exists() {
return Some(full);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canon_abs_mirror_cases() {
assert_eq!(canon_abs("/."), "/");
assert_eq!(canon_abs("/"), "/");
assert_eq!(canon_abs("/foo/./bar"), "/foo/bar");
assert_eq!(canon_abs("/foo/../bar"), "/bar");
assert_eq!(canon_abs("/.."), "/");
assert_eq!(canon_abs("/../.."), "/");
assert_eq!(canon_abs("/a/../.."), "/");
assert_eq!(canon_abs("/foo//bar"), "/foo/bar");
assert_eq!(canon_abs("/nix/store/"), "/nix/store");
assert_eq!(canon_abs("relative/kept"), "relative/kept");
}
#[test]
fn normalize_mirror_cases() {
assert_eq!(normalize(Path::new("/a/./b")), PathBuf::from("/a/b"));
assert_eq!(normalize(Path::new("/a/x/../b")), PathBuf::from("/a/b"));
assert_eq!(normalize(Path::new("a/./b")), PathBuf::from("a/b"));
}
#[test]
fn resolve_relative_mirror_cases() {
assert_eq!(
resolve_relative(Path::new("/base"), "sub/file.nix"),
PathBuf::from("/base/sub/file.nix")
);
assert_eq!(
resolve_relative(Path::new("/base/sub"), "../file.nix"),
PathBuf::from("/base/file.nix")
);
}
#[test]
fn resolve_import_relative_without_base_errors() {
assert!(resolve_import(None, "lib.nix").is_err());
}
}