Skip to main content

compose_rust/
storage.rs

1//! Named values only this application reads and writes, kept until the next run.
2//!
3//! A task list, a draft, a setting: state an application has to find again after it
4//! restarts, and that the user does not expect to see in a file manager. Files the user
5//! picked are something else, and go through the file dialog.
6//!
7//! The same code runs everywhere, and every operation is a future, because the browser
8//! has no synchronous storage the page can wait on:
9//!
10//! ```ignore
11//! scope.launch(async move {
12//!     let saved = storage::read_string("tasks.tsv").await;
13//!     // ...
14//!     storage::write("tasks.tsv", text).await?;
15//! });
16//! ```
17//!
18//! Where the values live:
19//!
20//! - macOS and iOS: `$HOME/Library/Application Support/<identifier>/storage/<key>`;
21//! - Linux: `$XDG_DATA_HOME/<identifier>/storage/<key>`, or
22//!   `~/.local/share/<identifier>/storage/<key>` without it;
23//! - Windows: `%APPDATA%\<identifier>\storage\<key>`;
24//! - Android: `<files directory>/compose-rust/storage/<key>`, the directory the Renderer
25//!   reports at start, because only a `Context` knows it;
26//! - the web: the IndexedDB database `compose-rust/<identifier>`, store `values`.
27//!
28//! `COMPOSE_RUST_STORAGE_DIR`, when set, replaces the whole `.../storage` directory on every
29//! platform but the web, so a test or a development run never touches the user's real data.
30//!
31//! On the native platforms the files are read and written by one storage thread of the
32//! Host's, never on the UI thread, and a write goes to a temporary file in the same
33//! directory that replaces the old one with a rename, so a crash leaves the old value or the
34//! new one and never half of either. On the web the Renderer does the work in the browser
35//! and answers each request with one event.
36//!
37//! Each application has its own storage, named by the identifier it gives
38//! `LaunchBuilder::identifier`. An application that gives none has no storage: every
39//! operation answers [`StorageError::Unavailable`] and says so. The identifier is never
40//! guessed from a bundle, a package name or an executable name, because the same program
41//! would then see different storage depending on how it was started.
42
43use std::collections::{HashMap, HashSet, VecDeque};
44use std::fmt;
45use std::future::Future;
46use std::io::Write;
47use std::path::{Path, PathBuf};
48use std::pin::Pin;
49use std::sync::atomic::{AtomicU64, Ordering};
50use std::sync::{Arc, Condvar, Mutex, OnceLock};
51use std::task::{Context, Poll, Waker};
52
53use crate::schema::{IoStatus, StorageOp};
54
55/// The longest key, in bytes.
56pub const MAX_KEY_BYTES: usize = 128;
57
58/// The largest value one key holds, in bytes.
59///
60/// The storage is for application state, and a value travels whole, so it has a ceiling.
61/// Anything larger belongs in a file the user picked.
62pub const MAX_VALUE_BYTES: usize = 16 * 1024 * 1024;
63
64/// The longest application identifier, in bytes.
65pub const MAX_IDENTIFIER_BYTES: usize = 255;
66
67/// The environment variable that replaces the storage directory on the native platforms.
68pub const STORAGE_DIR_ENV: &str = "COMPOSE_RUST_STORAGE_DIR";
69
70/// The most bytes one answer can carry back from the web Renderer: the largest event buffer
71/// the Host lends it, less the answer's own record. That is more than [`MAX_VALUE_BYTES`],
72/// so every value a write accepted reads back; a larger one, which only another writer to
73/// the same browser storage could have left, is reported as too large rather than cut
74/// short.
75pub const WEB_ANSWER_LIMIT: usize =
76    crate::schema::WEB_EVENT_BUFFER_MAX_BYTES as usize - IO_ANSWER_RECORD_BYTES;
77
78/// The `IoAnswer` record before the bytes it carries.
79const IO_ANSWER_RECORD_BYTES: usize = 32;
80
81/// Why a storage operation did not do what it was asked.
82#[non_exhaustive]
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum StorageError {
85    /// The key breaks the rules: 1 to 128 bytes of `a-z`, `0-9`, `-`, `_` and `.`, not
86    /// starting with `.`. Nothing was read or written.
87    InvalidKey,
88    /// The value is `len` bytes, more than the `limit` it may be. Nothing was written, and
89    /// nothing was cut short.
90    TooLarge { len: usize, limit: usize },
91    /// The disk or the browser's quota is full. The value that was there is still there.
92    QuotaExceeded,
93    /// This run has no storage: no application identifier, no directory the platform can
94    /// name, or a browser that refuses storage. The text says which.
95    Unavailable(String),
96    /// [`read_string`] found a value that is not UTF-8.
97    NotUtf8,
98    /// Any other failure, in the words the platform gave.
99    Io(String),
100}
101
102impl fmt::Display for StorageError {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            StorageError::InvalidKey => write!(
106                f,
107                "a storage key is 1 to {MAX_KEY_BYTES} bytes of a-z, 0-9, '-', '_' and '.', \
108                 and does not start with '.'"
109            ),
110            StorageError::TooLarge { len, limit } => {
111                write!(
112                    f,
113                    "the value is {len} bytes, more than the {limit} bytes allowed"
114                )
115            }
116            StorageError::QuotaExceeded => {
117                write!(f, "there is no room left for this application's storage")
118            }
119            StorageError::Unavailable(reason) => write!(f, "storage is unavailable: {reason}"),
120            StorageError::NotUtf8 => write!(f, "the stored value is not UTF-8 text"),
121            StorageError::Io(reason) => write!(f, "storage failed: {reason}"),
122        }
123    }
124}
125
126impl std::error::Error for StorageError {}
127
128/// The sentence every operation answers with when the application named no identifier.
129const NO_IDENTIFIER: &str = "this run has no application identifier, so there is no storage \
130     to use. Give one with LaunchBuilder::new().identifier(\"com.example.app\"), the same \
131     value as [bundle] identifier in Dioxus.toml";
132
133// ----- the API --------------------------------------------------------------------------------
134
135/// Reads the value stored under `key`. `Ok(None)` when there is none, which is what the
136/// first run of an application finds.
137///
138/// The operation is placed when this is called, not when the future is first polled, so
139/// operations on one key apply in the order they were called even if nobody awaits them.
140pub fn read(
141    key: impl Into<String>,
142) -> impl Future<Output = Result<Option<Vec<u8>>, StorageError>> + 'static {
143    let pending = submit(StorageOp::Read, key.into(), Vec::new());
144    async move {
145        match pending.await? {
146            Answer::Value(value) => Ok(value),
147            Answer::Done | Answer::Keys(_) => Ok(None),
148        }
149    }
150}
151
152/// [`read`] for a value that is text. [`StorageError::NotUtf8`] when it is not.
153pub fn read_string(
154    key: impl Into<String>,
155) -> impl Future<Output = Result<Option<String>, StorageError>> + 'static {
156    let read = read(key);
157    async move {
158        match read.await? {
159            Some(bytes) => String::from_utf8(bytes)
160                .map(Some)
161                .map_err(|_| StorageError::NotUtf8),
162            None => Ok(None),
163        }
164    }
165}
166
167/// Stores `value` under `key`, replacing what was there.
168///
169/// The write is atomic: a crash or a power cut in the middle of it leaves either the old
170/// value or the new one. A value over [`MAX_VALUE_BYTES`] is refused with
171/// [`StorageError::TooLarge`] and the old value stays.
172pub fn write(
173    key: impl Into<String>,
174    value: impl Into<Vec<u8>>,
175) -> impl Future<Output = Result<(), StorageError>> + 'static {
176    let pending = submit(StorageOp::Write, key.into(), value.into());
177    async move { pending.await.map(|_| ()) }
178}
179
180/// Removes the value stored under `key`. Removing a key that holds nothing succeeds.
181pub fn remove(key: impl Into<String>) -> impl Future<Output = Result<(), StorageError>> + 'static {
182    let pending = submit(StorageOp::Remove, key.into(), Vec::new());
183    async move { pending.await.map(|_| ()) }
184}
185
186/// Every key this application has stored a value under, in byte order.
187pub fn keys() -> impl Future<Output = Result<Vec<String>, StorageError>> + 'static {
188    let pending = submit(StorageOp::Keys, String::new(), Vec::new());
189    async move {
190        match pending.await? {
191            Answer::Keys(keys) => Ok(keys),
192            Answer::Value(_) | Answer::Done => Ok(Vec::new()),
193        }
194    }
195}
196
197/// Whether `key` follows the rules for a key: 1 to [`MAX_KEY_BYTES`] bytes of lower case
198/// ASCII letters, digits, `-`, `_` and `.`, not starting with `.`.
199///
200/// Lower case only so two keys never become one file on a file system that ignores case,
201/// which is the default on macOS and Windows. The key is the file name as it is, so
202/// nothing that a path would read as a separator or a parent is allowed.
203pub fn is_valid_key(key: &str) -> bool {
204    !key.is_empty()
205        && key.len() <= MAX_KEY_BYTES
206        && !key.starts_with('.')
207        && key
208            .bytes()
209            .all(|byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
210}
211
212/// Checks an application identifier: 1 to [`MAX_IDENTIFIER_BYTES`] bytes of ASCII letters,
213/// digits, `.` and `-`. It names a directory and a database as it is, so nothing else is
214/// allowed. The error says what is wrong with it.
215pub fn validate_identifier(identifier: &str) -> Result<(), String> {
216    if identifier.is_empty() {
217        return Err(
218            "the application identifier is empty; give a reverse domain name such \
219             as \"com.example.app\""
220                .to_owned(),
221        );
222    }
223    if identifier.len() > MAX_IDENTIFIER_BYTES {
224        return Err(format!(
225            "the application identifier is {} bytes, longer than the {MAX_IDENTIFIER_BYTES} \
226             bytes a directory name may safely be",
227            identifier.len()
228        ));
229    }
230    if let Some(bad) = identifier
231        .chars()
232        .find(|c| !(c.is_ascii_alphanumeric() || *c == '.' || *c == '-'))
233    {
234        return Err(format!(
235            "the application identifier {identifier:?} contains {bad:?}; only ASCII letters, \
236             digits, '.' and '-' are allowed, because it is used as a directory and a \
237             database name as it is"
238        ));
239    }
240    if identifier.starts_with('.') || identifier.contains("..") {
241        return Err(format!(
242            "the application identifier {identifier:?} starts with '.' or holds \"..\", \
243             which a path would read as a hidden or a parent directory"
244        ));
245    }
246    Ok(())
247}
248
249/// The identifier the application launched with, if it gave one.
250pub fn identifier() -> Option<String> {
251    config().identifier.clone()
252}
253
254/// Sets the identifier for the run. Called by the launch, after checking it.
255pub(crate) fn set_identifier(identifier: Option<&str>) {
256    config().identifier = identifier.map(str::to_owned);
257}
258
259// ----- where the files go -----------------------------------------------------------------
260
261/// The platforms whose storage directory is worked out differently.
262#[doc(hidden)]
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub enum Platform {
265    MacOs,
266    Ios,
267    Linux,
268    Windows,
269    Android,
270    Web,
271}
272
273impl Platform {
274    /// The platform this build runs on.
275    pub fn current() -> Self {
276        if cfg!(target_family = "wasm") {
277            Platform::Web
278        } else if cfg!(target_os = "android") {
279            Platform::Android
280        } else if cfg!(target_os = "ios") {
281            Platform::Ios
282        } else if cfg!(target_os = "macos") {
283            Platform::MacOs
284        } else if cfg!(target_os = "windows") {
285            Platform::Windows
286        } else {
287            Platform::Linux
288        }
289    }
290}
291
292/// The directory a platform keeps an application's values in, as the platform writes paths.
293///
294/// `env` answers an environment variable. `android_files` is the files directory the
295/// Android Renderer reported, if it has. The error says what is missing.
296///
297/// Every value comes from the environment, so the Host calls no platform function to find
298/// it, and that is why it can be tested for any platform on any other.
299#[doc(hidden)]
300pub fn platform_directory(
301    platform: Platform,
302    identifier: &str,
303    android_files: Option<&str>,
304    env: impl Fn(&str) -> Option<String>,
305) -> Result<String, String> {
306    let set = |name: &str| env(name).filter(|value| !value.is_empty());
307    if platform == Platform::Web {
308        return Err("the web keeps its values in the browser, not in a directory".to_owned());
309    }
310    if let Some(directory) = set(STORAGE_DIR_ENV) {
311        return Ok(directory);
312    }
313    let home = || {
314        set("HOME").ok_or_else(|| {
315            "HOME is not set, so there is no directory to keep this application's values in"
316                .to_owned()
317        })
318    };
319    match platform {
320        Platform::MacOs | Platform::Ios => Ok(format!(
321            "{}/Library/Application Support/{identifier}/storage",
322            trim_end(&home()?, '/')
323        )),
324        Platform::Linux => match set("XDG_DATA_HOME").filter(|value| value.starts_with('/')) {
325            Some(data) => Ok(format!("{}/{identifier}/storage", trim_end(&data, '/'))),
326            None => Ok(format!(
327                "{}/.local/share/{identifier}/storage",
328                trim_end(&home()?, '/')
329            )),
330        },
331        Platform::Windows => {
332            let roaming = set("APPDATA").ok_or_else(|| {
333                "APPDATA is not set, so there is no directory to keep this application's \
334                 values in"
335                    .to_owned()
336            })?;
337            Ok(format!(
338                "{}\\{identifier}\\storage",
339                trim_end(&roaming, '\\')
340            ))
341        }
342        Platform::Android => match android_files.filter(|value| !value.is_empty()) {
343            Some(files) => Ok(format!("{}/compose-rust/storage", trim_end(files, '/'))),
344            None => Err(
345                "the Android Renderer has not said where the application's files \
346                 directory is"
347                    .to_owned(),
348            ),
349        },
350        Platform::Web => unreachable!("answered above"),
351    }
352}
353
354fn trim_end(value: &str, separator: char) -> &str {
355    let trimmed = value.trim_end_matches(separator);
356    if trimmed.is_empty() { value } else { trimmed }
357}
358
359/// The name a write's temporary file has before the rename, in the key's own directory.
360///
361/// It starts with `.`, which no key does, so it can never be taken for a value, and it ends
362/// in `.tmp`, which is how the storage thread finds one a crash left behind.
363#[doc(hidden)]
364pub fn temporary_name(key: &str) -> String {
365    static NEXT: AtomicU64 = AtomicU64::new(0);
366    format!(
367        ".{key}.{}-{}.tmp",
368        std::process::id(),
369        NEXT.fetch_add(1, Ordering::Relaxed)
370    )
371}
372
373fn is_temporary(name: &str) -> bool {
374    name.starts_with('.') && name.ends_with(".tmp")
375}
376
377// ----- configuration ------------------------------------------------------------------------
378
379/// Who carries an operation out.
380#[doc(hidden)]
381#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub enum Backend {
383    /// The Host's storage thread, in a directory the environment names.
384    Files,
385    /// The Host's storage thread, in the directory the Android Renderer reports.
386    Android,
387    /// The Renderer, through a `StorageCall` command and its `IoAnswer`. The web.
388    Renderer,
389}
390
391impl Backend {
392    fn for_this_build() -> Self {
393        match Platform::current() {
394            Platform::Web => Backend::Renderer,
395            Platform::Android => Backend::Android,
396            _ => Backend::Files,
397        }
398    }
399}
400
401struct Config {
402    identifier: Option<String>,
403    backend: Option<Backend>,
404    /// The files directory the Android Renderer reported.
405    android_files: Option<String>,
406    /// Operations started before the Android Renderer said where the files go, in order.
407    held: Vec<Job>,
408    /// Bumped by a test that starts over, so the storage thread looks for leftover
409    /// temporary files again as it would at the start of a run.
410    run: u64,
411}
412
413static CONFIG: Mutex<Config> = Mutex::new(Config {
414    identifier: None,
415    backend: None,
416    android_files: None,
417    held: Vec::new(),
418    run: 0,
419});
420
421fn config() -> std::sync::MutexGuard<'static, Config> {
422    CONFIG.lock().unwrap_or_else(|poison| poison.into_inner())
423}
424
425/// Hooks for tests, which run several storages one after the other in one process.
426#[doc(hidden)]
427pub mod testing {
428    use super::*;
429
430    /// Starts over: the given identifier and backend, nothing held, nothing waiting, and
431    /// leftover temporary files looked for again as at the start of a run.
432    pub fn reset(identifier: Option<&str>, backend: Backend) {
433        let mut config = config();
434        config.identifier = identifier.map(str::to_owned);
435        config.backend = Some(backend);
436        config.android_files = None;
437        config.held.clear();
438        config.run += 1;
439        drop(config);
440        OUTBOX.with_borrow_mut(Vec::clear);
441        WAITING.with_borrow_mut(HashMap::clear);
442        *LAST_IO_THREAD
443            .lock()
444            .unwrap_or_else(|poison| poison.into_inner()) = None;
445    }
446
447    /// The thread the storage thread last did input or output on.
448    pub fn last_io_thread() -> Option<std::thread::ThreadId> {
449        *LAST_IO_THREAD
450            .lock()
451            .unwrap_or_else(|poison| poison.into_inner())
452    }
453
454    /// How many operations wait for the Android Renderer's directory.
455    pub fn held() -> usize {
456        config().held.len()
457    }
458
459    /// How many requests wait for the Renderer's answer.
460    pub fn waiting() -> usize {
461        WAITING.with_borrow(HashMap::len)
462    }
463}
464
465// ----- the futures ----------------------------------------------------------------------------
466
467/// What a finished operation hands back, before the public function shapes it.
468#[derive(Debug)]
469enum Answer {
470    Value(Option<Vec<u8>>),
471    Done,
472    Keys(Vec<String>),
473}
474
475type Outcome = Result<Answer, StorageError>;
476
477#[derive(Default)]
478struct Slot {
479    outcome: Option<Outcome>,
480    waker: Option<Waker>,
481}
482
483type Shared = Arc<Mutex<Slot>>;
484
485fn complete(shared: &Shared, outcome: Outcome) {
486    let waker = {
487        let mut slot = shared.lock().unwrap_or_else(|poison| poison.into_inner());
488        if slot.outcome.is_some() {
489            return;
490        }
491        slot.outcome = Some(outcome);
492        slot.waker.take()
493    };
494    if let Some(waker) = waker {
495        waker.wake();
496    }
497}
498
499/// One operation on its way. Ready once whoever carries it out has answered.
500struct Pending {
501    shared: Shared,
502}
503
504impl Future for Pending {
505    type Output = Outcome;
506
507    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Outcome> {
508        let mut slot = self
509            .shared
510            .lock()
511            .unwrap_or_else(|poison| poison.into_inner());
512        match slot.outcome.take() {
513            Some(outcome) => Poll::Ready(outcome),
514            None => {
515                slot.waker = Some(context.waker().clone());
516                Poll::Pending
517            }
518        }
519    }
520}
521
522fn ready(outcome: Outcome) -> Pending {
523    let shared = Shared::default();
524    shared
525        .lock()
526        .unwrap_or_else(|poison| poison.into_inner())
527        .outcome = Some(outcome);
528    Pending { shared }
529}
530
531/// Places one operation with whoever carries it out, now, and returns its answer to come.
532fn submit(op: StorageOp, key: String, data: Vec<u8>) -> Pending {
533    if op != StorageOp::Keys && !is_valid_key(&key) {
534        return ready(Err(StorageError::InvalidKey));
535    }
536    if data.len() > MAX_VALUE_BYTES {
537        return ready(Err(StorageError::TooLarge {
538            len: data.len(),
539            limit: MAX_VALUE_BYTES,
540        }));
541    }
542    let mut config = config();
543    let Some(identifier) = config.identifier.clone() else {
544        return ready(Err(StorageError::Unavailable(NO_IDENTIFIER.to_owned())));
545    };
546    let backend = *config.backend.get_or_insert_with(Backend::for_this_build);
547    let shared = Shared::default();
548    let pending = Pending {
549        shared: Arc::clone(&shared),
550    };
551    match backend {
552        Backend::Renderer => {
553            drop(config);
554            send_to_renderer(op, identifier, key, data, shared);
555        }
556        Backend::Files | Backend::Android => {
557            let platform = if backend == Backend::Android {
558                Platform::Android
559            } else {
560                Platform::current().native()
561            };
562            let mut job = Job {
563                op,
564                key,
565                data,
566                directory: PathBuf::new(),
567                run: config.run,
568                shared,
569            };
570            let env = |name: &str| std::env::var(name).ok();
571            let android_files = config.android_files.clone();
572            let resolved = platform_directory(platform, &identifier, android_files.as_deref(), env);
573            match resolved {
574                Ok(directory) => {
575                    drop(config);
576                    job.directory = PathBuf::from(directory);
577                    storage_thread().push(job);
578                }
579                // Android's directory arrives with the Renderer's first event. What is
580                // asked before then waits for it, in order.
581                Err(_) if backend == Backend::Android && android_files.is_none() => {
582                    config.held.push(job);
583                }
584                Err(reason) => {
585                    drop(config);
586                    complete(&job.shared, Err(StorageError::Unavailable(reason)));
587                }
588            }
589        }
590    }
591    pending
592}
593
594impl Platform {
595    /// The platform whose directory rule a native build follows. A test that runs the
596    /// file backend on the web's build target has none, and gets Linux's.
597    fn native(self) -> Self {
598        if self == Platform::Web {
599            Platform::Linux
600        } else {
601            self
602        }
603    }
604}
605
606/// Takes the files directory the Android Renderer reported, and lets every operation that
607/// was waiting for it go, in the order they were asked.
608pub(crate) fn set_android_files(path: &str) {
609    let mut config = config();
610    config.android_files = Some(path.to_owned());
611    let held = std::mem::take(&mut config.held);
612    let identifier = config.identifier.clone().unwrap_or_default();
613    let directory = platform_directory(Platform::Android, &identifier, Some(path), |name| {
614        std::env::var(name).ok()
615    });
616    drop(config);
617    for mut job in held {
618        match &directory {
619            Ok(directory) => {
620                job.directory = PathBuf::from(directory);
621                storage_thread().push(job);
622            }
623            Err(reason) => complete(&job.shared, Err(StorageError::Unavailable(reason.clone()))),
624        }
625    }
626}
627
628// ----- the storage thread -------------------------------------------------------------------
629
630struct Job {
631    op: StorageOp,
632    key: String,
633    data: Vec<u8>,
634    directory: PathBuf,
635    run: u64,
636    shared: Shared,
637}
638
639/// The one thread that reads and writes the files, and the operations in front of it.
640///
641/// One thread and one list keep the operations on a key in the order they were asked.
642/// The list is the Host's own, between the UI thread and this thread; nothing about it
643/// crosses to the Renderer.
644struct StorageThread {
645    queue: Mutex<VecDeque<Job>>,
646    ready: Condvar,
647}
648
649impl StorageThread {
650    fn push(&self, job: Job) {
651        self.queue
652            .lock()
653            .unwrap_or_else(|poison| poison.into_inner())
654            .push_back(job);
655        self.ready.notify_one();
656    }
657
658    fn run(&self) {
659        let mut cleaned: HashSet<PathBuf> = HashSet::new();
660        let mut run = 0;
661        loop {
662            let job = {
663                let mut queue = self
664                    .queue
665                    .lock()
666                    .unwrap_or_else(|poison| poison.into_inner());
667                loop {
668                    if let Some(job) = queue.pop_front() {
669                        break job;
670                    }
671                    queue = self
672                        .ready
673                        .wait(queue)
674                        .unwrap_or_else(|poison| poison.into_inner());
675                }
676            };
677            *LAST_IO_THREAD
678                .lock()
679                .unwrap_or_else(|poison| poison.into_inner()) = Some(std::thread::current().id());
680            if job.run != run {
681                run = job.run;
682                cleaned.clear();
683            }
684            // A write that a crash interrupted left its temporary file. The first time a
685            // run touches a directory, those go.
686            if cleaned.insert(job.directory.clone()) {
687                remove_leftovers(&job.directory);
688            }
689            let outcome = carry_out(&job);
690            complete(&job.shared, outcome);
691        }
692    }
693}
694
695static LAST_IO_THREAD: Mutex<Option<std::thread::ThreadId>> = Mutex::new(None);
696
697fn storage_thread() -> &'static StorageThread {
698    static THREAD: OnceLock<&'static StorageThread> = OnceLock::new();
699    THREAD.get_or_init(|| {
700        let thread: &'static StorageThread = Box::leak(Box::new(StorageThread {
701            queue: Mutex::new(VecDeque::new()),
702            ready: Condvar::new(),
703        }));
704        std::thread::Builder::new()
705            .name("compose-rust-storage".to_owned())
706            .spawn(move || thread.run())
707            .expect("the storage thread could not be started");
708        thread
709    })
710}
711
712fn remove_leftovers(directory: &Path) {
713    let Ok(entries) = std::fs::read_dir(directory) else {
714        return;
715    };
716    for entry in entries.flatten() {
717        if entry.file_name().to_str().is_some_and(is_temporary) {
718            let _ = std::fs::remove_file(entry.path());
719        }
720    }
721}
722
723fn carry_out(job: &Job) -> Outcome {
724    let path = job.directory.join(&job.key);
725    match job.op {
726        StorageOp::Read => match std::fs::read(&path) {
727            Ok(bytes) => Ok(Answer::Value(Some(bytes))),
728            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Answer::Value(None)),
729            Err(error) => Err(io_error(&error)),
730        },
731        StorageOp::Write => write_atomically(&job.directory, &job.key, &job.data)
732            .map(|()| Answer::Done)
733            .map_err(|error| io_error(&error)),
734        StorageOp::Remove => match std::fs::remove_file(&path) {
735            Ok(()) => Ok(Answer::Done),
736            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Answer::Done),
737            Err(error) => Err(io_error(&error)),
738        },
739        StorageOp::Keys => match std::fs::read_dir(&job.directory) {
740            Ok(entries) => {
741                let mut keys: Vec<String> = entries
742                    .flatten()
743                    .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file()))
744                    .filter_map(|entry| entry.file_name().into_string().ok())
745                    .filter(|name| is_valid_key(name))
746                    .collect();
747                keys.sort();
748                Ok(Answer::Keys(keys))
749            }
750            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
751                Ok(Answer::Keys(Vec::new()))
752            }
753            Err(error) => Err(io_error(&error)),
754        },
755    }
756}
757
758/// Writes `data` to a temporary file beside the key's, flushes it to the disk, and renames
759/// it over the key's file. A rename within one directory replaces the target in one step on
760/// every platform this runs on, Windows included, so the key holds the old bytes or the new
761/// ones whatever moment the process stops at.
762fn write_atomically(directory: &Path, key: &str, data: &[u8]) -> std::io::Result<()> {
763    std::fs::create_dir_all(directory)?;
764    let temporary = directory.join(temporary_name(key));
765    let written = (|| {
766        // The file is closed at the end of this block, before the rename: Windows will
767        // not rename a file that is still open.
768        {
769            let mut file = std::fs::File::create(&temporary)?;
770            file.write_all(data)?;
771            file.sync_all()?;
772        }
773        std::fs::rename(&temporary, directory.join(key))
774    })();
775    if written.is_err() {
776        let _ = std::fs::remove_file(&temporary);
777        return written;
778    }
779    // The rename itself is in the directory's entry, which a crash right after could lose
780    // on some file systems without this. Not every platform can open a directory to sync it,
781    // and a failure here leaves the old value at worst, so it is not an error.
782    #[cfg(unix)]
783    if let Ok(handle) = std::fs::File::open(directory) {
784        let _ = handle.sync_all();
785    }
786    Ok(())
787}
788
789fn io_error(error: &std::io::Error) -> StorageError {
790    use std::io::ErrorKind;
791    if is_full(error) {
792        return StorageError::QuotaExceeded;
793    }
794    match error.kind() {
795        ErrorKind::PermissionDenied | ErrorKind::ReadOnlyFilesystem => {
796            StorageError::Unavailable(error.to_string())
797        }
798        _ => StorageError::Io(error.to_string()),
799    }
800}
801
802/// Whether the disk, or the user's share of it, is full. The platform's own numbers are
803/// read as well as the error's kind, because a quota has no kind of its own on every
804/// toolchain this builds with.
805fn is_full(error: &std::io::Error) -> bool {
806    if error.kind() == std::io::ErrorKind::StorageFull {
807        return true;
808    }
809    let Some(code) = error.raw_os_error() else {
810        return false;
811    };
812    if cfg!(windows) {
813        // ERROR_HANDLE_DISK_FULL, ERROR_DISK_FULL, ERROR_DISK_QUOTA_EXCEEDED.
814        matches!(code, 39 | 112 | 1295)
815    } else if cfg!(any(target_os = "macos", target_os = "ios")) {
816        // ENOSPC, EDQUOT.
817        matches!(code, 28 | 69)
818    } else {
819        // ENOSPC, EDQUOT.
820        matches!(code, 28 | 122)
821    }
822}
823
824// ----- the Renderer's half, on the web -------------------------------------------------------
825
826/// One operation the Renderer carries out, waiting for the next batch.
827pub(crate) struct RendererCall {
828    pub request: u32,
829    pub op: StorageOp,
830    pub store: String,
831    pub key: String,
832    pub data: Vec<u8>,
833}
834
835thread_local! {
836    /// Operations for the Renderer, in the order they were asked, waiting for a batch.
837    static OUTBOX: std::cell::RefCell<Vec<RendererCall>> = const { std::cell::RefCell::new(Vec::new()) };
838    /// Requests waiting for their answer, by number, with the operation each one was.
839    static WAITING: std::cell::RefCell<HashMap<u32, (StorageOp, Shared)>> =
840        std::cell::RefCell::new(HashMap::new());
841    /// The next request number. 0 is skipped so a zeroed record never looks like a request.
842    static NEXT_REQUEST: std::cell::Cell<u32> = const { std::cell::Cell::new(1) };
843}
844
845fn next_request() -> u32 {
846    NEXT_REQUEST.with(|next| {
847        loop {
848            // The top bit belongs to the operations on files picked on the web, which share
849            // `IoAnswer` with storage and are told apart by it, so storage wraps before it.
850            let id = next.get() & !crate::picked_file::REQUEST_BIT;
851            next.set(id.wrapping_add(1));
852            // A number still waiting is not handed out twice, even after the counter wraps.
853            if id != 0 && !WAITING.with_borrow(|waiting| waiting.contains_key(&id)) {
854                return id;
855            }
856        }
857    })
858}
859
860fn send_to_renderer(op: StorageOp, store: String, key: String, data: Vec<u8>, shared: Shared) {
861    let request = next_request();
862    WAITING.with_borrow_mut(|waiting| waiting.insert(request, (op, shared)));
863    OUTBOX.with_borrow_mut(|outbox| {
864        outbox.push(RendererCall {
865            request,
866            op,
867            store,
868            key,
869            data,
870        })
871    });
872    // Inside a Host call the batch that call produces carries it. Anywhere else, a frame
873    // is what gets it out.
874    if !crate::boundary::in_host_call() {
875        crate::boundary::request_frame_from_worker();
876    }
877}
878
879/// Hands every operation waiting for the Renderer to `emit`, oldest first, and empties the
880/// list.
881pub(crate) fn drain_calls(mut emit: impl FnMut(&RendererCall)) {
882    let taken = OUTBOX.with_borrow_mut(std::mem::take);
883    for call in &taken {
884        emit(call);
885    }
886}
887
888/// Hands the Renderer's answer to the operation that asked. Returns whether anyone was still
889/// waiting for it: an answer to a request nobody knows, or to one whose future was dropped
890/// with the component that awaited it, changes nothing.
891pub(crate) fn deliver_answer(request: u32, status: IoStatus, data: &[u8]) -> bool {
892    let Some((op, shared)) = WAITING.with_borrow_mut(|waiting| waiting.remove(&request)) else {
893        return false;
894    };
895    let outcome = answer_from(op, status, data);
896    let awaited = Arc::strong_count(&shared) > 1;
897    complete(&shared, outcome);
898    awaited
899}
900
901fn answer_from(op: StorageOp, status: IoStatus, data: &[u8]) -> Outcome {
902    let said = |fallback: &str| {
903        std::str::from_utf8(data)
904            .ok()
905            .filter(|text| !text.is_empty())
906            .unwrap_or(fallback)
907            .to_owned()
908    };
909    match status {
910        IoStatus::Ok => match op {
911            StorageOp::Read => Ok(Answer::Value(Some(data.to_vec()))),
912            StorageOp::Write | StorageOp::Remove => Ok(Answer::Done),
913            StorageOp::Keys => {
914                let text = std::str::from_utf8(data).map_err(|_| {
915                    StorageError::Io("the Renderer sent a key that is not UTF-8".to_owned())
916                })?;
917                let mut keys: Vec<String> = text
918                    .split('\0')
919                    .filter(|key| !key.is_empty())
920                    .map(str::to_owned)
921                    .collect();
922                keys.sort();
923                Ok(Answer::Keys(keys))
924            }
925        },
926        IoStatus::NotFound => match op {
927            StorageOp::Read => Ok(Answer::Value(None)),
928            StorageOp::Remove => Ok(Answer::Done),
929            StorageOp::Keys => Ok(Answer::Keys(Vec::new())),
930            StorageOp::Write => Err(StorageError::Io(said(
931                "the store to write to was not found",
932            ))),
933        },
934        IoStatus::QuotaExceeded => Err(StorageError::QuotaExceeded),
935        IoStatus::Unavailable => Err(StorageError::Unavailable(said(
936            "the Renderer has no storage for this application",
937        ))),
938        IoStatus::PermissionDenied => Err(StorageError::Unavailable(said(
939            "the platform refused access to this application's storage",
940        ))),
941        IoStatus::ReadOnly => Err(StorageError::Io(said("the storage is read only"))),
942        IoStatus::Failed => Err(StorageError::Io(said(
943            "the Renderer could not carry it out",
944        ))),
945        // A value too large to carry back carries its length instead, as four bytes, when
946        // the Renderer knows it. A write the Renderer refused for its size is held to the
947        // value limit, a read to what one answer can carry.
948        IoStatus::TooLarge => {
949            let limit = if op == StorageOp::Read {
950                WEB_ANSWER_LIMIT
951            } else {
952                MAX_VALUE_BYTES
953            };
954            let len = match data {
955                [a, b, c, d] => u32::from_le_bytes([*a, *b, *c, *d]) as usize,
956                _ => limit + 1,
957            };
958            Err(StorageError::TooLarge {
959                len: len.max(limit + 1),
960                limit,
961            })
962        }
963    }
964}