1use 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#[derive(Debug, Clone, PartialEq, Eq)]
25#[repr(C)]
26pub struct FileError {
27 pub message: AzString,
29 pub kind: FileErrorKind,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(C)]
36pub enum FileErrorKind {
37 NotFound,
39 PermissionDenied,
41 AlreadyExists,
43 InvalidPath,
45 IoError,
47 DirectoryNotEmpty,
49 IsDirectory,
51 IsFile,
53 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 #[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
97impl_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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[repr(C)]
140pub enum FileType {
141 File,
143 Directory,
145 Symlink,
147 Other,
149}
150
151#[derive(Copy, Debug, Clone, PartialEq, Eq)]
153#[repr(C)]
154pub struct FileMetadata {
155 pub size: u64,
157 pub file_type: FileType,
159 pub is_readonly: bool,
161 pub modified_secs: u64,
163 pub created_secs: u64,
165}
166
167#[derive(Debug, Clone)]
169#[repr(C)]
170pub struct DirEntry {
171 pub name: AzString,
173 pub path: AzString,
175 pub file_type: FileType,
177}
178
179impl_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
186impl_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#[cfg(feature = "std")]
210pub 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#[cfg(feature = "std")]
221pub 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#[cfg(feature = "std")]
232pub 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#[cfg(feature = "std")]
242pub 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#[cfg(feature = "std")]
252pub 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#[cfg(feature = "std")]
272pub 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#[cfg(feature = "std")]
282pub 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#[cfg(feature = "std")]
292pub 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#[cfg(feature = "std")]
302#[must_use] pub fn path_exists(path: &str) -> bool {
303 Path::new(path).exists()
304}
305
306#[cfg(feature = "std")]
308#[must_use] pub fn path_is_file(path: &str) -> bool {
309 Path::new(path).is_file()
310}
311
312#[cfg(feature = "std")]
314#[must_use] pub fn path_is_dir(path: &str) -> bool {
315 Path::new(path).is_dir()
316}
317
318#[cfg(feature = "std")]
320pub 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#[cfg(not(feature = "std"))]
362fn no_std_file_error() -> FileError {
363 FileError::new(FileErrorKind::Other, "file IO requires the `std` feature")
364}
365
366#[cfg(not(feature = "std"))]
368pub fn file_read(_path: &str) -> Result<U8Vec, FileError> {
369 Err(no_std_file_error())
370}
371
372#[cfg(not(feature = "std"))]
374pub fn file_read_string(_path: &str) -> Result<AzString, FileError> {
375 Err(no_std_file_error())
376}
377
378#[cfg(not(feature = "std"))]
380pub fn file_write(_path: &str, _data: &[u8]) -> Result<EmptyStruct, FileError> {
381 Err(no_std_file_error())
382}
383
384#[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#[cfg(not(feature = "std"))]
392pub fn file_append(_path: &str, _data: &[u8]) -> Result<EmptyStruct, FileError> {
393 Err(no_std_file_error())
394}
395
396#[cfg(not(feature = "std"))]
398pub fn file_copy(_from: &str, _to: &str) -> Result<u64, FileError> {
399 Err(no_std_file_error())
400}
401
402#[cfg(not(feature = "std"))]
404pub fn file_rename(_from: &str, _to: &str) -> Result<EmptyStruct, FileError> {
405 Err(no_std_file_error())
406}
407
408#[cfg(not(feature = "std"))]
410pub fn file_delete(_path: &str) -> Result<EmptyStruct, FileError> {
411 Err(no_std_file_error())
412}
413
414#[cfg(not(feature = "std"))]
416pub fn path_exists(_path: &str) -> bool {
417 false
418}
419
420#[cfg(not(feature = "std"))]
422pub fn path_is_file(_path: &str) -> bool {
423 false
424}
425
426#[cfg(not(feature = "std"))]
428pub fn path_is_dir(_path: &str) -> bool {
429 false
430}
431
432#[cfg(not(feature = "std"))]
434pub fn file_metadata(_path: &str) -> Result<FileMetadata, FileError> {
435 Err(no_std_file_error())
436}
437
438#[cfg(feature = "std")]
444pub 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#[cfg(feature = "std")]
454pub 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#[cfg(feature = "std")]
464pub 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#[cfg(feature = "std")]
474pub 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#[cfg(feature = "std")]
484pub 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#[cfg(not(feature = "std"))]
521pub fn dir_create(_path: &str) -> Result<EmptyStruct, FileError> {
522 Err(no_std_file_error())
523}
524
525#[cfg(not(feature = "std"))]
527pub fn dir_create_all(_path: &str) -> Result<EmptyStruct, FileError> {
528 Err(no_std_file_error())
529}
530
531#[cfg(not(feature = "std"))]
533pub fn dir_delete(_path: &str) -> Result<EmptyStruct, FileError> {
534 Err(no_std_file_error())
535}
536
537#[cfg(not(feature = "std"))]
539pub fn dir_delete_all(_path: &str) -> Result<EmptyStruct, FileError> {
540 Err(no_std_file_error())
541}
542
543#[cfg(not(feature = "std"))]
545pub fn dir_list(_path: &str) -> Result<DirEntryVec, FileError> {
546 Err(no_std_file_error())
547}
548
549#[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#[cfg(feature = "std")]
562pub fn path_parent(path: &str) -> Option<AzString> {
563 Path::new(path).parent()
564 .map(path_to_azstring)
565}
566
567#[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#[cfg(feature = "std")]
576pub fn path_extension(path: &str) -> Option<AzString> {
577 Path::new(path).extension()
578 .map(path_to_azstring)
579}
580
581#[cfg(feature = "std")]
583pub 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#[cfg(feature = "std")]
598#[must_use] pub fn temp_dir() -> AzString {
599 path_to_azstring(std::env::temp_dir())
600}
601
602#[cfg(not(feature = "std"))]
604pub fn path_join(_base: &str, _path: &str) -> AzString {
605 AzString::from_const_str("")
606}
607
608#[cfg(not(feature = "std"))]
610pub fn path_parent(_path: &str) -> Option<AzString> {
611 None
612}
613
614#[cfg(not(feature = "std"))]
616pub fn path_file_name(_path: &str) -> Option<AzString> {
617 None
618}
619
620#[cfg(not(feature = "std"))]
622pub fn path_extension(_path: &str) -> Option<AzString> {
623 None
624}
625
626#[cfg(not(feature = "std"))]
628pub fn path_canonicalize(_path: &str) -> Result<AzString, FileError> {
629 Err(no_std_file_error())
630}
631
632#[cfg(not(feature = "std"))]
634pub fn temp_dir() -> AzString {
635 AzString::from_const_str("")
636}
637
638#[derive(Debug, Clone, PartialEq, Eq, Hash)]
646#[repr(C)]
647pub struct FilePath {
648 pub inner: AzString,
649}
650
651impl_result!(
653 FilePath,
654 FileError,
655 ResultFilePathFileError,
656 copy = false,
657 [Debug, Clone, PartialEq, Eq]
658);
659
660impl_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 #[must_use] pub const fn new(path: AzString) -> Self {
672 Self { inner: path }
673 }
674
675 #[must_use] pub fn empty() -> Self {
677 Self::default()
678 }
679
680 #[must_use] pub fn from_str(s: &str) -> Self {
682 Self { inner: AzString::from(String::from(s)) }
683 }
684
685 #[cfg(feature = "std")]
687 #[must_use] pub fn get_temp_dir() -> Self {
688 Self { inner: temp_dir() }
689 }
690
691 #[cfg(feature = "std")]
693 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[cfg(feature = "std")]
837 #[must_use] pub fn file_name(&self) -> Option<AzString> {
838 path_file_name(self.inner.as_str())
839 }
840
841 #[cfg(feature = "std")]
843 #[must_use] pub fn extension(&self) -> Option<AzString> {
844 path_extension(self.inner.as_str())
845 }
846
847 #[cfg(feature = "std")]
849 #[must_use] pub fn exists(&self) -> bool {
850 path_exists(self.inner.as_str())
851 }
852
853 #[cfg(feature = "std")]
855 #[must_use] pub fn is_file(&self) -> bool {
856 path_is_file(self.inner.as_str())
857 }
858
859 #[cfg(feature = "std")]
861 #[must_use] pub fn is_dir(&self) -> bool {
862 path_is_dir(self.inner.as_str())
863 }
864
865 #[cfg(feature = "std")]
867 #[must_use] pub fn is_absolute(&self) -> bool {
868 Path::new(self.inner.as_str()).is_absolute()
869 }
870
871 #[cfg(feature = "std")]
873 pub fn create_dir_all(&self) -> Result<EmptyStruct, FileError> {
877 dir_create_all(self.inner.as_str())
878 }
879
880 #[cfg(feature = "std")]
882 pub fn create_dir(&self) -> Result<EmptyStruct, FileError> {
886 dir_create(self.inner.as_str())
887 }
888
889 #[cfg(feature = "std")]
891 pub fn remove_file(&self) -> Result<EmptyStruct, FileError> {
895 file_delete(self.inner.as_str())
896 }
897
898 #[cfg(feature = "std")]
900 pub fn remove_dir(&self) -> Result<EmptyStruct, FileError> {
904 dir_delete(self.inner.as_str())
905 }
906
907 #[cfg(feature = "std")]
909 pub fn remove_dir_all(&self) -> Result<EmptyStruct, FileError> {
913 dir_delete_all(self.inner.as_str())
914 }
915
916 #[cfg(feature = "std")]
918 pub fn read_bytes(&self) -> Result<U8Vec, FileError> {
922 file_read(self.inner.as_str())
923 }
924
925 #[cfg(feature = "std")]
927 pub fn read_string(&self) -> Result<AzString, FileError> {
931 file_read_string(self.inner.as_str())
932 }
933
934 #[cfg(feature = "std")]
936 pub fn write_bytes(&self, data: &U8Vec) -> Result<EmptyStruct, FileError> {
940 file_write(self.inner.as_str(), data.as_ref())
941 }
942
943 #[cfg(feature = "std")]
945 pub fn write_string(&self, data: &AzString) -> Result<EmptyStruct, FileError> {
949 file_write_string(self.inner.as_str(), data.as_str())
950 }
951
952 #[cfg(feature = "std")]
954 pub fn copy_to(&self, dest: &Self) -> Result<u64, FileError> {
958 file_copy(self.inner.as_str(), dest.inner.as_str())
959 }
960
961 #[cfg(feature = "std")]
963 pub fn rename_to(&self, dest: &Self) -> Result<EmptyStruct, FileError> {
967 file_rename(self.inner.as_str(), dest.inner.as_str())
968 }
969
970 #[must_use] pub fn as_str(&self) -> &str {
972 self.inner.as_str()
973 }
974
975 #[must_use] pub fn as_string(&self) -> AzString {
977 self.inner.clone()
978 }
979
980 #[cfg(feature = "std")]
982 pub fn read_dir(&self) -> Result<DirEntryVec, FileError> {
986 dir_list(self.inner.as_str())
987 }
988
989 #[cfg(feature = "std")]
991 pub fn metadata(&self) -> Result<FileMetadata, FileError> {
995 file_metadata(self.inner.as_str())
996 }
997
998 #[cfg(feature = "std")]
1000 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 #[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 #[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 #[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 #[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 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 (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 #[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 let bogus = FileError::from_io_error(Error::from_raw_os_error(i32::MAX));
1237 assert_eq!(bogus.kind, FileErrorKind::IoError);
1238 }
1239
1240 #[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 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 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 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 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 let trimmed = case.child("spaced");
1395 assert!(file_read_string(&trimmed).is_err());
1396 assert!(!path_exists(&trimmed));
1397 }
1398
1399 #[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 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 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 #[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 #[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 let m = file_metadata(&link).expect("metadata");
1553 assert_eq!(m.file_type, FileType::Symlink);
1554
1555 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 assert!(!path_exists(&link));
1576 assert_eq!(
1577 file_read(&link).expect_err("dangling target").kind,
1578 FileErrorKind::NotFound
1579 );
1580 }
1581
1582 #[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 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 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 #[test]
1703 #[cfg(all(feature = "std", unix))]
1704 fn path_join_absolute_component_replaces_the_base() {
1705 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 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 assert_eq!(as_opt_string(path_extension(".hidden")), None);
1778 assert_eq!(as_opt_string(path_extension("a.")), Some(String::new()));
1780 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 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 #[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 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 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 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(©).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 let _ = FilePath::get_video_dir();
2087 let _ = FilePath::get_template_dir();
2088 }
2089
2090 #[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 #[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}