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}