1use std::collections::HashMap;
4use std::path::PathBuf;
5
6pub const ROOT_INO: u64 = 1;
8pub const RAGFS_DIR_INO: u64 = 2;
9pub const QUERY_DIR_INO: u64 = 3;
10pub const SEARCH_DIR_INO: u64 = 4;
11pub const INDEX_FILE_INO: u64 = 5;
12pub const CONFIG_FILE_INO: u64 = 6;
13pub const REINDEX_FILE_INO: u64 = 7;
14pub const SIMILAR_DIR_INO: u64 = 8;
15pub const HELP_FILE_INO: u64 = 9;
16
17pub const OPS_DIR_INO: u64 = 10;
19pub const OPS_CREATE_INO: u64 = 11;
20pub const OPS_DELETE_INO: u64 = 12;
21pub const OPS_MOVE_INO: u64 = 13;
22pub const OPS_BATCH_INO: u64 = 14;
23pub const OPS_RESULT_INO: u64 = 15;
24
25pub const SAFETY_DIR_INO: u64 = 20;
27pub const TRASH_DIR_INO: u64 = 21;
28pub const HISTORY_FILE_INO: u64 = 22;
29pub const UNDO_FILE_INO: u64 = 23;
30
31pub const SEMANTIC_DIR_INO: u64 = 30;
33pub const ORGANIZE_FILE_INO: u64 = 31;
34pub const SIMILAR_OPS_FILE_INO: u64 = 32;
35pub const CLEANUP_FILE_INO: u64 = 33;
36pub const DEDUPE_FILE_INO: u64 = 34;
37pub const PENDING_DIR_INO: u64 = 35;
38pub const APPROVE_FILE_INO: u64 = 36;
39pub const REJECT_FILE_INO: u64 = 37;
40
41pub const FIRST_REAL_INO: u64 = 1000;
42
43#[derive(Debug, Clone)]
45pub enum InodeKind {
46 Root,
48 RagfsDir,
50 QueryDir,
52 QueryResult { query: String },
54 SearchDir,
56 SearchResult { query: String },
58 IndexStatus,
60 Config,
62 Reindex,
64 Help,
66 SimilarDir,
68 SimilarLookup { source_path: PathBuf },
70 Real { path: PathBuf, underlying_ino: u64 },
72
73 OpsDir,
76 OpsCreate,
78 OpsDelete,
80 OpsMove,
82 OpsBatch,
84 OpsResult,
86
87 SafetyDir,
90 TrashDir,
92 TrashEntry { id: String },
94 History,
96 Undo,
98
99 SemanticDir,
102 Organize,
104 SimilarOps,
106 Cleanup,
108 Dedupe,
110 PendingDir,
112 PendingPlan { plan_id: String },
114 Approve,
116 Reject,
118}
119
120#[derive(Debug, Clone)]
122pub struct InodeEntry {
123 pub ino: u64,
125 pub kind: InodeKind,
127 pub parent: u64,
129 pub lookup_count: u64,
131}
132
133pub struct InodeTable {
135 inodes: HashMap<u64, InodeEntry>,
137 path_to_ino: HashMap<PathBuf, u64>,
139 real_ino_map: HashMap<u64, u64>,
141 query_to_ino: HashMap<String, u64>,
143 next_ino: u64,
145}
146
147impl InodeTable {
148 #[must_use]
150 pub fn new() -> Self {
151 let mut table = Self {
152 inodes: HashMap::new(),
153 path_to_ino: HashMap::new(),
154 real_ino_map: HashMap::new(),
155 query_to_ino: HashMap::new(),
156 next_ino: FIRST_REAL_INO,
157 };
158 table.init_virtual_inodes();
159 table
160 }
161
162 fn init_virtual_inodes(&mut self) {
163 self.inodes.insert(
165 ROOT_INO,
166 InodeEntry {
167 ino: ROOT_INO,
168 kind: InodeKind::Root,
169 parent: ROOT_INO,
170 lookup_count: 1,
171 },
172 );
173
174 self.inodes.insert(
176 RAGFS_DIR_INO,
177 InodeEntry {
178 ino: RAGFS_DIR_INO,
179 kind: InodeKind::RagfsDir,
180 parent: ROOT_INO,
181 lookup_count: 0,
182 },
183 );
184
185 self.inodes.insert(
187 QUERY_DIR_INO,
188 InodeEntry {
189 ino: QUERY_DIR_INO,
190 kind: InodeKind::QueryDir,
191 parent: RAGFS_DIR_INO,
192 lookup_count: 0,
193 },
194 );
195
196 self.inodes.insert(
198 SEARCH_DIR_INO,
199 InodeEntry {
200 ino: SEARCH_DIR_INO,
201 kind: InodeKind::SearchDir,
202 parent: RAGFS_DIR_INO,
203 lookup_count: 0,
204 },
205 );
206
207 self.inodes.insert(
209 INDEX_FILE_INO,
210 InodeEntry {
211 ino: INDEX_FILE_INO,
212 kind: InodeKind::IndexStatus,
213 parent: RAGFS_DIR_INO,
214 lookup_count: 0,
215 },
216 );
217
218 self.inodes.insert(
220 CONFIG_FILE_INO,
221 InodeEntry {
222 ino: CONFIG_FILE_INO,
223 kind: InodeKind::Config,
224 parent: RAGFS_DIR_INO,
225 lookup_count: 0,
226 },
227 );
228
229 self.inodes.insert(
231 REINDEX_FILE_INO,
232 InodeEntry {
233 ino: REINDEX_FILE_INO,
234 kind: InodeKind::Reindex,
235 parent: RAGFS_DIR_INO,
236 lookup_count: 0,
237 },
238 );
239
240 self.inodes.insert(
242 HELP_FILE_INO,
243 InodeEntry {
244 ino: HELP_FILE_INO,
245 kind: InodeKind::Help,
246 parent: RAGFS_DIR_INO,
247 lookup_count: 0,
248 },
249 );
250
251 self.inodes.insert(
253 SIMILAR_DIR_INO,
254 InodeEntry {
255 ino: SIMILAR_DIR_INO,
256 kind: InodeKind::SimilarDir,
257 parent: RAGFS_DIR_INO,
258 lookup_count: 0,
259 },
260 );
261
262 self.inodes.insert(
264 OPS_DIR_INO,
265 InodeEntry {
266 ino: OPS_DIR_INO,
267 kind: InodeKind::OpsDir,
268 parent: RAGFS_DIR_INO,
269 lookup_count: 0,
270 },
271 );
272
273 self.inodes.insert(
274 OPS_CREATE_INO,
275 InodeEntry {
276 ino: OPS_CREATE_INO,
277 kind: InodeKind::OpsCreate,
278 parent: OPS_DIR_INO,
279 lookup_count: 0,
280 },
281 );
282
283 self.inodes.insert(
284 OPS_DELETE_INO,
285 InodeEntry {
286 ino: OPS_DELETE_INO,
287 kind: InodeKind::OpsDelete,
288 parent: OPS_DIR_INO,
289 lookup_count: 0,
290 },
291 );
292
293 self.inodes.insert(
294 OPS_MOVE_INO,
295 InodeEntry {
296 ino: OPS_MOVE_INO,
297 kind: InodeKind::OpsMove,
298 parent: OPS_DIR_INO,
299 lookup_count: 0,
300 },
301 );
302
303 self.inodes.insert(
304 OPS_BATCH_INO,
305 InodeEntry {
306 ino: OPS_BATCH_INO,
307 kind: InodeKind::OpsBatch,
308 parent: OPS_DIR_INO,
309 lookup_count: 0,
310 },
311 );
312
313 self.inodes.insert(
314 OPS_RESULT_INO,
315 InodeEntry {
316 ino: OPS_RESULT_INO,
317 kind: InodeKind::OpsResult,
318 parent: OPS_DIR_INO,
319 lookup_count: 0,
320 },
321 );
322
323 self.inodes.insert(
325 SAFETY_DIR_INO,
326 InodeEntry {
327 ino: SAFETY_DIR_INO,
328 kind: InodeKind::SafetyDir,
329 parent: RAGFS_DIR_INO,
330 lookup_count: 0,
331 },
332 );
333
334 self.inodes.insert(
335 TRASH_DIR_INO,
336 InodeEntry {
337 ino: TRASH_DIR_INO,
338 kind: InodeKind::TrashDir,
339 parent: SAFETY_DIR_INO,
340 lookup_count: 0,
341 },
342 );
343
344 self.inodes.insert(
345 HISTORY_FILE_INO,
346 InodeEntry {
347 ino: HISTORY_FILE_INO,
348 kind: InodeKind::History,
349 parent: SAFETY_DIR_INO,
350 lookup_count: 0,
351 },
352 );
353
354 self.inodes.insert(
355 UNDO_FILE_INO,
356 InodeEntry {
357 ino: UNDO_FILE_INO,
358 kind: InodeKind::Undo,
359 parent: SAFETY_DIR_INO,
360 lookup_count: 0,
361 },
362 );
363
364 self.inodes.insert(
366 SEMANTIC_DIR_INO,
367 InodeEntry {
368 ino: SEMANTIC_DIR_INO,
369 kind: InodeKind::SemanticDir,
370 parent: RAGFS_DIR_INO,
371 lookup_count: 0,
372 },
373 );
374
375 self.inodes.insert(
376 ORGANIZE_FILE_INO,
377 InodeEntry {
378 ino: ORGANIZE_FILE_INO,
379 kind: InodeKind::Organize,
380 parent: SEMANTIC_DIR_INO,
381 lookup_count: 0,
382 },
383 );
384
385 self.inodes.insert(
386 SIMILAR_OPS_FILE_INO,
387 InodeEntry {
388 ino: SIMILAR_OPS_FILE_INO,
389 kind: InodeKind::SimilarOps,
390 parent: SEMANTIC_DIR_INO,
391 lookup_count: 0,
392 },
393 );
394
395 self.inodes.insert(
396 CLEANUP_FILE_INO,
397 InodeEntry {
398 ino: CLEANUP_FILE_INO,
399 kind: InodeKind::Cleanup,
400 parent: SEMANTIC_DIR_INO,
401 lookup_count: 0,
402 },
403 );
404
405 self.inodes.insert(
406 DEDUPE_FILE_INO,
407 InodeEntry {
408 ino: DEDUPE_FILE_INO,
409 kind: InodeKind::Dedupe,
410 parent: SEMANTIC_DIR_INO,
411 lookup_count: 0,
412 },
413 );
414
415 self.inodes.insert(
416 PENDING_DIR_INO,
417 InodeEntry {
418 ino: PENDING_DIR_INO,
419 kind: InodeKind::PendingDir,
420 parent: SEMANTIC_DIR_INO,
421 lookup_count: 0,
422 },
423 );
424
425 self.inodes.insert(
426 APPROVE_FILE_INO,
427 InodeEntry {
428 ino: APPROVE_FILE_INO,
429 kind: InodeKind::Approve,
430 parent: SEMANTIC_DIR_INO,
431 lookup_count: 0,
432 },
433 );
434
435 self.inodes.insert(
436 REJECT_FILE_INO,
437 InodeEntry {
438 ino: REJECT_FILE_INO,
439 kind: InodeKind::Reject,
440 parent: SEMANTIC_DIR_INO,
441 lookup_count: 0,
442 },
443 );
444 }
445
446 #[must_use]
448 pub fn get(&self, ino: u64) -> Option<&InodeEntry> {
449 self.inodes.get(&ino)
450 }
451
452 pub fn get_or_create_real(&mut self, path: PathBuf, underlying_ino: u64) -> u64 {
454 if let Some(&ino) = self.path_to_ino.get(&path) {
455 return ino;
456 }
457
458 let ino = self.next_ino;
459 self.next_ino += 1;
460
461 self.inodes.insert(
462 ino,
463 InodeEntry {
464 ino,
465 kind: InodeKind::Real {
466 path: path.clone(),
467 underlying_ino,
468 },
469 parent: ROOT_INO,
470 lookup_count: 0,
471 },
472 );
473
474 self.path_to_ino.insert(path, ino);
475 self.real_ino_map.insert(underlying_ino, ino);
476
477 ino
478 }
479
480 pub fn get_or_create_query_result(&mut self, parent: u64, query: String) -> u64 {
482 if let Some(&ino) = self.query_to_ino.get(&query) {
483 return ino;
484 }
485
486 let ino = self.next_ino;
487 self.next_ino += 1;
488
489 self.inodes.insert(
490 ino,
491 InodeEntry {
492 ino,
493 kind: InodeKind::QueryResult {
494 query: query.clone(),
495 },
496 parent,
497 lookup_count: 0,
498 },
499 );
500
501 self.query_to_ino.insert(query, ino);
502
503 ino
504 }
505
506 pub fn lookup(&mut self, ino: u64) {
508 if let Some(entry) = self.inodes.get_mut(&ino) {
509 entry.lookup_count += 1;
510 }
511 }
512
513 pub fn forget(&mut self, ino: u64, nlookup: u64) {
515 if let Some(entry) = self.inodes.get_mut(&ino) {
516 entry.lookup_count = entry.lookup_count.saturating_sub(nlookup);
517 }
518 }
519
520 #[must_use]
522 pub fn is_virtual(&self, ino: u64) -> bool {
523 ino < FIRST_REAL_INO
524 }
525
526 #[must_use]
528 pub fn get_by_path(&self, path: &PathBuf) -> Option<u64> {
529 self.path_to_ino.get(path).copied()
530 }
531
532 pub fn remove(&mut self, ino: u64) {
535 if self.is_virtual(ino) {
537 return;
538 }
539
540 if let Some(entry) = self.inodes.remove(&ino) {
541 if let InodeKind::Real {
542 path,
543 underlying_ino,
544 } = entry.kind
545 {
546 self.path_to_ino.remove(&path);
547 self.real_ino_map.remove(&underlying_ino);
548 } else if let InodeKind::QueryResult { query } = entry.kind {
549 self.query_to_ino.remove(&query);
550 }
551 }
552 }
553
554 pub fn update_path(&mut self, ino: u64, new_path: PathBuf) {
556 if let Some(entry) = self.inodes.get_mut(&ino)
557 && let InodeKind::Real {
558 ref path,
559 underlying_ino,
560 } = entry.kind
561 {
562 self.path_to_ino.remove(path);
564
565 entry.kind = InodeKind::Real {
567 path: new_path.clone(),
568 underlying_ino,
569 };
570
571 self.path_to_ino.insert(new_path, ino);
573 }
574 }
575}
576
577impl Default for InodeTable {
578 fn default() -> Self {
579 Self::new()
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
590 fn test_reserved_inode_constants() {
591 assert_eq!(ROOT_INO, 1);
592 assert_eq!(RAGFS_DIR_INO, 2);
593 assert_eq!(QUERY_DIR_INO, 3);
594 assert_eq!(SEARCH_DIR_INO, 4);
595 assert_eq!(INDEX_FILE_INO, 5);
596 assert_eq!(CONFIG_FILE_INO, 6);
597 assert_eq!(REINDEX_FILE_INO, 7);
598 assert_eq!(SIMILAR_DIR_INO, 8);
599 assert_eq!(HELP_FILE_INO, 9);
600 assert_eq!(FIRST_REAL_INO, 1000);
601 }
602
603 #[test]
604 fn test_reserved_inodes_are_below_first_real() {
605 assert!(ROOT_INO < FIRST_REAL_INO);
606 assert!(RAGFS_DIR_INO < FIRST_REAL_INO);
607 assert!(QUERY_DIR_INO < FIRST_REAL_INO);
608 assert!(SEARCH_DIR_INO < FIRST_REAL_INO);
609 assert!(INDEX_FILE_INO < FIRST_REAL_INO);
610 assert!(CONFIG_FILE_INO < FIRST_REAL_INO);
611 assert!(REINDEX_FILE_INO < FIRST_REAL_INO);
612 assert!(SIMILAR_DIR_INO < FIRST_REAL_INO);
613 assert!(HELP_FILE_INO < FIRST_REAL_INO);
614 }
615
616 #[test]
619 fn test_inode_kind_root() {
620 let kind = InodeKind::Root;
621 let debug_str = format!("{kind:?}");
622 assert!(debug_str.contains("Root"));
623 }
624
625 #[test]
626 fn test_inode_kind_ragfs_dir() {
627 let kind = InodeKind::RagfsDir;
628 let debug_str = format!("{kind:?}");
629 assert!(debug_str.contains("RagfsDir"));
630 }
631
632 #[test]
633 fn test_inode_kind_query_result() {
634 let kind = InodeKind::QueryResult {
635 query: "test query".to_string(),
636 };
637 let debug_str = format!("{kind:?}");
638 assert!(debug_str.contains("QueryResult"));
639 assert!(debug_str.contains("test query"));
640 }
641
642 #[test]
643 fn test_inode_kind_search_result() {
644 let kind = InodeKind::SearchResult {
645 query: "find files".to_string(),
646 };
647 let debug_str = format!("{kind:?}");
648 assert!(debug_str.contains("SearchResult"));
649 }
650
651 #[test]
652 fn test_inode_kind_real() {
653 let kind = InodeKind::Real {
654 path: PathBuf::from("/test/file.txt"),
655 underlying_ino: 12345,
656 };
657 let debug_str = format!("{kind:?}");
658 assert!(debug_str.contains("Real"));
659 assert!(debug_str.contains("file.txt"));
660 }
661
662 #[test]
663 fn test_inode_kind_similar_lookup() {
664 let kind = InodeKind::SimilarLookup {
665 source_path: PathBuf::from("/source/doc.pdf"),
666 };
667 let debug_str = format!("{kind:?}");
668 assert!(debug_str.contains("SimilarLookup"));
669 }
670
671 #[test]
672 fn test_inode_kind_clone() {
673 let kind = InodeKind::QueryResult {
674 query: "cloned query".to_string(),
675 };
676 let cloned = kind.clone();
677 if let InodeKind::QueryResult { query } = cloned {
678 assert_eq!(query, "cloned query");
679 } else {
680 panic!("Clone produced wrong variant");
681 }
682 }
683
684 #[test]
687 fn test_inode_entry_creation() {
688 let entry = InodeEntry {
689 ino: 100,
690 kind: InodeKind::Root,
691 parent: 1,
692 lookup_count: 5,
693 };
694 assert_eq!(entry.ino, 100);
695 assert_eq!(entry.parent, 1);
696 assert_eq!(entry.lookup_count, 5);
697 }
698
699 #[test]
700 fn test_inode_entry_debug() {
701 let entry = InodeEntry {
702 ino: 42,
703 kind: InodeKind::IndexStatus,
704 parent: 2,
705 lookup_count: 3,
706 };
707 let debug_str = format!("{entry:?}");
708 assert!(debug_str.contains("42"));
709 assert!(debug_str.contains("IndexStatus"));
710 }
711
712 #[test]
713 fn test_inode_entry_clone() {
714 let entry = InodeEntry {
715 ino: 50,
716 kind: InodeKind::Config,
717 parent: 2,
718 lookup_count: 10,
719 };
720 let cloned = entry.clone();
721 assert_eq!(cloned.ino, 50);
722 assert_eq!(cloned.lookup_count, 10);
723 }
724
725 #[test]
728 fn test_inode_table_new() {
729 let table = InodeTable::new();
730 assert!(table.get(ROOT_INO).is_some());
731 assert!(table.get(RAGFS_DIR_INO).is_some());
732 assert!(table.get(QUERY_DIR_INO).is_some());
733 }
734
735 #[test]
736 fn test_inode_table_default() {
737 let table = InodeTable::default();
738 assert!(table.get(ROOT_INO).is_some());
739 }
740
741 #[test]
742 fn test_virtual_inodes_initialized() {
743 let table = InodeTable::new();
744
745 let root = table.get(ROOT_INO).expect("Root should exist");
747 assert!(matches!(root.kind, InodeKind::Root));
748 assert_eq!(root.parent, ROOT_INO);
749
750 let ragfs = table.get(RAGFS_DIR_INO).expect("RagfsDir should exist");
752 assert!(matches!(ragfs.kind, InodeKind::RagfsDir));
753 assert_eq!(ragfs.parent, ROOT_INO);
754
755 let query = table.get(QUERY_DIR_INO).expect("QueryDir should exist");
757 assert!(matches!(query.kind, InodeKind::QueryDir));
758 assert_eq!(query.parent, RAGFS_DIR_INO);
759
760 let search = table.get(SEARCH_DIR_INO).expect("SearchDir should exist");
762 assert!(matches!(search.kind, InodeKind::SearchDir));
763 assert_eq!(search.parent, RAGFS_DIR_INO);
764
765 let index = table.get(INDEX_FILE_INO).expect("IndexFile should exist");
767 assert!(matches!(index.kind, InodeKind::IndexStatus));
768
769 let config = table.get(CONFIG_FILE_INO).expect("ConfigFile should exist");
771 assert!(matches!(config.kind, InodeKind::Config));
772
773 let reindex = table
775 .get(REINDEX_FILE_INO)
776 .expect("ReindexFile should exist");
777 assert!(matches!(reindex.kind, InodeKind::Reindex));
778
779 let similar = table.get(SIMILAR_DIR_INO).expect("SimilarDir should exist");
781 assert!(matches!(similar.kind, InodeKind::SimilarDir));
782
783 let help = table.get(HELP_FILE_INO).expect("HelpFile should exist");
785 assert!(matches!(help.kind, InodeKind::Help));
786 }
787
788 #[test]
791 fn test_get_existing_inode() {
792 let table = InodeTable::new();
793 assert!(table.get(ROOT_INO).is_some());
794 }
795
796 #[test]
797 fn test_get_nonexistent_inode() {
798 let table = InodeTable::new();
799 assert!(table.get(99999).is_none());
800 }
801
802 #[test]
805 fn test_get_or_create_real_new_path() {
806 let mut table = InodeTable::new();
807 let path = PathBuf::from("/test/file.txt");
808 let underlying_ino = 12345;
809
810 let ino = table.get_or_create_real(path.clone(), underlying_ino);
811
812 assert!(ino >= FIRST_REAL_INO);
813 let entry = table.get(ino).expect("Entry should exist");
814 if let InodeKind::Real {
815 path: p,
816 underlying_ino: u,
817 } = &entry.kind
818 {
819 assert_eq!(p, &path);
820 assert_eq!(*u, underlying_ino);
821 } else {
822 panic!("Expected Real inode kind");
823 }
824 }
825
826 #[test]
827 fn test_get_or_create_real_returns_existing() {
828 let mut table = InodeTable::new();
829 let path = PathBuf::from("/test/file.txt");
830 let underlying_ino = 12345;
831
832 let ino1 = table.get_or_create_real(path.clone(), underlying_ino);
833 let ino2 = table.get_or_create_real(path.clone(), underlying_ino);
834
835 assert_eq!(ino1, ino2, "Should return same inode for same path");
836 }
837
838 #[test]
839 fn test_get_or_create_real_increments_ino() {
840 let mut table = InodeTable::new();
841
842 let ino1 = table.get_or_create_real(PathBuf::from("/file1.txt"), 100);
843 let ino2 = table.get_or_create_real(PathBuf::from("/file2.txt"), 101);
844 let ino3 = table.get_or_create_real(PathBuf::from("/file3.txt"), 102);
845
846 assert!(ino2 > ino1);
847 assert!(ino3 > ino2);
848 }
849
850 #[test]
851 fn test_get_or_create_real_parent_is_root() {
852 let mut table = InodeTable::new();
853 let ino = table.get_or_create_real(PathBuf::from("/test.txt"), 100);
854
855 let entry = table.get(ino).unwrap();
856 assert_eq!(entry.parent, ROOT_INO);
857 }
858
859 #[test]
862 fn test_get_or_create_query_result_new() {
863 let mut table = InodeTable::new();
864 let query = "how to implement auth".to_string();
865
866 let ino = table.get_or_create_query_result(QUERY_DIR_INO, query.clone());
867
868 assert!(ino >= FIRST_REAL_INO);
869 let entry = table.get(ino).expect("Entry should exist");
870 if let InodeKind::QueryResult { query: q } = &entry.kind {
871 assert_eq!(q, &query);
872 } else {
873 panic!("Expected QueryResult kind");
874 }
875 }
876
877 #[test]
878 fn test_get_or_create_query_result_returns_existing() {
879 let mut table = InodeTable::new();
880 let query = "test query".to_string();
881
882 let ino1 = table.get_or_create_query_result(QUERY_DIR_INO, query.clone());
883 let ino2 = table.get_or_create_query_result(QUERY_DIR_INO, query.clone());
884
885 assert_eq!(ino1, ino2, "Should return same inode for same query");
886 }
887
888 #[test]
889 fn test_get_or_create_query_result_different_queries() {
890 let mut table = InodeTable::new();
891
892 let ino1 = table.get_or_create_query_result(QUERY_DIR_INO, "query1".to_string());
893 let ino2 = table.get_or_create_query_result(QUERY_DIR_INO, "query2".to_string());
894
895 assert_ne!(ino1, ino2, "Different queries should have different inodes");
896 }
897
898 #[test]
899 fn test_get_or_create_query_result_preserves_parent() {
900 let mut table = InodeTable::new();
901 let ino = table.get_or_create_query_result(QUERY_DIR_INO, "test".to_string());
902
903 let entry = table.get(ino).unwrap();
904 assert_eq!(entry.parent, QUERY_DIR_INO);
905 }
906
907 #[test]
910 fn test_lookup_increments_count() {
911 let mut table = InodeTable::new();
912
913 let initial_count = table.get(ROOT_INO).unwrap().lookup_count;
914 table.lookup(ROOT_INO);
915 let after_count = table.get(ROOT_INO).unwrap().lookup_count;
916
917 assert_eq!(after_count, initial_count + 1);
918 }
919
920 #[test]
921 fn test_lookup_multiple_times() {
922 let mut table = InodeTable::new();
923
924 let initial = table.get(RAGFS_DIR_INO).unwrap().lookup_count;
925 table.lookup(RAGFS_DIR_INO);
926 table.lookup(RAGFS_DIR_INO);
927 table.lookup(RAGFS_DIR_INO);
928 let final_count = table.get(RAGFS_DIR_INO).unwrap().lookup_count;
929
930 assert_eq!(final_count, initial + 3);
931 }
932
933 #[test]
934 fn test_lookup_nonexistent_does_nothing() {
935 let mut table = InodeTable::new();
936 table.lookup(99999); }
938
939 #[test]
942 fn test_forget_decrements_count() {
943 let mut table = InodeTable::new();
944 table.lookup(ROOT_INO);
945 table.lookup(ROOT_INO);
946
947 let before = table.get(ROOT_INO).unwrap().lookup_count;
948 table.forget(ROOT_INO, 1);
949 let after = table.get(ROOT_INO).unwrap().lookup_count;
950
951 assert_eq!(after, before - 1);
952 }
953
954 #[test]
955 fn test_forget_saturating_sub() {
956 let mut table = InodeTable::new();
957 table.forget(ROOT_INO, 100); let count = table.get(ROOT_INO).unwrap().lookup_count;
961 assert_eq!(count, 0);
962 }
963
964 #[test]
965 fn test_forget_nonexistent_does_nothing() {
966 let mut table = InodeTable::new();
967 table.forget(99999, 5); }
969
970 #[test]
973 fn test_is_virtual_true_for_reserved() {
974 let table = InodeTable::new();
975 assert!(table.is_virtual(ROOT_INO));
976 assert!(table.is_virtual(RAGFS_DIR_INO));
977 assert!(table.is_virtual(QUERY_DIR_INO));
978 assert!(table.is_virtual(SEARCH_DIR_INO));
979 assert!(table.is_virtual(INDEX_FILE_INO));
980 assert!(table.is_virtual(CONFIG_FILE_INO));
981 assert!(table.is_virtual(REINDEX_FILE_INO));
982 assert!(table.is_virtual(SIMILAR_DIR_INO));
983 assert!(table.is_virtual(HELP_FILE_INO));
984 }
985
986 #[test]
987 fn test_is_virtual_false_for_real() {
988 let table = InodeTable::new();
989 assert!(!table.is_virtual(FIRST_REAL_INO));
990 assert!(!table.is_virtual(FIRST_REAL_INO + 1));
991 assert!(!table.is_virtual(99999));
992 }
993
994 #[test]
995 fn test_is_virtual_boundary() {
996 let table = InodeTable::new();
997 assert!(table.is_virtual(FIRST_REAL_INO - 1));
998 assert!(!table.is_virtual(FIRST_REAL_INO));
999 }
1000
1001 #[test]
1004 fn test_get_by_path_existing() {
1005 let mut table = InodeTable::new();
1006 let path = PathBuf::from("/documents/test.txt");
1007 let ino = table.get_or_create_real(path.clone(), 100);
1008
1009 let found = table.get_by_path(&path);
1010 assert_eq!(found, Some(ino));
1011 }
1012
1013 #[test]
1014 fn test_get_by_path_nonexistent() {
1015 let table = InodeTable::new();
1016 let path = PathBuf::from("/nonexistent/file.txt");
1017
1018 assert!(table.get_by_path(&path).is_none());
1019 }
1020
1021 #[test]
1022 fn test_get_by_path_different_paths() {
1023 let mut table = InodeTable::new();
1024 let path1 = PathBuf::from("/file1.txt");
1025 let path2 = PathBuf::from("/file2.txt");
1026
1027 let ino1 = table.get_or_create_real(path1.clone(), 100);
1028 let ino2 = table.get_or_create_real(path2.clone(), 101);
1029
1030 assert_eq!(table.get_by_path(&path1), Some(ino1));
1031 assert_eq!(table.get_by_path(&path2), Some(ino2));
1032 }
1033
1034 #[test]
1037 fn test_mixed_real_and_query_inodes() {
1038 let mut table = InodeTable::new();
1039
1040 let real_ino = table.get_or_create_real(PathBuf::from("/doc.txt"), 100);
1041 let query_ino = table.get_or_create_query_result(QUERY_DIR_INO, "search".to_string());
1042
1043 assert_ne!(real_ino, query_ino);
1044 assert!(table.get(real_ino).is_some());
1045 assert!(table.get(query_ino).is_some());
1046 }
1047
1048 #[test]
1049 fn test_inode_numbers_are_sequential() {
1050 let mut table = InodeTable::new();
1051
1052 let ino1 = table.get_or_create_real(PathBuf::from("/a.txt"), 1);
1053 let ino2 = table.get_or_create_query_result(QUERY_DIR_INO, "q1".to_string());
1054 let ino3 = table.get_or_create_real(PathBuf::from("/b.txt"), 2);
1055
1056 assert_eq!(ino2, ino1 + 1);
1057 assert_eq!(ino3, ino2 + 1);
1058 }
1059
1060 #[test]
1063 fn test_remove_real_inode() {
1064 let mut table = InodeTable::new();
1065 let path = PathBuf::from("/test/file.txt");
1066 let underlying_ino = 12345;
1067
1068 let ino = table.get_or_create_real(path.clone(), underlying_ino);
1069 assert!(table.get(ino).is_some());
1070 assert!(table.get_by_path(&path).is_some());
1071
1072 table.remove(ino);
1073
1074 assert!(table.get(ino).is_none());
1075 assert!(table.get_by_path(&path).is_none());
1076 }
1077
1078 #[test]
1079 fn test_remove_query_result_inode() {
1080 let mut table = InodeTable::new();
1081 let query = "test query".to_string();
1082
1083 let ino = table.get_or_create_query_result(QUERY_DIR_INO, query.clone());
1084 assert!(table.get(ino).is_some());
1085
1086 table.remove(ino);
1087
1088 assert!(table.get(ino).is_none());
1089 let new_ino = table.get_or_create_query_result(QUERY_DIR_INO, query);
1091 assert_ne!(ino, new_ino);
1092 }
1093
1094 #[test]
1095 fn test_remove_nonexistent_does_nothing() {
1096 let mut table = InodeTable::new();
1097 table.remove(99999); }
1099
1100 #[test]
1101 fn test_remove_virtual_inode_does_nothing() {
1102 let mut table = InodeTable::new();
1103 table.remove(ROOT_INO);
1105 assert!(table.get(ROOT_INO).is_some());
1107 }
1108
1109 #[test]
1112 fn test_update_path_basic() {
1113 let mut table = InodeTable::new();
1114 let old_path = PathBuf::from("/old/path.txt");
1115 let new_path = PathBuf::from("/new/path.txt");
1116
1117 let ino = table.get_or_create_real(old_path.clone(), 100);
1118
1119 table.update_path(ino, new_path.clone());
1120
1121 assert!(table.get_by_path(&old_path).is_none());
1123 assert_eq!(table.get_by_path(&new_path), Some(ino));
1125
1126 let entry = table.get(ino).unwrap();
1128 if let InodeKind::Real { path, .. } = &entry.kind {
1129 assert_eq!(path, &new_path);
1130 } else {
1131 panic!("Expected Real inode kind");
1132 }
1133 }
1134
1135 #[test]
1136 fn test_update_path_preserves_underlying_ino() {
1137 let mut table = InodeTable::new();
1138 let old_path = PathBuf::from("/old.txt");
1139 let new_path = PathBuf::from("/new.txt");
1140 let underlying = 54321_u64;
1141
1142 let ino = table.get_or_create_real(old_path, underlying);
1143 table.update_path(ino, new_path);
1144
1145 let entry = table.get(ino).unwrap();
1146 if let InodeKind::Real { underlying_ino, .. } = &entry.kind {
1147 assert_eq!(*underlying_ino, underlying);
1148 } else {
1149 panic!("Expected Real inode kind");
1150 }
1151 }
1152
1153 #[test]
1154 fn test_update_path_nonexistent_does_nothing() {
1155 let mut table = InodeTable::new();
1156 table.update_path(99999, PathBuf::from("/new.txt")); }
1158
1159 #[test]
1160 fn test_update_path_virtual_inode_does_nothing() {
1161 let mut table = InodeTable::new();
1162 table.update_path(ROOT_INO, PathBuf::from("/new/root"));
1164 let root = table.get(ROOT_INO).unwrap();
1166 assert!(matches!(root.kind, InodeKind::Root));
1167 }
1168}