anyfs_backend/
path_resolver.rs

1//! # PathResolver Trait
2//!
3//! Strategy trait for pluggable path resolution algorithms.
4//!
5//! ## Responsibility
6//! - Define the contract for path resolution (canonicalization, normalization)
7//!
8//! ## Dependencies
9//! - [`Fs`] trait for filesystem queries
10//! - [`FsError`] for error handling
11//!
12//! ## Usage
13//!
14//! ```rust
15//! use anyfs_backend::{PathResolver, Fs, FsError};
16//! use std::path::{Path, PathBuf};
17//!
18//! struct MyCustomResolver;
19//!
20//! impl PathResolver for MyCustomResolver {
21//!     fn canonicalize(&self, path: &Path, _fs: &dyn Fs) -> Result<PathBuf, FsError> {
22//!         // Custom resolution logic
23//!         Ok(path.to_path_buf())
24//!     }
25//!     
26//!     fn soft_canonicalize(&self, path: &Path, _fs: &dyn Fs) -> Result<PathBuf, FsError> {
27//!         // Custom resolution logic (allows non-existent final component)
28//!         Ok(path.to_path_buf())
29//!     }
30//! }
31//! ```
32
33use std::path::{Path, PathBuf};
34
35use crate::{Fs, FsError};
36
37// ============================================================================
38// Trait Definition
39// ============================================================================
40
41/// Strategy trait for path resolution algorithms.
42///
43/// Encapsulates how paths are normalized, symlinks are followed,
44/// and `..`/`.` components are resolved.
45///
46/// # Thread Safety
47///
48/// All implementations must be `Send + Sync` to support concurrent access.
49///
50/// # Object Safety
51///
52/// Uses `&dyn Fs` to remain object-safe, enabling runtime resolver selection.
53///
54/// # Symlink Handling
55///
56/// The trait accepts `&dyn Fs` for object safety. Implementations that need
57/// symlink awareness can attempt to downcast to check for `FsLink` capabilities.
58/// All built-in virtual backends implement `FsLink`, so symlink-aware resolution
59/// works out of the box. For backends without `FsLink`, resolution still works
60/// but treats all entries as non-symlinks.
61///
62/// # Implementors
63///
64/// - `IterativeResolver` (default in `anyfs`): Walks path component by component
65/// - `NoOpResolver` (in `anyfs`): Pass-through for `SelfResolving` backends
66/// - `CachingResolver` (in `anyfs`): LRU cache wrapper for any resolver (with TTL expiration)
67///
68/// # Example
69///
70/// ```rust
71/// use anyfs_backend::{PathResolver, Fs, FsError};
72/// use std::path::{Path, PathBuf};
73///
74/// struct MyCustomResolver;
75///
76/// impl PathResolver for MyCustomResolver {
77///     fn canonicalize(&self, path: &Path, _fs: &dyn Fs) -> Result<PathBuf, FsError> {
78///         // Custom resolution logic
79///         Ok(path.to_path_buf())
80///     }
81///     
82///     fn soft_canonicalize(&self, path: &Path, _fs: &dyn Fs) -> Result<PathBuf, FsError> {
83///         // Custom resolution logic (allows non-existent final component)
84///         Ok(path.to_path_buf())
85///     }
86/// }
87/// ```
88pub trait PathResolver: Send + Sync {
89    /// Resolve path to canonical form.
90    ///
91    /// All symlinks are resolved, `.` and `..` are normalized,
92    /// and all path components must exist.
93    ///
94    /// # Arguments
95    ///
96    /// * `path` - The path to canonicalize
97    /// * `fs` - The filesystem to query for path resolution
98    ///
99    /// # Returns
100    ///
101    /// The fully resolved canonical path.
102    ///
103    /// # Errors
104    ///
105    /// - [`FsError::NotFound`] - A component doesn't exist
106    /// - [`FsError::InvalidData`] - Symlink loop detected (circular symlinks)
107    fn canonicalize(&self, path: &Path, fs: &dyn Fs) -> Result<PathBuf, FsError>;
108
109    /// Like [`canonicalize`](Self::canonicalize), but allows non-existent final component.
110    ///
111    /// Resolves parent path fully, appends final component lexically.
112    /// This is useful for `write()` operations where the target file
113    /// doesn't exist yet.
114    ///
115    /// # Arguments
116    ///
117    /// * `path` - The path to soft-canonicalize
118    /// * `fs` - The filesystem to query for path resolution
119    ///
120    /// # Returns
121    ///
122    /// The resolved path with the final component appended lexically.
123    ///
124    /// # Errors
125    ///
126    /// - [`FsError::NotFound`] - A parent component doesn't exist
127    /// - [`FsError::InvalidData`] - Symlink loop detected
128    fn soft_canonicalize(&self, path: &Path, fs: &dyn Fs) -> Result<PathBuf, FsError>;
129}
130
131// ============================================================================
132// Tests
133// ============================================================================
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::{FileType, FsDir, FsRead, FsWrite, Metadata, Permissions, ReadDirIter};
139    use std::io::{Read, Write};
140    use std::time::SystemTime;
141
142    // Mock filesystem for testing
143    struct MockFs;
144
145    impl FsRead for MockFs {
146        fn read(&self, _path: &Path) -> Result<Vec<u8>, FsError> {
147            Ok(vec![])
148        }
149
150        fn read_to_string(&self, _path: &Path) -> Result<String, FsError> {
151            Ok(String::new())
152        }
153
154        fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> Result<Vec<u8>, FsError> {
155            Ok(vec![])
156        }
157
158        fn exists(&self, _path: &Path) -> Result<bool, FsError> {
159            Ok(true)
160        }
161
162        fn metadata(&self, _path: &Path) -> Result<Metadata, FsError> {
163            Ok(Metadata {
164                file_type: FileType::File,
165                size: 0,
166                permissions: Permissions::default_file(),
167                created: SystemTime::UNIX_EPOCH,
168                modified: SystemTime::UNIX_EPOCH,
169                accessed: SystemTime::UNIX_EPOCH,
170                inode: 1,
171                nlink: 1,
172            })
173        }
174
175        fn open_read(&self, _path: &Path) -> Result<Box<dyn Read + Send>, FsError> {
176            Ok(Box::new(std::io::empty()))
177        }
178    }
179
180    impl FsWrite for MockFs {
181        fn write(&self, _path: &Path, _data: &[u8]) -> Result<(), FsError> {
182            Ok(())
183        }
184
185        fn append(&self, _path: &Path, _data: &[u8]) -> Result<(), FsError> {
186            Ok(())
187        }
188
189        fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
190            Ok(())
191        }
192
193        fn rename(&self, _from: &Path, _to: &Path) -> Result<(), FsError> {
194            Ok(())
195        }
196
197        fn copy(&self, _from: &Path, _to: &Path) -> Result<(), FsError> {
198            Ok(())
199        }
200
201        fn truncate(&self, _path: &Path, _size: u64) -> Result<(), FsError> {
202            Ok(())
203        }
204
205        fn open_write(&self, _path: &Path) -> Result<Box<dyn Write + Send>, FsError> {
206            Ok(Box::new(std::io::sink()))
207        }
208    }
209
210    impl FsDir for MockFs {
211        fn read_dir(&self, _path: &Path) -> Result<ReadDirIter, FsError> {
212            Ok(ReadDirIter::from_vec(vec![]))
213        }
214
215        fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
216            Ok(())
217        }
218
219        fn create_dir_all(&self, _path: &Path) -> Result<(), FsError> {
220            Ok(())
221        }
222
223        fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
224            Ok(())
225        }
226
227        fn remove_dir_all(&self, _path: &Path) -> Result<(), FsError> {
228            Ok(())
229        }
230    }
231
232    // Simple pass-through resolver for testing
233    struct TestResolver;
234
235    impl PathResolver for TestResolver {
236        fn canonicalize(&self, path: &Path, _fs: &dyn Fs) -> Result<PathBuf, FsError> {
237            // Simple: just return the path as-is (no actual resolution)
238            Ok(path.to_path_buf())
239        }
240
241        fn soft_canonicalize(&self, path: &Path, _fs: &dyn Fs) -> Result<PathBuf, FsError> {
242            Ok(path.to_path_buf())
243        }
244    }
245
246    #[test]
247    fn path_resolver_can_be_boxed() {
248        // Verify PathResolver can be boxed for dynamic dispatch
249        let resolver: Box<dyn PathResolver> = Box::new(TestResolver);
250        let mock_fs = MockFs;
251        let path = Path::new("/test/path");
252
253        let result = resolver.canonicalize(path, &mock_fs);
254        assert!(result.is_ok());
255        assert_eq!(result.unwrap(), PathBuf::from("/test/path"));
256    }
257
258    #[test]
259    fn path_resolver_canonicalize_returns_path() {
260        let resolver = TestResolver;
261        let mock_fs = MockFs;
262        let path = Path::new("/some/file.txt");
263
264        let result = resolver.canonicalize(path, &mock_fs);
265        assert!(result.is_ok());
266    }
267
268    #[test]
269    fn path_resolver_soft_canonicalize_returns_path() {
270        let resolver = TestResolver;
271        let mock_fs = MockFs;
272        let path = Path::new("/some/new/file.txt");
273
274        let result = resolver.soft_canonicalize(path, &mock_fs);
275        assert!(result.is_ok());
276    }
277}