Skip to main content

jax_daemon/fuse/
inode_table.rs

1//! Bidirectional inode ↔ path mapping for FUSE filesystem
2//!
3//! FUSE uses 64-bit inode numbers to identify files and directories.
4//! This module provides efficient bidirectional mapping between inodes and paths.
5
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9/// Bidirectional mapping between inodes and paths
10#[derive(Debug)]
11pub struct InodeTable {
12    /// Path to inode mapping
13    path_to_inode: HashMap<String, u64>,
14    /// Inode to path mapping
15    inode_to_path: HashMap<u64, String>,
16    /// Next available inode number (starts at 2, as 1 is reserved for root)
17    next_inode: AtomicU64,
18}
19
20impl Default for InodeTable {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl InodeTable {
27    /// Root inode number (always 1 in FUSE)
28    pub const ROOT_INODE: u64 = 1;
29
30    /// Create a new inode table with root pre-registered
31    pub fn new() -> Self {
32        let mut table = Self {
33            path_to_inode: HashMap::new(),
34            inode_to_path: HashMap::new(),
35            next_inode: AtomicU64::new(2), // Start at 2, 1 is root
36        };
37
38        // Register root
39        table
40            .path_to_inode
41            .insert("/".to_string(), Self::ROOT_INODE);
42        table
43            .inode_to_path
44            .insert(Self::ROOT_INODE, "/".to_string());
45
46        table
47    }
48
49    /// Get or create an inode for a path
50    pub fn get_or_create(&mut self, path: &str) -> u64 {
51        let normalized = Self::normalize_path(path);
52
53        if let Some(&inode) = self.path_to_inode.get(&normalized) {
54            return inode;
55        }
56
57        let inode = self.next_inode.fetch_add(1, Ordering::SeqCst);
58        self.path_to_inode.insert(normalized.clone(), inode);
59        self.inode_to_path.insert(inode, normalized);
60        inode
61    }
62
63    /// Get the inode for a path if it exists
64    pub fn get_inode(&self, path: &str) -> Option<u64> {
65        let normalized = Self::normalize_path(path);
66        self.path_to_inode.get(&normalized).copied()
67    }
68
69    /// Get the path for an inode if it exists
70    pub fn get_path(&self, inode: u64) -> Option<&str> {
71        self.inode_to_path.get(&inode).map(String::as_str)
72    }
73
74    /// Remove an inode and its path mapping
75    pub fn remove(&mut self, inode: u64) -> Option<String> {
76        if let Some(path) = self.inode_to_path.remove(&inode) {
77            self.path_to_inode.remove(&path);
78            Some(path)
79        } else {
80            None
81        }
82    }
83
84    /// Remove by path and return the inode
85    pub fn remove_by_path(&mut self, path: &str) -> Option<u64> {
86        let normalized = Self::normalize_path(path);
87        if let Some(inode) = self.path_to_inode.remove(&normalized) {
88            self.inode_to_path.remove(&inode);
89            Some(inode)
90        } else {
91            None
92        }
93    }
94
95    /// Rename a path (move inode to new path)
96    pub fn rename(&mut self, old_path: &str, new_path: &str) -> Option<u64> {
97        let old_normalized = Self::normalize_path(old_path);
98        let new_normalized = Self::normalize_path(new_path);
99
100        if let Some(inode) = self.path_to_inode.remove(&old_normalized) {
101            self.inode_to_path.insert(inode, new_normalized.clone());
102            self.path_to_inode.insert(new_normalized, inode);
103            Some(inode)
104        } else {
105            None
106        }
107    }
108
109    /// Clear all mappings except root
110    pub fn clear(&mut self) {
111        self.path_to_inode.clear();
112        self.inode_to_path.clear();
113        self.next_inode.store(2, Ordering::SeqCst);
114
115        // Re-register root
116        self.path_to_inode.insert("/".to_string(), Self::ROOT_INODE);
117        self.inode_to_path.insert(Self::ROOT_INODE, "/".to_string());
118    }
119
120    /// Normalize a path to a consistent format
121    fn normalize_path(path: &str) -> String {
122        let path = path.trim();
123
124        // Handle empty or root
125        if path.is_empty() || path == "/" {
126            return "/".to_string();
127        }
128
129        // Ensure leading slash, no trailing slash
130        let mut normalized = if path.starts_with('/') {
131            path.to_string()
132        } else {
133            format!("/{}", path)
134        };
135
136        if normalized.len() > 1 && normalized.ends_with('/') {
137            normalized.pop();
138        }
139
140        normalized
141    }
142
143    /// Get the parent path of a given path
144    pub fn parent_path(path: &str) -> String {
145        let normalized = Self::normalize_path(path);
146        if normalized == "/" {
147            return "/".to_string();
148        }
149
150        match normalized.rfind('/') {
151            Some(0) => "/".to_string(),
152            Some(pos) => normalized[..pos].to_string(),
153            None => "/".to_string(),
154        }
155    }
156
157    /// Get the filename component of a path
158    pub fn filename(path: &str) -> &str {
159        let normalized = path.trim();
160        if normalized == "/" || normalized.is_empty() {
161            return "";
162        }
163
164        match normalized.rfind('/') {
165            Some(pos) => &normalized[pos + 1..],
166            None => normalized,
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_root_inode() {
177        let table = InodeTable::new();
178        assert_eq!(table.get_inode("/"), Some(InodeTable::ROOT_INODE));
179        assert_eq!(table.get_path(InodeTable::ROOT_INODE), Some("/"));
180    }
181
182    #[test]
183    fn test_get_or_create() {
184        let mut table = InodeTable::new();
185
186        let inode1 = table.get_or_create("/foo");
187        let inode2 = table.get_or_create("/foo");
188        let inode3 = table.get_or_create("/bar");
189
190        assert_eq!(inode1, inode2);
191        assert_ne!(inode1, inode3);
192        assert_ne!(inode1, InodeTable::ROOT_INODE);
193    }
194
195    #[test]
196    fn test_normalize_path() {
197        assert_eq!(InodeTable::normalize_path(""), "/");
198        assert_eq!(InodeTable::normalize_path("/"), "/");
199        assert_eq!(InodeTable::normalize_path("foo"), "/foo");
200        assert_eq!(InodeTable::normalize_path("/foo"), "/foo");
201        assert_eq!(InodeTable::normalize_path("/foo/"), "/foo");
202        assert_eq!(InodeTable::normalize_path("/foo/bar"), "/foo/bar");
203    }
204
205    #[test]
206    fn test_parent_path() {
207        assert_eq!(InodeTable::parent_path("/"), "/");
208        assert_eq!(InodeTable::parent_path("/foo"), "/");
209        assert_eq!(InodeTable::parent_path("/foo/bar"), "/foo");
210        assert_eq!(InodeTable::parent_path("/foo/bar/baz"), "/foo/bar");
211    }
212
213    #[test]
214    fn test_filename() {
215        assert_eq!(InodeTable::filename("/"), "");
216        assert_eq!(InodeTable::filename("/foo"), "foo");
217        assert_eq!(InodeTable::filename("/foo/bar"), "bar");
218        assert_eq!(InodeTable::filename("/foo/bar.txt"), "bar.txt");
219    }
220
221    #[test]
222    fn test_remove() {
223        let mut table = InodeTable::new();
224        let inode = table.get_or_create("/foo");
225
226        assert!(table.get_inode("/foo").is_some());
227        table.remove(inode);
228        assert!(table.get_inode("/foo").is_none());
229    }
230
231    #[test]
232    fn test_rename() {
233        let mut table = InodeTable::new();
234        let inode = table.get_or_create("/old");
235
236        table.rename("/old", "/new");
237
238        assert!(table.get_inode("/old").is_none());
239        assert_eq!(table.get_inode("/new"), Some(inode));
240        assert_eq!(table.get_path(inode), Some("/new"));
241    }
242
243    #[test]
244    fn test_clear() {
245        let mut table = InodeTable::new();
246        table.get_or_create("/foo");
247        table.get_or_create("/bar");
248
249        table.clear();
250
251        assert_eq!(table.get_inode("/"), Some(InodeTable::ROOT_INODE));
252        assert!(table.get_inode("/foo").is_none());
253        assert!(table.get_inode("/bar").is_none());
254    }
255}