Skip to main content

mtp_mount/
inode.rs

1use std::collections::HashMap;
2use std::time::SystemTime;
3
4use mtp_rs::{ObjectHandle, StorageId};
5
6/// FUSE root inode number.
7pub const FUSE_ROOT_INODE: u64 = 1;
8
9/// What kind of entry an inode represents.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum InodeKind {
12    Root,
13    Storage { storage_id: StorageId },
14    Directory { handle: ObjectHandle },
15    File { handle: ObjectHandle },
16}
17
18/// Metadata cached for a single inode.
19#[derive(Debug, Clone)]
20pub struct InodeEntry {
21    pub inode: u64,
22    pub parent: u64,
23    pub name: String,
24    pub kind: InodeKind,
25    pub size: u64,
26    pub mtime: SystemTime,
27    pub atime: SystemTime,
28}
29
30impl InodeEntry {
31    pub fn is_dir(&self) -> bool {
32        matches!(
33            self.kind,
34            InodeKind::Root | InodeKind::Storage { .. } | InodeKind::Directory { .. }
35        )
36    }
37}
38
39/// Bidirectional mapping between FUSE inodes and MTP objects, with cached metadata.
40#[derive(Debug)]
41pub struct InodeTable {
42    entries: HashMap<u64, InodeEntry>,
43    /// (parent_inode, child_name) -> child_inode
44    name_index: HashMap<(u64, String), u64>,
45    /// parent_inode -> list of child inodes
46    children_index: HashMap<u64, Vec<u64>>,
47    next_inode: u64,
48}
49
50impl Default for InodeTable {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl InodeTable {
57    /// Creates a new table with only the root inode (inode 1).
58    pub fn new() -> Self {
59        let root = InodeEntry {
60            inode: FUSE_ROOT_INODE,
61            parent: FUSE_ROOT_INODE,
62            name: String::new(),
63            kind: InodeKind::Root,
64            size: 0,
65            mtime: SystemTime::UNIX_EPOCH,
66            atime: SystemTime::UNIX_EPOCH,
67        };
68        let mut entries = HashMap::new();
69        entries.insert(FUSE_ROOT_INODE, root);
70
71        Self {
72            entries,
73            name_index: HashMap::new(),
74            children_index: HashMap::new(),
75            next_inode: 2,
76        }
77    }
78
79    fn alloc_inode(&mut self) -> u64 {
80        let ino = self.next_inode;
81        self.next_inode += 1;
82        ino
83    }
84
85    fn insert(&mut self, entry: InodeEntry) -> u64 {
86        let ino = entry.inode;
87        let parent = entry.parent;
88        let name = entry.name.clone();
89
90        self.entries.insert(ino, entry);
91        self.name_index.insert((parent, name), ino);
92        self.children_index.entry(parent).or_default().push(ino);
93        ino
94    }
95
96    /// Adds a storage as a child of root. Returns the new inode number.
97    pub fn add_storage(&mut self, storage_id: StorageId, name: String) -> u64 {
98        let ino = self.alloc_inode();
99        let now = SystemTime::now();
100        self.insert(InodeEntry {
101            inode: ino,
102            parent: FUSE_ROOT_INODE,
103            name,
104            kind: InodeKind::Storage { storage_id },
105            size: 0,
106            mtime: now,
107            atime: now,
108        })
109    }
110
111    /// Adds a file or directory under the given parent. Returns the new inode number.
112    pub fn add_object(
113        &mut self,
114        parent_inode: u64,
115        handle: ObjectHandle,
116        name: String,
117        is_dir: bool,
118        size: u64,
119        mtime: SystemTime,
120    ) -> u64 {
121        let ino = self.alloc_inode();
122        let kind = if is_dir {
123            InodeKind::Directory { handle }
124        } else {
125            InodeKind::File { handle }
126        };
127        self.insert(InodeEntry {
128            inode: ino,
129            parent: parent_inode,
130            name,
131            kind,
132            size,
133            mtime,
134            atime: mtime,
135        })
136    }
137
138    /// Looks up an entry by inode number.
139    pub fn get(&self, inode: u64) -> Option<&InodeEntry> {
140        self.entries.get(&inode)
141    }
142
143    /// Mutable lookup by inode number.
144    pub fn get_mut(&mut self, inode: u64) -> Option<&mut InodeEntry> {
145        self.entries.get_mut(&inode)
146    }
147
148    /// Finds a child inode by parent inode and name.
149    pub fn lookup(&self, parent_inode: u64, name: &str) -> Option<u64> {
150        self.name_index
151            .get(&(parent_inode, name.to_string()))
152            .copied()
153    }
154
155    /// Returns the inodes of all children of the given parent.
156    pub fn children(&self, parent_inode: u64) -> Vec<u64> {
157        self.children_index
158            .get(&parent_inode)
159            .cloned()
160            .unwrap_or_default()
161    }
162
163    /// Removes an entry and its index entries. Does not remove descendants.
164    pub fn remove(&mut self, inode: u64) -> Option<InodeEntry> {
165        let entry = self.entries.remove(&inode)?;
166        self.name_index.remove(&(entry.parent, entry.name.clone()));
167        if let Some(siblings) = self.children_index.get_mut(&entry.parent) {
168            siblings.retain(|&i| i != inode);
169        }
170        // Also remove any children index for this inode (but not the children themselves).
171        self.children_index.remove(&inode);
172        Some(entry)
173    }
174
175    /// Updates an entry's parent and name (for rename/move operations).
176    pub fn rename(&mut self, inode: u64, new_parent: u64, new_name: String) {
177        let Some(entry) = self.entries.get_mut(&inode) else {
178            return;
179        };
180        let old_parent = entry.parent;
181        let old_name = entry.name.clone();
182
183        // Update the entry itself.
184        entry.parent = new_parent;
185        entry.name = new_name.clone();
186
187        // Update name index.
188        self.name_index.remove(&(old_parent, old_name));
189        self.name_index.insert((new_parent, new_name), inode);
190
191        // Update children index.
192        if let Some(siblings) = self.children_index.get_mut(&old_parent) {
193            siblings.retain(|&i| i != inode);
194        }
195        self.children_index
196            .entry(new_parent)
197            .or_default()
198            .push(inode);
199    }
200
201    /// Removes all children of the given parent (for cache invalidation).
202    pub fn clear_children(&mut self, parent_inode: u64) {
203        let child_inodes = self
204            .children_index
205            .remove(&parent_inode)
206            .unwrap_or_default();
207        for child_ino in child_inodes {
208            if let Some(entry) = self.entries.remove(&child_ino) {
209                self.name_index.remove(&(parent_inode, entry.name));
210            }
211            // Recursively clear grandchildren index entries (but not deeply).
212            self.children_index.remove(&child_ino);
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_new_has_root() {
223        let table = InodeTable::new();
224        let root = table.get(FUSE_ROOT_INODE).expect("root must exist");
225        assert_eq!(root.inode, FUSE_ROOT_INODE);
226        assert_eq!(root.kind, InodeKind::Root);
227        assert!(root.is_dir());
228    }
229
230    #[test]
231    fn test_add_storage() {
232        let mut table = InodeTable::new();
233        let ino = table.add_storage(StorageId(1), "Internal".into());
234        assert_eq!(ino, 2);
235
236        let entry = table.get(ino).unwrap();
237        assert_eq!(entry.name, "Internal");
238        assert_eq!(
239            entry.kind,
240            InodeKind::Storage {
241                storage_id: StorageId(1)
242            }
243        );
244        assert_eq!(entry.parent, FUSE_ROOT_INODE);
245        assert!(entry.is_dir());
246    }
247
248    #[test]
249    fn test_add_object_file() {
250        let mut table = InodeTable::new();
251        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
252        let mtime = SystemTime::UNIX_EPOCH;
253
254        let file_ino = table.add_object(
255            storage_ino,
256            ObjectHandle(100),
257            "photo.jpg".into(),
258            false,
259            4096,
260            mtime,
261        );
262
263        let entry = table.get(file_ino).unwrap();
264        assert_eq!(entry.name, "photo.jpg");
265        assert_eq!(
266            entry.kind,
267            InodeKind::File {
268                handle: ObjectHandle(100)
269            }
270        );
271        assert_eq!(entry.size, 4096);
272        assert_eq!(entry.parent, storage_ino);
273        assert!(!entry.is_dir());
274    }
275
276    #[test]
277    fn test_add_object_directory() {
278        let mut table = InodeTable::new();
279        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
280        let mtime = SystemTime::UNIX_EPOCH;
281
282        let dir_ino = table.add_object(
283            storage_ino,
284            ObjectHandle(200),
285            "DCIM".into(),
286            true,
287            0,
288            mtime,
289        );
290
291        let entry = table.get(dir_ino).unwrap();
292        assert_eq!(
293            entry.kind,
294            InodeKind::Directory {
295                handle: ObjectHandle(200)
296            }
297        );
298        assert!(entry.is_dir());
299    }
300
301    #[test]
302    fn test_lookup_by_name() {
303        let mut table = InodeTable::new();
304        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
305        let mtime = SystemTime::UNIX_EPOCH;
306        let file_ino = table.add_object(
307            storage_ino,
308            ObjectHandle(100),
309            "photo.jpg".into(),
310            false,
311            1024,
312            mtime,
313        );
314
315        assert_eq!(table.lookup(storage_ino, "photo.jpg"), Some(file_ino));
316        assert_eq!(table.lookup(FUSE_ROOT_INODE, "Internal"), Some(storage_ino));
317    }
318
319    #[test]
320    fn test_lookup_nonexistent() {
321        let table = InodeTable::new();
322        assert_eq!(table.lookup(FUSE_ROOT_INODE, "nope"), None);
323        assert!(table.get(999).is_none());
324    }
325
326    #[test]
327    fn test_children() {
328        let mut table = InodeTable::new();
329        let s1 = table.add_storage(StorageId(1), "Internal".into());
330        let s2 = table.add_storage(StorageId(2), "SD Card".into());
331
332        let root_children = table.children(FUSE_ROOT_INODE);
333        assert_eq!(root_children, vec![s1, s2]);
334
335        let mtime = SystemTime::UNIX_EPOCH;
336        let f1 = table.add_object(s1, ObjectHandle(10), "a.txt".into(), false, 100, mtime);
337        let f2 = table.add_object(s1, ObjectHandle(11), "b.txt".into(), false, 200, mtime);
338
339        let storage_children = table.children(s1);
340        assert_eq!(storage_children, vec![f1, f2]);
341
342        assert!(table.children(s2).is_empty());
343    }
344
345    #[test]
346    fn test_remove() {
347        let mut table = InodeTable::new();
348        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
349        let mtime = SystemTime::UNIX_EPOCH;
350        let file_ino = table.add_object(
351            storage_ino,
352            ObjectHandle(100),
353            "photo.jpg".into(),
354            false,
355            1024,
356            mtime,
357        );
358
359        let removed = table.remove(file_ino).expect("should remove");
360        assert_eq!(removed.name, "photo.jpg");
361        assert!(table.get(file_ino).is_none());
362        assert_eq!(table.lookup(storage_ino, "photo.jpg"), None);
363        assert!(table.children(storage_ino).is_empty());
364    }
365
366    #[test]
367    fn test_rename() {
368        let mut table = InodeTable::new();
369        let s1 = table.add_storage(StorageId(1), "Internal".into());
370        let mtime = SystemTime::UNIX_EPOCH;
371        let dir_ino = table.add_object(s1, ObjectHandle(200), "DCIM".into(), true, 0, mtime);
372        let file_ino = table.add_object(s1, ObjectHandle(100), "old.txt".into(), false, 512, mtime);
373
374        // Move file from storage root into DCIM and rename it.
375        table.rename(file_ino, dir_ino, "new.txt".into());
376
377        assert_eq!(table.lookup(s1, "old.txt"), None);
378        assert_eq!(table.lookup(dir_ino, "new.txt"), Some(file_ino));
379
380        let entry = table.get(file_ino).unwrap();
381        assert_eq!(entry.parent, dir_ino);
382        assert_eq!(entry.name, "new.txt");
383
384        assert!(!table.children(s1).contains(&file_ino));
385        assert!(table.children(dir_ino).contains(&file_ino));
386    }
387
388    #[test]
389    fn test_clear_children() {
390        let mut table = InodeTable::new();
391        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
392        let mtime = SystemTime::UNIX_EPOCH;
393        let f1 = table.add_object(
394            storage_ino,
395            ObjectHandle(10),
396            "a.txt".into(),
397            false,
398            100,
399            mtime,
400        );
401        let f2 = table.add_object(
402            storage_ino,
403            ObjectHandle(11),
404            "b.txt".into(),
405            false,
406            200,
407            mtime,
408        );
409
410        table.clear_children(storage_ino);
411
412        assert!(table.children(storage_ino).is_empty());
413        assert!(table.get(f1).is_none());
414        assert!(table.get(f2).is_none());
415        assert_eq!(table.lookup(storage_ino, "a.txt"), None);
416        assert_eq!(table.lookup(storage_ino, "b.txt"), None);
417        // The storage itself should still exist.
418        assert!(table.get(storage_ino).is_some());
419    }
420
421    #[test]
422    fn test_inode_uniqueness() {
423        let mut table = InodeTable::new();
424        let mtime = SystemTime::UNIX_EPOCH;
425        let mut inodes = vec![FUSE_ROOT_INODE];
426        inodes.push(table.add_storage(StorageId(1), "A".into()));
427        inodes.push(table.add_storage(StorageId(2), "B".into()));
428        inodes.push(table.add_object(inodes[1], ObjectHandle(1), "x".into(), false, 0, mtime));
429        inodes.push(table.add_object(inodes[1], ObjectHandle(2), "y".into(), true, 0, mtime));
430
431        let unique: std::collections::HashSet<u64> = inodes.iter().copied().collect();
432        assert_eq!(unique.len(), inodes.len(), "all inodes must be unique");
433    }
434
435    #[test]
436    fn test_nested_directories() {
437        let mut table = InodeTable::new();
438        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
439        let mtime = SystemTime::UNIX_EPOCH;
440
441        let dcim = table.add_object(storage_ino, ObjectHandle(1), "DCIM".into(), true, 0, mtime);
442        let camera = table.add_object(dcim, ObjectHandle(2), "Camera".into(), true, 0, mtime);
443        let photo = table.add_object(
444            camera,
445            ObjectHandle(3),
446            "IMG_001.jpg".into(),
447            false,
448            8192,
449            mtime,
450        );
451
452        // Verify the chain: root -> storage -> DCIM -> Camera -> photo
453        assert!(table.children(FUSE_ROOT_INODE).contains(&storage_ino));
454        assert!(table.children(storage_ino).contains(&dcim));
455        assert!(table.children(dcim).contains(&camera));
456        assert!(table.children(camera).contains(&photo));
457
458        // Lookup through the chain.
459        assert_eq!(table.lookup(FUSE_ROOT_INODE, "Internal"), Some(storage_ino));
460        assert_eq!(table.lookup(storage_ino, "DCIM"), Some(dcim));
461        assert_eq!(table.lookup(dcim, "Camera"), Some(camera));
462        assert_eq!(table.lookup(camera, "IMG_001.jpg"), Some(photo));
463
464        let photo_entry = table.get(photo).unwrap();
465        assert_eq!(photo_entry.parent, camera);
466        assert_eq!(photo_entry.size, 8192);
467    }
468
469    #[test]
470    fn test_get_mut() {
471        let mut table = InodeTable::new();
472        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
473        let mtime = SystemTime::UNIX_EPOCH;
474        let file_ino = table.add_object(
475            storage_ino,
476            ObjectHandle(1),
477            "f.txt".into(),
478            false,
479            100,
480            mtime,
481        );
482
483        table.get_mut(file_ino).unwrap().size = 999;
484        assert_eq!(table.get(file_ino).unwrap().size, 999);
485    }
486}