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 count_line_feeds_in_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<usize> {
327 let data = self.read_range(path, offset, len)?;
328 Ok(data.iter().filter(|&&b| b == b'\n').count())
329 }
330
331 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()>;
333
334 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
336
337 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>>;
339
340 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
342
343 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
345
346 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()>;
348
349 fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
364 let mut buffer = Vec::new();
366 for op in ops {
367 match op {
368 WriteOp::Copy { offset, len } => {
369 let data = self.read_range(src_path, *offset, *len as usize)?;
370 buffer.extend_from_slice(&data);
371 }
372 WriteOp::Insert { data } => {
373 buffer.extend_from_slice(data);
374 }
375 }
376 }
377 self.write_file(dst_path, &buffer)
378 }
379
380 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
386
387 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64>;
389
390 fn remove_file(&self, path: &Path) -> io::Result<()>;
392
393 fn remove_dir(&self, path: &Path) -> io::Result<()>;
395
396 fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
402
403 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
405
406 fn exists(&self, path: &Path) -> bool {
408 self.metadata(path).is_ok()
409 }
410
411 fn metadata_if_exists(&self, path: &Path) -> Option<FileMetadata> {
413 self.metadata(path).ok()
414 }
415
416 fn is_dir(&self, path: &Path) -> io::Result<bool>;
418
419 fn is_file(&self, path: &Path) -> io::Result<bool>;
421
422 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()>;
424
425 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
431
432 fn create_dir(&self, path: &Path) -> io::Result<()>;
434
435 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
437
438 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
444
445 fn current_uid(&self) -> u32;
451
452 fn is_owner(&self, path: &Path) -> bool {
454 #[cfg(unix)]
455 {
456 if let Ok(meta) = self.metadata(path) {
457 if let Some(uid) = meta.uid {
458 return uid == self.current_uid();
459 }
460 }
461 true
462 }
463 #[cfg(not(unix))]
464 {
465 let _ = path;
466 true
467 }
468 }
469
470 fn temp_path_for(&self, path: &Path) -> PathBuf {
472 path.with_extension("tmp")
473 }
474
475 fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
477 let temp_dir = std::env::temp_dir();
478 let file_name = dest_path
479 .file_name()
480 .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
481 let timestamp = std::time::SystemTime::now()
482 .duration_since(std::time::UNIX_EPOCH)
483 .map(|d| d.as_nanos())
484 .unwrap_or(0);
485 temp_dir.join(format!(
486 "{}-{}-{}.tmp",
487 file_name.to_string_lossy(),
488 std::process::id(),
489 timestamp
490 ))
491 }
492
493 fn remote_connection_info(&self) -> Option<&str> {
502 None
503 }
504
505 fn home_dir(&self) -> io::Result<PathBuf> {
510 dirs::home_dir()
511 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))
512 }
513
514 fn sudo_write(&self, path: &Path, data: &[u8], mode: u32, uid: u32, gid: u32)
525 -> io::Result<()>;
526}
527
528pub trait FileSystemExt: FileSystem {
553 fn read_file_async(
555 &self,
556 path: &Path,
557 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
558 async { self.read_file(path) }
559 }
560
561 fn read_range_async(
563 &self,
564 path: &Path,
565 offset: u64,
566 len: usize,
567 ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
568 async move { self.read_range(path, offset, len) }
569 }
570
571 fn count_line_feeds_in_range_async(
573 &self,
574 path: &Path,
575 offset: u64,
576 len: usize,
577 ) -> impl std::future::Future<Output = io::Result<usize>> + Send {
578 async move { self.count_line_feeds_in_range(path, offset, len) }
579 }
580
581 fn write_file_async(
583 &self,
584 path: &Path,
585 data: &[u8],
586 ) -> impl std::future::Future<Output = io::Result<()>> + Send {
587 async { self.write_file(path, data) }
588 }
589
590 fn metadata_async(
592 &self,
593 path: &Path,
594 ) -> impl std::future::Future<Output = io::Result<FileMetadata>> + Send {
595 async { self.metadata(path) }
596 }
597
598 fn exists_async(&self, path: &Path) -> impl std::future::Future<Output = bool> + Send {
600 async { self.exists(path) }
601 }
602
603 fn is_dir_async(
605 &self,
606 path: &Path,
607 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
608 async { self.is_dir(path) }
609 }
610
611 fn is_file_async(
613 &self,
614 path: &Path,
615 ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
616 async { self.is_file(path) }
617 }
618
619 fn read_dir_async(
621 &self,
622 path: &Path,
623 ) -> impl std::future::Future<Output = io::Result<Vec<DirEntry>>> + Send {
624 async { self.read_dir(path) }
625 }
626
627 fn canonicalize_async(
629 &self,
630 path: &Path,
631 ) -> impl std::future::Future<Output = io::Result<PathBuf>> + Send {
632 async { self.canonicalize(path) }
633 }
634}
635
636impl<T: FileSystem> FileSystemExt for T {}
638
639#[derive(Debug, Clone, Copy, Default)]
647pub struct StdFileSystem;
648
649impl StdFileSystem {
650 fn is_hidden(path: &Path) -> bool {
652 path.file_name()
653 .and_then(|n| n.to_str())
654 .is_some_and(|n| n.starts_with('.'))
655 }
656
657 fn build_metadata(path: &Path, meta: &std::fs::Metadata) -> FileMetadata {
659 #[cfg(unix)]
660 {
661 use std::os::unix::fs::MetadataExt;
662 FileMetadata {
663 size: meta.len(),
664 modified: meta.modified().ok(),
665 permissions: Some(FilePermissions::from_std(meta.permissions())),
666 is_hidden: Self::is_hidden(path),
667 is_readonly: meta.permissions().readonly(),
668 uid: Some(meta.uid()),
669 gid: Some(meta.gid()),
670 }
671 }
672 #[cfg(not(unix))]
673 {
674 FileMetadata {
675 size: meta.len(),
676 modified: meta.modified().ok(),
677 permissions: Some(FilePermissions::from_std(meta.permissions())),
678 is_hidden: Self::is_hidden(path),
679 is_readonly: meta.permissions().readonly(),
680 }
681 }
682 }
683}
684
685impl FileSystem for StdFileSystem {
686 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
688 std::fs::read(path)
689 }
690
691 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
692 let mut file = std::fs::File::open(path)?;
693 file.seek(io::SeekFrom::Start(offset))?;
694 let mut buffer = vec![0u8; len];
695 file.read_exact(&mut buffer)?;
696 Ok(buffer)
697 }
698
699 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
700 let original_metadata = self.metadata_if_exists(path);
701 let temp_path = self.temp_path_for(path);
702 {
703 let mut file = self.create_file(&temp_path)?;
704 file.write_all(data)?;
705 file.sync_all()?;
706 }
707 if let Some(ref meta) = original_metadata {
708 if let Some(ref perms) = meta.permissions {
709 #[allow(clippy::let_underscore_must_use)]
711 let _ = self.set_permissions(&temp_path, perms);
712 }
713 }
714 self.rename(&temp_path, path)?;
715 Ok(())
716 }
717
718 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
719 let file = std::fs::File::create(path)?;
720 Ok(Box::new(StdFileWriter(file)))
721 }
722
723 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
724 let file = std::fs::File::open(path)?;
725 Ok(Box::new(StdFileReader(file)))
726 }
727
728 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
729 let file = std::fs::OpenOptions::new()
730 .write(true)
731 .truncate(true)
732 .open(path)?;
733 Ok(Box::new(StdFileWriter(file)))
734 }
735
736 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
737 let file = std::fs::OpenOptions::new()
738 .create(true)
739 .append(true)
740 .open(path)?;
741 Ok(Box::new(StdFileWriter(file)))
742 }
743
744 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
745 let file = std::fs::OpenOptions::new().write(true).open(path)?;
746 file.set_len(len)
747 }
748
749 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
751 std::fs::rename(from, to)
752 }
753
754 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
755 std::fs::copy(from, to)
756 }
757
758 fn remove_file(&self, path: &Path) -> io::Result<()> {
759 std::fs::remove_file(path)
760 }
761
762 fn remove_dir(&self, path: &Path) -> io::Result<()> {
763 std::fs::remove_dir(path)
764 }
765
766 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
768 let meta = std::fs::metadata(path)?;
769 Ok(Self::build_metadata(path, &meta))
770 }
771
772 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
773 let meta = std::fs::symlink_metadata(path)?;
774 Ok(Self::build_metadata(path, &meta))
775 }
776
777 fn is_dir(&self, path: &Path) -> io::Result<bool> {
778 Ok(std::fs::metadata(path)?.is_dir())
779 }
780
781 fn is_file(&self, path: &Path) -> io::Result<bool> {
782 Ok(std::fs::metadata(path)?.is_file())
783 }
784
785 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
786 std::fs::set_permissions(path, permissions.to_std())
787 }
788
789 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
791 let mut entries = Vec::new();
792 for entry in std::fs::read_dir(path)? {
793 let entry = entry?;
794 let path = entry.path();
795 let name = entry.file_name().to_string_lossy().into_owned();
796 let file_type = entry.file_type()?;
797
798 let entry_type = if file_type.is_dir() {
799 EntryType::Directory
800 } else if file_type.is_symlink() {
801 EntryType::Symlink
802 } else {
803 EntryType::File
804 };
805
806 let mut dir_entry = DirEntry::new(path.clone(), name, entry_type);
807
808 if file_type.is_symlink() {
810 dir_entry.symlink_target_is_dir = std::fs::metadata(&path)
811 .map(|m| m.is_dir())
812 .unwrap_or(false);
813 }
814
815 entries.push(dir_entry);
816 }
817 Ok(entries)
818 }
819
820 fn create_dir(&self, path: &Path) -> io::Result<()> {
821 std::fs::create_dir(path)
822 }
823
824 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
825 std::fs::create_dir_all(path)
826 }
827
828 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
830 std::fs::canonicalize(path)
831 }
832
833 fn current_uid(&self) -> u32 {
835 #[cfg(all(unix, feature = "runtime"))]
836 {
837 unsafe { libc::getuid() }
839 }
840 #[cfg(not(all(unix, feature = "runtime")))]
841 {
842 0
843 }
844 }
845
846 fn sudo_write(
847 &self,
848 path: &Path,
849 data: &[u8],
850 mode: u32,
851 uid: u32,
852 gid: u32,
853 ) -> io::Result<()> {
854 use std::process::{Command, Stdio};
855
856 let mut child = Command::new("sudo")
858 .args(["tee", &path.to_string_lossy()])
859 .stdin(Stdio::piped())
860 .stdout(Stdio::null())
861 .stderr(Stdio::piped())
862 .spawn()
863 .map_err(|e| io::Error::other(format!("failed to spawn sudo: {}", e)))?;
864
865 if let Some(mut stdin) = child.stdin.take() {
866 use std::io::Write;
867 stdin.write_all(data)?;
868 }
869
870 let output = child.wait_with_output()?;
871 if !output.status.success() {
872 let stderr = String::from_utf8_lossy(&output.stderr);
873 return Err(io::Error::new(
874 io::ErrorKind::PermissionDenied,
875 format!("sudo tee failed: {}", stderr.trim()),
876 ));
877 }
878
879 let status = Command::new("sudo")
881 .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()])
882 .status()?;
883 if !status.success() {
884 return Err(io::Error::other("sudo chmod failed"));
885 }
886
887 let status = Command::new("sudo")
889 .args([
890 "chown",
891 &format!("{}:{}", uid, gid),
892 &path.to_string_lossy(),
893 ])
894 .status()?;
895 if !status.success() {
896 return Err(io::Error::other("sudo chown failed"));
897 }
898
899 Ok(())
900 }
901}
902
903#[derive(Debug, Clone, Copy, Default)]
912pub struct NoopFileSystem;
913
914impl NoopFileSystem {
915 fn unsupported<T>() -> io::Result<T> {
916 Err(io::Error::new(
917 io::ErrorKind::Unsupported,
918 "Filesystem not available",
919 ))
920 }
921}
922
923impl FileSystem for NoopFileSystem {
924 fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
925 Self::unsupported()
926 }
927
928 fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> io::Result<Vec<u8>> {
929 Self::unsupported()
930 }
931
932 fn write_file(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
933 Self::unsupported()
934 }
935
936 fn create_file(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
937 Self::unsupported()
938 }
939
940 fn open_file(&self, _path: &Path) -> io::Result<Box<dyn FileReader>> {
941 Self::unsupported()
942 }
943
944 fn open_file_for_write(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
945 Self::unsupported()
946 }
947
948 fn open_file_for_append(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
949 Self::unsupported()
950 }
951
952 fn set_file_length(&self, _path: &Path, _len: u64) -> io::Result<()> {
953 Self::unsupported()
954 }
955
956 fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
957 Self::unsupported()
958 }
959
960 fn copy(&self, _from: &Path, _to: &Path) -> io::Result<u64> {
961 Self::unsupported()
962 }
963
964 fn remove_file(&self, _path: &Path) -> io::Result<()> {
965 Self::unsupported()
966 }
967
968 fn remove_dir(&self, _path: &Path) -> io::Result<()> {
969 Self::unsupported()
970 }
971
972 fn metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
973 Self::unsupported()
974 }
975
976 fn symlink_metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
977 Self::unsupported()
978 }
979
980 fn is_dir(&self, _path: &Path) -> io::Result<bool> {
981 Self::unsupported()
982 }
983
984 fn is_file(&self, _path: &Path) -> io::Result<bool> {
985 Self::unsupported()
986 }
987
988 fn set_permissions(&self, _path: &Path, _permissions: &FilePermissions) -> io::Result<()> {
989 Self::unsupported()
990 }
991
992 fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
993 Self::unsupported()
994 }
995
996 fn create_dir(&self, _path: &Path) -> io::Result<()> {
997 Self::unsupported()
998 }
999
1000 fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1001 Self::unsupported()
1002 }
1003
1004 fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1005 Self::unsupported()
1006 }
1007
1008 fn current_uid(&self) -> u32 {
1009 0
1010 }
1011
1012 fn sudo_write(
1013 &self,
1014 _path: &Path,
1015 _data: &[u8],
1016 _mode: u32,
1017 _uid: u32,
1018 _gid: u32,
1019 ) -> io::Result<()> {
1020 Self::unsupported()
1021 }
1022}
1023
1024#[cfg(test)]
1029mod tests {
1030 use super::*;
1031 use tempfile::NamedTempFile;
1032
1033 #[test]
1034 fn test_std_filesystem_read_write() {
1035 let fs = StdFileSystem;
1036 let mut temp = NamedTempFile::new().unwrap();
1037 let path = temp.path().to_path_buf();
1038
1039 std::io::Write::write_all(&mut temp, b"Hello, World!").unwrap();
1040 std::io::Write::flush(&mut temp).unwrap();
1041
1042 let content = fs.read_file(&path).unwrap();
1043 assert_eq!(content, b"Hello, World!");
1044
1045 let range = fs.read_range(&path, 7, 5).unwrap();
1046 assert_eq!(range, b"World");
1047
1048 let meta = fs.metadata(&path).unwrap();
1049 assert_eq!(meta.size, 13);
1050 }
1051
1052 #[test]
1053 fn test_noop_filesystem() {
1054 let fs = NoopFileSystem;
1055 let path = Path::new("/some/path");
1056
1057 assert!(fs.read_file(path).is_err());
1058 assert!(fs.read_range(path, 0, 10).is_err());
1059 assert!(fs.write_file(path, b"data").is_err());
1060 assert!(fs.metadata(path).is_err());
1061 assert!(fs.read_dir(path).is_err());
1062 }
1063
1064 #[test]
1065 fn test_create_and_write_file() {
1066 let fs = StdFileSystem;
1067 let temp_dir = tempfile::tempdir().unwrap();
1068 let path = temp_dir.path().join("test.txt");
1069
1070 {
1071 let mut writer = fs.create_file(&path).unwrap();
1072 writer.write_all(b"test content").unwrap();
1073 writer.sync_all().unwrap();
1074 }
1075
1076 let content = fs.read_file(&path).unwrap();
1077 assert_eq!(content, b"test content");
1078 }
1079
1080 #[test]
1081 fn test_read_dir() {
1082 let fs = StdFileSystem;
1083 let temp_dir = tempfile::tempdir().unwrap();
1084
1085 fs.create_dir(&temp_dir.path().join("subdir")).unwrap();
1087 fs.write_file(&temp_dir.path().join("file1.txt"), b"content1")
1088 .unwrap();
1089 fs.write_file(&temp_dir.path().join("file2.txt"), b"content2")
1090 .unwrap();
1091
1092 let entries = fs.read_dir(temp_dir.path()).unwrap();
1093 assert_eq!(entries.len(), 3);
1094
1095 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1096 assert!(names.contains(&"subdir"));
1097 assert!(names.contains(&"file1.txt"));
1098 assert!(names.contains(&"file2.txt"));
1099 }
1100
1101 #[test]
1102 fn test_dir_entry_types() {
1103 let file = DirEntry::new(PathBuf::from("/file"), "file".to_string(), EntryType::File);
1104 assert!(file.is_file());
1105 assert!(!file.is_dir());
1106
1107 let dir = DirEntry::new(
1108 PathBuf::from("/dir"),
1109 "dir".to_string(),
1110 EntryType::Directory,
1111 );
1112 assert!(dir.is_dir());
1113 assert!(!dir.is_file());
1114
1115 let link_to_dir = DirEntry::new_symlink(PathBuf::from("/link"), "link".to_string(), true);
1116 assert!(link_to_dir.is_symlink());
1117 assert!(link_to_dir.is_dir());
1118 }
1119
1120 #[test]
1121 fn test_metadata_builder() {
1122 let meta = FileMetadata::default()
1123 .with_hidden(true)
1124 .with_readonly(true);
1125 assert!(meta.is_hidden);
1126 assert!(meta.is_readonly);
1127 }
1128
1129 #[test]
1130 fn test_atomic_write() {
1131 let fs = StdFileSystem;
1132 let temp_dir = tempfile::tempdir().unwrap();
1133 let path = temp_dir.path().join("atomic_test.txt");
1134
1135 fs.write_file(&path, b"initial").unwrap();
1136 assert_eq!(fs.read_file(&path).unwrap(), b"initial");
1137
1138 fs.write_file(&path, b"updated").unwrap();
1139 assert_eq!(fs.read_file(&path).unwrap(), b"updated");
1140 }
1141
1142 #[test]
1143 fn test_write_patched_default_impl() {
1144 let fs = StdFileSystem;
1146 let temp_dir = tempfile::tempdir().unwrap();
1147 let src_path = temp_dir.path().join("source.txt");
1148 let dst_path = temp_dir.path().join("dest.txt");
1149
1150 fs.write_file(&src_path, b"AAABBBCCC").unwrap();
1152
1153 let ops = vec![
1155 WriteOp::Copy { offset: 0, len: 3 }, WriteOp::Insert { data: b"XXX" }, WriteOp::Copy { offset: 6, len: 3 }, ];
1159
1160 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1161
1162 let result = fs.read_file(&dst_path).unwrap();
1163 assert_eq!(result, b"AAAXXXCCC");
1164 }
1165
1166 #[test]
1167 fn test_write_patched_same_file() {
1168 let fs = StdFileSystem;
1170 let temp_dir = tempfile::tempdir().unwrap();
1171 let path = temp_dir.path().join("file.txt");
1172
1173 fs.write_file(&path, b"Hello World").unwrap();
1175
1176 let ops = vec![
1178 WriteOp::Copy { offset: 0, len: 6 }, WriteOp::Insert { data: b"Rust" }, ];
1181
1182 fs.write_patched(&path, &path, &ops).unwrap();
1183
1184 let result = fs.read_file(&path).unwrap();
1185 assert_eq!(result, b"Hello Rust");
1186 }
1187
1188 #[test]
1189 fn test_write_patched_insert_only() {
1190 let fs = StdFileSystem;
1192 let temp_dir = tempfile::tempdir().unwrap();
1193 let src_path = temp_dir.path().join("empty.txt");
1194 let dst_path = temp_dir.path().join("new.txt");
1195
1196 fs.write_file(&src_path, b"").unwrap();
1198
1199 let ops = vec![WriteOp::Insert {
1200 data: b"All new content",
1201 }];
1202
1203 fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1204
1205 let result = fs.read_file(&dst_path).unwrap();
1206 assert_eq!(result, b"All new content");
1207 }
1208}