use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
pub struct InodeTable {
path_to_inode: HashMap<String, u64>,
inode_to_path: HashMap<u64, String>,
next_inode: AtomicU64,
}
impl Default for InodeTable {
fn default() -> Self {
Self::new()
}
}
impl InodeTable {
pub const ROOT_INODE: u64 = 1;
pub fn new() -> Self {
let mut table = Self {
path_to_inode: HashMap::new(),
inode_to_path: HashMap::new(),
next_inode: AtomicU64::new(2), };
table
.path_to_inode
.insert("/".to_string(), Self::ROOT_INODE);
table
.inode_to_path
.insert(Self::ROOT_INODE, "/".to_string());
table
}
pub fn get_or_create(&mut self, path: &str) -> u64 {
let normalized = Self::normalize_path(path);
if let Some(&inode) = self.path_to_inode.get(&normalized) {
return inode;
}
let inode = self.next_inode.fetch_add(1, Ordering::SeqCst);
self.path_to_inode.insert(normalized.clone(), inode);
self.inode_to_path.insert(inode, normalized);
inode
}
pub fn get_inode(&self, path: &str) -> Option<u64> {
let normalized = Self::normalize_path(path);
self.path_to_inode.get(&normalized).copied()
}
pub fn get_path(&self, inode: u64) -> Option<&str> {
self.inode_to_path.get(&inode).map(String::as_str)
}
pub fn remove(&mut self, inode: u64) -> Option<String> {
if let Some(path) = self.inode_to_path.remove(&inode) {
self.path_to_inode.remove(&path);
Some(path)
} else {
None
}
}
pub fn remove_by_path(&mut self, path: &str) -> Option<u64> {
let normalized = Self::normalize_path(path);
if let Some(inode) = self.path_to_inode.remove(&normalized) {
self.inode_to_path.remove(&inode);
Some(inode)
} else {
None
}
}
pub fn rename(&mut self, old_path: &str, new_path: &str) -> Option<u64> {
let old_normalized = Self::normalize_path(old_path);
let new_normalized = Self::normalize_path(new_path);
if let Some(inode) = self.path_to_inode.remove(&old_normalized) {
self.inode_to_path.insert(inode, new_normalized.clone());
self.path_to_inode.insert(new_normalized, inode);
Some(inode)
} else {
None
}
}
pub fn clear(&mut self) {
self.path_to_inode.clear();
self.inode_to_path.clear();
self.next_inode.store(2, Ordering::SeqCst);
self.path_to_inode.insert("/".to_string(), Self::ROOT_INODE);
self.inode_to_path.insert(Self::ROOT_INODE, "/".to_string());
}
fn normalize_path(path: &str) -> String {
let path = path.trim();
if path.is_empty() || path == "/" {
return "/".to_string();
}
let mut normalized = if path.starts_with('/') {
path.to_string()
} else {
format!("/{}", path)
};
if normalized.len() > 1 && normalized.ends_with('/') {
normalized.pop();
}
normalized
}
pub fn parent_path(path: &str) -> String {
let normalized = Self::normalize_path(path);
if normalized == "/" {
return "/".to_string();
}
match normalized.rfind('/') {
Some(0) => "/".to_string(),
Some(pos) => normalized[..pos].to_string(),
None => "/".to_string(),
}
}
pub fn filename(path: &str) -> &str {
let normalized = path.trim();
if normalized == "/" || normalized.is_empty() {
return "";
}
match normalized.rfind('/') {
Some(pos) => &normalized[pos + 1..],
None => normalized,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_root_inode() {
let table = InodeTable::new();
assert_eq!(table.get_inode("/"), Some(InodeTable::ROOT_INODE));
assert_eq!(table.get_path(InodeTable::ROOT_INODE), Some("/"));
}
#[test]
fn test_get_or_create() {
let mut table = InodeTable::new();
let inode1 = table.get_or_create("/foo");
let inode2 = table.get_or_create("/foo");
let inode3 = table.get_or_create("/bar");
assert_eq!(inode1, inode2);
assert_ne!(inode1, inode3);
assert_ne!(inode1, InodeTable::ROOT_INODE);
}
#[test]
fn test_normalize_path() {
assert_eq!(InodeTable::normalize_path(""), "/");
assert_eq!(InodeTable::normalize_path("/"), "/");
assert_eq!(InodeTable::normalize_path("foo"), "/foo");
assert_eq!(InodeTable::normalize_path("/foo"), "/foo");
assert_eq!(InodeTable::normalize_path("/foo/"), "/foo");
assert_eq!(InodeTable::normalize_path("/foo/bar"), "/foo/bar");
}
#[test]
fn test_parent_path() {
assert_eq!(InodeTable::parent_path("/"), "/");
assert_eq!(InodeTable::parent_path("/foo"), "/");
assert_eq!(InodeTable::parent_path("/foo/bar"), "/foo");
assert_eq!(InodeTable::parent_path("/foo/bar/baz"), "/foo/bar");
}
#[test]
fn test_filename() {
assert_eq!(InodeTable::filename("/"), "");
assert_eq!(InodeTable::filename("/foo"), "foo");
assert_eq!(InodeTable::filename("/foo/bar"), "bar");
assert_eq!(InodeTable::filename("/foo/bar.txt"), "bar.txt");
}
#[test]
fn test_remove() {
let mut table = InodeTable::new();
let inode = table.get_or_create("/foo");
assert!(table.get_inode("/foo").is_some());
table.remove(inode);
assert!(table.get_inode("/foo").is_none());
}
#[test]
fn test_rename() {
let mut table = InodeTable::new();
let inode = table.get_or_create("/old");
table.rename("/old", "/new");
assert!(table.get_inode("/old").is_none());
assert_eq!(table.get_inode("/new"), Some(inode));
assert_eq!(table.get_path(inode), Some("/new"));
}
#[test]
fn test_clear() {
let mut table = InodeTable::new();
table.get_or_create("/foo");
table.get_or_create("/bar");
table.clear();
assert_eq!(table.get_inode("/"), Some(InodeTable::ROOT_INODE));
assert!(table.get_inode("/foo").is_none());
assert!(table.get_inode("/bar").is_none());
}
}