Skip to main content

compose_rust/
picked_file.rs

1//! Reading and writing what the user chose in a file dialog, the same way on every platform.
2//!
3//! [`FileDialogResult::files`] turns a choice into [`PickedFile`]s. What one holds depends on
4//! where the application runs, and the application does not have to know:
5//!
6//! - On a desktop or a phone it is a path. [`PickedFile::read`], [`PickedFile::write`],
7//!   [`PickedFile::entries`] and [`PickedFile::child`] run on a Host worker with `std::fs`, so
8//!   the UI thread never waits for the disk. A write goes to a temporary file beside the
9//!   target and is renamed over it, so a reader never sees half of it.
10//! - In a browser there is no path. The Renderer keeps the browser's handle for the file and
11//!   the Host keeps its number. The dialog's answer carries one reference per file in
12//!   `Chosen.paths`, written `webfile:<handle>/<name>`, and `files()` turns each into a
13//!   `PickedFile`. Every operation then crosses to the Renderer as one `FileCall` record and
14//!   comes back as one event: an `IoAnswer` with the bytes or the reason, or a `FileHandles`
15//!   with the files a folder holds. When the last `PickedFile` for a handle is dropped, the
16//!   Host tells the Renderer to let go of it with `ReleaseFileHandle`.
17//!
18//! ```ignore
19//! FileDialog::open_file().filter("Text", &["txt", "md"]).show(move |result| {
20//!     if let Some(file) = result.files().into_iter().next() {
21//!         scope.launch(async move {
22//!             if let Ok(bytes) = file.read().await {
23//!                 document.set(String::from_utf8_lossy(&bytes).into_owned());
24//!             }
25//!         });
26//!     }
27//! });
28//! ```
29//!
30//! How a file reached the application is [`PickedFile::delivery`]. Only a
31//! [`Delivery::Handle`] can be written again in place: a browser without the File System
32//! Access API hands over a copy to read ([`Delivery::Upload`]) or turns a save into a download
33//! ([`Delivery::Download`]), and an application treats its next save as "save as" when
34//! [`PickedFile::can_write_again`] is false.
35//!
36//! The operations are futures to await on the UI thread, from an effect, a coroutine scope or
37//! a handler. They are not `Send` on the web, where the answer comes back on the UI thread
38//! inside a Host call.
39
40use crate::file_dialog::FileDialogResult;
41use crate::schema::{FileDelivery, FileDialogStatus, FileOp, IoStatus};
42use std::cell::RefCell;
43use std::collections::HashMap;
44use std::fmt;
45use std::future::Future;
46use std::pin::Pin;
47use std::rc::Rc;
48use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
49use std::sync::{Arc, Mutex, MutexGuard, OnceLock, Weak};
50use std::task::{Context, Poll, Waker};
51
52/// How a file reached the application.
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
54pub enum Delivery {
55    /// The file itself, in the file system. Writing it again changes the same file. Every
56    /// file on a desktop or a phone is delivered this way.
57    Handle,
58    /// A copy the browser handed over from a file input. It can be read and not written.
59    Upload,
60    /// A save that became a download. Every write is a new download, and the browser may
61    /// rename it, so the same file cannot be changed again.
62    Download,
63}
64
65impl From<FileDelivery> for Delivery {
66    fn from(value: FileDelivery) -> Self {
67        match value {
68            FileDelivery::Handle => Delivery::Handle,
69            FileDelivery::Upload => Delivery::Upload,
70            FileDelivery::Download => Delivery::Download,
71        }
72    }
73}
74
75/// Why an operation on a [`PickedFile`] did not happen.
76#[non_exhaustive]
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum FileError {
79    /// The file is not there any more, or, in a browser, its handle was let go of or belongs
80    /// to a page that has since been reloaded.
81    NotFound,
82    /// The platform or the user refused access.
83    PermissionDenied,
84    /// The file can only be read: an upload, or one the platform will not let this run write.
85    ReadOnly,
86    /// A listing or a child was asked of a file.
87    NotAFolder,
88    /// A read or a write was asked of a folder.
89    IsAFolder,
90    /// The file is longer than one read may carry. Nothing was read. `len` is its length when
91    /// it is known, and `limit + 1` when all that is known is that it is longer.
92    TooLarge { len: u64, limit: u64 },
93    /// This run has no way to do it. The reason is in the text.
94    Unavailable(String),
95    /// Any other failure, as the platform described it.
96    Io(String),
97}
98
99impl fmt::Display for FileError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            FileError::NotFound => f.write_str("the file is not there any more"),
103            FileError::PermissionDenied => f.write_str("access to the file was refused"),
104            FileError::ReadOnly => f.write_str("the file can only be read"),
105            FileError::NotAFolder => f.write_str("the file is not a folder"),
106            FileError::IsAFolder => f.write_str("the file is a folder"),
107            FileError::TooLarge { len, limit } => write!(
108                f,
109                "the file is {len} bytes, more than the {limit} bytes one read may carry"
110            ),
111            FileError::Unavailable(reason) => write!(f, "unavailable here: {reason}"),
112            FileError::Io(reason) => f.write_str(reason),
113        }
114    }
115}
116
117impl std::error::Error for FileError {}
118
119/// One file or folder the user chose, to read and write.
120///
121/// Cheap to clone: a clone names the same file. See the module documentation for what it
122/// holds on each platform.
123#[derive(Clone)]
124pub struct PickedFile {
125    inner: Inner,
126}
127
128#[derive(Clone)]
129enum Inner {
130    Path(PathFile),
131    Web(Arc<WebFile>),
132}
133
134#[derive(Clone)]
135struct PathFile {
136    path: String,
137    name: String,
138    /// Asked of the file system the first time it is wanted, because a dialog's answer says
139    /// only where a path is and not what it is.
140    folder: OnceLock<bool>,
141}
142
143/// The Host's half of a handle the Renderer keeps. Shared by every `PickedFile` for it; the
144/// last one dropped lets the Renderer go of the handle.
145struct WebFile {
146    handle: u32,
147    name: String,
148    folder: bool,
149    delivery: Delivery,
150    /// Made from a reference whose handle was already let go of. Every operation on it is
151    /// `NotFound` without asking, and dropping it releases nothing.
152    released: bool,
153}
154
155impl Drop for WebFile {
156    fn drop(&mut self) {
157        if self.released {
158            return;
159        }
160        let mut handles = registry();
161        // A `files()` racing this drop may have made a new holder for the handle already;
162        // then the handle is still in use and stays.
163        let still_held = handles
164            .get(&self.handle)
165            .and_then(|entry| entry.live.as_ref())
166            .is_some_and(|live| live.strong_count() > 0);
167        if still_held {
168            return;
169        }
170        handles.remove(&self.handle);
171        drop(handles);
172        queue_release(self.handle);
173    }
174}
175
176impl fmt::Debug for PickedFile {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match &self.inner {
179            Inner::Path(file) => f
180                .debug_struct("PickedFile")
181                .field("path", &file.path)
182                .finish(),
183            Inner::Web(file) => f
184                .debug_struct("PickedFile")
185                .field("handle", &file.handle)
186                .field("name", &file.name)
187                .field("folder", &file.folder)
188                .field("delivery", &file.delivery)
189                .finish(),
190        }
191    }
192}
193
194impl PartialEq for PickedFile {
195    fn eq(&self, other: &Self) -> bool {
196        match (&self.inner, &other.inner) {
197            (Inner::Path(left), Inner::Path(right)) => left.path == right.path,
198            (Inner::Web(left), Inner::Web(right)) => left.handle == right.handle,
199            _ => false,
200        }
201    }
202}
203
204impl Eq for PickedFile {}
205
206/// What a `webfile:` reference starts with.
207const WEB_REFERENCE_PREFIX: &str = "webfile:";
208
209/// The bytes one answer from a browser Renderer can carry: the largest event buffer the Host
210/// lends it, less the `IoAnswer` record in front of the bytes.
211const WEB_ANSWER_BYTES: u64 = crate::schema::WEB_EVENT_BUFFER_MAX_BYTES as u64 - 32;
212
213impl PickedFile {
214    /// The longest file [`PickedFile::read`] reads, in bytes. A longer one is
215    /// [`FileError::TooLarge`] before anything is read.
216    pub const MAX_READ_BYTES: u64 = 256 * 1024 * 1024;
217
218    /// A file by its path, without a dialog. For a path the application already had, such as
219    /// one it saved in its own storage last time.
220    ///
221    /// Only where there are paths: a browser gives a page none, so there is nothing to make
222    /// one from there.
223    #[cfg(not(target_arch = "wasm32"))]
224    pub fn from_path(path: impl Into<String>) -> PickedFile {
225        PickedFile::at_path(path.into(), None)
226    }
227
228    fn at_path(path: String, folder: Option<bool>) -> PickedFile {
229        let name = std::path::Path::new(&path)
230            .file_name()
231            .map(|name| name.to_string_lossy().into_owned())
232            .unwrap_or_else(|| path.clone());
233        let known = OnceLock::new();
234        if let Some(folder) = folder {
235            let _ = known.set(folder);
236        }
237        PickedFile {
238            inner: Inner::Path(PathFile {
239                path,
240                name,
241                folder: known,
242            }),
243        }
244    }
245
246    /// The file's name, without any folder in front of it.
247    pub fn name(&self) -> &str {
248        match &self.inner {
249            Inner::Path(file) => &file.name,
250            Inner::Web(file) => &file.name,
251        }
252    }
253
254    /// The file's absolute path, where there is one. Never in a browser.
255    pub fn path(&self) -> Option<&str> {
256        match &self.inner {
257            Inner::Path(file) => Some(&file.path),
258            Inner::Web(_) => None,
259        }
260    }
261
262    /// Whether this is a folder.
263    ///
264    /// For a path, the file system is asked the first time, which is one small call on the
265    /// calling thread. A browser said so when it handed the file over.
266    pub fn is_folder(&self) -> bool {
267        match &self.inner {
268            Inner::Path(file) => *file
269                .folder
270                .get_or_init(|| std::path::Path::new(&file.path).is_dir()),
271            Inner::Web(file) => file.folder,
272        }
273    }
274
275    /// How the file reached the application. Always [`Delivery::Handle`] for a path.
276    pub fn delivery(&self) -> Delivery {
277        match &self.inner {
278            Inner::Path(_) => Delivery::Handle,
279            Inner::Web(file) => file.delivery,
280        }
281    }
282
283    /// Whether a write changes this same file again: a [`Delivery::Handle`] that is not a
284    /// folder. When it is false the next save is a "save as".
285    pub fn can_write_again(&self) -> bool {
286        self.delivery() == Delivery::Handle && !self.is_folder()
287    }
288
289    /// Reads the whole file.
290    pub async fn read(&self) -> Result<Vec<u8>, FileError> {
291        match &self.inner {
292            Inner::Path(file) => {
293                let path = file.path.clone();
294                crate::runtime::with_worker(move || native::read(&path)).await
295            }
296            Inner::Web(file) => {
297                if file.released {
298                    return Err(FileError::NotFound);
299                }
300                if file.folder {
301                    return Err(FileError::IsAFolder);
302                }
303                match web_call(FileOp::Read, false, file.handle, String::new(), Vec::new()).await {
304                    Answer::Io {
305                        status: IoStatus::Ok,
306                        data,
307                    } => Ok(data),
308                    Answer::Io { status, .. } => Err(error_of(status)),
309                    Answer::Files { .. } => Err(unexpected_answer("a read")),
310                }
311            }
312        }
313    }
314
315    /// Replaces the file's contents with `bytes`.
316    ///
317    /// For a path the new contents go to a temporary file in the same folder, which is then
318    /// renamed over the file, so the file is always either what it was or what was written.
319    /// A browser does the same inside the File System Access API's writable stream. On a
320    /// [`Delivery::Download`] every write is a new download of `bytes` under the file's name.
321    pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), FileError> {
322        let bytes = bytes.into();
323        match &self.inner {
324            Inner::Path(file) => {
325                let path = file.path.clone();
326                crate::runtime::with_worker(move || native::write(&path, &bytes)).await
327            }
328            Inner::Web(file) => {
329                if file.released {
330                    return Err(FileError::NotFound);
331                }
332                if file.folder {
333                    return Err(FileError::IsAFolder);
334                }
335                if file.delivery == Delivery::Upload {
336                    return Err(FileError::ReadOnly);
337                }
338                match web_call(FileOp::Write, false, file.handle, String::new(), bytes).await {
339                    Answer::Io {
340                        status: IoStatus::Ok,
341                        ..
342                    } => Ok(()),
343                    Answer::Io { status, .. } => Err(error_of(status)),
344                    Answer::Files { .. } => Err(unexpected_answer("a write")),
345                }
346            }
347        }
348    }
349
350    /// What a folder holds directly, files and folders both, in name order for a path and
351    /// in the browser's order in a browser.
352    pub async fn entries(&self) -> Result<Vec<PickedFile>, FileError> {
353        match &self.inner {
354            Inner::Path(file) => {
355                let path = file.path.clone();
356                crate::runtime::with_worker(move || native::entries(&path)).await
357            }
358            Inner::Web(file) => {
359                if file.released {
360                    return Err(FileError::NotFound);
361                }
362                if !file.folder {
363                    return Err(FileError::NotAFolder);
364                }
365                match web_call(
366                    FileOp::Entries,
367                    false,
368                    file.handle,
369                    String::new(),
370                    Vec::new(),
371                )
372                .await
373                {
374                    Answer::Files(files) => Ok(files),
375                    Answer::Io { status, .. } => Err(error_of(status)),
376                }
377            }
378        }
379    }
380
381    /// The file or folder called `name` directly inside this folder. With `create`, a file of
382    /// that name is made, empty, when there is none.
383    ///
384    /// `name` is one name: one with a path separator in it, or `.` or `..`, is refused before
385    /// anything is asked.
386    pub async fn child(&self, name: &str, create: bool) -> Result<PickedFile, FileError> {
387        if !is_plain_name(name) {
388            return Err(FileError::Io(format!(
389                "{name:?} is not the name of one file: it is empty, holds a path separator or a NUL, or is . or .."
390            )));
391        }
392        match &self.inner {
393            Inner::Path(file) => {
394                let path = file.path.clone();
395                let name = name.to_owned();
396                crate::runtime::with_worker(move || native::child(&path, &name, create)).await
397            }
398            Inner::Web(file) => {
399                if file.released {
400                    return Err(FileError::NotFound);
401                }
402                if !file.folder {
403                    return Err(FileError::NotAFolder);
404                }
405                match web_call(
406                    FileOp::Child,
407                    create,
408                    file.handle,
409                    name.to_owned(),
410                    Vec::new(),
411                )
412                .await
413                {
414                    Answer::Files(mut files) if files.len() == 1 => Ok(files.remove(0)),
415                    Answer::Files(files) => Err(FileError::Io(format!(
416                        "the browser answered a request for one child with {} files",
417                        files.len()
418                    ))),
419                    Answer::Io { status, .. } => Err(error_of(status)),
420                }
421            }
422        }
423    }
424}
425
426fn is_plain_name(name: &str) -> bool {
427    !name.is_empty() && name != "." && name != ".." && !name.contains(['/', '\\', '\0'])
428}
429
430fn unexpected_answer(what: &str) -> FileError {
431    FileError::Io(format!(
432        "the browser answered {what} with a list of files instead of bytes"
433    ))
434}
435
436/// What an `IoAnswer` status means for a file. `Ok` never gets here.
437fn error_of(status: IoStatus) -> FileError {
438    match status {
439        IoStatus::NotFound => FileError::NotFound,
440        IoStatus::PermissionDenied => FileError::PermissionDenied,
441        IoStatus::ReadOnly => FileError::ReadOnly,
442        IoStatus::TooLarge => FileError::TooLarge {
443            len: WEB_ANSWER_BYTES + 1,
444            limit: WEB_ANSWER_BYTES,
445        },
446        IoStatus::QuotaExceeded => {
447            FileError::Io("the browser has no room left to write the file".to_owned())
448        }
449        IoStatus::Unavailable => FileError::Unavailable(
450            "the Renderer has no way to reach this file in this run".to_owned(),
451        ),
452        IoStatus::Failed | IoStatus::Ok => {
453            FileError::Io("the browser could not finish the operation".to_owned())
454        }
455    }
456}
457
458impl FileDialogResult {
459    /// What was chosen, as files to read and write: in the same order as `paths` and as
460    /// many, or none when the dialog did not end in a choice.
461    pub fn files(&self) -> Vec<PickedFile> {
462        match self {
463            FileDialogResult::Chosen { paths, .. } => {
464                paths.iter().map(|path| picked_from_entry(path)).collect()
465            }
466            FileDialogResult::Cancelled | FileDialogResult::Unavailable => Vec::new(),
467        }
468    }
469}
470
471/// One entry of `Chosen.paths` as a file: a `webfile:` reference becomes the Renderer's
472/// handle, anything else is a path. No path on any platform starts with `webfile:`, because
473/// a dialog's paths are absolute.
474fn picked_from_entry(entry: &str) -> PickedFile {
475    match parse_reference(entry) {
476        Some((handle, name)) => web_file(handle, name),
477        None => PickedFile::at_path(entry.to_owned(), None),
478    }
479}
480
481/// The handle and name of a `webfile:<handle>/<name>` reference.
482fn parse_reference(entry: &str) -> Option<(u32, &str)> {
483    let rest = entry.strip_prefix(WEB_REFERENCE_PREFIX)?;
484    let (number, name) = rest.split_once('/')?;
485    if number.is_empty() || !number.bytes().all(|digit| digit.is_ascii_digit()) {
486        return None;
487    }
488    Some((number.parse().ok()?, name))
489}
490
491fn reference(handle: u32, name: &str) -> String {
492    format!("{WEB_REFERENCE_PREFIX}{handle}/{name}")
493}
494
495// ----- the handles the Renderer holds ------------------------------------------------------
496
497/// What the Host knows about one handle the Renderer holds, from the event that named it.
498struct HandleEntry {
499    name: String,
500    folder: bool,
501    delivery: Delivery,
502    /// The `PickedFile`s for it share this. None until `files()` made the first one.
503    live: Option<Weak<WebFile>>,
504}
505
506static REGISTRY: OnceLock<Mutex<HashMap<u32, HandleEntry>>> = OnceLock::new();
507
508fn registry() -> MutexGuard<'static, HashMap<u32, HandleEntry>> {
509    REGISTRY
510        .get_or_init(|| Mutex::new(HashMap::new()))
511        .lock()
512        .unwrap_or_else(|poison| poison.into_inner())
513}
514
515/// Remembers the handles one `FileHandles` event named, and returns their references in
516/// order.
517fn register(
518    first_handle: u32,
519    delivery: Delivery,
520    names: &str,
521    kinds: &str,
522) -> Vec<(u32, String, bool)> {
523    let names: Vec<&str> = if names.is_empty() {
524        Vec::new()
525    } else {
526        names.split('\0').collect()
527    };
528    let mut handles = registry();
529    names
530        .iter()
531        .zip(kinds.bytes())
532        .enumerate()
533        .map(|(index, (name, kind))| {
534            let handle = first_handle.wrapping_add(index as u32);
535            let folder = kind == b'd';
536            handles.insert(
537                handle,
538                HandleEntry {
539                    name: (*name).to_owned(),
540                    folder,
541                    delivery,
542                    live: None,
543                },
544            );
545            (handle, (*name).to_owned(), folder)
546        })
547        .collect()
548}
549
550/// The `PickedFile` for a handle, shared with every other one for the same handle.
551fn web_file(handle: u32, name: &str) -> PickedFile {
552    let mut handles = registry();
553    let Some(entry) = handles.get_mut(&handle) else {
554        // Let go of already, or never named: its operations are `NotFound`, which is what
555        // the Renderer would answer too.
556        return PickedFile {
557            inner: Inner::Web(Arc::new(WebFile {
558                handle,
559                name: name.to_owned(),
560                folder: false,
561                delivery: Delivery::Handle,
562                released: true,
563            })),
564        };
565    };
566    if let Some(live) = entry.live.as_ref().and_then(Weak::upgrade) {
567        return PickedFile {
568            inner: Inner::Web(live),
569        };
570    }
571    let file = Arc::new(WebFile {
572        handle,
573        name: entry.name.clone(),
574        folder: entry.folder,
575        delivery: entry.delivery,
576        released: false,
577    });
578    entry.live = Some(Arc::downgrade(&file));
579    PickedFile {
580        inner: Inner::Web(file),
581    }
582}
583
584// ----- what leaves for the Renderer --------------------------------------------------------
585
586/// One `FileCall` waiting for the next batch.
587pub(crate) struct Call {
588    pub(crate) request: u32,
589    pub(crate) op: FileOp,
590    pub(crate) create: bool,
591    pub(crate) handle: u32,
592    pub(crate) name: String,
593    pub(crate) data: Vec<u8>,
594}
595
596static CALLS: Mutex<Vec<Call>> = Mutex::new(Vec::new());
597static RELEASES: Mutex<Vec<u32>> = Mutex::new(Vec::new());
598/// Whether either queue has anything in it, read without a lock, so a batch with nothing to
599/// write pays one atomic load.
600static PENDING: AtomicBool = AtomicBool::new(false);
601
602/// The next request number. File operations number theirs with the top bit set, so a
603/// request of theirs is never one a dialog or the application's storage numbered, and an
604/// answer finds its waiter by number alone.
605static NEXT_REQUEST: AtomicU32 = AtomicU32::new(1);
606pub(crate) const REQUEST_BIT: u32 = 0x8000_0000;
607
608fn next_request() -> u32 {
609    loop {
610        let low = NEXT_REQUEST.fetch_add(1, Ordering::Relaxed) & !REQUEST_BIT;
611        if low != 0 {
612            return REQUEST_BIT | low;
613        }
614    }
615}
616
617fn lock<T>(mutex: &'static Mutex<T>) -> MutexGuard<'static, T> {
618    // A thread that panicked while holding the lock does not make the queue wrong.
619    mutex.lock().unwrap_or_else(|poison| poison.into_inner())
620}
621
622fn wake_batch() {
623    PENDING.store(true, Ordering::Release);
624    // Inside a Host call the batch that call produces carries it.
625    if !crate::boundary::in_host_call() {
626        crate::boundary::request_frame_from_worker();
627    }
628}
629
630fn queue_release(handle: u32) {
631    lock(&RELEASES).push(handle);
632    wake_batch();
633}
634
635/// Hands every queued release and call to the batch, releases first, and empties the queues.
636pub(crate) fn drain(mut release: impl FnMut(u32), mut call: impl FnMut(&Call)) {
637    if !PENDING.swap(false, Ordering::AcqRel) {
638        return;
639    }
640    let releases = std::mem::take(&mut *lock(&RELEASES));
641    for handle in releases {
642        release(handle);
643    }
644    let calls = std::mem::take(&mut *lock(&CALLS));
645    for waiting in &calls {
646        call(waiting);
647    }
648    if !lock(&RELEASES).is_empty() || !lock(&CALLS).is_empty() {
649        // Something was queued while these were being written, and is still waiting.
650        PENDING.store(true, Ordering::Release);
651    }
652}
653
654// ----- answers ------------------------------------------------------------------------------
655
656enum Answer {
657    Io { status: IoStatus, data: Vec<u8> },
658    Files(Vec<PickedFile>),
659}
660
661#[derive(Default)]
662struct Slot {
663    answer: Option<Answer>,
664    waker: Option<Waker>,
665}
666
667thread_local! {
668    /// The operations waiting for their answer on this thread, by request number.
669    static WAITING: RefCell<HashMap<u32, Rc<RefCell<Slot>>>> = RefCell::new(HashMap::new());
670}
671
672fn answer(request: u32, answer: Answer) -> bool {
673    let Some(slot) = WAITING.with_borrow_mut(|waiting| waiting.remove(&request)) else {
674        return false;
675    };
676    let waker = {
677        let mut slot = slot.borrow_mut();
678        slot.answer = Some(answer);
679        slot.waker.take()
680    };
681    if let Some(waker) = waker {
682        waker.wake();
683    }
684    true
685}
686
687/// Hands an `IoAnswer` to the file operation that asked for it. Returns whether one did:
688/// a request that is not a file operation's, or is no longer waiting, is someone else's.
689pub(crate) fn deliver_io(request: u32, status: IoStatus, data: &[u8]) -> bool {
690    if request & REQUEST_BIT == 0 {
691        return false;
692    }
693    self::answer(
694        request,
695        Answer::Io {
696            status,
697            data: data.to_vec(),
698        },
699    )
700}
701
702/// Hands a `FileHandles` event to whoever asked: a folder's listing or child, or else the
703/// file dialog with that number, as a choice of `webfile:` references. Returns whether
704/// anybody was waiting.
705pub(crate) fn deliver_handles(
706    request: u32,
707    status: FileDialogStatus,
708    filter: u32,
709    delivery: FileDelivery,
710    first_handle: u32,
711    names: &str,
712    kinds: &str,
713) -> bool {
714    let delivery = Delivery::from(delivery);
715    let named = register(first_handle, delivery, names, kinds);
716    if request & REQUEST_BIT != 0 {
717        let files = named
718            .iter()
719            .map(|(handle, name, _)| web_file(*handle, name))
720            .collect();
721        return self::answer(request, Answer::Files(files));
722    }
723    if status != FileDialogStatus::Chosen {
724        // Only a choice comes this way; the other two ends come as `FileDialogResult`.
725        return crate::file_dialog::deliver(request, FileDialogStatus::Unavailable, u32::MAX, "");
726    }
727    let paths: Vec<String> = named
728        .iter()
729        .map(|(handle, name, _)| reference(*handle, name))
730        .collect();
731    crate::file_dialog::deliver(request, status, filter, &paths.join("\0"))
732}
733
734/// One operation sent to the Renderer, resolved by its answer. Asked for on the first poll;
735/// a future dropped before then sends nothing, and one dropped after leaves its answer to
736/// be ignored.
737fn web_call(op: FileOp, create: bool, handle: u32, name: String, data: Vec<u8>) -> WebCall {
738    WebCall {
739        call: Some(Call {
740            request: 0,
741            op,
742            create,
743            handle,
744            name,
745            data,
746        }),
747        slot: None,
748    }
749}
750
751struct WebCall {
752    call: Option<Call>,
753    slot: Option<Rc<RefCell<Slot>>>,
754}
755
756impl Unpin for WebCall {}
757
758impl Future for WebCall {
759    type Output = Answer;
760
761    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Answer> {
762        if let Some(mut call) = self.call.take() {
763            let request = next_request();
764            call.request = request;
765            let slot = Rc::new(RefCell::new(Slot {
766                answer: None,
767                waker: Some(context.waker().clone()),
768            }));
769            WAITING.with_borrow_mut(|waiting| waiting.insert(request, Rc::clone(&slot)));
770            self.slot = Some(slot);
771            lock(&CALLS).push(call);
772            wake_batch();
773            return Poll::Pending;
774        }
775        let Some(slot) = &self.slot else {
776            return Poll::Pending;
777        };
778        let mut slot = slot.borrow_mut();
779        match slot.answer.take() {
780            Some(answer) => Poll::Ready(answer),
781            None => {
782                slot.waker = Some(context.waker().clone());
783                Poll::Pending
784            }
785        }
786    }
787}
788
789// ----- paths ------------------------------------------------------------------------------
790
791mod native {
792    use super::{FileError, PickedFile};
793    use std::io::Write;
794    use std::path::Path;
795    use std::sync::atomic::{AtomicU64, Ordering};
796
797    fn error(failure: std::io::Error) -> FileError {
798        match failure.kind() {
799            std::io::ErrorKind::NotFound => FileError::NotFound,
800            std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied,
801            std::io::ErrorKind::Unsupported => FileError::Unavailable(failure.to_string()),
802            _ => FileError::Io(failure.to_string()),
803        }
804    }
805
806    pub(super) fn read(path: &str) -> Result<Vec<u8>, FileError> {
807        let metadata = std::fs::metadata(path).map_err(error)?;
808        if metadata.is_dir() {
809            return Err(FileError::IsAFolder);
810        }
811        if metadata.len() > PickedFile::MAX_READ_BYTES {
812            return Err(FileError::TooLarge {
813                len: metadata.len(),
814                limit: PickedFile::MAX_READ_BYTES,
815            });
816        }
817        let bytes = std::fs::read(path).map_err(error)?;
818        // The file may have grown between the two calls; the limit holds for what was read.
819        if bytes.len() as u64 > PickedFile::MAX_READ_BYTES {
820            return Err(FileError::TooLarge {
821                len: bytes.len() as u64,
822                limit: PickedFile::MAX_READ_BYTES,
823            });
824        }
825        Ok(bytes)
826    }
827
828    static TEMPORARY: AtomicU64 = AtomicU64::new(0);
829
830    pub(super) fn write(path: &str, bytes: &[u8]) -> Result<(), FileError> {
831        let target = Path::new(path);
832        let existing = match std::fs::metadata(target) {
833            Ok(metadata) if metadata.is_dir() => return Err(FileError::IsAFolder),
834            Ok(metadata) => Some(metadata),
835            Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => None,
836            Err(failure) => return Err(error(failure)),
837        };
838        if existing
839            .as_ref()
840            .is_some_and(|metadata| metadata.permissions().readonly())
841        {
842            return Err(FileError::ReadOnly);
843        }
844        let folder = target
845            .parent()
846            .filter(|parent| !parent.as_os_str().is_empty())
847            .unwrap_or_else(|| Path::new("."));
848        let name = target
849            .file_name()
850            .map(|name| name.to_string_lossy().into_owned())
851            .unwrap_or_default();
852        // Beside the target, so the rename stays on one file system and replaces it whole.
853        let temporary = folder.join(format!(
854            ".{name}.{}.{}.compose-rust.tmp",
855            std::process::id(),
856            TEMPORARY.fetch_add(1, Ordering::Relaxed)
857        ));
858        let written = (|| {
859            let mut file = std::fs::OpenOptions::new()
860                .write(true)
861                .create_new(true)
862                .open(&temporary)?;
863            file.write_all(bytes)?;
864            file.sync_all()?;
865            drop(file);
866            // The file keeps the permissions it had, not the new file's defaults.
867            if let Some(metadata) = &existing {
868                std::fs::set_permissions(&temporary, metadata.permissions())?;
869            }
870            std::fs::rename(&temporary, target)
871        })();
872        if let Err(failure) = written {
873            let _ = std::fs::remove_file(&temporary);
874            return Err(error(failure));
875        }
876        Ok(())
877    }
878
879    fn folder_of(path: &str) -> Result<(), FileError> {
880        let metadata = std::fs::metadata(path).map_err(error)?;
881        if !metadata.is_dir() {
882            return Err(FileError::NotAFolder);
883        }
884        Ok(())
885    }
886
887    fn joined(path: &str, name: &str) -> Result<String, FileError> {
888        Path::new(path)
889            .join(name)
890            .into_os_string()
891            .into_string()
892            .map_err(|_| FileError::Io(format!("{name:?} inside {path:?} is not UTF-8")))
893    }
894
895    pub(super) fn entries(path: &str) -> Result<Vec<PickedFile>, FileError> {
896        folder_of(path)?;
897        let mut found = Vec::new();
898        for entry in std::fs::read_dir(path).map_err(error)? {
899            let entry = entry.map_err(error)?;
900            // A name that is not UTF-8 cannot be handed over as a path; it is left out
901            // rather than given a name that is not its own.
902            let Ok(name) = entry.file_name().into_string() else {
903                continue;
904            };
905            let folder = entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false);
906            found.push((name, folder));
907        }
908        found.sort();
909        found
910            .into_iter()
911            .map(|(name, folder)| Ok(PickedFile::at_path(joined(path, &name)?, Some(folder))))
912            .collect()
913    }
914
915    pub(super) fn child(path: &str, name: &str, create: bool) -> Result<PickedFile, FileError> {
916        folder_of(path)?;
917        let child = joined(path, name)?;
918        match std::fs::metadata(&child) {
919            Ok(metadata) => Ok(PickedFile::at_path(child, Some(metadata.is_dir()))),
920            Err(failure) if failure.kind() == std::io::ErrorKind::NotFound && create => {
921                match std::fs::OpenOptions::new()
922                    .write(true)
923                    .create_new(true)
924                    .open(&child)
925                {
926                    Ok(_) => Ok(PickedFile::at_path(child, Some(false))),
927                    // Made by someone else in between: it is there now, which is what was
928                    // asked for.
929                    Err(again) if again.kind() == std::io::ErrorKind::AlreadyExists => {
930                        let folder = Path::new(&child).is_dir();
931                        Ok(PickedFile::at_path(child, Some(folder)))
932                    }
933                    Err(again) => Err(error(again)),
934                }
935            }
936            Err(failure) => Err(error(failure)),
937        }
938    }
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944
945    #[test]
946    fn fr72_a_reference_names_its_handle_and_a_path_is_not_one() {
947        assert_eq!(
948            parse_reference("webfile:7/notes.txt"),
949            Some((7, "notes.txt"))
950        );
951        assert_eq!(parse_reference("webfile:12/"), Some((12, "")));
952        assert_eq!(parse_reference("webfile:/a"), None);
953        assert_eq!(parse_reference("webfile:x/a"), None);
954        assert_eq!(parse_reference("/home/webfile:1/a"), None);
955        assert_eq!(parse_reference("C:\\webfile:1\\a"), None);
956        assert_eq!(reference(3, "한글 파일.txt"), "webfile:3/한글 파일.txt");
957    }
958
959    #[test]
960    fn fr72_a_child_name_is_one_name() {
961        for refused in ["", ".", "..", "a/b", "a\\b", "a\0b"] {
962            assert!(!is_plain_name(refused), "{refused:?}");
963        }
964        for accepted in ["a.txt", "한글", ".hidden", "a b"] {
965            assert!(is_plain_name(accepted), "{accepted:?}");
966        }
967    }
968
969    #[test]
970    fn fr72_file_requests_never_share_a_number_with_a_dialog() {
971        for _ in 0..4 {
972            let request = next_request();
973            assert_ne!(request & REQUEST_BIT, 0);
974            assert_ne!(request & !REQUEST_BIT, 0);
975        }
976        assert!(
977            !deliver_io(5, IoStatus::Ok, b""),
978            "a dialog's number is not ours"
979        );
980    }
981}