use fuser::FUSE_ROOT_ID;
use std::collections::HashMap;
use std::hash::Hash;
#[derive(Clone, Debug)]
pub(crate) struct InodeRecord<K, T> {
pub(crate) ino: u64,
pub(crate) generation: u64,
pub(crate) lookup_count: u64,
pub(crate) key: K,
pub(crate) node: T,
}
#[derive(Debug)]
pub(crate) struct InodeTable<K, T> {
next_ino: u64,
records: HashMap<u64, InodeRecord<K, T>>,
by_key: HashMap<K, u64>,
}
impl<K, T> InodeTable<K, T>
where
K: Clone + Eq + Hash,
T: Clone,
{
pub(crate) fn new(root_key: K, root_node: T) -> Self {
let root_record = InodeRecord {
ino: FUSE_ROOT_ID,
generation: 0,
lookup_count: 1,
key: root_key.clone(),
node: root_node,
};
let mut records = HashMap::new();
records.insert(FUSE_ROOT_ID, root_record);
let mut by_key = HashMap::new();
by_key.insert(root_key, FUSE_ROOT_ID);
Self {
next_ino: FUSE_ROOT_ID + 1,
records,
by_key,
}
}
pub(crate) fn get(&self, ino: u64) -> Option<&InodeRecord<K, T>> {
self.records.get(&ino)
}
pub(crate) fn len(&self) -> usize {
self.records.len()
}
pub(crate) fn remember_lookup<F>(&mut self, key: K, create: F) -> Result<InodeRecord<K, T>, i32>
where
F: FnOnce() -> Result<T, i32>,
{
if let Some(existing) = self.by_key.get(&key).copied() {
if let Some(record) = self.records.get_mut(&existing) {
record.lookup_count = record.lookup_count.saturating_add(1);
return Ok(record.clone());
}
}
let node = create()?;
let ino = self.next_ino;
self.next_ino = self.next_ino.saturating_add(1);
let record = InodeRecord {
ino,
generation: 1,
lookup_count: 1,
key: key.clone(),
node,
};
self.by_key.insert(key, ino);
self.records.insert(ino, record.clone());
Ok(record)
}
pub(crate) fn remember_seen<F>(&mut self, key: K, create: F) -> Result<InodeRecord<K, T>, i32>
where
F: FnOnce() -> Result<T, i32>,
{
if let Some(existing) = self.by_key.get(&key).copied() {
if let Some(record) = self.records.get(&existing) {
return Ok(record.clone());
}
}
let node = create()?;
let ino = self.next_ino;
self.next_ino = self.next_ino.saturating_add(1);
let record = InodeRecord {
ino,
generation: 1,
lookup_count: 0,
key: key.clone(),
node,
};
self.by_key.insert(key, ino);
self.records.insert(ino, record.clone());
Ok(record)
}
pub(crate) fn forget(&mut self, ino: u64, nlookup: u64) {
if ino == FUSE_ROOT_ID {
return;
}
if let Some(record) = self.records.get_mut(&ino) {
record.lookup_count = record.lookup_count.saturating_sub(nlookup);
}
self.evict_unreferenced();
}
pub(crate) fn evict_unreferenced(&mut self) {
let doomed: Vec<(u64, K)> = self
.records
.iter()
.filter(|(ino, record)| **ino != FUSE_ROOT_ID && record.lookup_count == 0)
.map(|(ino, record)| (*ino, record.key.clone()))
.collect();
for (ino, key) in doomed {
self.records.remove(&ino);
self.by_key.remove(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::InodeTable;
#[test]
fn lookup_reuses_existing_inode_and_tracks_counts() {
let mut table = InodeTable::new(String::from("/"), String::from("root"));
let first = table
.remember_lookup(String::from("/a"), || Ok(String::from("a")))
.expect("lookup should allocate inode");
let second = table
.remember_lookup(String::from("/a"), || Ok(String::from("a")))
.expect("lookup should reuse inode");
assert_eq!(first.ino, second.ino);
assert_eq!(second.lookup_count, 2);
assert_eq!(second.generation, 1);
}
#[test]
fn forget_saturates_at_zero() {
let mut table = InodeTable::new(String::from("/"), String::from("root"));
let record = table
.remember_lookup(String::from("/a"), || Ok(String::from("a")))
.expect("lookup should allocate inode");
table.forget(record.ino, 2);
assert!(table.get(record.ino).is_none());
}
#[test]
fn readdir_seen_nodes_do_not_consume_lookup_counts() {
let mut table = InodeTable::new(String::from("/"), String::from("root"));
let record = table
.remember_seen(String::from("/a"), || Ok(String::from("a")))
.expect("readdir should remember inode");
assert_eq!(record.lookup_count, 0);
assert_eq!(table.len(), 2);
}
#[test]
fn evict_unreferenced_drops_readdir_only_entries() {
let mut table = InodeTable::new(String::from("/"), String::from("root"));
let record = table
.remember_seen(String::from("/a"), || Ok(String::from("a")))
.expect("readdir should remember inode");
table.evict_unreferenced();
assert!(table.get(record.ino).is_none());
assert_eq!(table.len(), 1);
}
}