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 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 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 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 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 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 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}