Skip to main content

azul_layout/
file.rs

1//! File system operations module for C API
2//!
3//! Provides C-compatible wrappers around Rust's std::fs API.
4//! This allows C code to use Rust's file operations without importing stdio.h.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8use core::fmt;
9use azul_css::{AzString, U8Vec, EmptyStruct, impl_result, impl_result_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut, impl_option, impl_option_inner};
10
11#[cfg(feature = "std")]
12use std::path::Path;
13
14#[cfg(feature = "std")]
15fn path_to_azstring(p: impl AsRef<Path>) -> AzString {
16    AzString::from(p.as_ref().to_string_lossy().into_owned())
17}
18
19// ============================================================================
20// Error types
21// ============================================================================
22
23/// Error when performing file operations
24#[derive(Debug, Clone, PartialEq, Eq)]
25#[repr(C)]
26pub struct FileError {
27    /// Error message
28    pub message: AzString,
29    /// Error kind
30    pub kind: FileErrorKind,
31}
32
33/// Kind of file error
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(C)]
36pub enum FileErrorKind {
37    /// File or directory not found
38    NotFound,
39    /// Permission denied
40    PermissionDenied,
41    /// File already exists
42    AlreadyExists,
43    /// Invalid path
44    InvalidPath,
45    /// I/O error
46    IoError,
47    /// Directory not empty
48    DirectoryNotEmpty,
49    /// Is a directory (expected file)
50    IsDirectory,
51    /// Is a file (expected directory)
52    IsFile,
53    /// Other error
54    Other,
55}
56
57impl FileError {
58    pub fn new(kind: FileErrorKind, message: impl Into<String>) -> Self {
59        Self {
60            message: AzString::from(message.into()),
61            kind,
62        }
63    }
64    
65    #[cfg(feature = "std")]
66    // taken by value so it can be used directly as `.map_err(FileError::from_io_error)`
67    // (map_err yields an owned io::Error); a &-param would break every such call site.
68    #[allow(clippy::needless_pass_by_value)]
69    #[must_use] pub fn from_io_error(e: std::io::Error) -> Self {
70        use std::io::ErrorKind;
71        
72        let kind = match e.kind() {
73            ErrorKind::NotFound => FileErrorKind::NotFound,
74            ErrorKind::PermissionDenied => FileErrorKind::PermissionDenied,
75            ErrorKind::AlreadyExists => FileErrorKind::AlreadyExists,
76            ErrorKind::IsADirectory => FileErrorKind::IsDirectory,
77            ErrorKind::DirectoryNotEmpty => FileErrorKind::DirectoryNotEmpty,
78            _ => FileErrorKind::IoError,
79        };
80        
81        Self {
82            message: AzString::from(e.to_string()),
83            kind,
84        }
85    }
86}
87
88impl fmt::Display for FileError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(f, "{}", self.message.as_str())
91    }
92}
93
94#[cfg(feature = "std")]
95impl std::error::Error for FileError {}
96
97// FFI-safe Result types for file operations
98impl_result!(
99    EmptyStruct,
100    FileError,
101    ResultEmptyStructFileError,
102    copy = false,
103    [Debug, Clone, PartialEq, Eq]
104);
105
106impl_result!(
107    U8Vec,
108    FileError,
109    ResultU8VecFileError,
110    copy = false,
111    [Debug, Clone, PartialEq, Eq]
112);
113
114impl_result!(
115    AzString,
116    FileError,
117    ResultStringFileError,
118    copy = false,
119    [Debug, Clone, PartialEq, Eq]
120);
121
122impl_result!(
123    u64,
124    FileError,
125    Resultu64FileError,
126    copy = false,
127    [Debug, Clone, PartialEq, Eq]
128);
129
130// Forward declarations for result types that need later-defined types
131// (FilePath, FileMetadata, DirEntryVec are defined below)
132
133// ============================================================================
134// File metadata
135// ============================================================================
136
137/// File type (file, directory, symlink)
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[repr(C)]
140pub enum FileType {
141    /// Regular file
142    File,
143    /// Directory
144    Directory,
145    /// Symbolic link
146    Symlink,
147    /// Other (device, socket, etc.)
148    Other,
149}
150
151/// Metadata about a file
152#[derive(Copy, Debug, Clone, PartialEq, Eq)]
153#[repr(C)]
154pub struct FileMetadata {
155    /// File size in bytes
156    pub size: u64,
157    /// File type
158    pub file_type: FileType,
159    /// Is read-only
160    pub is_readonly: bool,
161    /// Last modification time (Unix timestamp in seconds)
162    pub modified_secs: u64,
163    /// Creation time (Unix timestamp in seconds, 0 if unavailable)
164    pub created_secs: u64,
165}
166
167/// A directory entry
168#[derive(Debug, Clone)]
169#[repr(C)]
170pub struct DirEntry {
171    /// File name (not full path)
172    pub name: AzString,
173    /// Full path
174    pub path: AzString,
175    /// File type
176    pub file_type: FileType,
177}
178
179/// Vec of DirEntry
180impl_option!(DirEntry, OptionDirEntry, copy = false, [Debug, Clone]);
181impl_vec!(DirEntry, DirEntryVec, DirEntryVecDestructor, DirEntryVecDestructorType, DirEntryVecSlice, OptionDirEntry);
182impl_vec_clone!(DirEntry, DirEntryVec, DirEntryVecDestructor);
183impl_vec_debug!(DirEntry, DirEntryVec);
184impl_vec_mut!(DirEntry, DirEntryVec);
185
186// Additional FFI-safe Result types for complex types
187impl_result!(
188    FileMetadata,
189    FileError,
190    ResultFileMetadataFileError,
191    copy = false,
192    [Debug, Clone, PartialEq, Eq]
193);
194
195impl_result!(
196    DirEntryVec,
197    FileError,
198    ResultDirEntryVecFileError,
199    copy = false,
200    clone = false,
201    [Debug, Clone]
202);
203
204// ============================================================================
205// File operations
206// ============================================================================
207
208/// Read a file to bytes
209#[cfg(feature = "std")]
210/// # Errors
211///
212/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
213pub fn file_read(path: &str) -> Result<U8Vec, FileError> {
214    let data = std::fs::read(path)
215        .map_err(FileError::from_io_error)?;
216    Ok(U8Vec::from(data))
217}
218
219/// Read a file to string (UTF-8)
220#[cfg(feature = "std")]
221/// # Errors
222///
223/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
224pub fn file_read_string(path: &str) -> Result<AzString, FileError> {
225    let data = std::fs::read_to_string(path)
226        .map_err(FileError::from_io_error)?;
227    Ok(AzString::from(data))
228}
229
230/// Write bytes to a file (creates or overwrites)
231#[cfg(feature = "std")]
232/// # Errors
233///
234/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
235pub fn file_write(path: &str, data: &[u8]) -> Result<EmptyStruct, FileError> {
236    std::fs::write(path, data)
237        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
238}
239
240/// Write string to a file (creates or overwrites)
241#[cfg(feature = "std")]
242/// # Errors
243///
244/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
245pub fn file_write_string(path: &str, data: &str) -> Result<EmptyStruct, FileError> {
246    std::fs::write(path, data.as_bytes())
247        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
248}
249
250/// Append bytes to a file
251#[cfg(feature = "std")]
252/// # Errors
253///
254/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
255pub fn file_append(path: &str, data: &[u8]) -> Result<EmptyStruct, FileError> {
256    use std::fs::OpenOptions;
257    use std::io::Write;
258    
259    let mut file = OpenOptions::new()
260        .create(true)
261        .append(true)
262        .open(path)
263        .map_err(FileError::from_io_error)?;
264    
265    file.write_all(data)
266        .map(|()| EmptyStruct::default())
267        .map_err(FileError::from_io_error)
268}
269
270/// Copy a file
271#[cfg(feature = "std")]
272/// # Errors
273///
274/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
275pub fn file_copy(from: &str, to: &str) -> Result<u64, FileError> {
276    std::fs::copy(from, to)
277        .map_err(FileError::from_io_error)
278}
279
280/// Rename/move a file
281#[cfg(feature = "std")]
282/// # Errors
283///
284/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
285pub fn file_rename(from: &str, to: &str) -> Result<EmptyStruct, FileError> {
286    std::fs::rename(from, to)
287        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
288}
289
290/// Delete a file
291#[cfg(feature = "std")]
292/// # Errors
293///
294/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
295pub fn file_delete(path: &str) -> Result<EmptyStruct, FileError> {
296    std::fs::remove_file(path)
297        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
298}
299
300/// Check if a file or directory exists
301#[cfg(feature = "std")]
302#[must_use] pub fn path_exists(path: &str) -> bool {
303    Path::new(path).exists()
304}
305
306/// Check if path is a file
307#[cfg(feature = "std")]
308#[must_use] pub fn path_is_file(path: &str) -> bool {
309    Path::new(path).is_file()
310}
311
312/// Check if path is a directory
313#[cfg(feature = "std")]
314#[must_use] pub fn path_is_dir(path: &str) -> bool {
315    Path::new(path).is_dir()
316}
317
318/// Get file metadata
319#[cfg(feature = "std")]
320/// # Errors
321///
322/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
323pub fn file_metadata(path: &str) -> Result<FileMetadata, FileError> {
324    let meta = std::fs::symlink_metadata(path)
325        .map_err(FileError::from_io_error)?;
326    
327    let file_type = if meta.is_file() {
328        FileType::File
329    } else if meta.is_dir() {
330        FileType::Directory
331    } else if meta.is_symlink() {
332        FileType::Symlink
333    } else {
334        FileType::Other
335    };
336    
337    let modified_secs = meta.modified()
338        .ok()
339        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
340        .map_or(0, |d| d.as_secs());
341    
342    let created_secs = meta.created()
343        .ok()
344        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
345        .map_or(0, |d| d.as_secs());
346    
347    Ok(FileMetadata {
348        size: meta.len(),
349        file_type,
350        is_readonly: meta.permissions().readonly(),
351        modified_secs,
352        created_secs,
353    })
354}
355
356// ----------------------------------------------------------------------------
357// no_std stubs: keep the file API surface present without std::fs.
358// ----------------------------------------------------------------------------
359
360/// Error returned by file stubs when the `std` feature is disabled.
361#[cfg(not(feature = "std"))]
362fn no_std_file_error() -> FileError {
363    FileError::new(FileErrorKind::Other, "file IO requires the `std` feature")
364}
365
366/// Stub: `std` feature disabled.
367#[cfg(not(feature = "std"))]
368pub fn file_read(_path: &str) -> Result<U8Vec, FileError> {
369    Err(no_std_file_error())
370}
371
372/// Stub: `std` feature disabled.
373#[cfg(not(feature = "std"))]
374pub fn file_read_string(_path: &str) -> Result<AzString, FileError> {
375    Err(no_std_file_error())
376}
377
378/// Stub: `std` feature disabled.
379#[cfg(not(feature = "std"))]
380pub fn file_write(_path: &str, _data: &[u8]) -> Result<EmptyStruct, FileError> {
381    Err(no_std_file_error())
382}
383
384/// Stub: `std` feature disabled.
385#[cfg(not(feature = "std"))]
386pub fn file_write_string(_path: &str, _data: &str) -> Result<EmptyStruct, FileError> {
387    Err(no_std_file_error())
388}
389
390/// Stub: `std` feature disabled.
391#[cfg(not(feature = "std"))]
392pub fn file_append(_path: &str, _data: &[u8]) -> Result<EmptyStruct, FileError> {
393    Err(no_std_file_error())
394}
395
396/// Stub: `std` feature disabled.
397#[cfg(not(feature = "std"))]
398pub fn file_copy(_from: &str, _to: &str) -> Result<u64, FileError> {
399    Err(no_std_file_error())
400}
401
402/// Stub: `std` feature disabled.
403#[cfg(not(feature = "std"))]
404pub fn file_rename(_from: &str, _to: &str) -> Result<EmptyStruct, FileError> {
405    Err(no_std_file_error())
406}
407
408/// Stub: `std` feature disabled.
409#[cfg(not(feature = "std"))]
410pub fn file_delete(_path: &str) -> Result<EmptyStruct, FileError> {
411    Err(no_std_file_error())
412}
413
414/// Stub: `std` feature disabled.
415#[cfg(not(feature = "std"))]
416pub fn path_exists(_path: &str) -> bool {
417    false
418}
419
420/// Stub: `std` feature disabled.
421#[cfg(not(feature = "std"))]
422pub fn path_is_file(_path: &str) -> bool {
423    false
424}
425
426/// Stub: `std` feature disabled.
427#[cfg(not(feature = "std"))]
428pub fn path_is_dir(_path: &str) -> bool {
429    false
430}
431
432/// Stub: `std` feature disabled.
433#[cfg(not(feature = "std"))]
434pub fn file_metadata(_path: &str) -> Result<FileMetadata, FileError> {
435    Err(no_std_file_error())
436}
437
438// ============================================================================
439// Directory operations
440// ============================================================================
441
442/// Create a directory
443#[cfg(feature = "std")]
444/// # Errors
445///
446/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
447pub fn dir_create(path: &str) -> Result<EmptyStruct, FileError> {
448    std::fs::create_dir(path)
449        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
450}
451
452/// Create a directory and all parent directories
453#[cfg(feature = "std")]
454/// # Errors
455///
456/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
457pub fn dir_create_all(path: &str) -> Result<EmptyStruct, FileError> {
458    std::fs::create_dir_all(path)
459        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
460}
461
462/// Delete an empty directory
463#[cfg(feature = "std")]
464/// # Errors
465///
466/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
467pub fn dir_delete(path: &str) -> Result<EmptyStruct, FileError> {
468    std::fs::remove_dir(path)
469        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
470}
471
472/// Delete a directory and all its contents
473#[cfg(feature = "std")]
474/// # Errors
475///
476/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
477pub fn dir_delete_all(path: &str) -> Result<EmptyStruct, FileError> {
478    std::fs::remove_dir_all(path)
479        .map(|()| EmptyStruct::default()).map_err(FileError::from_io_error)
480}
481
482/// List directory contents
483#[cfg(feature = "std")]
484/// # Errors
485///
486/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
487pub fn dir_list(path: &str) -> Result<DirEntryVec, FileError> {
488    let entries = std::fs::read_dir(path)
489        .map_err(FileError::from_io_error)?;
490    
491    let mut result = Vec::new();
492    
493    for entry in entries {
494        let entry = entry.map_err(FileError::from_io_error)?;
495        let file_type = entry.file_type()
496            .map(|ft| {
497                if ft.is_file() {
498                    FileType::File
499                } else if ft.is_dir() {
500                    FileType::Directory
501                } else if ft.is_symlink() {
502                    FileType::Symlink
503                } else {
504                    FileType::Other
505                }
506            })
507            .unwrap_or(FileType::Other);
508        
509        result.push(DirEntry {
510            name: path_to_azstring(entry.file_name()),
511            path: path_to_azstring(entry.path()),
512            file_type,
513        });
514    }
515    
516    Ok(DirEntryVec::from_vec(result))
517}
518
519/// Stub: `std` feature disabled.
520#[cfg(not(feature = "std"))]
521pub fn dir_create(_path: &str) -> Result<EmptyStruct, FileError> {
522    Err(no_std_file_error())
523}
524
525/// Stub: `std` feature disabled.
526#[cfg(not(feature = "std"))]
527pub fn dir_create_all(_path: &str) -> Result<EmptyStruct, FileError> {
528    Err(no_std_file_error())
529}
530
531/// Stub: `std` feature disabled.
532#[cfg(not(feature = "std"))]
533pub fn dir_delete(_path: &str) -> Result<EmptyStruct, FileError> {
534    Err(no_std_file_error())
535}
536
537/// Stub: `std` feature disabled.
538#[cfg(not(feature = "std"))]
539pub fn dir_delete_all(_path: &str) -> Result<EmptyStruct, FileError> {
540    Err(no_std_file_error())
541}
542
543/// Stub: `std` feature disabled.
544#[cfg(not(feature = "std"))]
545pub fn dir_list(_path: &str) -> Result<DirEntryVec, FileError> {
546    Err(no_std_file_error())
547}
548
549// ============================================================================
550// Path operations
551// ============================================================================
552
553/// Join two paths
554#[cfg(feature = "std")]
555#[must_use] pub fn path_join(base: &str, path: &str) -> AzString {
556    let joined = Path::new(base).join(path);
557    path_to_azstring(joined)
558}
559
560/// Get the parent directory of a path
561#[cfg(feature = "std")]
562pub fn path_parent(path: &str) -> Option<AzString> {
563    Path::new(path).parent()
564        .map(path_to_azstring)
565}
566
567/// Get the file name from a path
568#[cfg(feature = "std")]
569pub fn path_file_name(path: &str) -> Option<AzString> {
570    Path::new(path).file_name()
571        .map(path_to_azstring)
572}
573
574/// Get the file extension from a path
575#[cfg(feature = "std")]
576pub fn path_extension(path: &str) -> Option<AzString> {
577    Path::new(path).extension()
578        .map(path_to_azstring)
579}
580
581/// Canonicalize a path (resolve symlinks, make absolute)
582#[cfg(feature = "std")]
583/// # Errors
584///
585/// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
586pub fn path_canonicalize(path: &str) -> Result<AzString, FileError> {
587    let canonical = std::fs::canonicalize(path)
588        .map_err(FileError::from_io_error)?;
589    Ok(path_to_azstring(canonical))
590}
591
592// ============================================================================
593// Temporary files
594// ============================================================================
595
596/// Get the system temporary directory
597#[cfg(feature = "std")]
598#[must_use] pub fn temp_dir() -> AzString {
599    path_to_azstring(std::env::temp_dir())
600}
601
602/// Stub: `std` feature disabled.
603#[cfg(not(feature = "std"))]
604pub fn path_join(_base: &str, _path: &str) -> AzString {
605    AzString::from_const_str("")
606}
607
608/// Stub: `std` feature disabled.
609#[cfg(not(feature = "std"))]
610pub fn path_parent(_path: &str) -> Option<AzString> {
611    None
612}
613
614/// Stub: `std` feature disabled.
615#[cfg(not(feature = "std"))]
616pub fn path_file_name(_path: &str) -> Option<AzString> {
617    None
618}
619
620/// Stub: `std` feature disabled.
621#[cfg(not(feature = "std"))]
622pub fn path_extension(_path: &str) -> Option<AzString> {
623    None
624}
625
626/// Stub: `std` feature disabled.
627#[cfg(not(feature = "std"))]
628pub fn path_canonicalize(_path: &str) -> Result<AzString, FileError> {
629    Err(no_std_file_error())
630}
631
632/// Stub: `std` feature disabled.
633#[cfg(not(feature = "std"))]
634pub fn temp_dir() -> AzString {
635    AzString::from_const_str("")
636}
637
638// ============================================================================
639// OOP-style Path wrapper
640// ============================================================================
641
642/// FFI-safe path type with OOP-style methods
643/// 
644/// This wraps a string path and provides method-based access to file operations.
645#[derive(Debug, Clone, PartialEq, Eq, Hash)]
646#[repr(C)]
647pub struct FilePath {
648    pub inner: AzString,
649}
650
651// Result type for FilePath operations (must be after FilePath definition)
652impl_result!(
653    FilePath,
654    FileError,
655    ResultFilePathFileError,
656    copy = false,
657    [Debug, Clone, PartialEq, Eq]
658);
659
660// Option type for FilePath
661impl_option!(FilePath, OptionFilePath, copy = false, [Clone, Debug, PartialEq, Eq]);
662
663impl Default for FilePath {
664    fn default() -> Self {
665        Self { inner: AzString::from_const_str("") }
666    }
667}
668
669impl FilePath {
670    /// Creates a new path from a string
671    #[must_use] pub const fn new(path: AzString) -> Self {
672        Self { inner: path }
673    }
674
675    /// Creates an empty path
676    #[must_use] pub fn empty() -> Self {
677        Self::default()
678    }
679
680    /// Creates a path from a string slice
681    #[must_use] pub fn from_str(s: &str) -> Self {
682        Self { inner: AzString::from(String::from(s)) }
683    }
684
685    /// Returns the system temporary directory
686    #[cfg(feature = "std")]
687    #[must_use] pub fn get_temp_dir() -> Self {
688        Self { inner: temp_dir() }
689    }
690
691    /// Returns the current working directory
692    #[cfg(feature = "std")]
693    /// # Errors
694    ///
695    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
696    pub fn get_current_dir() -> Result<Self, FileError> {
697        match std::env::current_dir() {
698            Ok(p) => Ok(Self { inner: path_to_azstring(p) }),
699            Err(e) => Err(FileError::from_io_error(e)),
700        }
701    }
702
703    /// Returns the user's home directory (e.g., /home/username on Linux, C:\Users\username on Windows)
704    #[cfg(all(feature = "std", feature = "extra"))]
705    #[must_use] pub fn get_home_dir() -> Option<Self> {
706        dirs::home_dir().map(|p| Self { inner: path_to_azstring(p) })
707    }
708
709    /// Returns the user's cache directory (e.g., ~/.cache on Linux, ~/Library/Caches on macOS)
710    #[cfg(all(feature = "std", feature = "extra"))]
711    #[must_use] pub fn get_cache_dir() -> Option<Self> {
712        dirs::cache_dir().map(|p| Self { inner: path_to_azstring(p) })
713    }
714
715    /// Returns the user's config directory (e.g., ~/.config on Linux, ~/Library/Application Support on macOS)
716    #[cfg(all(feature = "std", feature = "extra"))]
717    #[must_use] pub fn get_config_dir() -> Option<Self> {
718        dirs::config_dir().map(|p| Self { inner: path_to_azstring(p) })
719    }
720
721    /// Returns the user's local config directory (e.g., ~/.config on Linux, ~/Library/Application Support on macOS)
722    #[cfg(all(feature = "std", feature = "extra"))]
723    #[must_use] pub fn get_config_local_dir() -> Option<Self> {
724        dirs::config_local_dir().map(|p| Self { inner: path_to_azstring(p) })
725    }
726
727    /// Returns the user's data directory (e.g., ~/.local/share on Linux, ~/Library/Application Support on macOS)
728    #[cfg(all(feature = "std", feature = "extra"))]
729    #[must_use] pub fn get_data_dir() -> Option<Self> {
730        dirs::data_dir().map(|p| Self { inner: path_to_azstring(p) })
731    }
732
733    /// Returns the user's local data directory (e.g., ~/.local/share on Linux, ~/Library/Application Support on macOS)
734    #[cfg(all(feature = "std", feature = "extra"))]
735    #[must_use] pub fn get_data_local_dir() -> Option<Self> {
736        dirs::data_local_dir().map(|p| Self { inner: path_to_azstring(p) })
737    }
738
739    /// Returns the user's desktop directory (e.g., ~/Desktop)
740    #[cfg(all(feature = "std", feature = "extra"))]
741    #[must_use] pub fn get_desktop_dir() -> Option<Self> {
742        dirs::desktop_dir().map(|p| Self { inner: path_to_azstring(p) })
743    }
744
745    /// Returns the user's documents directory (e.g., ~/Documents)
746    #[cfg(all(feature = "std", feature = "extra"))]
747    #[must_use] pub fn get_document_dir() -> Option<Self> {
748        dirs::document_dir().map(|p| Self { inner: path_to_azstring(p) })
749    }
750
751    /// Returns the user's downloads directory (e.g., ~/Downloads)
752    #[cfg(all(feature = "std", feature = "extra"))]
753    #[must_use] pub fn get_download_dir() -> Option<Self> {
754        dirs::download_dir().map(|p| Self { inner: path_to_azstring(p) })
755    }
756
757    /// Returns the user's executable directory (e.g., ~/.local/bin on Linux)
758    #[cfg(all(feature = "std", feature = "extra"))]
759    #[must_use] pub fn get_executable_dir() -> Option<Self> {
760        dirs::executable_dir().map(|p| Self { inner: path_to_azstring(p) })
761    }
762
763    /// Returns the user's font directory (e.g., ~/.local/share/fonts on Linux, ~/Library/Fonts on macOS)
764    #[cfg(all(feature = "std", feature = "extra"))]
765    #[must_use] pub fn get_font_dir() -> Option<Self> {
766        dirs::font_dir().map(|p| Self { inner: path_to_azstring(p) })
767    }
768
769    /// Returns the user's pictures directory (e.g., ~/Pictures)
770    #[cfg(all(feature = "std", feature = "extra"))]
771    #[must_use] pub fn get_picture_dir() -> Option<Self> {
772        dirs::picture_dir().map(|p| Self { inner: path_to_azstring(p) })
773    }
774
775    /// Returns the user's preference directory (e.g., ~/.config on Linux, ~/Library/Preferences on macOS)
776    #[cfg(all(feature = "std", feature = "extra"))]
777    #[must_use] pub fn get_preference_dir() -> Option<Self> {
778        dirs::preference_dir().map(|p| Self { inner: path_to_azstring(p) })
779    }
780
781    /// Returns the user's public directory (e.g., ~/Public)
782    #[cfg(all(feature = "std", feature = "extra"))]
783    #[must_use] pub fn get_public_dir() -> Option<Self> {
784        dirs::public_dir().map(|p| Self { inner: path_to_azstring(p) })
785    }
786
787    /// Returns the user's runtime directory (e.g., /run/user/1000 on Linux)
788    #[cfg(all(feature = "std", feature = "extra"))]
789    #[must_use] pub fn get_runtime_dir() -> Option<Self> {
790        dirs::runtime_dir().map(|p| Self { inner: path_to_azstring(p) })
791    }
792
793    /// Returns the user's state directory (e.g., ~/.local/state on Linux)
794    #[cfg(all(feature = "std", feature = "extra"))]
795    #[must_use] pub fn get_state_dir() -> Option<Self> {
796        dirs::state_dir().map(|p| Self { inner: path_to_azstring(p) })
797    }
798
799    /// Returns the user's audio directory (e.g., ~/Music)
800    #[cfg(all(feature = "std", feature = "extra"))]
801    #[must_use] pub fn get_audio_dir() -> Option<Self> {
802        dirs::audio_dir().map(|p| Self { inner: path_to_azstring(p) })
803    }
804
805    /// Returns the user's video directory (e.g., ~/Videos)
806    #[cfg(all(feature = "std", feature = "extra"))]
807    #[must_use] pub fn get_video_dir() -> Option<Self> {
808        dirs::video_dir().map(|p| Self { inner: path_to_azstring(p) })
809    }
810
811    /// Returns the user's templates directory
812    #[cfg(all(feature = "std", feature = "extra"))]
813    #[must_use] pub fn get_template_dir() -> Option<Self> {
814        dirs::template_dir().map(|p| Self { inner: path_to_azstring(p) })
815    }
816
817    /// Joins this path with another path component
818    #[cfg(feature = "std")]
819    #[must_use] pub fn join(&self, other: &Self) -> Self {
820        Self { inner: path_join(self.inner.as_str(), other.inner.as_str()) }
821    }
822
823    /// Joins this path with a string component
824    #[cfg(feature = "std")]
825    #[must_use] pub fn join_str(&self, component: &AzString) -> Self {
826        Self { inner: path_join(self.inner.as_str(), component.as_str()) }
827    }
828
829    /// Returns the parent directory of this path
830    #[cfg(feature = "std")]
831    #[must_use] pub fn parent(&self) -> Option<Self> {
832        path_parent(self.inner.as_str()).map(|p| Self { inner: p })
833    }
834
835    /// Returns the file name component of this path
836    #[cfg(feature = "std")]
837    #[must_use] pub fn file_name(&self) -> Option<AzString> {
838        path_file_name(self.inner.as_str())
839    }
840
841    /// Returns the file extension of this path
842    #[cfg(feature = "std")]
843    #[must_use] pub fn extension(&self) -> Option<AzString> {
844        path_extension(self.inner.as_str())
845    }
846
847    /// Checks if the path exists on the filesystem
848    #[cfg(feature = "std")]
849    #[must_use] pub fn exists(&self) -> bool {
850        path_exists(self.inner.as_str())
851    }
852
853    /// Checks if the path is a file
854    #[cfg(feature = "std")]
855    #[must_use] pub fn is_file(&self) -> bool {
856        path_is_file(self.inner.as_str())
857    }
858
859    /// Checks if the path is a directory
860    #[cfg(feature = "std")]
861    #[must_use] pub fn is_dir(&self) -> bool {
862        path_is_dir(self.inner.as_str())
863    }
864
865    /// Checks if the path is absolute
866    #[cfg(feature = "std")]
867    #[must_use] pub fn is_absolute(&self) -> bool {
868        Path::new(self.inner.as_str()).is_absolute()
869    }
870
871    /// Creates this directory and all parent directories
872    #[cfg(feature = "std")]
873    /// # Errors
874    ///
875    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
876    pub fn create_dir_all(&self) -> Result<EmptyStruct, FileError> {
877        dir_create_all(self.inner.as_str())
878    }
879
880    /// Creates this directory (parent must exist)
881    #[cfg(feature = "std")]
882    /// # Errors
883    ///
884    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
885    pub fn create_dir(&self) -> Result<EmptyStruct, FileError> {
886        dir_create(self.inner.as_str())
887    }
888
889    /// Removes this file
890    #[cfg(feature = "std")]
891    /// # Errors
892    ///
893    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
894    pub fn remove_file(&self) -> Result<EmptyStruct, FileError> {
895        file_delete(self.inner.as_str())
896    }
897
898    /// Removes this directory (must be empty)
899    #[cfg(feature = "std")]
900    /// # Errors
901    ///
902    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
903    pub fn remove_dir(&self) -> Result<EmptyStruct, FileError> {
904        dir_delete(self.inner.as_str())
905    }
906
907    /// Removes this directory and all contents
908    #[cfg(feature = "std")]
909    /// # Errors
910    ///
911    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
912    pub fn remove_dir_all(&self) -> Result<EmptyStruct, FileError> {
913        dir_delete_all(self.inner.as_str())
914    }
915
916    /// Reads the entire file at this path as bytes
917    #[cfg(feature = "std")]
918    /// # Errors
919    ///
920    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
921    pub fn read_bytes(&self) -> Result<U8Vec, FileError> {
922        file_read(self.inner.as_str())
923    }
924
925    /// Reads the entire file at this path as a string
926    #[cfg(feature = "std")]
927    /// # Errors
928    ///
929    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
930    pub fn read_string(&self) -> Result<AzString, FileError> {
931        file_read_string(self.inner.as_str())
932    }
933
934    /// Writes bytes to the file at this path
935    #[cfg(feature = "std")]
936    /// # Errors
937    ///
938    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
939    pub fn write_bytes(&self, data: &U8Vec) -> Result<EmptyStruct, FileError> {
940        file_write(self.inner.as_str(), data.as_ref())
941    }
942
943    /// Writes a string to the file at this path
944    #[cfg(feature = "std")]
945    /// # Errors
946    ///
947    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
948    pub fn write_string(&self, data: &AzString) -> Result<EmptyStruct, FileError> {
949        file_write_string(self.inner.as_str(), data.as_str())
950    }
951
952    /// Copies a file from this path to another path
953    #[cfg(feature = "std")]
954    /// # Errors
955    ///
956    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
957    pub fn copy_to(&self, dest: &Self) -> Result<u64, FileError> {
958        file_copy(self.inner.as_str(), dest.inner.as_str())
959    }
960
961    /// Renames/moves a file from this path to another path
962    #[cfg(feature = "std")]
963    /// # Errors
964    ///
965    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
966    pub fn rename_to(&self, dest: &Self) -> Result<EmptyStruct, FileError> {
967        file_rename(self.inner.as_str(), dest.inner.as_str())
968    }
969
970    /// Returns the path as a string reference
971    #[must_use] pub fn as_str(&self) -> &str {
972        self.inner.as_str()
973    }
974
975    /// Returns the path as an `AzString`
976    #[must_use] pub fn as_string(&self) -> AzString {
977        self.inner.clone()
978    }
979
980    /// Lists directory contents
981    #[cfg(feature = "std")]
982    /// # Errors
983    ///
984    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
985    pub fn read_dir(&self) -> Result<DirEntryVec, FileError> {
986        dir_list(self.inner.as_str())
987    }
988
989    /// Returns metadata about the file/directory
990    #[cfg(feature = "std")]
991    /// # Errors
992    ///
993    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
994    pub fn metadata(&self) -> Result<FileMetadata, FileError> {
995        file_metadata(self.inner.as_str())
996    }
997
998    /// Makes the path canonical (absolute, with no `.` or `..` components)
999    #[cfg(feature = "std")]
1000    /// # Errors
1001    ///
1002    /// Returns a `FileError` if the filesystem operation fails (e.g. path not found, permission denied, or an I/O error).
1003    pub fn canonicalize(&self) -> Result<Self, FileError> {
1004        path_canonicalize(self.inner.as_str()).map(|p| Self { inner: p })
1005    }
1006}
1007
1008impl From<String> for FilePath {
1009    fn from(s: String) -> Self {
1010        Self { inner: AzString::from(s) }
1011    }
1012}
1013
1014impl From<&str> for FilePath {
1015    fn from(s: &str) -> Self {
1016        Self { inner: AzString::from(String::from(s)) }
1017    }
1018}
1019
1020impl From<AzString> for FilePath {
1021    fn from(s: AzString) -> Self {
1022        Self { inner: s }
1023    }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028    use super::*;
1029    
1030    #[test]
1031    #[cfg(feature = "std")]
1032    fn test_temp_dir() {
1033        let temp = temp_dir();
1034        assert!(!temp.as_str().is_empty());
1035    }
1036    
1037    #[test]
1038    #[cfg(feature = "std")]
1039    fn test_path_join() {
1040        let joined = path_join("/home/user", "file.txt");
1041        assert!(joined.as_str().contains("file.txt"));
1042    }
1043}
1044
1045#[cfg(test)]
1046#[allow(
1047    clippy::too_many_lines,
1048    clippy::cast_possible_truncation,
1049    clippy::redundant_clone,
1050    clippy::similar_names,
1051    clippy::case_sensitive_file_extension_comparisons
1052)]
1053mod autotest_generated {
1054    use alloc::string::ToString;
1055
1056    use super::*;
1057
1058    // ------------------------------------------------------------------
1059    // Harness: every test gets its own directory under the system temp dir,
1060    // removed on drop. Tests run in parallel in one process, so names must
1061    // not collide.
1062    // ------------------------------------------------------------------
1063
1064    #[cfg(feature = "std")]
1065    #[derive(Debug)]
1066    struct CaseDir(String);
1067
1068    #[cfg(feature = "std")]
1069    impl CaseDir {
1070        fn new(tag: &str) -> Self {
1071            use core::sync::atomic::{AtomicUsize, Ordering};
1072            static COUNTER: AtomicUsize = AtomicUsize::new(0);
1073            let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1074            let name = format!("azul_autotest_file_{}_{}_{}", std::process::id(), tag, n);
1075            let dir = path_join(temp_dir().as_str(), &name).as_str().to_string();
1076            let _ = dir_delete_all(&dir);
1077            assert!(dir_create_all(&dir).is_ok(), "could not create case dir {dir}");
1078            Self(dir)
1079        }
1080
1081        fn path(&self) -> &str {
1082            &self.0
1083        }
1084
1085        fn child(&self, name: &str) -> String {
1086            path_join(&self.0, name).as_str().to_string()
1087        }
1088    }
1089
1090    #[cfg(feature = "std")]
1091    impl Drop for CaseDir {
1092        fn drop(&mut self) {
1093            let _ = dir_delete_all(&self.0);
1094        }
1095    }
1096
1097    /// Paths that must never panic and never resolve to anything on disk.
1098    ///
1099    /// Deliberately excludes `"../".repeat(n)` and `"/".repeat(n)`: on unix both
1100    /// resolve to the root directory, so they *do* exist. See
1101    /// `traversal_above_root_and_repeated_slashes_resolve_to_root`.
1102    #[cfg(feature = "std")]
1103    fn hostile_paths() -> Vec<String> {
1104        vec![
1105            String::new(),
1106            "   ".to_string(),
1107            "\t\n".to_string(),
1108            "\0".to_string(),
1109            "a\0b".to_string(),
1110            "valid;garbage".to_string(),
1111            "0".to_string(),
1112            "-0".to_string(),
1113            i64::MAX.to_string(),
1114            i64::MIN.to_string(),
1115            "NaN".to_string(),
1116            "inf".to_string(),
1117            "\u{1F600}".to_string(),
1118            "e\u{301}\u{327}".to_string(),
1119            "x".repeat(100_000),
1120            "[".repeat(10_000),
1121        ]
1122    }
1123
1124    #[cfg(feature = "std")]
1125    fn as_opt_string(s: Option<AzString>) -> Option<String> {
1126        s.map(|x| x.as_str().to_string())
1127    }
1128
1129    // ==================================================================
1130    // path_to_azstring (private)
1131    // ==================================================================
1132
1133    #[test]
1134    #[cfg(feature = "std")]
1135    fn path_to_azstring_preserves_utf8_and_empty() {
1136        assert_eq!(path_to_azstring(Path::new("")).as_str(), "");
1137        assert_eq!(
1138            path_to_azstring(Path::new("/tmp/a b/\u{1F600}.txt")).as_str(),
1139            "/tmp/a b/\u{1F600}.txt"
1140        );
1141    }
1142
1143    #[test]
1144    #[cfg(all(feature = "std", unix))]
1145    fn path_to_azstring_is_lossy_never_panics_on_invalid_utf8() {
1146        use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
1147
1148        let os = OsStr::from_bytes(&[b'/', b't', 0xFF, 0xFE, b'p']);
1149        let s = path_to_azstring(Path::new(os));
1150        assert!(s.as_str().contains('\u{FFFD}'));
1151        assert!(s.as_str().starts_with("/t"));
1152        assert!(s.as_str().ends_with('p'));
1153    }
1154
1155    // ==================================================================
1156    // FileError: constructor / io mapping / Display
1157    // ==================================================================
1158
1159    #[test]
1160    fn file_error_new_keeps_fields_verbatim() {
1161        let e = FileError::new(FileErrorKind::NotFound, "boom");
1162        assert_eq!(e.kind, FileErrorKind::NotFound);
1163        assert_eq!(e.message.as_str(), "boom");
1164
1165        let empty = FileError::new(FileErrorKind::Other, "");
1166        assert_eq!(empty.message.as_str(), "");
1167        assert_eq!(empty.to_string(), "");
1168    }
1169
1170    #[test]
1171    fn file_error_new_survives_huge_and_unicode_messages() {
1172        let huge = "x".repeat(1_000_000);
1173        let e = FileError::new(FileErrorKind::IoError, huge.clone());
1174        assert_eq!(e.message.as_str().len(), 1_000_000);
1175        assert_eq!(e.message.as_str(), huge);
1176
1177        let uni = FileError::new(FileErrorKind::InvalidPath, "\u{1F600} e\u{301} \u{202E}");
1178        assert_eq!(uni.message.as_str(), "\u{1F600} e\u{301} \u{202E}");
1179        assert_eq!(uni.to_string(), "\u{1F600} e\u{301} \u{202E}");
1180    }
1181
1182    #[test]
1183    fn file_error_display_does_not_interpret_format_specifiers() {
1184        // The message is user/OS-supplied; it must be printed, not treated as a
1185        // format string.
1186        let e = FileError::new(FileErrorKind::Other, "{} {0} {{}} %s %n");
1187        assert_eq!(e.to_string(), "{} {0} {{}} %s %n");
1188        assert!(format!("{e:?}").contains("Other"));
1189    }
1190
1191    #[test]
1192    #[cfg(feature = "std")]
1193    fn file_error_from_io_error_maps_every_known_kind() {
1194        use std::io::{Error, ErrorKind};
1195
1196        let cases = [
1197            (ErrorKind::NotFound, FileErrorKind::NotFound),
1198            (ErrorKind::PermissionDenied, FileErrorKind::PermissionDenied),
1199            (ErrorKind::AlreadyExists, FileErrorKind::AlreadyExists),
1200            (ErrorKind::IsADirectory, FileErrorKind::IsDirectory),
1201            (ErrorKind::DirectoryNotEmpty, FileErrorKind::DirectoryNotEmpty),
1202            // everything else collapses to IoError
1203            (ErrorKind::InvalidData, FileErrorKind::IoError),
1204            (ErrorKind::InvalidInput, FileErrorKind::IoError),
1205            (ErrorKind::UnexpectedEof, FileErrorKind::IoError),
1206            (ErrorKind::WriteZero, FileErrorKind::IoError),
1207            (ErrorKind::Other, FileErrorKind::IoError),
1208        ];
1209
1210        for (io_kind, expected) in cases {
1211            let e = FileError::from_io_error(Error::new(io_kind, "msg"));
1212            assert_eq!(e.kind, expected, "unexpected mapping for {io_kind:?}");
1213            assert_eq!(e.message.as_str(), "msg");
1214        }
1215    }
1216
1217    #[test]
1218    #[cfg(feature = "std")]
1219    fn file_error_from_io_error_tolerates_empty_and_raw_os_errors() {
1220        use std::io::{Error, ErrorKind};
1221
1222        let empty = FileError::from_io_error(Error::other(""));
1223        assert_eq!(empty.kind, FileErrorKind::IoError);
1224        assert_eq!(empty.message.as_str(), "");
1225
1226        // ENOENT is 2 on every unix; the OS-error path must map like the
1227        // ErrorKind path.
1228        #[cfg(unix)]
1229        {
1230            let raw = FileError::from_io_error(Error::from_raw_os_error(2));
1231            assert_eq!(raw.kind, FileErrorKind::NotFound);
1232            assert!(!raw.message.as_str().is_empty());
1233        }
1234
1235        // A nonsense errno must not panic and must not be silently classified.
1236        let bogus = FileError::from_io_error(Error::from_raw_os_error(i32::MAX));
1237        assert_eq!(bogus.kind, FileErrorKind::IoError);
1238    }
1239
1240    // ==================================================================
1241    // file_read / file_write: round-trips and hostile paths
1242    // ==================================================================
1243
1244    #[test]
1245    #[cfg(feature = "std")]
1246    fn file_write_read_roundtrip_bytes_including_invalid_utf8() {
1247        let case = CaseDir::new("rt_bytes");
1248        let p = case.child("blob.bin");
1249
1250        let data: Vec<u8> = vec![0x00, 0xFF, 0xFE, 0x80, 0x7F, b'\n', 0x00];
1251        assert!(file_write(&p, &data).is_ok());
1252
1253        let read = file_read(&p).expect("read back");
1254        assert_eq!(read.as_slice(), &data[..]);
1255
1256        // The same bytes are not valid UTF-8: the string reader must reject
1257        // them instead of lossily decoding.
1258        let err = file_read_string(&p).expect_err("invalid utf-8 must not decode");
1259        assert_eq!(err.kind, FileErrorKind::IoError);
1260        assert!(!err.message.as_str().is_empty());
1261    }
1262
1263    #[test]
1264    #[cfg(feature = "std")]
1265    fn file_write_read_roundtrip_empty_file() {
1266        let case = CaseDir::new("rt_empty");
1267        let p = case.child("empty.bin");
1268
1269        assert!(file_write(&p, b"").is_ok());
1270        assert!(file_read(&p).expect("read").is_empty());
1271        assert_eq!(file_read_string(&p).expect("read str").as_str(), "");
1272        assert_eq!(file_metadata(&p).expect("meta").size, 0);
1273        assert!(path_exists(&p));
1274        assert!(path_is_file(&p));
1275    }
1276
1277    #[test]
1278    #[cfg(feature = "std")]
1279    fn file_write_read_roundtrip_unicode_string() {
1280        let case = CaseDir::new("rt_unicode");
1281        let p = case.child("\u{1F600}_e\u{301}.txt");
1282
1283        let content = "\u{1F600}\u{200D}\u{1F5A5}\u{FE0F} e\u{301}\u{327} \u{202E}rtl \u{0}nul";
1284        assert!(file_write_string(&p, content).is_ok());
1285        assert_eq!(file_read_string(&p).expect("read").as_str(), content);
1286        assert_eq!(file_read(&p).expect("read bytes").as_slice(), content.as_bytes());
1287    }
1288
1289    #[test]
1290    #[cfg(feature = "std")]
1291    fn file_write_read_roundtrip_one_megabyte() {
1292        let case = CaseDir::new("rt_big");
1293        let p = case.child("big.bin");
1294
1295        let data: Vec<u8> = (0..1_000_000u32).map(|i| (i % 251) as u8).collect();
1296        assert!(file_write(&p, &data).is_ok());
1297
1298        let read = file_read(&p).expect("read back");
1299        assert_eq!(read.len(), 1_000_000);
1300        assert_eq!(read.as_slice(), &data[..]);
1301        assert_eq!(file_metadata(&p).expect("meta").size, 1_000_000);
1302    }
1303
1304    #[test]
1305    #[cfg(feature = "std")]
1306    fn file_read_rejects_every_hostile_path_without_panicking() {
1307        // Read-only probes only: these are relative paths resolved against the
1308        // test CWD, so a destructive op here could touch a real file.
1309        for p in hostile_paths() {
1310            let r = file_read(&p);
1311            assert!(r.is_err(), "file_read unexpectedly succeeded for {p:?}");
1312            let e = r.unwrap_err();
1313            assert!(
1314                !e.message.as_str().is_empty(),
1315                "empty error message for {p:?}"
1316            );
1317
1318            assert!(file_read_string(&p).is_err());
1319            assert!(file_metadata(&p).is_err());
1320            assert!(path_canonicalize(&p).is_err());
1321            assert!(dir_list(&p).is_err());
1322        }
1323    }
1324
1325    #[test]
1326    #[cfg(all(feature = "std", unix))]
1327    fn traversal_above_root_and_repeated_slashes_resolve_to_root() {
1328        // Two path shapes that look like "obviously invalid garbage" but are in
1329        // fact valid names for `/`: `..` at the root is the root, and runs of
1330        // separators collapse. Anything treating these as rejected input is
1331        // wrong.
1332        let climb = "../".repeat(64);
1333        let slashes = "/".repeat(1024);
1334
1335        for p in [climb.as_str(), slashes.as_str()] {
1336            assert!(path_exists(p), "{p:?} resolves to /, so it exists");
1337            assert!(path_is_dir(p));
1338            assert!(!path_is_file(p));
1339            assert_eq!(
1340                path_canonicalize(p).expect("resolves to root").as_str(),
1341                "/"
1342            );
1343        }
1344    }
1345
1346    #[test]
1347    #[cfg(feature = "std")]
1348    fn file_read_interior_nul_path_is_an_io_error_not_a_crash() {
1349        let err = file_read("some\0path").expect_err("NUL byte cannot reach the OS");
1350        assert_eq!(err.kind, FileErrorKind::IoError);
1351    }
1352
1353    #[test]
1354    #[cfg(feature = "std")]
1355    fn file_read_overlong_path_saturates_to_an_error() {
1356        let case = CaseDir::new("longpath");
1357        // A single component far beyond NAME_MAX.
1358        let p = case.child(&"n".repeat(5_000));
1359        let err = file_read(&p).expect_err("component exceeds NAME_MAX");
1360        assert_eq!(err.kind, FileErrorKind::IoError);
1361        assert!(!path_exists(&p));
1362    }
1363
1364    #[test]
1365    #[cfg(all(feature = "std", unix))]
1366    fn file_read_on_a_directory_reports_is_directory() {
1367        let case = CaseDir::new("read_dir_as_file");
1368        let err = file_read(case.path()).expect_err("a directory is not readable as bytes");
1369        assert_eq!(err.kind, FileErrorKind::IsDirectory);
1370    }
1371
1372    #[test]
1373    #[cfg(feature = "std")]
1374    fn file_write_into_missing_parent_reports_not_found() {
1375        let case = CaseDir::new("write_missing_parent");
1376        let p = path_join(&case.child("nope"), "f.txt").as_str().to_string();
1377
1378        let err = file_write(&p, b"x").expect_err("parent does not exist");
1379        assert_eq!(err.kind, FileErrorKind::NotFound);
1380        assert!(file_write_string(&p, "x").is_err());
1381        assert!(!path_exists(&p));
1382    }
1383
1384    #[test]
1385    #[cfg(feature = "std")]
1386    fn file_paths_are_never_trimmed() {
1387        let case = CaseDir::new("no_trim");
1388        let padded = case.child("  spaced  ");
1389
1390        assert!(file_write_string(&padded, "payload").is_ok());
1391        assert_eq!(file_read_string(&padded).expect("exact name").as_str(), "payload");
1392
1393        // The trimmed name is a *different* file and must not resolve.
1394        let trimmed = case.child("spaced");
1395        assert!(file_read_string(&trimmed).is_err());
1396        assert!(!path_exists(&trimmed));
1397    }
1398
1399    // ==================================================================
1400    // file_append / file_copy / file_rename / file_delete
1401    // ==================================================================
1402
1403    #[test]
1404    #[cfg(feature = "std")]
1405    fn file_append_creates_then_concatenates() {
1406        let case = CaseDir::new("append");
1407        let p = case.child("log.bin");
1408
1409        assert!(!path_exists(&p));
1410        assert!(file_append(&p, b"aa").is_ok(), "append must create the file");
1411        assert!(file_append(&p, b"").is_ok(), "empty append is a no-op, not an error");
1412        assert!(file_append(&p, &[0xFF, 0x00]).is_ok());
1413
1414        assert_eq!(file_read(&p).expect("read").as_slice(), &[b'a', b'a', 0xFF, 0x00]);
1415    }
1416
1417    #[test]
1418    #[cfg(feature = "std")]
1419    fn file_copy_returns_byte_count_and_duplicates_content() {
1420        let case = CaseDir::new("copy");
1421        let src = case.child("src.bin");
1422        let dst = case.child("dst.bin");
1423        let data: Vec<u8> = (0..=255u8).cycle().take(4096).collect();
1424
1425        assert!(file_write(&src, &data).is_ok());
1426        assert_eq!(file_copy(&src, &dst).expect("copy"), 4096);
1427        assert_eq!(file_read(&dst).expect("read dst").as_slice(), &data[..]);
1428        assert!(path_exists(&src), "copy must not consume the source");
1429
1430        // Copy overwrites an existing destination rather than failing.
1431        assert_eq!(file_copy(&src, &dst).expect("re-copy"), 4096);
1432
1433        let missing = case.child("ghost.bin");
1434        assert_eq!(
1435            file_copy(&missing, &dst).expect_err("missing source").kind,
1436            FileErrorKind::NotFound
1437        );
1438    }
1439
1440    #[test]
1441    #[cfg(feature = "std")]
1442    fn file_rename_moves_and_missing_source_is_not_found() {
1443        let case = CaseDir::new("rename");
1444        let src = case.child("a.txt");
1445        let dst = case.child("b.txt");
1446
1447        assert!(file_write_string(&src, "payload").is_ok());
1448        assert!(file_rename(&src, &dst).is_ok());
1449        assert!(!path_exists(&src), "source must be gone after rename");
1450        assert_eq!(file_read_string(&dst).expect("read").as_str(), "payload");
1451
1452        assert_eq!(
1453            file_rename(&src, &dst).expect_err("source gone").kind,
1454            FileErrorKind::NotFound
1455        );
1456    }
1457
1458    #[test]
1459    #[cfg(feature = "std")]
1460    fn file_delete_is_not_idempotent_and_refuses_directories() {
1461        let case = CaseDir::new("delete");
1462        let p = case.child("victim.txt");
1463
1464        assert!(file_write_string(&p, "x").is_ok());
1465        assert!(file_delete(&p).is_ok());
1466        assert!(!path_exists(&p));
1467
1468        assert_eq!(
1469            file_delete(&p).expect_err("already deleted").kind,
1470            FileErrorKind::NotFound
1471        );
1472
1473        // unlink() on a directory is EISDIR on Linux but EPERM on macOS, so only
1474        // assert that it fails rather than pinning the kind.
1475        assert!(
1476            file_delete(case.path()).is_err(),
1477            "file_delete must not remove a directory"
1478        );
1479        assert!(path_is_dir(case.path()));
1480    }
1481
1482    // ==================================================================
1483    // path_exists / path_is_file / path_is_dir
1484    // ==================================================================
1485
1486    #[test]
1487    #[cfg(feature = "std")]
1488    fn path_predicates_are_false_for_hostile_paths() {
1489        for p in hostile_paths() {
1490            assert!(!path_exists(&p), "{p:?} should not exist");
1491            assert!(!path_is_file(&p));
1492            assert!(!path_is_dir(&p));
1493        }
1494    }
1495
1496    #[test]
1497    #[cfg(feature = "std")]
1498    fn path_is_file_and_is_dir_are_mutually_exclusive() {
1499        let case = CaseDir::new("predicates");
1500        let f = case.child("f.txt");
1501        assert!(file_write_string(&f, "x").is_ok());
1502
1503        let candidates = [case.path().to_string(), f.clone(), case.child("ghost")];
1504        for p in &candidates {
1505            assert!(
1506                !(path_is_file(p) && path_is_dir(p)),
1507                "{p:?} claimed to be both a file and a directory"
1508            );
1509            assert_eq!(path_exists(p), path_is_file(p) || path_is_dir(p));
1510        }
1511
1512        assert!(path_is_file(&f) && !path_is_dir(&f));
1513        assert!(path_is_dir(case.path()) && !path_is_file(case.path()));
1514    }
1515
1516    // ==================================================================
1517    // file_metadata
1518    // ==================================================================
1519
1520    #[test]
1521    #[cfg(feature = "std")]
1522    fn file_metadata_reports_size_type_and_mtime() {
1523        let case = CaseDir::new("meta");
1524        let f = case.child("f.bin");
1525        assert!(file_write(&f, &[1u8; 1234]).is_ok());
1526
1527        let m = file_metadata(&f).expect("file metadata");
1528        assert_eq!(m.size, 1234);
1529        assert_eq!(m.file_type, FileType::File);
1530        assert!(!m.is_readonly);
1531        assert!(m.modified_secs > 0, "mtime must be a real unix timestamp");
1532
1533        let d = file_metadata(case.path()).expect("dir metadata");
1534        assert_eq!(d.file_type, FileType::Directory);
1535
1536        assert_eq!(
1537            file_metadata(&case.child("ghost")).expect_err("missing").kind,
1538            FileErrorKind::NotFound
1539        );
1540    }
1541
1542    #[test]
1543    #[cfg(all(feature = "std", unix))]
1544    fn file_metadata_does_not_follow_symlinks_but_path_is_file_does() {
1545        let case = CaseDir::new("symlink_meta");
1546        let target = case.child("target.txt");
1547        let link = case.child("link.txt");
1548        assert!(file_write_string(&target, "hello").is_ok());
1549        std::os::unix::fs::symlink(&target, &link).expect("symlink");
1550
1551        // symlink_metadata semantics: the link itself, not its target.
1552        let m = file_metadata(&link).expect("metadata");
1553        assert_eq!(m.file_type, FileType::Symlink);
1554
1555        // ...while the predicates and readers *do* follow it.
1556        assert!(path_is_file(&link));
1557        assert_eq!(file_read_string(&link).expect("read through link").as_str(), "hello");
1558        assert_eq!(
1559            path_canonicalize(&link).expect("canonicalize").as_str(),
1560            path_canonicalize(&target).expect("canonicalize target").as_str()
1561        );
1562    }
1563
1564    #[test]
1565    #[cfg(all(feature = "std", unix))]
1566    fn file_metadata_detects_dangling_symlink_without_error() {
1567        let case = CaseDir::new("dangling");
1568        let link = case.child("dangling.txt");
1569        std::os::unix::fs::symlink(case.child("nowhere"), &link).expect("symlink");
1570
1571        let m = file_metadata(&link).expect("symlink_metadata must not follow");
1572        assert_eq!(m.file_type, FileType::Symlink);
1573
1574        // Everything that follows the link must fail cleanly.
1575        assert!(!path_exists(&link));
1576        assert_eq!(
1577            file_read(&link).expect_err("dangling target").kind,
1578            FileErrorKind::NotFound
1579        );
1580    }
1581
1582    // ==================================================================
1583    // Directory operations
1584    // ==================================================================
1585
1586    #[test]
1587    #[cfg(feature = "std")]
1588    fn dir_create_rejects_existing_and_missing_parent() {
1589        let case = CaseDir::new("dir_create");
1590
1591        assert_eq!(
1592            dir_create(case.path()).expect_err("already exists").kind,
1593            FileErrorKind::AlreadyExists
1594        );
1595
1596        let orphan = path_join(&case.child("missing"), "child").as_str().to_string();
1597        assert_eq!(
1598            dir_create(&orphan).expect_err("missing parent").kind,
1599            FileErrorKind::NotFound
1600        );
1601        assert!(!path_exists(&orphan));
1602
1603        // create_all fills in the parents and is idempotent.
1604        assert!(dir_create_all(&orphan).is_ok());
1605        assert!(dir_create_all(&orphan).is_ok(), "create_all must be idempotent");
1606        assert!(path_is_dir(&orphan));
1607    }
1608
1609    #[test]
1610    #[cfg(feature = "std")]
1611    fn dir_create_all_handles_deep_nesting_without_stack_overflow() {
1612        let case = CaseDir::new("deep");
1613        let mut deep = case.child("d0");
1614        for i in 1..64 {
1615            deep = path_join(&deep, &format!("d{i}")).as_str().to_string();
1616        }
1617
1618        assert!(dir_create_all(&deep).is_ok());
1619        assert!(path_is_dir(&deep));
1620
1621        // Recursive removal of the same chain must also stay iterative.
1622        assert!(dir_delete_all(&case.child("d0")).is_ok());
1623        assert!(!path_exists(&deep));
1624    }
1625
1626    #[test]
1627    #[cfg(feature = "std")]
1628    fn dir_delete_refuses_non_empty_but_delete_all_succeeds() {
1629        let case = CaseDir::new("dir_delete");
1630        let sub = case.child("sub");
1631        assert!(dir_create(&sub).is_ok());
1632        assert!(file_write_string(path_join(&sub, "f.txt").as_str(), "x").is_ok());
1633
1634        let err = dir_delete(&sub).expect_err("non-empty directory");
1635        #[cfg(unix)]
1636        assert_eq!(err.kind, FileErrorKind::DirectoryNotEmpty);
1637        assert!(!err.message.as_str().is_empty());
1638        assert!(path_is_dir(&sub), "failed delete must not have removed anything");
1639
1640        assert!(dir_delete_all(&sub).is_ok());
1641        assert!(!path_exists(&sub));
1642
1643        assert_eq!(
1644            dir_delete(&sub).expect_err("already gone").kind,
1645            FileErrorKind::NotFound
1646        );
1647    }
1648
1649    #[test]
1650    #[cfg(feature = "std")]
1651    fn dir_list_enumerates_entries_with_matching_names_and_types() {
1652        let case = CaseDir::new("dir_list");
1653        let sub = case.child("subdir");
1654        let f = case.child("\u{1F600}.txt");
1655        assert!(dir_create(&sub).is_ok());
1656        assert!(file_write_string(&f, "x").is_ok());
1657
1658        let entries = dir_list(case.path()).expect("list");
1659        assert_eq!(entries.len(), 2);
1660
1661        for e in entries.iter() {
1662            assert!(!e.name.as_str().is_empty());
1663            assert!(
1664                e.path.as_str().ends_with(e.name.as_str()),
1665                "entry path {:?} must end with its name {:?}",
1666                e.path.as_str(),
1667                e.name.as_str()
1668            );
1669            assert!(path_exists(e.path.as_str()));
1670
1671            match e.name.as_str() {
1672                "subdir" => assert_eq!(e.file_type, FileType::Directory),
1673                "\u{1F600}.txt" => assert_eq!(e.file_type, FileType::File),
1674                other => panic!("unexpected entry {other:?}"),
1675            }
1676        }
1677
1678        assert!(dir_list(&sub).expect("empty dir").is_empty());
1679        assert!(dir_list(&f).is_err(), "listing a file must fail");
1680    }
1681
1682    #[test]
1683    #[cfg(all(feature = "std", unix))]
1684    fn dir_list_reports_symlinks_as_symlinks() {
1685        let case = CaseDir::new("dir_list_symlink");
1686        let target = case.child("target.txt");
1687        assert!(file_write_string(&target, "x").is_ok());
1688        std::os::unix::fs::symlink(&target, case.child("link.txt")).expect("symlink");
1689
1690        let entries = dir_list(case.path()).expect("list");
1691        let link = entries
1692            .iter()
1693            .find(|e| e.name.as_str() == "link.txt")
1694            .expect("link entry");
1695        assert_eq!(link.file_type, FileType::Symlink);
1696    }
1697
1698    // ==================================================================
1699    // Path manipulation (pure, no filesystem)
1700    // ==================================================================
1701
1702    #[test]
1703    #[cfg(all(feature = "std", unix))]
1704    fn path_join_absolute_component_replaces_the_base() {
1705        // std::path::Path::join semantics: an absolute second component wins.
1706        // This is the classic traversal footgun, so pin it down.
1707        assert_eq!(path_join("/home/user", "/etc/passwd").as_str(), "/etc/passwd");
1708        assert_eq!(path_join("/srv/data", "/").as_str(), "/");
1709    }
1710
1711    #[test]
1712    #[cfg(feature = "std")]
1713    fn path_join_does_not_normalize_parent_traversal() {
1714        let joined = path_join("/srv/data", "../../etc/passwd");
1715        assert!(
1716            joined.as_str().contains(".."),
1717            "path_join is purely lexical and must not silently resolve `..`: {:?}",
1718            joined.as_str()
1719        );
1720
1721        assert_eq!(path_join("", "x").as_str(), "x");
1722        assert!(path_join("base", "").as_str().starts_with("base"));
1723        assert!(path_join("base", "\u{1F600}").as_str().ends_with("\u{1F600}"));
1724    }
1725
1726    #[test]
1727    #[cfg(feature = "std")]
1728    fn path_parent_edge_cases() {
1729        assert_eq!(as_opt_string(path_parent("")), None);
1730        assert_eq!(as_opt_string(path_parent("..")), Some(String::new()));
1731        assert_eq!(as_opt_string(path_parent("a")), Some(String::new()));
1732
1733        #[cfg(unix)]
1734        {
1735            assert_eq!(as_opt_string(path_parent("/")), None);
1736            assert_eq!(as_opt_string(path_parent("/a")), Some("/".to_string()));
1737            assert_eq!(as_opt_string(path_parent("/a/b/c")), Some("/a/b".to_string()));
1738            assert_eq!(as_opt_string(path_parent("/a/b/")), Some("/a".to_string()));
1739        }
1740    }
1741
1742    #[test]
1743    #[cfg(all(feature = "std", unix))]
1744    fn path_parent_of_ten_thousand_components_does_not_stack_overflow() {
1745        let deep = format!("/{}", vec!["a"; 10_000].join("/"));
1746        let parent = path_parent(&deep).expect("parent of a deep path");
1747        assert_eq!(parent.as_str().len(), deep.len() - 2);
1748        assert!(parent.as_str().starts_with("/a/a/"));
1749    }
1750
1751    #[test]
1752    #[cfg(feature = "std")]
1753    fn path_file_name_edge_cases() {
1754        assert_eq!(as_opt_string(path_file_name("")), None);
1755        assert_eq!(as_opt_string(path_file_name("..")), None);
1756        assert_eq!(as_opt_string(path_file_name(".")), None);
1757        assert_eq!(as_opt_string(path_file_name("foo.txt")), Some("foo.txt".to_string()));
1758        assert_eq!(
1759            as_opt_string(path_file_name("\u{1F600}.txt")),
1760            Some("\u{1F600}.txt".to_string())
1761        );
1762
1763        #[cfg(unix)]
1764        {
1765            assert_eq!(as_opt_string(path_file_name("/")), None);
1766            // A trailing separator is stripped, not treated as an empty name.
1767            assert_eq!(as_opt_string(path_file_name("/a/b/")), Some("b".to_string()));
1768        }
1769    }
1770
1771    #[test]
1772    #[cfg(feature = "std")]
1773    fn path_extension_edge_cases() {
1774        assert_eq!(as_opt_string(path_extension("")), None);
1775        assert_eq!(as_opt_string(path_extension("noext")), None);
1776        // A dotfile has no extension...
1777        assert_eq!(as_opt_string(path_extension(".hidden")), None);
1778        // ...but a trailing dot yields an *empty* extension, not None.
1779        assert_eq!(as_opt_string(path_extension("a.")), Some(String::new()));
1780        // Only the last segment counts.
1781        assert_eq!(as_opt_string(path_extension("a.tar.gz")), Some("gz".to_string()));
1782        assert_eq!(as_opt_string(path_extension("a.\u{1F600}")), Some("\u{1F600}".to_string()));
1783        assert_eq!(as_opt_string(path_extension("..")), None);
1784    }
1785
1786    #[test]
1787    #[cfg(feature = "std")]
1788    fn path_canonicalize_requires_existence_and_returns_absolute() {
1789        let case = CaseDir::new("canon");
1790        let f = case.child("f.txt");
1791        assert!(file_write_string(&f, "x").is_ok());
1792
1793        let canon = path_canonicalize(&f).expect("canonicalize");
1794        assert!(FilePath::from_str(canon.as_str()).is_absolute());
1795        assert!(path_exists(canon.as_str()));
1796        assert!(!canon.as_str().contains(".."));
1797
1798        assert_eq!(
1799            path_canonicalize("").expect_err("empty path").kind,
1800            FileErrorKind::NotFound
1801        );
1802        assert_eq!(
1803            path_canonicalize(&case.child("ghost")).expect_err("missing").kind,
1804            FileErrorKind::NotFound
1805        );
1806
1807        // `..` is resolved away — but only through components that really are
1808        // directories (`f.txt/../f.txt` is ENOTDIR, not a lexical rewrite).
1809        let sub = case.child("sub");
1810        assert!(dir_create(&sub).is_ok());
1811        let via_dotdot = path_join(path_join(&sub, "..").as_str(), "f.txt")
1812            .as_str()
1813            .to_string();
1814        assert_eq!(
1815            path_canonicalize(&via_dotdot).expect("resolve .. through a real dir").as_str(),
1816            canon.as_str()
1817        );
1818
1819        let through_a_file = path_join(path_join(&f, "..").as_str(), "f.txt")
1820            .as_str()
1821            .to_string();
1822        assert!(
1823            path_canonicalize(&through_a_file).is_err(),
1824            "a regular file must not act as a directory component"
1825        );
1826    }
1827
1828    #[test]
1829    #[cfg(feature = "std")]
1830    fn temp_dir_is_a_real_directory() {
1831        let t = temp_dir();
1832        assert!(!t.as_str().is_empty());
1833        assert!(path_is_dir(t.as_str()));
1834        assert!(FilePath::from_str(t.as_str()).is_absolute());
1835    }
1836
1837    // ==================================================================
1838    // FilePath: construction, invariants, delegation
1839    // ==================================================================
1840
1841    #[test]
1842    fn file_path_constructors_agree_and_roundtrip_exactly() {
1843        let inputs = [
1844            "",
1845            "   ",
1846            "/usr/bin",
1847            "rel/path.txt",
1848            "\u{1F600}/e\u{301}",
1849            "with\0nul",
1850        ];
1851
1852        for s in inputs {
1853            let from_str = FilePath::from_str(s);
1854            let new = FilePath::new(AzString::from(String::from(s)));
1855            let from_ref: FilePath = s.into();
1856            let from_string: FilePath = String::from(s).into();
1857            let from_az: FilePath = AzString::from(String::from(s)).into();
1858
1859            assert_eq!(from_str.as_str(), s, "as_str must round-trip verbatim");
1860            assert_eq!(from_str, new);
1861            assert_eq!(from_str, from_ref);
1862            assert_eq!(from_str, from_string);
1863            assert_eq!(from_str, from_az);
1864            assert_eq!(from_str.as_string().as_str(), s);
1865        }
1866    }
1867
1868    #[test]
1869    fn file_path_empty_and_default_are_the_neutral_value() {
1870        let e = FilePath::empty();
1871        assert_eq!(e, FilePath::default());
1872        assert_eq!(e, FilePath::from_str(""));
1873        assert!(e.as_str().is_empty());
1874        assert!(e.as_string().as_str().is_empty());
1875    }
1876
1877    #[test]
1878    fn file_path_handles_a_100k_char_component() {
1879        let huge = "x".repeat(100_000);
1880        let p = FilePath::from_str(&huge);
1881        assert_eq!(p.as_str().len(), 100_000);
1882        assert_eq!(p.as_str(), huge);
1883        assert_eq!(p.clone(), p);
1884    }
1885
1886    #[test]
1887    #[cfg(feature = "std")]
1888    fn file_path_eq_implies_equal_hashes() {
1889        use std::collections::HashSet;
1890
1891        let mut set = HashSet::new();
1892        assert!(set.insert(FilePath::from_str("/a/b")));
1893        assert!(
1894            !set.insert(FilePath::new(AzString::from(String::from("/a/b")))),
1895            "equal FilePaths must hash equally"
1896        );
1897        assert!(set.insert(FilePath::from_str("/a/b/")), "trailing slash is a distinct string");
1898        assert_eq!(set.len(), 2);
1899    }
1900
1901    #[test]
1902    #[cfg(feature = "std")]
1903    fn file_path_accessors_delegate_to_the_free_functions() {
1904        let inputs = [
1905            "",
1906            "   ",
1907            "/",
1908            "..",
1909            "/a/b/c.tar.gz",
1910            "a.",
1911            ".hidden",
1912            "\u{1F600}.txt",
1913            "with\0nul",
1914        ];
1915
1916        for s in inputs {
1917            let p = FilePath::from_str(s);
1918            assert_eq!(
1919                p.parent().map(|q| q.as_str().to_string()),
1920                as_opt_string(path_parent(s)),
1921                "parent() diverged from path_parent for {s:?}"
1922            );
1923            assert_eq!(as_opt_string(p.file_name()), as_opt_string(path_file_name(s)));
1924            assert_eq!(as_opt_string(p.extension()), as_opt_string(path_extension(s)));
1925            assert_eq!(p.exists(), path_exists(s));
1926            assert_eq!(p.is_file(), path_is_file(s));
1927            assert_eq!(p.is_dir(), path_is_dir(s));
1928
1929            let joined = p.join(&FilePath::from_str("child"));
1930            assert_eq!(joined.as_str(), path_join(s, "child").as_str());
1931            let joined_str = p.join_str(&AzString::from(String::from("child")));
1932            assert_eq!(joined_str.as_str(), joined.as_str());
1933        }
1934    }
1935
1936    #[test]
1937    #[cfg(all(feature = "std", unix))]
1938    fn file_path_is_absolute_edge_cases() {
1939        assert!(FilePath::from_str("/").is_absolute());
1940        assert!(FilePath::from_str("/etc/passwd").is_absolute());
1941        assert!(!FilePath::from_str("").is_absolute());
1942        assert!(!FilePath::from_str("   ").is_absolute());
1943        assert!(!FilePath::from_str("relative").is_absolute());
1944        assert!(!FilePath::from_str("./relative").is_absolute());
1945        assert!(!FilePath::from_str("../escape").is_absolute());
1946    }
1947
1948    #[test]
1949    #[cfg(feature = "std")]
1950    fn empty_file_path_fails_every_filesystem_operation_cleanly() {
1951        let e = FilePath::empty();
1952
1953        assert!(!e.exists());
1954        assert!(!e.is_file());
1955        assert!(!e.is_dir());
1956        assert!(e.read_bytes().is_err());
1957        assert!(e.read_string().is_err());
1958        assert!(e.read_dir().is_err());
1959        assert!(e.metadata().is_err());
1960        assert!(e.canonicalize().is_err());
1961        assert!(e.remove_file().is_err());
1962        assert!(e.remove_dir().is_err());
1963        assert!(e.remove_dir_all().is_err());
1964        assert!(e.create_dir().is_err());
1965        assert!(e.write_bytes(&U8Vec::from_vec(vec![1, 2, 3])).is_err());
1966        assert!(e.write_string(&AzString::from(String::from("x"))).is_err());
1967        assert!(e.copy_to(&FilePath::from_str("/tmp/whatever")).is_err());
1968        assert!(e.rename_to(&FilePath::from_str("/tmp/whatever")).is_err());
1969
1970        // Pure path ops still work on the empty path.
1971        assert!(e.parent().is_none());
1972        assert!(e.file_name().is_none());
1973        assert!(e.extension().is_none());
1974    }
1975
1976    #[test]
1977    #[cfg(feature = "std")]
1978    fn create_dir_all_on_the_empty_path_reports_success_but_creates_nothing() {
1979        // Inherited from std: `fs::create_dir_all` short-circuits `""` to
1980        // `Ok(())`. So this is the one directory op where the empty path does
1981        // NOT fail — callers cannot treat Ok as "the directory now exists".
1982        let e = FilePath::empty();
1983        assert!(e.create_dir_all().is_ok());
1984        assert!(dir_create_all("").is_ok());
1985        assert!(!e.exists(), "Ok(()) must not be read as `the dir was created`");
1986
1987        // The non-recursive variant does fail, so the two disagree on `""`.
1988        assert!(dir_create("").is_err());
1989    }
1990
1991    #[test]
1992    #[cfg(feature = "std")]
1993    fn file_path_oop_roundtrip_write_read_copy_rename_remove() {
1994        let case = CaseDir::new("oop");
1995        let root = FilePath::from_str(case.path());
1996
1997        let nested = root.join(&FilePath::from_str("a")).join(&FilePath::from_str("b"));
1998        assert!(nested.create_dir_all().is_ok());
1999        assert!(nested.is_dir());
2000
2001        let f = nested.join_str(&AzString::from(String::from("data.bin")));
2002        let payload = U8Vec::from_vec(vec![0u8, 0xFF, 0x41]);
2003        assert!(f.write_bytes(&payload).is_ok());
2004        assert_eq!(f.read_bytes().expect("read").as_slice(), payload.as_slice());
2005        assert_eq!(f.metadata().expect("meta").size, 3);
2006        assert_eq!(f.file_name().expect("name").as_str(), "data.bin");
2007        assert_eq!(f.extension().expect("ext").as_str(), "bin");
2008        assert_eq!(f.parent().expect("parent").as_str(), nested.as_str());
2009
2010        let text = f.join(&FilePath::empty());
2011        assert!(text.as_str().starts_with(f.as_str()));
2012
2013        let copy = nested.join(&FilePath::from_str("copy.bin"));
2014        assert_eq!(f.copy_to(&copy).expect("copy"), 3);
2015        assert!(copy.is_file());
2016
2017        let moved = nested.join(&FilePath::from_str("moved.bin"));
2018        assert!(copy.rename_to(&moved).is_ok());
2019        assert!(!copy.exists());
2020        assert!(moved.is_file());
2021
2022        assert_eq!(nested.read_dir().expect("list").len(), 2);
2023
2024        assert!(moved.remove_file().is_ok());
2025        assert!(f.remove_file().is_ok());
2026        assert!(nested.remove_dir().is_ok(), "now-empty dir must be removable");
2027        assert!(root.join(&FilePath::from_str("a")).remove_dir_all().is_ok());
2028    }
2029
2030    #[test]
2031    #[cfg(feature = "std")]
2032    fn file_path_write_string_overwrites_rather_than_appends() {
2033        let case = CaseDir::new("overwrite");
2034        let f = FilePath::from_str(&case.child("f.txt"));
2035
2036        assert!(f.write_string(&AzString::from(String::from("first"))).is_ok());
2037        assert!(f.write_string(&AzString::from(String::from("2"))).is_ok());
2038        assert_eq!(f.read_string().expect("read").as_str(), "2");
2039        assert_eq!(f.metadata().expect("meta").size, 1);
2040    }
2041
2042    #[test]
2043    #[cfg(feature = "std")]
2044    fn file_path_env_dirs_are_sane() {
2045        let t = FilePath::get_temp_dir();
2046        assert_eq!(t.as_str(), temp_dir().as_str());
2047        assert!(t.is_dir());
2048
2049        let cwd = FilePath::get_current_dir().expect("current dir");
2050        assert!(cwd.is_absolute());
2051        assert!(cwd.is_dir());
2052    }
2053
2054    #[test]
2055    #[cfg(all(feature = "std", feature = "extra"))]
2056    fn file_path_user_dirs_never_panic_and_are_never_empty_strings() {
2057        let probes: [(&str, Option<FilePath>); 17] = [
2058            ("home", FilePath::get_home_dir()),
2059            ("cache", FilePath::get_cache_dir()),
2060            ("config", FilePath::get_config_dir()),
2061            ("config_local", FilePath::get_config_local_dir()),
2062            ("data", FilePath::get_data_dir()),
2063            ("data_local", FilePath::get_data_local_dir()),
2064            ("desktop", FilePath::get_desktop_dir()),
2065            ("document", FilePath::get_document_dir()),
2066            ("download", FilePath::get_download_dir()),
2067            ("executable", FilePath::get_executable_dir()),
2068            ("font", FilePath::get_font_dir()),
2069            ("picture", FilePath::get_picture_dir()),
2070            ("preference", FilePath::get_preference_dir()),
2071            ("public", FilePath::get_public_dir()),
2072            ("runtime", FilePath::get_runtime_dir()),
2073            ("state", FilePath::get_state_dir()),
2074            ("audio", FilePath::get_audio_dir()),
2075        ];
2076
2077        for (name, dir) in probes {
2078            if let Some(d) = dir {
2079                assert!(!d.as_str().is_empty(), "{name} dir resolved to an empty path");
2080                assert!(d.is_absolute(), "{name} dir must be absolute: {:?}", d.as_str());
2081            }
2082        }
2083
2084        // The remaining two go through the same `dirs` path; just prove they
2085        // do not panic.
2086        let _ = FilePath::get_video_dir();
2087        let _ = FilePath::get_template_dir();
2088    }
2089
2090    // ==================================================================
2091    // Plain data types
2092    // ==================================================================
2093
2094    #[test]
2095    fn file_metadata_holds_saturated_u64_fields() {
2096        let m = FileMetadata {
2097            size: u64::MAX,
2098            file_type: FileType::Other,
2099            is_readonly: true,
2100            modified_secs: u64::MAX,
2101            created_secs: 0,
2102        };
2103        let copy = m;
2104
2105        assert_eq!(m, copy);
2106        assert_eq!(copy.size, u64::MAX);
2107        assert_eq!(copy.modified_secs, u64::MAX);
2108        assert!(format!("{m:?}").contains("18446744073709551615"));
2109    }
2110
2111    #[test]
2112    fn dir_entry_vec_bounds_are_checked() {
2113        let empty = DirEntryVec::from_vec(Vec::new());
2114        assert!(empty.is_empty());
2115        assert_eq!(empty.len(), 0);
2116        assert!(empty.get(0).is_none());
2117        assert!(empty.get(usize::MAX).is_none());
2118
2119        let one = DirEntryVec::from_vec(vec![DirEntry {
2120            name: AzString::from(String::from("f.txt")),
2121            path: AzString::from(String::from("/tmp/f.txt")),
2122            file_type: FileType::File,
2123        }]);
2124        assert_eq!(one.len(), 1);
2125        assert_eq!(one.get(0).expect("first").name.as_str(), "f.txt");
2126        assert!(one.get(1).is_none());
2127        assert!(one.get(usize::MAX).is_none());
2128    }
2129
2130    // ==================================================================
2131    // no_std stubs: every entry point must fail closed, never panic.
2132    // ==================================================================
2133
2134    #[test]
2135    #[cfg(not(feature = "std"))]
2136    fn no_std_stubs_fail_closed() {
2137        for p in ["", "   ", "/etc/passwd", "\u{1F600}"] {
2138            assert_eq!(file_read(p).unwrap_err().kind, FileErrorKind::Other);
2139            assert!(file_read_string(p).is_err());
2140            assert!(file_write(p, &[0xFF, 0x00]).is_err());
2141            assert!(file_write_string(p, "x").is_err());
2142            assert!(file_append(p, b"").is_err());
2143            assert!(file_copy(p, p).is_err());
2144            assert!(file_rename(p, p).is_err());
2145            assert!(file_delete(p).is_err());
2146            assert!(file_metadata(p).is_err());
2147            assert!(dir_create(p).is_err());
2148            assert!(dir_create_all(p).is_err());
2149            assert!(dir_delete(p).is_err());
2150            assert!(dir_delete_all(p).is_err());
2151            assert!(dir_list(p).is_err());
2152            assert!(path_canonicalize(p).is_err());
2153
2154            assert!(!path_exists(p));
2155            assert!(!path_is_file(p));
2156            assert!(!path_is_dir(p));
2157            assert!(path_parent(p).is_none());
2158            assert!(path_file_name(p).is_none());
2159            assert!(path_extension(p).is_none());
2160            assert_eq!(path_join(p, p).as_str(), "");
2161        }
2162
2163        assert_eq!(temp_dir().as_str(), "");
2164        assert_eq!(no_std_file_error().kind, FileErrorKind::Other);
2165        assert!(!no_std_file_error().message.as_str().is_empty());
2166    }
2167}