1use alloc::sync::Arc;
7use core::fmt;
8use std::{
9 fs,
10 io::{Read, Write},
11 sync::Mutex,
12};
13
14use azul_css::{impl_option, impl_option_inner, AzString, U8Vec};
15
16#[repr(C)]
18pub struct File {
19 pub ptr: Box<Arc<Mutex<fs::File>>>,
20 pub path: AzString,
21 pub run_destructor: bool,
22}
23
24impl Clone for File {
25 fn clone(&self) -> Self {
26 Self {
27 ptr: self.ptr.clone(),
28 path: self.path.clone(),
29 run_destructor: true,
30 }
31 }
32}
33
34impl Drop for File {
35 fn drop(&mut self) {
36 self.run_destructor = false;
37 }
38}
39
40impl fmt::Debug for File {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}", self.path.as_str())
43 }
44}
45
46impl fmt::Display for File {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{}", self.path.as_str())
49 }
50}
51
52impl PartialEq for File {
53 fn eq(&self, other: &Self) -> bool {
54 self.path.as_str().eq(other.path.as_str())
55 }
56}
57
58impl Eq for File {}
59
60impl PartialOrd for File {
61 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
62 self.path.as_str().partial_cmp(other.path.as_str())
63 }
64}
65
66impl_option!(File, OptionFile, copy = false, [Clone, Debug]);
67
68impl File {
69 fn new(f: fs::File, path: AzString) -> Self {
70 Self {
71 ptr: Box::new(Arc::new(Mutex::new(f))),
72 path,
73 run_destructor: true,
74 }
75 }
76 #[must_use] pub fn open(path: &str) -> Option<Self> {
78 Some(Self::new(
79 fs::File::open(path).ok()?,
80 path.to_string().into(),
81 ))
82 }
83 #[must_use] pub fn create(path: &str) -> Option<Self> {
85 Some(Self::new(
86 fs::File::create(path).ok()?,
87 path.to_string().into(),
88 ))
89 }
90 pub fn read_to_string(&mut self) -> Option<AzString> {
92 let file_string = fs::read_to_string(self.path.as_str()).ok()?;
93 Some(file_string.into())
94 }
95 pub fn read_to_bytes(&mut self) -> Option<U8Vec> {
97 let file_bytes = fs::read(self.path.as_str()).ok()?;
98 Some(file_bytes.into())
99 }
100 pub fn write_string(&mut self, string: &str) -> bool {
102 self.write_bytes(string.as_bytes())
103 }
104 pub fn write_bytes(&mut self, bytes: &[u8]) -> bool {
106 let Ok(mut lock) = self.ptr.lock() else {
107 return false;
108 };
109 lock.write_all(bytes).is_ok() && lock.sync_all().is_ok()
110 }
111 pub fn close(self) {}
113}
114
115#[cfg(test)]
116mod autotest_generated {
117 use core::sync::atomic::{AtomicUsize, Ordering};
118 use std::path::PathBuf;
119
120 use super::*;
121
122 struct TempPath(PathBuf);
136
137 impl TempPath {
138 fn new(tag: &str) -> Self {
139 Self(unique_temp_path(tag))
140 }
141 fn as_str(&self) -> &str {
142 self.0.to_str().expect("temp dir must be valid UTF-8")
143 }
144 fn exists(&self) -> bool {
145 self.0.exists()
146 }
147 }
148
149 impl Drop for TempPath {
150 fn drop(&mut self) {
151 let _ = fs::remove_file(&self.0);
152 }
153 }
154
155 fn unique_temp_path(tag: &str) -> PathBuf {
156 static COUNTER: AtomicUsize = AtomicUsize::new(0);
157 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
158 let nanos = std::time::SystemTime::now()
159 .duration_since(std::time::UNIX_EPOCH)
160 .map_or(0, |d| d.as_nanos());
161 std::env::temp_dir().join(format!(
162 "azul_autotest_file_{}_{}_{}_{}",
163 std::process::id(),
164 nanos,
165 n,
166 tag
167 ))
168 }
169
170 fn detached_handle() -> fs::File {
173 let path = unique_temp_path("detached");
174 let handle = fs::File::create(&path).expect("temp dir must be writable");
175 let _ = fs::remove_file(&path);
176 handle
177 }
178
179 fn with_path(path: &str) -> File {
180 File::new(detached_handle(), path.to_string().into())
181 }
182
183 #[test]
186 fn new_sets_fields_and_arms_the_destructor() {
187 let temp = TempPath::new("new_fields");
188 let handle = fs::File::create(temp.as_str()).expect("create");
189 let file = File::new(handle, temp.as_str().to_string().into());
190
191 assert_eq!(file.path.as_str(), temp.as_str());
192 assert!(file.run_destructor, "a freshly built File must own its handle");
193 assert_eq!(
194 Arc::strong_count(&file.ptr),
195 1,
196 "an un-cloned File must be the sole owner of the handle"
197 );
198 }
199
200 #[test]
201 fn new_accepts_extreme_paths_without_panicking() {
202 for path in [
205 "",
206 " ",
207 "\u{0}embedded-nul",
208 "\u{1F600}\u{0301}",
209 "relative/../../../etc/passwd",
210 ] {
211 let file = with_path(path);
212 assert_eq!(file.path.as_str(), path, "path must be stored verbatim");
213 assert!(file.run_destructor);
214 }
215
216 let huge = "x".repeat(100_000);
217 assert_eq!(with_path(&huge).path.as_str().len(), 100_000);
218 }
219
220 #[test]
223 fn open_empty_path_returns_none() {
224 assert!(File::open("").is_none());
225 }
226
227 #[test]
228 fn open_whitespace_only_path_is_not_trimmed_into_something_valid() {
229 for path in [" ", "\t\n", " ", "\r\n\t "] {
230 assert!(
231 File::open(path).is_none(),
232 "whitespace-only path {path:?} must not open anything"
233 );
234 }
235 }
236
237 #[test]
238 fn open_missing_path_returns_none() {
239 let temp = TempPath::new("never_created");
240 assert!(!temp.exists());
241 assert!(File::open(temp.as_str()).is_none());
242 }
243
244 #[test]
245 fn open_garbage_path_returns_none_without_panicking() {
246 for path in [
248 "\u{0}",
249 "abc\u{0}def",
250 "::**?<>|\"",
254 "\u{FFFD}\u{202E}\u{200B}",
255 ] {
256 assert!(
257 File::open(path).is_none(),
258 "garbage path {path:?} must return None"
259 );
260 }
261 }
262
263 #[test]
264 fn open_boundary_number_paths_return_none() {
265 for name in [
266 "0",
267 "-0",
268 "9223372036854775807",
269 "-9223372036854775808",
270 "18446744073709551616",
271 "NaN",
272 "inf",
273 "-inf",
274 "1e400",
275 "0.0000000000000000000001",
276 ] {
277 let path = std::env::temp_dir().join(format!("azul_autotest_missing_{name}"));
278 let path = path.to_str().expect("utf-8 temp dir");
279 assert!(
280 File::open(path).is_none(),
281 "numeric-looking missing path {path:?} must return None"
282 );
283 }
284 }
285
286 #[test]
287 fn open_extremely_long_path_returns_none_and_terminates() {
288 let path = "a".repeat(1_000_000);
289 assert!(
290 File::open(&path).is_none(),
291 "a 1M-char path is over every OS limit and must fail cleanly"
292 );
293 }
294
295 #[test]
296 fn open_deeply_nested_path_does_not_stack_overflow() {
297 let mut path = String::with_capacity(20_002);
299 for _ in 0..10_000 {
300 path.push_str("a/");
301 }
302 path.push('x');
303 assert!(File::open(&path).is_none());
304 }
305
306 #[test]
307 fn open_directory_never_yields_readable_content() {
308 let dir = std::env::temp_dir();
311 let dir = dir.to_str().expect("utf-8 temp dir");
312 if let Some(mut file) = File::open(dir) {
313 assert_eq!(file.path.as_str(), dir);
314 assert!(
315 file.read_to_string().is_none(),
316 "a directory must not read back as a string"
317 );
318 assert!(
319 file.read_to_bytes().is_none(),
320 "a directory must not read back as bytes"
321 );
322 }
323 }
324
325 #[test]
326 fn open_is_read_only_so_writes_fail_and_leave_content_intact() {
327 let temp = TempPath::new("readonly");
328 {
329 let mut file = File::create(temp.as_str()).expect("create");
330 assert!(file.write_string("original"));
331 }
332
333 let mut file = File::open(temp.as_str()).expect("open existing file");
334 assert!(
335 !file.write_string("clobbered"),
336 "writing through a read-only handle must return false, not panic"
337 );
338 assert!(!file.write_bytes(b"clobbered"));
339 assert_eq!(
340 file.read_to_string().expect("read back").as_str(),
341 "original",
342 "a failed write must not have corrupted the file"
343 );
344 }
345
346 #[test]
349 fn create_empty_path_returns_none() {
350 assert!(File::create("").is_none());
351 }
352
353 #[test]
354 fn create_in_missing_directory_returns_none() {
355 let dir = unique_temp_path("missing_dir");
356 let path = dir.join("child.txt");
357 let path = path.to_str().expect("utf-8 temp dir");
358 assert!(File::create(path).is_none());
359 }
360
361 #[test]
362 fn create_over_a_directory_returns_none() {
363 let dir = std::env::temp_dir();
364 let dir = dir.to_str().expect("utf-8 temp dir");
365 assert!(
366 File::create(dir).is_none(),
367 "create() must refuse to truncate a directory"
368 );
369 }
370
371 #[test]
372 fn create_garbage_path_returns_none_without_panicking() {
373 let over_long = "z".repeat(1_000_000);
374 for path in ["\u{0}", "bad\u{0}name", "", over_long.as_str()] {
375 assert!(
376 File::create(path).is_none(),
377 "create() of a {}-char garbage path must return None",
378 path.len()
379 );
380 }
381 }
382
383 #[test]
384 fn create_truncates_existing_content() {
385 let temp = TempPath::new("truncate");
386 {
387 let mut file = File::create(temp.as_str()).expect("create");
388 assert!(file.write_string("a fairly long pre-existing payload"));
389 }
390
391 let mut file = File::create(temp.as_str()).expect("re-create");
392 assert_eq!(
393 file.read_to_string().expect("read back").as_str(),
394 "",
395 "create() must truncate an existing file to zero bytes"
396 );
397 }
398
399 #[test]
402 fn round_trip_representative_string() {
403 let temp = TempPath::new("round_trip");
404 let mut file = File::create(temp.as_str()).expect("create");
405 assert!(file.write_string("hello world"));
406 assert_eq!(file.read_to_string().expect("read").as_str(), "hello world");
407 assert_eq!(file.read_to_bytes().expect("read").as_slice(), b"hello world");
408
409 file.close();
411 let mut reopened = File::open(temp.as_str()).expect("reopen");
412 assert_eq!(reopened.read_to_string().expect("read").as_str(), "hello world");
413 }
414
415 #[test]
416 fn round_trip_empty_write_produces_an_empty_file() {
417 let temp = TempPath::new("empty_write");
418 let mut file = File::create(temp.as_str()).expect("create");
419
420 assert!(file.write_bytes(&[]), "writing zero bytes must still succeed");
421 assert!(file.write_string(""));
422 assert_eq!(file.read_to_string().expect("read").as_str(), "");
423 assert_eq!(file.read_to_bytes().expect("read").len(), 0);
424 }
425
426 #[test]
427 fn round_trip_unicode_content() {
428 let content = "\u{1F600} héllo e\u{0301} \u{202E}rtl\u{202C} \u{0}nul \r\n mixed";
429 let temp = TempPath::new("unicode_content");
430 let mut file = File::create(temp.as_str()).expect("create");
431
432 assert!(file.write_string(content));
433 assert_eq!(file.read_to_string().expect("read").as_str(), content);
434 assert_eq!(
435 file.read_to_bytes().expect("read").as_slice(),
436 content.as_bytes(),
437 "byte round-trip must be exact for multibyte content"
438 );
439 }
440
441 #[test]
442 fn round_trip_unicode_path() {
443 let temp = TempPath::new("p\u{1F600}_h\u{0301}ll\u{00F6}");
444 let mut file = File::create(temp.as_str()).expect("create with unicode path");
445
446 assert_eq!(file.path.as_str(), temp.as_str());
447 assert!(file.write_string("ok"));
448 assert_eq!(file.read_to_string().expect("read").as_str(), "ok");
449
450 let reopened = File::open(temp.as_str()).expect("reopen unicode path");
451 assert_eq!(reopened.path.as_str(), temp.as_str());
452 assert_eq!(reopened, file, "equality is path-based, so these must match");
453 }
454
455 #[test]
456 fn round_trip_all_256_byte_values_and_invalid_utf8_reads_as_none() {
457 let bytes: Vec<u8> = (0..=255u8).collect();
458 let temp = TempPath::new("all_bytes");
459 let mut file = File::create(temp.as_str()).expect("create");
460
461 assert!(file.write_bytes(&bytes));
462 assert_eq!(
463 file.read_to_bytes().expect("read").as_slice(),
464 bytes.as_slice(),
465 "every byte value must survive the round-trip untouched"
466 );
467 assert!(
468 file.read_to_string().is_none(),
469 "non-UTF-8 content must return None, never a lossy string or a panic"
470 );
471 }
472
473 #[test]
474 fn round_trip_one_mib_payload() {
475 let bytes = vec![0xABu8; 1024 * 1024];
476 let temp = TempPath::new("one_mib");
477 let mut file = File::create(temp.as_str()).expect("create");
478
479 assert!(file.write_bytes(&bytes));
480 let read = file.read_to_bytes().expect("read");
481 assert_eq!(read.len(), bytes.len());
482 assert_eq!(read.as_slice(), bytes.as_slice());
483 }
484
485 #[test]
486 fn writes_append_at_the_handle_cursor_rather_than_overwriting() {
487 let temp = TempPath::new("append_cursor");
488 let mut file = File::create(temp.as_str()).expect("create");
489
490 assert!(file.write_string("a"));
491 assert!(file.write_string("bc"));
492 assert!(file.write_bytes(b"d"));
493 assert_eq!(
494 file.read_to_string().expect("read").as_str(),
495 "abcd",
496 "consecutive writes advance the cursor; none of them rewind"
497 );
498 }
499
500 #[test]
501 fn reads_follow_the_path_not_the_handle() {
502 let temp = TempPath::new("path_vs_handle");
505 let mut file = File::create(temp.as_str()).expect("create");
506 assert!(file.write_string("written through the handle"));
507
508 fs::write(temp.as_str(), "replaced out of band").expect("out-of-band write");
509 assert_eq!(
510 file.read_to_string().expect("read").as_str(),
511 "replaced out of band"
512 );
513 }
514
515 #[test]
516 fn reads_return_none_after_the_path_is_deleted_under_an_open_handle() {
517 let temp = TempPath::new("deleted_under_handle");
518 let mut file = File::create(temp.as_str()).expect("create");
519 assert!(file.write_string("content"));
520
521 fs::remove_file(temp.as_str()).expect("unlink");
522 assert!(
523 file.read_to_string().is_none(),
524 "reads go through the path, which is now gone"
525 );
526 assert!(file.read_to_bytes().is_none());
527 assert!(
529 file.write_string("still writable"),
530 "an unlinked-but-open handle is still writable on unix"
531 );
532 }
533
534 #[test]
535 fn read_and_write_on_a_file_whose_path_never_existed() {
536 let mut file = with_path("/this/path/does/not/exist\u{1F600}");
539 assert!(file.read_to_string().is_none());
540 assert!(file.read_to_bytes().is_none());
541 assert!(file.write_string("goes to the detached handle"));
542
543 let mut empty_path = with_path("");
544 assert!(empty_path.read_to_string().is_none());
545 assert!(empty_path.read_to_bytes().is_none());
546 }
547
548 #[test]
549 fn write_bytes_returns_false_on_a_poisoned_mutex() {
550 let temp = TempPath::new("poisoned");
551 let mut file = File::create(temp.as_str()).expect("create");
552
553 let arc = (*file.ptr).clone();
554 let joined = std::thread::spawn(move || {
555 let _guard = arc.lock().expect("first lock cannot be poisoned");
556 panic!("intentional panic to poison the file mutex");
557 })
558 .join();
559 assert!(joined.is_err(), "the helper thread must have panicked");
560
561 assert!(
562 !file.write_bytes(b"nope"),
563 "a poisoned mutex must surface as `false`, not as an unwrap panic"
564 );
565 assert!(!file.write_string("nope"));
566 }
567
568 #[test]
571 fn clone_shares_the_handle_and_the_path() {
572 let temp = TempPath::new("clone_shares");
573 let mut file = File::create(temp.as_str()).expect("create");
574 let mut clone = file.clone();
575
576 assert_eq!(Arc::strong_count(&file.ptr), 2, "clone must share the Arc");
577 assert!(clone.run_destructor);
578 assert_eq!(clone, file);
579
580 assert!(file.write_string("a"));
582 assert!(clone.write_string("b"));
583 assert!(file.write_string("c"));
584 assert_eq!(file.read_to_string().expect("read").as_str(), "abc");
585 }
586
587 #[test]
588 fn close_drops_only_one_owner_and_leaves_the_content_on_disk() {
589 let temp = TempPath::new("close");
590 let mut file = File::create(temp.as_str()).expect("create");
591 assert!(file.write_string("persisted"));
592
593 let clone = file.clone();
594 clone.close();
595 assert_eq!(
596 Arc::strong_count(&file.ptr),
597 1,
598 "closing a clone must release exactly one Arc reference"
599 );
600
601 assert!(file.write_string("!"));
603 assert_eq!(file.read_to_string().expect("read").as_str(), "persisted!");
604
605 file.close();
606 let mut reopened = File::open(temp.as_str()).expect("reopen after close");
607 assert_eq!(reopened.read_to_string().expect("read").as_str(), "persisted!");
608 }
609
610 #[test]
611 fn close_on_an_extreme_file_does_not_panic() {
612 with_path("").close();
613 with_path(&"q".repeat(100_000)).close();
614 with_path("\u{1F600}\u{0}\u{202E}").close();
615 }
616
617 #[test]
620 fn debug_and_display_render_exactly_the_path() {
621 for path in [
624 "/tmp/plain.txt",
625 "",
626 "{}",
627 "{0} {:?} %s %n",
628 "\u{1F600} h\u{00E9}llo e\u{0301}",
629 "line\nbreak\ttab",
630 ] {
631 let file = with_path(path);
632 assert_eq!(format!("{file}"), path, "Display must echo the path verbatim");
633 assert_eq!(format!("{file:?}"), path, "Debug must echo the path verbatim");
634 assert_eq!(
635 format!("{file}"),
636 format!("{file:?}"),
637 "Debug and Display must agree"
638 );
639 }
640 }
641
642 #[test]
643 fn display_is_non_empty_for_a_real_file_and_idempotent() {
644 let temp = TempPath::new("display_stable");
645 let file = File::create(temp.as_str()).expect("create");
646
647 let once = format!("{file}");
648 assert!(!once.is_empty());
649 assert_eq!(once, temp.as_str());
650 let reopened = File::open(&once).expect("the rendered path must reopen the same file");
652 assert_eq!(format!("{reopened}"), once);
653 assert_eq!(reopened, file);
654 }
655
656 #[test]
657 fn display_of_a_huge_path_is_not_truncated() {
658 let path = "w".repeat(300_000);
659 let file = with_path(&path);
660 assert_eq!(format!("{file}").len(), 300_000);
661 }
662
663 #[test]
664 fn eq_and_ord_are_purely_path_based() {
665 let a1 = with_path("a");
666 let a2 = with_path("a"); let b = with_path("b");
668
669 assert_eq!(a1, a2, "equality ignores the handle entirely");
670 assert_ne!(a1, b);
671 assert_eq!(a1.partial_cmp(&a2), Some(core::cmp::Ordering::Equal));
672 assert_eq!(a1.partial_cmp(&b), Some(core::cmp::Ordering::Less));
673 assert_eq!(b.partial_cmp(&a1), Some(core::cmp::Ordering::Greater));
674 assert!(a1 < b);
675
676 let empty = with_path("");
678 let emoji = with_path("\u{1F600}");
679 assert_eq!(empty.partial_cmp(&emoji), "".partial_cmp("\u{1F600}"));
680 assert_eq!(emoji.partial_cmp(&empty), "\u{1F600}".partial_cmp(""));
681 assert_eq!(emoji.partial_cmp(&emoji.clone()), Some(core::cmp::Ordering::Equal));
682
683 let cloned = a1.clone();
685 assert_eq!(a1, cloned);
686 assert_eq!(cloned, a1);
687 }
688
689 #[test]
690 fn option_file_round_trips_through_the_ffi_option() {
691 assert!(matches!(OptionFile::default(), OptionFile::None));
692 assert!(OptionFile::default().as_option().is_none());
693
694 let none: OptionFile = Option::<File>::None.into();
695 assert!(Option::<File>::from(none).is_none());
696
697 let file = with_path("\u{1F600}/round/trip");
698 let ffi: OptionFile = Some(file).into();
699 assert_eq!(
700 ffi.as_option().expect("Some").path.as_str(),
701 "\u{1F600}/round/trip"
702 );
703
704 let back = Option::<File>::from(ffi).expect("Some survives the round-trip");
705 assert_eq!(back.path.as_str(), "\u{1F600}/round/trip");
706 assert!(back.run_destructor);
707 }
708}