1use std::collections::HashMap;
2use std::time::SystemTime;
3
4use mtp_rs::{ObjectHandle, StorageId};
5
6pub const FUSE_ROOT_INODE: u64 = 1;
8
9#[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#[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 pub generation: u64,
32}
33
34impl InodeEntry {
35 pub fn is_dir(&self) -> bool {
36 matches!(
37 self.kind,
38 InodeKind::Root | InodeKind::Storage { .. } | InodeKind::Directory { .. }
39 )
40 }
41}
42
43#[derive(Debug)]
45pub struct InodeTable {
46 entries: HashMap<u64, InodeEntry>,
47 name_index: HashMap<(u64, String), u64>,
49 children_index: HashMap<u64, Vec<u64>>,
51 next_inode: u64,
52 generation: u64,
54}
55
56#[derive(Debug, Clone)]
58pub struct ChildInfo {
59 pub handle: ObjectHandle,
60 pub name: String,
61 pub is_dir: bool,
62 pub size: u64,
63 pub mtime: SystemTime,
64}
65
66impl Default for InodeTable {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl InodeTable {
73 pub fn new() -> Self {
75 let root = InodeEntry {
76 inode: FUSE_ROOT_INODE,
77 parent: FUSE_ROOT_INODE,
78 name: String::new(),
79 kind: InodeKind::Root,
80 size: 0,
81 mtime: SystemTime::UNIX_EPOCH,
82 atime: SystemTime::UNIX_EPOCH,
83 generation: 0,
84 };
85 let mut entries = HashMap::new();
86 entries.insert(FUSE_ROOT_INODE, root);
87
88 Self {
89 entries,
90 name_index: HashMap::new(),
91 children_index: HashMap::new(),
92 next_inode: 2,
93 generation: 0,
94 }
95 }
96
97 pub fn bump_generation(&mut self) {
102 self.generation += 1;
103 }
104
105 pub fn is_fresh(&self, inode: u64) -> bool {
109 match self.entries.get(&inode) {
110 Some(entry) => match entry.kind {
111 InodeKind::Root | InodeKind::Storage { .. } => true,
112 InodeKind::Directory { .. } | InodeKind::File { .. } => {
113 entry.generation == self.generation
114 }
115 },
116 None => false,
117 }
118 }
119
120 pub fn set_handle(&mut self, inode: u64, handle: ObjectHandle) {
122 let generation = self.generation;
123 let Some(entry) = self.entries.get_mut(&inode) else {
124 return;
125 };
126 entry.kind = match entry.kind {
127 InodeKind::Directory { .. } => InodeKind::Directory { handle },
128 InodeKind::File { .. } => InodeKind::File { handle },
129 _ => return,
130 };
131 entry.generation = generation;
132 }
133
134 pub fn set_storage_id(&mut self, inode: u64, storage_id: StorageId) {
136 if let Some(entry) = self.entries.get_mut(&inode) {
137 if matches!(entry.kind, InodeKind::Storage { .. }) {
138 entry.kind = InodeKind::Storage { storage_id };
139 }
140 }
141 }
142
143 fn alloc_inode(&mut self) -> u64 {
144 let ino = self.next_inode;
145 self.next_inode += 1;
146 ino
147 }
148
149 fn insert(&mut self, entry: InodeEntry) -> u64 {
150 let ino = entry.inode;
151 let parent = entry.parent;
152 let name = entry.name.clone();
153
154 self.entries.insert(ino, entry);
155 self.name_index.insert((parent, name), ino);
156 self.children_index.entry(parent).or_default().push(ino);
157 ino
158 }
159
160 pub fn add_storage(&mut self, storage_id: StorageId, name: String) -> u64 {
162 let ino = self.alloc_inode();
163 let now = SystemTime::now();
164 self.insert(InodeEntry {
165 inode: ino,
166 parent: FUSE_ROOT_INODE,
167 name,
168 kind: InodeKind::Storage { storage_id },
169 size: 0,
170 mtime: now,
171 atime: now,
172 generation: self.generation,
173 })
174 }
175
176 pub fn add_object(
178 &mut self,
179 parent_inode: u64,
180 handle: ObjectHandle,
181 name: String,
182 is_dir: bool,
183 size: u64,
184 mtime: SystemTime,
185 ) -> u64 {
186 let ino = self.alloc_inode();
187 let kind = if is_dir {
188 InodeKind::Directory { handle }
189 } else {
190 InodeKind::File { handle }
191 };
192 self.insert(InodeEntry {
193 inode: ino,
194 parent: parent_inode,
195 name,
196 kind,
197 size,
198 mtime,
199 atime: mtime,
200 generation: self.generation,
201 })
202 }
203
204 pub fn sync_children(&mut self, parent_inode: u64, children: &[ChildInfo]) {
214 let generation = self.generation;
215
216 for child in children {
217 match self.lookup(parent_inode, &child.name) {
218 Some(ino)
219 if self
220 .entries
221 .get(&ino)
222 .is_some_and(|e| e.is_dir() == child.is_dir) =>
223 {
224 let entry = self.entries.get_mut(&ino).expect("looked up above");
225 entry.kind = if child.is_dir {
226 InodeKind::Directory {
227 handle: child.handle,
228 }
229 } else {
230 InodeKind::File {
231 handle: child.handle,
232 }
233 };
234 entry.size = child.size;
235 entry.mtime = child.mtime;
236 entry.generation = generation;
237 }
238 Some(ino) => {
241 self.remove(ino);
242 self.add_object(
243 parent_inode,
244 child.handle,
245 child.name.clone(),
246 child.is_dir,
247 child.size,
248 child.mtime,
249 );
250 }
251 None => {
252 self.add_object(
253 parent_inode,
254 child.handle,
255 child.name.clone(),
256 child.is_dir,
257 child.size,
258 child.mtime,
259 );
260 }
261 }
262 }
263
264 let gone: Vec<u64> = self
265 .children(parent_inode)
266 .into_iter()
267 .filter(|ino| {
268 self.entries
269 .get(ino)
270 .is_some_and(|e| !children.iter().any(|c| c.name == e.name))
271 })
272 .collect();
273 for ino in gone {
274 self.remove(ino);
275 }
276 }
277
278 pub fn get(&self, inode: u64) -> Option<&InodeEntry> {
280 self.entries.get(&inode)
281 }
282
283 pub fn get_mut(&mut self, inode: u64) -> Option<&mut InodeEntry> {
285 self.entries.get_mut(&inode)
286 }
287
288 pub fn lookup(&self, parent_inode: u64, name: &str) -> Option<u64> {
290 self.name_index
291 .get(&(parent_inode, name.to_string()))
292 .copied()
293 }
294
295 pub fn children(&self, parent_inode: u64) -> Vec<u64> {
297 self.children_index
298 .get(&parent_inode)
299 .cloned()
300 .unwrap_or_default()
301 }
302
303 pub fn remove(&mut self, inode: u64) -> Option<InodeEntry> {
305 let entry = self.entries.remove(&inode)?;
306 self.name_index.remove(&(entry.parent, entry.name.clone()));
307 if let Some(siblings) = self.children_index.get_mut(&entry.parent) {
308 siblings.retain(|&i| i != inode);
309 }
310 self.children_index.remove(&inode);
312 Some(entry)
313 }
314
315 pub fn rename(&mut self, inode: u64, new_parent: u64, new_name: String) {
317 let Some(entry) = self.entries.get_mut(&inode) else {
318 return;
319 };
320 let old_parent = entry.parent;
321 let old_name = entry.name.clone();
322
323 entry.parent = new_parent;
325 entry.name = new_name.clone();
326
327 self.name_index.remove(&(old_parent, old_name));
329 self.name_index.insert((new_parent, new_name), inode);
330
331 if let Some(siblings) = self.children_index.get_mut(&old_parent) {
333 siblings.retain(|&i| i != inode);
334 }
335 self.children_index
336 .entry(new_parent)
337 .or_default()
338 .push(inode);
339 }
340
341 pub fn find_parent_by_handle(&self, handle: ObjectHandle) -> Option<u64> {
344 self.entries.values().find_map(|e| match &e.kind {
345 InodeKind::File { handle: h } | InodeKind::Directory { handle: h } if *h == handle => {
346 Some(e.parent)
347 }
348 _ => None,
349 })
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_new_has_root() {
359 let table = InodeTable::new();
360 let root = table.get(FUSE_ROOT_INODE).expect("root must exist");
361 assert_eq!(root.inode, FUSE_ROOT_INODE);
362 assert_eq!(root.kind, InodeKind::Root);
363 assert!(root.is_dir());
364 }
365
366 #[test]
367 fn test_add_storage() {
368 let mut table = InodeTable::new();
369 let ino = table.add_storage(StorageId(1), "Internal".into());
370 assert_eq!(ino, 2);
371
372 let entry = table.get(ino).unwrap();
373 assert_eq!(entry.name, "Internal");
374 assert_eq!(
375 entry.kind,
376 InodeKind::Storage {
377 storage_id: StorageId(1)
378 }
379 );
380 assert_eq!(entry.parent, FUSE_ROOT_INODE);
381 assert!(entry.is_dir());
382 }
383
384 #[test]
385 fn test_add_object_file() {
386 let mut table = InodeTable::new();
387 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
388 let mtime = SystemTime::UNIX_EPOCH;
389
390 let file_ino = table.add_object(
391 storage_ino,
392 ObjectHandle(100),
393 "photo.jpg".into(),
394 false,
395 4096,
396 mtime,
397 );
398
399 let entry = table.get(file_ino).unwrap();
400 assert_eq!(entry.name, "photo.jpg");
401 assert_eq!(
402 entry.kind,
403 InodeKind::File {
404 handle: ObjectHandle(100)
405 }
406 );
407 assert_eq!(entry.size, 4096);
408 assert_eq!(entry.parent, storage_ino);
409 assert!(!entry.is_dir());
410 }
411
412 #[test]
413 fn test_add_object_directory() {
414 let mut table = InodeTable::new();
415 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
416 let mtime = SystemTime::UNIX_EPOCH;
417
418 let dir_ino = table.add_object(
419 storage_ino,
420 ObjectHandle(200),
421 "DCIM".into(),
422 true,
423 0,
424 mtime,
425 );
426
427 let entry = table.get(dir_ino).unwrap();
428 assert_eq!(
429 entry.kind,
430 InodeKind::Directory {
431 handle: ObjectHandle(200)
432 }
433 );
434 assert!(entry.is_dir());
435 }
436
437 #[test]
438 fn test_lookup_by_name() {
439 let mut table = InodeTable::new();
440 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
441 let mtime = SystemTime::UNIX_EPOCH;
442 let file_ino = table.add_object(
443 storage_ino,
444 ObjectHandle(100),
445 "photo.jpg".into(),
446 false,
447 1024,
448 mtime,
449 );
450
451 assert_eq!(table.lookup(storage_ino, "photo.jpg"), Some(file_ino));
452 assert_eq!(table.lookup(FUSE_ROOT_INODE, "Internal"), Some(storage_ino));
453 }
454
455 #[test]
456 fn test_lookup_nonexistent() {
457 let table = InodeTable::new();
458 assert_eq!(table.lookup(FUSE_ROOT_INODE, "nope"), None);
459 assert!(table.get(999).is_none());
460 }
461
462 #[test]
463 fn test_children() {
464 let mut table = InodeTable::new();
465 let s1 = table.add_storage(StorageId(1), "Internal".into());
466 let s2 = table.add_storage(StorageId(2), "SD Card".into());
467
468 let root_children = table.children(FUSE_ROOT_INODE);
469 assert_eq!(root_children, vec![s1, s2]);
470
471 let mtime = SystemTime::UNIX_EPOCH;
472 let f1 = table.add_object(s1, ObjectHandle(10), "a.txt".into(), false, 100, mtime);
473 let f2 = table.add_object(s1, ObjectHandle(11), "b.txt".into(), false, 200, mtime);
474
475 let storage_children = table.children(s1);
476 assert_eq!(storage_children, vec![f1, f2]);
477
478 assert!(table.children(s2).is_empty());
479 }
480
481 #[test]
482 fn test_remove() {
483 let mut table = InodeTable::new();
484 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
485 let mtime = SystemTime::UNIX_EPOCH;
486 let file_ino = table.add_object(
487 storage_ino,
488 ObjectHandle(100),
489 "photo.jpg".into(),
490 false,
491 1024,
492 mtime,
493 );
494
495 let removed = table.remove(file_ino).expect("should remove");
496 assert_eq!(removed.name, "photo.jpg");
497 assert!(table.get(file_ino).is_none());
498 assert_eq!(table.lookup(storage_ino, "photo.jpg"), None);
499 assert!(table.children(storage_ino).is_empty());
500 }
501
502 #[test]
503 fn test_rename() {
504 let mut table = InodeTable::new();
505 let s1 = table.add_storage(StorageId(1), "Internal".into());
506 let mtime = SystemTime::UNIX_EPOCH;
507 let dir_ino = table.add_object(s1, ObjectHandle(200), "DCIM".into(), true, 0, mtime);
508 let file_ino = table.add_object(s1, ObjectHandle(100), "old.txt".into(), false, 512, mtime);
509
510 table.rename(file_ino, dir_ino, "new.txt".into());
512
513 assert_eq!(table.lookup(s1, "old.txt"), None);
514 assert_eq!(table.lookup(dir_ino, "new.txt"), Some(file_ino));
515
516 let entry = table.get(file_ino).unwrap();
517 assert_eq!(entry.parent, dir_ino);
518 assert_eq!(entry.name, "new.txt");
519
520 assert!(!table.children(s1).contains(&file_ino));
521 assert!(table.children(dir_ino).contains(&file_ino));
522 }
523
524 fn child(handle: u64, name: &str, is_dir: bool, size: u64) -> ChildInfo {
525 ChildInfo {
526 handle: ObjectHandle(handle),
527 name: name.into(),
528 is_dir,
529 size,
530 mtime: SystemTime::UNIX_EPOCH,
531 }
532 }
533
534 #[test]
535 fn test_sync_children_keeps_inode_numbers_stable() {
536 let mut table = InodeTable::new();
537 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
538 table.sync_children(
539 storage_ino,
540 &[
541 child(10, "a.txt", false, 100),
542 child(11, "b.txt", false, 200),
543 ],
544 );
545 let a = table.lookup(storage_ino, "a.txt").unwrap();
546
547 table.sync_children(
550 storage_ino,
551 &[
552 child(77, "a.txt", false, 150),
553 child(78, "b.txt", false, 200),
554 ],
555 );
556
557 assert_eq!(table.lookup(storage_ino, "a.txt"), Some(a));
558 let entry = table.get(a).unwrap();
559 assert_eq!(
560 entry.kind,
561 InodeKind::File {
562 handle: ObjectHandle(77)
563 }
564 );
565 assert_eq!(entry.size, 150);
566 }
567
568 #[test]
569 fn test_sync_children_adds_and_removes() {
570 let mut table = InodeTable::new();
571 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
572 table.sync_children(
573 storage_ino,
574 &[
575 child(10, "stays.txt", false, 1),
576 child(11, "goes.txt", false, 2),
577 ],
578 );
579 let goes = table.lookup(storage_ino, "goes.txt").unwrap();
580
581 table.sync_children(
582 storage_ino,
583 &[
584 child(10, "stays.txt", false, 1),
585 child(12, "new.txt", false, 3),
586 ],
587 );
588
589 assert!(table.get(goes).is_none());
590 assert_eq!(table.lookup(storage_ino, "goes.txt"), None);
591 assert!(table.lookup(storage_ino, "new.txt").is_some());
592 assert_eq!(table.children(storage_ino).len(), 2);
593 }
594
595 #[test]
596 fn test_sync_children_replaces_when_kind_flips() {
597 let mut table = InodeTable::new();
598 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
599 table.sync_children(storage_ino, &[child(10, "thing", false, 5)]);
600 let file_ino = table.lookup(storage_ino, "thing").unwrap();
601
602 table.sync_children(storage_ino, &[child(11, "thing", true, 0)]);
603
604 let dir_ino = table.lookup(storage_ino, "thing").unwrap();
605 assert_ne!(
606 dir_ino, file_ino,
607 "a file replaced by a dir is a new object"
608 );
609 assert!(table.get(dir_ino).unwrap().is_dir());
610 }
611
612 #[test]
613 fn test_generation_marks_handles_stale() {
614 let mut table = InodeTable::new();
615 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
616 table.sync_children(storage_ino, &[child(10, "a.txt", false, 100)]);
617 let a = table.lookup(storage_ino, "a.txt").unwrap();
618 assert!(table.is_fresh(a));
619
620 table.bump_generation();
621
622 assert!(!table.is_fresh(a), "handles from an old session are stale");
623 assert!(
624 table.is_fresh(storage_ino),
625 "storage inodes carry no session-scoped handle"
626 );
627 assert!(table.is_fresh(FUSE_ROOT_INODE));
628
629 table.set_handle(a, ObjectHandle(999));
630 assert!(table.is_fresh(a));
631 assert_eq!(
632 table.get(a).unwrap().kind,
633 InodeKind::File {
634 handle: ObjectHandle(999)
635 }
636 );
637 }
638
639 #[test]
640 fn test_set_storage_id_remaps_in_place() {
641 let mut table = InodeTable::new();
642 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
643 table.set_storage_id(storage_ino, StorageId(42));
644 assert_eq!(
645 table.get(storage_ino).unwrap().kind,
646 InodeKind::Storage {
647 storage_id: StorageId(42)
648 }
649 );
650 }
651
652 #[test]
653 fn test_inode_uniqueness() {
654 let mut table = InodeTable::new();
655 let mtime = SystemTime::UNIX_EPOCH;
656 let mut inodes = vec![FUSE_ROOT_INODE];
657 inodes.push(table.add_storage(StorageId(1), "A".into()));
658 inodes.push(table.add_storage(StorageId(2), "B".into()));
659 inodes.push(table.add_object(inodes[1], ObjectHandle(1), "x".into(), false, 0, mtime));
660 inodes.push(table.add_object(inodes[1], ObjectHandle(2), "y".into(), true, 0, mtime));
661
662 let unique: std::collections::HashSet<u64> = inodes.iter().copied().collect();
663 assert_eq!(unique.len(), inodes.len(), "all inodes must be unique");
664 }
665
666 #[test]
667 fn test_nested_directories() {
668 let mut table = InodeTable::new();
669 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
670 let mtime = SystemTime::UNIX_EPOCH;
671
672 let dcim = table.add_object(storage_ino, ObjectHandle(1), "DCIM".into(), true, 0, mtime);
673 let camera = table.add_object(dcim, ObjectHandle(2), "Camera".into(), true, 0, mtime);
674 let photo = table.add_object(
675 camera,
676 ObjectHandle(3),
677 "IMG_001.jpg".into(),
678 false,
679 8192,
680 mtime,
681 );
682
683 assert!(table.children(FUSE_ROOT_INODE).contains(&storage_ino));
685 assert!(table.children(storage_ino).contains(&dcim));
686 assert!(table.children(dcim).contains(&camera));
687 assert!(table.children(camera).contains(&photo));
688
689 assert_eq!(table.lookup(FUSE_ROOT_INODE, "Internal"), Some(storage_ino));
691 assert_eq!(table.lookup(storage_ino, "DCIM"), Some(dcim));
692 assert_eq!(table.lookup(dcim, "Camera"), Some(camera));
693 assert_eq!(table.lookup(camera, "IMG_001.jpg"), Some(photo));
694
695 let photo_entry = table.get(photo).unwrap();
696 assert_eq!(photo_entry.parent, camera);
697 assert_eq!(photo_entry.size, 8192);
698 }
699
700 #[test]
701 fn test_get_mut() {
702 let mut table = InodeTable::new();
703 let storage_ino = table.add_storage(StorageId(1), "Internal".into());
704 let mtime = SystemTime::UNIX_EPOCH;
705 let file_ino = table.add_object(
706 storage_ino,
707 ObjectHandle(1),
708 "f.txt".into(),
709 false,
710 100,
711 mtime,
712 );
713
714 table.get_mut(file_ino).unwrap().size = 999;
715 assert_eq!(table.get(file_ino).unwrap().size, 999);
716 }
717}