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}