use std::cell::RefCell;
use std::path::{Component, Path, PathBuf};
thread_local! {
static INPUT_SOURCE_MAP: RefCell<Vec<(PathBuf, PathBuf, PathBuf)>> = const { RefCell::new(Vec::new()) };
static FETCHED_SOURCE_NAMES: RefCell<Vec<(PathBuf, String, PathBuf)>> = const { RefCell::new(Vec::new()) };
static CANON_MEMO: RefCell<std::collections::HashMap<PathBuf, PathBuf>> =
RefCell::new(std::collections::HashMap::new());
}
fn canonicalize_memo(path: &Path) -> Option<PathBuf> {
if let Some(hit) = CANON_MEMO.with(|m| m.borrow().get(path).cloned()) {
return Some(hit);
}
match path.canonicalize() {
Ok(canon) => {
CANON_MEMO.with(|m| {
m.borrow_mut().insert(path.to_path_buf(), canon.clone());
});
Some(canon)
}
Err(_) => None,
}
}
pub fn register_fetched_source(read_dir: &Path, name: &str) {
FETCHED_SOURCE_NAMES.with(|m| {
let mut m = m.borrow_mut();
if m.iter().any(|(rd, _, _)| rd == read_dir) {
return;
}
let canon = read_dir
.canonicalize()
.unwrap_or_else(|_| read_dir.to_path_buf());
m.push((read_dir.to_path_buf(), name.to_string(), canon));
});
}
pub fn register_input_source(store_path: &Path, read_dir: &Path) {
if store_path == read_dir {
return;
}
INPUT_SOURCE_MAP.with(|m| {
let mut m = m.borrow_mut();
if m.iter().any(|(sp, _, _)| sp == store_path) {
return;
}
let canon = read_dir
.canonicalize()
.unwrap_or_else(|_| read_dir.to_path_buf());
m.push((store_path.to_path_buf(), read_dir.to_path_buf(), canon));
});
}
#[must_use]
pub fn materialize(path: &Path) -> PathBuf {
INPUT_SOURCE_MAP.with(|m| {
for (store_path, read_dir, _) in m.borrow().iter() {
if path == store_path {
return read_dir.clone();
}
if let Ok(suffix) = path.strip_prefix(store_path) {
return read_dir.join(suffix);
}
}
path.to_path_buf()
})
}
#[must_use]
pub fn materialize_str(path: &str) -> String {
materialize(Path::new(path)).to_string_lossy().into_owned()
}
#[must_use]
pub fn dematerialize(path: &Path) -> PathBuf {
let canon = canonicalize_memo(path);
let probe: &Path = canon.as_deref().unwrap_or(path);
INPUT_SOURCE_MAP.with(|m| {
for (store_path, read_dir, rd_canon) in m.borrow().iter() {
if probe == rd_canon.as_path() {
return store_path.clone();
}
if let Ok(suffix) = probe.strip_prefix(rd_canon) {
return store_path.join(suffix);
}
if path == read_dir.as_path() {
return store_path.clone();
}
if let Ok(suffix) = path.strip_prefix(read_dir) {
return store_path.join(suffix);
}
}
path.to_path_buf()
})
}
#[must_use]
pub fn dematerialize_str(path: &str) -> String {
dematerialize(Path::new(path)).to_string_lossy().into_owned()
}
#[must_use]
pub fn source_name_for_read_dir(real: &Path) -> Option<String> {
let real_canon = canonicalize_memo(real)?;
let from_input = INPUT_SOURCE_MAP.with(|m| {
for (store_path, _read_dir, rd_canon) in m.borrow().iter() {
if real_canon == *rd_canon {
return store_path
.file_name()
.map(|n| n.to_string_lossy().into_owned());
}
}
None
});
if from_input.is_some() {
return from_input;
}
FETCHED_SOURCE_NAMES.with(|m| {
for (_read_dir, name, rd_canon) in m.borrow().iter() {
if real_canon == *rd_canon {
return Some(name.clone());
}
}
None
})
}
#[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 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 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(|| {
format!("relative import '{raw}' with no base directory")
})?;
resolve_relative(base, raw)
};
if materialize(&resolved).is_dir() {
Ok(resolved.join("default.nix"))
} else {
Ok(resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_removes_dot() {
assert_eq!(normalize(Path::new("/a/./b")), PathBuf::from("/a/b"));
}
#[test]
fn normalize_resolves_dotdot() {
assert_eq!(normalize(Path::new("/a/b/../c")), PathBuf::from("/a/c"));
}
#[test]
fn normalize_multiple_dots() {
assert_eq!(
normalize(Path::new("/a/./b/./c")),
PathBuf::from("/a/b/c")
);
}
#[test]
fn normalize_preserves_absolute() {
assert_eq!(normalize(Path::new("/a/b/c")), PathBuf::from("/a/b/c"));
}
#[test]
fn normalize_empty_result_becomes_dot() {
assert_eq!(normalize(Path::new(".")), PathBuf::from("."));
}
#[test]
fn resolve_relative_basic() {
assert_eq!(
resolve_relative(Path::new("/base"), "sub/file.nix"),
PathBuf::from("/base/sub/file.nix")
);
}
#[test]
fn resolve_relative_with_dotdot() {
assert_eq!(
resolve_relative(Path::new("/base/sub"), "../file.nix"),
PathBuf::from("/base/file.nix")
);
}
#[test]
fn resolve_import_absolute() {
let r = resolve_import(None, "/absolute/path.nix").unwrap();
assert_eq!(r, PathBuf::from("/absolute/path.nix"));
}
#[test]
fn resolve_import_relative_needs_base() {
assert!(resolve_import(None, "./relative.nix").is_err());
}
#[test]
fn canon_abs_root_dot() {
assert_eq!(canon_abs("/."), "/");
}
#[test]
fn canon_abs_root_identity() {
assert_eq!(canon_abs("/"), "/");
}
#[test]
fn canon_abs_removes_dot() {
assert_eq!(canon_abs("/foo/./bar"), "/foo/bar");
}
#[test]
fn canon_abs_resolves_dotdot() {
assert_eq!(canon_abs("/foo/../bar"), "/bar");
}
#[test]
fn canon_abs_dotdot_clamps_at_root() {
assert_eq!(canon_abs("/.."), "/");
assert_eq!(canon_abs("/../.."), "/");
assert_eq!(canon_abs("/a/../.."), "/");
}
#[test]
fn canon_abs_collapses_redundant_slashes() {
assert_eq!(canon_abs("/foo//bar"), "/foo/bar");
assert_eq!(canon_abs("/nix/store/"), "/nix/store");
}
#[test]
fn canon_abs_store_path_identity() {
assert_eq!(canon_abs("/nix/store"), "/nix/store");
assert_eq!(
canon_abs("/nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo"),
"/nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo"
);
}
#[test]
fn canon_abs_leaves_relative_untouched() {
assert_eq!(canon_abs("~/foo"), "~/foo");
assert_eq!(canon_abs("./foo"), "./foo");
}
#[test]
fn resolve_import_absolute_with_dotdot() {
let r = resolve_import(None, "/a/b/../c.nix").unwrap();
assert_eq!(r, PathBuf::from("/a/c.nix"));
}
#[test]
fn resolve_import_directory_appends_default_nix() {
let r = resolve_import(None, "/tmp").unwrap();
assert_eq!(r, PathBuf::from("/tmp/default.nix"));
}
#[test]
fn resolve_import_relative_with_base() {
let r = resolve_import(Some(Path::new("/base")), "sub/file.nix").unwrap();
assert_eq!(r, PathBuf::from("/base/sub/file.nix"));
}
#[test]
fn materialize_remaps_registered_store_prefix() {
register_input_source(
Path::new("/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-source"),
Path::new("/home/u/.cache/sui/inputs/dead"),
);
assert_eq!(
materialize(Path::new(
"/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-source/lib/foo.nix"
)),
PathBuf::from("/home/u/.cache/sui/inputs/dead/lib/foo.nix"),
);
assert_eq!(
materialize(Path::new(
"/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-source"
)),
PathBuf::from("/home/u/.cache/sui/inputs/dead"),
);
}
#[test]
fn dematerialize_lifts_cache_path_to_store_source() {
let cache = tempfile::tempdir().unwrap();
std::fs::create_dir_all(cache.path().join("modules/system")).unwrap();
let store = Path::new(
"/nix/store/npm9dap7j0i92l524y09x255zi9447qp-source",
);
register_input_source(store, cache.path());
let real = cache.path().join("modules/system/dock.nix");
assert_eq!(
dematerialize(&real),
store.join("modules/system/dock.nix"),
);
assert_eq!(dematerialize(cache.path()), store.to_path_buf());
}
#[test]
fn dematerialize_passes_unregistered_paths_through() {
let unrelated = tempfile::tempdir().unwrap();
let p = unrelated.path().join("some/file.nix");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(&p, b"x").unwrap();
assert_eq!(dematerialize(&p), p);
}
#[test]
fn materialize_passes_unregistered_paths_through() {
let p = "/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-hello-2.12/bin/hello";
assert_eq!(materialize(Path::new(p)), PathBuf::from(p));
assert_eq!(materialize(Path::new("/etc/nix/nix.conf")), PathBuf::from("/etc/nix/nix.conf"));
}
#[test]
fn register_input_source_ignores_identity_mapping() {
let dir = Path::new("/some/local/path-input");
register_input_source(dir, dir);
assert_eq!(materialize(dir), PathBuf::from(dir));
}
#[test]
fn register_input_source_prefix_boundary_is_exact() {
register_input_source(
Path::new("/nix/store/cccccccccccccccccccccccccccccccc-source"),
Path::new("/cache/c"),
);
let sibling = "/nix/store/cccccccccccccccccccccccccccccccc-source-extra/x";
assert_eq!(materialize(Path::new(sibling)), PathBuf::from(sibling));
}
#[test]
fn source_name_maps_read_dir_root_to_store_source_name() {
let cache = tempfile::tempdir().unwrap();
let store_path = Path::new(
"/nix/store/9qcaaxf4dyy09df0gv4ibfj93aplq3jk-source",
);
register_input_source(store_path, cache.path());
assert_eq!(
source_name_for_read_dir(cache.path()).as_deref(),
Some("9qcaaxf4dyy09df0gv4ibfj93aplq3jk-source"),
);
}
#[test]
fn source_name_returns_none_for_unregistered_or_subpath() {
let unrelated = tempfile::tempdir().unwrap();
assert_eq!(source_name_for_read_dir(unrelated.path()), None);
let cache = tempfile::tempdir().unwrap();
std::fs::create_dir(cache.path().join("subdir")).unwrap();
register_input_source(
Path::new("/nix/store/dddddddddddddddddddddddddddddddd-source"),
cache.path(),
);
assert_eq!(source_name_for_read_dir(&cache.path().join("subdir")), None);
}
#[test]
fn fetched_source_dir_maps_to_source_name() {
let cache = tempfile::tempdir().unwrap();
register_fetched_source(cache.path(), "source");
assert_eq!(
source_name_for_read_dir(cache.path()).as_deref(),
Some("source"),
);
}
#[test]
fn fetched_source_registry_honors_explicit_name() {
let cache = tempfile::tempdir().unwrap();
register_fetched_source(cache.path(), "my-thing");
assert_eq!(
source_name_for_read_dir(cache.path()).as_deref(),
Some("my-thing"),
);
}
}