anyfs_backend/traits/
fs_inode.rs

1//! Inode-based filesystem operations for FUSE mounting.
2//!
3//! This module provides the [`FsInode`] trait which enables inode-based
4//! operations required for FUSE filesystem implementations.
5//!
6//! # Overview
7//!
8//! FUSE (Filesystem in Userspace) operates on inodes rather than paths for
9//! efficiency. The `FsInode` trait provides the necessary mappings between
10//! paths and inodes, as well as inode-based metadata lookup.
11//!
12//! # Example
13//!
14//! ```rust
15//! use anyfs_backend::{FsInode, FsError, Metadata};
16//! use std::path::Path;
17//! use std::ffi::OsStr;
18//!
19//! // Generic function that works with any FsInode implementation
20//! fn get_child_metadata<B: FsInode>(
21//!     backend: &B,
22//!     parent: u64,
23//!     name: &OsStr,
24//! ) -> Result<Metadata, FsError> {
25//!     let child_inode = backend.lookup(parent, name)?;
26//!     backend.metadata_by_inode(child_inode)
27//! }
28//! ```
29//!
30//! # Thread Safety
31//!
32//! Like all AnyFS traits, `FsInode` requires `Send + Sync`. Implementations
33//! must use interior mutability for any mutable state.
34
35use std::ffi::OsStr;
36use std::path::{Path, PathBuf};
37
38use crate::{FsError, Metadata};
39
40/// Inode-based filesystem operations for FUSE mounting.
41///
42/// This trait provides the bridge between path-based and inode-based
43/// operations. FUSE implementations require efficient inode lookups
44/// and path-to-inode mappings.
45///
46/// # Root Inode
47///
48/// The root inode is conventionally `1` (see [`crate::ROOT_INODE`]).
49/// Implementations should ensure that path "/" maps to inode 1.
50///
51/// # Example
52///
53/// ```rust
54/// use anyfs_backend::{FsInode, FsError, Metadata, ROOT_INODE};
55/// use std::path::{Path, PathBuf};
56/// use std::ffi::OsStr;
57///
58/// struct MyInodeFs { /* ... */ }
59///
60/// impl FsInode for MyInodeFs {
61///     fn path_to_inode(&self, path: &Path) -> Result<u64, FsError> {
62///         // Map path to inode
63///         if path == Path::new("/") {
64///             Ok(ROOT_INODE)
65///         } else {
66///             // ... lookup in inode table
67///             Ok(2)
68///         }
69///     }
70///
71///     fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError> {
72///         // Map inode back to path
73///         if inode == ROOT_INODE {
74///             Ok(PathBuf::from("/"))
75///         } else {
76///             // ... lookup in path table
77///             Ok(PathBuf::from("/file.txt"))
78///         }
79///     }
80///
81///     fn lookup(&self, _parent_inode: u64, _name: &OsStr) -> Result<u64, FsError> {
82///         // Find child inode by name within parent directory
83///         Ok(2)
84///     }
85///
86///     fn metadata_by_inode(&self, _inode: u64) -> Result<Metadata, FsError> {
87///         // Get metadata directly by inode
88///         Ok(Metadata::default())
89///     }
90/// }
91/// ```
92pub trait FsInode: Send + Sync {
93    /// Convert a path to its inode number.
94    ///
95    /// # Arguments
96    ///
97    /// * `path` - The filesystem path to look up
98    ///
99    /// # Returns
100    ///
101    /// The inode number for the path.
102    ///
103    /// # Errors
104    ///
105    /// - [`FsError::NotFound`] if the path does not exist
106    /// - [`FsError::NotADirectory`] if a component is not a directory
107    /// - [`FsError::PermissionDenied`] if access is denied
108    fn path_to_inode(&self, path: &Path) -> Result<u64, FsError>;
109
110    /// Convert an inode number back to its path.
111    ///
112    /// # Arguments
113    ///
114    /// * `inode` - The inode number to look up
115    ///
116    /// # Returns
117    ///
118    /// The filesystem path for the inode.
119    ///
120    /// # Errors
121    ///
122    /// - [`FsError::NotFound`] if the inode does not exist
123    ///
124    /// # Note
125    ///
126    /// For filesystems with hard links, an inode may have multiple paths.
127    /// This method returns one valid path (typically the canonical one).
128    fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError>;
129
130    /// Look up a child entry within a parent directory by name.
131    ///
132    /// This is the core FUSE lookup operation. Given a parent directory's
133    /// inode and a child name, return the child's inode.
134    ///
135    /// # Arguments
136    ///
137    /// * `parent_inode` - The inode of the parent directory
138    /// * `name` - The name of the child entry to find
139    ///
140    /// # Returns
141    ///
142    /// The inode number of the child entry.
143    ///
144    /// # Errors
145    ///
146    /// - [`FsError::NotFound`] if the child does not exist
147    /// - [`FsError::NotADirectory`] if parent is not a directory
148    /// - [`FsError::PermissionDenied`] if access is denied
149    fn lookup(&self, parent_inode: u64, name: &OsStr) -> Result<u64, FsError>;
150
151    /// Get metadata for an inode directly.
152    ///
153    /// This is more efficient than `inode_to_path` + `metadata` for FUSE
154    /// operations, as it avoids path string manipulation.
155    ///
156    /// # Arguments
157    ///
158    /// * `inode` - The inode number to get metadata for
159    ///
160    /// # Returns
161    ///
162    /// The metadata for the inode.
163    ///
164    /// # Errors
165    ///
166    /// - [`FsError::NotFound`] if the inode does not exist
167    fn metadata_by_inode(&self, inode: u64) -> Result<Metadata, FsError>;
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::{FileType, ROOT_INODE};
174    use std::collections::HashMap;
175    use std::sync::RwLock;
176
177    /// Mock implementation of FsInode for testing
178    struct MockInodeFs {
179        // Maps inode -> (path, file_type)
180        inodes: RwLock<HashMap<u64, (PathBuf, FileType)>>,
181        // Maps path -> inode
182        paths: RwLock<HashMap<PathBuf, u64>>,
183        // Maps (parent_inode, name) -> child_inode
184        children: RwLock<HashMap<(u64, String), u64>>,
185    }
186
187    impl MockInodeFs {
188        fn new() -> Self {
189            let mut inodes = HashMap::new();
190            let mut paths = HashMap::new();
191
192            // Root directory
193            inodes.insert(ROOT_INODE, (PathBuf::from("/"), FileType::Directory));
194            paths.insert(PathBuf::from("/"), ROOT_INODE);
195
196            Self {
197                inodes: RwLock::new(inodes),
198                paths: RwLock::new(paths),
199                children: RwLock::new(HashMap::new()),
200            }
201        }
202
203        fn add_file(&self, path: &Path, inode: u64, file_type: FileType) {
204            self.inodes
205                .write()
206                .unwrap()
207                .insert(inode, (path.to_path_buf(), file_type));
208            self.paths
209                .write()
210                .unwrap()
211                .insert(path.to_path_buf(), inode);
212
213            // Add to parent's children
214            if let Some(parent) = path.parent() {
215                if let Some(name) = path.file_name() {
216                    let parent_inode = *self.paths.read().unwrap().get(parent).unwrap_or(&1);
217                    self.children
218                        .write()
219                        .unwrap()
220                        .insert((parent_inode, name.to_string_lossy().into_owned()), inode);
221                }
222            }
223        }
224    }
225
226    impl FsInode for MockInodeFs {
227        fn path_to_inode(&self, path: &Path) -> Result<u64, FsError> {
228            self.paths
229                .read()
230                .unwrap()
231                .get(path)
232                .copied()
233                .ok_or_else(|| FsError::NotFound {
234                    path: path.to_path_buf(),
235                })
236        }
237
238        fn inode_to_path(&self, inode: u64) -> Result<PathBuf, FsError> {
239            self.inodes
240                .read()
241                .unwrap()
242                .get(&inode)
243                .map(|(path, _)| path.clone())
244                .ok_or(FsError::InodeNotFound { inode })
245        }
246
247        fn lookup(&self, parent_inode: u64, name: &OsStr) -> Result<u64, FsError> {
248            // Check parent exists and is a directory
249            let inodes = self.inodes.read().unwrap();
250            match inodes.get(&parent_inode) {
251                None => {
252                    return Err(FsError::InodeNotFound {
253                        inode: parent_inode,
254                    });
255                }
256                Some((_, file_type)) if *file_type != FileType::Directory => {
257                    let (path, _) = inodes.get(&parent_inode).unwrap();
258                    return Err(FsError::NotADirectory { path: path.clone() });
259                }
260                _ => {}
261            }
262            drop(inodes);
263
264            let name_str = name.to_string_lossy().into_owned();
265            self.children
266                .read()
267                .unwrap()
268                .get(&(parent_inode, name_str.clone()))
269                .copied()
270                .ok_or_else(|| {
271                    let parent_path = self
272                        .inodes
273                        .read()
274                        .unwrap()
275                        .get(&parent_inode)
276                        .map(|(p, _)| p.clone())
277                        .unwrap_or_else(|| PathBuf::from("/"));
278                    FsError::NotFound {
279                        path: parent_path.join(&name_str),
280                    }
281                })
282        }
283
284        fn metadata_by_inode(&self, inode: u64) -> Result<Metadata, FsError> {
285            self.inodes
286                .read()
287                .unwrap()
288                .get(&inode)
289                .map(|(_, file_type)| Metadata {
290                    file_type: *file_type,
291                    size: 0,
292                    permissions: crate::Permissions::default_file(),
293                    created: std::time::SystemTime::UNIX_EPOCH,
294                    modified: std::time::SystemTime::UNIX_EPOCH,
295                    accessed: std::time::SystemTime::UNIX_EPOCH,
296                    inode,
297                    nlink: 1,
298                })
299                .ok_or(FsError::InodeNotFound { inode })
300        }
301    }
302
303    #[test]
304    fn path_to_inode_root() {
305        let fs = MockInodeFs::new();
306        let inode = fs.path_to_inode(Path::new("/")).unwrap();
307        assert_eq!(inode, ROOT_INODE);
308    }
309
310    #[test]
311    fn path_to_inode_not_found() {
312        let fs = MockInodeFs::new();
313        let result = fs.path_to_inode(Path::new("/nonexistent"));
314        assert!(matches!(result, Err(FsError::NotFound { .. })));
315    }
316
317    #[test]
318    fn inode_to_path_root() {
319        let fs = MockInodeFs::new();
320        let path = fs.inode_to_path(ROOT_INODE).unwrap();
321        assert_eq!(path, PathBuf::from("/"));
322    }
323
324    #[test]
325    fn inode_to_path_not_found() {
326        let fs = MockInodeFs::new();
327        let result = fs.inode_to_path(9999);
328        assert!(matches!(result, Err(FsError::InodeNotFound { .. })));
329    }
330
331    #[test]
332    fn lookup_child_in_directory() {
333        let fs = MockInodeFs::new();
334        fs.add_file(Path::new("/file.txt"), 2, FileType::File);
335
336        let inode = fs
337            .lookup(ROOT_INODE, std::ffi::OsStr::new("file.txt"))
338            .unwrap();
339        assert_eq!(inode, 2);
340    }
341
342    #[test]
343    fn lookup_child_not_found() {
344        let fs = MockInodeFs::new();
345        let result = fs.lookup(ROOT_INODE, std::ffi::OsStr::new("nonexistent"));
346        assert!(matches!(result, Err(FsError::NotFound { .. })));
347    }
348
349    #[test]
350    fn lookup_parent_not_directory() {
351        let fs = MockInodeFs::new();
352        fs.add_file(Path::new("/file.txt"), 2, FileType::File);
353
354        let result = fs.lookup(2, std::ffi::OsStr::new("child"));
355        assert!(matches!(result, Err(FsError::NotADirectory { .. })));
356    }
357
358    #[test]
359    fn metadata_by_inode_returns_metadata() {
360        let fs = MockInodeFs::new();
361        fs.add_file(Path::new("/file.txt"), 2, FileType::File);
362
363        let meta = fs.metadata_by_inode(2).unwrap();
364        assert_eq!(meta.file_type, FileType::File);
365        assert_eq!(meta.inode, 2);
366    }
367
368    #[test]
369    fn metadata_by_inode_not_found() {
370        let fs = MockInodeFs::new();
371        let result = fs.metadata_by_inode(9999);
372        assert!(matches!(result, Err(FsError::InodeNotFound { .. })));
373    }
374
375    #[test]
376    fn round_trip_path_inode_path() {
377        let fs = MockInodeFs::new();
378        fs.add_file(Path::new("/subdir"), 2, FileType::Directory);
379        fs.add_file(Path::new("/subdir/file.txt"), 3, FileType::File);
380
381        let path = Path::new("/subdir/file.txt");
382        let inode = fs.path_to_inode(path).unwrap();
383        let recovered_path = fs.inode_to_path(inode).unwrap();
384        assert_eq!(path, recovered_path);
385    }
386}