1use std::{
11 cell::RefCell,
12 collections::{BTreeMap, BTreeSet},
13 sync::{Arc, Mutex, RwLock},
14};
15
16use bytes::BytesMut;
17
18#[cfg(feature = "simulator-real-fs")]
20mod real_fs_support {
21 use bytes::BytesMut;
22 use scoped_tls::scoped_thread_local;
23 use std::sync::{Arc, Mutex};
24
25 pub struct RealFs;
26
27 scoped_thread_local! {
28 pub(super) static REAL_FS: RealFs
29 }
30
31 pub fn with_real_fs<T>(f: impl FnOnce() -> T) -> T {
37 REAL_FS.set(&RealFs, f)
38 }
39
40 #[inline]
41 pub fn is_real_fs() -> bool {
42 REAL_FS.is_set()
43 }
44
45 pub fn convert_std_file_to_simulator(
47 std_file: std::fs::File,
48 path: impl AsRef<std::path::Path>,
49 read: bool,
50 write: bool,
51 ) -> std::io::Result<super::sync::File> {
52 let content = if read {
53 use std::io::Read;
54 let mut std_file = std_file;
55 let mut content = Vec::new();
56 std_file.read_to_end(&mut content)?;
57 content
58 } else {
59 Vec::new()
60 };
61
62 Ok(super::sync::File {
63 path: path.as_ref().to_path_buf(),
64 data: Arc::new(Mutex::new(BytesMut::from(content.as_slice()))),
65 position: 0,
66 write,
67 })
68 }
69
70 #[cfg(feature = "async")]
72 pub async fn convert_std_file_to_simulator_async(
73 std_file: std::fs::File,
74 path: impl AsRef<std::path::Path>,
75 read: bool,
76 write: bool,
77 ) -> std::io::Result<super::unsync::File> {
78 let path_buf = path.as_ref().to_path_buf();
79 let content = if read {
80 switchy_async::task::spawn_blocking(move || {
81 use std::io::Read;
82 let mut std_file = std_file;
83 let mut content = Vec::new();
84 std_file.read_to_end(&mut content)?;
85 Ok::<Vec<u8>, std::io::Error>(content)
86 })
87 .await
88 .unwrap()?
89 } else {
90 Vec::new()
91 };
92
93 Ok(super::unsync::File {
94 path: path_buf,
95 data: Arc::new(Mutex::new(BytesMut::from(content.as_slice()))),
96 position: 0,
97 write,
98 })
99 }
100}
101
102#[cfg(not(feature = "simulator-real-fs"))]
104mod real_fs_support {
105 pub fn with_real_fs<T>(f: impl FnOnce() -> T) -> T {
109 f()
110 }
111
112 #[inline]
113 #[allow(dead_code)]
114 pub const fn is_real_fs() -> bool {
115 false
116 }
117}
118
119pub use real_fs_support::with_real_fs;
121
122thread_local! {
123 static FILES: RefCell<RwLock<BTreeMap<String, Arc<Mutex<BytesMut>>>>> =
124 const { RefCell::new(RwLock::new(BTreeMap::new())) };
125 static DIRECTORIES: RefCell<RwLock<BTreeSet<String>>> =
126 const { RefCell::new(RwLock::new(BTreeSet::new())) };
127}
128
129pub fn reset_fs() {
137 FILES.with_borrow_mut(|x| x.write().unwrap().clear());
138 reset_directories();
139}
140
141pub fn reset_directories() {
149 DIRECTORIES.with_borrow_mut(|x| x.write().unwrap().clear());
150}
151
152fn get_parent_directories(path: &str) -> Vec<String> {
154 let mut parents = Vec::new();
155 let path_buf = std::path::Path::new(path);
156
157 let mut current = path_buf.parent();
158 while let Some(parent) = current {
159 if let Some(parent_str) = parent.to_str()
160 && !parent_str.is_empty()
161 && parent_str != "/"
162 {
163 parents.push(parent_str.to_string());
164 }
165 current = parent.parent();
166 }
167
168 if path != "/" {
170 parents.push("/".to_string());
171 }
172
173 parents.reverse();
174 parents
175}
176
177fn normalize_path(path: &str) -> String {
182 let mut components: Vec<&str> = Vec::new();
183 let is_absolute = path.starts_with('/');
184
185 for component in path.split('/') {
186 match component {
187 "" | "." => {
188 }
190 ".." => {
191 if !components.is_empty() && components.last() != Some(&"..") {
193 components.pop();
194 } else if !is_absolute {
195 components.push("..");
197 }
198 }
200 other => {
201 components.push(other);
202 }
203 }
204 }
205
206 if is_absolute {
207 if components.is_empty() {
208 "/".to_string()
209 } else {
210 format!("/{}", components.join("/"))
211 }
212 } else if components.is_empty() {
213 ".".to_string()
214 } else {
215 components.join("/")
216 }
217}
218
219#[must_use]
226pub fn exists<P: AsRef<std::path::Path>>(path: P) -> bool {
227 let Some(path) = path.as_ref().to_str() else {
228 return false;
229 };
230 DIRECTORIES.with_borrow(|dirs| dirs.read().unwrap().contains(path))
231 || FILES.with_borrow(|files| files.read().unwrap().contains_key(path))
232}
233
234fn get_directory_children(dir_path: &str) -> (Vec<String>, Vec<String>) {
236 let normalized_dir = if dir_path == "/" {
237 "/"
238 } else {
239 &format!("{dir_path}/")
240 };
241
242 let files = FILES.with_borrow(|files| {
244 files
245 .read()
246 .unwrap()
247 .keys()
248 .filter_map(|file_path| {
249 file_path.strip_prefix(normalized_dir).and_then(|stripped| {
250 if !stripped.contains('/') && !stripped.is_empty() {
251 Some(stripped.to_string())
252 } else {
253 None
254 }
255 })
256 })
257 .collect::<Vec<_>>()
258 });
259
260 let subdirs = DIRECTORIES.with_borrow(|dirs| {
262 dirs.read()
263 .unwrap()
264 .iter()
265 .filter_map(|subdir_path| {
266 subdir_path
267 .strip_prefix(normalized_dir)
268 .and_then(|stripped| {
269 if !stripped.contains('/') && !stripped.is_empty() {
270 Some(stripped.to_string())
271 } else {
272 None
273 }
274 })
275 })
276 .collect::<Vec<_>>()
277 });
278
279 (files, subdirs)
280}
281
282pub fn init_minimal_fs() -> std::io::Result<()> {
288 #[cfg(feature = "sync")]
289 {
290 sync::create_dir_all("/")?;
291 sync::create_dir_all("/tmp")?;
292 sync::create_dir_all("/home")?;
293 }
294 Ok(())
295}
296
297pub fn init_standard_fs() -> std::io::Result<()> {
303 #[cfg(feature = "sync")]
304 {
305 sync::create_dir_all("/")?;
307 sync::create_dir_all("/bin")?;
308 sync::create_dir_all("/etc")?;
309 sync::create_dir_all("/home")?;
310 sync::create_dir_all("/lib")?;
311 sync::create_dir_all("/opt")?;
312 sync::create_dir_all("/root")?;
313 sync::create_dir_all("/sbin")?;
314 sync::create_dir_all("/tmp")?;
315 sync::create_dir_all("/usr")?;
316 sync::create_dir_all("/var")?;
317
318 sync::create_dir_all("/usr/bin")?;
320 sync::create_dir_all("/usr/lib")?;
321 sync::create_dir_all("/usr/local")?;
322 sync::create_dir_all("/usr/local/bin")?;
323 sync::create_dir_all("/usr/share")?;
324
325 sync::create_dir_all("/var/log")?;
327 sync::create_dir_all("/var/tmp")?;
328 sync::create_dir_all("/var/cache")?;
329 }
330 Ok(())
331}
332
333pub fn init_user_home(username: &str) -> std::io::Result<()> {
339 let home = format!("/home/{username}");
340
341 #[cfg(feature = "sync")]
342 {
343 sync::create_dir_all(&home)?;
344 sync::create_dir_all(format!("{home}/.config"))?;
345 sync::create_dir_all(format!("{home}/.local"))?;
346 sync::create_dir_all(format!("{home}/.local/share"))?;
347 sync::create_dir_all(format!("{home}/.cache"))?;
348 sync::create_dir_all(format!("{home}/Documents"))?;
349 sync::create_dir_all(format!("{home}/Downloads"))?;
350 }
351 Ok(())
352}
353
354#[cfg(all(feature = "simulator-real-fs", feature = "sync", feature = "std"))]
375pub fn seed_from_real_fs<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
376 real_path: P,
377 sim_path: Q,
378) -> std::io::Result<()> {
379 seed_recursive(real_path.as_ref(), sim_path.as_ref())
380}
381
382#[cfg(all(feature = "simulator-real-fs", feature = "sync", feature = "std"))]
383fn seed_recursive(real_path: &std::path::Path, sim_path: &std::path::Path) -> std::io::Result<()> {
384 sync::create_dir_all(sim_path)?;
386
387 let entries = with_real_fs(|| crate::standard::sync::read_dir_sorted(real_path))?;
389
390 for entry in entries {
391 let entry_name = entry.file_name();
392 let real_entry_path = real_path.join(&entry_name);
393 let sim_entry_path = sim_path.join(&entry_name);
394
395 let file_type = entry.file_type()?;
396 if file_type.is_dir() {
397 seed_recursive(&real_entry_path, &sim_entry_path)?;
398 } else if file_type.is_file() {
399 let content = with_real_fs(|| std::fs::read(&real_entry_path))?;
401 sync::write(&sim_entry_path, content)?;
403 }
404 }
406
407 Ok(())
408}
409
410macro_rules! path_to_str {
411 ($path:expr) => {{
412 $path.as_ref().to_str().ok_or_else(|| {
413 std::io::Error::new(std::io::ErrorKind::InvalidData, "path is invalid str")
414 })
415 }};
416}
417
418macro_rules! impl_file_sync {
419 ($file:ident $(,)?) => {
420 impl std::io::Read for $file {
421 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
422 if buf.is_empty() {
423 return Ok(0);
424 }
425
426 let binding = self.data.lock().unwrap();
427
428 let len = binding.len();
429 let pos = usize::try_from(self.position).unwrap();
430
431 let remaining = len - pos;
432 let read_count = std::cmp::min(remaining, buf.len());
433
434 if read_count == 0 {
435 return Ok(0);
436 }
437
438 let data = &binding[pos..(pos + read_count)];
439 buf[..read_count].copy_from_slice(data);
440
441 self.position += read_count as u64;
442
443 drop(binding);
444
445 Ok(read_count)
446 }
447 }
448
449 impl std::io::Write for $file {
450 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
451 {
452 use bytes::BufMut as _;
453
454 if !self.write {
455 return Err(std::io::Error::new(
456 std::io::ErrorKind::PermissionDenied,
457 "File not opened in write mode",
458 ));
459 }
460 let mut binding = self.data.lock().unwrap();
461
462 binding.put(buf);
463
464 drop(binding);
465
466 Ok(buf.len())
467 }
468 }
469
470 fn flush(&mut self) -> std::io::Result<()> {
471 Ok(())
472 }
473 }
474
475 impl std::io::Seek for $file {
476 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
477 self.position = match pos {
478 std::io::SeekFrom::Start(x) => x,
479 std::io::SeekFrom::End(x) => {
480 u64::try_from(i64::try_from(self.data.lock().unwrap().len()).unwrap() - x)
481 .unwrap()
482 }
483 std::io::SeekFrom::Current(x) => {
484 u64::try_from(i64::try_from(self.position).unwrap() + x).unwrap()
485 }
486 };
487
488 Ok(self.position)
489 }
490 }
491 };
492}
493
494#[derive(Debug, Clone)]
498pub struct FileType {
499 is_dir: bool,
500 is_file: bool,
501 is_symlink: bool,
502}
503
504impl FileType {
505 #[must_use]
507 pub const fn is_dir(&self) -> bool {
508 self.is_dir
509 }
510
511 #[must_use]
513 pub const fn is_file(&self) -> bool {
514 self.is_file
515 }
516
517 #[must_use]
519 pub const fn is_symlink(&self) -> bool {
520 self.is_symlink
521 }
522}
523
524#[derive(Debug, Clone)]
528pub struct Metadata {
529 pub(crate) len: u64,
530 pub(crate) is_file: bool,
531 pub(crate) is_dir: bool,
532 pub(crate) is_symlink: bool,
533}
534
535impl Metadata {
536 #[must_use]
538 pub const fn len(&self) -> u64 {
539 self.len
540 }
541
542 #[must_use]
544 pub const fn is_empty(&self) -> bool {
545 self.len == 0
546 }
547
548 #[must_use]
550 pub const fn is_file(&self) -> bool {
551 self.is_file
552 }
553
554 #[must_use]
556 pub const fn is_dir(&self) -> bool {
557 self.is_dir
558 }
559
560 #[must_use]
562 pub const fn is_symlink(&self) -> bool {
563 self.is_symlink
564 }
565}
566
567impl From<std::fs::Metadata> for Metadata {
568 fn from(meta: std::fs::Metadata) -> Self {
569 Self {
570 len: meta.len(),
571 is_file: meta.is_file(),
572 is_dir: meta.is_dir(),
573 is_symlink: meta.is_symlink(),
574 }
575 }
576}
577
578#[cfg(test)]
579mod file_type_tests {
580 use super::FileType;
581 use pretty_assertions::assert_eq;
582
583 #[test_log::test]
584 fn test_file_type_for_directory() {
585 let file_type = FileType {
586 is_dir: true,
587 is_file: false,
588 is_symlink: false,
589 };
590 assert_eq!(file_type.is_dir(), true);
591 assert_eq!(file_type.is_file(), false);
592 assert_eq!(file_type.is_symlink(), false);
593 }
594
595 #[test_log::test]
596 fn test_file_type_for_regular_file() {
597 let file_type = FileType {
598 is_dir: false,
599 is_file: true,
600 is_symlink: false,
601 };
602 assert_eq!(file_type.is_dir(), false);
603 assert_eq!(file_type.is_file(), true);
604 assert_eq!(file_type.is_symlink(), false);
605 }
606
607 #[test_log::test]
608 fn test_file_type_for_symlink() {
609 let file_type = FileType {
610 is_dir: false,
611 is_file: false,
612 is_symlink: true,
613 };
614 assert_eq!(file_type.is_dir(), false);
615 assert_eq!(file_type.is_file(), false);
616 assert_eq!(file_type.is_symlink(), true);
617 }
618
619 #[test_log::test]
620 fn test_file_type_clone() {
621 let original = FileType {
622 is_dir: true,
623 is_file: false,
624 is_symlink: false,
625 };
626 let cloned = original.clone();
627 assert_eq!(cloned.is_dir(), original.is_dir());
628 assert_eq!(cloned.is_file(), original.is_file());
629 assert_eq!(cloned.is_symlink(), original.is_symlink());
630 }
631}
632
633#[cfg(feature = "sync")]
638pub mod sync {
639 use std::{
640 path::{Path, PathBuf},
641 sync::{Arc, Mutex},
642 };
643
644 use bytes::BytesMut;
645
646 use crate::sync::OpenOptions;
647
648 use super::{DIRECTORIES, FILES};
649
650 pub struct File {
654 #[cfg_attr(not(feature = "simulator-real-fs"), allow(dead_code))]
655 pub(crate) path: PathBuf,
656 pub(crate) data: Arc<Mutex<BytesMut>>,
657 pub(crate) position: u64,
658 pub(crate) write: bool,
659 }
660
661 impl File {
662 pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
671 OpenOptions::new().read(true).open(path)
672 }
673
674 pub fn create(path: impl AsRef<Path>) -> std::io::Result<Self> {
684 OpenOptions::new()
685 .create(true)
686 .write(true)
687 .truncate(true)
688 .open(path)
689 }
690
691 #[must_use]
693 pub const fn options() -> OpenOptions {
694 OpenOptions::new()
695 }
696
697 pub fn metadata(&self) -> std::io::Result<Metadata> {
707 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
708 if super::real_fs_support::is_real_fs() {
709 return Ok(std::fs::metadata(&self.path)?.into());
710 }
711
712 Ok(Metadata {
713 len: u64::try_from(self.data.lock().unwrap().len()).unwrap_or(0),
714 is_file: true,
715 is_dir: false,
716 is_symlink: false,
717 })
718 }
719
720 #[cfg(feature = "async")]
722 #[must_use]
723 pub fn into_async(self) -> crate::unsync::File {
724 crate::unsync::File {
725 path: self.path,
726 data: self.data,
727 position: self.position,
728 write: self.write,
729 }
730 }
731 }
732
733 pub use super::Metadata;
734
735 impl_file_sync!(File);
736
737 impl OpenOptions {
738 pub fn open(self, path: impl AsRef<::std::path::Path>) -> ::std::io::Result<File> {
748 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
750 if super::real_fs_support::is_real_fs() {
751 let std_options: std::fs::OpenOptions = self.clone().into();
752 let std_file = std_options.open(&path)?;
753 return super::real_fs_support::convert_std_file_to_simulator(
754 std_file, &path, self.read, self.write,
755 );
756 }
757
758 let location = path_to_str!(path)?;
760 let data = if let Some(data) =
761 FILES.with_borrow(|x| x.read().unwrap().get(location).cloned())
762 {
763 data
764 } else if self.create {
765 if let Some(parent) = std::path::Path::new(location).parent()
767 && let Some(parent_str) = parent.to_str()
768 {
769 let parent_normalized = if parent_str.is_empty() || parent_str == "." {
770 ".".to_string()
771 } else if parent_str == "/" {
772 "/".to_string()
773 } else {
774 parent_str.trim_end_matches('/').to_string()
775 };
776
777 if parent_normalized != "." && !super::exists(&parent_normalized) {
779 return Err(std::io::Error::new(
780 std::io::ErrorKind::NotFound,
781 format!("Parent directory not found: {parent_normalized}"),
782 ));
783 }
784 }
785
786 let data = Arc::new(Mutex::new(BytesMut::new()));
787 FILES.with_borrow_mut(|x| {
788 x.write()
789 .unwrap()
790 .insert(location.to_string(), data.clone())
791 });
792 data
793 } else {
794 return Err(std::io::Error::new(
795 std::io::ErrorKind::NotFound,
796 format!("File not found at path={location}"),
797 ));
798 };
799
800 if self.truncate {
801 data.lock().unwrap().clear();
802 }
803
804 Ok(File {
805 path: path.as_ref().to_path_buf(),
806 data,
807 position: 0,
808 write: self.write,
809 })
810 }
811 }
812
813 pub fn read<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<u8>> {
824 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
825 if super::real_fs_support::is_real_fs() {
826 return std::fs::read(path);
827 }
828
829 let location = path_to_str!(path)?;
831 let Some(existing) = FILES.with_borrow(|x| x.read().unwrap().get(location).cloned()) else {
832 return Err(std::io::Error::new(
833 std::io::ErrorKind::NotFound,
834 format!("File not found at path={location}"),
835 ));
836 };
837
838 Ok(existing.lock().unwrap().to_vec())
839 }
840
841 pub fn read_to_string<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
852 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
853 if super::real_fs_support::is_real_fs() {
854 return crate::standard::sync::read_to_string(path);
855 }
856
857 let location = path_to_str!(path)?;
859 let Some(existing) = FILES.with_borrow(|x| x.read().unwrap().get(location).cloned()) else {
860 return Err(std::io::Error::new(
861 std::io::ErrorKind::NotFound,
862 format!("File not found at path={location}"),
863 ));
864 };
865
866 String::from_utf8(existing.lock().unwrap().to_vec())
867 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
868 }
869
870 pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> std::io::Result<()> {
878 use std::io::Write;
879
880 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
881 if super::real_fs_support::is_real_fs() {
882 return crate::standard::sync::write(path, contents);
883 }
884
885 let mut file = OpenOptions::new()
886 .create(true)
887 .write(true)
888 .truncate(true)
889 .open(path)?;
890
891 file.write_all(contents.as_ref())?;
892 Ok(())
893 }
894
895 pub fn create_dir<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
907 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
908 if super::real_fs_support::is_real_fs() {
909 return crate::standard::sync::create_dir(path);
910 }
911
912 let path_str = path_to_str!(path)?;
913
914 let normalized = if path_str == "/" {
916 "/".to_string()
917 } else {
918 path_str.trim_end_matches('/').to_string()
919 };
920
921 if let Some(parent) = std::path::Path::new(&normalized).parent() {
923 let parent_str = parent.to_string_lossy().to_string();
924 if !parent_str.is_empty() && parent_str != "/" {
925 let parent_exists =
926 DIRECTORIES.with_borrow(|dirs| dirs.read().unwrap().contains(&parent_str));
927 if !parent_exists {
928 return Err(std::io::Error::new(
929 std::io::ErrorKind::NotFound,
930 format!("Parent directory does not exist: {parent_str}"),
931 ));
932 }
933 }
934 }
935
936 DIRECTORIES.with_borrow_mut(|dirs| {
938 dirs.write().unwrap().insert(normalized);
939 });
940
941 Ok(())
942 }
943
944 pub fn create_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
955 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
956 if super::real_fs_support::is_real_fs() {
957 return crate::standard::sync::create_dir_all(path);
958 }
959
960 let path_str = path_to_str!(path)?;
961
962 let normalized = if path_str == "/" {
964 "/".to_string()
965 } else {
966 path_str.trim_end_matches('/').to_string()
967 };
968
969 let mut dirs_to_create = super::get_parent_directories(&normalized);
971 dirs_to_create.push(normalized);
972
973 DIRECTORIES.with_borrow_mut(|dirs| {
975 let mut dirs_write = dirs.write().unwrap();
976 for dir in dirs_to_create {
977 dirs_write.insert(dir);
978 }
979 });
980
981 Ok(())
982 }
983
984 pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
996 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
997 if super::real_fs_support::is_real_fs() {
998 return crate::standard::sync::remove_dir_all(path);
999 }
1000
1001 let path_str = path_to_str!(path)?;
1002
1003 let normalized = if path_str == "/" {
1005 "/".to_string()
1006 } else {
1007 path_str.trim_end_matches('/').to_string()
1008 };
1009
1010 if !super::exists(&normalized) {
1012 return Err(std::io::Error::new(
1013 std::io::ErrorKind::NotFound,
1014 format!("Directory not found: {normalized}"),
1015 ));
1016 }
1017
1018 let prefix = if normalized == "/" {
1020 "/"
1021 } else {
1022 &format!("{normalized}/")
1023 };
1024
1025 FILES.with_borrow_mut(|files| {
1027 let mut files_write = files.write().unwrap();
1028 files_write
1029 .retain(|file_path, _| !file_path.starts_with(prefix) && file_path != &normalized);
1030 });
1031
1032 DIRECTORIES.with_borrow_mut(|dirs| {
1034 let mut dirs_write = dirs.write().unwrap();
1035 dirs_write.retain(|dir_path| !dir_path.starts_with(prefix) && dir_path != &normalized);
1036 });
1037
1038 Ok(())
1039 }
1040
1041 pub fn canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
1052 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
1053 if super::real_fs_support::is_real_fs() {
1054 return crate::standard::sync::canonicalize(path);
1055 }
1056
1057 let path_str = path_to_str!(path)?;
1058
1059 let normalized = super::normalize_path(path_str);
1061
1062 if !super::exists(&normalized) {
1064 return Err(std::io::Error::new(
1065 std::io::ErrorKind::NotFound,
1066 format!("Path not found: {normalized}"),
1067 ));
1068 }
1069
1070 Ok(std::path::PathBuf::from(normalized))
1071 }
1072
1073 pub fn read_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
1082 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
1083 if super::real_fs_support::is_real_fs() {
1084 let std_entries = crate::standard::sync::read_dir_sorted(path)?;
1085 return std_entries
1086 .into_iter()
1087 .map(|x| DirEntry::from_std(&x))
1088 .collect::<std::io::Result<Vec<_>>>();
1089 }
1090
1091 let path_str = path_to_str!(path)?;
1092
1093 let normalized = if path_str == "/" {
1095 "/".to_string()
1096 } else {
1097 path_str.trim_end_matches('/').to_string()
1098 };
1099
1100 if !super::exists(&normalized) {
1102 return Err(std::io::Error::new(
1103 std::io::ErrorKind::NotFound,
1104 format!("Directory not found: {normalized}"),
1105 ));
1106 }
1107
1108 let (files, subdirs) = super::get_directory_children(&normalized);
1110
1111 let mut entries = Vec::new();
1112
1113 for filename in files {
1115 let full_path = if normalized == "/" {
1116 format!("/{filename}")
1117 } else {
1118 format!("{normalized}/{filename}")
1119 };
1120 entries.push(DirEntry::new_file(full_path, filename)?);
1121 }
1122
1123 for dirname in subdirs {
1125 let full_path = if normalized == "/" {
1126 format!("/{dirname}")
1127 } else {
1128 format!("{normalized}/{dirname}")
1129 };
1130 entries.push(DirEntry::new_dir(full_path, dirname)?);
1131 }
1132
1133 entries.sort_by_key(DirEntry::file_name);
1135
1136 Ok(entries)
1137 }
1138
1139 pub fn walk_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
1148 fn walk_recursive(dir_path: &str) -> std::io::Result<Vec<DirEntry>> {
1149 let mut all_entries = Vec::new();
1150
1151 let (files, subdirs) = super::get_directory_children(dir_path);
1153
1154 for filename in files {
1156 let full_path = if dir_path == "/" {
1157 format!("/{filename}")
1158 } else {
1159 format!("{dir_path}/{filename}")
1160 };
1161 all_entries.push(DirEntry::new_file(full_path, filename)?);
1162 }
1163
1164 for dirname in subdirs {
1166 let full_path = if dir_path == "/" {
1167 format!("/{dirname}")
1168 } else {
1169 format!("{dir_path}/{dirname}")
1170 };
1171
1172 all_entries.push(DirEntry::new_dir(full_path.clone(), dirname)?);
1174
1175 let sub_entries = walk_recursive(&full_path)?;
1177 all_entries.extend(sub_entries);
1178 }
1179
1180 Ok(all_entries)
1181 }
1182
1183 #[cfg(all(feature = "simulator-real-fs", feature = "std"))]
1184 if super::real_fs_support::is_real_fs() {
1185 let std_entries = crate::standard::sync::walk_dir_sorted(path)?;
1186 return std_entries
1187 .into_iter()
1188 .map(|x| DirEntry::from_std(&x))
1189 .collect::<std::io::Result<Vec<_>>>();
1190 }
1191
1192 let path_str = path_to_str!(path)?;
1193
1194 let normalized = if path_str == "/" {
1196 "/".to_string()
1197 } else {
1198 path_str.trim_end_matches('/').to_string()
1199 };
1200
1201 if !super::exists(&normalized) {
1203 return Err(std::io::Error::new(
1204 std::io::ErrorKind::NotFound,
1205 format!("Directory not found: {normalized}"),
1206 ));
1207 }
1208
1209 let mut all_entries = walk_recursive(&normalized)?;
1210
1211 all_entries.sort_by_key(DirEntry::path);
1213
1214 Ok(all_entries)
1215 }
1216
1217 pub struct DirEntry {
1222 path: PathBuf,
1223 file_name: std::ffi::OsString,
1224 file_type_info: super::FileType,
1225 }
1226
1227 impl DirEntry {
1228 pub fn from_std(entry: &std::fs::DirEntry) -> std::io::Result<Self> {
1234 let file_type = entry.file_type()?;
1235 Ok(Self {
1236 path: entry.path(),
1237 file_name: entry.file_name(),
1238 file_type_info: super::FileType {
1239 is_dir: file_type.is_dir(),
1240 is_file: file_type.is_file(),
1241 is_symlink: file_type.is_symlink(),
1242 },
1243 })
1244 }
1245
1246 pub fn new_file(full_path: String, file_name: String) -> std::io::Result<Self> {
1252 Ok(Self {
1253 path: PathBuf::from(full_path),
1254 file_name: std::ffi::OsString::from(file_name),
1255 file_type_info: super::FileType {
1256 is_dir: false,
1257 is_file: true,
1258 is_symlink: false,
1259 },
1260 })
1261 }
1262
1263 pub fn new_dir(full_path: String, dir_name: String) -> std::io::Result<Self> {
1269 Ok(Self {
1270 path: PathBuf::from(full_path),
1271 file_name: std::ffi::OsString::from(dir_name),
1272 file_type_info: super::FileType {
1273 is_dir: true,
1274 is_file: false,
1275 is_symlink: false,
1276 },
1277 })
1278 }
1279
1280 #[must_use]
1282 pub fn path(&self) -> PathBuf {
1283 self.path.clone()
1284 }
1285
1286 #[must_use]
1288 pub fn file_name(&self) -> std::ffi::OsString {
1289 self.file_name.clone()
1290 }
1291
1292 pub fn file_type(&self) -> std::io::Result<super::FileType> {
1299 Ok(self.file_type_info.clone())
1300 }
1301 }
1302
1303 #[cfg(test)]
1304 mod test {
1305 use std::{
1306 io::Read as _,
1307 sync::{Arc, Mutex},
1308 };
1309
1310 use bytes::BytesMut;
1311 use pretty_assertions::assert_eq;
1312
1313 use crate::simulator::FILES;
1314
1315 use super::OpenOptions;
1316
1317 #[switchy_async::test]
1318 async fn can_read_empty_file() {
1319 const FILENAME: &str = "sync::test1";
1320
1321 FILES.with_borrow_mut(|x| {
1322 x.write()
1323 .unwrap()
1324 .insert(FILENAME.to_string(), Arc::new(Mutex::new(BytesMut::new())))
1325 });
1326
1327 let mut file = OpenOptions::new().create(true).open(FILENAME).unwrap();
1328
1329 let mut buf = [0u8; 1024];
1330
1331 let read_count = file.read(&mut buf).unwrap();
1332
1333 assert_eq!(read_count, 0);
1334 }
1335
1336 #[switchy_async::test]
1337 async fn can_read_small_bytes_file() {
1338 const FILENAME: &str = "sync::test2";
1339
1340 FILES.with_borrow_mut(|x| {
1341 x.write().unwrap().insert(
1342 FILENAME.to_string(),
1343 Arc::new(Mutex::new(BytesMut::from(b"hey" as &[u8]))),
1344 )
1345 });
1346
1347 let mut file = OpenOptions::new().create(true).open(FILENAME).unwrap();
1348
1349 let mut buf = [0u8; 1024];
1350
1351 let read_count = file.read(&mut buf).unwrap();
1352
1353 assert_eq!(read_count, 3);
1354 }
1355
1356 #[test_log::test]
1357 fn test_write_without_write_permission() {
1358 {
1359 use std::io::Write as _;
1360
1361 super::super::reset_fs();
1362 super::create_dir_all("/tmp").unwrap();
1363
1364 let mut file = OpenOptions::new()
1366 .create(true)
1367 .write(true)
1368 .open("/tmp/test_perms.txt")
1369 .unwrap();
1370 file.write_all(b"initial").unwrap();
1371 drop(file);
1372
1373 let mut file = OpenOptions::new()
1375 .read(true)
1376 .open("/tmp/test_perms.txt")
1377 .unwrap();
1378
1379 let result = file.write_all(b"should fail");
1381 assert!(result.is_err());
1382 assert_eq!(
1383 result.unwrap_err().kind(),
1384 std::io::ErrorKind::PermissionDenied
1385 );
1386 }
1387 }
1388
1389 #[test_log::test]
1390 fn test_truncate_existing_file() {
1391 {
1392 use std::io::Write as _;
1393
1394 super::super::reset_fs();
1395 super::create_dir_all("/tmp").unwrap();
1396
1397 super::write(
1399 "/tmp/truncate_test.txt",
1400 b"initial content that should be removed",
1401 )
1402 .unwrap();
1403
1404 let content = super::read_to_string("/tmp/truncate_test.txt").unwrap();
1406 assert_eq!(content, "initial content that should be removed");
1407
1408 let mut file = OpenOptions::new()
1410 .write(true)
1411 .truncate(true)
1412 .open("/tmp/truncate_test.txt")
1413 .unwrap();
1414
1415 file.write_all(b"new").unwrap();
1417 drop(file);
1418
1419 let content = super::read_to_string("/tmp/truncate_test.txt").unwrap();
1421 assert_eq!(content, "new");
1422 }
1423 }
1424
1425 #[test_log::test]
1426 fn test_partial_reads() {
1427 {
1428 use std::io::Read as _;
1429
1430 super::super::reset_fs();
1431 super::create_dir_all("/tmp").unwrap();
1432
1433 let test_data = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; super::write("/tmp/partial_read.txt", test_data).unwrap();
1436
1437 let mut file = OpenOptions::new()
1438 .read(true)
1439 .open("/tmp/partial_read.txt")
1440 .unwrap();
1441
1442 let mut buf = [0u8; 10];
1444 let mut total_read = Vec::new();
1445
1446 loop {
1447 let count = file.read(&mut buf).unwrap();
1448 if count == 0 {
1449 break;
1450 }
1451 total_read.extend_from_slice(&buf[..count]);
1452 }
1453
1454 assert_eq!(total_read.as_slice(), test_data);
1456 }
1457 }
1458
1459 #[test_log::test]
1460 fn test_seek_and_read() {
1461 {
1462 use std::io::{Read as _, Seek as _, SeekFrom};
1463
1464 super::super::reset_fs();
1465 super::create_dir_all("/tmp").unwrap();
1466
1467 super::write("/tmp/seek_test.txt", b"Hello, World!").unwrap();
1469
1470 let mut file = OpenOptions::new()
1471 .read(true)
1472 .open("/tmp/seek_test.txt")
1473 .unwrap();
1474
1475 let pos = file.seek(SeekFrom::Start(7)).unwrap();
1477 assert_eq!(pos, 7);
1478
1479 let mut buf = [0u8; 5];
1481 let count = file.read(&mut buf).unwrap();
1482 assert_eq!(count, 5);
1483 assert_eq!(&buf, b"World");
1484
1485 let pos = file.seek(SeekFrom::Start(0)).unwrap();
1487 assert_eq!(pos, 0);
1488
1489 let mut buf = [0u8; 5];
1491 let count = file.read(&mut buf).unwrap();
1492 assert_eq!(count, 5);
1493 assert_eq!(&buf, b"Hello");
1494 }
1495 }
1496
1497 #[test_log::test]
1498 fn test_seek_from_end() {
1499 {
1500 use std::io::{Read as _, Seek as _, SeekFrom};
1501
1502 super::super::reset_fs();
1503 super::create_dir_all("/tmp").unwrap();
1504
1505 super::write("/tmp/seek_end.txt", b"0123456789").unwrap(); let mut file = OpenOptions::new()
1509 .read(true)
1510 .open("/tmp/seek_end.txt")
1511 .unwrap();
1512
1513 let pos = file.seek(SeekFrom::End(0)).unwrap();
1519 assert_eq!(pos, 10, "Seek to end of 10-byte file");
1520
1521 let mut buf = [0u8; 10];
1523 let count = file.read(&mut buf).unwrap();
1524 assert_eq!(count, 0);
1525
1526 }
1529 }
1530
1531 #[test_log::test]
1532 fn test_seek_from_current() {
1533 {
1534 use std::io::{Read as _, Seek as _, SeekFrom};
1535
1536 super::super::reset_fs();
1537 super::create_dir_all("/tmp").unwrap();
1538
1539 super::write("/tmp/seek_current.txt", b"0123456789").unwrap();
1540
1541 let mut file = OpenOptions::new()
1542 .read(true)
1543 .open("/tmp/seek_current.txt")
1544 .unwrap();
1545
1546 let mut buf = [0u8; 3];
1548 file.read_exact(&mut buf).unwrap();
1549 assert_eq!(&buf, b"012");
1550
1551 let pos = file.seek(SeekFrom::Current(2)).unwrap();
1553 assert_eq!(pos, 5);
1554
1555 file.read_exact(&mut buf).unwrap();
1557 assert_eq!(&buf, b"567");
1558
1559 let pos = file.seek(SeekFrom::Current(-4)).unwrap();
1561 assert_eq!(pos, 4);
1562
1563 file.read_exact(&mut buf).unwrap();
1565 assert_eq!(&buf, b"456");
1566 }
1567 }
1568
1569 #[test_log::test]
1570 fn test_seek_past_eof() {
1571 {
1572 use std::io::{Seek as _, SeekFrom};
1573
1574 super::super::reset_fs();
1575 super::create_dir_all("/tmp").unwrap();
1576
1577 super::write("/tmp/seek_past_eof.txt", b"12345").unwrap(); let mut file = OpenOptions::new()
1580 .read(true)
1581 .open("/tmp/seek_past_eof.txt")
1582 .unwrap();
1583
1584 let pos = file.seek(SeekFrom::Start(100)).unwrap();
1586 assert_eq!(pos, 100);
1587
1588 }
1592 }
1593
1594 #[test_log::test]
1595 fn test_multiple_handles_same_file() {
1596 {
1597 use std::io::{Read as _, Write as _};
1598
1599 super::super::reset_fs();
1600 super::create_dir_all("/tmp").unwrap();
1601
1602 super::write("/tmp/shared.txt", b"initial").unwrap();
1604
1605 let mut writer = OpenOptions::new()
1607 .write(true)
1608 .truncate(true)
1609 .open("/tmp/shared.txt")
1610 .unwrap();
1611
1612 let mut reader = OpenOptions::new()
1614 .read(true)
1615 .open("/tmp/shared.txt")
1616 .unwrap();
1617
1618 writer.write_all(b"updated content").unwrap();
1620 drop(writer);
1621
1622 let mut buf = Vec::new();
1624 reader.read_to_end(&mut buf).unwrap();
1625 assert_eq!(buf, b"updated content");
1626 }
1627 }
1628
1629 #[test_log::test]
1630 fn test_empty_buffer_read() {
1631 super::super::reset_fs();
1632 super::create_dir_all("/tmp").unwrap();
1633
1634 super::write("/tmp/empty_buf.txt", b"content").unwrap();
1635
1636 let mut file = OpenOptions::new()
1637 .read(true)
1638 .open("/tmp/empty_buf.txt")
1639 .unwrap();
1640
1641 let mut buf = [];
1643 let count = file.read(&mut buf).unwrap();
1644 assert_eq!(count, 0);
1645 }
1646
1647 #[test_log::test]
1648 fn test_file_position_after_operations() {
1649 {
1650 use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
1651
1652 super::super::reset_fs();
1653 super::create_dir_all("/tmp").unwrap();
1654
1655 let mut file = OpenOptions::new()
1656 .create(true)
1657 .read(true)
1658 .write(true)
1659 .open("/tmp/position_test.txt")
1660 .unwrap();
1661
1662 file.write_all(b"0123456789").unwrap();
1664
1665 let pos = file.stream_position().unwrap();
1669 assert_eq!(pos, 0, "BUG: Write should update position but doesn't");
1670
1671 file.seek(SeekFrom::Start(0)).unwrap();
1673
1674 let mut buf = [0u8; 5];
1676 file.read_exact(&mut buf).unwrap();
1677
1678 let pos = file.stream_position().unwrap();
1680 assert_eq!(pos, 5);
1681 }
1682 }
1683
1684 #[test_log::test]
1685 #[cfg(all(feature = "sync", feature = "async"))]
1686 fn test_into_async_conversion() {
1687 {
1688 use std::io::{Seek as _, SeekFrom, Write as _};
1689
1690 super::super::reset_fs();
1691 super::create_dir_all("/tmp").unwrap();
1692
1693 let mut file = OpenOptions::new()
1695 .create(true)
1696 .read(true)
1697 .write(true)
1698 .open("/tmp/convert_test.txt")
1699 .unwrap();
1700
1701 file.write_all(b"Hello, World!").unwrap();
1702 file.seek(SeekFrom::Start(7)).unwrap();
1703
1704 let position = file.position;
1705 let path = file.path.clone();
1706
1707 let async_file = file.into_async();
1709
1710 assert_eq!(async_file.position, position);
1712 assert_eq!(async_file.path, path);
1713 assert_eq!(async_file.write, true);
1714 }
1715 }
1716
1717 #[test_log::test]
1718 fn test_remove_empty_directory() {
1719 super::super::reset_fs();
1720
1721 super::create_dir_all("/tmp/empty_dir").unwrap();
1723
1724 assert!(super::super::exists("/tmp/empty_dir"));
1726
1727 super::remove_dir_all("/tmp/empty_dir").unwrap();
1729
1730 assert!(!super::super::exists("/tmp/empty_dir"));
1732 }
1733
1734 #[test_log::test]
1735 fn test_remove_nonexistent_directory() {
1736 super::super::reset_fs();
1737
1738 let result = super::remove_dir_all("/nonexistent");
1740 assert!(result.is_err());
1741 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
1742 }
1743
1744 #[test_log::test]
1745 fn test_root_directory_operations() {
1746 super::super::reset_fs();
1747 super::create_dir_all("/").unwrap();
1748
1749 let _entries = super::read_dir_sorted("/").unwrap();
1751
1752 assert!(super::super::exists("/"));
1754 }
1755
1756 #[test_log::test]
1757 fn test_create_file_in_current_directory() {
1758 {
1759 use std::io::Write as _;
1760
1761 super::super::reset_fs();
1762
1763 let mut file = OpenOptions::new()
1765 .create(true)
1766 .write(true)
1767 .open("./test.txt")
1768 .unwrap();
1769
1770 file.write_all(b"content").unwrap();
1771 drop(file);
1772
1773 let content = super::read_to_string("./test.txt").unwrap();
1775 assert_eq!(content, "content");
1776 }
1777 }
1778 }
1779}
1780
1781#[cfg(feature = "async")]
1787pub mod unsync {
1788 use std::{
1789 path::{Path, PathBuf},
1790 sync::{Arc, Mutex},
1791 task::Poll,
1792 };
1793
1794 use bytes::BytesMut;
1795
1796 use crate::unsync::OpenOptions;
1797
1798 pub struct File {
1802 pub(crate) path: PathBuf,
1803 pub(crate) data: Arc<Mutex<BytesMut>>,
1804 pub(crate) position: u64,
1805 pub(crate) write: bool,
1806 }
1807
1808 impl File {
1809 #[allow(clippy::future_not_send)]
1818 pub async fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
1819 OpenOptions::new().read(true).open(path).await
1820 }
1821
1822 #[allow(clippy::future_not_send)]
1832 pub async fn create(path: impl AsRef<Path>) -> std::io::Result<Self> {
1833 OpenOptions::new()
1834 .create(true)
1835 .write(true)
1836 .truncate(true)
1837 .open(path)
1838 .await
1839 }
1840
1841 #[must_use]
1843 pub const fn options() -> OpenOptions {
1844 OpenOptions::new()
1845 }
1846
1847 #[allow(clippy::unused_async)]
1858 pub async fn metadata(&self) -> std::io::Result<Metadata> {
1859 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
1860 if super::real_fs_support::is_real_fs() {
1861 let path = self.path.clone();
1862 return switchy_async::task::spawn_blocking(move || {
1863 Ok(std::fs::metadata(&path)?.into())
1864 })
1865 .await
1866 .unwrap();
1867 }
1868
1869 Ok(Metadata {
1870 len: u64::try_from(self.data.lock().unwrap().len()).unwrap_or(0),
1871 is_file: true,
1872 is_dir: false,
1873 is_symlink: false,
1874 })
1875 }
1876
1877 #[cfg(feature = "sync")]
1879 #[must_use]
1880 pub fn into_sync(self) -> crate::sync::File {
1881 crate::sync::File {
1882 path: self.path,
1883 data: self.data,
1884 position: self.position,
1885 write: self.write,
1886 }
1887 }
1888 }
1889
1890 pub use super::Metadata;
1891
1892 impl_file_sync!(File);
1893
1894 impl tokio::io::AsyncRead for File {
1895 fn poll_read(
1896 self: std::pin::Pin<&mut Self>,
1897 _cx: &mut std::task::Context<'_>,
1898 buf: &mut tokio::io::ReadBuf<'_>,
1899 ) -> Poll<std::io::Result<()>> {
1900 {
1901 use std::io::Read as _;
1902
1903 let dst = buf.initialize_unfilled();
1904
1905 match self.get_mut().read(dst) {
1906 Ok(count) => {
1907 buf.advance(count);
1908 }
1909 Err(e) => return Poll::Ready(Err(e)),
1910 }
1911
1912 Poll::Ready(Ok(()))
1913 }
1914 }
1915 }
1916
1917 impl tokio::io::AsyncSeek for File {
1918 fn start_seek(
1919 self: std::pin::Pin<&mut Self>,
1920 position: std::io::SeekFrom,
1921 ) -> std::io::Result<()> {
1922 {
1923 use std::io::Seek as _;
1924
1925 self.get_mut().seek(position)?;
1926 Ok(())
1927 }
1928 }
1929
1930 fn poll_complete(
1931 self: std::pin::Pin<&mut Self>,
1932 _cx: &mut std::task::Context<'_>,
1933 ) -> Poll<std::io::Result<u64>> {
1934 {
1935 use std::io::Seek as _;
1936
1937 Poll::Ready(self.get_mut().stream_position())
1938 }
1939 }
1940 }
1941
1942 impl tokio::io::AsyncWrite for File {
1943 fn poll_write(
1944 self: std::pin::Pin<&mut Self>,
1945 _cx: &mut std::task::Context<'_>,
1946 buf: &[u8],
1947 ) -> Poll<Result<usize, std::io::Error>> {
1948 {
1949 use std::io::Write as _;
1950
1951 Poll::Ready(self.get_mut().write(buf))
1952 }
1953 }
1954
1955 fn poll_flush(
1956 self: std::pin::Pin<&mut Self>,
1957 _cx: &mut std::task::Context<'_>,
1958 ) -> Poll<Result<(), std::io::Error>> {
1959 {
1960 use std::io::Write as _;
1961
1962 Poll::Ready(self.get_mut().flush())
1963 }
1964 }
1965
1966 fn poll_shutdown(
1967 self: std::pin::Pin<&mut Self>,
1968 _cx: &mut std::task::Context<'_>,
1969 ) -> Poll<Result<(), std::io::Error>> {
1970 Poll::Ready(Ok(()))
1971 }
1972 }
1973
1974 impl OpenOptions {
1975 #[allow(clippy::unused_async, clippy::future_not_send)]
1985 pub async fn open(self, path: impl AsRef<::std::path::Path>) -> ::std::io::Result<File> {
1986 #[cfg(all(feature = "simulator-real-fs", feature = "async",))]
1987 if super::real_fs_support::is_real_fs() {
1988 let path_buf = path.as_ref().to_path_buf();
1989 let options = self.clone();
1990 let std_file = switchy_async::task::spawn_blocking(move || {
1991 let std_options: std::fs::OpenOptions = options.into();
1992 std_options.open(&path_buf)
1993 })
1994 .await
1995 .unwrap()?;
1996 return super::real_fs_support::convert_std_file_to_simulator_async(
1997 std_file, &path, self.read, self.write,
1998 )
1999 .await;
2000 }
2001
2002 Ok(self.into_sync().open(path)?.into_async())
2004 }
2005 }
2006
2007 pub async fn read<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<u8>> {
2018 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2019 if super::real_fs_support::is_real_fs() {
2020 let path = path.as_ref().to_path_buf();
2021 return switchy_async::task::spawn_blocking(move || std::fs::read(path))
2022 .await
2023 .unwrap();
2024 }
2025
2026 super::sync::read(path)
2028 }
2029
2030 pub async fn read_to_string<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
2041 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2042 if super::real_fs_support::is_real_fs() {
2043 let path = path.as_ref().to_path_buf();
2044 return switchy_async::task::spawn_blocking(move || std::fs::read_to_string(path))
2045 .await
2046 .unwrap();
2047 }
2048
2049 super::sync::read_to_string(path)
2051 }
2052
2053 #[allow(clippy::unused_async)]
2057 pub async fn exists<P: AsRef<Path>>(path: P) -> bool {
2058 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2059 if super::real_fs_support::is_real_fs() {
2060 let path = path.as_ref().to_path_buf();
2061 return switchy_async::task::spawn_blocking(move || path.exists())
2062 .await
2063 .unwrap_or(false);
2064 }
2065
2066 super::exists(path)
2067 }
2068
2069 #[allow(clippy::unused_async)]
2077 pub async fn is_file<P: AsRef<Path>>(path: P) -> bool {
2078 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2079 if super::real_fs_support::is_real_fs() {
2080 let path = path.as_ref().to_path_buf();
2081 return switchy_async::task::spawn_blocking(move || path.is_file())
2082 .await
2083 .unwrap_or(false);
2084 }
2085
2086 let path_str = path.as_ref().to_string_lossy().to_string();
2087 super::FILES.with_borrow(|files| files.read().unwrap().contains_key(&path_str))
2088 }
2089
2090 #[allow(clippy::unused_async)]
2098 pub async fn is_dir<P: AsRef<Path>>(path: P) -> bool {
2099 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2100 if super::real_fs_support::is_real_fs() {
2101 let path = path.as_ref().to_path_buf();
2102 return switchy_async::task::spawn_blocking(move || path.is_dir())
2103 .await
2104 .unwrap_or(false);
2105 }
2106
2107 let path_str = path.as_ref().to_string_lossy().to_string();
2108 super::DIRECTORIES.with_borrow(|dirs| dirs.read().unwrap().contains(&path_str))
2109 }
2110
2111 pub async fn write<P: AsRef<Path> + Send + Sync, C: AsRef<[u8]> + Send>(
2119 path: P,
2120 contents: C,
2121 ) -> std::io::Result<()> {
2122 use switchy_async::io::AsyncWriteExt;
2123
2124 #[cfg(all(feature = "simulator-real-fs", feature = "tokio"))]
2125 if super::real_fs_support::is_real_fs() {
2126 return crate::tokio::unsync::write(path, contents).await;
2127 }
2128
2129 let mut file = OpenOptions::new()
2130 .create(true)
2131 .write(true)
2132 .truncate(true)
2133 .open(path)
2134 .await?;
2135
2136 file.write_all(contents.as_ref()).await?;
2137 Ok(())
2138 }
2139
2140 pub async fn create_dir<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
2151 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2152 if super::real_fs_support::is_real_fs() {
2153 let path = path.as_ref().to_path_buf();
2154 return switchy_async::task::spawn_blocking(move || std::fs::create_dir(path))
2155 .await
2156 .unwrap();
2157 }
2158
2159 super::sync::create_dir(path)
2160 }
2161
2162 pub async fn create_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
2172 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2173 if super::real_fs_support::is_real_fs() {
2174 let path = path.as_ref().to_path_buf();
2175 return switchy_async::task::spawn_blocking(move || std::fs::create_dir_all(path))
2176 .await
2177 .unwrap();
2178 }
2179
2180 super::sync::create_dir_all(path)
2181 }
2182
2183 pub async fn remove_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
2193 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2194 if super::real_fs_support::is_real_fs() {
2195 let path = path.as_ref().to_path_buf();
2196 return switchy_async::task::spawn_blocking(move || std::fs::remove_dir_all(path))
2197 .await
2198 .unwrap();
2199 }
2200
2201 super::sync::remove_dir_all(path)
2202 }
2203
2204 pub async fn canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
2216 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2217 if super::real_fs_support::is_real_fs() {
2218 let path = path.as_ref().to_path_buf();
2219 return switchy_async::task::spawn_blocking(move || std::fs::canonicalize(path))
2220 .await
2221 .unwrap();
2222 }
2223
2224 super::sync::canonicalize(path)
2225 }
2226
2227 pub struct DirEntry {
2232 path: PathBuf,
2233 file_name: std::ffi::OsString,
2234 file_type_info: super::FileType,
2235 }
2236
2237 impl DirEntry {
2238 pub fn from_std(entry: &std::fs::DirEntry) -> std::io::Result<Self> {
2244 let file_type = entry.file_type()?;
2245 Ok(Self {
2246 path: entry.path(),
2247 file_name: entry.file_name(),
2248 file_type_info: super::FileType {
2249 is_dir: file_type.is_dir(),
2250 is_file: file_type.is_file(),
2251 is_symlink: file_type.is_symlink(),
2252 },
2253 })
2254 }
2255
2256 pub fn new_file(full_path: String, file_name: String) -> std::io::Result<Self> {
2262 Ok(Self {
2263 path: PathBuf::from(full_path),
2264 file_name: std::ffi::OsString::from(file_name),
2265 file_type_info: super::FileType {
2266 is_dir: false,
2267 is_file: true,
2268 is_symlink: false,
2269 },
2270 })
2271 }
2272
2273 pub fn new_dir(full_path: String, dir_name: String) -> std::io::Result<Self> {
2279 Ok(Self {
2280 path: PathBuf::from(full_path),
2281 file_name: std::ffi::OsString::from(dir_name),
2282 file_type_info: super::FileType {
2283 is_dir: true,
2284 is_file: false,
2285 is_symlink: false,
2286 },
2287 })
2288 }
2289
2290 #[must_use]
2292 pub fn path(&self) -> PathBuf {
2293 self.path.clone()
2294 }
2295
2296 #[must_use]
2298 pub fn file_name(&self) -> std::ffi::OsString {
2299 self.file_name.clone()
2300 }
2301
2302 #[allow(clippy::unused_async)]
2308 pub async fn file_type(&self) -> std::io::Result<super::FileType> {
2309 Ok(self.file_type_info.clone())
2310 }
2311
2312 #[allow(clippy::unused_async)]
2323 pub async fn metadata(&self) -> std::io::Result<Metadata> {
2324 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2325 if super::real_fs_support::is_real_fs() {
2326 let path = self.path.clone();
2327 return switchy_async::task::spawn_blocking(move || {
2328 Ok(std::fs::metadata(&path)?.into())
2329 })
2330 .await
2331 .unwrap();
2332 }
2333
2334 if self.file_type_info.is_dir() {
2335 Ok(Metadata {
2336 len: 0,
2337 is_file: false,
2338 is_dir: true,
2339 is_symlink: false,
2340 })
2341 } else if self.file_type_info.is_file() {
2342 let path_str = self.path.to_str().ok_or_else(|| {
2344 std::io::Error::new(std::io::ErrorKind::InvalidData, "path is invalid str")
2345 })?;
2346 let len = super::FILES
2347 .with_borrow(|files| {
2348 files
2349 .read()
2350 .unwrap()
2351 .get(path_str)
2352 .map(|data| u64::try_from(data.lock().unwrap().len()).unwrap_or(0))
2353 })
2354 .unwrap_or(0);
2355 Ok(Metadata {
2356 len,
2357 is_file: true,
2358 is_dir: false,
2359 is_symlink: false,
2360 })
2361 } else {
2362 Ok(Metadata {
2363 len: 0,
2364 is_file: false,
2365 is_dir: false,
2366 is_symlink: self.file_type_info.is_symlink(),
2367 })
2368 }
2369 }
2370 }
2371
2372 pub struct ReadDir {
2377 entries: std::vec::IntoIter<DirEntry>,
2378 }
2379
2380 impl ReadDir {
2381 #[allow(clippy::unused_async)]
2389 pub async fn next_entry(&mut self) -> std::io::Result<Option<DirEntry>> {
2390 Ok(self.entries.next())
2391 }
2392 }
2393
2394 #[allow(clippy::unused_async, clippy::needless_collect)]
2405 pub async fn read_dir<P: AsRef<Path>>(path: P) -> std::io::Result<ReadDir> {
2406 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2407 if super::real_fs_support::is_real_fs() {
2408 let path = path.as_ref().to_path_buf();
2409 let entries = switchy_async::task::spawn_blocking(move || {
2410 let std_entries = crate::standard::sync::read_dir_sorted(path)?;
2411 std_entries
2412 .into_iter()
2413 .map(|x| DirEntry::from_std(&x))
2414 .collect::<std::io::Result<Vec<_>>>()
2415 })
2416 .await
2417 .unwrap()?;
2418 return Ok(ReadDir {
2419 entries: entries.into_iter(),
2420 });
2421 }
2422
2423 let sync_entries = super::sync::read_dir_sorted(&path)?;
2425 let entries: Vec<DirEntry> = sync_entries
2426 .into_iter()
2427 .map(|e| DirEntry {
2428 path: e.path(),
2429 file_name: e.file_name(),
2430 file_type_info: e.file_type().unwrap(),
2431 })
2432 .collect();
2433
2434 Ok(ReadDir {
2435 entries: entries.into_iter(),
2436 })
2437 }
2438
2439 #[allow(clippy::unused_async)]
2450 pub async fn read_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
2451 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2452 if super::real_fs_support::is_real_fs() {
2453 let path = path.as_ref().to_path_buf();
2454 return switchy_async::task::spawn_blocking(move || {
2455 let std_entries = crate::standard::sync::read_dir_sorted(path)?;
2456 std_entries
2457 .into_iter()
2458 .map(|x| DirEntry::from_std(&x))
2459 .collect::<std::io::Result<Vec<_>>>()
2460 })
2461 .await
2462 .unwrap();
2463 }
2464
2465 let sync_entries = super::sync::read_dir_sorted(&path)?;
2467 Ok(sync_entries
2468 .into_iter()
2469 .map(|e| DirEntry {
2470 path: e.path(),
2471 file_name: e.file_name(),
2472 file_type_info: e.file_type().unwrap(),
2473 })
2474 .collect())
2475 }
2476
2477 #[allow(clippy::unused_async)]
2488 pub async fn walk_dir_sorted<P: AsRef<Path>>(path: P) -> std::io::Result<Vec<DirEntry>> {
2489 #[cfg(all(feature = "simulator-real-fs", feature = "async"))]
2490 if super::real_fs_support::is_real_fs() {
2491 let path = path.as_ref().to_path_buf();
2492 return switchy_async::task::spawn_blocking(move || {
2493 let std_entries = crate::standard::sync::walk_dir_sorted(path)?;
2494 std_entries
2495 .into_iter()
2496 .map(|x| DirEntry::from_std(&x))
2497 .collect::<std::io::Result<Vec<_>>>()
2498 })
2499 .await
2500 .unwrap();
2501 }
2502
2503 let sync_entries = super::sync::walk_dir_sorted(&path)?;
2505 Ok(sync_entries
2506 .into_iter()
2507 .map(|e| DirEntry {
2508 path: e.path(),
2509 file_name: e.file_name(),
2510 file_type_info: e.file_type().unwrap(),
2511 })
2512 .collect())
2513 }
2514
2515 #[cfg(test)]
2516 #[allow(clippy::await_holding_lock)]
2517 mod test {
2518 use std::sync::{Arc, Mutex};
2519
2520 use bytes::BytesMut;
2521 use pretty_assertions::assert_eq;
2522 use tokio::io::AsyncReadExt as _;
2523
2524 use crate::simulator::FILES;
2525
2526 use super::OpenOptions;
2527
2528 #[switchy_async::test]
2529 async fn can_read_empty_file() {
2530 const FILENAME: &str = "unsync::test1";
2531
2532 FILES.with_borrow_mut(|x| {
2533 x.write()
2534 .unwrap()
2535 .insert(FILENAME.to_string(), Arc::new(Mutex::new(BytesMut::new())))
2536 });
2537
2538 let mut file = OpenOptions::new()
2539 .create(true)
2540 .open(FILENAME)
2541 .await
2542 .unwrap();
2543
2544 let mut buf = [0u8; 1024];
2545
2546 let read_count = file.read(&mut buf).await.unwrap();
2547
2548 assert_eq!(read_count, 0);
2549 }
2550
2551 #[switchy_async::test]
2552 async fn can_read_small_bytes_file() {
2553 const FILENAME: &str = "unsync::test2";
2554
2555 FILES.with_borrow_mut(|x| {
2556 x.write().unwrap().insert(
2557 FILENAME.to_string(),
2558 Arc::new(Mutex::new(BytesMut::from(b"hey" as &[u8]))),
2559 )
2560 });
2561
2562 let mut file = OpenOptions::new()
2563 .create(true)
2564 .open(FILENAME)
2565 .await
2566 .unwrap();
2567
2568 let mut buf = [0u8; 1024];
2569
2570 let read_count = file.read(&mut buf).await.unwrap();
2571
2572 assert_eq!(read_count, 3);
2573 }
2574 }
2575}
2576
2577#[cfg(test)]
2578mod real_fs_tests {
2579 use std::io::Write as _;
2580
2581 #[switchy_async::test]
2582 async fn test_simulator_mode_no_real_fs() {
2583 assert!(
2585 !super::real_fs_support::is_real_fs(),
2586 "real_fs should NOT be set in normal test"
2587 );
2588
2589 let content = "test content";
2591 let path = "/simulated/path/file.txt";
2592
2593 super::sync::create_dir_all("/simulated/path").unwrap();
2595
2596 let mut file = crate::sync::OpenOptions::new()
2598 .create(true)
2599 .write(true)
2600 .open(path)
2601 .unwrap();
2602 file.write_all(content.as_bytes()).unwrap();
2603
2604 let read_content = super::sync::read_to_string(path).unwrap();
2606 assert_eq!(read_content, content);
2607
2608 assert!(!std::path::Path::new(path).exists());
2610 }
2611}
2612
2613#[cfg(test)]
2614mod exists_tests {
2615 use super::{exists, reset_fs, sync};
2616 use pretty_assertions::assert_eq;
2617
2618 #[test_log::test]
2619 fn test_exists_returns_false_for_nonexistent_path() {
2620 reset_fs();
2621 assert_eq!(exists("/nonexistent/path"), false);
2622 }
2623
2624 #[test_log::test]
2625 fn test_exists_returns_true_for_directory() {
2626 reset_fs();
2627 sync::create_dir_all("/existing/directory").unwrap();
2628 assert_eq!(exists("/existing/directory"), true);
2629 }
2630
2631 #[test_log::test]
2632 fn test_exists_returns_true_for_file() {
2633 reset_fs();
2634 sync::create_dir_all("/existing").unwrap();
2635 sync::write("/existing/file.txt", b"content").unwrap();
2636 assert_eq!(exists("/existing/file.txt"), true);
2637 }
2638
2639 #[test_log::test]
2640 fn test_exists_with_root_path() {
2641 reset_fs();
2642 sync::create_dir_all("/").unwrap();
2643 assert_eq!(exists("/"), true);
2644 }
2645}
2646
2647#[cfg(test)]
2648mod get_parent_directories_tests {
2649 use super::get_parent_directories;
2650 use pretty_assertions::assert_eq;
2651
2652 #[test_log::test]
2653 fn test_parent_directories_for_deeply_nested_path() {
2654 let parents = get_parent_directories("/a/b/c/d/e");
2655 assert_eq!(parents, vec!["/", "/a", "/a/b", "/a/b/c", "/a/b/c/d"]);
2656 }
2657
2658 #[test_log::test]
2659 fn test_parent_directories_for_single_level() {
2660 let parents = get_parent_directories("/single");
2661 assert_eq!(parents, vec!["/"]);
2662 }
2663
2664 #[test_log::test]
2665 fn test_parent_directories_for_root() {
2666 let parents = get_parent_directories("/");
2667 assert!(parents.is_empty());
2669 }
2670
2671 #[test_log::test]
2672 fn test_parent_directories_preserves_order() {
2673 let parents = get_parent_directories("/usr/local/bin");
2675 assert_eq!(parents, vec!["/", "/usr", "/usr/local"]);
2676 }
2677}
2678
2679#[cfg(test)]
2680mod get_directory_children_tests {
2681 use super::{get_directory_children, reset_fs, sync};
2682 use pretty_assertions::assert_eq;
2683
2684 #[test_log::test]
2685 fn test_children_of_empty_directory() {
2686 reset_fs();
2687 sync::create_dir_all("/empty").unwrap();
2688
2689 let (files, subdirs) = get_directory_children("/empty");
2690 assert!(files.is_empty());
2691 assert!(subdirs.is_empty());
2692 }
2693
2694 #[test_log::test]
2695 fn test_children_with_files_only() {
2696 reset_fs();
2697 sync::create_dir_all("/files_only").unwrap();
2698 sync::write("/files_only/a.txt", b"a").unwrap();
2699 sync::write("/files_only/b.txt", b"b").unwrap();
2700
2701 let (mut files, subdirs) = get_directory_children("/files_only");
2702 files.sort();
2703 assert_eq!(files, vec!["a.txt", "b.txt"]);
2704 assert!(subdirs.is_empty());
2705 }
2706
2707 #[test_log::test]
2708 fn test_children_with_subdirs_only() {
2709 reset_fs();
2710 sync::create_dir_all("/dirs_only/subdir1").unwrap();
2711 sync::create_dir_all("/dirs_only/subdir2").unwrap();
2712
2713 let (files, mut subdirs) = get_directory_children("/dirs_only");
2714 subdirs.sort();
2715 assert!(files.is_empty());
2716 assert_eq!(subdirs, vec!["subdir1", "subdir2"]);
2717 }
2718
2719 #[test_log::test]
2720 fn test_children_mixed_content() {
2721 reset_fs();
2722 sync::create_dir_all("/mixed/sub").unwrap();
2723 sync::write("/mixed/file.txt", b"data").unwrap();
2724
2725 let (files, subdirs) = get_directory_children("/mixed");
2726 assert_eq!(files, vec!["file.txt"]);
2727 assert_eq!(subdirs, vec!["sub"]);
2728 }
2729
2730 #[test_log::test]
2731 fn test_children_of_root_directory() {
2732 reset_fs();
2733 sync::create_dir_all("/root_test").unwrap();
2734 sync::create_dir_all("/another").unwrap();
2735
2736 let (files, mut subdirs) = get_directory_children("/");
2737 subdirs.sort();
2738 assert!(files.is_empty());
2739 assert!(subdirs.contains(&"root_test".to_string()));
2740 assert!(subdirs.contains(&"another".to_string()));
2741 }
2742
2743 #[test_log::test]
2744 fn test_children_excludes_nested_items() {
2745 reset_fs();
2747 sync::create_dir_all("/parent/child").unwrap();
2748 sync::write("/parent/child/nested.txt", b"nested").unwrap();
2749 sync::write("/parent/direct.txt", b"direct").unwrap();
2750
2751 let (files, subdirs) = get_directory_children("/parent");
2752 assert_eq!(files, vec!["direct.txt"]);
2753 assert_eq!(subdirs, vec!["child"]);
2754 assert!(!files.contains(&"nested.txt".to_string()));
2756 }
2757}
2758
2759#[cfg(test)]
2760#[cfg(feature = "async")]
2761mod async_file_conversion_tests {
2762 use super::{reset_fs, sync};
2763 use pretty_assertions::assert_eq;
2764 use std::io::{Seek as _, SeekFrom, Write as _};
2765
2766 #[test_log::test]
2767 fn test_async_file_into_sync_preserves_state() {
2768 reset_fs();
2769 sync::create_dir_all("/tmp").unwrap();
2770
2771 let mut sync_file = crate::sync::OpenOptions::new()
2773 .create(true)
2774 .read(true)
2775 .write(true)
2776 .open("/tmp/async_to_sync.txt")
2777 .unwrap();
2778
2779 sync_file.write_all(b"test data here").unwrap();
2780 sync_file.seek(SeekFrom::Start(5)).unwrap();
2781
2782 let async_file = sync_file.into_async();
2783
2784 assert_eq!(async_file.position, 5);
2786 assert_eq!(async_file.path.to_string_lossy(), "/tmp/async_to_sync.txt");
2787 assert!(async_file.write);
2788
2789 let sync_file_again = async_file.into_sync();
2791 assert_eq!(sync_file_again.position, 5);
2792 assert_eq!(
2793 sync_file_again.path.to_string_lossy(),
2794 "/tmp/async_to_sync.txt"
2795 );
2796 }
2797}
2798
2799#[cfg(test)]
2800#[cfg(feature = "async")]
2801mod async_operations_tests {
2802 use super::{reset_fs, sync, unsync};
2803 use pretty_assertions::assert_eq;
2804
2805 #[test_log::test(switchy_async::test)]
2806 async fn test_async_write_and_read() {
2807 reset_fs();
2808 sync::create_dir_all("/async_test").unwrap();
2809
2810 unsync::write("/async_test/file.txt", b"async content")
2812 .await
2813 .unwrap();
2814
2815 let content = unsync::read_to_string("/async_test/file.txt")
2817 .await
2818 .unwrap();
2819 assert_eq!(content, "async content");
2820 }
2821
2822 #[test_log::test(switchy_async::test)]
2823 async fn test_async_create_dir_all() {
2824 reset_fs();
2825
2826 unsync::create_dir_all("/async_dirs/nested/deep")
2828 .await
2829 .unwrap();
2830
2831 assert!(super::exists("/async_dirs/nested/deep"));
2833 }
2834
2835 #[test_log::test(switchy_async::test)]
2836 async fn test_async_remove_dir_all() {
2837 reset_fs();
2838 sync::create_dir_all("/to_remove/sub").unwrap();
2839 sync::write("/to_remove/file.txt", b"data").unwrap();
2840
2841 unsync::remove_dir_all("/to_remove").await.unwrap();
2843
2844 assert!(!super::exists("/to_remove"));
2846 assert!(!super::exists("/to_remove/sub"));
2847 assert!(!super::exists("/to_remove/file.txt"));
2848 }
2849
2850 #[test_log::test(switchy_async::test)]
2851 async fn test_async_remove_nonexistent_dir_fails() {
2852 reset_fs();
2853
2854 let result = unsync::remove_dir_all("/does_not_exist").await;
2855 assert!(result.is_err());
2856 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
2857 }
2858
2859 #[test_log::test(switchy_async::test)]
2860 async fn test_async_open_options() {
2861 reset_fs();
2862 sync::create_dir_all("/async_open").unwrap();
2863
2864 let file = crate::unsync::OpenOptions::new()
2866 .create(true)
2867 .write(true)
2868 .open("/async_open/new_file.txt")
2869 .await
2870 .unwrap();
2871
2872 assert!(file.write);
2873 assert_eq!(file.path.to_string_lossy(), "/async_open/new_file.txt");
2874 }
2875
2876 #[test_log::test(switchy_async::test)]
2877 async fn test_async_read_nonexistent_file() {
2878 reset_fs();
2879
2880 let result = unsync::read_to_string("/nonexistent.txt").await;
2881 assert!(result.is_err());
2882 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
2883 }
2884
2885 #[test_log::test(switchy_async::test)]
2886 async fn test_async_write_without_parent_fails() {
2887 reset_fs();
2888
2889 let result = unsync::write("/no/parent/file.txt", b"data").await;
2890 assert!(result.is_err());
2891 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
2892 }
2893}
2894
2895#[cfg(test)]
2896#[cfg(feature = "async")]
2897mod async_file_io_tests {
2898 use super::{reset_fs, sync};
2899 use pretty_assertions::assert_eq;
2900 use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _, AsyncWriteExt as _};
2901
2902 #[test_log::test(switchy_async::test)]
2903 async fn test_async_read_trait() {
2904 reset_fs();
2905 sync::create_dir_all("/tmp").unwrap();
2906 sync::write("/tmp/async_read.txt", b"Hello, async world!").unwrap();
2907
2908 let mut file = crate::unsync::OpenOptions::new()
2909 .read(true)
2910 .open("/tmp/async_read.txt")
2911 .await
2912 .unwrap();
2913
2914 let mut buf = [0u8; 5];
2915 file.read_exact(&mut buf).await.unwrap();
2916 assert_eq!(&buf, b"Hello");
2917 }
2918
2919 #[test_log::test(switchy_async::test)]
2920 async fn test_async_write_trait() {
2921 reset_fs();
2922 sync::create_dir_all("/tmp").unwrap();
2923
2924 let mut file = crate::unsync::OpenOptions::new()
2925 .create(true)
2926 .write(true)
2927 .open("/tmp/async_write.txt")
2928 .await
2929 .unwrap();
2930
2931 file.write_all(b"async write test").await.unwrap();
2932 file.flush().await.unwrap();
2933 drop(file);
2934
2935 let content = sync::read_to_string("/tmp/async_write.txt").unwrap();
2937 assert_eq!(content, "async write test");
2938 }
2939
2940 #[test_log::test(switchy_async::test)]
2941 async fn test_async_seek_trait() {
2942 reset_fs();
2943 sync::create_dir_all("/tmp").unwrap();
2944 sync::write("/tmp/async_seek.txt", b"0123456789").unwrap();
2945
2946 let mut file = crate::unsync::OpenOptions::new()
2947 .read(true)
2948 .open("/tmp/async_seek.txt")
2949 .await
2950 .unwrap();
2951
2952 let pos = file.seek(std::io::SeekFrom::Start(5)).await.unwrap();
2954 assert_eq!(pos, 5);
2955
2956 let mut buf = [0u8; 5];
2958 file.read_exact(&mut buf).await.unwrap();
2959 assert_eq!(&buf, b"56789");
2960 }
2961}
2962
2963pub mod temp_dir {
2965 use std::{
2966 cell::RefCell,
2967 collections::BTreeMap,
2968 ffi::{OsStr, OsString},
2969 path::{Path, PathBuf},
2970 sync::RwLock,
2971 };
2972
2973 struct TempDirState {
2975 cleanup_enabled: bool,
2976 }
2977
2978 thread_local! {
2979 static TEMP_DIRS: RefCell<RwLock<BTreeMap<PathBuf, TempDirState>>> =
2980 const { RefCell::new(RwLock::new(BTreeMap::new())) };
2981 }
2982
2983 pub fn reset_temp_dirs() {
2989 TEMP_DIRS.with_borrow_mut(|x| x.write().unwrap().clear());
2990 }
2991
2992 pub struct TempDir {
2994 path: PathBuf,
2995 cleanup_enabled: bool,
2996 }
2997
2998 impl TempDir {
2999 pub fn new() -> std::io::Result<Self> {
3009 #[cfg(feature = "simulator-real-fs")]
3010 if super::real_fs_support::is_real_fs() {
3011 let real_temp = tempfile::TempDir::new()?;
3012 let path = real_temp.path().to_path_buf();
3013 std::mem::forget(real_temp); return Ok(Self {
3015 path,
3016 cleanup_enabled: true,
3017 });
3018 }
3019
3020 let dir_name = generate_temp_name(None, None, 6);
3021 let path = PathBuf::from("/tmp").join(dir_name);
3022
3023 super::sync::create_dir_all(&path)?;
3025
3026 TEMP_DIRS.with_borrow_mut(|dirs| {
3028 dirs.write().unwrap().insert(
3029 path.clone(),
3030 TempDirState {
3031 cleanup_enabled: true,
3032 },
3033 );
3034 });
3035
3036 Ok(Self {
3037 path,
3038 cleanup_enabled: true,
3039 })
3040 }
3041
3042 pub fn new_in<P: AsRef<Path>>(dir: P) -> std::io::Result<Self> {
3052 #[cfg(feature = "simulator-real-fs")]
3053 if super::real_fs_support::is_real_fs() {
3054 let real_temp = tempfile::TempDir::new_in(dir)?;
3055 let path = real_temp.path().to_path_buf();
3056 std::mem::forget(real_temp);
3057 return Ok(Self {
3058 path,
3059 cleanup_enabled: true,
3060 });
3061 }
3062
3063 let dir_name = generate_temp_name(None, None, 6);
3064 let path = dir.as_ref().join(dir_name);
3065
3066 super::sync::create_dir_all(&path)?;
3067
3068 TEMP_DIRS.with_borrow_mut(|dirs| {
3069 dirs.write().unwrap().insert(
3070 path.clone(),
3071 TempDirState {
3072 cleanup_enabled: true,
3073 },
3074 );
3075 });
3076
3077 Ok(Self {
3078 path,
3079 cleanup_enabled: true,
3080 })
3081 }
3082
3083 pub fn with_prefix<S: AsRef<OsStr>>(prefix: S) -> std::io::Result<Self> {
3093 #[cfg(feature = "simulator-real-fs")]
3094 if super::real_fs_support::is_real_fs() {
3095 let real_temp = tempfile::TempDir::with_prefix(prefix)?;
3096 let path = real_temp.path().to_path_buf();
3097 std::mem::forget(real_temp);
3098 return Ok(Self {
3099 path,
3100 cleanup_enabled: true,
3101 });
3102 }
3103
3104 let dir_name = generate_temp_name(Some(prefix.as_ref()), None, 6);
3105 let path = PathBuf::from("/tmp").join(dir_name);
3106
3107 super::sync::create_dir_all(&path)?;
3108
3109 TEMP_DIRS.with_borrow_mut(|dirs| {
3110 dirs.write().unwrap().insert(
3111 path.clone(),
3112 TempDirState {
3113 cleanup_enabled: true,
3114 },
3115 );
3116 });
3117
3118 Ok(Self {
3119 path,
3120 cleanup_enabled: true,
3121 })
3122 }
3123
3124 pub fn with_suffix<S: AsRef<OsStr>>(suffix: S) -> std::io::Result<Self> {
3134 #[cfg(feature = "simulator-real-fs")]
3135 if super::real_fs_support::is_real_fs() {
3136 let real_temp = tempfile::TempDir::with_suffix(suffix)?;
3137 let path = real_temp.path().to_path_buf();
3138 std::mem::forget(real_temp);
3139 return Ok(Self {
3140 path,
3141 cleanup_enabled: true,
3142 });
3143 }
3144
3145 let dir_name = generate_temp_name(None, Some(suffix.as_ref()), 6);
3146 let path = PathBuf::from("/tmp").join(dir_name);
3147
3148 super::sync::create_dir_all(&path)?;
3149
3150 TEMP_DIRS.with_borrow_mut(|dirs| {
3151 dirs.write().unwrap().insert(
3152 path.clone(),
3153 TempDirState {
3154 cleanup_enabled: true,
3155 },
3156 );
3157 });
3158
3159 Ok(Self {
3160 path,
3161 cleanup_enabled: true,
3162 })
3163 }
3164
3165 pub fn with_prefix_in<S: AsRef<OsStr>, P: AsRef<Path>>(
3175 prefix: S,
3176 dir: P,
3177 ) -> std::io::Result<Self> {
3178 #[cfg(feature = "simulator-real-fs")]
3179 if super::real_fs_support::is_real_fs() {
3180 let real_temp = tempfile::TempDir::with_prefix_in(prefix, dir)?;
3181 let path = real_temp.path().to_path_buf();
3182 std::mem::forget(real_temp);
3183 return Ok(Self {
3184 path,
3185 cleanup_enabled: true,
3186 });
3187 }
3188
3189 let dir_name = generate_temp_name(Some(prefix.as_ref()), None, 6);
3190 let path = dir.as_ref().join(dir_name);
3191
3192 super::sync::create_dir_all(&path)?;
3193
3194 TEMP_DIRS.with_borrow_mut(|dirs| {
3195 dirs.write().unwrap().insert(
3196 path.clone(),
3197 TempDirState {
3198 cleanup_enabled: true,
3199 },
3200 );
3201 });
3202
3203 Ok(Self {
3204 path,
3205 cleanup_enabled: true,
3206 })
3207 }
3208
3209 pub fn with_suffix_in<S: AsRef<OsStr>, P: AsRef<Path>>(
3219 suffix: S,
3220 dir: P,
3221 ) -> std::io::Result<Self> {
3222 #[cfg(feature = "simulator-real-fs")]
3223 if super::real_fs_support::is_real_fs() {
3224 let real_temp = tempfile::TempDir::with_suffix_in(suffix, dir)?;
3225 let path = real_temp.path().to_path_buf();
3226 std::mem::forget(real_temp);
3227 return Ok(Self {
3228 path,
3229 cleanup_enabled: true,
3230 });
3231 }
3232
3233 let dir_name = generate_temp_name(None, Some(suffix.as_ref()), 6);
3234 let path = dir.as_ref().join(dir_name);
3235
3236 super::sync::create_dir_all(&path)?;
3237
3238 TEMP_DIRS.with_borrow_mut(|dirs| {
3239 dirs.write().unwrap().insert(
3240 path.clone(),
3241 TempDirState {
3242 cleanup_enabled: true,
3243 },
3244 );
3245 });
3246
3247 Ok(Self {
3248 path,
3249 cleanup_enabled: true,
3250 })
3251 }
3252
3253 #[must_use]
3255 pub fn path(&self) -> &Path {
3256 &self.path
3257 }
3258
3259 #[must_use]
3265 pub fn keep(mut self) -> PathBuf {
3266 self.cleanup_enabled = false;
3267 TEMP_DIRS.with_borrow_mut(|dirs| {
3268 if let Some(state) = dirs.write().unwrap().get_mut(&self.path) {
3269 state.cleanup_enabled = false;
3270 }
3271 });
3272 self.path.clone()
3273 }
3274
3275 #[deprecated = "use TempDir::keep()"]
3281 #[must_use]
3282 pub fn into_path(self) -> PathBuf {
3283 self.keep()
3284 }
3285
3286 pub fn disable_cleanup(&mut self, disable_cleanup: bool) {
3292 self.cleanup_enabled = !disable_cleanup;
3293 TEMP_DIRS.with_borrow_mut(|dirs| {
3294 if let Some(state) = dirs.write().unwrap().get_mut(&self.path) {
3295 state.cleanup_enabled = !disable_cleanup;
3296 }
3297 });
3298 }
3299
3300 pub fn close(mut self) -> std::io::Result<()> {
3310 if !self.cleanup_enabled {
3311 return Ok(());
3312 }
3313
3314 #[cfg(feature = "simulator-real-fs")]
3315 if super::real_fs_support::is_real_fs() {
3316 return std::fs::remove_dir_all(&self.path);
3317 }
3318
3319 TEMP_DIRS.with_borrow_mut(|dirs| {
3321 dirs.write().unwrap().remove(&self.path);
3322 });
3323
3324 super::sync::remove_dir_all(&self.path)?;
3326
3327 self.cleanup_enabled = false;
3329 Ok(())
3330 }
3331 }
3332
3333 impl AsRef<Path> for TempDir {
3334 fn as_ref(&self) -> &Path {
3335 self.path()
3336 }
3337 }
3338
3339 impl std::fmt::Debug for TempDir {
3340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3341 f.debug_struct("TempDir")
3342 .field("path", &self.path)
3343 .field("cleanup_enabled", &self.cleanup_enabled)
3344 .finish()
3345 }
3346 }
3347
3348 impl Drop for TempDir {
3349 fn drop(&mut self) {
3350 if self.cleanup_enabled {
3351 #[cfg(feature = "simulator-real-fs")]
3352 if super::real_fs_support::is_real_fs() {
3353 let _ = std::fs::remove_dir_all(&self.path);
3354 return;
3355 }
3356
3357 TEMP_DIRS.with_borrow_mut(|dirs| {
3358 dirs.write().unwrap().remove(&self.path);
3359 });
3360
3361 let _ = super::sync::remove_dir_all(&self.path);
3362 }
3363 }
3364 }
3365
3366 pub struct Builder {
3368 prefix: Option<OsString>,
3369 suffix: Option<OsString>,
3370 rand_bytes: usize,
3371 }
3372
3373 impl Default for Builder {
3374 fn default() -> Self {
3375 Self::new()
3376 }
3377 }
3378
3379 impl Builder {
3380 #[must_use]
3382 pub const fn new() -> Self {
3383 Self {
3384 prefix: None,
3385 suffix: None,
3386 rand_bytes: 6,
3387 }
3388 }
3389
3390 pub fn prefix<S: AsRef<OsStr>>(&mut self, prefix: S) -> &mut Self {
3392 self.prefix = Some(prefix.as_ref().to_os_string());
3393 self
3394 }
3395
3396 pub fn suffix<S: AsRef<OsStr>>(&mut self, suffix: S) -> &mut Self {
3398 self.suffix = Some(suffix.as_ref().to_os_string());
3399 self
3400 }
3401
3402 pub const fn rand_bytes(&mut self, rand: usize) -> &mut Self {
3404 self.rand_bytes = rand;
3405 self
3406 }
3407
3408 pub fn tempdir(&self) -> std::io::Result<TempDir> {
3418 #[cfg(feature = "simulator-real-fs")]
3419 if super::real_fs_support::is_real_fs() {
3420 let mut builder = tempfile::Builder::new();
3421 if let Some(ref prefix) = self.prefix {
3422 builder.prefix(prefix);
3423 }
3424 if let Some(ref suffix) = self.suffix {
3425 builder.suffix(suffix);
3426 }
3427 builder.rand_bytes(self.rand_bytes);
3428 let real_temp = builder.tempdir()?;
3429 let path = real_temp.path().to_path_buf();
3430 std::mem::forget(real_temp);
3431 return Ok(TempDir {
3432 path,
3433 cleanup_enabled: true,
3434 });
3435 }
3436
3437 let dir_name = generate_temp_name(
3438 self.prefix.as_deref(),
3439 self.suffix.as_deref(),
3440 self.rand_bytes,
3441 );
3442 let path = PathBuf::from("/tmp").join(dir_name);
3443
3444 super::sync::create_dir_all(&path)?;
3445
3446 TEMP_DIRS.with_borrow_mut(|dirs| {
3447 dirs.write().unwrap().insert(
3448 path.clone(),
3449 TempDirState {
3450 cleanup_enabled: true,
3451 },
3452 );
3453 });
3454
3455 Ok(TempDir {
3456 path,
3457 cleanup_enabled: true,
3458 })
3459 }
3460
3461 pub fn tempdir_in<P: AsRef<Path>>(&self, dir: P) -> std::io::Result<TempDir> {
3471 #[cfg(feature = "simulator-real-fs")]
3472 if super::real_fs_support::is_real_fs() {
3473 let mut builder = tempfile::Builder::new();
3474 if let Some(ref prefix) = self.prefix {
3475 builder.prefix(prefix);
3476 }
3477 if let Some(ref suffix) = self.suffix {
3478 builder.suffix(suffix);
3479 }
3480 builder.rand_bytes(self.rand_bytes);
3481 let real_temp = builder.tempdir_in(dir)?;
3482 let path = real_temp.path().to_path_buf();
3483 std::mem::forget(real_temp);
3484 return Ok(TempDir {
3485 path,
3486 cleanup_enabled: true,
3487 });
3488 }
3489
3490 let dir_name = generate_temp_name(
3491 self.prefix.as_deref(),
3492 self.suffix.as_deref(),
3493 self.rand_bytes,
3494 );
3495 let path = dir.as_ref().join(dir_name);
3496
3497 super::sync::create_dir_all(&path)?;
3498
3499 TEMP_DIRS.with_borrow_mut(|dirs| {
3500 dirs.write().unwrap().insert(
3501 path.clone(),
3502 TempDirState {
3503 cleanup_enabled: true,
3504 },
3505 );
3506 });
3507
3508 Ok(TempDir {
3509 path,
3510 cleanup_enabled: true,
3511 })
3512 }
3513 }
3514
3515 pub fn tempdir() -> std::io::Result<TempDir> {
3521 TempDir::new()
3522 }
3523
3524 pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> std::io::Result<TempDir> {
3530 TempDir::new_in(dir)
3531 }
3532
3533 fn generate_temp_name(
3535 prefix: Option<&OsStr>,
3536 suffix: Option<&OsStr>,
3537 rand_bytes: usize,
3538 ) -> OsString {
3539 let mut name = OsString::new();
3540
3541 if let Some(p) = prefix {
3542 name.push(p);
3543 }
3544
3545 for i in 0..rand_bytes {
3547 #[allow(clippy::cast_possible_truncation)]
3548 let c = char::from(b'a' + (i % 26) as u8);
3549 name.push(c.to_string());
3550 }
3551
3552 if let Some(s) = suffix {
3553 name.push(s);
3554 }
3555
3556 name
3557 }
3558
3559 #[cfg(test)]
3560 mod tests {
3561 use super::*;
3562 use crate::simulator::{exists, reset_fs};
3563 use pretty_assertions::assert_eq;
3564
3565 #[test_log::test]
3566 fn test_generate_temp_name_without_prefix_or_suffix() {
3567 let name = generate_temp_name(None, None, 6);
3568 assert_eq!(name.to_str().unwrap(), "abcdef");
3569 }
3570
3571 #[test_log::test]
3572 fn test_generate_temp_name_with_prefix() {
3573 let name = generate_temp_name(Some(std::ffi::OsStr::new("test-")), None, 4);
3574 assert_eq!(name.to_str().unwrap(), "test-abcd");
3575 }
3576
3577 #[test_log::test]
3578 fn test_generate_temp_name_with_suffix() {
3579 let name = generate_temp_name(None, Some(std::ffi::OsStr::new("-end")), 4);
3580 assert_eq!(name.to_str().unwrap(), "abcd-end");
3581 }
3582
3583 #[test_log::test]
3584 fn test_generate_temp_name_with_both() {
3585 let name = generate_temp_name(
3586 Some(std::ffi::OsStr::new("pre-")),
3587 Some(std::ffi::OsStr::new("-suf")),
3588 3,
3589 );
3590 assert_eq!(name.to_str().unwrap(), "pre-abc-suf");
3591 }
3592
3593 #[test_log::test]
3594 fn test_generate_temp_name_wraps_alphabet() {
3595 let name = generate_temp_name(None, None, 30);
3597 let s = name.to_str().unwrap();
3598 assert!(s.starts_with("abcdefghijklmnopqrstuvwxyzabcd"));
3600 }
3601
3602 #[test_log::test]
3603 fn test_builder_default_values() {
3604 reset_fs();
3605
3606 let builder = Builder::new();
3607 let temp = builder.tempdir().unwrap();
3608
3609 assert!(temp.path().starts_with("/tmp"));
3611 assert!(exists(temp.path()));
3612 }
3613
3614 #[test_log::test]
3615 fn test_builder_with_prefix() {
3616 reset_fs();
3617
3618 let mut builder = Builder::new();
3619 builder.prefix("myprefix-");
3620 let temp = builder.tempdir().unwrap();
3621
3622 let file_name = temp.path().file_name().unwrap().to_str().unwrap();
3623 assert!(file_name.starts_with("myprefix-"));
3624 }
3625
3626 #[test_log::test]
3627 fn test_builder_with_suffix() {
3628 reset_fs();
3629
3630 let mut builder = Builder::new();
3631 builder.suffix("-mysuffix");
3632 let temp = builder.tempdir().unwrap();
3633
3634 let file_name = temp.path().file_name().unwrap().to_str().unwrap();
3635 assert!(file_name.ends_with("-mysuffix"));
3636 }
3637
3638 #[test_log::test]
3639 fn test_builder_with_custom_rand_bytes() {
3640 reset_fs();
3641
3642 let mut builder = Builder::new();
3643 builder.rand_bytes(10);
3644 let temp = builder.tempdir().unwrap();
3645
3646 let file_name = temp.path().file_name().unwrap().to_str().unwrap();
3647 assert!(file_name.contains("abcdefghij"));
3649 }
3650
3651 #[test_log::test]
3652 fn test_builder_tempdir_in() {
3653 reset_fs();
3654 crate::simulator::sync::create_dir_all("/custom").unwrap();
3655
3656 let mut builder = Builder::new();
3657 builder.prefix("test-");
3658 let temp = builder.tempdir_in("/custom").unwrap();
3659
3660 assert!(temp.path().starts_with("/custom"));
3661 assert!(
3662 temp.path()
3663 .file_name()
3664 .unwrap()
3665 .to_str()
3666 .unwrap()
3667 .starts_with("test-")
3668 );
3669 }
3670
3671 #[test_log::test]
3672 fn test_builder_chaining() {
3673 reset_fs();
3674
3675 let temp = Builder::new()
3676 .prefix("start-")
3677 .suffix("-end")
3678 .rand_bytes(3)
3679 .tempdir()
3680 .unwrap();
3681
3682 let file_name = temp.path().file_name().unwrap().to_str().unwrap();
3683 assert_eq!(file_name, "start-abc-end");
3684 }
3685
3686 #[test_log::test]
3687 fn test_disable_cleanup_prevents_deletion() {
3688 reset_fs();
3689
3690 let path = {
3691 let mut temp = TempDir::new().unwrap();
3692 temp.disable_cleanup(true);
3693 temp.path().to_path_buf()
3694 };
3695
3696 assert!(
3698 exists(&path),
3699 "Directory should exist after drop with cleanup disabled"
3700 );
3701 }
3702
3703 #[test_log::test]
3704 fn test_disable_cleanup_can_be_reenabled() {
3705 reset_fs();
3706
3707 let path = {
3708 let mut temp = TempDir::new().unwrap();
3709 temp.disable_cleanup(true);
3710 temp.disable_cleanup(false); temp.path().to_path_buf()
3712 };
3713
3714 assert!(
3716 !exists(&path),
3717 "Directory should be removed when cleanup is re-enabled"
3718 );
3719 }
3720
3721 #[test_log::test]
3722 fn test_tempdir_with_prefix_in() {
3723 reset_fs();
3724 crate::simulator::sync::create_dir_all("/base").unwrap();
3725
3726 let temp = TempDir::with_prefix_in("pfx-", "/base").unwrap();
3727 assert!(temp.path().starts_with("/base"));
3728 assert!(
3729 temp.path()
3730 .file_name()
3731 .unwrap()
3732 .to_str()
3733 .unwrap()
3734 .starts_with("pfx-")
3735 );
3736 }
3737
3738 #[test_log::test]
3739 fn test_tempdir_with_suffix_in() {
3740 reset_fs();
3741 crate::simulator::sync::create_dir_all("/base").unwrap();
3742
3743 let temp = TempDir::with_suffix_in("-sfx", "/base").unwrap();
3744 assert!(temp.path().starts_with("/base"));
3745 assert!(
3746 temp.path()
3747 .file_name()
3748 .unwrap()
3749 .to_str()
3750 .unwrap()
3751 .ends_with("-sfx")
3752 );
3753 }
3754
3755 #[test_log::test]
3756 fn test_tempdir_drop_removes_directory() {
3757 reset_fs();
3758
3759 let path = {
3760 let temp = TempDir::new().unwrap();
3761 let p = temp.path().to_path_buf();
3762 assert!(exists(&p), "Directory should exist before drop");
3763 p
3764 };
3765
3766 assert!(!exists(&path), "Directory should be removed after drop");
3767 }
3768
3769 #[test_log::test]
3770 fn test_tempdir_close_removes_directory() {
3771 reset_fs();
3772
3773 let temp = TempDir::new().unwrap();
3774 let path = temp.path().to_path_buf();
3775 assert!(exists(&path));
3776
3777 temp.close().unwrap();
3778 assert!(!exists(&path), "Directory should be removed after close()");
3779 }
3780
3781 #[test_log::test]
3782 fn test_tempdir_close_with_cleanup_disabled() {
3783 reset_fs();
3784
3785 let mut temp = TempDir::new().unwrap();
3786 let path = temp.path().to_path_buf();
3787 temp.disable_cleanup(true);
3788
3789 temp.close().unwrap();
3791 assert!(
3792 exists(&path),
3793 "Directory should exist after close() with cleanup disabled"
3794 );
3795 }
3796
3797 #[test_log::test]
3798 fn test_tempdir_as_ref() {
3799 reset_fs();
3800
3801 let temp = TempDir::new().unwrap();
3802 let path_ref: &Path = temp.as_ref();
3803 assert_eq!(path_ref, temp.path());
3804 }
3805
3806 #[test_log::test]
3807 fn test_tempdir_debug_format() {
3808 reset_fs();
3809
3810 let temp = TempDir::new().unwrap();
3811 let debug_str = format!("{temp:?}");
3812
3813 assert!(debug_str.contains("TempDir"));
3815 assert!(debug_str.contains("path"));
3816 assert!(debug_str.contains("cleanup_enabled"));
3817 }
3818
3819 #[test_log::test]
3820 fn test_builder_default_impl() {
3821 let builder1 = Builder::new();
3823 let builder2 = Builder::default();
3824
3825 assert!(builder1.prefix.is_none());
3827 assert!(builder2.prefix.is_none());
3828 assert!(builder1.suffix.is_none());
3829 assert!(builder2.suffix.is_none());
3830 assert_eq!(builder1.rand_bytes, builder2.rand_bytes);
3831 }
3832
3833 #[test_log::test]
3834 fn test_reset_temp_dirs_clears_state() {
3835 reset_fs();
3836
3837 let _temp1 = TempDir::new().unwrap();
3839 let _temp2 = TempDir::new().unwrap();
3840
3841 reset_temp_dirs();
3843
3844 }
3847 }
3848}
3849
3850#[cfg(test)]
3851mod init_fs_tests {
3852 use super::{exists, init_minimal_fs, init_standard_fs, init_user_home, reset_fs, sync};
3853
3854 #[test_log::test]
3855 fn test_init_minimal_fs_creates_essential_directories() {
3856 reset_fs();
3857 init_minimal_fs().unwrap();
3858
3859 assert!(exists("/"), "root directory should exist");
3861 assert!(exists("/tmp"), "/tmp directory should exist");
3862 assert!(exists("/home"), "/home directory should exist");
3863 }
3864
3865 #[test_log::test]
3866 fn test_init_standard_fs_creates_fhs_structure() {
3867 reset_fs();
3868 init_standard_fs().unwrap();
3869
3870 assert!(exists("/bin"), "/bin should exist");
3872 assert!(exists("/etc"), "/etc should exist");
3873 assert!(exists("/home"), "/home should exist");
3874 assert!(exists("/lib"), "/lib should exist");
3875 assert!(exists("/opt"), "/opt should exist");
3876 assert!(exists("/root"), "/root should exist");
3877 assert!(exists("/sbin"), "/sbin should exist");
3878 assert!(exists("/tmp"), "/tmp should exist");
3879 assert!(exists("/usr"), "/usr should exist");
3880 assert!(exists("/var"), "/var should exist");
3881
3882 assert!(exists("/usr/bin"), "/usr/bin should exist");
3884 assert!(exists("/usr/lib"), "/usr/lib should exist");
3885 assert!(exists("/usr/local"), "/usr/local should exist");
3886 assert!(exists("/usr/local/bin"), "/usr/local/bin should exist");
3887 assert!(exists("/usr/share"), "/usr/share should exist");
3888
3889 assert!(exists("/var/log"), "/var/log should exist");
3891 assert!(exists("/var/tmp"), "/var/tmp should exist");
3892 assert!(exists("/var/cache"), "/var/cache should exist");
3893 }
3894
3895 #[test_log::test]
3896 fn test_init_user_home_creates_standard_user_directories() {
3897 reset_fs();
3898 init_minimal_fs().unwrap();
3899 init_user_home("testuser").unwrap();
3900
3901 assert!(exists("/home/testuser"), "user home should exist");
3903 assert!(
3904 exists("/home/testuser/.config"),
3905 ".config directory should exist"
3906 );
3907 assert!(
3908 exists("/home/testuser/.local"),
3909 ".local directory should exist"
3910 );
3911 assert!(
3912 exists("/home/testuser/.local/share"),
3913 ".local/share directory should exist"
3914 );
3915 assert!(
3916 exists("/home/testuser/.cache"),
3917 ".cache directory should exist"
3918 );
3919 assert!(
3920 exists("/home/testuser/Documents"),
3921 "Documents directory should exist"
3922 );
3923 assert!(
3924 exists("/home/testuser/Downloads"),
3925 "Downloads directory should exist"
3926 );
3927 }
3928
3929 #[test_log::test]
3930 fn test_init_user_home_with_different_usernames() {
3931 reset_fs();
3932 init_minimal_fs().unwrap();
3933
3934 init_user_home("alice").unwrap();
3935 init_user_home("bob").unwrap();
3936
3937 assert!(exists("/home/alice"), "alice home should exist");
3938 assert!(exists("/home/bob"), "bob home should exist");
3939 assert!(
3940 exists("/home/alice/Documents"),
3941 "alice Documents should exist"
3942 );
3943 assert!(exists("/home/bob/Documents"), "bob Documents should exist");
3944 }
3945
3946 #[test_log::test]
3947 fn test_init_standard_fs_allows_listing_usr_subdirs() {
3948 reset_fs();
3949 init_standard_fs().unwrap();
3950
3951 let entries = sync::read_dir_sorted("/usr").unwrap();
3952 let dir_names: Vec<_> = entries.iter().map(sync::DirEntry::file_name).collect();
3953
3954 assert!(
3956 dir_names.iter().any(|n| n == "bin"),
3957 "should contain bin directory"
3958 );
3959 assert!(
3960 dir_names.iter().any(|n| n == "lib"),
3961 "should contain lib directory"
3962 );
3963 assert!(
3964 dir_names.iter().any(|n| n == "local"),
3965 "should contain local directory"
3966 );
3967 assert!(
3968 dir_names.iter().any(|n| n == "share"),
3969 "should contain share directory"
3970 );
3971 }
3972}
3973
3974#[cfg(test)]
3975mod metadata_tests {
3976 use super::{Metadata, reset_fs, sync};
3977 use pretty_assertions::assert_eq;
3978
3979 #[test_log::test]
3980 fn test_metadata_for_empty_file() {
3981 reset_fs();
3982 sync::create_dir_all("/tmp").unwrap();
3983 sync::write("/tmp/empty.txt", b"").unwrap();
3984
3985 let file = sync::File::open("/tmp/empty.txt").unwrap();
3986 let metadata = file.metadata().unwrap();
3987
3988 assert_eq!(metadata.len(), 0, "empty file should have length 0");
3989 assert!(
3990 metadata.is_empty(),
3991 "empty file should return true for is_empty"
3992 );
3993 assert!(metadata.is_file(), "should be a file");
3994 assert!(!metadata.is_dir(), "should not be a directory");
3995 assert!(!metadata.is_symlink(), "should not be a symlink");
3996 }
3997
3998 #[test_log::test]
3999 fn test_metadata_for_file_with_content() {
4000 reset_fs();
4001 sync::create_dir_all("/tmp").unwrap();
4002 sync::write("/tmp/content.txt", b"Hello, World!").unwrap();
4003
4004 let file = sync::File::open("/tmp/content.txt").unwrap();
4005 let metadata = file.metadata().unwrap();
4006
4007 assert_eq!(metadata.len(), 13, "file should have correct length");
4008 assert!(
4009 !metadata.is_empty(),
4010 "non-empty file should return false for is_empty"
4011 );
4012 assert!(metadata.is_file(), "should be a file");
4013 }
4014
4015 #[test_log::test]
4016 fn test_metadata_from_std_fs_metadata() {
4017 let metadata = Metadata {
4022 len: 1024,
4023 is_file: true,
4024 is_dir: false,
4025 is_symlink: false,
4026 };
4027
4028 assert_eq!(metadata.len(), 1024);
4029 assert!(metadata.is_file());
4030 assert!(!metadata.is_dir());
4031 assert!(!metadata.is_symlink());
4032 }
4033
4034 #[test_log::test]
4035 fn test_metadata_for_directory_entry() {
4036 let dir_metadata = Metadata {
4037 len: 0,
4038 is_file: false,
4039 is_dir: true,
4040 is_symlink: false,
4041 };
4042
4043 assert!(dir_metadata.is_dir());
4044 assert!(!dir_metadata.is_file());
4045 assert!(dir_metadata.is_empty());
4046 }
4047
4048 #[test_log::test]
4049 fn test_metadata_for_symlink_entry() {
4050 let symlink_metadata = Metadata {
4051 len: 0,
4052 is_file: false,
4053 is_dir: false,
4054 is_symlink: true,
4055 };
4056
4057 assert!(symlink_metadata.is_symlink());
4058 assert!(!symlink_metadata.is_file());
4059 assert!(!symlink_metadata.is_dir());
4060 }
4061}
4062
4063#[cfg(test)]
4064mod walk_dir_sorted_tests {
4065 use super::{reset_fs, sync};
4066 use pretty_assertions::assert_eq;
4067
4068 #[test_log::test]
4069 fn test_walk_dir_sorted_empty_directory() {
4070 reset_fs();
4071 sync::create_dir_all("/walk_empty").unwrap();
4072
4073 let entries = sync::walk_dir_sorted("/walk_empty").unwrap();
4074 assert!(entries.is_empty(), "empty directory should have no entries");
4075 }
4076
4077 #[test_log::test]
4078 fn test_walk_dir_sorted_flat_directory() {
4079 reset_fs();
4080 sync::create_dir_all("/walk_flat").unwrap();
4081 sync::write("/walk_flat/a.txt", b"a").unwrap();
4082 sync::write("/walk_flat/b.txt", b"b").unwrap();
4083 sync::write("/walk_flat/c.txt", b"c").unwrap();
4084
4085 let entries = sync::walk_dir_sorted("/walk_flat").unwrap();
4086 assert_eq!(entries.len(), 3, "should have 3 files");
4087
4088 let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
4090 assert_eq!(
4091 paths,
4092 vec![
4093 std::path::PathBuf::from("/walk_flat/a.txt"),
4094 std::path::PathBuf::from("/walk_flat/b.txt"),
4095 std::path::PathBuf::from("/walk_flat/c.txt"),
4096 ]
4097 );
4098 }
4099
4100 #[test_log::test]
4101 fn test_walk_dir_sorted_nested_structure() {
4102 reset_fs();
4103 sync::create_dir_all("/walk_nested/dir1").unwrap();
4104 sync::create_dir_all("/walk_nested/dir2").unwrap();
4105 sync::write("/walk_nested/root.txt", b"root").unwrap();
4106 sync::write("/walk_nested/dir1/nested1.txt", b"nested1").unwrap();
4107 sync::write("/walk_nested/dir2/nested2.txt", b"nested2").unwrap();
4108
4109 let entries = sync::walk_dir_sorted("/walk_nested").unwrap();
4110
4111 let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
4113
4114 assert!(
4116 paths.contains(&std::path::PathBuf::from("/walk_nested/dir1")),
4117 "should contain dir1"
4118 );
4119 assert!(
4120 paths.contains(&std::path::PathBuf::from("/walk_nested/dir2")),
4121 "should contain dir2"
4122 );
4123 assert!(
4124 paths.contains(&std::path::PathBuf::from("/walk_nested/root.txt")),
4125 "should contain root.txt"
4126 );
4127 assert!(
4128 paths.contains(&std::path::PathBuf::from("/walk_nested/dir1/nested1.txt")),
4129 "should contain nested1.txt"
4130 );
4131 assert!(
4132 paths.contains(&std::path::PathBuf::from("/walk_nested/dir2/nested2.txt")),
4133 "should contain nested2.txt"
4134 );
4135 }
4136
4137 #[test_log::test]
4138 fn test_walk_dir_sorted_deeply_nested() {
4139 reset_fs();
4140 sync::create_dir_all("/deep/a/b/c").unwrap();
4141 sync::write("/deep/a/b/c/file.txt", b"deep").unwrap();
4142
4143 let entries = sync::walk_dir_sorted("/deep").unwrap();
4144
4145 let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
4147
4148 assert!(
4149 paths.contains(&std::path::PathBuf::from("/deep/a")),
4150 "should contain /deep/a"
4151 );
4152 assert!(
4153 paths.contains(&std::path::PathBuf::from("/deep/a/b")),
4154 "should contain /deep/a/b"
4155 );
4156 assert!(
4157 paths.contains(&std::path::PathBuf::from("/deep/a/b/c")),
4158 "should contain /deep/a/b/c"
4159 );
4160 assert!(
4161 paths.contains(&std::path::PathBuf::from("/deep/a/b/c/file.txt")),
4162 "should contain the file"
4163 );
4164 }
4165
4166 #[test_log::test]
4167 fn test_walk_dir_sorted_nonexistent_dir_fails() {
4168 reset_fs();
4169
4170 let result = sync::walk_dir_sorted("/nonexistent_walk");
4171 assert!(result.is_err());
4172 let err = result.err().unwrap();
4173 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
4174 }
4175
4176 #[test_log::test]
4177 fn test_walk_dir_sorted_returns_sorted_paths() {
4178 reset_fs();
4179 sync::create_dir_all("/sorted/z_dir").unwrap();
4180 sync::create_dir_all("/sorted/a_dir").unwrap();
4181 sync::write("/sorted/m_file.txt", b"m").unwrap();
4182 sync::write("/sorted/a_dir/nested.txt", b"nested").unwrap();
4183
4184 let entries = sync::walk_dir_sorted("/sorted").unwrap();
4185 let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
4186
4187 let mut sorted_paths = paths.clone();
4189 sorted_paths.sort();
4190 assert_eq!(
4191 paths, sorted_paths,
4192 "walk_dir_sorted should return paths in sorted order"
4193 );
4194 }
4195}
4196
4197#[cfg(test)]
4198mod read_dir_sorted_sync_tests {
4199 use super::{reset_fs, sync};
4200 use pretty_assertions::assert_eq;
4201
4202 #[test_log::test]
4203 fn test_read_dir_sorted_empty_directory() {
4204 reset_fs();
4205 sync::create_dir_all("/read_empty").unwrap();
4206
4207 let entries = sync::read_dir_sorted("/read_empty").unwrap();
4208 assert!(entries.is_empty(), "empty directory should have no entries");
4209 }
4210
4211 #[test_log::test]
4212 fn test_read_dir_sorted_files_and_dirs_mixed() {
4213 reset_fs();
4214 sync::create_dir_all("/mixed_content/subdir").unwrap();
4215 sync::write("/mixed_content/file1.txt", b"f1").unwrap();
4216 sync::write("/mixed_content/file2.txt", b"f2").unwrap();
4217
4218 let entries = sync::read_dir_sorted("/mixed_content").unwrap();
4219 assert_eq!(entries.len(), 3, "should have 2 files and 1 directory");
4220
4221 let file_count = entries
4223 .iter()
4224 .filter(|e| e.file_type().unwrap().is_file())
4225 .count();
4226 let dir_count = entries
4227 .iter()
4228 .filter(|e| e.file_type().unwrap().is_dir())
4229 .count();
4230
4231 assert_eq!(file_count, 2, "should have 2 files");
4232 assert_eq!(dir_count, 1, "should have 1 directory");
4233 }
4234
4235 #[test_log::test]
4236 fn test_read_dir_sorted_returns_sorted_by_filename() {
4237 reset_fs();
4238 sync::create_dir_all("/sort_test").unwrap();
4239 sync::write("/sort_test/zebra.txt", b"z").unwrap();
4240 sync::write("/sort_test/apple.txt", b"a").unwrap();
4241 sync::write("/sort_test/mango.txt", b"m").unwrap();
4242
4243 let entries = sync::read_dir_sorted("/sort_test").unwrap();
4244 let filenames: Vec<_> = entries.iter().map(sync::DirEntry::file_name).collect();
4245
4246 assert_eq!(
4248 filenames,
4249 vec![
4250 std::ffi::OsString::from("apple.txt"),
4251 std::ffi::OsString::from("mango.txt"),
4252 std::ffi::OsString::from("zebra.txt"),
4253 ]
4254 );
4255 }
4256
4257 #[test_log::test]
4258 fn test_read_dir_sorted_nonexistent_dir_fails() {
4259 reset_fs();
4260
4261 let result = sync::read_dir_sorted("/nonexistent_read");
4262 assert!(result.is_err());
4263 let err = result.err().unwrap();
4264 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
4265 }
4266
4267 #[test_log::test]
4268 fn test_read_dir_sorted_does_not_include_nested() {
4269 reset_fs();
4270 sync::create_dir_all("/parent/child").unwrap();
4271 sync::write("/parent/direct.txt", b"direct").unwrap();
4272 sync::write("/parent/child/nested.txt", b"nested").unwrap();
4273
4274 let entries = sync::read_dir_sorted("/parent").unwrap();
4275 let filenames: Vec<_> = entries.iter().map(sync::DirEntry::file_name).collect();
4276
4277 assert!(
4279 filenames.contains(&std::ffi::OsString::from("direct.txt")),
4280 "should contain direct.txt"
4281 );
4282 assert!(
4283 filenames.contains(&std::ffi::OsString::from("child")),
4284 "should contain child directory"
4285 );
4286 assert!(
4287 !filenames.contains(&std::ffi::OsString::from("nested.txt")),
4288 "should NOT contain nested.txt"
4289 );
4290 }
4291}
4292
4293#[cfg(test)]
4294#[cfg(feature = "async")]
4295mod async_read_dir_tests {
4296 use super::{reset_fs, sync, unsync};
4297 use pretty_assertions::assert_eq;
4298
4299 #[test_log::test(switchy_async::test)]
4300 async fn test_async_read_dir_iteration() {
4301 reset_fs();
4302 sync::create_dir_all("/async_iter").unwrap();
4303 sync::write("/async_iter/a.txt", b"a").unwrap();
4304 sync::write("/async_iter/b.txt", b"b").unwrap();
4305
4306 let mut read_dir = unsync::read_dir("/async_iter").await.unwrap();
4307
4308 let mut entries = Vec::new();
4310 while let Some(entry) = read_dir.next_entry().await.unwrap() {
4311 entries.push(entry);
4312 }
4313
4314 assert_eq!(entries.len(), 2, "should have 2 entries");
4315 }
4316
4317 #[test_log::test(switchy_async::test)]
4318 async fn test_async_read_dir_empty() {
4319 reset_fs();
4320 sync::create_dir_all("/async_empty").unwrap();
4321
4322 let mut read_dir = unsync::read_dir("/async_empty").await.unwrap();
4323
4324 let entry = read_dir.next_entry().await.unwrap();
4326 assert!(entry.is_none(), "empty directory should return None");
4327 }
4328
4329 #[test_log::test(switchy_async::test)]
4330 async fn test_async_dir_entry_file_type() {
4331 reset_fs();
4332 sync::create_dir_all("/async_type/subdir").unwrap();
4333 sync::write("/async_type/file.txt", b"content").unwrap();
4334
4335 let entries = unsync::read_dir_sorted("/async_type").await.unwrap();
4336
4337 let file_entry = entries
4339 .iter()
4340 .find(|e| e.file_name() == "file.txt")
4341 .unwrap();
4342 let dir_entry = entries.iter().find(|e| e.file_name() == "subdir").unwrap();
4343
4344 let file_type = file_entry.file_type().await.unwrap();
4346 assert!(file_type.is_file(), "file.txt should be a file");
4347 assert!(!file_type.is_dir(), "file.txt should not be a directory");
4348
4349 let dir_type = dir_entry.file_type().await.unwrap();
4350 assert!(dir_type.is_dir(), "subdir should be a directory");
4351 assert!(!dir_type.is_file(), "subdir should not be a file");
4352 }
4353
4354 #[test_log::test(switchy_async::test)]
4355 async fn test_async_dir_entry_metadata() {
4356 reset_fs();
4357 sync::create_dir_all("/async_meta").unwrap();
4358 sync::write("/async_meta/test.txt", b"test content").unwrap();
4359
4360 let entries = unsync::read_dir_sorted("/async_meta").await.unwrap();
4361 let file_entry = entries
4362 .iter()
4363 .find(|e| e.file_name() == "test.txt")
4364 .unwrap();
4365
4366 let metadata = file_entry.metadata().await.unwrap();
4367 assert_eq!(
4368 metadata.len(),
4369 12,
4370 "file should have correct length (12 bytes)"
4371 );
4372 assert!(metadata.is_file(), "should be a file");
4373 }
4374
4375 #[test_log::test(switchy_async::test)]
4376 async fn test_async_dir_entry_path_and_file_name() {
4377 reset_fs();
4378 sync::create_dir_all("/async_paths").unwrap();
4379 sync::write("/async_paths/example.txt", b"data").unwrap();
4380
4381 let entries = unsync::read_dir_sorted("/async_paths").await.unwrap();
4382 let entry = &entries[0];
4383
4384 assert_eq!(
4385 entry.path(),
4386 std::path::PathBuf::from("/async_paths/example.txt")
4387 );
4388 assert_eq!(entry.file_name(), std::ffi::OsString::from("example.txt"));
4389 }
4390
4391 #[test_log::test(switchy_async::test)]
4392 async fn test_async_walk_dir_sorted() {
4393 reset_fs();
4394 sync::create_dir_all("/async_walk/sub").unwrap();
4395 sync::write("/async_walk/root.txt", b"root").unwrap();
4396 sync::write("/async_walk/sub/nested.txt", b"nested").unwrap();
4397
4398 let entries = unsync::walk_dir_sorted("/async_walk").await.unwrap();
4399
4400 let paths: Vec<_> = entries.iter().map(unsync::DirEntry::path).collect();
4402 assert!(
4403 paths.contains(&std::path::PathBuf::from("/async_walk/sub")),
4404 "should contain sub directory"
4405 );
4406 assert!(
4407 paths.contains(&std::path::PathBuf::from("/async_walk/root.txt")),
4408 "should contain root.txt"
4409 );
4410 assert!(
4411 paths.contains(&std::path::PathBuf::from("/async_walk/sub/nested.txt")),
4412 "should contain nested.txt"
4413 );
4414 }
4415
4416 #[test_log::test(switchy_async::test)]
4417 async fn test_async_dir_entry_metadata_for_directory() {
4418 reset_fs();
4419 sync::create_dir_all("/async_dir_meta/subdir").unwrap();
4420
4421 let entries = unsync::read_dir_sorted("/async_dir_meta").await.unwrap();
4422 let dir_entry = &entries[0];
4423
4424 let metadata = dir_entry.metadata().await.unwrap();
4425 assert!(metadata.is_dir(), "should be a directory");
4426 assert_eq!(metadata.len(), 0, "directory should have length 0");
4427 }
4428}
4429
4430#[cfg(test)]
4431mod dir_entry_sync_tests {
4432 use super::{reset_fs, sync};
4433 use pretty_assertions::assert_eq;
4434
4435 #[test_log::test]
4436 fn test_dir_entry_new_file() {
4437 let entry =
4438 sync::DirEntry::new_file("/path/to/file.txt".to_string(), "file.txt".to_string())
4439 .unwrap();
4440
4441 assert_eq!(entry.path(), std::path::PathBuf::from("/path/to/file.txt"));
4442 assert_eq!(entry.file_name(), std::ffi::OsString::from("file.txt"));
4443 assert!(entry.file_type().unwrap().is_file());
4444 assert!(!entry.file_type().unwrap().is_dir());
4445 }
4446
4447 #[test_log::test]
4448 fn test_dir_entry_new_dir() {
4449 let entry = sync::DirEntry::new_dir("/path/to/dir".to_string(), "dir".to_string()).unwrap();
4450
4451 assert_eq!(entry.path(), std::path::PathBuf::from("/path/to/dir"));
4452 assert_eq!(entry.file_name(), std::ffi::OsString::from("dir"));
4453 assert!(entry.file_type().unwrap().is_dir());
4454 assert!(!entry.file_type().unwrap().is_file());
4455 }
4456
4457 #[test_log::test]
4458 fn test_dir_entry_file_type_accessor() {
4459 reset_fs();
4460 sync::create_dir_all("/entry_test/subdir").unwrap();
4461 sync::write("/entry_test/file.txt", b"content").unwrap();
4462
4463 let entries = sync::read_dir_sorted("/entry_test").unwrap();
4464
4465 for entry in entries {
4466 let file_type = entry.file_type().unwrap();
4467 let type_count = [
4469 file_type.is_file(),
4470 file_type.is_dir(),
4471 file_type.is_symlink(),
4472 ]
4473 .iter()
4474 .filter(|&&x| x)
4475 .count();
4476 assert_eq!(type_count, 1, "each entry should have exactly one type");
4477 }
4478 }
4479}
4480
4481#[cfg(test)]
4482mod file_create_and_open_tests {
4483 use super::{reset_fs, sync};
4484 use pretty_assertions::assert_eq;
4485 use std::io::{Read as _, Write as _};
4486
4487 #[test_log::test]
4488 fn test_file_create_new_file() {
4489 reset_fs();
4490 sync::create_dir_all("/tmp").unwrap();
4491
4492 let mut file = sync::File::create("/tmp/new_file.txt").unwrap();
4493 file.write_all(b"created content").unwrap();
4494 drop(file);
4495
4496 let content = sync::read_to_string("/tmp/new_file.txt").unwrap();
4497 assert_eq!(content, "created content");
4498 }
4499
4500 #[test_log::test]
4501 fn test_file_create_truncates_existing() {
4502 reset_fs();
4503 sync::create_dir_all("/tmp").unwrap();
4504 sync::write("/tmp/existing.txt", b"original content that is long").unwrap();
4505
4506 let mut file = sync::File::create("/tmp/existing.txt").unwrap();
4507 file.write_all(b"short").unwrap();
4508 drop(file);
4509
4510 let content = sync::read_to_string("/tmp/existing.txt").unwrap();
4511 assert_eq!(content, "short");
4512 }
4513
4514 #[test_log::test]
4515 fn test_file_open_reads_content() {
4516 reset_fs();
4517 sync::create_dir_all("/tmp").unwrap();
4518 sync::write("/tmp/read_test.txt", b"readable content").unwrap();
4519
4520 let mut file = sync::File::open("/tmp/read_test.txt").unwrap();
4521 let mut content = String::new();
4522 file.read_to_string(&mut content).unwrap();
4523
4524 assert_eq!(content, "readable content");
4525 }
4526
4527 #[test_log::test]
4528 fn test_file_open_nonexistent_fails() {
4529 reset_fs();
4530
4531 let result = sync::File::open("/nonexistent/file.txt");
4532 assert!(result.is_err());
4533 let err = result.err().unwrap();
4534 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
4535 }
4536
4537 #[test_log::test]
4538 fn test_file_create_without_parent_fails() {
4539 reset_fs();
4540
4541 let result = sync::File::create("/no/parent/file.txt");
4542 assert!(result.is_err());
4543 let err = result.err().unwrap();
4544 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
4545 }
4546
4547 #[test_log::test]
4548 fn test_file_options_returns_open_options() {
4549 let options = sync::File::options();
4550 let _ = options.read(true).write(true).create(true);
4552 }
4553}
4554
4555#[cfg(test)]
4556mod normalize_path_tests {
4557 use super::normalize_path;
4558 use pretty_assertions::assert_eq;
4559
4560 #[test_log::test]
4561 fn test_normalize_absolute_path_with_single_dot() {
4562 assert_eq!(normalize_path("/a/./b"), "/a/b");
4564 assert_eq!(normalize_path("/./a"), "/a");
4565 assert_eq!(normalize_path("/a/."), "/a");
4566 assert_eq!(normalize_path("/././."), "/");
4567 }
4568
4569 #[test_log::test]
4570 fn test_normalize_absolute_path_with_double_dots() {
4571 assert_eq!(normalize_path("/a/b/../c"), "/a/c");
4573 assert_eq!(normalize_path("/a/b/c/../../d"), "/a/d");
4574 assert_eq!(normalize_path("/a/../b"), "/b");
4575 }
4576
4577 #[test_log::test]
4578 fn test_normalize_absolute_path_double_dots_at_root() {
4579 assert_eq!(normalize_path("/.."), "/");
4581 assert_eq!(normalize_path("/../a"), "/a");
4582 assert_eq!(normalize_path("/../../a/b"), "/a/b");
4583 }
4584
4585 #[test_log::test]
4586 fn test_normalize_absolute_path_with_trailing_slashes() {
4587 assert_eq!(normalize_path("/a/b/"), "/a/b");
4589 assert_eq!(normalize_path("/a//b"), "/a/b");
4590 assert_eq!(normalize_path("///a///b///"), "/a/b");
4591 }
4592
4593 #[test_log::test]
4594 fn test_normalize_relative_path_with_single_dot() {
4595 assert_eq!(normalize_path("./a"), "a");
4596 assert_eq!(normalize_path("a/./b"), "a/b");
4597 assert_eq!(normalize_path("."), ".");
4598 }
4599
4600 #[test_log::test]
4601 fn test_normalize_relative_path_with_double_dots() {
4602 assert_eq!(normalize_path("../a"), "../a");
4604 assert_eq!(normalize_path("../../a"), "../../a");
4605 assert_eq!(normalize_path("a/b/../../c"), "c");
4606 assert_eq!(normalize_path("a/../b"), "b");
4607 }
4608
4609 #[test_log::test]
4610 fn test_normalize_relative_path_double_dots_preserved() {
4611 assert_eq!(normalize_path("a/../../b"), "../b");
4613 assert_eq!(normalize_path("../.."), "../..");
4614 }
4615
4616 #[test_log::test]
4617 fn test_normalize_empty_path() {
4618 assert_eq!(normalize_path(""), ".");
4620 }
4621
4622 #[test_log::test]
4623 fn test_normalize_root_path() {
4624 assert_eq!(normalize_path("/"), "/");
4625 }
4626
4627 #[test_log::test]
4628 fn test_normalize_mixed_dots() {
4629 assert_eq!(normalize_path("/a/./b/../c/./d"), "/a/c/d");
4631 assert_eq!(normalize_path("./a/./b/../c"), "a/c");
4632 }
4633}
4634
4635#[cfg(test)]
4636mod canonicalize_tests {
4637 use super::{reset_fs, sync};
4638 use pretty_assertions::assert_eq;
4639
4640 #[test_log::test]
4641 fn test_canonicalize_existing_directory() {
4642 reset_fs();
4643 sync::create_dir_all("/test/path/to/dir").unwrap();
4644
4645 let result = sync::canonicalize("/test/path/to/dir").unwrap();
4646 assert_eq!(result.to_str().unwrap(), "/test/path/to/dir");
4647 }
4648
4649 #[test_log::test]
4650 fn test_canonicalize_existing_file() {
4651 reset_fs();
4652 sync::create_dir_all("/test").unwrap();
4653 sync::write("/test/file.txt", b"content").unwrap();
4654
4655 let result = sync::canonicalize("/test/file.txt").unwrap();
4656 assert_eq!(result.to_str().unwrap(), "/test/file.txt");
4657 }
4658
4659 #[test_log::test]
4660 fn test_canonicalize_path_with_dots() {
4661 reset_fs();
4662 sync::create_dir_all("/a/b/c").unwrap();
4663
4664 let result = sync::canonicalize("/a/b/../b/./c").unwrap();
4666 assert_eq!(result.to_str().unwrap(), "/a/b/c");
4667 }
4668
4669 #[test_log::test]
4670 fn test_canonicalize_path_with_double_slashes() {
4671 reset_fs();
4672 sync::create_dir_all("/test/dir").unwrap();
4673
4674 let result = sync::canonicalize("/test//dir").unwrap();
4675 assert_eq!(result.to_str().unwrap(), "/test/dir");
4676 }
4677
4678 #[test_log::test]
4679 fn test_canonicalize_nonexistent_path_fails() {
4680 reset_fs();
4681
4682 let result = sync::canonicalize("/nonexistent/path");
4683 assert!(result.is_err());
4684 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
4685 }
4686
4687 #[test_log::test]
4688 fn test_canonicalize_root() {
4689 reset_fs();
4690 sync::create_dir_all("/").unwrap();
4691
4692 let result = sync::canonicalize("/").unwrap();
4693 assert_eq!(result.to_str().unwrap(), "/");
4694 }
4695
4696 #[test_log::test]
4697 fn test_canonicalize_double_dots_past_root() {
4698 reset_fs();
4699 sync::create_dir_all("/existing").unwrap();
4700
4701 let result = sync::canonicalize("/../existing").unwrap();
4703 assert_eq!(result.to_str().unwrap(), "/existing");
4704 }
4705}
4706
4707#[cfg(test)]
4708mod create_dir_tests {
4709 use super::{exists, reset_fs, sync};
4710 use pretty_assertions::assert_eq;
4711
4712 #[test_log::test]
4713 fn test_create_dir_single_level() {
4714 reset_fs();
4715 sync::create_dir_all("/").unwrap();
4716
4717 sync::create_dir("/toplevel").unwrap();
4718 assert!(exists("/toplevel"));
4719 }
4720
4721 #[test_log::test]
4722 fn test_create_dir_with_existing_parent() {
4723 reset_fs();
4724 sync::create_dir_all("/parent").unwrap();
4725
4726 sync::create_dir("/parent/child").unwrap();
4727 assert!(exists("/parent/child"));
4728 }
4729
4730 #[test_log::test]
4731 fn test_create_dir_without_parent_fails() {
4732 reset_fs();
4733
4734 let result = sync::create_dir("/missing/child");
4736 assert!(result.is_err());
4737 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
4738 }
4739
4740 #[test_log::test]
4741 fn test_create_dir_nested_without_parent_fails() {
4742 reset_fs();
4743 sync::create_dir_all("/a").unwrap();
4744
4745 let result = sync::create_dir("/a/b/c");
4747 assert!(result.is_err());
4748 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
4749 }
4750
4751 #[test_log::test]
4752 fn test_create_dir_root() {
4753 reset_fs();
4754
4755 sync::create_dir("/").unwrap();
4757 assert!(exists("/"));
4758 }
4759
4760 #[test_log::test]
4761 fn test_create_dir_with_trailing_slash() {
4762 reset_fs();
4763 sync::create_dir_all("/parent").unwrap();
4764
4765 sync::create_dir("/parent/child/").unwrap();
4766 assert!(exists("/parent/child"));
4768 }
4769
4770 #[test_log::test]
4771 fn test_create_dir_idempotent() {
4772 reset_fs();
4773 sync::create_dir_all("/parent").unwrap();
4774
4775 sync::create_dir("/parent/child").unwrap();
4777 sync::create_dir("/parent/child").unwrap();
4778 assert!(exists("/parent/child"));
4779 }
4780}
4781
4782#[cfg(test)]
4783#[cfg(feature = "async")]
4784mod async_is_file_is_dir_tests {
4785 use super::{reset_fs, sync, unsync};
4786
4787 #[test_log::test(switchy_async::test)]
4788 async fn test_is_file_returns_true_for_file() {
4789 reset_fs();
4790 sync::create_dir_all("/test").unwrap();
4791 sync::write("/test/file.txt", b"content").unwrap();
4792
4793 assert!(unsync::is_file("/test/file.txt").await);
4794 }
4795
4796 #[test_log::test(switchy_async::test)]
4797 async fn test_is_file_returns_false_for_directory() {
4798 reset_fs();
4799 sync::create_dir_all("/test/subdir").unwrap();
4800
4801 assert!(!unsync::is_file("/test/subdir").await);
4802 }
4803
4804 #[test_log::test(switchy_async::test)]
4805 async fn test_is_file_returns_false_for_nonexistent() {
4806 reset_fs();
4807
4808 assert!(!unsync::is_file("/nonexistent/file.txt").await);
4809 }
4810
4811 #[test_log::test(switchy_async::test)]
4812 async fn test_is_dir_returns_true_for_directory() {
4813 reset_fs();
4814 sync::create_dir_all("/test/subdir").unwrap();
4815
4816 assert!(unsync::is_dir("/test/subdir").await);
4817 }
4818
4819 #[test_log::test(switchy_async::test)]
4820 async fn test_is_dir_returns_false_for_file() {
4821 reset_fs();
4822 sync::create_dir_all("/test").unwrap();
4823 sync::write("/test/file.txt", b"content").unwrap();
4824
4825 assert!(!unsync::is_dir("/test/file.txt").await);
4826 }
4827
4828 #[test_log::test(switchy_async::test)]
4829 async fn test_is_dir_returns_false_for_nonexistent() {
4830 reset_fs();
4831
4832 assert!(!unsync::is_dir("/nonexistent/dir").await);
4833 }
4834
4835 #[test_log::test(switchy_async::test)]
4836 async fn test_is_file_with_invalid_path() {
4837 reset_fs();
4838
4839 assert!(!unsync::is_file("").await);
4843 }
4844
4845 #[test_log::test(switchy_async::test)]
4846 async fn test_is_dir_with_root() {
4847 reset_fs();
4848 sync::create_dir_all("/").unwrap();
4849
4850 assert!(unsync::is_dir("/").await);
4851 }
4852}
4853
4854#[cfg(test)]
4855#[cfg(feature = "async")]
4856mod async_canonicalize_tests {
4857 use super::{reset_fs, sync, unsync};
4858 use pretty_assertions::assert_eq;
4859
4860 #[test_log::test(switchy_async::test)]
4861 async fn test_async_canonicalize_existing_path() {
4862 reset_fs();
4863 sync::create_dir_all("/async/path/here").unwrap();
4864
4865 let result = unsync::canonicalize("/async/path/here").await.unwrap();
4866 assert_eq!(result.to_str().unwrap(), "/async/path/here");
4867 }
4868
4869 #[test_log::test(switchy_async::test)]
4870 async fn test_async_canonicalize_with_dots() {
4871 reset_fs();
4872 sync::create_dir_all("/a/b").unwrap();
4873
4874 let result = unsync::canonicalize("/a/./b/../b").await.unwrap();
4875 assert_eq!(result.to_str().unwrap(), "/a/b");
4876 }
4877
4878 #[test_log::test(switchy_async::test)]
4879 async fn test_async_canonicalize_nonexistent_fails() {
4880 reset_fs();
4881
4882 let result = unsync::canonicalize("/does/not/exist").await;
4883 assert!(result.is_err());
4884 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
4885 }
4886}
4887
4888#[cfg(test)]
4889#[cfg(feature = "async")]
4890mod async_create_dir_tests {
4891 use super::{exists, reset_fs, sync, unsync};
4892 use pretty_assertions::assert_eq;
4893
4894 #[test_log::test(switchy_async::test)]
4895 async fn test_async_create_dir_with_parent() {
4896 reset_fs();
4897 sync::create_dir_all("/async_parent").unwrap();
4898
4899 unsync::create_dir("/async_parent/child").await.unwrap();
4900 assert!(exists("/async_parent/child"));
4901 }
4902
4903 #[test_log::test(switchy_async::test)]
4904 async fn test_async_create_dir_without_parent_fails() {
4905 reset_fs();
4906
4907 let result = unsync::create_dir("/no_parent/child").await;
4908 assert!(result.is_err());
4909 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
4910 }
4911}