use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
pub const ROOT: u64 = 1;
#[derive(Debug)]
struct Entry {
parent: u64,
name: OsString,
}
#[derive(Debug)]
pub struct IdTable {
next: u64,
entries: HashMap<u64, Entry>,
children: HashMap<u64, HashMap<OsString, u64>>,
}
impl IdTable {
pub fn new() -> Self {
Self {
next: ROOT + 1,
entries: HashMap::new(),
children: HashMap::new(),
}
}
pub fn path(&self, root: &Path, id: u64) -> Option<PathBuf> {
let mut names: Vec<&OsStr> = Vec::new();
let mut current = id;
while current != ROOT {
let entry = self.entries.get(¤t)?;
names.push(&entry.name);
current = entry.parent;
}
let mut path = root.to_path_buf();
path.extend(names.into_iter().rev());
Some(path)
}
pub fn parent(&self, id: u64) -> Option<u64> {
if id == ROOT {
return Some(ROOT);
}
self.entries.get(&id).map(|entry| entry.parent)
}
pub fn child(&mut self, parent: u64, name: &OsStr) -> u64 {
if let Some(id) = self.lookup(parent, name) {
return id;
}
let id = self.next;
self.next += 1;
self.entries.insert(
id,
Entry {
parent,
name: name.to_owned(),
},
);
self.children
.entry(parent)
.or_default()
.insert(name.to_owned(), id);
id
}
pub fn lookup(&self, parent: u64, name: &OsStr) -> Option<u64> {
self.children.get(&parent)?.get(name).copied()
}
pub fn forget(&mut self, parent: u64, name: &OsStr) {
let Some(id) = self
.children
.get_mut(&parent)
.and_then(|children| children.remove(name))
else {
return;
};
let mut pending = vec![id];
while let Some(id) = pending.pop() {
self.entries.remove(&id);
if let Some(children) = self.children.remove(&id) {
pending.extend(children.into_values());
}
}
}
pub fn rename(&mut self, from_parent: u64, from_name: &OsStr, to_parent: u64, to_name: &OsStr) {
if from_parent == to_parent && from_name == to_name {
return;
}
self.forget(to_parent, to_name);
let Some(id) = self
.children
.get_mut(&from_parent)
.and_then(|children| children.remove(from_name))
else {
return;
};
if let Some(entry) = self.entries.get_mut(&id) {
entry.parent = to_parent;
to_name.clone_into(&mut entry.name);
}
self.children
.entry(to_parent)
.or_default()
.insert(to_name.to_owned(), id);
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn name(s: &str) -> &OsStr {
OsStr::new(s)
}
#[test]
fn ids_are_stable_and_resolve_to_paths() {
let mut table = IdTable::new();
let etc = table.child(ROOT, name("etc"));
let hosts = table.child(etc, name("hosts"));
assert_eq!(
table.child(ROOT, name("etc")),
etc,
"a second sight reuses the id"
);
assert_ne!(etc, hosts);
assert_eq!(
table.path(Path::new("/"), hosts),
Some(PathBuf::from("/etc/hosts"))
);
assert_eq!(table.path(Path::new("/"), ROOT), Some(PathBuf::from("/")));
assert_eq!(table.parent(hosts), Some(etc));
assert_eq!(table.parent(ROOT), Some(ROOT));
assert_eq!(table.path(Path::new("/"), 99), None, "never issued");
}
#[test]
fn renaming_a_directory_carries_its_subtree() {
let mut table = IdTable::new();
let src = table.child(ROOT, name("src"));
let main = table.child(src, name("main.rs"));
table.rename(ROOT, name("src"), ROOT, name("lib"));
assert_eq!(
table.path(Path::new("/"), main),
Some(PathBuf::from("/lib/main.rs"))
);
assert_eq!(table.lookup(ROOT, name("lib")), Some(src));
assert_eq!(table.lookup(ROOT, name("src")), None);
}
#[test]
fn renaming_over_an_entry_forgets_it_and_its_children() {
let mut table = IdTable::new();
let old = table.child(ROOT, name("old"));
let old_child = table.child(old, name("x"));
let new = table.child(ROOT, name("new"));
table.rename(ROOT, name("new"), ROOT, name("old"));
assert_eq!(table.lookup(ROOT, name("old")), Some(new));
assert_eq!(table.path(Path::new("/"), old), None);
assert_eq!(table.path(Path::new("/"), old_child), None);
assert_eq!(table.len(), 1);
}
#[test]
fn renaming_onto_itself_keeps_the_entry() {
let mut table = IdTable::new();
let file = table.child(ROOT, name("f"));
table.rename(ROOT, name("f"), ROOT, name("f"));
assert_eq!(table.lookup(ROOT, name("f")), Some(file));
}
#[test]
fn forgetting_drops_the_whole_subtree() {
let mut table = IdTable::new();
let dir = table.child(ROOT, name("dir"));
let sub = table.child(dir, name("sub"));
let leaf = table.child(sub, name("leaf"));
let other = table.child(ROOT, name("other"));
table.forget(ROOT, name("dir"));
for id in [dir, sub, leaf] {
assert_eq!(table.path(Path::new("/"), id), None);
}
assert_eq!(
table.path(Path::new("/"), other),
Some(PathBuf::from("/other"))
);
assert_eq!(table.len(), 1);
table.forget(ROOT, name("missing"));
assert_eq!(table.len(), 1);
}
}