1#![allow(
8 clippy::cast_sign_loss,
9 clippy::cast_possible_wrap,
10 clippy::cast_possible_truncation
11)]
12
13use crate::cache::{NegativeCache, NegativeCacheConfig};
14use crate::error::{FsError, Result};
15use std::collections::HashMap;
16use std::ffi::{OsStr, OsString};
17use std::fs::{File, OpenOptions};
18use std::io::{Read, Seek, SeekFrom, Write};
19use std::os::unix::ffi::OsStrExt;
20use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
21use std::path::{Path, PathBuf};
22use std::sync::RwLock;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::time::Duration;
25
26#[derive(Debug)]
32#[allow(dead_code)]
33struct InodeData {
34 path: PathBuf,
36 refcount: AtomicU64,
38 file_type: FileType,
40 kernel_ino: u64,
47}
48
49impl InodeData {
50 fn new(path: PathBuf, file_type: FileType, kernel_ino: u64) -> Self {
51 Self {
52 path,
53 refcount: AtomicU64::new(1),
54 file_type,
55 kernel_ino,
56 }
57 }
58
59 fn inc_ref(&self) {
60 self.refcount.fetch_add(1, Ordering::Relaxed);
61 }
62
63 fn dec_ref(&self) -> u64 {
64 self.refcount.fetch_sub(1, Ordering::Relaxed)
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum FileType {
71 Regular,
72 Directory,
73 Symlink,
74 BlockDevice,
75 CharDevice,
76 Fifo,
77 Socket,
78 Unknown,
79}
80
81impl FileType {
82 fn from_mode(mode: u32) -> Self {
83 let file_type = mode & u32::from(libc::S_IFMT);
84 if file_type == u32::from(libc::S_IFREG) {
85 Self::Regular
86 } else if file_type == u32::from(libc::S_IFDIR) {
87 Self::Directory
88 } else if file_type == u32::from(libc::S_IFLNK) {
89 Self::Symlink
90 } else if file_type == u32::from(libc::S_IFBLK) {
91 Self::BlockDevice
92 } else if file_type == u32::from(libc::S_IFCHR) {
93 Self::CharDevice
94 } else if file_type == u32::from(libc::S_IFIFO) {
95 Self::Fifo
96 } else if file_type == u32::from(libc::S_IFSOCK) {
97 Self::Socket
98 } else {
99 Self::Unknown
100 }
101 }
102
103 #[allow(dead_code)]
104 fn is_dir(self) -> bool {
105 self == Self::Directory
106 }
107
108 #[must_use]
110 pub fn to_dirent_type(self) -> u32 {
111 match self {
112 Self::Regular => libc::DT_REG as u32,
113 Self::Directory => libc::DT_DIR as u32,
114 Self::Symlink => libc::DT_LNK as u32,
115 Self::BlockDevice => libc::DT_BLK as u32,
116 Self::CharDevice => libc::DT_CHR as u32,
117 Self::Fifo => libc::DT_FIFO as u32,
118 Self::Socket => libc::DT_SOCK as u32,
119 Self::Unknown => libc::DT_UNKNOWN as u32,
120 }
121 }
122}
123
124#[derive(Debug)]
130#[allow(dead_code)]
131struct HandleData {
132 file: File,
134 inode: u64,
136 flags: u32,
138}
139
140#[derive(Debug)]
142struct DirHandleData {
143 inode: u64,
145 entries: Vec<DirEntry>,
147}
148
149#[derive(Debug, Clone)]
151pub struct DirEntry {
152 pub name: OsString,
154 pub ino: u64,
156 pub file_type: FileType,
158}
159
160#[derive(Debug, Clone)]
166pub struct PassthroughConfig {
167 pub negative_cache_enabled: bool,
169 pub negative_cache_max_entries: usize,
171 pub negative_cache_timeout: Duration,
173}
174
175impl Default for PassthroughConfig {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181impl PassthroughConfig {
182 #[must_use]
184 pub const fn new() -> Self {
185 Self {
186 negative_cache_enabled: true,
187 negative_cache_max_entries: 10_000,
188 negative_cache_timeout: Duration::from_secs(5),
189 }
190 }
191}
192
193pub struct PassthroughFs {
203 root: PathBuf,
205 inodes: RwLock<HashMap<u64, InodeData>>,
207 next_inode: AtomicU64,
209 handles: RwLock<HashMap<u64, HandleData>>,
211 dir_handles: RwLock<HashMap<u64, DirHandleData>>,
213 next_handle: AtomicU64,
215 negative_cache: Option<NegativeCache>,
217 #[allow(dead_code)]
219 config: PassthroughConfig,
220}
221
222impl PassthroughFs {
223 pub const ROOT_INODE: u64 = 1;
225
226 pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
232 Self::with_config(root, PassthroughConfig::default())
233 }
234
235 pub fn with_config(root: impl Into<PathBuf>, config: PassthroughConfig) -> Result<Self> {
241 let root = root.into();
242 if !root.is_dir() {
243 return Err(FsError::InvalidPath(format!(
244 "root path is not a directory: {}",
245 root.display()
246 )));
247 }
248
249 let negative_cache = if config.negative_cache_enabled {
250 Some(NegativeCache::new(NegativeCacheConfig {
251 max_entries: config.negative_cache_max_entries,
252 timeout: config.negative_cache_timeout,
253 adaptive_ttl: Some(crate::cache::AdaptiveTtlConfig::default()),
254 }))
255 } else {
256 None
257 };
258
259 let root_kernel_ino = {
266 use std::os::unix::fs::MetadataExt;
267 std::fs::symlink_metadata(&root)
268 .map(|m| m.ino())
269 .map_err(|e| {
270 FsError::io(std::io::Error::new(
271 e.kind(),
272 format!("failed to stat root path '{}': {e}", root.display()),
273 ))
274 })?
275 };
276 let mut inodes = HashMap::new();
277 inodes.insert(
278 Self::ROOT_INODE,
279 InodeData::new(PathBuf::new(), FileType::Directory, root_kernel_ino),
280 );
281
282 Ok(Self {
283 root,
284 inodes: RwLock::new(inodes),
285 next_inode: AtomicU64::new(Self::ROOT_INODE + 1),
286 handles: RwLock::new(HashMap::new()),
287 dir_handles: RwLock::new(HashMap::new()),
288 next_handle: AtomicU64::new(1),
289 negative_cache,
290 config,
291 })
292 }
293
294 #[must_use]
296 pub fn root(&self) -> &Path {
297 &self.root
298 }
299
300 #[must_use]
302 pub fn negative_cache(&self) -> Option<&NegativeCache> {
303 self.negative_cache.as_ref()
304 }
305
306 fn alloc_inode(&self) -> u64 {
312 self.next_inode.fetch_add(1, Ordering::Relaxed)
313 }
314
315 fn alloc_handle(&self) -> u64 {
317 self.next_handle.fetch_add(1, Ordering::Relaxed)
318 }
319
320 pub(crate) fn inode_path(&self, inode: u64) -> Result<PathBuf> {
322 if inode == Self::ROOT_INODE {
323 return Ok(self.root.clone());
324 }
325
326 let inodes = self
327 .inodes
328 .read()
329 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
330
331 let data = inodes.get(&inode).ok_or(FsError::InvalidHandle(inode))?;
332 Ok(self.root.join(&data.path))
333 }
334
335 pub(crate) fn kernel_ino_for(&self, inode: u64) -> Option<u64> {
342 let inodes = self
343 .inodes
344 .read()
345 .unwrap_or_else(std::sync::PoisonError::into_inner);
346 inodes.get(&inode).map(|d| d.kernel_ino)
347 }
348
349 #[allow(clippy::significant_drop_tightening)]
351 fn get_path(&self, parent: u64, name: &OsStr) -> Result<PathBuf> {
352 if parent == Self::ROOT_INODE {
353 return Ok(self.root.join(name));
354 }
355
356 let inodes = self
357 .inodes
358 .read()
359 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
360
361 let parent_data = inodes.get(&parent).ok_or(FsError::InvalidHandle(parent))?;
362 Ok(self.root.join(&parent_data.path).join(name))
363 }
364
365 fn relative_path(&self, path: &Path) -> PathBuf {
367 path.strip_prefix(&self.root)
368 .map_or_else(|_| path.to_path_buf(), Path::to_path_buf)
369 }
370
371 #[allow(clippy::cast_possible_truncation)]
373 fn metadata_to_attr(ino: u64, metadata: &std::fs::Metadata) -> crate::fuse::FuseAttr {
374 crate::fuse::FuseAttr {
375 ino,
376 size: metadata.len(),
377 blocks: metadata.blocks(),
378 atime: metadata.atime() as u64,
379 mtime: metadata.mtime() as u64,
380 ctime: metadata.ctime() as u64,
381 atimensec: metadata.atime_nsec() as u32,
382 mtimensec: metadata.mtime_nsec() as u32,
383 ctimensec: metadata.ctime_nsec() as u32,
384 mode: metadata.mode(),
385 nlink: metadata.nlink() as u32,
386 uid: metadata.uid(),
387 gid: metadata.gid(),
388 rdev: metadata.rdev() as u32,
389 blksize: metadata.blksize() as u32,
390 padding: 0,
391 }
392 }
393
394 fn invalidate_negative_cache(&self, path: &Path) {
396 if let Some(ref cache) = self.negative_cache {
397 tracing::trace!(path = %path.display(), "invalidating negative cache");
398 cache.invalidate(path);
399 }
400 }
401
402 pub fn lookup(&self, parent: u64, name: &OsStr) -> Result<(u64, crate::fuse::FuseAttr)> {
414 let path = self.get_path(parent, name)?;
415
416 if let Some(ref cache) = self.negative_cache {
418 if cache.contains(&path) {
419 tracing::trace!(path = %path.display(), "negative cache hit");
420 return Err(FsError::not_found(path.display().to_string()));
421 }
422 }
423
424 let metadata = match std::fs::symlink_metadata(&path) {
426 Ok(m) => m,
427 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
428 if let Some(ref cache) = self.negative_cache {
430 tracing::trace!(path = %path.display(), "adding to negative cache");
431 cache.insert(path.clone());
432 }
433 return Err(FsError::not_found(path.display().to_string()));
434 }
435 Err(e) => return Err(FsError::io(e)),
436 };
437
438 let file_type = FileType::from_mode(metadata.mode());
439 let relative = self.relative_path(&path);
440
441 {
443 let inodes = self
444 .inodes
445 .read()
446 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
447 for (&ino, data) in inodes.iter() {
448 if data.path == relative {
449 data.inc_ref();
450 return Ok((ino, Self::metadata_to_attr(ino, &metadata)));
451 }
452 }
453 }
454
455 let kernel_ino = metadata.ino();
457 let inode = self.alloc_inode();
458 {
459 let mut inodes = self
460 .inodes
461 .write()
462 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
463 inodes.insert(inode, InodeData::new(relative, file_type, kernel_ino));
464 }
465
466 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
467 }
468
469 pub fn forget(&self, inode: u64, nlookup: u64) {
473 if inode == Self::ROOT_INODE {
474 return;
475 }
476
477 let should_remove = {
478 let inodes = match self.inodes.read() {
479 Ok(i) => i,
480 Err(_) => return,
481 };
482 if let Some(data) = inodes.get(&inode) {
483 for _ in 0..nlookup {
484 if data.dec_ref() == 1 {
485 return;
486 }
487 }
488 data.refcount.load(Ordering::Relaxed) == 0
489 } else {
490 false
491 }
492 };
493
494 if should_remove {
495 if let Ok(mut inodes) = self.inodes.write() {
496 inodes.remove(&inode);
497 }
498 }
499 }
500
501 pub fn getattr(&self, inode: u64) -> Result<crate::fuse::FuseAttr> {
508 let path = self.inode_path(inode)?;
509 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
510 Ok(Self::metadata_to_attr(inode, &metadata))
511 }
512
513 #[allow(clippy::too_many_arguments)]
520 pub fn setattr(
521 &self,
522 inode: u64,
523 mode: Option<u32>,
524 uid: Option<u32>,
525 gid: Option<u32>,
526 size: Option<u64>,
527 atime: Option<(i64, u32)>,
528 mtime: Option<(i64, u32)>,
529 ) -> Result<crate::fuse::FuseAttr> {
530 let path = self.inode_path(inode)?;
531
532 if let Some(mode) = mode {
534 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
535 .map_err(FsError::io)?;
536 }
537
538 if uid.is_some() || gid.is_some() {
540 let uid = uid.map_or(-1_i32 as libc::uid_t, |u| u);
541 let gid = gid.map_or(-1_i32 as libc::gid_t, |g| g);
542 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
543 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
544 let ret = unsafe { libc::chown(path_cstr.as_ptr(), uid, gid) };
545 if ret != 0 {
546 return Err(FsError::io(std::io::Error::last_os_error()));
547 }
548 }
549
550 if let Some(size) = size {
552 let file = OpenOptions::new()
553 .write(true)
554 .open(&path)
555 .map_err(FsError::io)?;
556 file.set_len(size).map_err(FsError::io)?;
557 }
558
559 if atime.is_some() || mtime.is_some() {
561 let atime_spec = atime.map_or(
562 libc::timespec {
563 tv_sec: 0,
564 tv_nsec: libc::UTIME_OMIT,
565 },
566 |(sec, nsec)| libc::timespec {
567 tv_sec: sec,
568 tv_nsec: i64::from(nsec),
569 },
570 );
571 let mtime_spec = mtime.map_or(
572 libc::timespec {
573 tv_sec: 0,
574 tv_nsec: libc::UTIME_OMIT,
575 },
576 |(sec, nsec)| libc::timespec {
577 tv_sec: sec,
578 tv_nsec: i64::from(nsec),
579 },
580 );
581 let times = [atime_spec, mtime_spec];
582 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
583 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
584 let ret =
585 unsafe { libc::utimensat(libc::AT_FDCWD, path_cstr.as_ptr(), times.as_ptr(), 0) };
586 if ret != 0 {
587 return Err(FsError::io(std::io::Error::last_os_error()));
588 }
589 }
590
591 self.getattr(inode)
592 }
593
594 pub fn readlink(&self, inode: u64) -> Result<PathBuf> {
601 let path = self.inode_path(inode)?;
602 std::fs::read_link(&path).map_err(FsError::io)
603 }
604
605 pub fn create(
616 &self,
617 parent: u64,
618 name: &OsStr,
619 mode: u32,
620 flags: u32,
621 ) -> Result<(u64, crate::fuse::FuseAttr, u64)> {
622 let path = self.get_path(parent, name)?;
623
624 let mut opts = OpenOptions::new();
626 Self::apply_flags(&mut opts, flags);
627 opts.create(true);
628 opts.mode(mode & 0o7777);
629
630 let file = opts.open(&path).map_err(FsError::io)?;
631 let metadata = file.metadata().map_err(FsError::io)?;
632
633 self.invalidate_negative_cache(&path);
635
636 let relative = self.relative_path(&path);
638 let kernel_ino = metadata.ino();
639 let inode = self.alloc_inode();
640 {
641 let mut inodes = self
642 .inodes
643 .write()
644 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
645 inodes.insert(
646 inode,
647 InodeData::new(relative, FileType::Regular, kernel_ino),
648 );
649 }
650
651 let handle = self.alloc_handle();
653 {
654 let mut handles = self
655 .handles
656 .write()
657 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
658 handles.insert(handle, HandleData { file, inode, flags });
659 }
660
661 Ok((inode, Self::metadata_to_attr(inode, &metadata), handle))
662 }
663
664 pub fn mkdir(
671 &self,
672 parent: u64,
673 name: &OsStr,
674 mode: u32,
675 ) -> Result<(u64, crate::fuse::FuseAttr)> {
676 let path = self.get_path(parent, name)?;
677
678 std::fs::create_dir(&path).map_err(FsError::io)?;
679 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode & 0o7777))
680 .map_err(FsError::io)?;
681
682 self.invalidate_negative_cache(&path);
683
684 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
685 let relative = self.relative_path(&path);
686 let kernel_ino = metadata.ino();
687 let inode = self.alloc_inode();
688
689 {
690 let mut inodes = self
691 .inodes
692 .write()
693 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
694 inodes.insert(
695 inode,
696 InodeData::new(relative, FileType::Directory, kernel_ino),
697 );
698 }
699
700 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
701 }
702
703 pub fn symlink(
710 &self,
711 parent: u64,
712 name: &OsStr,
713 target: &Path,
714 ) -> Result<(u64, crate::fuse::FuseAttr)> {
715 let path = self.get_path(parent, name)?;
716
717 std::os::unix::fs::symlink(target, &path).map_err(FsError::io)?;
718
719 self.invalidate_negative_cache(&path);
720
721 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
722 let relative = self.relative_path(&path);
723 let kernel_ino = metadata.ino();
724 let inode = self.alloc_inode();
725
726 {
727 let mut inodes = self
728 .inodes
729 .write()
730 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
731 inodes.insert(
732 inode,
733 InodeData::new(relative, FileType::Symlink, kernel_ino),
734 );
735 }
736
737 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
738 }
739
740 pub fn link(
747 &self,
748 inode: u64,
749 new_parent: u64,
750 new_name: &OsStr,
751 ) -> Result<(u64, crate::fuse::FuseAttr)> {
752 let source_path = self.inode_path(inode)?;
753 let new_path = self.get_path(new_parent, new_name)?;
754
755 std::fs::hard_link(&source_path, &new_path).map_err(FsError::io)?;
756
757 self.invalidate_negative_cache(&new_path);
758
759 let metadata = std::fs::symlink_metadata(&new_path).map_err(FsError::io)?;
761
762 {
764 let inodes = self
765 .inodes
766 .read()
767 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
768 if let Some(data) = inodes.get(&inode) {
769 data.inc_ref();
770 }
771 }
772
773 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
774 }
775
776 #[allow(clippy::cast_possible_truncation)]
783 pub fn mknod(
784 &self,
785 parent: u64,
786 name: &OsStr,
787 mode: u32,
788 rdev: u64,
789 ) -> Result<(u64, crate::fuse::FuseAttr)> {
790 let path = self.get_path(parent, name)?;
791 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
792 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
793
794 let ret = unsafe {
795 libc::mknod(
796 path_cstr.as_ptr(),
797 mode as libc::mode_t,
798 rdev as libc::dev_t,
799 )
800 };
801 if ret != 0 {
802 return Err(FsError::io(std::io::Error::last_os_error()));
803 }
804
805 self.invalidate_negative_cache(&path);
806
807 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
808 let file_type = FileType::from_mode(metadata.mode());
809 let relative = self.relative_path(&path);
810 let kernel_ino = metadata.ino();
811 let inode = self.alloc_inode();
812
813 {
814 let mut inodes = self
815 .inodes
816 .write()
817 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
818 inodes.insert(inode, InodeData::new(relative, file_type, kernel_ino));
819 }
820
821 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
822 }
823
824 pub fn unlink(&self, parent: u64, name: &OsStr) -> Result<()> {
835 let path = self.get_path(parent, name)?;
836 std::fs::remove_file(&path).map_err(FsError::io)?;
837
838 Ok(())
842 }
843
844 pub fn rmdir(&self, parent: u64, name: &OsStr) -> Result<()> {
851 let path = self.get_path(parent, name)?;
852 std::fs::remove_dir(&path).map_err(FsError::io)
853 }
854
855 pub fn rename(
862 &self,
863 parent: u64,
864 name: &OsStr,
865 new_parent: u64,
866 new_name: &OsStr,
867 _flags: u32,
868 ) -> Result<()> {
869 let old_path = self.get_path(parent, name)?;
870 let new_path = self.get_path(new_parent, new_name)?;
871
872 std::fs::rename(&old_path, &new_path).map_err(FsError::io)?;
873
874 self.invalidate_negative_cache(&old_path);
876 self.invalidate_negative_cache(&new_path);
877
878 let old_relative = self.relative_path(&old_path);
880 let new_relative = self.relative_path(&new_path);
881
882 {
883 let mut inodes = self
884 .inodes
885 .write()
886 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
887 for data in inodes.values_mut() {
888 if data.path == old_relative {
889 data.path = new_relative;
890 break;
891 }
892 }
893 }
894
895 Ok(())
896 }
897
898 fn apply_flags(opts: &mut OpenOptions, flags: u32) {
904 let access_mode = flags & libc::O_ACCMODE as u32;
905 match access_mode {
906 x if x == libc::O_RDONLY as u32 => {
907 opts.read(true);
908 }
909 x if x == libc::O_WRONLY as u32 => {
910 opts.write(true);
911 }
912 x if x == libc::O_RDWR as u32 => {
913 opts.read(true).write(true);
914 }
915 _ => {
916 opts.read(true);
917 }
918 }
919
920 if flags & libc::O_APPEND as u32 != 0 {
921 opts.append(true);
922 }
923 if flags & libc::O_TRUNC as u32 != 0 {
924 opts.truncate(true);
925 }
926 }
927
928 pub fn open(&self, inode: u64, flags: u32) -> Result<u64> {
935 let path = self.inode_path(inode)?;
936
937 let mut opts = OpenOptions::new();
938 Self::apply_flags(&mut opts, flags);
939
940 let file = opts.open(&path).map_err(FsError::io)?;
941 let handle = self.alloc_handle();
942
943 {
944 let mut handles = self
945 .handles
946 .write()
947 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
948 handles.insert(handle, HandleData { file, inode, flags });
949 }
950
951 Ok(handle)
952 }
953
954 pub fn read(&self, handle: u64, offset: u64, size: u32) -> Result<Vec<u8>> {
961 let mut handles = self
962 .handles
963 .write()
964 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
965
966 let data = handles
967 .get_mut(&handle)
968 .ok_or(FsError::InvalidHandle(handle))?;
969
970 data.file
971 .seek(SeekFrom::Start(offset))
972 .map_err(FsError::io)?;
973
974 let mut buf = vec![0u8; size as usize];
975 let n = data.file.read(&mut buf).map_err(FsError::io)?;
976 buf.truncate(n);
977
978 Ok(buf)
979 }
980
981 pub fn write(&self, handle: u64, offset: u64, data: &[u8], _flags: u32) -> Result<u32> {
988 let mut handles = self
989 .handles
990 .write()
991 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
992
993 let handle_data = handles
994 .get_mut(&handle)
995 .ok_or(FsError::InvalidHandle(handle))?;
996
997 handle_data
998 .file
999 .seek(SeekFrom::Start(offset))
1000 .map_err(FsError::io)?;
1001 let n = handle_data.file.write(data).map_err(FsError::io)?;
1002
1003 #[allow(clippy::cast_possible_truncation)]
1004 Ok(n as u32)
1005 }
1006
1007 pub fn flush(&self, handle: u64) -> Result<()> {
1014 let mut handles = self
1015 .handles
1016 .write()
1017 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1018
1019 let data = handles
1020 .get_mut(&handle)
1021 .ok_or(FsError::InvalidHandle(handle))?;
1022 data.file.flush().map_err(FsError::io)
1023 }
1024
1025 pub fn fsync(&self, handle: u64, datasync: bool) -> Result<()> {
1032 let handles = self
1033 .handles
1034 .read()
1035 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1036
1037 let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
1038
1039 if datasync {
1040 data.file.sync_data().map_err(FsError::io)
1041 } else {
1042 data.file.sync_all().map_err(FsError::io)
1043 }
1044 }
1045
1046 pub fn release(&self, handle: u64) -> Result<()> {
1048 let mut handles = self
1049 .handles
1050 .write()
1051 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1052
1053 handles.remove(&handle);
1054 Ok(())
1055 }
1056
1057 pub fn get_file_raw_fd(&self, handle: u64) -> Option<std::os::unix::io::RawFd> {
1059 use std::os::unix::io::AsRawFd;
1060 let handles = self.handles.read().ok()?;
1061 handles.get(&handle).map(|h| h.file.as_raw_fd())
1062 }
1063
1064 pub fn lseek(&self, handle: u64, offset: i64, whence: u32) -> Result<u64> {
1071 let mut handles = self
1072 .handles
1073 .write()
1074 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1075
1076 let data = handles
1077 .get_mut(&handle)
1078 .ok_or(FsError::InvalidHandle(handle))?;
1079
1080 let seek_from = match whence {
1081 0 => SeekFrom::Start(offset as u64), 1 => SeekFrom::Current(offset), 2 => SeekFrom::End(offset), _ => return Err(FsError::InvalidPath("invalid whence".to_string())),
1085 };
1086
1087 data.file.seek(seek_from).map_err(FsError::io)
1088 }
1089
1090 #[cfg(target_os = "linux")]
1097 pub fn fallocate(&self, handle: u64, mode: u32, offset: u64, length: u64) -> Result<()> {
1098 let handles = self
1099 .handles
1100 .read()
1101 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1102
1103 let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
1104 let fd = data.file.as_raw_fd();
1105
1106 #[allow(clippy::cast_possible_wrap)]
1107 let ret = unsafe { libc::fallocate(fd, mode as i32, offset as i64, length as i64) };
1108
1109 if ret != 0 {
1110 Err(FsError::io(std::io::Error::last_os_error()))
1111 } else {
1112 Ok(())
1113 }
1114 }
1115
1116 #[cfg(target_os = "macos")]
1117 pub fn fallocate(&self, handle: u64, _mode: u32, offset: u64, length: u64) -> Result<()> {
1118 let mut handles = self
1120 .handles
1121 .write()
1122 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1123
1124 let data = handles
1125 .get_mut(&handle)
1126 .ok_or(FsError::InvalidHandle(handle))?;
1127 let new_size = offset + length;
1128 data.file.set_len(new_size).map_err(FsError::io)
1129 }
1130
1131 pub fn opendir(&self, inode: u64) -> Result<u64> {
1142 let path = self.inode_path(inode)?;
1143
1144 let mut entries = Vec::new();
1146
1147 entries.push(DirEntry {
1149 name: OsString::from("."),
1150 ino: inode,
1151 file_type: FileType::Directory,
1152 });
1153
1154 entries.push(DirEntry {
1156 name: OsString::from(".."),
1157 ino: Self::ROOT_INODE,
1158 file_type: FileType::Directory,
1159 });
1160
1161 for entry in std::fs::read_dir(&path).map_err(FsError::io)? {
1163 let entry = entry.map_err(FsError::io)?;
1164 let metadata = entry.metadata().map_err(FsError::io)?;
1165 let file_type = FileType::from_mode(metadata.mode());
1166
1167 let entry_path = entry.path();
1169 let relative = self.relative_path(&entry_path);
1170 let entry_ino = {
1171 let inodes = self
1172 .inodes
1173 .read()
1174 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
1175 let mut found_ino = None;
1176 for (&ino, data) in inodes.iter() {
1177 if data.path == relative {
1178 found_ino = Some(ino);
1179 break;
1180 }
1181 }
1182 found_ino
1183 };
1184
1185 let ino = if let Some(ino) = entry_ino {
1186 ino
1187 } else {
1188 let kernel_ino = metadata.ino();
1190 let new_ino = self.alloc_inode();
1191 let mut inodes = self
1192 .inodes
1193 .write()
1194 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
1195 inodes.insert(new_ino, InodeData::new(relative, file_type, kernel_ino));
1196 new_ino
1197 };
1198
1199 entries.push(DirEntry {
1200 name: entry.file_name(),
1201 ino,
1202 file_type,
1203 });
1204 }
1205
1206 let handle = self.alloc_handle();
1207 {
1208 let mut dir_handles = self
1209 .dir_handles
1210 .write()
1211 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1212 dir_handles.insert(handle, DirHandleData { inode, entries });
1213 }
1214
1215 Ok(handle)
1216 }
1217
1218 pub fn readdir(&self, handle: u64, offset: u64) -> Result<Vec<DirEntry>> {
1226 let dir_handles = self
1227 .dir_handles
1228 .read()
1229 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1230
1231 let data = dir_handles
1232 .get(&handle)
1233 .ok_or(FsError::InvalidHandle(handle))?;
1234
1235 let entries: Vec<DirEntry> = data.entries.iter().skip(offset as usize).cloned().collect();
1236
1237 Ok(entries)
1238 }
1239
1240 pub fn releasedir(&self, handle: u64) -> Result<()> {
1242 let mut dir_handles = self
1243 .dir_handles
1244 .write()
1245 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1246
1247 dir_handles.remove(&handle);
1248 Ok(())
1249 }
1250
1251 pub fn fsyncdir(&self, handle: u64, _datasync: bool) -> Result<()> {
1258 let dir_handles = self
1259 .dir_handles
1260 .read()
1261 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1262
1263 let data = dir_handles
1264 .get(&handle)
1265 .ok_or(FsError::InvalidHandle(handle))?;
1266 let path = self.inode_path(data.inode)?;
1267
1268 let dir = File::open(&path).map_err(FsError::io)?;
1270 dir.sync_all().map_err(FsError::io)
1271 }
1272
1273 #[cfg(target_os = "linux")]
1284 pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
1285 use std::os::unix::ffi::OsStrExt;
1286
1287 let path = self.inode_path(inode)?;
1288 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1289 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1290 let name_cstr = std::ffi::CString::new(name.as_bytes())
1291 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1292
1293 if size == 0 {
1294 let ret = unsafe {
1296 libc::getxattr(
1297 path_cstr.as_ptr(),
1298 name_cstr.as_ptr(),
1299 std::ptr::null_mut(),
1300 0,
1301 )
1302 };
1303 if ret < 0 {
1304 return Err(FsError::io(std::io::Error::last_os_error()));
1305 }
1306 Ok(vec![0u8; ret as usize])
1307 } else {
1308 let mut buf = vec![0u8; size as usize];
1309 let ret = unsafe {
1310 libc::getxattr(
1311 path_cstr.as_ptr(),
1312 name_cstr.as_ptr(),
1313 buf.as_mut_ptr().cast(),
1314 size as usize,
1315 )
1316 };
1317 if ret < 0 {
1318 return Err(FsError::io(std::io::Error::last_os_error()));
1319 }
1320 buf.truncate(ret as usize);
1321 Ok(buf)
1322 }
1323 }
1324
1325 #[cfg(target_os = "macos")]
1326 pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
1327 let path = self.inode_path(inode)?;
1328 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1329 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1330 let name_cstr = std::ffi::CString::new(name.as_bytes())
1331 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1332
1333 if size == 0 {
1334 let ret = unsafe {
1335 libc::getxattr(
1336 path_cstr.as_ptr(),
1337 name_cstr.as_ptr(),
1338 std::ptr::null_mut(),
1339 0,
1340 0,
1341 0,
1342 )
1343 };
1344 if ret < 0 {
1345 return Err(FsError::io(std::io::Error::last_os_error()));
1346 }
1347 Ok(vec![0u8; ret as usize])
1348 } else {
1349 let mut buf = vec![0u8; size as usize];
1350 let ret = unsafe {
1351 libc::getxattr(
1352 path_cstr.as_ptr(),
1353 name_cstr.as_ptr(),
1354 buf.as_mut_ptr().cast(),
1355 size as usize,
1356 0,
1357 0,
1358 )
1359 };
1360 if ret < 0 {
1361 return Err(FsError::io(std::io::Error::last_os_error()));
1362 }
1363 buf.truncate(ret as usize);
1364 Ok(buf)
1365 }
1366 }
1367
1368 #[cfg(target_os = "linux")]
1375 pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
1376 let path = self.inode_path(inode)?;
1377 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1378 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1379 let name_cstr = std::ffi::CString::new(name.as_bytes())
1380 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1381
1382 let ret = unsafe {
1383 libc::setxattr(
1384 path_cstr.as_ptr(),
1385 name_cstr.as_ptr(),
1386 value.as_ptr().cast(),
1387 value.len(),
1388 flags as i32,
1389 )
1390 };
1391 if ret != 0 {
1392 Err(FsError::io(std::io::Error::last_os_error()))
1393 } else {
1394 Ok(())
1395 }
1396 }
1397
1398 #[cfg(target_os = "macos")]
1399 pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
1400 let path = self.inode_path(inode)?;
1401 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1402 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1403 let name_cstr = std::ffi::CString::new(name.as_bytes())
1404 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1405
1406 let ret = unsafe {
1407 libc::setxattr(
1408 path_cstr.as_ptr(),
1409 name_cstr.as_ptr(),
1410 value.as_ptr().cast(),
1411 value.len(),
1412 0,
1413 flags as i32,
1414 )
1415 };
1416 if ret != 0 {
1417 Err(FsError::io(std::io::Error::last_os_error()))
1418 } else {
1419 Ok(())
1420 }
1421 }
1422
1423 #[cfg(target_os = "linux")]
1430 pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
1431 let path = self.inode_path(inode)?;
1432 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1433 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1434 let name_cstr = std::ffi::CString::new(name.as_bytes())
1435 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1436
1437 let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr()) };
1438 if ret != 0 {
1439 Err(FsError::io(std::io::Error::last_os_error()))
1440 } else {
1441 Ok(())
1442 }
1443 }
1444
1445 #[cfg(target_os = "macos")]
1446 pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
1447 let path = self.inode_path(inode)?;
1448 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1449 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1450 let name_cstr = std::ffi::CString::new(name.as_bytes())
1451 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1452
1453 let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr(), 0) };
1454 if ret != 0 {
1455 Err(FsError::io(std::io::Error::last_os_error()))
1456 } else {
1457 Ok(())
1458 }
1459 }
1460
1461 pub fn statfs(&self) -> Result<crate::fuse::StatFs> {
1471 let path_cstr = std::ffi::CString::new(self.root.as_os_str().as_bytes())
1472 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1473
1474 let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
1475 let ret = unsafe { libc::statfs(path_cstr.as_ptr(), &raw mut stat) };
1476 if ret != 0 {
1477 return Err(FsError::io(std::io::Error::last_os_error()));
1478 }
1479
1480 #[allow(
1481 clippy::cast_sign_loss,
1482 clippy::cast_possible_truncation,
1483 clippy::unnecessary_cast )]
1485 Ok(crate::fuse::StatFs {
1486 blocks: stat.f_blocks as u64,
1487 bfree: stat.f_bfree as u64,
1488 bavail: stat.f_bavail as u64,
1489 files: stat.f_files as u64,
1490 ffree: stat.f_ffree as u64,
1491 bsize: stat.f_bsize as u32,
1492 namelen: 255, frsize: stat.f_bsize as u32,
1494 })
1495 }
1496
1497 pub fn access(&self, inode: u64, mask: u32) -> Result<()> {
1504 let path = self.inode_path(inode)?;
1505 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1506 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1507
1508 #[allow(clippy::cast_possible_wrap)]
1509 let ret = unsafe { libc::access(path_cstr.as_ptr(), mask as i32) };
1510 if ret != 0 {
1511 let err = std::io::Error::last_os_error();
1512 if err.raw_os_error() == Some(libc::EACCES) {
1513 Err(FsError::permission_denied(path.display().to_string()))
1514 } else {
1515 Err(FsError::io(err))
1516 }
1517 } else {
1518 Ok(())
1519 }
1520 }
1521}
1522
1523#[allow(clippy::missing_fields_in_debug)]
1527impl std::fmt::Debug for PassthroughFs {
1528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1529 f.debug_struct("PassthroughFs")
1530 .field("root", &self.root)
1531 .field("inodes", &self.inodes.read().map_or(0, |i| i.len()))
1532 .field("handles", &self.handles.read().map_or(0, |h| h.len()))
1533 .field(
1534 "dir_handles",
1535 &self.dir_handles.read().map_or(0, |h| h.len()),
1536 )
1537 .finish()
1538 }
1539}
1540
1541#[cfg(test)]
1546mod tests {
1547 use super::*;
1548 use tempfile::TempDir;
1549
1550 const S_IFDIR: u32 = libc::S_IFDIR as u32;
1551 const S_IFREG: u32 = libc::S_IFREG as u32;
1552
1553 fn setup_test_fs() -> (TempDir, PassthroughFs) {
1554 let temp = TempDir::new().expect("failed to create temp dir");
1555 let fs = PassthroughFs::new(temp.path()).expect("failed to create fs");
1556 (temp, fs)
1557 }
1558
1559 #[test]
1560 fn test_new_filesystem() {
1561 let temp = TempDir::new().unwrap();
1562 let fs = PassthroughFs::new(temp.path()).unwrap();
1563 assert_eq!(fs.root(), temp.path());
1564 }
1565
1566 #[test]
1567 fn test_new_invalid_path() {
1568 let result = PassthroughFs::new("/nonexistent/path/12345");
1569 assert!(result.is_err());
1570 }
1571
1572 #[test]
1573 fn test_lookup_existing_file() {
1574 let (temp, fs) = setup_test_fs();
1575
1576 let file_path = temp.path().join("test.txt");
1578 std::fs::write(&file_path, "hello").unwrap();
1579
1580 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("test.txt"));
1582 assert!(result.is_ok());
1583 let (inode, attr) = result.unwrap();
1584 assert!(inode > PassthroughFs::ROOT_INODE);
1585 assert_eq!(attr.size, 5);
1586 }
1587
1588 #[test]
1589 fn test_lookup_nonexistent_file() {
1590 let (_temp, fs) = setup_test_fs();
1591
1592 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("nonexistent.txt"));
1593 assert!(result.is_err());
1594 assert!(result.unwrap_err().is_not_found());
1595 }
1596
1597 #[test]
1598 fn test_lookup_negative_cache() {
1599 let (_temp, fs) = setup_test_fs();
1600
1601 let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
1603
1604 if let Some(cache) = fs.negative_cache() {
1606 let stats = cache.stats();
1607 assert!(stats.entries > 0 || stats.misses > 0);
1608 }
1609
1610 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
1612 assert!(result.is_err());
1613 assert!(result.unwrap_err().is_not_found());
1614 }
1615
1616 #[test]
1617 fn test_getattr_root() {
1618 let (_temp, fs) = setup_test_fs();
1619
1620 let result = fs.getattr(PassthroughFs::ROOT_INODE);
1621 assert!(result.is_ok());
1622 let attr = result.unwrap();
1623 assert_eq!(attr.ino, PassthroughFs::ROOT_INODE);
1624 assert!(attr.mode & S_IFDIR != 0);
1625 }
1626
1627 #[test]
1628 fn test_create_and_read_file() {
1629 let (_temp, fs) = setup_test_fs();
1630
1631 let (inode, _attr, handle) = fs
1633 .create(
1634 PassthroughFs::ROOT_INODE,
1635 OsStr::new("newfile.txt"),
1636 0o644,
1637 libc::O_RDWR as u32,
1638 )
1639 .unwrap();
1640
1641 assert!(inode > PassthroughFs::ROOT_INODE);
1642
1643 let data = b"hello world";
1645 let written = fs.write(handle, 0, data, 0).unwrap();
1646 assert_eq!(written, data.len() as u32);
1647
1648 let read_data = fs.read(handle, 0, 100).unwrap();
1650 assert_eq!(read_data, data);
1651
1652 fs.release(handle).unwrap();
1654 }
1655
1656 #[test]
1657 fn test_mkdir_and_rmdir() {
1658 let (_temp, fs) = setup_test_fs();
1659
1660 let (inode, attr) = fs
1662 .mkdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"), 0o755)
1663 .unwrap();
1664
1665 assert!(inode > PassthroughFs::ROOT_INODE);
1666 assert!(attr.mode & S_IFDIR != 0);
1667
1668 fs.rmdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"))
1670 .unwrap();
1671
1672 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("testdir"));
1674 assert!(result.is_err());
1675 assert!(result.unwrap_err().is_not_found());
1676 }
1677
1678 #[test]
1679 fn test_symlink_and_readlink() {
1680 let (temp, fs) = setup_test_fs();
1681
1682 let target = temp.path().join("target.txt");
1684 std::fs::write(&target, "target content").unwrap();
1685
1686 let (inode, _attr) = fs
1688 .symlink(
1689 PassthroughFs::ROOT_INODE,
1690 OsStr::new("link"),
1691 Path::new("target.txt"),
1692 )
1693 .unwrap();
1694
1695 let link_target = fs.readlink(inode).unwrap();
1697 assert_eq!(link_target, Path::new("target.txt"));
1698 }
1699
1700 #[test]
1701 fn test_hard_link() {
1702 let (temp, fs) = setup_test_fs();
1703
1704 let original = temp.path().join("original.txt");
1706 std::fs::write(&original, "content").unwrap();
1707
1708 let (orig_inode, _) = fs
1710 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("original.txt"))
1711 .unwrap();
1712
1713 let (link_inode, attr) = fs
1715 .link(
1716 orig_inode,
1717 PassthroughFs::ROOT_INODE,
1718 OsStr::new("hardlink.txt"),
1719 )
1720 .unwrap();
1721
1722 assert_eq!(link_inode, orig_inode);
1724 assert!(attr.nlink >= 2);
1725 }
1726
1727 #[test]
1728 fn test_unlink() {
1729 let (temp, fs) = setup_test_fs();
1730
1731 let file_path = temp.path().join("todelete.txt");
1733 std::fs::write(&file_path, "delete me").unwrap();
1734
1735 fs.unlink(PassthroughFs::ROOT_INODE, OsStr::new("todelete.txt"))
1737 .unwrap();
1738
1739 assert!(!file_path.exists());
1741 }
1742
1743 #[test]
1744 fn test_rename() {
1745 let (temp, fs) = setup_test_fs();
1746
1747 let old_path = temp.path().join("old.txt");
1749 std::fs::write(&old_path, "content").unwrap();
1750
1751 fs.rename(
1753 PassthroughFs::ROOT_INODE,
1754 OsStr::new("old.txt"),
1755 PassthroughFs::ROOT_INODE,
1756 OsStr::new("new.txt"),
1757 0,
1758 )
1759 .unwrap();
1760
1761 assert!(!old_path.exists());
1763 assert!(temp.path().join("new.txt").exists());
1765 }
1766
1767 #[test]
1768 fn test_opendir_readdir() {
1769 let (temp, fs) = setup_test_fs();
1770
1771 std::fs::write(temp.path().join("file1.txt"), "1").unwrap();
1773 std::fs::write(temp.path().join("file2.txt"), "2").unwrap();
1774 std::fs::create_dir(temp.path().join("subdir")).unwrap();
1775
1776 let handle = fs.opendir(PassthroughFs::ROOT_INODE).unwrap();
1778
1779 let entries = fs.readdir(handle, 0).unwrap();
1781
1782 assert!(entries.len() >= 5);
1784
1785 let names: Vec<_> = entries
1787 .iter()
1788 .map(|e| e.name.to_string_lossy().to_string())
1789 .collect();
1790 assert!(names.contains(&".".to_string()));
1791 assert!(names.contains(&"..".to_string()));
1792 assert!(names.contains(&"file1.txt".to_string()));
1793 assert!(names.contains(&"file2.txt".to_string()));
1794 assert!(names.contains(&"subdir".to_string()));
1795
1796 fs.releasedir(handle).unwrap();
1798 }
1799
1800 #[test]
1801 fn test_setattr_size() {
1802 let (temp, fs) = setup_test_fs();
1803
1804 let file_path = temp.path().join("truncate.txt");
1806 std::fs::write(&file_path, "hello world").unwrap();
1807
1808 let (inode, _) = fs
1809 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("truncate.txt"))
1810 .unwrap();
1811
1812 let attr = fs
1814 .setattr(inode, None, None, None, Some(5), None, None)
1815 .unwrap();
1816 assert_eq!(attr.size, 5);
1817
1818 let content = std::fs::read_to_string(&file_path).unwrap();
1820 assert_eq!(content, "hello");
1821 }
1822
1823 #[test]
1824 fn test_setattr_mode() {
1825 let (temp, fs) = setup_test_fs();
1826
1827 let file_path = temp.path().join("chmod.txt");
1828 std::fs::write(&file_path, "test").unwrap();
1829
1830 let (inode, _) = fs
1831 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("chmod.txt"))
1832 .unwrap();
1833
1834 let attr = fs
1836 .setattr(inode, Some(0o600), None, None, None, None, None)
1837 .unwrap();
1838 assert_eq!(attr.mode & 0o777, 0o600);
1839 }
1840
1841 #[test]
1842 fn test_open_read_write() {
1843 let (temp, fs) = setup_test_fs();
1844
1845 let file_path = temp.path().join("rw.txt");
1847 std::fs::write(&file_path, "initial").unwrap();
1848
1849 let (inode, _) = fs
1850 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("rw.txt"))
1851 .unwrap();
1852
1853 let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1855
1856 let data = fs.read(handle, 0, 100).unwrap();
1858 assert_eq!(data, b"initial");
1859
1860 fs.write(handle, 0, b"INITIAL", 0).unwrap();
1862
1863 let data = fs.read(handle, 0, 100).unwrap();
1865 assert_eq!(data, b"INITIAL");
1866
1867 fs.release(handle).unwrap();
1868 }
1869
1870 #[test]
1871 fn test_fsync() {
1872 let (temp, fs) = setup_test_fs();
1873
1874 let file_path = temp.path().join("sync.txt");
1875 std::fs::write(&file_path, "test").unwrap();
1876
1877 let (inode, _) = fs
1878 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("sync.txt"))
1879 .unwrap();
1880
1881 let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1882 fs.write(handle, 0, b"updated", 0).unwrap();
1883
1884 fs.fsync(handle, false).unwrap();
1886 fs.fsync(handle, true).unwrap();
1887
1888 fs.release(handle).unwrap();
1889 }
1890
1891 #[test]
1892 fn test_flush() {
1893 let (temp, fs) = setup_test_fs();
1894
1895 let file_path = temp.path().join("flush.txt");
1896 std::fs::write(&file_path, "test").unwrap();
1897
1898 let (inode, _) = fs
1899 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("flush.txt"))
1900 .unwrap();
1901
1902 let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1903 fs.write(handle, 0, b"updated", 0).unwrap();
1904 fs.flush(handle).unwrap();
1905 fs.release(handle).unwrap();
1906 }
1907
1908 #[test]
1909 fn test_lseek() {
1910 let (temp, fs) = setup_test_fs();
1911
1912 let file_path = temp.path().join("seek.txt");
1913 std::fs::write(&file_path, "0123456789").unwrap();
1914
1915 let (inode, _) = fs
1916 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("seek.txt"))
1917 .unwrap();
1918
1919 let handle = fs.open(inode, libc::O_RDONLY as u32).unwrap();
1920
1921 let pos = fs.lseek(handle, 5, 0).unwrap(); assert_eq!(pos, 5);
1924
1925 let data = fs.read(handle, pos, 5).unwrap();
1927 assert_eq!(data, b"56789");
1928
1929 fs.release(handle).unwrap();
1930 }
1931
1932 #[test]
1933 fn test_statfs() {
1934 let (_temp, fs) = setup_test_fs();
1935
1936 let stat = fs.statfs().unwrap();
1937 assert!(stat.blocks > 0);
1938 assert!(stat.bsize > 0);
1939 }
1940
1941 #[test]
1942 fn test_access() {
1943 let (temp, fs) = setup_test_fs();
1944
1945 let file_path = temp.path().join("access.txt");
1946 std::fs::write(&file_path, "test").unwrap();
1947
1948 let (inode, _) = fs
1949 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("access.txt"))
1950 .unwrap();
1951
1952 fs.access(inode, libc::R_OK as u32).unwrap();
1954
1955 fs.access(inode, libc::W_OK as u32).unwrap();
1957 }
1958
1959 #[test]
1960 fn test_forget() {
1961 let (temp, fs) = setup_test_fs();
1962
1963 let file_path = temp.path().join("forget.txt");
1964 std::fs::write(&file_path, "test").unwrap();
1965
1966 let (inode, _) = fs
1967 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
1968 .unwrap();
1969
1970 let (inode2, _) = fs
1972 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
1973 .unwrap();
1974 assert_eq!(inode, inode2);
1975
1976 fs.forget(inode, 1);
1978
1979 assert!(fs.getattr(inode).is_ok());
1981
1982 fs.forget(inode, 1);
1984
1985 }
1987
1988 #[test]
1989 #[ignore = "mknod requires elevated permissions on macOS"]
1990 fn test_mknod_regular_file() {
1991 let (_temp, fs) = setup_test_fs();
1992
1993 let (inode, attr) = fs
1995 .mknod(
1996 PassthroughFs::ROOT_INODE,
1997 OsStr::new("mknod_file"),
1998 S_IFREG | 0o644,
1999 0,
2000 )
2001 .unwrap();
2002
2003 assert!(inode > PassthroughFs::ROOT_INODE);
2004 assert!(attr.mode & S_IFREG != 0);
2005 }
2006
2007 #[test]
2008 fn test_concurrent_operations() {
2009 use std::sync::Arc;
2010 use std::thread;
2011
2012 let (temp, fs) = setup_test_fs();
2013 let fs = Arc::new(fs);
2014
2015 for i in 0..10 {
2017 std::fs::write(
2018 temp.path().join(format!("file{i}.txt")),
2019 format!("content{i}"),
2020 )
2021 .unwrap();
2022 }
2023
2024 let mut handles = vec![];
2025
2026 for i in 0..4 {
2028 let fs = Arc::clone(&fs);
2029 handles.push(thread::spawn(move || {
2030 for j in 0..100 {
2031 let name = format!("file{}.txt", (i + j) % 10);
2032 let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name));
2033 }
2034 }));
2035 }
2036
2037 for i in 0..4 {
2039 let fs = Arc::clone(&fs);
2040 handles.push(thread::spawn(move || {
2041 for j in 0..50 {
2042 let name = format!("file{}.txt", (i + j) % 10);
2043 if let Ok((inode, _)) = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name))
2044 {
2045 if let Ok(handle) = fs.open(inode, libc::O_RDONLY as u32) {
2046 let _ = fs.read(handle, 0, 100);
2047 let _ = fs.release(handle);
2048 }
2049 }
2050 }
2051 }));
2052 }
2053
2054 for handle in handles {
2055 handle.join().expect("Thread panicked");
2056 }
2057 }
2058}