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}
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#[derive(Debug)]
41pub struct InodeTable {
42 entries: HashMap<u64, InodeEntry>,
43 name_index: HashMap<(u64, String), u64>,
45 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 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 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 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 pub fn get(&self, inode: u64) -> Option<&InodeEntry> {
140 self.entries.get(&inode)
141 }
142
143 pub fn get_mut(&mut self, inode: u64) -> Option<&mut InodeEntry> {
145 self.entries.get_mut(&inode)
146 }
147
148 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 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 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 self.children_index.remove(&inode);
172 Some(entry)
173 }
174
175 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 entry.parent = new_parent;
185 entry.name = new_name.clone();
186
187 self.name_index.remove(&(old_parent, old_name));
189 self.name_index.insert((new_parent, new_name), inode);
190
191 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 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 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 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 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 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 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 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}