1use std::io::{self, Read, Seek, Write};
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum EntryType {
23 File,
24 Directory,
25 Symlink,
26}
27
28#[derive(Debug, Clone)]
30pub struct DirEntry {
31 pub path: PathBuf,
33 pub name: String,
35 pub entry_type: EntryType,
37 pub metadata: Option<FileMetadata>,
39 pub symlink_target_is_dir: bool,
41}
42
43impl DirEntry {
44 pub fn new(path: PathBuf, name: String, entry_type: EntryType) -> Self {
46 Self {
47 path,
48 name,
49 entry_type,
50 metadata: None,
51 symlink_target_is_dir: false,
52 }
53 }
54
55 pub fn new_symlink(path: PathBuf, name: String, target_is_dir: bool) -> Self {
57 Self {
58 path,
59 name,
60 entry_type: EntryType::Symlink,
61 metadata: None,
62 symlink_target_is_dir: target_is_dir,
63 }
64 }
65
66 pub fn with_metadata(mut self, metadata: FileMetadata) -> Self {
68 self.metadata = Some(metadata);
69 self
70 }
71
72 pub fn is_dir(&self) -> bool {
74 self.entry_type == EntryType::Directory
75 || (self.entry_type == EntryType::Symlink && self.symlink_target_is_dir)
76 }
77
78 pub fn is_file(&self) -> bool {
80 self.entry_type == EntryType::File
81 || (self.entry_type == EntryType::Symlink && !self.symlink_target_is_dir)
82 }
83
84 pub fn is_symlink(&self) -> bool {
86 self.entry_type == EntryType::Symlink
87 }
88}
89
90#[derive(Debug, Clone)]
96pub struct FileMetadata {
97 pub size: u64,
99 pub modified: Option<SystemTime>,
101 pub permissions: Option<FilePermissions>,
103 pub is_hidden: bool,
105 pub is_readonly: bool,
107 #[cfg(unix)]
109 pub uid: Option<u32>,
110 #[cfg(unix)]
112 pub gid: Option<u32>,
113}
114
115impl FileMetadata {
116 pub fn new(size: u64) -> Self {
118 Self {
119 size,
120 modified: None,
121 permissions: None,
122 is_hidden: false,
123 is_readonly: false,
124 #[cfg(unix)]
125 uid: None,
126 #[cfg(unix)]
127 gid: None,
128 }
129 }
130
131 pub fn with_modified(mut self, modified: SystemTime) -> Self {
133 self.modified = Some(modified);
134 self
135 }
136
137 pub fn with_hidden(mut self, hidden: bool) -> Self {
139 self.is_hidden = hidden;
140 self
141 }
142
143 pub fn with_readonly(mut self, readonly: bool) -> Self {
145 self.is_readonly = readonly;
146 self
147 }
148
149 pub fn with_permissions(mut self, permissions: FilePermissions) -> Self {
151 self.permissions = Some(permissions);
152 self
153 }
154}
155
156impl Default for FileMetadata {
157 fn default() -> Self {
158 Self::new(0)
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct FilePermissions {
165 #[cfg(unix)]
166 mode: u32,
167 #[cfg(not(unix))]
168 readonly: bool,
169}
170
171impl FilePermissions {
172 #[cfg(unix)]
174 pub fn from_mode(mode: u32) -> Self {
175 Self { mode }
176 }
177
178 #[cfg(not(unix))]
180 pub fn from_mode(mode: u32) -> Self {
181 Self {
182 readonly: mode & 0o222 == 0,
183 }
184 }
185
186 #[cfg(unix)]
188 pub fn from_std(perms: std::fs::Permissions) -> Self {
189 use std::os::unix::fs::PermissionsExt;
190 Self { mode: perms.mode() }
191 }
192
193 #[cfg(not(unix))]
194 pub fn from_std(perms: std::fs::Permissions) -> Self {
195 Self {
196 readonly: perms.readonly(),
197 }
198 }
199
200 #[cfg(unix)]
202 pub fn to_std(&self) -> std::fs::Permissions {
203 use std::os::unix::fs::PermissionsExt;
204 std::fs::Permissions::from_mode(self.mode)
205 }
206
207 #[cfg(not(unix))]
208 pub fn to_std(&self) -> std::fs::Permissions {
209 let mut perms = std::fs::Permissions::from(std::fs::metadata(".").unwrap().permissions());
210 perms.set_readonly(self.readonly);
211 perms
212 }
213
214 #[cfg(unix)]
216 pub fn mode(&self) -> u32 {
217 self.mode
218 }
219
220 pub fn is_readonly(&self) -> bool {
222 #[cfg(unix)]
223 {
224 self.mode & 0o222 == 0
225 }
226 #[cfg(not(unix))]
227 {
228 self.readonly
229 }
230 }
231}
232
233pub trait FileWriter: Write + Send {
239 fn sync_all(&self) -> io::Result<()>;
241}
242
243#[derive(Debug, Clone)]
249pub enum WriteOp<'a> {
250 Copy { offset: u64, len: u64 },
252 Insert { data: &'a [u8] },
254}
255
256struct StdFileWriter(std::fs::File);
258
259impl Write for StdFileWriter {
260 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
261 self.0.write(buf)
262 }
263
264 fn flush(&mut self) -> io::Result<()> {
265 self.0.flush()
266 }
267}
268
269impl FileWriter for StdFileWriter {
270 fn sync_all(&self) -> io::Result<()> {
271 self.0.sync_all()
272 }
273}
274
275pub trait FileReader: Read + Seek + Send {}
277
278struct StdFileReader(std::fs::File);
280
281impl Read for StdFileReader {
282 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
283 self.0.read(buf)
284 }
285}
286
287impl Seek for StdFileReader {
288 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
289 self.0.seek(pos)
290 }
291}
292
293impl FileReader for StdFileReader {}
294
295pub trait FileSystem: Send + Sync {
309 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>>;
315
316 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>>;
318
319 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()>;
321
322 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
324
325 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>>;
327
328 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
330
331 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
333
334 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()>;
336
337 fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
352 let mut buffer = Vec::new();
354 for op in ops {
355 match op {
356 WriteOp::Copy { offset, len } => {
357 let data = self.read_range(src_path, *offset, *len as usize)?;
358 buffer.extend_from_slice(&data);
359 }
360 WriteOp::Insert { data } => {
361 buffer.extend_from_slice(data);
362 }
363 }
364 }
365 self.write_file(dst_path, &buffer)
366 }
367
368 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
374
375 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64>;
377
378 fn remove_file(&self, path: &Path) -> io::Result<()>;
380
381 fn remove_dir(&self, path: &Path) -> io::Result<()>;
383
384 fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
390
391 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
393
394 fn exists(&self, path: &Path) -> bool {
396 self.metadata(path).is_ok()
397 }
398
399 fn metadata_if_exists(&self, path: &Path) -> Option<FileMetadata> {
401 self.metadata(path).ok()
402 }
403
404 fn is_dir(&self, path: &Path) -> io::Result<bool>;
406
407 fn is_file(&self, path: &Path) -> io::Result<bool>;
409
410 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()>;
412
413 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
419
420 fn create_dir(&self, path: &Path) -> io::Result<()>;
422
423 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
425
426 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
432
433 fn current_uid(&self) -> u32;
439
440 fn is_owner(&self, path: &Path) -> bool {
442 #[cfg(unix)]
443 {
444 if let Ok(meta) = self.metadata(path) {
445 if let Some(uid) = meta.uid {
446 return uid == self.current_uid();
447 }
448 }
449 true
450 }
451 #[cfg(not(unix))]
452 {
453 let _ = path;
454 true
455 }
456 }
457
458 fn temp_path_for(&self, path: &Path) -> PathBuf {
460 path.with_extension("tmp")
461 }
462
463 fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
465 let temp_dir = std::env::temp_dir();
466 let file_name = dest_path
467 .file_name()
468 .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
469 let timestamp = std::time::SystemTime::now()
470 .duration_since(std::time::UNIX_EPOCH)
471 .map(|d| d.as_nanos())
472 .unwrap_or(0);
473 temp_dir.join(format!(
474 "{}-{}-{}.tmp",
475 file_name.to_string_lossy(),
476 std::process::id(),
477 timestamp
478 ))
479 }
480
481 fn remote_connection_info(&self) -> Option<&str> {
490 None
491 }
492
493 fn home_dir(&self) -> io::Result<PathBuf> {
498 dirs::home_dir()
499 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))
500 }
501
502 fn sudo_write(&self, path: &Path, data: &[u8], mode: u32, uid: u32, gid: u32)
513 -> io::Result<()>;
514}
515
516pub trait FileSystemExt: FileSystem {
541 fn read_file_async(
543 &self,
544 path: &Path,
545 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
546 async { self.read_file(path) }
547 }
548
549 fn read_range_async(
551 &self,
552 path: &Path,
553 offset: u64,
554 len: usize,
555 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
556 async move { self.read_range(path, offset, len) }
557 }
558
559 fn write_file_async(
561 &self,
562 path: &Path,
563 data: &[u8],
564 ) -> impl std::future::Future<Output = io::Result<()>> + Send {
565 async { self.write_file(path, data) }
566 }
567
568 fn metadata_async(
570 &self,
571 path: &Path,
572 ) -> impl std::future::Future<Output = io::Result<FileMetadata>> + Send {
573 async { self.metadata(path) }
574 }
575
576 fn exists_async(&self, path: &Path) -> impl std::future::Future<Output = bool> + Send {
578 async { self.exists(path) }
579 }
580
581 fn is_dir_async(
583 &self,
584 path: &Path,
585 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
586 async { self.is_dir(path) }
587 }
588
589 fn is_file_async(
591 &self,
592 path: &Path,
593 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
594 async { self.is_file(path) }
595 }
596
597 fn read_dir_async(
599 &self,
600 path: &Path,
601 ) -> impl std::future::Future<Output = io::Result<Vec<DirEntry>>> + Send {
602 async { self.read_dir(path) }
603 }
604
605 fn canonicalize_async(
607 &self,
608 path: &Path,
609 ) -> impl std::future::Future<Output = io::Result<PathBuf>> + Send {
610 async { self.canonicalize(path) }
611 }
612}
613
614impl<T: FileSystem> FileSystemExt for T {}
616
617#[derive(Debug, Clone, Copy, Default)]
625pub struct StdFileSystem;
626
627impl StdFileSystem {
628 fn is_hidden(path: &Path) -> bool {
630 path.file_name()
631 .and_then(|n| n.to_str())
632 .is_some_and(|n| n.starts_with('.'))
633 }
634
635 fn build_metadata(path: &Path, meta: &std::fs::Metadata) -> FileMetadata {
637 #[cfg(unix)]
638 {
639 use std::os::unix::fs::MetadataExt;
640 FileMetadata {
641 size: meta.len(),
642 modified: meta.modified().ok(),
643 permissions: Some(FilePermissions::from_std(meta.permissions())),
644 is_hidden: Self::is_hidden(path),
645 is_readonly: meta.permissions().readonly(),
646 uid: Some(meta.uid()),
647 gid: Some(meta.gid()),
648 }
649 }
650 #[cfg(not(unix))]
651 {
652 FileMetadata {
653 size: meta.len(),
654 modified: meta.modified().ok(),
655 permissions: Some(FilePermissions::from_std(meta.permissions())),
656 is_hidden: Self::is_hidden(path),
657 is_readonly: meta.permissions().readonly(),
658 }
659 }
660 }
661}
662
663impl FileSystem for StdFileSystem {
664 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
666 std::fs::read(path)
667 }
668
669 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
670 let mut file = std::fs::File::open(path)?;
671 file.seek(io::SeekFrom::Start(offset))?;
672 let mut buffer = vec![0u8; len];
673 file.read_exact(&mut buffer)?;
674 Ok(buffer)
675 }
676
677 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
678 let original_metadata = self.metadata_if_exists(path);
679 let temp_path = self.temp_path_for(path);
680 {
681 let mut file = self.create_file(&temp_path)?;
682 file.write_all(data)?;
683 file.sync_all()?;
684 }
685 if let Some(ref meta) = original_metadata {
686 if let Some(ref perms) = meta.permissions {
687 let _ = self.set_permissions(&temp_path, perms);
688 }
689 }
690 self.rename(&temp_path, path)?;
691 Ok(())
692 }
693
694 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
695 let file = std::fs::File::create(path)?;
696 Ok(Box::new(StdFileWriter(file)))
697 }
698
699 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
700 let file = std::fs::File::open(path)?;
701 Ok(Box::new(StdFileReader(file)))
702 }
703
704 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
705 let file = std::fs::OpenOptions::new()
706 .write(true)
707 .truncate(true)
708 .open(path)?;
709 Ok(Box::new(StdFileWriter(file)))
710 }
711
712 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
713 let file = std::fs::OpenOptions::new()
714 .create(true)
715 .append(true)
716 .open(path)?;
717 Ok(Box::new(StdFileWriter(file)))
718 }
719
720 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
721 let file = std::fs::OpenOptions::new().write(true).open(path)?;
722 file.set_len(len)
723 }
724
725 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
727 std::fs::rename(from, to)
728 }
729
730 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
731 std::fs::copy(from, to)
732 }
733
734 fn remove_file(&self, path: &Path) -> io::Result<()> {
735 std::fs::remove_file(path)
736 }
737
738 fn remove_dir(&self, path: &Path) -> io::Result<()> {
739 std::fs::remove_dir(path)
740 }
741
742 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
744 let meta = std::fs::metadata(path)?;
745 Ok(Self::build_metadata(path, &meta))
746 }
747
748 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
749 let meta = std::fs::symlink_metadata(path)?;
750 Ok(Self::build_metadata(path, &meta))
751 }
752
753 fn is_dir(&self, path: &Path) -> io::Result<bool> {
754 Ok(std::fs::metadata(path)?.is_dir())
755 }
756
757 fn is_file(&self, path: &Path) -> io::Result<bool> {
758 Ok(std::fs::metadata(path)?.is_file())
759 }
760
761 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
762 std::fs::set_permissions(path, permissions.to_std())
763 }
764
765 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
767 let mut entries = Vec::new();
768 for entry in std::fs::read_dir(path)? {
769 let entry = entry?;
770 let path = entry.path();
771 let name = entry.file_name().to_string_lossy().into_owned();
772 let file_type = entry.file_type()?;
773
774 let entry_type = if file_type.is_dir() {
775 EntryType::Directory
776 } else if file_type.is_symlink() {
777 EntryType::Symlink
778 } else {
779 EntryType::File
780 };
781
782 let mut dir_entry = DirEntry::new(path.clone(), name, entry_type);
783
784 if file_type.is_symlink() {
786 dir_entry.symlink_target_is_dir = std::fs::metadata(&path)
787 .map(|m| m.is_dir())
788 .unwrap_or(false);
789 }
790
791 entries.push(dir_entry);
792 }
793 Ok(entries)
794 }
795
796 fn create_dir(&self, path: &Path) -> io::Result<()> {
797 std::fs::create_dir(path)
798 }
799
800 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
801 std::fs::create_dir_all(path)
802 }
803
804 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
806 std::fs::canonicalize(path)
807 }
808
809 fn current_uid(&self) -> u32 {
811 #[cfg(all(unix, feature = "runtime"))]
812 {
813 unsafe { libc::getuid() }
815 }
816 #[cfg(not(all(unix, feature = "runtime")))]
817 {
818 0
819 }
820 }
821
822 fn sudo_write(
823 &self,
824 path: &Path,
825 data: &[u8],
826 mode: u32,
827 uid: u32,
828 gid: u32,
829 ) -> io::Result<()> {
830 use std::process::{Command, Stdio};
831
832 let mut child = Command::new("sudo")
834 .args(["tee", &path.to_string_lossy()])
835 .stdin(Stdio::piped())
836 .stdout(Stdio::null())
837 .stderr(Stdio::piped())
838 .spawn()
839 .map_err(|e| {
840 io::Error::new(io::ErrorKind::Other, format!("failed to spawn sudo: {}", e))
841 })?;
842
843 if let Some(mut stdin) = child.stdin.take() {
844 use std::io::Write;
845 stdin.write_all(data)?;
846 }
847
848 let output = child.wait_with_output()?;
849 if !output.status.success() {
850 let stderr = String::from_utf8_lossy(&output.stderr);
851 return Err(io::Error::new(
852 io::ErrorKind::PermissionDenied,
853 format!("sudo tee failed: {}", stderr.trim()),
854 ));
855 }
856
857 let status = Command::new("sudo")
859 .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()])
860 .status()?;
861 if !status.success() {
862 return Err(io::Error::new(io::ErrorKind::Other, "sudo chmod failed"));
863 }
864
865 let status = Command::new("sudo")
867 .args([
868 "chown",
869 &format!("{}:{}", uid, gid),
870 &path.to_string_lossy(),
871 ])
872 .status()?;
873 if !status.success() {
874 return Err(io::Error::new(io::ErrorKind::Other, "sudo chown failed"));
875 }
876
877 Ok(())
878 }
879}
880
881#[derive(Debug, Clone, Copy, Default)]
890pub struct NoopFileSystem;
891
892impl NoopFileSystem {
893 fn unsupported<T>() -> io::Result<T> {
894 Err(io::Error::new(
895 io::ErrorKind::Unsupported,
896 "Filesystem not available",
897 ))
898 }
899}
900
901impl FileSystem for NoopFileSystem {
902 fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
903 Self::unsupported()
904 }
905
906 fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> io::Result<Vec<u8>> {
907 Self::unsupported()
908 }
909
910 fn write_file(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
911 Self::unsupported()
912 }
913
914 fn create_file(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
915 Self::unsupported()
916 }
917
918 fn open_file(&self, _path: &Path) -> io::Result<Box<dyn FileReader>> {
919 Self::unsupported()
920 }
921
922 fn open_file_for_write(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
923 Self::unsupported()
924 }
925
926 fn open_file_for_append(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
927 Self::unsupported()
928 }
929
930 fn set_file_length(&self, _path: &Path, _len: u64) -> io::Result<()> {
931 Self::unsupported()
932 }
933
934 fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
935 Self::unsupported()
936 }
937
938 fn copy(&self, _from: &Path, _to: &Path) -> io::Result<u64> {
939 Self::unsupported()
940 }
941
942 fn remove_file(&self, _path: &Path) -> io::Result<()> {
943 Self::unsupported()
944 }
945
946 fn remove_dir(&self, _path: &Path) -> io::Result<()> {
947 Self::unsupported()
948 }
949
950 fn metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
951 Self::unsupported()
952 }
953
954 fn symlink_metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
955 Self::unsupported()
956 }
957
958 fn is_dir(&self, _path: &Path) -> io::Result<bool> {
959 Self::unsupported()
960 }
961
962 fn is_file(&self, _path: &Path) -> io::Result<bool> {
963 Self::unsupported()
964 }
965
966 fn set_permissions(&self, _path: &Path, _permissions: &FilePermissions) -> io::Result<()> {
967 Self::unsupported()
968 }
969
970 fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
971 Self::unsupported()
972 }
973
974 fn create_dir(&self, _path: &Path) -> io::Result<()> {
975 Self::unsupported()
976 }
977
978 fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
979 Self::unsupported()
980 }
981
982 fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
983 Self::unsupported()
984 }
985
986 fn current_uid(&self) -> u32 {
987 0
988 }
989
990 fn sudo_write(
991 &self,
992 _path: &Path,
993 _data: &[u8],
994 _mode: u32,
995 _uid: u32,
996 _gid: u32,
997 ) -> io::Result<()> {
998 Self::unsupported()
999 }
1000}
1001
1002#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use tempfile::NamedTempFile;
1010
1011 #[test]
1012 fn test_std_filesystem_read_write() {
1013 let fs = StdFileSystem;
1014 let mut temp = NamedTempFile::new().unwrap();
1015 let path = temp.path().to_path_buf();
1016
1017 std::io::Write::write_all(&mut temp, b"Hello, World!").unwrap();
1018 std::io::Write::flush(&mut temp).unwrap();
1019
1020 let content = fs.read_file(&path).unwrap();
1021 assert_eq!(content, b"Hello, World!");
1022
1023 let range = fs.read_range(&path, 7, 5).unwrap();
1024 assert_eq!(range, b"World");
1025
1026 let meta = fs.metadata(&path).unwrap();
1027 assert_eq!(meta.size, 13);
1028 }
1029
1030 #[test]
1031 fn test_noop_filesystem() {
1032 let fs = NoopFileSystem;
1033 let path = Path::new("/some/path");
1034
1035 assert!(fs.read_file(path).is_err());
1036 assert!(fs.read_range(path, 0, 10).is_err());
1037 assert!(fs.write_file(path, b"data").is_err());
1038 assert!(fs.metadata(path).is_err());
1039 assert!(fs.read_dir(path).is_err());
1040 }
1041
1042 #[test]
1043 fn test_create_and_write_file() {
1044 let fs = StdFileSystem;
1045 let temp_dir = tempfile::tempdir().unwrap();
1046 let path = temp_dir.path().join("test.txt");
1047
1048 {
1049 let mut writer = fs.create_file(&path).unwrap();
1050 writer.write_all(b"test content").unwrap();
1051 writer.sync_all().unwrap();
1052 }
1053
1054 let content = fs.read_file(&path).unwrap();
1055 assert_eq!(content, b"test content");
1056 }
1057
1058 #[test]
1059 fn test_read_dir() {
1060 let fs = StdFileSystem;
1061 let temp_dir = tempfile::tempdir().unwrap();
1062
1063 fs.create_dir(&temp_dir.path().join("subdir")).unwrap();
1065 fs.write_file(&temp_dir.path().join("file1.txt"), b"content1")
1066 .unwrap();
1067 fs.write_file(&temp_dir.path().join("file2.txt"), b"content2")
1068 .unwrap();
1069
1070 let entries = fs.read_dir(temp_dir.path()).unwrap();
1071 assert_eq!(entries.len(), 3);
1072
1073 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1074 assert!(names.contains(&"subdir"));
1075 assert!(names.contains(&"file1.txt"));
1076 assert!(names.contains(&"file2.txt"));
1077 }
1078
1079 #[test]
1080 fn test_dir_entry_types() {
1081 let file = DirEntry::new(PathBuf::from("/file"), "file".to_string(), EntryType::File);
1082 assert!(file.is_file());
1083 assert!(!file.is_dir());
1084
1085 let dir = DirEntry::new(
1086 PathBuf::from("/dir"),
1087 "dir".to_string(),
1088 EntryType::Directory,
1089 );
1090 assert!(dir.is_dir());
1091 assert!(!dir.is_file());
1092
1093 let link_to_dir = DirEntry::new_symlink(PathBuf::from("/link"), "link".to_string(), true);
1094 assert!(link_to_dir.is_symlink());
1095 assert!(link_to_dir.is_dir());
1096 }
1097
1098 #[test]
1099 fn test_metadata_builder() {
1100 let meta = FileMetadata::default()
1101 .with_hidden(true)
1102 .with_readonly(true);
1103 assert!(meta.is_hidden);
1104 assert!(meta.is_readonly);
1105 }
1106
1107 #[test]
1108 fn test_atomic_write() {
1109 let fs = StdFileSystem;
1110 let temp_dir = tempfile::tempdir().unwrap();
1111 let path = temp_dir.path().join("atomic_test.txt");
1112
1113 fs.write_file(&path, b"initial").unwrap();
1114 assert_eq!(fs.read_file(&path).unwrap(), b"initial");
1115
1116 fs.write_file(&path, b"updated").unwrap();
1117 assert_eq!(fs.read_file(&path).unwrap(), b"updated");
1118 }
1119
1120 #[test]
1121 fn test_write_patched_default_impl() {
1122 let fs = StdFileSystem;
1124 let temp_dir = tempfile::tempdir().unwrap();
1125 let src_path = temp_dir.path().join("source.txt");
1126 let dst_path = temp_dir.path().join("dest.txt");
1127
1128 fs.write_file(&src_path, b"AAABBBCCC").unwrap();
1130
1131 let ops = vec![
1133 WriteOp::Copy { offset: 0, len: 3 }, WriteOp::Insert { data: b"XXX" }, WriteOp::Copy { offset: 6, len: 3 }, ];
1137
1138 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1139
1140 let result = fs.read_file(&dst_path).unwrap();
1141 assert_eq!(result, b"AAAXXXCCC");
1142 }
1143
1144 #[test]
1145 fn test_write_patched_same_file() {
1146 let fs = StdFileSystem;
1148 let temp_dir = tempfile::tempdir().unwrap();
1149 let path = temp_dir.path().join("file.txt");
1150
1151 fs.write_file(&path, b"Hello World").unwrap();
1153
1154 let ops = vec![
1156 WriteOp::Copy { offset: 0, len: 6 }, WriteOp::Insert { data: b"Rust" }, ];
1159
1160 fs.write_patched(&path, &path, &ops).unwrap();
1161
1162 let result = fs.read_file(&path).unwrap();
1163 assert_eq!(result, b"Hello Rust");
1164 }
1165
1166 #[test]
1167 fn test_write_patched_insert_only() {
1168 let fs = StdFileSystem;
1170 let temp_dir = tempfile::tempdir().unwrap();
1171 let src_path = temp_dir.path().join("empty.txt");
1172 let dst_path = temp_dir.path().join("new.txt");
1173
1174 fs.write_file(&src_path, b"").unwrap();
1176
1177 let ops = vec![WriteOp::Insert {
1178 data: b"All new content",
1179 }];
1180
1181 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1182
1183 let result = fs.read_file(&dst_path).unwrap();
1184 assert_eq!(result, b"All new content");
1185 }
1186}