snix_castore/fs/
inode_tracker.rs

1use std::{collections::HashMap, sync::Arc};
2
3use super::inodes::{DirectoryInodeData, InodeData};
4use crate::B3Digest;
5
6/// InodeTracker keeps track of inodes, stores data being these inodes and deals
7/// with inode allocation.
8pub struct InodeTracker {
9    data: HashMap<u64, Arc<InodeData>>,
10
11    // lookup table for blobs by their B3Digest
12    blob_digest_to_inode: HashMap<B3Digest, u64>,
13
14    // lookup table for symlinks by their target
15    symlink_target_to_inode: HashMap<bytes::Bytes, u64>,
16
17    // lookup table for directories by their B3Digest.
18    // Note the corresponding directory may not be present in data yet.
19    directory_digest_to_inode: HashMap<B3Digest, u64>,
20
21    // the next inode to allocate
22    next_inode: u64,
23}
24
25impl Default for InodeTracker {
26    fn default() -> Self {
27        Self {
28            data: Default::default(),
29
30            blob_digest_to_inode: Default::default(),
31            symlink_target_to_inode: Default::default(),
32            directory_digest_to_inode: Default::default(),
33
34            next_inode: 2,
35        }
36    }
37}
38
39impl InodeTracker {
40    // Retrieves data for a given inode, if it exists.
41    pub fn get(&self, ino: u64) -> Option<Arc<InodeData>> {
42        self.data.get(&ino).cloned()
43    }
44
45    // Replaces data for a given inode.
46    // Panics if the inode doesn't already exist.
47    pub fn replace(&mut self, ino: u64, data: Arc<InodeData>) {
48        if self.data.insert(ino, data).is_none() {
49            panic!("replace called on unknown inode");
50        }
51    }
52
53    // Stores data and returns the inode for it.
54    // In case an inode has already been allocated for the same data, that inode
55    // is returned, otherwise a new one is allocated.
56    // In case data is a [InodeData::Directory], inodes for all items are looked
57    // up
58    pub fn put(&mut self, data: InodeData) -> u64 {
59        match data {
60            InodeData::Regular(ref digest, _, _) => {
61                match self.blob_digest_to_inode.get(digest) {
62                    Some(found_ino) => {
63                        // We already have it, return the inode.
64                        *found_ino
65                    }
66                    None => self.insert_and_increment(data),
67                }
68            }
69            InodeData::Symlink(ref target) => {
70                match self.symlink_target_to_inode.get(target) {
71                    Some(found_ino) => {
72                        // We already have it, return the inode.
73                        *found_ino
74                    }
75                    None => self.insert_and_increment(data),
76                }
77            }
78            InodeData::Directory(DirectoryInodeData::Sparse(ref digest, _size)) => {
79                // check the lookup table if the B3Digest is known.
80                match self.directory_digest_to_inode.get(digest) {
81                    Some(found_ino) => {
82                        // We already have it, return the inode.
83                        *found_ino
84                    }
85                    None => {
86                        // insert and return the inode
87                        self.insert_and_increment(data)
88                    }
89                }
90            }
91            // Inserting [DirectoryInodeData::Populated] doesn't normally happen,
92            // only via [replace].
93            InodeData::Directory(DirectoryInodeData::Populated(..)) => {
94                unreachable!("should never be called with DirectoryInodeData::Populated")
95            }
96        }
97    }
98
99    // Inserts the data and returns the inode it was stored at, while
100    // incrementing next_inode.
101    fn insert_and_increment(&mut self, data: InodeData) -> u64 {
102        let ino = self.next_inode;
103        // insert into lookup tables
104        match data {
105            InodeData::Regular(ref digest, _, _) => {
106                self.blob_digest_to_inode.insert(digest.clone(), ino);
107            }
108            InodeData::Symlink(ref target) => {
109                self.symlink_target_to_inode.insert(target.clone(), ino);
110            }
111            InodeData::Directory(DirectoryInodeData::Sparse(ref digest, _size)) => {
112                self.directory_digest_to_inode.insert(digest.clone(), ino);
113            }
114            // This is currently not used outside test fixtures.
115            // Usually a [DirectoryInodeData::Sparse] is inserted and later
116            // "upgraded" with more data.
117            // However, as a future optimization, a lookup for a PathInfo could trigger a
118            // [DirectoryService::get_recursive()] request that "forks into
119            // background" and prepopulates all Directories in a closure.
120            InodeData::Directory(DirectoryInodeData::Populated(ref digest, _)) => {
121                self.directory_digest_to_inode.insert(digest.clone(), ino);
122            }
123        }
124        // Insert data
125        self.data.insert(ino, Arc::new(data));
126
127        // increment inode counter and return old inode.
128        self.next_inode += 1;
129        ino
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::fixtures;
136
137    use super::InodeData;
138    use super::InodeTracker;
139
140    /// Getting something non-existent should be none
141    #[test]
142    fn get_nonexistent() {
143        let inode_tracker = InodeTracker::default();
144        assert!(inode_tracker.get(1).is_none());
145    }
146
147    /// Put of a regular file should allocate a uid, which should be the same when inserting again.
148    #[test]
149    fn put_regular() {
150        let mut inode_tracker = InodeTracker::default();
151        let f = InodeData::Regular(
152            fixtures::BLOB_A_DIGEST.clone(),
153            fixtures::BLOB_A.len() as u64,
154            false,
155        );
156
157        // put it in
158        let ino = inode_tracker.put(f.clone());
159
160        // a get should return the right data
161        let data = inode_tracker.get(ino).expect("must be some");
162        match *data {
163            InodeData::Regular(ref digest, _, _) => {
164                assert_eq!(&fixtures::BLOB_A_DIGEST.clone(), digest);
165            }
166            InodeData::Symlink(_) | InodeData::Directory(..) => panic!("wrong type"),
167        }
168
169        // another put should return the same ino
170        assert_eq!(ino, inode_tracker.put(f));
171
172        // inserting another file should return a different ino
173        assert_ne!(
174            ino,
175            inode_tracker.put(InodeData::Regular(
176                fixtures::BLOB_B_DIGEST.clone(),
177                fixtures::BLOB_B.len() as u64,
178                false,
179            ))
180        );
181    }
182
183    // Put of a symlink should allocate a uid, which should be the same when inserting again
184    #[test]
185    fn put_symlink() {
186        let mut inode_tracker = InodeTracker::default();
187        let f = InodeData::Symlink("target".into());
188
189        // put it in
190        let ino = inode_tracker.put(f.clone());
191
192        // a get should return the right data
193        let data = inode_tracker.get(ino).expect("must be some");
194        match *data {
195            InodeData::Symlink(ref target) => {
196                assert_eq!(b"target".to_vec(), *target);
197            }
198            InodeData::Regular(..) | InodeData::Directory(..) => panic!("wrong type"),
199        }
200
201        // another put should return the same ino
202        assert_eq!(ino, inode_tracker.put(f));
203
204        // inserting another file should return a different ino
205        assert_ne!(ino, inode_tracker.put(InodeData::Symlink("target2".into())));
206    }
207}