Skip to main content

switchy_fs/
simulator.rs

1//! In-memory filesystem simulator for testing.
2//!
3//! This module provides a complete filesystem simulation that runs entirely in memory,
4//! allowing you to test filesystem operations without touching the actual disk. This is
5//! particularly useful for unit tests, integration tests, and development environments.
6//!
7//! The simulator supports both synchronous and asynchronous operations, directory hierarchies,
8//! and temporary directory management.
9
10use std::{
11    cell::RefCell,
12    collections::{BTreeMap, BTreeSet},
13    sync::{Arc, Mutex, RwLock},
14};
15
16use bytes::BytesMut;
17
18// Module that contains all real_fs functionality
19#[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    /// Executes a function using the actual filesystem instead of the simulator
32    ///
33    /// This function temporarily switches to using real filesystem operations within
34    /// the provided closure, allowing you to interact with the actual disk even when
35    /// the simulator is enabled.
36    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    // Simple conversion for std::fs::File to simulator File
46    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    // Async conversion for std::fs::File to simulator File
71    #[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// When the feature is not enabled, provide no-op implementations
103#[cfg(not(feature = "simulator-real-fs"))]
104mod real_fs_support {
105    /// Executes a function without switching filesystem modes (no-op when `simulator-real-fs` feature is disabled)
106    ///
107    /// This function simply executes the provided closure without changing filesystem behavior.
108    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
119// Re-export at module level for clean access
120pub 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
129/// Resets the simulated filesystem to an empty state
130///
131/// Clears all files and directories from the in-memory filesystem simulator.
132///
133/// # Panics
134///
135/// * If the `FILES` `RwLock` fails to write to
136pub fn reset_fs() {
137    FILES.with_borrow_mut(|x| x.write().unwrap().clear());
138    reset_directories();
139}
140
141/// Resets all directories in the simulated filesystem
142///
143/// Clears the directory registry, removing all tracked directories from the in-memory filesystem.
144///
145/// # Panics
146///
147/// * If the `DIRECTORIES` `RwLock` fails to write to
148pub fn reset_directories() {
149    DIRECTORIES.with_borrow_mut(|x| x.write().unwrap().clear());
150}
151
152/// Get all parent directories of a path
153fn 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    // Always include root
169    if path != "/" {
170        parents.push("/".to_string());
171    }
172
173    parents.reverse();
174    parents
175}
176
177/// Normalize a path by resolving `.` and `..` components
178///
179/// This function handles path normalization without requiring filesystem access,
180/// making it suitable for the simulator.
181fn 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                // Skip empty components and current directory references
189            }
190            ".." => {
191                // Go up one directory (if possible)
192                if !components.is_empty() && components.last() != Some(&"..") {
193                    components.pop();
194                } else if !is_absolute {
195                    // For relative paths, keep the .. if we can't go up
196                    components.push("..");
197                }
198                // For absolute paths, ignore .. at root
199            }
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/// Check if a path exists
220///
221/// # Panics
222///
223/// * If the `DIRECTORIES` `RwLock` is poisoned
224/// * If the `FILES` `RwLock` is poisoned
225#[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
234/// Get immediate children (files and directories) of a directory
235fn 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    // Get files in this directory
243    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    // Get subdirectories in this directory
261    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
282/// Initialize minimal filesystem structure (just essentials)
283///
284/// # Errors
285///
286/// * If any directory creation fails
287pub 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
297/// Initialize standard FHS-like filesystem structure
298///
299/// # Errors
300///
301/// * If any directory creation fails
302pub fn init_standard_fs() -> std::io::Result<()> {
303    #[cfg(feature = "sync")]
304    {
305        // Root directories
306        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        // Common /usr subdirectories
319        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        // Common /var subdirectories
326        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
333/// Initialize a user's home directory with standard subdirectories
334///
335/// # Errors
336///
337/// * If any directory creation fails
338pub 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/// Seeds the simulator filesystem from a real filesystem path.
355///
356/// This function reads all files and directories from the real filesystem
357/// at `real_path` and populates them into the simulator at `sim_path`.
358///
359/// This is useful for tests that need to load test fixtures from the real
360/// filesystem into the simulator before running.
361///
362/// # Arguments
363/// * `real_path` - Path on the real filesystem to read from
364/// * `sim_path` - Path in the simulator where contents will be placed
365///
366/// # Errors
367///
368/// * If reading from the real filesystem fails
369/// * If writing to the simulator fails
370///
371/// # Panics
372///
373/// * If the real filesystem path cannot be read
374#[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    // Create the directory in simulator (outside of with_real_fs)
385    sync::create_dir_all(sim_path)?;
386
387    // Read entries from real filesystem
388    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            // Read file content from real FS
400            let content = with_real_fs(|| std::fs::read(&real_entry_path))?;
401            // Write to simulator (outside of with_real_fs)
402            sync::write(&sim_entry_path, content)?;
403        }
404        // Skip symlinks and other special files for now
405    }
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/// File type information for directory entries
495///
496/// Provides methods to determine whether an entry is a directory, file, or symbolic link.
497#[derive(Debug, Clone)]
498pub struct FileType {
499    is_dir: bool,
500    is_file: bool,
501    is_symlink: bool,
502}
503
504impl FileType {
505    /// Returns `true` if this entry represents a directory
506    #[must_use]
507    pub const fn is_dir(&self) -> bool {
508        self.is_dir
509    }
510
511    /// Returns `true` if this entry represents a regular file
512    #[must_use]
513    pub const fn is_file(&self) -> bool {
514        self.is_file
515    }
516
517    /// Returns `true` if this entry represents a symbolic link
518    #[must_use]
519    pub const fn is_symlink(&self) -> bool {
520        self.is_symlink
521    }
522}
523
524/// Metadata information about a file in the simulated filesystem
525///
526/// Provides information about file size and type (file, directory, symlink).
527#[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    /// Returns the size of the file in bytes
537    #[must_use]
538    pub const fn len(&self) -> u64 {
539        self.len
540    }
541
542    /// Returns `true` if the file has zero length
543    #[must_use]
544    pub const fn is_empty(&self) -> bool {
545        self.len == 0
546    }
547
548    /// Returns `true` if this metadata is for a regular file
549    #[must_use]
550    pub const fn is_file(&self) -> bool {
551        self.is_file
552    }
553
554    /// Returns `true` if this metadata is for a directory
555    #[must_use]
556    pub const fn is_dir(&self) -> bool {
557        self.is_dir
558    }
559
560    /// Returns `true` if this metadata is for a symbolic link
561    #[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/// Synchronous filesystem operations for the simulator
634///
635/// This module provides blocking filesystem operations that work with the in-memory
636/// simulated filesystem. All operations are immediate and do not touch the actual disk.
637#[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    /// File handle for synchronous operations in the simulated filesystem
651    ///
652    /// Provides read, write, and seek operations on files stored in the in-memory filesystem.
653    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        /// Opens a file in read-only mode
663        ///
664        /// This is a convenience method equivalent to `OpenOptions::new().read(true).open(path)`.
665        ///
666        /// # Errors
667        ///
668        /// * If the file does not exist
669        /// * If the path cannot be converted to a string
670        pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
671            OpenOptions::new().read(true).open(path)
672        }
673
674        /// Creates a new file for writing, truncating any existing file
675        ///
676        /// This is a convenience method equivalent to
677        /// `OpenOptions::new().create(true).write(true).truncate(true).open(path)`.
678        ///
679        /// # Errors
680        ///
681        /// * If the parent directory does not exist
682        /// * If the path cannot be converted to a string
683        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        /// Returns a new `OpenOptions` builder for configuring how a file is opened
692        #[must_use]
693        pub const fn options() -> OpenOptions {
694            OpenOptions::new()
695        }
696
697        /// Retrieves metadata about the file
698        ///
699        /// # Errors
700        ///
701        /// * If the file metadata cannot be retrieved (when using real filesystem)
702        ///
703        /// # Panics
704        ///
705        /// * If the internal data mutex is poisoned (when using simulator)
706        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        /// Converts this synchronous file handle into an asynchronous file handle
721        #[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        /// Opens a file with the configured options
739        ///
740        /// # Errors
741        ///
742        /// * If an I/O error occurs
743        ///
744        /// # Panics
745        ///
746        /// * If the `FILES` `RwLock` fails to read.
747        pub fn open(self, path: impl AsRef<::std::path::Path>) -> ::std::io::Result<File> {
748            // Only try to use real fs if both simulator-real-fs AND std features are enabled
749            #[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            // Original simulator implementation (fallback)
759            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                // Check if parent directory exists when creating a file
766                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                    // Allow current directory "." to exist by default, otherwise check DIRECTORIES
778                    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    /// Reads the entire contents of a file into a byte vector
814    ///
815    /// # Errors
816    ///
817    /// * If the file doesn't exist
818    /// * If the file `Path` cannot be converted to a `str`
819    ///
820    /// # Panics
821    ///
822    /// * If the `FILES` `RwLock` fails to read.
823    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        // Original simulator implementation (fallback)
830        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    /// Reads the entire contents of a file into a string
842    ///
843    /// # Errors
844    ///
845    /// * Returns `std::io::ErrorKind::NotFound` if the file does not exist.
846    /// * Returns `std::io::ErrorKind::InvalidData` if the file contains invalid UTF-8.
847    ///
848    /// # Panics
849    ///
850    /// * If the `FILES` `RwLock` fails to read.
851    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        // Original simulator implementation (fallback)
858        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    /// Writes a slice as the entire contents of a file
871    ///
872    /// # Errors
873    ///
874    /// * If the file cannot be created
875    /// * If the file cannot be written to
876    /// * If the `FILES` `RwLock` fails to write to
877    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    /// Creates a directory and all missing parent directories
896    ///
897    /// # Errors
898    ///
899    /// * If underlying `std::fs::create_dir` fails (when using real filesystem)
900    /// * If the path cannot be converted to a string
901    /// * If the parent directory does not exist
902    ///
903    /// # Panics
904    ///
905    /// * If the `DIRECTORIES` `RwLock` fails to write to
906    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        // Normalize path - remove trailing slashes except for root
915        let normalized = if path_str == "/" {
916            "/".to_string()
917        } else {
918            path_str.trim_end_matches('/').to_string()
919        };
920
921        // Check that parent directory exists
922        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        // Create the directory
937        DIRECTORIES.with_borrow_mut(|dirs| {
938            dirs.write().unwrap().insert(normalized);
939        });
940
941        Ok(())
942    }
943
944    /// Creates a directory and all missing parent directories
945    ///
946    /// # Errors
947    ///
948    /// * If underlying `std::fs::create_dir_all` fails (when using real filesystem)
949    /// * If the path cannot be converted to a string
950    ///
951    /// # Panics
952    ///
953    /// * If the `DIRECTORIES` `RwLock` fails to write to
954    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        // Normalize path - remove trailing slashes except for root
963        let normalized = if path_str == "/" {
964            "/".to_string()
965        } else {
966            path_str.trim_end_matches('/').to_string()
967        };
968
969        // Get all directories that need to be created (including parents)
970        let mut dirs_to_create = super::get_parent_directories(&normalized);
971        dirs_to_create.push(normalized);
972
973        // Create all directories
974        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    /// Removes a directory and all its contents recursively
985    ///
986    /// # Errors
987    ///
988    /// * If underlying `std::fs::remove_dir_all` fails (when using real filesystem)
989    /// * If the path cannot be converted to a string
990    /// * If the directory doesn't exist
991    ///
992    /// # Panics
993    ///
994    /// * If the `DIRECTORIES` or `FILES` `RwLock` fails to write to
995    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        // Normalize path - remove trailing slashes except for root
1004        let normalized = if path_str == "/" {
1005            "/".to_string()
1006        } else {
1007            path_str.trim_end_matches('/').to_string()
1008        };
1009
1010        // Check if directory exists
1011        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        // Find all subdirectories and files to remove
1019        let prefix = if normalized == "/" {
1020            "/"
1021        } else {
1022            &format!("{normalized}/")
1023        };
1024
1025        // Remove all files in this directory and subdirectories
1026        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        // Remove all subdirectories
1033        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    /// Canonicalizes a path by resolving `.` and `..` components and normalizing it
1042    ///
1043    /// Unlike `std::fs::canonicalize`, this does not require the path to exist,
1044    /// but it will verify the path exists in the simulator filesystem.
1045    ///
1046    /// # Errors
1047    ///
1048    /// * If underlying `std::fs::canonicalize` fails (when using real filesystem)
1049    /// * If the path cannot be converted to a string
1050    /// * If the path does not exist in the simulator
1051    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        // Normalize the path by resolving . and .. components
1060        let normalized = super::normalize_path(path_str);
1061
1062        // Check if the path exists (either as file or directory)
1063        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    /// Read directory entries and return them sorted by filename for deterministic iteration
1074    ///
1075    /// # Errors
1076    ///
1077    /// * If underlying `std::fs::read_dir` fails (when using real filesystem)
1078    /// * If any directory entry cannot be read (when using real filesystem)
1079    /// * If the path cannot be converted to a string
1080    /// * If the directory doesn't exist
1081    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        // Normalize path
1094        let normalized = if path_str == "/" {
1095            "/".to_string()
1096        } else {
1097            path_str.trim_end_matches('/').to_string()
1098        };
1099
1100        // Check if directory exists
1101        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        // Get children
1109        let (files, subdirs) = super::get_directory_children(&normalized);
1110
1111        let mut entries = Vec::new();
1112
1113        // Add file entries
1114        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        // Add directory entries
1124        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        // Sort by filename for deterministic ordering
1134        entries.sort_by_key(DirEntry::file_name);
1135
1136        Ok(entries)
1137    }
1138
1139    /// Recursively walk directory tree and return all entries sorted by path for deterministic iteration
1140    ///
1141    /// # Errors
1142    ///
1143    /// * If any directory cannot be read (when using real filesystem)
1144    /// * If any directory entry cannot be accessed (when using real filesystem)
1145    /// * If the path cannot be converted to a string
1146    /// * If the directory doesn't exist
1147    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            // Get immediate children
1152            let (files, subdirs) = super::get_directory_children(dir_path);
1153
1154            // Add all files in current directory
1155            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            // Add all subdirectories and recursively walk them
1165            for dirname in subdirs {
1166                let full_path = if dir_path == "/" {
1167                    format!("/{dirname}")
1168                } else {
1169                    format!("{dir_path}/{dirname}")
1170                };
1171
1172                // Add the directory itself
1173                all_entries.push(DirEntry::new_dir(full_path.clone(), dirname)?);
1174
1175                // Recursively walk the subdirectory
1176                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        // Normalize path
1195        let normalized = if path_str == "/" {
1196            "/".to_string()
1197        } else {
1198            path_str.trim_end_matches('/').to_string()
1199        };
1200
1201        // Check if directory exists
1202        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        // Sort by full path for deterministic ordering
1212        all_entries.sort_by_key(DirEntry::path);
1213
1214        Ok(all_entries)
1215    }
1216
1217    /// Directory entry for synchronous filesystem operations
1218    ///
1219    /// Represents a single entry (file or directory) when iterating over directory contents.
1220    /// Provides methods to access the entry's path, name, and type information.
1221    pub struct DirEntry {
1222        path: PathBuf,
1223        file_name: std::ffi::OsString,
1224        file_type_info: super::FileType,
1225    }
1226
1227    impl DirEntry {
1228        /// Create a new `DirEntry` from `std::fs::DirEntry`
1229        ///
1230        /// # Errors
1231        ///
1232        /// * If the file type cannot be determined
1233        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        /// Create a new `DirEntry` for a file in the simulator
1247        ///
1248        /// # Errors
1249        ///
1250        /// * Infallible in current implementation
1251        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        /// Create a new `DirEntry` for a directory in the simulator
1264        ///
1265        /// # Errors
1266        ///
1267        /// * Infallible in current implementation
1268        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        /// Returns the full path to this entry
1281        #[must_use]
1282        pub fn path(&self) -> PathBuf {
1283            self.path.clone()
1284        }
1285
1286        /// Returns the file name of this entry
1287        #[must_use]
1288        pub fn file_name(&self) -> std::ffi::OsString {
1289            self.file_name.clone()
1290        }
1291
1292        /// Returns the file type of this entry
1293        ///
1294        /// # Errors
1295        ///
1296        /// This function always succeeds for simulator entries, but returns
1297        /// `Result` to match the `std::fs::DirEntry::file_type()` API.
1298        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                // Create file with write permission
1365                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                // Open file with read-only permission
1374                let mut file = OpenOptions::new()
1375                    .read(true)
1376                    .open("/tmp/test_perms.txt")
1377                    .unwrap();
1378
1379                // Attempt to write should fail with PermissionDenied
1380                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                // Create file with initial content
1398                super::write(
1399                    "/tmp/truncate_test.txt",
1400                    b"initial content that should be removed",
1401                )
1402                .unwrap();
1403
1404                // Verify initial content exists
1405                let content = super::read_to_string("/tmp/truncate_test.txt").unwrap();
1406                assert_eq!(content, "initial content that should be removed");
1407
1408                // Open with truncate flag
1409                let mut file = OpenOptions::new()
1410                    .write(true)
1411                    .truncate(true)
1412                    .open("/tmp/truncate_test.txt")
1413                    .unwrap();
1414
1415                // Write new content
1416                file.write_all(b"new").unwrap();
1417                drop(file);
1418
1419                // Verify file was truncated and only has new content
1420                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                // Create file with known content
1434                let test_data = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 36 bytes
1435                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                // Read in small chunks
1443                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                // Verify all data was read correctly
1455                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                // Create file with content
1468                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                // Seek to position 7 (start of "World")
1476                let pos = file.seek(SeekFrom::Start(7)).unwrap();
1477                assert_eq!(pos, 7);
1478
1479                // Read from new position
1480                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                // Seek back to beginning
1486                let pos = file.seek(SeekFrom::Start(0)).unwrap();
1487                assert_eq!(pos, 0);
1488
1489                // Read again
1490                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                // Create file with content
1506                super::write("/tmp/seek_end.txt", b"0123456789").unwrap(); // 10 bytes
1507
1508                let mut file = OpenOptions::new()
1509                    .read(true)
1510                    .open("/tmp/seek_end.txt")
1511                    .unwrap();
1512
1513                // NOTE: Current implementation bug with SeekFrom::End
1514                // The formula is: length - offset
1515                // When offset is negative, it should ADD to length, but instead:
1516                // length - (-3) causes underflow in u64::try_from(i64 - i64)
1517                // We test with positive offset to avoid underflow while documenting the issue
1518                let pos = file.seek(SeekFrom::End(0)).unwrap();
1519                assert_eq!(pos, 10, "Seek to end of 10-byte file");
1520
1521                // Reading should return 0 bytes (at EOF)
1522                let mut buf = [0u8; 10];
1523                let count = file.read(&mut buf).unwrap();
1524                assert_eq!(count, 0);
1525
1526                // BUG: SeekFrom::End with negative offsets causes underflow
1527                // This is a known bug - negative offsets subtract instead of add
1528            }
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                // Read first 3 bytes
1547                let mut buf = [0u8; 3];
1548                file.read_exact(&mut buf).unwrap();
1549                assert_eq!(&buf, b"012");
1550
1551                // Seek forward 2 bytes from current position
1552                let pos = file.seek(SeekFrom::Current(2)).unwrap();
1553                assert_eq!(pos, 5);
1554
1555                // Read next 3 bytes (should be "567")
1556                file.read_exact(&mut buf).unwrap();
1557                assert_eq!(&buf, b"567");
1558
1559                // Seek backward 4 bytes from current position
1560                let pos = file.seek(SeekFrom::Current(-4)).unwrap();
1561                assert_eq!(pos, 4);
1562
1563                // Read should give "456"
1564                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(); // 5 bytes
1578
1579                let mut file = OpenOptions::new()
1580                    .read(true)
1581                    .open("/tmp/seek_past_eof.txt")
1582                    .unwrap();
1583
1584                // Seek past EOF using Start should succeed
1585                let pos = file.seek(SeekFrom::Start(100)).unwrap();
1586                assert_eq!(pos, 100);
1587
1588                // NOTE: Current implementation has an underflow bug with SeekFrom::End
1589                // when seeking way past EOF. We test normal seek behavior above.
1590                // The overflow happens because: length - large_negative_offset overflows
1591            }
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                // Create initial file
1603                super::write("/tmp/shared.txt", b"initial").unwrap();
1604
1605                // Open file for writing
1606                let mut writer = OpenOptions::new()
1607                    .write(true)
1608                    .truncate(true)
1609                    .open("/tmp/shared.txt")
1610                    .unwrap();
1611
1612                // Open same file for reading
1613                let mut reader = OpenOptions::new()
1614                    .read(true)
1615                    .open("/tmp/shared.txt")
1616                    .unwrap();
1617
1618                // Write new content
1619                writer.write_all(b"updated content").unwrap();
1620                drop(writer);
1621
1622                // Reader should see updated content (shared Arc<Mutex<BytesMut>>)
1623                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            // Reading into empty buffer should return 0 without error
1642            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                // Write data
1663                file.write_all(b"0123456789").unwrap();
1664
1665                // NOTE: Current implementation bug - write does not update position
1666                // It always appends, so position stays at 0
1667                // This test documents the current buggy behavior
1668                let pos = file.stream_position().unwrap();
1669                assert_eq!(pos, 0, "BUG: Write should update position but doesn't");
1670
1671                // Seek to beginning (no-op since we're at 0)
1672                file.seek(SeekFrom::Start(0)).unwrap();
1673
1674                // Read 5 bytes
1675                let mut buf = [0u8; 5];
1676                file.read_exact(&mut buf).unwrap();
1677
1678                // Position should be at 5 after read
1679                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                // Create file with content and specific position
1694                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                // Convert to async
1708                let async_file = file.into_async();
1709
1710                // Verify state is preserved
1711                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            // Create empty directory
1722            super::create_dir_all("/tmp/empty_dir").unwrap();
1723
1724            // Verify it exists
1725            assert!(super::super::exists("/tmp/empty_dir"));
1726
1727            // Remove it
1728            super::remove_dir_all("/tmp/empty_dir").unwrap();
1729
1730            // Should no longer exist
1731            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            // Attempt to remove non-existent directory should fail
1739            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            // Should be able to read root directory
1750            let _entries = super::read_dir_sorted("/").unwrap();
1751
1752            // Root should exist
1753            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                // Creating file in current directory "." should work
1764                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                // Should be able to read it back
1774                let content = super::read_to_string("./test.txt").unwrap();
1775                assert_eq!(content, "content");
1776            }
1777        }
1778    }
1779}
1780
1781/// Asynchronous filesystem operations for the simulator
1782///
1783/// This module provides async filesystem operations that work with the in-memory
1784/// simulated filesystem. Operations are non-blocking but execute immediately since
1785/// no actual I/O is performed.
1786#[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    /// File handle for asynchronous operations in the simulated filesystem
1799    ///
1800    /// Provides async read, write, and seek operations on files stored in the in-memory filesystem.
1801    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        /// Opens a file in read-only mode asynchronously
1810        ///
1811        /// This is a convenience method equivalent to `OpenOptions::new().read(true).open(path)`.
1812        ///
1813        /// # Errors
1814        ///
1815        /// * If the file does not exist
1816        /// * If the path cannot be converted to a string
1817        #[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        /// Creates a new file for writing, truncating any existing file
1823        ///
1824        /// This is a convenience method equivalent to
1825        /// `OpenOptions::new().create(true).write(true).truncate(true).open(path)`.
1826        ///
1827        /// # Errors
1828        ///
1829        /// * If the parent directory does not exist
1830        /// * If the path cannot be converted to a string
1831        #[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        /// Returns a new `OpenOptions` builder for configuring how a file is opened
1842        #[must_use]
1843        pub const fn options() -> OpenOptions {
1844            OpenOptions::new()
1845        }
1846
1847        /// Retrieves metadata about the file asynchronously
1848        ///
1849        /// # Errors
1850        ///
1851        /// * If the file metadata cannot be retrieved (when using real filesystem)
1852        ///
1853        /// # Panics
1854        ///
1855        /// * If the internal data mutex is poisoned (when using simulator)
1856        /// * If the `spawn_blocking` task panics (when using real filesystem)
1857        #[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        /// Converts this asynchronous file handle into a synchronous file handle
1878        #[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        /// Opens a file asynchronously with the configured options
1976        ///
1977        /// # Errors
1978        ///
1979        /// * If an I/O error occurs
1980        ///
1981        /// # Panics
1982        ///
1983        /// * If the `FILES` `RwLock` fails to read.
1984        #[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            // Fallback to sync simulator implementation
2003            Ok(self.into_sync().open(path)?.into_async())
2004        }
2005    }
2006
2007    /// Reads the entire contents of a file into a byte vector asynchronously
2008    ///
2009    /// # Errors
2010    ///
2011    /// * If the file doesn't exist
2012    /// * If the file `Path` cannot be converted to a `str`
2013    ///
2014    /// # Panics
2015    ///
2016    /// * If the `spawn_blocking` task fails
2017    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        // Fallback to sync simulator implementation
2027        super::sync::read(path)
2028    }
2029
2030    /// Reads the entire contents of a file into a string asynchronously
2031    ///
2032    /// # Errors
2033    ///
2034    /// * Returns `std::io::ErrorKind::NotFound` if the file does not exist.
2035    /// * Returns `std::io::ErrorKind::InvalidData` if the file contains invalid UTF-8.
2036    ///
2037    /// # Panics
2038    ///
2039    /// * If the `spawn_blocking` task fails
2040    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        // Fallback to sync simulator implementation
2050        super::sync::read_to_string(path)
2051    }
2052
2053    /// Checks if a path exists asynchronously
2054    ///
2055    /// Returns `true` if the path exists, `false` otherwise.
2056    #[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    /// Checks if a path is a file asynchronously
2070    ///
2071    /// Returns `true` if the path exists and is a file, `false` otherwise.
2072    ///
2073    /// # Panics
2074    ///
2075    /// * If the `FILES` `RwLock` fails to read from
2076    #[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    /// Checks if a path is a directory asynchronously
2091    ///
2092    /// Returns `true` if the path exists and is a directory, `false` otherwise.
2093    ///
2094    /// # Panics
2095    ///
2096    /// * If the `DIRECTORIES` `RwLock` fails to read from
2097    #[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    /// Writes a slice as the entire contents of a file
2112    ///
2113    /// # Errors
2114    ///
2115    /// * If the file cannot be created
2116    /// * If the file cannot be written to
2117    /// * If the `FILES` `RwLock` fails to write to
2118    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    /// Creates a directory and all missing parent directories asynchronously
2141    ///
2142    /// # Errors
2143    ///
2144    /// * If underlying `std::fs::create_dir` fails (when using real filesystem)
2145    /// * If the parent directory does not exist
2146    ///
2147    /// # Panics
2148    ///
2149    /// * If the `spawn_blocking` task fails
2150    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    /// Creates a directory and all missing parent directories asynchronously
2163    ///
2164    /// # Errors
2165    ///
2166    /// * If underlying `std::fs::create_dir_all` fails (when using real filesystem)
2167    ///
2168    /// # Panics
2169    ///
2170    /// * If the `spawn_blocking` task fails
2171    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    /// Removes a directory and all its contents recursively asynchronously
2184    ///
2185    /// # Errors
2186    ///
2187    /// * If underlying `std::fs::remove_dir_all` fails (when using real filesystem)
2188    ///
2189    /// # Panics
2190    ///
2191    /// * If the `spawn_blocking` task fails
2192    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    /// Canonicalizes a path asynchronously by resolving `.` and `..` components
2205    ///
2206    /// # Errors
2207    ///
2208    /// * If underlying `std::fs::canonicalize` fails (when using real filesystem)
2209    /// * If the path cannot be converted to a string
2210    /// * If the path does not exist in the simulator
2211    ///
2212    /// # Panics
2213    ///
2214    /// * If the `spawn_blocking` task fails
2215    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    /// Directory entry for asynchronous filesystem operations
2228    ///
2229    /// Represents a single entry (file or directory) when iterating over directory contents.
2230    /// Provides async methods to access the entry's path, name, type, and metadata information.
2231    pub struct DirEntry {
2232        path: PathBuf,
2233        file_name: std::ffi::OsString,
2234        file_type_info: super::FileType,
2235    }
2236
2237    impl DirEntry {
2238        /// Create a new `DirEntry` from `std::fs::DirEntry`
2239        ///
2240        /// # Errors
2241        ///
2242        /// * If the file type cannot be determined
2243        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        /// Create a new `DirEntry` for a file in the simulator
2257        ///
2258        /// # Errors
2259        ///
2260        /// * Infallible in current implementation
2261        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        /// Create a new `DirEntry` for a directory in the simulator
2274        ///
2275        /// # Errors
2276        ///
2277        /// * Infallible in current implementation
2278        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        /// Returns the full path to this entry
2291        #[must_use]
2292        pub fn path(&self) -> PathBuf {
2293            self.path.clone()
2294        }
2295
2296        /// Returns the file name of this entry
2297        #[must_use]
2298        pub fn file_name(&self) -> std::ffi::OsString {
2299            self.file_name.clone()
2300        }
2301
2302        /// Returns the file type of this entry
2303        ///
2304        /// # Errors
2305        ///
2306        /// * Infallible
2307        #[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        /// Returns metadata for this entry
2313        ///
2314        /// # Errors
2315        ///
2316        /// * If the file/directory no longer exists
2317        ///
2318        /// # Panics
2319        ///
2320        /// * If the FILES or data mutex is poisoned (when using simulator)
2321        /// * If the `spawn_blocking` task panics (when using real filesystem)
2322        #[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                // For files, get the actual size from the simulator storage
2343                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    /// Async directory reader that yields directory entries
2373    ///
2374    /// This struct is returned by [`read_dir`] and provides an async iterator
2375    /// over the entries in a directory.
2376    pub struct ReadDir {
2377        entries: std::vec::IntoIter<DirEntry>,
2378    }
2379
2380    impl ReadDir {
2381        /// Returns the next entry in the directory
2382        ///
2383        /// Returns `Ok(None)` when there are no more entries.
2384        ///
2385        /// # Errors
2386        ///
2387        /// * Infallible in simulator mode
2388        #[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    /// Returns an async iterator over the entries in a directory
2395    ///
2396    /// # Errors
2397    ///
2398    /// * If the directory does not exist
2399    /// * If the path cannot be converted to a string
2400    ///
2401    /// # Panics
2402    ///
2403    /// * If the `spawn_blocking` task fails (when using real filesystem)
2404    #[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        // Use sync implementation which properly handles simulator filesystem
2424        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    /// Read directory entries and return them sorted by filename for deterministic iteration
2440    ///
2441    /// # Errors
2442    ///
2443    /// * If the directory does not exist
2444    /// * If the path cannot be converted to a string
2445    ///
2446    /// # Panics
2447    ///
2448    /// * If the `spawn_blocking` task fails (when using real filesystem)
2449    #[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        // Use sync implementation which properly handles simulator filesystem
2466        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    /// Recursively walk directory tree and return all entries sorted by path for deterministic iteration
2478    ///
2479    /// # Errors
2480    ///
2481    /// * If the directory does not exist
2482    /// * If the path cannot be converted to a string
2483    ///
2484    /// # Panics
2485    ///
2486    /// * If the `spawn_blocking` task fails (when using real filesystem)
2487    #[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        // Use sync implementation which properly handles simulator filesystem
2504        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        // Verify that real_fs is NOT set in normal test
2584        assert!(
2585            !super::real_fs_support::is_real_fs(),
2586            "real_fs should NOT be set in normal test"
2587        );
2588
2589        // This test should use simulated filesystem
2590        let content = "test content";
2591        let path = "/simulated/path/file.txt";
2592
2593        // Create parent directory first (required by new implementation)
2594        super::sync::create_dir_all("/simulated/path").unwrap();
2595
2596        // Write to simulated filesystem
2597        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        // Read from simulated filesystem
2605        let read_content = super::sync::read_to_string(path).unwrap();
2606        assert_eq!(read_content, content);
2607
2608        // This file should not exist on real filesystem
2609        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        // Root has no parents
2668        assert!(parents.is_empty());
2669    }
2670
2671    #[test_log::test]
2672    fn test_parent_directories_preserves_order() {
2673        // Parents should be returned from root to immediate parent
2674        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        // Files/dirs in subdirectories should not appear in parent's children
2746        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        // nested.txt should NOT appear
2755        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        // Create async file with specific state
2772        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        // Position and path should be preserved
2785        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        // Convert back to sync
2790        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        // Write using async API
2811        unsync::write("/async_test/file.txt", b"async content")
2812            .await
2813            .unwrap();
2814
2815        // Read back using async API
2816        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        // Create nested directories asynchronously
2827        unsync::create_dir_all("/async_dirs/nested/deep")
2828            .await
2829            .unwrap();
2830
2831        // Verify using sync API
2832        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        // Remove using async API
2842        unsync::remove_dir_all("/to_remove").await.unwrap();
2843
2844        // Should no longer exist
2845        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        // Open with create
2865        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        // Verify content
2936        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        // Seek to position 5
2953        let pos = file.seek(std::io::SeekFrom::Start(5)).await.unwrap();
2954        assert_eq!(pos, 5);
2955
2956        // Read remaining
2957        let mut buf = [0u8; 5];
2958        file.read_exact(&mut buf).await.unwrap();
2959        assert_eq!(&buf, b"56789");
2960    }
2961}
2962
2963/// Temporary directory functionality for the simulator
2964pub 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    /// Tracking state for temp directories in simulator
2974    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    /// Reset temp directory state (useful for testing)
2984    ///
2985    /// # Panics
2986    ///
2987    /// * If the `TEMP_DIRS` `RwLock` fails to write to
2988    pub fn reset_temp_dirs() {
2989        TEMP_DIRS.with_borrow_mut(|x| x.write().unwrap().clear());
2990    }
2991
2992    /// A directory in the filesystem that is automatically deleted when it goes out of scope
2993    pub struct TempDir {
2994        path: PathBuf,
2995        cleanup_enabled: bool,
2996    }
2997
2998    impl TempDir {
2999        /// Attempts to make a temporary directory inside of the system temp directory
3000        ///
3001        /// # Errors
3002        ///
3003        /// * If the directory cannot be created in the simulated filesystem
3004        ///
3005        /// # Panics
3006        ///
3007        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3008        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); // Let our Drop handle cleanup
3014                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            // Create in simulated filesystem
3024            super::sync::create_dir_all(&path)?;
3025
3026            // Register in temp directory tracking
3027            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        /// Attempts to make a temporary directory inside the specified directory
3043        ///
3044        /// # Errors
3045        ///
3046        /// * If the directory cannot be created in the simulated filesystem
3047        ///
3048        /// # Panics
3049        ///
3050        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3051        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        /// Attempts to make a temporary directory with the specified prefix
3084        ///
3085        /// # Errors
3086        ///
3087        /// * If the directory cannot be created in the simulated filesystem
3088        ///
3089        /// # Panics
3090        ///
3091        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3092        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        /// Attempts to make a temporary directory with the specified suffix
3125        ///
3126        /// # Errors
3127        ///
3128        /// * If the directory cannot be created in the simulated filesystem
3129        ///
3130        /// # Panics
3131        ///
3132        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3133        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        /// Attempts to make a temporary directory with the specified prefix in the specified directory
3166        ///
3167        /// # Errors
3168        ///
3169        /// * If the directory cannot be created in the simulated filesystem
3170        ///
3171        /// # Panics
3172        ///
3173        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3174        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        /// Attempts to make a temporary directory with the specified suffix in the specified directory
3210        ///
3211        /// # Errors
3212        ///
3213        /// * If the directory cannot be created in the simulated filesystem
3214        ///
3215        /// # Panics
3216        ///
3217        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3218        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        /// Accesses the Path to the temporary directory
3254        #[must_use]
3255        pub fn path(&self) -> &Path {
3256            &self.path
3257        }
3258
3259        /// Persist the temporary directory to disk, returning the `PathBuf` where it is located
3260        ///
3261        /// # Panics
3262        ///
3263        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3264        #[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 alias for `keep()`
3276        ///
3277        /// # Panics
3278        ///
3279        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3280        #[deprecated = "use TempDir::keep()"]
3281        #[must_use]
3282        pub fn into_path(self) -> PathBuf {
3283            self.keep()
3284        }
3285
3286        /// Disable cleanup of the temporary directory
3287        ///
3288        /// # Panics
3289        ///
3290        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3291        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        /// Closes and removes the temporary directory
3301        ///
3302        /// # Errors
3303        ///
3304        /// * If the directory cannot be removed from the simulated filesystem
3305        ///
3306        /// # Panics
3307        ///
3308        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3309        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            // Remove from tracking
3320            TEMP_DIRS.with_borrow_mut(|dirs| {
3321                dirs.write().unwrap().remove(&self.path);
3322            });
3323
3324            // Remove from simulated filesystem
3325            super::sync::remove_dir_all(&self.path)?;
3326
3327            // Prevent double cleanup in Drop
3328            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    /// Builder for configuring temporary directory creation
3367    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        /// Create a new Builder with default settings
3381        #[must_use]
3382        pub const fn new() -> Self {
3383            Self {
3384                prefix: None,
3385                suffix: None,
3386                rand_bytes: 6,
3387            }
3388        }
3389
3390        /// Set the prefix for the temporary directory name
3391        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        /// Set the suffix for the temporary directory name
3397        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        /// Set the number of random bytes to use for the directory name
3403        pub const fn rand_bytes(&mut self, rand: usize) -> &mut Self {
3404            self.rand_bytes = rand;
3405            self
3406        }
3407
3408        /// Create a temporary directory in the default location
3409        ///
3410        /// # Errors
3411        ///
3412        /// * If the directory cannot be created in the simulated filesystem
3413        ///
3414        /// # Panics
3415        ///
3416        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3417        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        /// Create a temporary directory in the specified location
3462        ///
3463        /// # Errors
3464        ///
3465        /// * If the directory cannot be created in the simulated filesystem
3466        ///
3467        /// # Panics
3468        ///
3469        /// * If the `TEMP_DIRS` `RwLock` fails to write to
3470        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    /// Convenience function to create a temporary directory
3516    ///
3517    /// # Errors
3518    ///
3519    /// * If the directory cannot be created
3520    pub fn tempdir() -> std::io::Result<TempDir> {
3521        TempDir::new()
3522    }
3523
3524    /// Convenience function to create a temporary directory in the specified location
3525    ///
3526    /// # Errors
3527    ///
3528    /// * If the directory cannot be created
3529    pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> std::io::Result<TempDir> {
3530        TempDir::new_in(dir)
3531    }
3532
3533    /// Generate deterministic temp directory name
3534    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        // Generate deterministic random part
3546        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            // With 30 rand_bytes, it should wrap around the alphabet
3596            let name = generate_temp_name(None, None, 30);
3597            let s = name.to_str().unwrap();
3598            // First 26 chars are a-z, then wraps: 0%26=a, 1%26=b, etc.
3599            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            // Should be created in /tmp with deterministic name
3610            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            // 10 rand bytes = "abcdefghij"
3648            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            // After drop, directory should still exist
3697            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); // Re-enable cleanup
3711                temp.path().to_path_buf()
3712            };
3713
3714            // Directory should be removed after drop
3715            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            // close() should be a no-op when cleanup is disabled
3790            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            // Debug output should contain "TempDir", path, and cleanup_enabled
3814            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            // Builder::default() should be equivalent to Builder::new()
3822            let builder1 = Builder::new();
3823            let builder2 = Builder::default();
3824
3825            // Both should have same default behavior
3826            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            // Create some temp directories
3838            let _temp1 = TempDir::new().unwrap();
3839            let _temp2 = TempDir::new().unwrap();
3840
3841            // Reset should clear tracking state
3842            reset_temp_dirs();
3843
3844            // This just verifies the function doesn't panic
3845            // The actual cleanup is handled by drop
3846        }
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        // Verify essential directories are created
3860        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        // Verify root directories
3871        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        // Verify /usr subdirectories
3883        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        // Verify /var subdirectories
3890        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        // Verify user home directories
3902        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        // Verify we can list subdirectories
3955        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        // Test the From<std::fs::Metadata> implementation
4018        // We can't easily create std::fs::Metadata directly, but we can test
4019        // the Metadata struct behavior directly
4020
4021        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        // Verify entries are sorted by path
4089        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        // Should include directories and files
4112        let paths: Vec<_> = entries.iter().map(sync::DirEntry::path).collect();
4113
4114        // Verify all expected entries are present (directories + files)
4115        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        // Should include all intermediate directories and the file
4146        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        // Verify paths are sorted
4188        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        // Verify we have both files and directory
4222        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        // Should be sorted alphabetically
4247        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        // Should only include direct children, not nested
4278        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        // Collect all entries
4309        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        // Should return None immediately for empty directory
4325        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        // Find file and directory entries
4338        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        // Test file_type method
4345        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        // Should include all entries (directories and files)
4401        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            // Each entry should have exactly one type set
4468            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        // Verify it returns an OpenOptions by chaining methods
4551        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        // Single dot should be removed
4563        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        // Double dots should go up one directory
4572        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        // Double dots at root should be ignored for absolute paths
4580        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        // Trailing slashes should be handled (empty components ignored)
4588        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        // For relative paths, .. at the start should be preserved
4603        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        // Double dots that can't go further up should be preserved for relative paths
4612        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        // Empty path should become "."
4619        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        // Complex combinations of . and ..
4630        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        // Path with . and .. should be normalized
4665        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        // Even if we try to go above root with .., the result should stay at root level
4702        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        // Parent doesn't exist
4735        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        // /a/b doesn't exist, so creating /a/b/c should fail
4746        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        // Creating root should work (no parent check needed)
4756        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        // Should normalize and create the directory
4767        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        // Creating the same directory twice should work
4776        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        // Path that can't be converted to str should return false
4840        // (Though this is hard to test directly since most paths are valid utf-8)
4841        // We'll just verify that an empty path returns false
4842        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}