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}
41
42impl InodeData {
43 fn new(path: PathBuf, file_type: FileType) -> Self {
44 Self {
45 path,
46 refcount: AtomicU64::new(1),
47 file_type,
48 }
49 }
50
51 fn inc_ref(&self) {
52 self.refcount.fetch_add(1, Ordering::Relaxed);
53 }
54
55 fn dec_ref(&self) -> u64 {
56 self.refcount.fetch_sub(1, Ordering::Relaxed)
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum FileType {
63 Regular,
64 Directory,
65 Symlink,
66 BlockDevice,
67 CharDevice,
68 Fifo,
69 Socket,
70 Unknown,
71}
72
73impl FileType {
74 fn from_mode(mode: u32) -> Self {
75 let file_type = mode & u32::from(libc::S_IFMT);
76 if file_type == u32::from(libc::S_IFREG) {
77 Self::Regular
78 } else if file_type == u32::from(libc::S_IFDIR) {
79 Self::Directory
80 } else if file_type == u32::from(libc::S_IFLNK) {
81 Self::Symlink
82 } else if file_type == u32::from(libc::S_IFBLK) {
83 Self::BlockDevice
84 } else if file_type == u32::from(libc::S_IFCHR) {
85 Self::CharDevice
86 } else if file_type == u32::from(libc::S_IFIFO) {
87 Self::Fifo
88 } else if file_type == u32::from(libc::S_IFSOCK) {
89 Self::Socket
90 } else {
91 Self::Unknown
92 }
93 }
94
95 #[allow(dead_code)]
96 fn is_dir(self) -> bool {
97 self == Self::Directory
98 }
99
100 #[must_use]
102 pub fn to_dirent_type(self) -> u32 {
103 match self {
104 Self::Regular => libc::DT_REG as u32,
105 Self::Directory => libc::DT_DIR as u32,
106 Self::Symlink => libc::DT_LNK as u32,
107 Self::BlockDevice => libc::DT_BLK as u32,
108 Self::CharDevice => libc::DT_CHR as u32,
109 Self::Fifo => libc::DT_FIFO as u32,
110 Self::Socket => libc::DT_SOCK as u32,
111 Self::Unknown => libc::DT_UNKNOWN as u32,
112 }
113 }
114}
115
116#[derive(Debug)]
122#[allow(dead_code)]
123struct HandleData {
124 file: File,
126 inode: u64,
128 flags: u32,
130}
131
132#[derive(Debug)]
134struct DirHandleData {
135 inode: u64,
137 entries: Vec<DirEntry>,
139}
140
141#[derive(Debug, Clone)]
143pub struct DirEntry {
144 pub name: OsString,
146 pub ino: u64,
148 pub file_type: FileType,
150}
151
152#[derive(Debug, Clone)]
158pub struct PassthroughConfig {
159 pub negative_cache_enabled: bool,
161 pub negative_cache_max_entries: usize,
163 pub negative_cache_timeout: Duration,
165}
166
167impl Default for PassthroughConfig {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173impl PassthroughConfig {
174 #[must_use]
176 pub const fn new() -> Self {
177 Self {
178 negative_cache_enabled: true,
179 negative_cache_max_entries: 10_000,
180 negative_cache_timeout: Duration::from_secs(1),
181 }
182 }
183}
184
185pub struct PassthroughFs {
195 root: PathBuf,
197 inodes: RwLock<HashMap<u64, InodeData>>,
199 next_inode: AtomicU64,
201 handles: RwLock<HashMap<u64, HandleData>>,
203 dir_handles: RwLock<HashMap<u64, DirHandleData>>,
205 next_handle: AtomicU64,
207 negative_cache: Option<NegativeCache>,
209 #[allow(dead_code)]
211 config: PassthroughConfig,
212}
213
214impl PassthroughFs {
215 pub const ROOT_INODE: u64 = 1;
217
218 pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
224 Self::with_config(root, PassthroughConfig::default())
225 }
226
227 pub fn with_config(root: impl Into<PathBuf>, config: PassthroughConfig) -> Result<Self> {
233 let root = root.into();
234 if !root.is_dir() {
235 return Err(FsError::InvalidPath(format!(
236 "root path is not a directory: {}",
237 root.display()
238 )));
239 }
240
241 let negative_cache = if config.negative_cache_enabled {
242 Some(NegativeCache::new(NegativeCacheConfig {
243 max_entries: config.negative_cache_max_entries,
244 timeout: config.negative_cache_timeout,
245 }))
246 } else {
247 None
248 };
249
250 let mut inodes = HashMap::new();
252 inodes.insert(
253 Self::ROOT_INODE,
254 InodeData::new(PathBuf::new(), FileType::Directory),
255 );
256
257 Ok(Self {
258 root,
259 inodes: RwLock::new(inodes),
260 next_inode: AtomicU64::new(Self::ROOT_INODE + 1),
261 handles: RwLock::new(HashMap::new()),
262 dir_handles: RwLock::new(HashMap::new()),
263 next_handle: AtomicU64::new(1),
264 negative_cache,
265 config,
266 })
267 }
268
269 #[must_use]
271 pub fn root(&self) -> &Path {
272 &self.root
273 }
274
275 #[must_use]
277 pub fn negative_cache(&self) -> Option<&NegativeCache> {
278 self.negative_cache.as_ref()
279 }
280
281 fn alloc_inode(&self) -> u64 {
287 self.next_inode.fetch_add(1, Ordering::Relaxed)
288 }
289
290 fn alloc_handle(&self) -> u64 {
292 self.next_handle.fetch_add(1, Ordering::Relaxed)
293 }
294
295 fn inode_path(&self, inode: u64) -> Result<PathBuf> {
297 if inode == Self::ROOT_INODE {
298 return Ok(self.root.clone());
299 }
300
301 let inodes = self
302 .inodes
303 .read()
304 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
305
306 let data = inodes.get(&inode).ok_or(FsError::InvalidHandle(inode))?;
307 Ok(self.root.join(&data.path))
308 }
309
310 #[allow(clippy::significant_drop_tightening)]
312 fn get_path(&self, parent: u64, name: &OsStr) -> Result<PathBuf> {
313 if parent == Self::ROOT_INODE {
314 return Ok(self.root.join(name));
315 }
316
317 let inodes = self
318 .inodes
319 .read()
320 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
321
322 let parent_data = inodes.get(&parent).ok_or(FsError::InvalidHandle(parent))?;
323 Ok(self.root.join(&parent_data.path).join(name))
324 }
325
326 fn relative_path(&self, path: &Path) -> PathBuf {
328 path.strip_prefix(&self.root)
329 .map_or_else(|_| path.to_path_buf(), Path::to_path_buf)
330 }
331
332 #[allow(clippy::cast_possible_truncation)]
334 fn metadata_to_attr(ino: u64, metadata: &std::fs::Metadata) -> crate::fuse::FuseAttr {
335 crate::fuse::FuseAttr {
336 ino,
337 size: metadata.len(),
338 blocks: metadata.blocks(),
339 atime: metadata.atime() as u64,
340 mtime: metadata.mtime() as u64,
341 ctime: metadata.ctime() as u64,
342 atimensec: metadata.atime_nsec() as u32,
343 mtimensec: metadata.mtime_nsec() as u32,
344 ctimensec: metadata.ctime_nsec() as u32,
345 mode: metadata.mode(),
346 nlink: metadata.nlink() as u32,
347 uid: metadata.uid(),
348 gid: metadata.gid(),
349 rdev: metadata.rdev() as u32,
350 blksize: metadata.blksize() as u32,
351 padding: 0,
352 }
353 }
354
355 fn invalidate_negative_cache(&self, path: &Path) {
357 if let Some(ref cache) = self.negative_cache {
358 tracing::trace!(path = %path.display(), "invalidating negative cache");
359 cache.invalidate(path);
360 }
361 }
362
363 pub fn lookup(&self, parent: u64, name: &OsStr) -> Result<(u64, crate::fuse::FuseAttr)> {
375 let path = self.get_path(parent, name)?;
376
377 if let Some(ref cache) = self.negative_cache {
379 if cache.contains(&path) {
380 tracing::trace!(path = %path.display(), "negative cache hit");
381 return Err(FsError::not_found(path.display().to_string()));
382 }
383 }
384
385 let metadata = match std::fs::symlink_metadata(&path) {
387 Ok(m) => m,
388 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
389 if let Some(ref cache) = self.negative_cache {
391 tracing::trace!(path = %path.display(), "adding to negative cache");
392 cache.insert(path.clone());
393 }
394 return Err(FsError::not_found(path.display().to_string()));
395 }
396 Err(e) => return Err(FsError::io(e)),
397 };
398
399 let file_type = FileType::from_mode(metadata.mode());
400 let relative = self.relative_path(&path);
401
402 {
404 let inodes = self
405 .inodes
406 .read()
407 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
408 for (&ino, data) in inodes.iter() {
409 if data.path == relative {
410 data.inc_ref();
411 return Ok((ino, Self::metadata_to_attr(ino, &metadata)));
412 }
413 }
414 }
415
416 let inode = self.alloc_inode();
418 {
419 let mut inodes = self
420 .inodes
421 .write()
422 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
423 inodes.insert(inode, InodeData::new(relative, file_type));
424 }
425
426 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
427 }
428
429 pub fn forget(&self, inode: u64, nlookup: u64) {
433 if inode == Self::ROOT_INODE {
434 return;
435 }
436
437 let should_remove = {
438 let inodes = match self.inodes.read() {
439 Ok(i) => i,
440 Err(_) => return,
441 };
442 if let Some(data) = inodes.get(&inode) {
443 for _ in 0..nlookup {
444 if data.dec_ref() == 1 {
445 return;
446 }
447 }
448 data.refcount.load(Ordering::Relaxed) == 0
449 } else {
450 false
451 }
452 };
453
454 if should_remove {
455 if let Ok(mut inodes) = self.inodes.write() {
456 inodes.remove(&inode);
457 }
458 }
459 }
460
461 pub fn getattr(&self, inode: u64) -> Result<crate::fuse::FuseAttr> {
468 let path = self.inode_path(inode)?;
469 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
470 Ok(Self::metadata_to_attr(inode, &metadata))
471 }
472
473 #[allow(clippy::too_many_arguments)]
480 pub fn setattr(
481 &self,
482 inode: u64,
483 mode: Option<u32>,
484 uid: Option<u32>,
485 gid: Option<u32>,
486 size: Option<u64>,
487 atime: Option<(i64, u32)>,
488 mtime: Option<(i64, u32)>,
489 ) -> Result<crate::fuse::FuseAttr> {
490 let path = self.inode_path(inode)?;
491
492 if let Some(mode) = mode {
494 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
495 .map_err(FsError::io)?;
496 }
497
498 if uid.is_some() || gid.is_some() {
500 let uid = uid.map_or(-1_i32 as libc::uid_t, |u| u);
501 let gid = gid.map_or(-1_i32 as libc::gid_t, |g| g);
502 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
503 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
504 let ret = unsafe { libc::chown(path_cstr.as_ptr(), uid, gid) };
505 if ret != 0 {
506 return Err(FsError::io(std::io::Error::last_os_error()));
507 }
508 }
509
510 if let Some(size) = size {
512 let file = OpenOptions::new()
513 .write(true)
514 .open(&path)
515 .map_err(FsError::io)?;
516 file.set_len(size).map_err(FsError::io)?;
517 }
518
519 if atime.is_some() || mtime.is_some() {
521 let atime_spec = atime.map_or(
522 libc::timespec {
523 tv_sec: 0,
524 tv_nsec: libc::UTIME_OMIT,
525 },
526 |(sec, nsec)| libc::timespec {
527 tv_sec: sec,
528 tv_nsec: i64::from(nsec),
529 },
530 );
531 let mtime_spec = mtime.map_or(
532 libc::timespec {
533 tv_sec: 0,
534 tv_nsec: libc::UTIME_OMIT,
535 },
536 |(sec, nsec)| libc::timespec {
537 tv_sec: sec,
538 tv_nsec: i64::from(nsec),
539 },
540 );
541 let times = [atime_spec, mtime_spec];
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 =
545 unsafe { libc::utimensat(libc::AT_FDCWD, path_cstr.as_ptr(), times.as_ptr(), 0) };
546 if ret != 0 {
547 return Err(FsError::io(std::io::Error::last_os_error()));
548 }
549 }
550
551 self.getattr(inode)
552 }
553
554 pub fn readlink(&self, inode: u64) -> Result<PathBuf> {
561 let path = self.inode_path(inode)?;
562 std::fs::read_link(&path).map_err(FsError::io)
563 }
564
565 pub fn create(
576 &self,
577 parent: u64,
578 name: &OsStr,
579 mode: u32,
580 flags: u32,
581 ) -> Result<(u64, crate::fuse::FuseAttr, u64)> {
582 let path = self.get_path(parent, name)?;
583
584 let mut opts = OpenOptions::new();
586 Self::apply_flags(&mut opts, flags);
587 opts.create(true);
588 opts.mode(mode & 0o7777);
589
590 let file = opts.open(&path).map_err(FsError::io)?;
591 let metadata = file.metadata().map_err(FsError::io)?;
592
593 self.invalidate_negative_cache(&path);
595
596 let relative = self.relative_path(&path);
598 let inode = self.alloc_inode();
599 {
600 let mut inodes = self
601 .inodes
602 .write()
603 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
604 inodes.insert(inode, InodeData::new(relative, FileType::Regular));
605 }
606
607 let handle = self.alloc_handle();
609 {
610 let mut handles = self
611 .handles
612 .write()
613 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
614 handles.insert(handle, HandleData { file, inode, flags });
615 }
616
617 Ok((inode, Self::metadata_to_attr(inode, &metadata), handle))
618 }
619
620 pub fn mkdir(
627 &self,
628 parent: u64,
629 name: &OsStr,
630 mode: u32,
631 ) -> Result<(u64, crate::fuse::FuseAttr)> {
632 let path = self.get_path(parent, name)?;
633
634 std::fs::create_dir(&path).map_err(FsError::io)?;
635 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode & 0o7777))
636 .map_err(FsError::io)?;
637
638 self.invalidate_negative_cache(&path);
639
640 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
641 let relative = self.relative_path(&path);
642 let inode = self.alloc_inode();
643
644 {
645 let mut inodes = self
646 .inodes
647 .write()
648 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
649 inodes.insert(inode, InodeData::new(relative, FileType::Directory));
650 }
651
652 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
653 }
654
655 pub fn symlink(
662 &self,
663 parent: u64,
664 name: &OsStr,
665 target: &Path,
666 ) -> Result<(u64, crate::fuse::FuseAttr)> {
667 let path = self.get_path(parent, name)?;
668
669 std::os::unix::fs::symlink(target, &path).map_err(FsError::io)?;
670
671 self.invalidate_negative_cache(&path);
672
673 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
674 let relative = self.relative_path(&path);
675 let inode = self.alloc_inode();
676
677 {
678 let mut inodes = self
679 .inodes
680 .write()
681 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
682 inodes.insert(inode, InodeData::new(relative, FileType::Symlink));
683 }
684
685 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
686 }
687
688 pub fn link(
695 &self,
696 inode: u64,
697 new_parent: u64,
698 new_name: &OsStr,
699 ) -> Result<(u64, crate::fuse::FuseAttr)> {
700 let source_path = self.inode_path(inode)?;
701 let new_path = self.get_path(new_parent, new_name)?;
702
703 std::fs::hard_link(&source_path, &new_path).map_err(FsError::io)?;
704
705 self.invalidate_negative_cache(&new_path);
706
707 let metadata = std::fs::symlink_metadata(&new_path).map_err(FsError::io)?;
709
710 {
712 let inodes = self
713 .inodes
714 .read()
715 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
716 if let Some(data) = inodes.get(&inode) {
717 data.inc_ref();
718 }
719 }
720
721 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
722 }
723
724 #[allow(clippy::cast_possible_truncation)]
731 pub fn mknod(
732 &self,
733 parent: u64,
734 name: &OsStr,
735 mode: u32,
736 rdev: u64,
737 ) -> Result<(u64, crate::fuse::FuseAttr)> {
738 let path = self.get_path(parent, name)?;
739 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
740 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
741
742 let ret = unsafe {
743 libc::mknod(
744 path_cstr.as_ptr(),
745 mode as libc::mode_t,
746 rdev as libc::dev_t,
747 )
748 };
749 if ret != 0 {
750 return Err(FsError::io(std::io::Error::last_os_error()));
751 }
752
753 self.invalidate_negative_cache(&path);
754
755 let metadata = std::fs::symlink_metadata(&path).map_err(FsError::io)?;
756 let file_type = FileType::from_mode(metadata.mode());
757 let relative = self.relative_path(&path);
758 let inode = self.alloc_inode();
759
760 {
761 let mut inodes = self
762 .inodes
763 .write()
764 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
765 inodes.insert(inode, InodeData::new(relative, file_type));
766 }
767
768 Ok((inode, Self::metadata_to_attr(inode, &metadata)))
769 }
770
771 pub fn unlink(&self, parent: u64, name: &OsStr) -> Result<()> {
782 let path = self.get_path(parent, name)?;
783 std::fs::remove_file(&path).map_err(FsError::io)?;
784
785 Ok(())
789 }
790
791 pub fn rmdir(&self, parent: u64, name: &OsStr) -> Result<()> {
798 let path = self.get_path(parent, name)?;
799 std::fs::remove_dir(&path).map_err(FsError::io)
800 }
801
802 pub fn rename(
809 &self,
810 parent: u64,
811 name: &OsStr,
812 new_parent: u64,
813 new_name: &OsStr,
814 _flags: u32,
815 ) -> Result<()> {
816 let old_path = self.get_path(parent, name)?;
817 let new_path = self.get_path(new_parent, new_name)?;
818
819 std::fs::rename(&old_path, &new_path).map_err(FsError::io)?;
820
821 self.invalidate_negative_cache(&old_path);
823 self.invalidate_negative_cache(&new_path);
824
825 let old_relative = self.relative_path(&old_path);
827 let new_relative = self.relative_path(&new_path);
828
829 {
830 let mut inodes = self
831 .inodes
832 .write()
833 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
834 for data in inodes.values_mut() {
835 if data.path == old_relative {
836 data.path = new_relative;
837 break;
838 }
839 }
840 }
841
842 Ok(())
843 }
844
845 fn apply_flags(opts: &mut OpenOptions, flags: u32) {
851 let access_mode = flags & libc::O_ACCMODE as u32;
852 match access_mode {
853 x if x == libc::O_RDONLY as u32 => {
854 opts.read(true);
855 }
856 x if x == libc::O_WRONLY as u32 => {
857 opts.write(true);
858 }
859 x if x == libc::O_RDWR as u32 => {
860 opts.read(true).write(true);
861 }
862 _ => {
863 opts.read(true);
864 }
865 }
866
867 if flags & libc::O_APPEND as u32 != 0 {
868 opts.append(true);
869 }
870 if flags & libc::O_TRUNC as u32 != 0 {
871 opts.truncate(true);
872 }
873 }
874
875 pub fn open(&self, inode: u64, flags: u32) -> Result<u64> {
882 let path = self.inode_path(inode)?;
883
884 let mut opts = OpenOptions::new();
885 Self::apply_flags(&mut opts, flags);
886
887 let file = opts.open(&path).map_err(FsError::io)?;
888 let handle = self.alloc_handle();
889
890 {
891 let mut handles = self
892 .handles
893 .write()
894 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
895 handles.insert(handle, HandleData { file, inode, flags });
896 }
897
898 Ok(handle)
899 }
900
901 pub fn read(&self, handle: u64, offset: u64, size: u32) -> Result<Vec<u8>> {
908 let mut handles = self
909 .handles
910 .write()
911 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
912
913 let data = handles
914 .get_mut(&handle)
915 .ok_or(FsError::InvalidHandle(handle))?;
916
917 data.file
918 .seek(SeekFrom::Start(offset))
919 .map_err(FsError::io)?;
920
921 let mut buf = vec![0u8; size as usize];
922 let n = data.file.read(&mut buf).map_err(FsError::io)?;
923 buf.truncate(n);
924
925 Ok(buf)
926 }
927
928 pub fn write(&self, handle: u64, offset: u64, data: &[u8], _flags: u32) -> Result<u32> {
935 let mut handles = self
936 .handles
937 .write()
938 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
939
940 let handle_data = handles
941 .get_mut(&handle)
942 .ok_or(FsError::InvalidHandle(handle))?;
943
944 handle_data
945 .file
946 .seek(SeekFrom::Start(offset))
947 .map_err(FsError::io)?;
948 let n = handle_data.file.write(data).map_err(FsError::io)?;
949
950 #[allow(clippy::cast_possible_truncation)]
951 Ok(n as u32)
952 }
953
954 pub fn flush(&self, handle: u64) -> Result<()> {
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 data.file.flush().map_err(FsError::io)
970 }
971
972 pub fn fsync(&self, handle: u64, datasync: bool) -> Result<()> {
979 let handles = self
980 .handles
981 .read()
982 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
983
984 let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
985
986 if datasync {
987 data.file.sync_data().map_err(FsError::io)
988 } else {
989 data.file.sync_all().map_err(FsError::io)
990 }
991 }
992
993 pub fn release(&self, handle: u64) -> Result<()> {
995 let mut handles = self
996 .handles
997 .write()
998 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
999
1000 handles.remove(&handle);
1001 Ok(())
1002 }
1003
1004 pub fn lseek(&self, handle: u64, offset: i64, whence: u32) -> Result<u64> {
1011 let mut handles = self
1012 .handles
1013 .write()
1014 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1015
1016 let data = handles
1017 .get_mut(&handle)
1018 .ok_or(FsError::InvalidHandle(handle))?;
1019
1020 let seek_from = match whence {
1021 0 => SeekFrom::Start(offset as u64), 1 => SeekFrom::Current(offset), 2 => SeekFrom::End(offset), _ => return Err(FsError::InvalidPath("invalid whence".to_string())),
1025 };
1026
1027 data.file.seek(seek_from).map_err(FsError::io)
1028 }
1029
1030 #[cfg(target_os = "linux")]
1037 pub fn fallocate(&self, handle: u64, mode: u32, offset: u64, length: u64) -> Result<()> {
1038 let handles = self
1039 .handles
1040 .read()
1041 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1042
1043 let data = handles.get(&handle).ok_or(FsError::InvalidHandle(handle))?;
1044 let fd = data.file.as_raw_fd();
1045
1046 #[allow(clippy::cast_possible_wrap)]
1047 let ret = unsafe { libc::fallocate(fd, mode as i32, offset as i64, length as i64) };
1048
1049 if ret != 0 {
1050 Err(FsError::io(std::io::Error::last_os_error()))
1051 } else {
1052 Ok(())
1053 }
1054 }
1055
1056 #[cfg(target_os = "macos")]
1057 pub fn fallocate(&self, handle: u64, _mode: u32, offset: u64, length: u64) -> Result<()> {
1058 let mut handles = self
1060 .handles
1061 .write()
1062 .map_err(|_| FsError::Cache("failed to acquire handle lock".to_string()))?;
1063
1064 let data = handles
1065 .get_mut(&handle)
1066 .ok_or(FsError::InvalidHandle(handle))?;
1067 let new_size = offset + length;
1068 data.file.set_len(new_size).map_err(FsError::io)
1069 }
1070
1071 pub fn opendir(&self, inode: u64) -> Result<u64> {
1082 let path = self.inode_path(inode)?;
1083
1084 let mut entries = Vec::new();
1086
1087 entries.push(DirEntry {
1089 name: OsString::from("."),
1090 ino: inode,
1091 file_type: FileType::Directory,
1092 });
1093
1094 let parent_ino = if inode == Self::ROOT_INODE {
1096 Self::ROOT_INODE
1097 } else {
1098 Self::ROOT_INODE
1100 };
1101 entries.push(DirEntry {
1102 name: OsString::from(".."),
1103 ino: parent_ino,
1104 file_type: FileType::Directory,
1105 });
1106
1107 for entry in std::fs::read_dir(&path).map_err(FsError::io)? {
1109 let entry = entry.map_err(FsError::io)?;
1110 let metadata = entry.metadata().map_err(FsError::io)?;
1111 let file_type = FileType::from_mode(metadata.mode());
1112
1113 let entry_path = entry.path();
1115 let relative = self.relative_path(&entry_path);
1116 let entry_ino = {
1117 let inodes = self
1118 .inodes
1119 .read()
1120 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
1121 let mut found_ino = None;
1122 for (&ino, data) in inodes.iter() {
1123 if data.path == relative {
1124 found_ino = Some(ino);
1125 break;
1126 }
1127 }
1128 found_ino
1129 };
1130
1131 let ino = if let Some(ino) = entry_ino {
1132 ino
1133 } else {
1134 let new_ino = self.alloc_inode();
1136 let mut inodes = self
1137 .inodes
1138 .write()
1139 .map_err(|_| FsError::Cache("failed to acquire inode lock".to_string()))?;
1140 inodes.insert(new_ino, InodeData::new(relative, file_type));
1141 new_ino
1142 };
1143
1144 entries.push(DirEntry {
1145 name: entry.file_name(),
1146 ino,
1147 file_type,
1148 });
1149 }
1150
1151 let handle = self.alloc_handle();
1152 {
1153 let mut dir_handles = self
1154 .dir_handles
1155 .write()
1156 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1157 dir_handles.insert(handle, DirHandleData { inode, entries });
1158 }
1159
1160 Ok(handle)
1161 }
1162
1163 pub fn readdir(&self, handle: u64, offset: u64) -> Result<Vec<DirEntry>> {
1171 let dir_handles = self
1172 .dir_handles
1173 .read()
1174 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1175
1176 let data = dir_handles
1177 .get(&handle)
1178 .ok_or(FsError::InvalidHandle(handle))?;
1179
1180 let entries: Vec<DirEntry> = data.entries.iter().skip(offset as usize).cloned().collect();
1181
1182 Ok(entries)
1183 }
1184
1185 pub fn releasedir(&self, handle: u64) -> Result<()> {
1187 let mut dir_handles = self
1188 .dir_handles
1189 .write()
1190 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1191
1192 dir_handles.remove(&handle);
1193 Ok(())
1194 }
1195
1196 pub fn fsyncdir(&self, handle: u64, _datasync: bool) -> Result<()> {
1203 let dir_handles = self
1204 .dir_handles
1205 .read()
1206 .map_err(|_| FsError::Cache("failed to acquire dir handle lock".to_string()))?;
1207
1208 let data = dir_handles
1209 .get(&handle)
1210 .ok_or(FsError::InvalidHandle(handle))?;
1211 let path = self.inode_path(data.inode)?;
1212
1213 let dir = File::open(&path).map_err(FsError::io)?;
1215 dir.sync_all().map_err(FsError::io)
1216 }
1217
1218 #[cfg(target_os = "linux")]
1229 pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
1230 use std::os::unix::ffi::OsStrExt;
1231
1232 let path = self.inode_path(inode)?;
1233 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1234 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1235 let name_cstr = std::ffi::CString::new(name.as_bytes())
1236 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1237
1238 if size == 0 {
1239 let ret = unsafe {
1241 libc::getxattr(
1242 path_cstr.as_ptr(),
1243 name_cstr.as_ptr(),
1244 std::ptr::null_mut(),
1245 0,
1246 )
1247 };
1248 if ret < 0 {
1249 return Err(FsError::io(std::io::Error::last_os_error()));
1250 }
1251 Ok(vec![0u8; ret as usize])
1252 } else {
1253 let mut buf = vec![0u8; size as usize];
1254 let ret = unsafe {
1255 libc::getxattr(
1256 path_cstr.as_ptr(),
1257 name_cstr.as_ptr(),
1258 buf.as_mut_ptr().cast(),
1259 size as usize,
1260 )
1261 };
1262 if ret < 0 {
1263 return Err(FsError::io(std::io::Error::last_os_error()));
1264 }
1265 buf.truncate(ret as usize);
1266 Ok(buf)
1267 }
1268 }
1269
1270 #[cfg(target_os = "macos")]
1271 pub fn getxattr(&self, inode: u64, name: &OsStr, size: u32) -> Result<Vec<u8>> {
1272 let path = self.inode_path(inode)?;
1273 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1274 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1275 let name_cstr = std::ffi::CString::new(name.as_bytes())
1276 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1277
1278 if size == 0 {
1279 let ret = unsafe {
1280 libc::getxattr(
1281 path_cstr.as_ptr(),
1282 name_cstr.as_ptr(),
1283 std::ptr::null_mut(),
1284 0,
1285 0,
1286 0,
1287 )
1288 };
1289 if ret < 0 {
1290 return Err(FsError::io(std::io::Error::last_os_error()));
1291 }
1292 Ok(vec![0u8; ret as usize])
1293 } else {
1294 let mut buf = vec![0u8; size as usize];
1295 let ret = unsafe {
1296 libc::getxattr(
1297 path_cstr.as_ptr(),
1298 name_cstr.as_ptr(),
1299 buf.as_mut_ptr().cast(),
1300 size as usize,
1301 0,
1302 0,
1303 )
1304 };
1305 if ret < 0 {
1306 return Err(FsError::io(std::io::Error::last_os_error()));
1307 }
1308 buf.truncate(ret as usize);
1309 Ok(buf)
1310 }
1311 }
1312
1313 #[cfg(target_os = "linux")]
1320 pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
1321 let path = self.inode_path(inode)?;
1322 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1323 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1324 let name_cstr = std::ffi::CString::new(name.as_bytes())
1325 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1326
1327 let ret = unsafe {
1328 libc::setxattr(
1329 path_cstr.as_ptr(),
1330 name_cstr.as_ptr(),
1331 value.as_ptr().cast(),
1332 value.len(),
1333 flags as i32,
1334 )
1335 };
1336 if ret != 0 {
1337 Err(FsError::io(std::io::Error::last_os_error()))
1338 } else {
1339 Ok(())
1340 }
1341 }
1342
1343 #[cfg(target_os = "macos")]
1344 pub fn setxattr(&self, inode: u64, name: &OsStr, value: &[u8], flags: u32) -> Result<()> {
1345 let path = self.inode_path(inode)?;
1346 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1347 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1348 let name_cstr = std::ffi::CString::new(name.as_bytes())
1349 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1350
1351 let ret = unsafe {
1352 libc::setxattr(
1353 path_cstr.as_ptr(),
1354 name_cstr.as_ptr(),
1355 value.as_ptr().cast(),
1356 value.len(),
1357 0,
1358 flags as i32,
1359 )
1360 };
1361 if ret != 0 {
1362 Err(FsError::io(std::io::Error::last_os_error()))
1363 } else {
1364 Ok(())
1365 }
1366 }
1367
1368 #[cfg(target_os = "linux")]
1375 pub fn removexattr(&self, inode: u64, name: &OsStr) -> 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 { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr()) };
1383 if ret != 0 {
1384 Err(FsError::io(std::io::Error::last_os_error()))
1385 } else {
1386 Ok(())
1387 }
1388 }
1389
1390 #[cfg(target_os = "macos")]
1391 pub fn removexattr(&self, inode: u64, name: &OsStr) -> Result<()> {
1392 let path = self.inode_path(inode)?;
1393 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1394 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1395 let name_cstr = std::ffi::CString::new(name.as_bytes())
1396 .map_err(|_| FsError::InvalidPath("invalid name".to_string()))?;
1397
1398 let ret = unsafe { libc::removexattr(path_cstr.as_ptr(), name_cstr.as_ptr(), 0) };
1399 if ret != 0 {
1400 Err(FsError::io(std::io::Error::last_os_error()))
1401 } else {
1402 Ok(())
1403 }
1404 }
1405
1406 pub fn statfs(&self) -> Result<crate::fuse::StatFs> {
1416 let path_cstr = std::ffi::CString::new(self.root.as_os_str().as_bytes())
1417 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1418
1419 let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
1420 let ret = unsafe { libc::statfs(path_cstr.as_ptr(), &raw mut stat) };
1421 if ret != 0 {
1422 return Err(FsError::io(std::io::Error::last_os_error()));
1423 }
1424
1425 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1426 Ok(crate::fuse::StatFs {
1427 blocks: stat.f_blocks as u64,
1428 bfree: stat.f_bfree as u64,
1429 bavail: stat.f_bavail as u64,
1430 files: stat.f_files as u64,
1431 ffree: stat.f_ffree as u64,
1432 bsize: stat.f_bsize as u32,
1433 namelen: 255, frsize: stat.f_bsize as u32,
1435 })
1436 }
1437
1438 pub fn access(&self, inode: u64, mask: u32) -> Result<()> {
1445 let path = self.inode_path(inode)?;
1446 let path_cstr = std::ffi::CString::new(path.as_os_str().as_bytes())
1447 .map_err(|_| FsError::InvalidPath("invalid path".to_string()))?;
1448
1449 #[allow(clippy::cast_possible_wrap)]
1450 let ret = unsafe { libc::access(path_cstr.as_ptr(), mask as i32) };
1451 if ret != 0 {
1452 let err = std::io::Error::last_os_error();
1453 if err.raw_os_error() == Some(libc::EACCES) {
1454 Err(FsError::permission_denied(path.display().to_string()))
1455 } else {
1456 Err(FsError::io(err))
1457 }
1458 } else {
1459 Ok(())
1460 }
1461 }
1462}
1463
1464impl std::fmt::Debug for PassthroughFs {
1465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1466 f.debug_struct("PassthroughFs")
1467 .field("root", &self.root)
1468 .field("inodes", &self.inodes.read().map(|i| i.len()).unwrap_or(0))
1469 .field(
1470 "handles",
1471 &self.handles.read().map(|h| h.len()).unwrap_or(0),
1472 )
1473 .field(
1474 "dir_handles",
1475 &self.dir_handles.read().map(|h| h.len()).unwrap_or(0),
1476 )
1477 .finish()
1478 }
1479}
1480
1481#[cfg(test)]
1486mod tests {
1487 use super::*;
1488 use tempfile::TempDir;
1489
1490 const S_IFDIR: u32 = libc::S_IFDIR as u32;
1491 const S_IFREG: u32 = libc::S_IFREG as u32;
1492
1493 fn setup_test_fs() -> (TempDir, PassthroughFs) {
1494 let temp = TempDir::new().expect("failed to create temp dir");
1495 let fs = PassthroughFs::new(temp.path()).expect("failed to create fs");
1496 (temp, fs)
1497 }
1498
1499 #[test]
1500 fn test_new_filesystem() {
1501 let temp = TempDir::new().unwrap();
1502 let fs = PassthroughFs::new(temp.path()).unwrap();
1503 assert_eq!(fs.root(), temp.path());
1504 }
1505
1506 #[test]
1507 fn test_new_invalid_path() {
1508 let result = PassthroughFs::new("/nonexistent/path/12345");
1509 assert!(result.is_err());
1510 }
1511
1512 #[test]
1513 fn test_lookup_existing_file() {
1514 let (temp, fs) = setup_test_fs();
1515
1516 let file_path = temp.path().join("test.txt");
1518 std::fs::write(&file_path, "hello").unwrap();
1519
1520 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("test.txt"));
1522 assert!(result.is_ok());
1523 let (inode, attr) = result.unwrap();
1524 assert!(inode > PassthroughFs::ROOT_INODE);
1525 assert_eq!(attr.size, 5);
1526 }
1527
1528 #[test]
1529 fn test_lookup_nonexistent_file() {
1530 let (_temp, fs) = setup_test_fs();
1531
1532 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("nonexistent.txt"));
1533 assert!(result.is_err());
1534 assert!(result.unwrap_err().is_not_found());
1535 }
1536
1537 #[test]
1538 fn test_lookup_negative_cache() {
1539 let (_temp, fs) = setup_test_fs();
1540
1541 let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
1543
1544 if let Some(cache) = fs.negative_cache() {
1546 let stats = cache.stats();
1547 assert!(stats.entries > 0 || stats.misses > 0);
1548 }
1549
1550 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("missing.txt"));
1552 assert!(result.is_err());
1553 assert!(result.unwrap_err().is_not_found());
1554 }
1555
1556 #[test]
1557 fn test_getattr_root() {
1558 let (_temp, fs) = setup_test_fs();
1559
1560 let result = fs.getattr(PassthroughFs::ROOT_INODE);
1561 assert!(result.is_ok());
1562 let attr = result.unwrap();
1563 assert_eq!(attr.ino, PassthroughFs::ROOT_INODE);
1564 assert!(attr.mode & S_IFDIR != 0);
1565 }
1566
1567 #[test]
1568 fn test_create_and_read_file() {
1569 let (_temp, fs) = setup_test_fs();
1570
1571 let (inode, _attr, handle) = fs
1573 .create(
1574 PassthroughFs::ROOT_INODE,
1575 OsStr::new("newfile.txt"),
1576 0o644,
1577 libc::O_RDWR as u32,
1578 )
1579 .unwrap();
1580
1581 assert!(inode > PassthroughFs::ROOT_INODE);
1582
1583 let data = b"hello world";
1585 let written = fs.write(handle, 0, data, 0).unwrap();
1586 assert_eq!(written, data.len() as u32);
1587
1588 let read_data = fs.read(handle, 0, 100).unwrap();
1590 assert_eq!(read_data, data);
1591
1592 fs.release(handle).unwrap();
1594 }
1595
1596 #[test]
1597 fn test_mkdir_and_rmdir() {
1598 let (_temp, fs) = setup_test_fs();
1599
1600 let (inode, attr) = fs
1602 .mkdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"), 0o755)
1603 .unwrap();
1604
1605 assert!(inode > PassthroughFs::ROOT_INODE);
1606 assert!(attr.mode & S_IFDIR != 0);
1607
1608 fs.rmdir(PassthroughFs::ROOT_INODE, OsStr::new("testdir"))
1610 .unwrap();
1611
1612 let result = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new("testdir"));
1614 assert!(result.is_err());
1615 assert!(result.unwrap_err().is_not_found());
1616 }
1617
1618 #[test]
1619 fn test_symlink_and_readlink() {
1620 let (temp, fs) = setup_test_fs();
1621
1622 let target = temp.path().join("target.txt");
1624 std::fs::write(&target, "target content").unwrap();
1625
1626 let (inode, _attr) = fs
1628 .symlink(
1629 PassthroughFs::ROOT_INODE,
1630 OsStr::new("link"),
1631 Path::new("target.txt"),
1632 )
1633 .unwrap();
1634
1635 let link_target = fs.readlink(inode).unwrap();
1637 assert_eq!(link_target, Path::new("target.txt"));
1638 }
1639
1640 #[test]
1641 fn test_hard_link() {
1642 let (temp, fs) = setup_test_fs();
1643
1644 let original = temp.path().join("original.txt");
1646 std::fs::write(&original, "content").unwrap();
1647
1648 let (orig_inode, _) = fs
1650 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("original.txt"))
1651 .unwrap();
1652
1653 let (link_inode, attr) = fs
1655 .link(
1656 orig_inode,
1657 PassthroughFs::ROOT_INODE,
1658 OsStr::new("hardlink.txt"),
1659 )
1660 .unwrap();
1661
1662 assert_eq!(link_inode, orig_inode);
1664 assert!(attr.nlink >= 2);
1665 }
1666
1667 #[test]
1668 fn test_unlink() {
1669 let (temp, fs) = setup_test_fs();
1670
1671 let file_path = temp.path().join("todelete.txt");
1673 std::fs::write(&file_path, "delete me").unwrap();
1674
1675 fs.unlink(PassthroughFs::ROOT_INODE, OsStr::new("todelete.txt"))
1677 .unwrap();
1678
1679 assert!(!file_path.exists());
1681 }
1682
1683 #[test]
1684 fn test_rename() {
1685 let (temp, fs) = setup_test_fs();
1686
1687 let old_path = temp.path().join("old.txt");
1689 std::fs::write(&old_path, "content").unwrap();
1690
1691 fs.rename(
1693 PassthroughFs::ROOT_INODE,
1694 OsStr::new("old.txt"),
1695 PassthroughFs::ROOT_INODE,
1696 OsStr::new("new.txt"),
1697 0,
1698 )
1699 .unwrap();
1700
1701 assert!(!old_path.exists());
1703 assert!(temp.path().join("new.txt").exists());
1705 }
1706
1707 #[test]
1708 fn test_opendir_readdir() {
1709 let (temp, fs) = setup_test_fs();
1710
1711 std::fs::write(temp.path().join("file1.txt"), "1").unwrap();
1713 std::fs::write(temp.path().join("file2.txt"), "2").unwrap();
1714 std::fs::create_dir(temp.path().join("subdir")).unwrap();
1715
1716 let handle = fs.opendir(PassthroughFs::ROOT_INODE).unwrap();
1718
1719 let entries = fs.readdir(handle, 0).unwrap();
1721
1722 assert!(entries.len() >= 5);
1724
1725 let names: Vec<_> = entries
1727 .iter()
1728 .map(|e| e.name.to_string_lossy().to_string())
1729 .collect();
1730 assert!(names.contains(&".".to_string()));
1731 assert!(names.contains(&"..".to_string()));
1732 assert!(names.contains(&"file1.txt".to_string()));
1733 assert!(names.contains(&"file2.txt".to_string()));
1734 assert!(names.contains(&"subdir".to_string()));
1735
1736 fs.releasedir(handle).unwrap();
1738 }
1739
1740 #[test]
1741 fn test_setattr_size() {
1742 let (temp, fs) = setup_test_fs();
1743
1744 let file_path = temp.path().join("truncate.txt");
1746 std::fs::write(&file_path, "hello world").unwrap();
1747
1748 let (inode, _) = fs
1749 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("truncate.txt"))
1750 .unwrap();
1751
1752 let attr = fs
1754 .setattr(inode, None, None, None, Some(5), None, None)
1755 .unwrap();
1756 assert_eq!(attr.size, 5);
1757
1758 let content = std::fs::read_to_string(&file_path).unwrap();
1760 assert_eq!(content, "hello");
1761 }
1762
1763 #[test]
1764 fn test_setattr_mode() {
1765 let (temp, fs) = setup_test_fs();
1766
1767 let file_path = temp.path().join("chmod.txt");
1768 std::fs::write(&file_path, "test").unwrap();
1769
1770 let (inode, _) = fs
1771 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("chmod.txt"))
1772 .unwrap();
1773
1774 let attr = fs
1776 .setattr(inode, Some(0o600), None, None, None, None, None)
1777 .unwrap();
1778 assert_eq!(attr.mode & 0o777, 0o600);
1779 }
1780
1781 #[test]
1782 fn test_open_read_write() {
1783 let (temp, fs) = setup_test_fs();
1784
1785 let file_path = temp.path().join("rw.txt");
1787 std::fs::write(&file_path, "initial").unwrap();
1788
1789 let (inode, _) = fs
1790 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("rw.txt"))
1791 .unwrap();
1792
1793 let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1795
1796 let data = fs.read(handle, 0, 100).unwrap();
1798 assert_eq!(data, b"initial");
1799
1800 fs.write(handle, 0, b"INITIAL", 0).unwrap();
1802
1803 let data = fs.read(handle, 0, 100).unwrap();
1805 assert_eq!(data, b"INITIAL");
1806
1807 fs.release(handle).unwrap();
1808 }
1809
1810 #[test]
1811 fn test_fsync() {
1812 let (temp, fs) = setup_test_fs();
1813
1814 let file_path = temp.path().join("sync.txt");
1815 std::fs::write(&file_path, "test").unwrap();
1816
1817 let (inode, _) = fs
1818 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("sync.txt"))
1819 .unwrap();
1820
1821 let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1822 fs.write(handle, 0, b"updated", 0).unwrap();
1823
1824 fs.fsync(handle, false).unwrap();
1826 fs.fsync(handle, true).unwrap();
1827
1828 fs.release(handle).unwrap();
1829 }
1830
1831 #[test]
1832 fn test_flush() {
1833 let (temp, fs) = setup_test_fs();
1834
1835 let file_path = temp.path().join("flush.txt");
1836 std::fs::write(&file_path, "test").unwrap();
1837
1838 let (inode, _) = fs
1839 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("flush.txt"))
1840 .unwrap();
1841
1842 let handle = fs.open(inode, libc::O_RDWR as u32).unwrap();
1843 fs.write(handle, 0, b"updated", 0).unwrap();
1844 fs.flush(handle).unwrap();
1845 fs.release(handle).unwrap();
1846 }
1847
1848 #[test]
1849 fn test_lseek() {
1850 let (temp, fs) = setup_test_fs();
1851
1852 let file_path = temp.path().join("seek.txt");
1853 std::fs::write(&file_path, "0123456789").unwrap();
1854
1855 let (inode, _) = fs
1856 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("seek.txt"))
1857 .unwrap();
1858
1859 let handle = fs.open(inode, libc::O_RDONLY as u32).unwrap();
1860
1861 let pos = fs.lseek(handle, 5, 0).unwrap(); assert_eq!(pos, 5);
1864
1865 let data = fs.read(handle, pos, 5).unwrap();
1867 assert_eq!(data, b"56789");
1868
1869 fs.release(handle).unwrap();
1870 }
1871
1872 #[test]
1873 fn test_statfs() {
1874 let (_temp, fs) = setup_test_fs();
1875
1876 let stat = fs.statfs().unwrap();
1877 assert!(stat.blocks > 0);
1878 assert!(stat.bsize > 0);
1879 }
1880
1881 #[test]
1882 fn test_access() {
1883 let (temp, fs) = setup_test_fs();
1884
1885 let file_path = temp.path().join("access.txt");
1886 std::fs::write(&file_path, "test").unwrap();
1887
1888 let (inode, _) = fs
1889 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("access.txt"))
1890 .unwrap();
1891
1892 fs.access(inode, libc::R_OK as u32).unwrap();
1894
1895 fs.access(inode, libc::W_OK as u32).unwrap();
1897 }
1898
1899 #[test]
1900 fn test_forget() {
1901 let (temp, fs) = setup_test_fs();
1902
1903 let file_path = temp.path().join("forget.txt");
1904 std::fs::write(&file_path, "test").unwrap();
1905
1906 let (inode, _) = fs
1907 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
1908 .unwrap();
1909
1910 let (inode2, _) = fs
1912 .lookup(PassthroughFs::ROOT_INODE, OsStr::new("forget.txt"))
1913 .unwrap();
1914 assert_eq!(inode, inode2);
1915
1916 fs.forget(inode, 1);
1918
1919 assert!(fs.getattr(inode).is_ok());
1921
1922 fs.forget(inode, 1);
1924
1925 }
1927
1928 #[test]
1929 #[ignore = "mknod requires elevated permissions on macOS"]
1930 fn test_mknod_regular_file() {
1931 let (_temp, fs) = setup_test_fs();
1932
1933 let (inode, attr) = fs
1935 .mknod(
1936 PassthroughFs::ROOT_INODE,
1937 OsStr::new("mknod_file"),
1938 S_IFREG | 0o644,
1939 0,
1940 )
1941 .unwrap();
1942
1943 assert!(inode > PassthroughFs::ROOT_INODE);
1944 assert!(attr.mode & S_IFREG != 0);
1945 }
1946
1947 #[test]
1948 fn test_concurrent_operations() {
1949 use std::sync::Arc;
1950 use std::thread;
1951
1952 let (temp, fs) = setup_test_fs();
1953 let fs = Arc::new(fs);
1954
1955 for i in 0..10 {
1957 std::fs::write(
1958 temp.path().join(format!("file{i}.txt")),
1959 format!("content{i}"),
1960 )
1961 .unwrap();
1962 }
1963
1964 let mut handles = vec![];
1965
1966 for i in 0..4 {
1968 let fs = Arc::clone(&fs);
1969 handles.push(thread::spawn(move || {
1970 for j in 0..100 {
1971 let name = format!("file{}.txt", (i + j) % 10);
1972 let _ = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name));
1973 }
1974 }));
1975 }
1976
1977 for i in 0..4 {
1979 let fs = Arc::clone(&fs);
1980 handles.push(thread::spawn(move || {
1981 for j in 0..50 {
1982 let name = format!("file{}.txt", (i + j) % 10);
1983 if let Ok((inode, _)) = fs.lookup(PassthroughFs::ROOT_INODE, OsStr::new(&name))
1984 {
1985 if let Ok(handle) = fs.open(inode, libc::O_RDONLY as u32) {
1986 let _ = fs.read(handle, 0, 100);
1987 let _ = fs.release(handle);
1988 }
1989 }
1990 }
1991 }));
1992 }
1993
1994 for handle in handles {
1995 handle.join().expect("Thread panicked");
1996 }
1997 }
1998}