1use std::path::{Component, Path, PathBuf};
31
32use crate::{FileType, FsError, FsLink, FsRead};
33
34const MAX_SYMLINK_DEPTH: usize = 40;
40
41pub trait FsPath: FsRead + FsLink {
79 fn canonicalize(&self, path: &Path) -> Result<PathBuf, FsError> {
112 default_canonicalize(self, path)
113 }
114
115 fn soft_canonicalize(&self, path: &Path) -> Result<PathBuf, FsError> {
149 default_soft_canonicalize(self, path)
150 }
151}
152
153impl<T: FsRead + FsLink> FsPath for T {}
155
156fn default_canonicalize<F: FsRead + FsLink + ?Sized>(
165 fs: &F,
166 path: &Path,
167) -> Result<PathBuf, FsError> {
168 resolve_path_internal(fs, path, 0, true)
169}
170
171fn default_soft_canonicalize<F: FsRead + FsLink + ?Sized>(
175 fs: &F,
176 path: &Path,
177) -> Result<PathBuf, FsError> {
178 let parent = path.parent();
180 let file_name = path.file_name();
181
182 match (parent, file_name) {
183 (Some(parent_path), Some(name)) if !parent_path.as_os_str().is_empty() => {
184 let resolved_parent = resolve_path_internal(fs, parent_path, 0, true)?;
186 Ok(resolved_parent.join(name))
188 }
189 (None, Some(_)) | (Some(_), Some(_)) => {
190 normalize_path(path)
192 }
193 (_, None) => {
194 default_canonicalize(fs, path)
196 }
197 }
198}
199
200fn resolve_path_internal<F: FsRead + FsLink + ?Sized>(
202 fs: &F,
203 path: &Path,
204 depth: usize,
205 require_exists: bool,
206) -> Result<PathBuf, FsError> {
207 if depth > MAX_SYMLINK_DEPTH {
208 return Err(FsError::InvalidData {
209 path: path.to_path_buf(),
210 details: format!("symlink loop detected (exceeded max depth of {MAX_SYMLINK_DEPTH})"),
211 });
212 }
213
214 let mut resolved = PathBuf::new();
215
216 for component in path.components() {
217 match component {
218 Component::RootDir => {
219 resolved = PathBuf::from("/");
220 }
221 Component::CurDir => {
222 }
224 Component::ParentDir => {
225 resolved.pop();
227 if resolved.as_os_str().is_empty() {
229 resolved = PathBuf::from("/");
230 }
231 }
232 Component::Normal(name) => {
233 resolved.push(name);
234
235 match fs.symlink_metadata(&resolved) {
237 Ok(meta) => {
238 if meta.file_type == FileType::Symlink {
239 let target = fs.read_link(&resolved)?;
241
242 resolved.pop();
244
245 let target_resolved = if target.is_absolute() {
247 resolve_path_internal(fs, &target, depth + 1, require_exists)?
248 } else {
249 let full_target = resolved.join(&target);
251 resolve_path_internal(fs, &full_target, depth + 1, require_exists)?
252 };
253
254 resolved = target_resolved;
255 }
256 }
258 Err(FsError::NotFound { .. }) if !require_exists => {
259 }
262 Err(e) => return Err(e),
263 }
264 }
265 Component::Prefix(_) => {
266 resolved.push(component);
269 }
270 }
271 }
272
273 if resolved.as_os_str().is_empty() {
275 resolved = PathBuf::from("/");
276 }
277
278 if require_exists && !fs.exists(&resolved)? {
280 return Err(FsError::NotFound { path: resolved });
281 }
282
283 Ok(resolved)
284}
285
286fn normalize_path(path: &Path) -> Result<PathBuf, FsError> {
290 let mut normalized = PathBuf::new();
291
292 for component in path.components() {
293 match component {
294 Component::RootDir => {
295 normalized = PathBuf::from("/");
296 }
297 Component::CurDir => {
298 }
300 Component::ParentDir => {
301 normalized.pop();
302 if normalized.as_os_str().is_empty() {
303 normalized = PathBuf::from("/");
304 }
305 }
306 Component::Normal(name) => {
307 normalized.push(name);
308 }
309 Component::Prefix(prefix) => {
310 normalized.push(prefix.as_os_str());
311 }
312 }
313 }
314
315 if normalized.as_os_str().is_empty() {
316 normalized = PathBuf::from("/");
317 }
318
319 Ok(normalized)
320}
321
322#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::{FsDir, FsWrite, Metadata, Permissions, ReadDirIter};
330 use std::collections::HashMap;
331 use std::io::{Read, Write};
332 use std::sync::RwLock;
333 use std::time::SystemTime;
334
335 struct MockFs {
337 entries: RwLock<HashMap<PathBuf, MockEntry>>,
338 }
339
340 #[derive(Clone)]
341 enum MockEntry {
342 File,
343 Directory,
344 Symlink(PathBuf),
345 }
346
347 impl MockFs {
348 fn new() -> Self {
349 let mut entries = HashMap::new();
350 entries.insert(PathBuf::from("/"), MockEntry::Directory);
352 Self {
353 entries: RwLock::new(entries),
354 }
355 }
356
357 fn add_file(&self, path: impl Into<PathBuf>) {
358 self.entries
359 .write()
360 .unwrap()
361 .insert(path.into(), MockEntry::File);
362 }
363
364 fn add_dir(&self, path: impl Into<PathBuf>) {
365 self.entries
366 .write()
367 .unwrap()
368 .insert(path.into(), MockEntry::Directory);
369 }
370
371 fn add_symlink(&self, path: impl Into<PathBuf>, target: impl Into<PathBuf>) {
372 self.entries
373 .write()
374 .unwrap()
375 .insert(path.into(), MockEntry::Symlink(target.into()));
376 }
377 }
378
379 impl FsRead for MockFs {
380 fn read(&self, _path: &Path) -> Result<Vec<u8>, FsError> {
381 Ok(vec![])
382 }
383
384 fn read_to_string(&self, _path: &Path) -> Result<String, FsError> {
385 Ok(String::new())
386 }
387
388 fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> Result<Vec<u8>, FsError> {
389 Ok(vec![])
390 }
391
392 fn exists(&self, path: &Path) -> Result<bool, FsError> {
393 Ok(self.entries.read().unwrap().contains_key(path))
394 }
395
396 fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
397 let entries = self.entries.read().unwrap();
398 match entries.get(path) {
399 Some(entry) => {
400 let file_type = match entry {
401 MockEntry::File => FileType::File,
402 MockEntry::Directory => FileType::Directory,
403 MockEntry::Symlink(target) => {
404 let target = target.clone();
406 drop(entries);
407 return self.metadata(&target);
408 }
409 };
410 Ok(Metadata {
411 file_type,
412 size: 0,
413 permissions: Permissions::default_file(),
414 created: SystemTime::UNIX_EPOCH,
415 modified: SystemTime::UNIX_EPOCH,
416 accessed: SystemTime::UNIX_EPOCH,
417 inode: 1,
418 nlink: 1,
419 })
420 }
421 None => Err(FsError::NotFound {
422 path: path.to_path_buf(),
423 }),
424 }
425 }
426
427 fn open_read(&self, _path: &Path) -> Result<Box<dyn Read + Send>, FsError> {
428 Ok(Box::new(std::io::empty()))
429 }
430 }
431
432 impl FsWrite for MockFs {
433 fn write(&self, _path: &Path, _data: &[u8]) -> Result<(), FsError> {
434 Ok(())
435 }
436
437 fn append(&self, _path: &Path, _data: &[u8]) -> Result<(), FsError> {
438 Ok(())
439 }
440
441 fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
442 Ok(())
443 }
444
445 fn rename(&self, _from: &Path, _to: &Path) -> Result<(), FsError> {
446 Ok(())
447 }
448
449 fn copy(&self, _from: &Path, _to: &Path) -> Result<(), FsError> {
450 Ok(())
451 }
452
453 fn truncate(&self, _path: &Path, _size: u64) -> Result<(), FsError> {
454 Ok(())
455 }
456
457 fn open_write(&self, _path: &Path) -> Result<Box<dyn Write + Send>, FsError> {
458 Ok(Box::new(std::io::sink()))
459 }
460 }
461
462 impl FsDir for MockFs {
463 fn read_dir(&self, _path: &Path) -> Result<ReadDirIter, FsError> {
464 Ok(ReadDirIter::from_vec(vec![]))
465 }
466
467 fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
468 Ok(())
469 }
470
471 fn create_dir_all(&self, _path: &Path) -> Result<(), FsError> {
472 Ok(())
473 }
474
475 fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
476 Ok(())
477 }
478
479 fn remove_dir_all(&self, _path: &Path) -> Result<(), FsError> {
480 Ok(())
481 }
482 }
483
484 impl FsLink for MockFs {
485 fn symlink(&self, _target: &Path, _link: &Path) -> Result<(), FsError> {
486 Ok(())
487 }
488
489 fn hard_link(&self, _original: &Path, _link: &Path) -> Result<(), FsError> {
490 Ok(())
491 }
492
493 fn read_link(&self, path: &Path) -> Result<PathBuf, FsError> {
494 let entries = self.entries.read().unwrap();
495 match entries.get(path) {
496 Some(MockEntry::Symlink(target)) => Ok(target.clone()),
497 Some(_) => Err(FsError::InvalidData {
498 path: path.to_path_buf(),
499 details: "not a symlink".to_string(),
500 }),
501 None => Err(FsError::NotFound {
502 path: path.to_path_buf(),
503 }),
504 }
505 }
506
507 fn symlink_metadata(&self, path: &Path) -> Result<Metadata, FsError> {
508 let entries = self.entries.read().unwrap();
509 match entries.get(path) {
510 Some(entry) => {
511 let file_type = match entry {
512 MockEntry::File => FileType::File,
513 MockEntry::Directory => FileType::Directory,
514 MockEntry::Symlink(_) => FileType::Symlink,
515 };
516 Ok(Metadata {
517 file_type,
518 size: 0,
519 permissions: Permissions::default_file(),
520 created: SystemTime::UNIX_EPOCH,
521 modified: SystemTime::UNIX_EPOCH,
522 accessed: SystemTime::UNIX_EPOCH,
523 inode: 1,
524 nlink: 1,
525 })
526 }
527 None => Err(FsError::NotFound {
528 path: path.to_path_buf(),
529 }),
530 }
531 }
532 }
533
534 #[test]
535 fn fs_path_blanket_impl_works() {
536 let fs = MockFs::new();
538 fs.add_dir(PathBuf::from("/test"));
539 fs.add_file(PathBuf::from("/test/file.txt"));
540
541 let result = fs.canonicalize(Path::new("/test/file.txt"));
543 assert!(result.is_ok());
544 }
545
546 #[test]
547 fn canonicalize_simple_path() {
548 let fs = MockFs::new();
549 fs.add_dir(PathBuf::from("/dir"));
550 fs.add_file(PathBuf::from("/dir/file.txt"));
551
552 let result = fs.canonicalize(Path::new("/dir/file.txt"));
553 assert_eq!(result.unwrap(), PathBuf::from("/dir/file.txt"));
554 }
555
556 #[test]
557 fn canonicalize_resolves_dot() {
558 let fs = MockFs::new();
559 fs.add_dir(PathBuf::from("/dir"));
560 fs.add_file(PathBuf::from("/dir/file.txt"));
561
562 let result = fs.canonicalize(Path::new("/dir/./file.txt"));
563 assert_eq!(result.unwrap(), PathBuf::from("/dir/file.txt"));
564 }
565
566 #[test]
567 fn canonicalize_resolves_dotdot() {
568 let fs = MockFs::new();
569 fs.add_dir(PathBuf::from("/dir"));
570 fs.add_dir(PathBuf::from("/dir/sub"));
571 fs.add_file(PathBuf::from("/dir/file.txt"));
572
573 let result = fs.canonicalize(Path::new("/dir/sub/../file.txt"));
574 assert_eq!(result.unwrap(), PathBuf::from("/dir/file.txt"));
575 }
576
577 #[test]
578 fn canonicalize_follows_symlink() {
579 let fs = MockFs::new();
580 fs.add_dir(PathBuf::from("/target"));
581 fs.add_file(PathBuf::from("/target/file.txt"));
582 fs.add_symlink(PathBuf::from("/link"), PathBuf::from("/target"));
583
584 let result = fs.canonicalize(Path::new("/link/file.txt"));
585 assert_eq!(result.unwrap(), PathBuf::from("/target/file.txt"));
586 }
587
588 #[test]
589 fn canonicalize_follows_relative_symlink() {
590 let fs = MockFs::new();
591 fs.add_dir(PathBuf::from("/dir"));
592 fs.add_dir(PathBuf::from("/dir/target"));
593 fs.add_file(PathBuf::from("/dir/target/file.txt"));
594 fs.add_symlink(PathBuf::from("/dir/link"), PathBuf::from("target"));
595
596 let result = fs.canonicalize(Path::new("/dir/link/file.txt"));
597 assert_eq!(result.unwrap(), PathBuf::from("/dir/target/file.txt"));
598 }
599
600 #[test]
601 fn canonicalize_detects_symlink_loop() {
602 let fs = MockFs::new();
603 fs.add_symlink(PathBuf::from("/loop1"), PathBuf::from("/loop2"));
604 fs.add_symlink(PathBuf::from("/loop2"), PathBuf::from("/loop1"));
605
606 let result = fs.canonicalize(Path::new("/loop1"));
607 assert!(result.is_err());
608 if let Err(FsError::InvalidData { details, .. }) = result {
609 assert!(details.contains("symlink loop"));
610 } else {
611 panic!("Expected InvalidData error for symlink loop");
612 }
613 }
614
615 #[test]
616 fn canonicalize_not_found() {
617 let fs = MockFs::new();
618
619 let result = fs.canonicalize(Path::new("/nonexistent"));
620 assert!(matches!(result, Err(FsError::NotFound { .. })));
621 }
622
623 #[test]
624 fn soft_canonicalize_allows_nonexistent_final() {
625 let fs = MockFs::new();
626 fs.add_dir(PathBuf::from("/dir"));
627
628 let result = fs.soft_canonicalize(Path::new("/dir/new_file.txt"));
630 assert_eq!(result.unwrap(), PathBuf::from("/dir/new_file.txt"));
631 }
632
633 #[test]
634 fn soft_canonicalize_resolves_parent_symlink() {
635 let fs = MockFs::new();
636 fs.add_dir(PathBuf::from("/target"));
637 fs.add_symlink(PathBuf::from("/link"), PathBuf::from("/target"));
638
639 let result = fs.soft_canonicalize(Path::new("/link/new.txt"));
641 assert_eq!(result.unwrap(), PathBuf::from("/target/new.txt"));
642 }
643
644 #[test]
645 fn soft_canonicalize_fails_for_nonexistent_parent() {
646 let fs = MockFs::new();
647 let result = fs.soft_canonicalize(Path::new("/nonexistent/file.txt"));
650 assert!(matches!(result, Err(FsError::NotFound { .. })));
651 }
652
653 #[test]
654 fn canonicalize_root() {
655 let fs = MockFs::new();
656
657 let result = fs.canonicalize(Path::new("/"));
658 assert_eq!(result.unwrap(), PathBuf::from("/"));
659 }
660
661 #[test]
662 fn normalize_path_handles_dots() {
663 let result = normalize_path(Path::new("/a/./b/../c"));
664 assert_eq!(result.unwrap(), PathBuf::from("/a/c"));
665 }
666
667 #[test]
668 fn normalize_path_handles_root() {
669 let result = normalize_path(Path::new("/"));
670 assert_eq!(result.unwrap(), PathBuf::from("/"));
671 }
672}