use std::{collections::HashMap, sync::Arc}; use super::inodes::{DirectoryInodeData, InodeData}; use tvix_castore::B3Digest; /// InodeTracker keeps track of inodes, stores data being these inodes and deals /// with inode allocation. pub struct InodeTracker { data: HashMap>, // lookup table for blobs by their B3Digest blob_digest_to_inode: HashMap, // lookup table for symlinks by their target symlink_target_to_inode: HashMap, // lookup table for directories by their B3Digest. // Note the corresponding directory may not be present in data yet. directory_digest_to_inode: HashMap, // the next inode to allocate next_inode: u64, } impl Default for InodeTracker { fn default() -> Self { Self { data: Default::default(), blob_digest_to_inode: Default::default(), symlink_target_to_inode: Default::default(), directory_digest_to_inode: Default::default(), next_inode: 2, } } } impl InodeTracker { // Retrieves data for a given inode, if it exists. pub fn get(&self, ino: u64) -> Option> { self.data.get(&ino).cloned() } // Replaces data for a given inode. // Panics if the inode doesn't already exist. pub fn replace(&mut self, ino: u64, data: Arc) { if self.data.insert(ino, data).is_none() { panic!("replace called on unknown inode"); } } // Stores data and returns the inode for it. // In case an inode has already been allocated for the same data, that inode // is returned, otherwise a new one is allocated. // In case data is a [InodeData::Directory], inodes for all items are looked // up pub fn put(&mut self, data: InodeData) -> u64 { match data { InodeData::Regular(ref digest, _, _) => { match self.blob_digest_to_inode.get(digest) { Some(found_ino) => { // We already have it, return the inode. *found_ino } None => self.insert_and_increment(data), } } InodeData::Symlink(ref target) => { match self.symlink_target_to_inode.get(target) { Some(found_ino) => { // We already have it, return the inode. *found_ino } None => self.insert_and_increment(data), } } InodeData::Directory(DirectoryInodeData::Sparse(ref digest, _size)) => { // check the lookup table if the B3Digest is known. match self.directory_digest_to_inode.get(digest) { Some(found_ino) => { // We already have it, return the inode. *found_ino } None => { // insert and return the inode self.insert_and_increment(data) } } } // Inserting [DirectoryInodeData::Populated] doesn't normally happen, // only via [replace]. InodeData::Directory(DirectoryInodeData::Populated(..)) => { unreachable!("should never be called with DirectoryInodeData::Populated") } } } // Inserts the data and returns the inode it was stored at, while // incrementing next_inode. fn insert_and_increment(&mut self, data: InodeData) -> u64 { let ino = self.next_inode; // insert into lookup tables match data { InodeData::Regular(ref digest, _, _) => { self.blob_digest_to_inode.insert(digest.clone(), ino); } InodeData::Symlink(ref target) => { self.symlink_target_to_inode.insert(target.clone(), ino); } InodeData::Directory(DirectoryInodeData::Sparse(ref digest, _size)) => { self.directory_digest_to_inode.insert(digest.clone(), ino); } // This is currently not used outside test fixtures. // Usually a [DirectoryInodeData::Sparse] is inserted and later // "upgraded" with more data. // However, as a future optimization, a lookup for a PathInfo could trigger a // [DirectoryService::get_recursive()] request that "forks into // background" and prepopulates all Directories in a closure. InodeData::Directory(DirectoryInodeData::Populated(ref digest, _)) => { self.directory_digest_to_inode.insert(digest.clone(), ino); } } // Insert data self.data.insert(ino, Arc::new(data)); // increment inode counter and return old inode. self.next_inode += 1; ino } } #[cfg(test)] mod tests { use crate::tests::fixtures; use super::InodeData; use super::InodeTracker; /// Getting something non-existent should be none #[test] fn get_nonexistent() { let inode_tracker = InodeTracker::default(); assert!(inode_tracker.get(1).is_none()); } /// Put of a regular file should allocate a uid, which should be the same when inserting again. #[test] fn put_regular() { let mut inode_tracker = InodeTracker::default(); let f = InodeData::Regular( fixtures::BLOB_A_DIGEST.clone(), fixtures::BLOB_A.len() as u64, false, ); // put it in let ino = inode_tracker.put(f.clone()); // a get should return the right data let data = inode_tracker.get(ino).expect("must be some"); match *data { InodeData::Regular(ref digest, _, _) => { assert_eq!(&fixtures::BLOB_A_DIGEST.clone(), digest); } InodeData::Symlink(_) | InodeData::Directory(..) => panic!("wrong type"), } // another put should return the same ino assert_eq!(ino, inode_tracker.put(f)); // inserting another file should return a different ino assert_ne!( ino, inode_tracker.put(InodeData::Regular( fixtures::BLOB_B_DIGEST.clone(), fixtures::BLOB_B.len() as u64, false, )) ); } // Put of a symlink should allocate a uid, which should be the same when inserting again #[test] fn put_symlink() { let mut inode_tracker = InodeTracker::default(); let f = InodeData::Symlink("target".into()); // put it in let ino = inode_tracker.put(f.clone()); // a get should return the right data let data = inode_tracker.get(ino).expect("must be some"); match *data { InodeData::Symlink(ref target) => { assert_eq!(b"target".to_vec(), *target); } InodeData::Regular(..) | InodeData::Directory(..) => panic!("wrong type"), } // another put should return the same ino assert_eq!(ino, inode_tracker.put(f)); // inserting another file should return a different ino assert_ne!(ino, inode_tracker.put(InodeData::Symlink("target2".into()))); } }