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    /// Finds the parent inode of an entry identified by its MTP object handle.
202    /// Returns `None` if the handle is not in the table.
203    pub fn find_parent_by_handle(&self, handle: ObjectHandle) -> Option<u64> {
204        self.entries.values().find_map(|e| match &e.kind {
205            InodeKind::File { handle: h } | InodeKind::Directory { handle: h } if *h == handle => {
206                Some(e.parent)
207            }
208            _ => None,
209        })
210    }
211
212    /// Removes all children of the given parent (for cache invalidation).
213    pub fn clear_children(&mut self, parent_inode: u64) {
214        let child_inodes = self
215            .children_index
216            .remove(&parent_inode)
217            .unwrap_or_default();
218        for child_ino in child_inodes {
219            if let Some(entry) = self.entries.remove(&child_ino) {
220                self.name_index.remove(&(parent_inode, entry.name));
221            }
222            // Recursively clear grandchildren index entries (but not deeply).
223            self.children_index.remove(&child_ino);
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_new_has_root() {
234        let table = InodeTable::new();
235        let root = table.get(FUSE_ROOT_INODE).expect("root must exist");
236        assert_eq!(root.inode, FUSE_ROOT_INODE);
237        assert_eq!(root.kind, InodeKind::Root);
238        assert!(root.is_dir());
239    }
240
241    #[test]
242    fn test_add_storage() {
243        let mut table = InodeTable::new();
244        let ino = table.add_storage(StorageId(1), "Internal".into());
245        assert_eq!(ino, 2);
246
247        let entry = table.get(ino).unwrap();
248        assert_eq!(entry.name, "Internal");
249        assert_eq!(
250            entry.kind,
251            InodeKind::Storage {
252                storage_id: StorageId(1)
253            }
254        );
255        assert_eq!(entry.parent, FUSE_ROOT_INODE);
256        assert!(entry.is_dir());
257    }
258
259    #[test]
260    fn test_add_object_file() {
261        let mut table = InodeTable::new();
262        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
263        let mtime = SystemTime::UNIX_EPOCH;
264
265        let file_ino = table.add_object(
266            storage_ino,
267            ObjectHandle(100),
268            "photo.jpg".into(),
269            false,
270            4096,
271            mtime,
272        );
273
274        let entry = table.get(file_ino).unwrap();
275        assert_eq!(entry.name, "photo.jpg");
276        assert_eq!(
277            entry.kind,
278            InodeKind::File {
279                handle: ObjectHandle(100)
280            }
281        );
282        assert_eq!(entry.size, 4096);
283        assert_eq!(entry.parent, storage_ino);
284        assert!(!entry.is_dir());
285    }
286
287    #[test]
288    fn test_add_object_directory() {
289        let mut table = InodeTable::new();
290        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
291        let mtime = SystemTime::UNIX_EPOCH;
292
293        let dir_ino = table.add_object(
294            storage_ino,
295            ObjectHandle(200),
296            "DCIM".into(),
297            true,
298            0,
299            mtime,
300        );
301
302        let entry = table.get(dir_ino).unwrap();
303        assert_eq!(
304            entry.kind,
305            InodeKind::Directory {
306                handle: ObjectHandle(200)
307            }
308        );
309        assert!(entry.is_dir());
310    }
311
312    #[test]
313    fn test_lookup_by_name() {
314        let mut table = InodeTable::new();
315        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
316        let mtime = SystemTime::UNIX_EPOCH;
317        let file_ino = table.add_object(
318            storage_ino,
319            ObjectHandle(100),
320            "photo.jpg".into(),
321            false,
322            1024,
323            mtime,
324        );
325
326        assert_eq!(table.lookup(storage_ino, "photo.jpg"), Some(file_ino));
327        assert_eq!(table.lookup(FUSE_ROOT_INODE, "Internal"), Some(storage_ino));
328    }
329
330    #[test]
331    fn test_lookup_nonexistent() {
332        let table = InodeTable::new();
333        assert_eq!(table.lookup(FUSE_ROOT_INODE, "nope"), None);
334        assert!(table.get(999).is_none());
335    }
336
337    #[test]
338    fn test_children() {
339        let mut table = InodeTable::new();
340        let s1 = table.add_storage(StorageId(1), "Internal".into());
341        let s2 = table.add_storage(StorageId(2), "SD Card".into());
342
343        let root_children = table.children(FUSE_ROOT_INODE);
344        assert_eq!(root_children, vec![s1, s2]);
345
346        let mtime = SystemTime::UNIX_EPOCH;
347        let f1 = table.add_object(s1, ObjectHandle(10), "a.txt".into(), false, 100, mtime);
348        let f2 = table.add_object(s1, ObjectHandle(11), "b.txt".into(), false, 200, mtime);
349
350        let storage_children = table.children(s1);
351        assert_eq!(storage_children, vec![f1, f2]);
352
353        assert!(table.children(s2).is_empty());
354    }
355
356    #[test]
357    fn test_remove() {
358        let mut table = InodeTable::new();
359        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
360        let mtime = SystemTime::UNIX_EPOCH;
361        let file_ino = table.add_object(
362            storage_ino,
363            ObjectHandle(100),
364            "photo.jpg".into(),
365            false,
366            1024,
367            mtime,
368        );
369
370        let removed = table.remove(file_ino).expect("should remove");
371        assert_eq!(removed.name, "photo.jpg");
372        assert!(table.get(file_ino).is_none());
373        assert_eq!(table.lookup(storage_ino, "photo.jpg"), None);
374        assert!(table.children(storage_ino).is_empty());
375    }
376
377    #[test]
378    fn test_rename() {
379        let mut table = InodeTable::new();
380        let s1 = table.add_storage(StorageId(1), "Internal".into());
381        let mtime = SystemTime::UNIX_EPOCH;
382        let dir_ino = table.add_object(s1, ObjectHandle(200), "DCIM".into(), true, 0, mtime);
383        let file_ino = table.add_object(s1, ObjectHandle(100), "old.txt".into(), false, 512, mtime);
384
385        // Move file from storage root into DCIM and rename it.
386        table.rename(file_ino, dir_ino, "new.txt".into());
387
388        assert_eq!(table.lookup(s1, "old.txt"), None);
389        assert_eq!(table.lookup(dir_ino, "new.txt"), Some(file_ino));
390
391        let entry = table.get(file_ino).unwrap();
392        assert_eq!(entry.parent, dir_ino);
393        assert_eq!(entry.name, "new.txt");
394
395        assert!(!table.children(s1).contains(&file_ino));
396        assert!(table.children(dir_ino).contains(&file_ino));
397    }
398
399    #[test]
400    fn test_clear_children() {
401        let mut table = InodeTable::new();
402        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
403        let mtime = SystemTime::UNIX_EPOCH;
404        let f1 = table.add_object(
405            storage_ino,
406            ObjectHandle(10),
407            "a.txt".into(),
408            false,
409            100,
410            mtime,
411        );
412        let f2 = table.add_object(
413            storage_ino,
414            ObjectHandle(11),
415            "b.txt".into(),
416            false,
417            200,
418            mtime,
419        );
420
421        table.clear_children(storage_ino);
422
423        assert!(table.children(storage_ino).is_empty());
424        assert!(table.get(f1).is_none());
425        assert!(table.get(f2).is_none());
426        assert_eq!(table.lookup(storage_ino, "a.txt"), None);
427        assert_eq!(table.lookup(storage_ino, "b.txt"), None);
428        // The storage itself should still exist.
429        assert!(table.get(storage_ino).is_some());
430    }
431
432    #[test]
433    fn test_inode_uniqueness() {
434        let mut table = InodeTable::new();
435        let mtime = SystemTime::UNIX_EPOCH;
436        let mut inodes = vec![FUSE_ROOT_INODE];
437        inodes.push(table.add_storage(StorageId(1), "A".into()));
438        inodes.push(table.add_storage(StorageId(2), "B".into()));
439        inodes.push(table.add_object(inodes[1], ObjectHandle(1), "x".into(), false, 0, mtime));
440        inodes.push(table.add_object(inodes[1], ObjectHandle(2), "y".into(), true, 0, mtime));
441
442        let unique: std::collections::HashSet<u64> = inodes.iter().copied().collect();
443        assert_eq!(unique.len(), inodes.len(), "all inodes must be unique");
444    }
445
446    #[test]
447    fn test_nested_directories() {
448        let mut table = InodeTable::new();
449        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
450        let mtime = SystemTime::UNIX_EPOCH;
451
452        let dcim = table.add_object(storage_ino, ObjectHandle(1), "DCIM".into(), true, 0, mtime);
453        let camera = table.add_object(dcim, ObjectHandle(2), "Camera".into(), true, 0, mtime);
454        let photo = table.add_object(
455            camera,
456            ObjectHandle(3),
457            "IMG_001.jpg".into(),
458            false,
459            8192,
460            mtime,
461        );
462
463        // Verify the chain: root -> storage -> DCIM -> Camera -> photo
464        assert!(table.children(FUSE_ROOT_INODE).contains(&storage_ino));
465        assert!(table.children(storage_ino).contains(&dcim));
466        assert!(table.children(dcim).contains(&camera));
467        assert!(table.children(camera).contains(&photo));
468
469        // Lookup through the chain.
470        assert_eq!(table.lookup(FUSE_ROOT_INODE, "Internal"), Some(storage_ino));
471        assert_eq!(table.lookup(storage_ino, "DCIM"), Some(dcim));
472        assert_eq!(table.lookup(dcim, "Camera"), Some(camera));
473        assert_eq!(table.lookup(camera, "IMG_001.jpg"), Some(photo));
474
475        let photo_entry = table.get(photo).unwrap();
476        assert_eq!(photo_entry.parent, camera);
477        assert_eq!(photo_entry.size, 8192);
478    }
479
480    #[test]
481    fn test_get_mut() {
482        let mut table = InodeTable::new();
483        let storage_ino = table.add_storage(StorageId(1), "Internal".into());
484        let mtime = SystemTime::UNIX_EPOCH;
485        let file_ino = table.add_object(
486            storage_ino,
487            ObjectHandle(1),
488            "f.txt".into(),
489            false,
490            100,
491            mtime,
492        );
493
494        table.get_mut(file_ino).unwrap().size = 999;
495        assert_eq!(table.get(file_ino).unwrap().size, 999);
496    }
497}