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

            
6
use alloc::string::String;
7
use alloc::vec::Vec;
8
use core::fmt;
9
use 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")]
12
use std::path::Path;
13

            
14
// ============================================================================
15
// Error types
16
// ============================================================================
17

            
18
/// Error when performing file operations
19
#[derive(Debug, Clone, PartialEq)]
20
#[repr(C)]
21
pub struct FileError {
22
    /// Error message
23
    pub message: AzString,
24
    /// Error kind
25
    pub kind: FileErrorKind,
26
}
27

            
28
/// Kind of file error
29
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30
#[repr(C)]
31
pub enum FileErrorKind {
32
    /// File or directory not found
33
    NotFound,
34
    /// Permission denied
35
    PermissionDenied,
36
    /// File already exists
37
    AlreadyExists,
38
    /// Invalid path
39
    InvalidPath,
40
    /// I/O error
41
    IoError,
42
    /// Directory not empty
43
    DirectoryNotEmpty,
44
    /// Is a directory (expected file)
45
    IsDirectory,
46
    /// Is a file (expected directory)
47
    IsFile,
48
    /// Other error
49
    Other,
50
}
51

            
52
impl FileError {
53
    pub fn new(kind: FileErrorKind, message: impl Into<String>) -> Self {
54
        Self {
55
            message: AzString::from(message.into()),
56
            kind,
57
        }
58
    }
59
    
60
    #[cfg(feature = "std")]
61
    pub fn from_io_error(e: std::io::Error) -> Self {
62
        use std::io::ErrorKind;
63
        
64
        let kind = match e.kind() {
65
            ErrorKind::NotFound => FileErrorKind::NotFound,
66
            ErrorKind::PermissionDenied => FileErrorKind::PermissionDenied,
67
            ErrorKind::AlreadyExists => FileErrorKind::AlreadyExists,
68
            ErrorKind::IsADirectory => FileErrorKind::IsDirectory,
69
            ErrorKind::DirectoryNotEmpty => FileErrorKind::DirectoryNotEmpty,
70
            _ => FileErrorKind::IoError,
71
        };
72
        
73
        Self {
74
            message: AzString::from(e.to_string()),
75
            kind,
76
        }
77
    }
78
}
79

            
80
impl fmt::Display for FileError {
81
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82
        write!(f, "{}", self.message.as_str())
83
    }
84
}
85

            
86
#[cfg(feature = "std")]
87
impl std::error::Error for FileError {}
88

            
89
// FFI-safe Result types for file operations
90
impl_result!(
91
    EmptyStruct,
92
    FileError,
93
    ResultEmptyStructFileError,
94
    copy = false,
95
    [Debug, Clone, PartialEq]
96
);
97

            
98
impl_result!(
99
    U8Vec,
100
    FileError,
101
    ResultU8VecFileError,
102
    copy = false,
103
    [Debug, Clone, PartialEq]
104
);
105

            
106
impl_result!(
107
    AzString,
108
    FileError,
109
    ResultStringFileError,
110
    copy = false,
111
    [Debug, Clone, PartialEq]
112
);
113

            
114
impl_result!(
115
    u64,
116
    FileError,
117
    Resultu64FileError,
118
    copy = false,
119
    [Debug, Clone, PartialEq]
120
);
121

            
122
// Forward declarations for result types that need later-defined types
123
// (FilePath, FileMetadata, DirEntryVec are defined below)
124

            
125
// ============================================================================
126
// File metadata
127
// ============================================================================
128

            
129
/// File type (file, directory, symlink)
130
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131
#[repr(C)]
132
pub enum FileType {
133
    /// Regular file
134
    File,
135
    /// Directory
136
    Directory,
137
    /// Symbolic link
138
    Symlink,
139
    /// Other (device, socket, etc.)
140
    Other,
141
}
142

            
143
/// Metadata about a file
144
#[derive(Debug, Clone, PartialEq)]
145
#[repr(C)]
146
pub struct FileMetadata {
147
    /// File size in bytes
148
    pub size: u64,
149
    /// File type
150
    pub file_type: FileType,
151
    /// Is read-only
152
    pub is_readonly: bool,
153
    /// Last modification time (Unix timestamp in seconds)
154
    pub modified_secs: u64,
155
    /// Creation time (Unix timestamp in seconds, 0 if unavailable)
156
    pub created_secs: u64,
157
}
158

            
159
/// A directory entry
160
#[derive(Debug, Clone)]
161
#[repr(C)]
162
pub struct DirEntry {
163
    /// File name (not full path)
164
    pub name: AzString,
165
    /// Full path
166
    pub path: AzString,
167
    /// File type
168
    pub file_type: FileType,
169
}
170

            
171
/// Vec of DirEntry
172
impl_option!(DirEntry, OptionDirEntry, copy = false, [Debug, Clone]);
173
impl_vec!(DirEntry, DirEntryVec, DirEntryVecDestructor, DirEntryVecDestructorType, DirEntryVecSlice, OptionDirEntry);
174
impl_vec_clone!(DirEntry, DirEntryVec, DirEntryVecDestructor);
175
impl_vec_debug!(DirEntry, DirEntryVec);
176
impl_vec_mut!(DirEntry, DirEntryVec);
177

            
178
// Additional FFI-safe Result types for complex types
179
impl_result!(
180
    FileMetadata,
181
    FileError,
182
    ResultFileMetadataFileError,
183
    copy = false,
184
    [Debug, Clone, PartialEq]
185
);
186

            
187
impl_result!(
188
    DirEntryVec,
189
    FileError,
190
    ResultDirEntryVecFileError,
191
    copy = false,
192
    clone = false,
193
    [Debug, Clone]
194
);
195

            
196
// ============================================================================
197
// File operations
198
// ============================================================================
199

            
200
/// Read a file to bytes
201
#[cfg(feature = "std")]
202
pub fn file_read(path: &str) -> Result<U8Vec, FileError> {
203
    let data = std::fs::read(path)
204
        .map_err(FileError::from_io_error)?;
205
    Ok(U8Vec::from(data))
206
}
207

            
208
/// Read a file to string (UTF-8)
209
#[cfg(feature = "std")]
210
pub fn file_read_string(path: &str) -> Result<AzString, FileError> {
211
    let data = std::fs::read_to_string(path)
212
        .map_err(FileError::from_io_error)?;
213
    Ok(AzString::from(data))
214
}
215

            
216
/// Write bytes to a file (creates or overwrites)
217
#[cfg(feature = "std")]
218
pub fn file_write(path: &str, data: &[u8]) -> Result<EmptyStruct, FileError> {
219
    std::fs::write(path, data)
220
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
221
}
222

            
223
/// Write string to a file (creates or overwrites)
224
#[cfg(feature = "std")]
225
pub fn file_write_string(path: &str, data: &str) -> Result<EmptyStruct, FileError> {
226
    std::fs::write(path, data.as_bytes())
227
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
228
}
229

            
230
/// Append bytes to a file
231
#[cfg(feature = "std")]
232
pub fn file_append(path: &str, data: &[u8]) -> Result<EmptyStruct, FileError> {
233
    use std::fs::OpenOptions;
234
    use std::io::Write;
235
    
236
    let mut file = OpenOptions::new()
237
        .create(true)
238
        .append(true)
239
        .open(path)
240
        .map_err(FileError::from_io_error)?;
241
    
242
    file.write_all(data)
243
        .map(|_| EmptyStruct::default())
244
        .map_err(FileError::from_io_error)
245
}
246

            
247
/// Copy a file
248
#[cfg(feature = "std")]
249
pub fn file_copy(from: &str, to: &str) -> Result<u64, FileError> {
250
    std::fs::copy(from, to)
251
        .map_err(FileError::from_io_error)
252
}
253

            
254
/// Rename/move a file
255
#[cfg(feature = "std")]
256
pub fn file_rename(from: &str, to: &str) -> Result<EmptyStruct, FileError> {
257
    std::fs::rename(from, to)
258
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
259
}
260

            
261
/// Delete a file
262
#[cfg(feature = "std")]
263
pub fn file_delete(path: &str) -> Result<EmptyStruct, FileError> {
264
    std::fs::remove_file(path)
265
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
266
}
267

            
268
/// Check if a file or directory exists
269
#[cfg(feature = "std")]
270
pub fn path_exists(path: &str) -> bool {
271
    Path::new(path).exists()
272
}
273

            
274
/// Check if path is a file
275
#[cfg(feature = "std")]
276
pub fn path_is_file(path: &str) -> bool {
277
    Path::new(path).is_file()
278
}
279

            
280
/// Check if path is a directory
281
#[cfg(feature = "std")]
282
pub fn path_is_dir(path: &str) -> bool {
283
    Path::new(path).is_dir()
284
}
285

            
286
/// Get file metadata
287
#[cfg(feature = "std")]
288
pub fn file_metadata(path: &str) -> Result<FileMetadata, FileError> {
289
    let meta = std::fs::metadata(path)
290
        .map_err(FileError::from_io_error)?;
291
    
292
    let file_type = if meta.is_file() {
293
        FileType::File
294
    } else if meta.is_dir() {
295
        FileType::Directory
296
    } else if meta.is_symlink() {
297
        FileType::Symlink
298
    } else {
299
        FileType::Other
300
    };
301
    
302
    let modified_secs = meta.modified()
303
        .ok()
304
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
305
        .map(|d| d.as_secs())
306
        .unwrap_or(0);
307
    
308
    let created_secs = meta.created()
309
        .ok()
310
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
311
        .map(|d| d.as_secs())
312
        .unwrap_or(0);
313
    
314
    Ok(FileMetadata {
315
        size: meta.len(),
316
        file_type,
317
        is_readonly: meta.permissions().readonly(),
318
        modified_secs,
319
        created_secs,
320
    })
321
}
322

            
323
// ----------------------------------------------------------------------------
324
// no_std stubs: keep the file API surface present without std::fs.
325
// ----------------------------------------------------------------------------
326

            
327
/// Error returned by file stubs when the `std` feature is disabled.
328
#[cfg(not(feature = "std"))]
329
fn no_std_file_error() -> FileError {
330
    FileError::new(FileErrorKind::Other, "file IO requires the `std` feature")
331
}
332

            
333
/// Stub: `std` feature disabled.
334
#[cfg(not(feature = "std"))]
335
pub fn file_read(_path: &str) -> Result<U8Vec, FileError> {
336
    Err(no_std_file_error())
337
}
338

            
339
/// Stub: `std` feature disabled.
340
#[cfg(not(feature = "std"))]
341
pub fn file_read_string(_path: &str) -> Result<AzString, FileError> {
342
    Err(no_std_file_error())
343
}
344

            
345
/// Stub: `std` feature disabled.
346
#[cfg(not(feature = "std"))]
347
pub fn file_write(_path: &str, _data: &[u8]) -> Result<EmptyStruct, FileError> {
348
    Err(no_std_file_error())
349
}
350

            
351
/// Stub: `std` feature disabled.
352
#[cfg(not(feature = "std"))]
353
pub fn file_write_string(_path: &str, _data: &str) -> Result<EmptyStruct, FileError> {
354
    Err(no_std_file_error())
355
}
356

            
357
/// Stub: `std` feature disabled.
358
#[cfg(not(feature = "std"))]
359
pub fn file_append(_path: &str, _data: &[u8]) -> Result<EmptyStruct, FileError> {
360
    Err(no_std_file_error())
361
}
362

            
363
/// Stub: `std` feature disabled.
364
#[cfg(not(feature = "std"))]
365
pub fn file_copy(_from: &str, _to: &str) -> Result<u64, FileError> {
366
    Err(no_std_file_error())
367
}
368

            
369
/// Stub: `std` feature disabled.
370
#[cfg(not(feature = "std"))]
371
pub fn file_rename(_from: &str, _to: &str) -> Result<EmptyStruct, FileError> {
372
    Err(no_std_file_error())
373
}
374

            
375
/// Stub: `std` feature disabled.
376
#[cfg(not(feature = "std"))]
377
pub fn file_delete(_path: &str) -> Result<EmptyStruct, FileError> {
378
    Err(no_std_file_error())
379
}
380

            
381
/// Stub: `std` feature disabled.
382
#[cfg(not(feature = "std"))]
383
pub fn path_exists(_path: &str) -> bool {
384
    false
385
}
386

            
387
/// Stub: `std` feature disabled.
388
#[cfg(not(feature = "std"))]
389
pub fn path_is_file(_path: &str) -> bool {
390
    false
391
}
392

            
393
/// Stub: `std` feature disabled.
394
#[cfg(not(feature = "std"))]
395
pub fn path_is_dir(_path: &str) -> bool {
396
    false
397
}
398

            
399
/// Stub: `std` feature disabled.
400
#[cfg(not(feature = "std"))]
401
pub fn file_metadata(_path: &str) -> Result<FileMetadata, FileError> {
402
    Err(no_std_file_error())
403
}
404

            
405
// ============================================================================
406
// Directory operations
407
// ============================================================================
408

            
409
/// Create a directory
410
#[cfg(feature = "std")]
411
pub fn dir_create(path: &str) -> Result<EmptyStruct, FileError> {
412
    std::fs::create_dir(path)
413
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
414
}
415

            
416
/// Create a directory and all parent directories
417
#[cfg(feature = "std")]
418
pub fn dir_create_all(path: &str) -> Result<EmptyStruct, FileError> {
419
    std::fs::create_dir_all(path)
420
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
421
}
422

            
423
/// Delete an empty directory
424
#[cfg(feature = "std")]
425
pub fn dir_delete(path: &str) -> Result<EmptyStruct, FileError> {
426
    std::fs::remove_dir(path)
427
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
428
}
429

            
430
/// Delete a directory and all its contents
431
#[cfg(feature = "std")]
432
pub fn dir_delete_all(path: &str) -> Result<EmptyStruct, FileError> {
433
    std::fs::remove_dir_all(path)
434
        .map(|_| EmptyStruct::default()).map_err(FileError::from_io_error)
435
}
436

            
437
/// List directory contents
438
#[cfg(feature = "std")]
439
pub fn dir_list(path: &str) -> Result<DirEntryVec, FileError> {
440
    let entries = std::fs::read_dir(path)
441
        .map_err(FileError::from_io_error)?;
442
    
443
    let mut result = Vec::new();
444
    
445
    for entry in entries {
446
        let entry = entry.map_err(FileError::from_io_error)?;
447
        let file_type = entry.file_type()
448
            .map(|ft| {
449
                if ft.is_file() {
450
                    FileType::File
451
                } else if ft.is_dir() {
452
                    FileType::Directory
453
                } else if ft.is_symlink() {
454
                    FileType::Symlink
455
                } else {
456
                    FileType::Other
457
                }
458
            })
459
            .unwrap_or(FileType::Other);
460
        
461
        result.push(DirEntry {
462
            name: AzString::from(entry.file_name().to_string_lossy().to_string()),
463
            path: AzString::from(entry.path().to_string_lossy().to_string()),
464
            file_type,
465
        });
466
    }
467
    
468
    Ok(DirEntryVec::from_vec(result))
469
}
470

            
471
/// Stub: `std` feature disabled.
472
#[cfg(not(feature = "std"))]
473
pub fn dir_create(_path: &str) -> Result<EmptyStruct, FileError> {
474
    Err(no_std_file_error())
475
}
476

            
477
/// Stub: `std` feature disabled.
478
#[cfg(not(feature = "std"))]
479
pub fn dir_create_all(_path: &str) -> Result<EmptyStruct, FileError> {
480
    Err(no_std_file_error())
481
}
482

            
483
/// Stub: `std` feature disabled.
484
#[cfg(not(feature = "std"))]
485
pub fn dir_delete(_path: &str) -> Result<EmptyStruct, FileError> {
486
    Err(no_std_file_error())
487
}
488

            
489
/// Stub: `std` feature disabled.
490
#[cfg(not(feature = "std"))]
491
pub fn dir_delete_all(_path: &str) -> Result<EmptyStruct, FileError> {
492
    Err(no_std_file_error())
493
}
494

            
495
/// Stub: `std` feature disabled.
496
#[cfg(not(feature = "std"))]
497
pub fn dir_list(_path: &str) -> Result<DirEntryVec, FileError> {
498
    Err(no_std_file_error())
499
}
500

            
501
// ============================================================================
502
// Path operations
503
// ============================================================================
504

            
505
/// Join two paths
506
#[cfg(feature = "std")]
507
1
pub fn path_join(base: &str, path: &str) -> AzString {
508
1
    let joined = Path::new(base).join(path);
509
1
    AzString::from(joined.to_string_lossy().to_string())
510
1
}
511

            
512
/// Get the parent directory of a path
513
#[cfg(feature = "std")]
514
pub fn path_parent(path: &str) -> Option<AzString> {
515
    Path::new(path).parent()
516
        .map(|p| AzString::from(p.to_string_lossy().to_string()))
517
}
518

            
519
/// Get the file name from a path
520
#[cfg(feature = "std")]
521
pub fn path_file_name(path: &str) -> Option<AzString> {
522
    Path::new(path).file_name()
523
        .map(|n| AzString::from(n.to_string_lossy().to_string()))
524
}
525

            
526
/// Get the file extension from a path
527
#[cfg(feature = "std")]
528
pub fn path_extension(path: &str) -> Option<AzString> {
529
    Path::new(path).extension()
530
        .map(|e| AzString::from(e.to_string_lossy().to_string()))
531
}
532

            
533
/// Canonicalize a path (resolve symlinks, make absolute)
534
#[cfg(feature = "std")]
535
pub fn path_canonicalize(path: &str) -> Result<AzString, FileError> {
536
    let canonical = std::fs::canonicalize(path)
537
        .map_err(FileError::from_io_error)?;
538
    Ok(AzString::from(canonical.to_string_lossy().to_string()))
539
}
540

            
541
// ============================================================================
542
// Temporary files
543
// ============================================================================
544

            
545
/// Get the system temporary directory
546
#[cfg(feature = "std")]
547
1
pub fn temp_dir() -> AzString {
548
1
    AzString::from(std::env::temp_dir().to_string_lossy().to_string())
549
1
}
550

            
551
/// Stub: `std` feature disabled.
552
#[cfg(not(feature = "std"))]
553
pub fn path_join(_base: &str, _path: &str) -> AzString {
554
    AzString::from_const_str("")
555
}
556

            
557
/// Stub: `std` feature disabled.
558
#[cfg(not(feature = "std"))]
559
pub fn path_parent(_path: &str) -> Option<AzString> {
560
    None
561
}
562

            
563
/// Stub: `std` feature disabled.
564
#[cfg(not(feature = "std"))]
565
pub fn path_file_name(_path: &str) -> Option<AzString> {
566
    None
567
}
568

            
569
/// Stub: `std` feature disabled.
570
#[cfg(not(feature = "std"))]
571
pub fn path_extension(_path: &str) -> Option<AzString> {
572
    None
573
}
574

            
575
/// Stub: `std` feature disabled.
576
#[cfg(not(feature = "std"))]
577
pub fn path_canonicalize(_path: &str) -> Result<AzString, FileError> {
578
    Err(no_std_file_error())
579
}
580

            
581
/// Stub: `std` feature disabled.
582
#[cfg(not(feature = "std"))]
583
pub fn temp_dir() -> AzString {
584
    AzString::from_const_str("")
585
}
586

            
587
// ============================================================================
588
// OOP-style Path wrapper
589
// ============================================================================
590

            
591
/// FFI-safe path type with OOP-style methods
592
/// 
593
/// This wraps a string path and provides method-based access to file operations.
594
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
595
#[repr(C)]
596
pub struct FilePath {
597
    pub inner: AzString,
598
}
599

            
600
// Result type for FilePath operations (must be after FilePath definition)
601
impl_result!(
602
    FilePath,
603
    FileError,
604
    ResultFilePathFileError,
605
    copy = false,
606
    [Debug, Clone, PartialEq]
607
);
608

            
609
// Option type for FilePath
610
impl_option!(FilePath, OptionFilePath, copy = false, [Clone, Debug, PartialEq]);
611

            
612
impl Default for FilePath {
613
    fn default() -> Self {
614
        Self { inner: AzString::from_const_str("") }
615
    }
616
}
617

            
618
impl FilePath {
619
    /// Creates a new path from a string
620
    pub fn new(path: AzString) -> Self {
621
        Self { inner: path }
622
    }
623

            
624
    /// Creates an empty path
625
    pub fn empty() -> Self {
626
        Self::default()
627
    }
628

            
629
    /// Creates a path from a string slice
630
    pub fn from_str(s: &str) -> Self {
631
        Self { inner: AzString::from(String::from(s)) }
632
    }
633

            
634
    /// Returns the system temporary directory
635
    #[cfg(feature = "std")]
636
    pub fn get_temp_dir() -> Self {
637
        Self { inner: temp_dir() }
638
    }
639

            
640
    /// Returns the current working directory
641
    #[cfg(feature = "std")]
642
    pub fn get_current_dir() -> Result<FilePath, FileError> {
643
        match std::env::current_dir() {
644
            Ok(p) => Ok(Self { inner: AzString::from(p.to_string_lossy().into_owned()) }),
645
            Err(e) => Err(FileError::from_io_error(e)),
646
        }
647
    }
648

            
649
    /// Returns the user's home directory (e.g., /home/username on Linux, C:\Users\username on Windows)
650
    #[cfg(all(feature = "std", feature = "extra"))]
651
    pub fn get_home_dir() -> Option<FilePath> {
652
        dirs::home_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
653
    }
654

            
655
    /// Returns the user's cache directory (e.g., ~/.cache on Linux, ~/Library/Caches on macOS)
656
    #[cfg(all(feature = "std", feature = "extra"))]
657
    pub fn get_cache_dir() -> Option<FilePath> {
658
        dirs::cache_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
659
    }
660

            
661
    /// Returns the user's config directory (e.g., ~/.config on Linux, ~/Library/Application Support on macOS)
662
    #[cfg(all(feature = "std", feature = "extra"))]
663
    pub fn get_config_dir() -> Option<FilePath> {
664
        dirs::config_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
665
    }
666

            
667
    /// Returns the user's local config directory (e.g., ~/.config on Linux, ~/Library/Application Support on macOS)
668
    #[cfg(all(feature = "std", feature = "extra"))]
669
    pub fn get_config_local_dir() -> Option<FilePath> {
670
        dirs::config_local_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
671
    }
672

            
673
    /// Returns the user's data directory (e.g., ~/.local/share on Linux, ~/Library/Application Support on macOS)
674
    #[cfg(all(feature = "std", feature = "extra"))]
675
    pub fn get_data_dir() -> Option<FilePath> {
676
        dirs::data_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
677
    }
678

            
679
    /// Returns the user's local data directory (e.g., ~/.local/share on Linux, ~/Library/Application Support on macOS)
680
    #[cfg(all(feature = "std", feature = "extra"))]
681
    pub fn get_data_local_dir() -> Option<FilePath> {
682
        dirs::data_local_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
683
    }
684

            
685
    /// Returns the user's desktop directory (e.g., ~/Desktop)
686
    #[cfg(all(feature = "std", feature = "extra"))]
687
    pub fn get_desktop_dir() -> Option<FilePath> {
688
        dirs::desktop_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
689
    }
690

            
691
    /// Returns the user's documents directory (e.g., ~/Documents)
692
    #[cfg(all(feature = "std", feature = "extra"))]
693
    pub fn get_document_dir() -> Option<FilePath> {
694
        dirs::document_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
695
    }
696

            
697
    /// Returns the user's downloads directory (e.g., ~/Downloads)
698
    #[cfg(all(feature = "std", feature = "extra"))]
699
    pub fn get_download_dir() -> Option<FilePath> {
700
        dirs::download_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
701
    }
702

            
703
    /// Returns the user's executable directory (e.g., ~/.local/bin on Linux)
704
    #[cfg(all(feature = "std", feature = "extra"))]
705
    pub fn get_executable_dir() -> Option<FilePath> {
706
        dirs::executable_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
707
    }
708

            
709
    /// Returns the user's font directory (e.g., ~/.local/share/fonts on Linux, ~/Library/Fonts on macOS)
710
    #[cfg(all(feature = "std", feature = "extra"))]
711
    pub fn get_font_dir() -> Option<FilePath> {
712
        dirs::font_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
713
    }
714

            
715
    /// Returns the user's pictures directory (e.g., ~/Pictures)
716
    #[cfg(all(feature = "std", feature = "extra"))]
717
    pub fn get_picture_dir() -> Option<FilePath> {
718
        dirs::picture_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
719
    }
720

            
721
    /// Returns the user's preference directory (e.g., ~/.config on Linux, ~/Library/Preferences on macOS)
722
    #[cfg(all(feature = "std", feature = "extra"))]
723
    pub fn get_preference_dir() -> Option<FilePath> {
724
        dirs::preference_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
725
    }
726

            
727
    /// Returns the user's public directory (e.g., ~/Public)
728
    #[cfg(all(feature = "std", feature = "extra"))]
729
    pub fn get_public_dir() -> Option<FilePath> {
730
        dirs::public_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
731
    }
732

            
733
    /// Returns the user's runtime directory (e.g., /run/user/1000 on Linux)
734
    #[cfg(all(feature = "std", feature = "extra"))]
735
    pub fn get_runtime_dir() -> Option<FilePath> {
736
        dirs::runtime_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
737
    }
738

            
739
    /// Returns the user's state directory (e.g., ~/.local/state on Linux)
740
    #[cfg(all(feature = "std", feature = "extra"))]
741
    pub fn get_state_dir() -> Option<FilePath> {
742
        dirs::state_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
743
    }
744

            
745
    /// Returns the user's audio directory (e.g., ~/Music)
746
    #[cfg(all(feature = "std", feature = "extra"))]
747
    pub fn get_audio_dir() -> Option<FilePath> {
748
        dirs::audio_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
749
    }
750

            
751
    /// Returns the user's video directory (e.g., ~/Videos)
752
    #[cfg(all(feature = "std", feature = "extra"))]
753
    pub fn get_video_dir() -> Option<FilePath> {
754
        dirs::video_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
755
    }
756

            
757
    /// Returns the user's templates directory
758
    #[cfg(all(feature = "std", feature = "extra"))]
759
    pub fn get_template_dir() -> Option<FilePath> {
760
        dirs::template_dir().map(|p| Self { inner: AzString::from(p.to_string_lossy().into_owned()) })
761
    }
762

            
763
    /// Joins this path with another path component
764
    #[cfg(feature = "std")]
765
    pub fn join(&self, other: &FilePath) -> FilePath {
766
        FilePath { inner: path_join(self.inner.as_str(), other.inner.as_str()) }
767
    }
768

            
769
    /// Joins this path with a string component
770
    #[cfg(feature = "std")]
771
    pub fn join_str(&self, component: &AzString) -> FilePath {
772
        FilePath { inner: path_join(self.inner.as_str(), component.as_str()) }
773
    }
774

            
775
    /// Returns the parent directory of this path
776
    #[cfg(feature = "std")]
777
    pub fn parent(&self) -> Option<FilePath> {
778
        path_parent(self.inner.as_str()).map(|p| FilePath { inner: p })
779
    }
780

            
781
    /// Returns the file name component of this path
782
    #[cfg(feature = "std")]
783
    pub fn file_name(&self) -> Option<AzString> {
784
        path_file_name(self.inner.as_str())
785
    }
786

            
787
    /// Returns the file extension of this path
788
    #[cfg(feature = "std")]
789
    pub fn extension(&self) -> Option<AzString> {
790
        path_extension(self.inner.as_str())
791
    }
792

            
793
    /// Checks if the path exists on the filesystem
794
    #[cfg(feature = "std")]
795
    pub fn exists(&self) -> bool {
796
        path_exists(self.inner.as_str())
797
    }
798

            
799
    /// Checks if the path is a file
800
    #[cfg(feature = "std")]
801
    pub fn is_file(&self) -> bool {
802
        path_is_file(self.inner.as_str())
803
    }
804

            
805
    /// Checks if the path is a directory
806
    #[cfg(feature = "std")]
807
    pub fn is_dir(&self) -> bool {
808
        path_is_dir(self.inner.as_str())
809
    }
810

            
811
    /// Checks if the path is absolute
812
    #[cfg(feature = "std")]
813
    pub fn is_absolute(&self) -> bool {
814
        Path::new(self.inner.as_str()).is_absolute()
815
    }
816

            
817
    /// Creates this directory and all parent directories
818
    #[cfg(feature = "std")]
819
    pub fn create_dir_all(&self) -> Result<EmptyStruct, FileError> {
820
        dir_create_all(self.inner.as_str())
821
    }
822

            
823
    /// Creates this directory (parent must exist)
824
    #[cfg(feature = "std")]
825
    pub fn create_dir(&self) -> Result<EmptyStruct, FileError> {
826
        dir_create(self.inner.as_str())
827
    }
828

            
829
    /// Removes this file
830
    #[cfg(feature = "std")]
831
    pub fn remove_file(&self) -> Result<EmptyStruct, FileError> {
832
        file_delete(self.inner.as_str())
833
    }
834

            
835
    /// Removes this directory (must be empty)
836
    #[cfg(feature = "std")]
837
    pub fn remove_dir(&self) -> Result<EmptyStruct, FileError> {
838
        dir_delete(self.inner.as_str())
839
    }
840

            
841
    /// Removes this directory and all contents
842
    #[cfg(feature = "std")]
843
    pub fn remove_dir_all(&self) -> Result<EmptyStruct, FileError> {
844
        dir_delete_all(self.inner.as_str())
845
    }
846

            
847
    /// Reads the entire file at this path as bytes
848
    #[cfg(feature = "std")]
849
    pub fn read_bytes(&self) -> Result<U8Vec, FileError> {
850
        file_read(self.inner.as_str())
851
    }
852

            
853
    /// Reads the entire file at this path as a string
854
    #[cfg(feature = "std")]
855
    pub fn read_string(&self) -> Result<AzString, FileError> {
856
        file_read_string(self.inner.as_str())
857
    }
858

            
859
    /// Writes bytes to the file at this path
860
    #[cfg(feature = "std")]
861
    pub fn write_bytes(&self, data: &U8Vec) -> Result<EmptyStruct, FileError> {
862
        file_write(self.inner.as_str(), data.as_ref())
863
    }
864

            
865
    /// Writes a string to the file at this path
866
    #[cfg(feature = "std")]
867
    pub fn write_string(&self, data: &AzString) -> Result<EmptyStruct, FileError> {
868
        file_write_string(self.inner.as_str(), data.as_str())
869
    }
870

            
871
    /// Copies a file from this path to another path
872
    #[cfg(feature = "std")]
873
    pub fn copy_to(&self, dest: &FilePath) -> Result<u64, FileError> {
874
        file_copy(self.inner.as_str(), dest.inner.as_str())
875
    }
876

            
877
    /// Renames/moves a file from this path to another path
878
    #[cfg(feature = "std")]
879
    pub fn rename_to(&self, dest: &FilePath) -> Result<EmptyStruct, FileError> {
880
        file_rename(self.inner.as_str(), dest.inner.as_str())
881
    }
882

            
883
    /// Returns the path as a string reference
884
    pub fn as_str(&self) -> &str {
885
        self.inner.as_str()
886
    }
887

            
888
    /// Returns the path as an AzString
889
    pub fn as_string(&self) -> AzString {
890
        self.inner.clone()
891
    }
892

            
893
    /// Lists directory contents
894
    #[cfg(feature = "std")]
895
    pub fn read_dir(&self) -> Result<DirEntryVec, FileError> {
896
        dir_list(self.inner.as_str())
897
    }
898

            
899
    /// Returns metadata about the file/directory
900
    #[cfg(feature = "std")]
901
    pub fn metadata(&self) -> Result<FileMetadata, FileError> {
902
        file_metadata(self.inner.as_str())
903
    }
904

            
905
    /// Makes the path canonical (absolute, with no `.` or `..` components)
906
    #[cfg(feature = "std")]
907
    pub fn canonicalize(&self) -> Result<FilePath, FileError> {
908
        path_canonicalize(self.inner.as_str()).map(|p| FilePath { inner: p })
909
    }
910
}
911

            
912
impl From<String> for FilePath {
913
    fn from(s: String) -> Self {
914
        Self { inner: AzString::from(s) }
915
    }
916
}
917

            
918
impl From<&str> for FilePath {
919
    fn from(s: &str) -> Self {
920
        Self { inner: AzString::from(String::from(s)) }
921
    }
922
}
923

            
924
impl From<AzString> for FilePath {
925
    fn from(s: AzString) -> Self {
926
        Self { inner: s }
927
    }
928
}
929

            
930
#[cfg(test)]
931
mod tests {
932
    use super::*;
933
    
934
    #[test]
935
    #[cfg(feature = "std")]
936
1
    fn test_temp_dir() {
937
1
        let temp = temp_dir();
938
1
        assert!(!temp.as_str().is_empty());
939
1
    }
940
    
941
    #[test]
942
    #[cfg(feature = "std")]
943
1
    fn test_path_join() {
944
1
        let joined = path_join("/home/user", "file.txt");
945
1
        assert!(joined.as_str().contains("file.txt"));
946
1
    }
947
}