Skip to main content

azul_layout/desktop/
file.rs

1//! File I/O wrapper for the desktop C API layer.
2//!
3//! Note: `layout/src/file.rs` provides a more complete file API with
4//! proper error types (`FileError`) and a `FilePath` wrapper.
5
6use 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/// Thread-safe file handle with path tracking for the C API.
17#[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    /// Opens a file in read-only mode, returning `None` on failure.
77    #[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    /// Creates a file (truncating if it exists), returning `None` on failure.
84    #[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    /// Reads the file at `self.path` into a string.
91    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    /// Reads the file at `self.path` into a byte vector.
96    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    /// Writes a string to the file handle. Returns `false` on failure.
101    pub fn write_string(&mut self, string: &str) -> bool {
102        self.write_bytes(string.as_bytes())
103    }
104    /// Writes bytes to the file handle and syncs to disk. Returns `false` on failure.
105    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    /// Closes the file by dropping the handle. Provided for C API symmetry.
112    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    // NOTE ON THE TWO HALVES OF THIS TYPE
123    //
124    // `File` is really two things glued together:
125    //   * a live `fs::File` handle behind `Arc<Mutex<_>>` -- this is what `write_bytes`
126    //     (and therefore `write_string`) talks to, and
127    //   * a `path: AzString` -- this is what `read_to_string` / `read_to_bytes` /
128    //     `Debug` / `Display` / `PartialEq` / `PartialOrd` talk to.
129    //
130    // The two halves can disagree (path deleted under an open handle, path never
131    // pointing at the handle at all -- `File::new` accepts *any* AzString), so most
132    // tests below deliberately probe the seam rather than the happy path.
133
134    /// A temp path that unlinks itself on drop, so a failing assert can't leak files.
135    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    /// A real, open `fs::File` handle whose path is already gone -- lets us build a
171    /// `File` with a *completely arbitrary* `path` field for the fmt/cmp tests.
172    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    // ---------------------------------------------------------------- File::new
184
185    #[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        // `new` never touches the filesystem, so even a path that could never exist
203        // must be stored verbatim rather than validated/normalized/truncated.
204        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    // --------------------------------------------------------------- File::open
221
222    #[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        // Interior NUL cannot be turned into a CString -> must surface as None, not a panic.
247        for path in [
248            "\u{0}",
249            "abc\u{0}def",
250            // NOTE: "//////" is deliberately NOT here -- POSIX collapses a run of
251            // slashes to "/", and opening the root directory read-only legitimately
252            // succeeds. It is not garbage.
253            "::**?<>|\"",
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        // 10_000 nested segments -- exercises the "recursive input" case for a path parser.
298        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        // On unix, open(2) on a directory succeeds, so `File::open` hands back a `File`
309        // whose handle is a directory. Reading through it must still fail cleanly.
310        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    // ------------------------------------------------------------- File::create
347
348    #[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    // ------------------------------------------------------- write/read round-trips
400
401    #[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        // ...and the same content survives a close + reopen cycle.
410        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        // The seam: `read_*` re-opens `self.path` from scratch, so out-of-band changes
503        // to the path are visible and the handle's cursor position is irrelevant.
504        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        // The handle itself is still alive, so writes to it must still succeed.
528        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        // `File::new` is happy to pair a live handle with a bogus path. Reads must then
537        // fail cleanly while writes (which use the handle) still work.
538        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    // --------------------------------------------------- Clone / Drop / close
569
570    #[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        // Both halves write into the same cursor, so the writes interleave in order.
581        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        // The surviving handle still works, and the bytes are still on disk.
602        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    // ------------------------------------------------- Debug / Display / Eq / Ord
618
619    #[test]
620    fn debug_and_display_render_exactly_the_path() {
621        // Includes format-specifier-looking payloads: these must be printed literally,
622        // never interpreted, and never truncated.
623        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        // Re-rendering the re-parsed value is stable (serialize . parse . serialize == serialize).
651        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"); // different handle, same path
667        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        // Ordering must track `str` ordering (byte-wise), including for multibyte paths.
677        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        // Reflexive / symmetric on a clone (which shares the handle).
684        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}