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}