1use 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
55pub const MAX_KEY_BYTES: usize = 128;
57
58pub const MAX_VALUE_BYTES: usize = 16 * 1024 * 1024;
63
64pub const MAX_IDENTIFIER_BYTES: usize = 255;
66
67pub const STORAGE_DIR_ENV: &str = "COMPOSE_RUST_STORAGE_DIR";
69
70pub const WEB_ANSWER_LIMIT: usize =
76 crate::schema::WEB_EVENT_BUFFER_MAX_BYTES as usize - IO_ANSWER_RECORD_BYTES;
77
78const IO_ANSWER_RECORD_BYTES: usize = 32;
80
81#[non_exhaustive]
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum StorageError {
85 InvalidKey,
88 TooLarge { len: usize, limit: usize },
91 QuotaExceeded,
93 Unavailable(String),
96 NotUtf8,
98 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
128const 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
133pub 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
152pub 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
167pub 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
180pub 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
186pub 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
197pub 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
212pub 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
249pub fn identifier() -> Option<String> {
251 config().identifier.clone()
252}
253
254pub(crate) fn set_identifier(identifier: Option<&str>) {
256 config().identifier = identifier.map(str::to_owned);
257}
258
259#[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 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#[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#[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#[doc(hidden)]
381#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub enum Backend {
383 Files,
385 Android,
387 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 android_files: Option<String>,
406 held: Vec<Job>,
408 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#[doc(hidden)]
427pub mod testing {
428 use super::*;
429
430 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 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 pub fn held() -> usize {
456 config().held.len()
457 }
458
459 pub fn waiting() -> usize {
461 WAITING.with_borrow(HashMap::len)
462 }
463}
464
465#[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
499struct 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
531fn 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 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 fn native(self) -> Self {
598 if self == Platform::Web {
599 Platform::Linux
600 } else {
601 self
602 }
603 }
604}
605
606pub(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
628struct Job {
631 op: StorageOp,
632 key: String,
633 data: Vec<u8>,
634 directory: PathBuf,
635 run: u64,
636 shared: Shared,
637}
638
639struct 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 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
758fn 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 let mut file = std::fs::File::create(&temporary)?;
767 file.write_all(data)?;
768 file.sync_all()?;
769 drop(file);
770 std::fs::rename(&temporary, directory.join(key))
771 })();
772 if written.is_err() {
773 let _ = std::fs::remove_file(&temporary);
774 return written;
775 }
776 #[cfg(unix)]
780 if let Ok(handle) = std::fs::File::open(directory) {
781 let _ = handle.sync_all();
782 }
783 Ok(())
784}
785
786fn io_error(error: &std::io::Error) -> StorageError {
787 use std::io::ErrorKind;
788 if is_full(error) {
789 return StorageError::QuotaExceeded;
790 }
791 match error.kind() {
792 ErrorKind::PermissionDenied | ErrorKind::ReadOnlyFilesystem => {
793 StorageError::Unavailable(error.to_string())
794 }
795 _ => StorageError::Io(error.to_string()),
796 }
797}
798
799fn is_full(error: &std::io::Error) -> bool {
803 if error.kind() == std::io::ErrorKind::StorageFull {
804 return true;
805 }
806 let Some(code) = error.raw_os_error() else {
807 return false;
808 };
809 if cfg!(windows) {
810 matches!(code, 39 | 112 | 1295)
812 } else if cfg!(any(target_os = "macos", target_os = "ios")) {
813 matches!(code, 28 | 69)
815 } else {
816 matches!(code, 28 | 122)
818 }
819}
820
821pub(crate) struct RendererCall {
825 pub request: u32,
826 pub op: StorageOp,
827 pub store: String,
828 pub key: String,
829 pub data: Vec<u8>,
830}
831
832thread_local! {
833 static OUTBOX: std::cell::RefCell<Vec<RendererCall>> = const { std::cell::RefCell::new(Vec::new()) };
835 static WAITING: std::cell::RefCell<HashMap<u32, (StorageOp, Shared)>> =
837 std::cell::RefCell::new(HashMap::new());
838 static NEXT_REQUEST: std::cell::Cell<u32> = const { std::cell::Cell::new(1) };
840}
841
842fn next_request() -> u32 {
843 NEXT_REQUEST.with(|next| {
844 loop {
845 let id = next.get() & !crate::picked_file::REQUEST_BIT;
848 next.set(id.wrapping_add(1));
849 if id != 0 && !WAITING.with_borrow(|waiting| waiting.contains_key(&id)) {
851 return id;
852 }
853 }
854 })
855}
856
857fn send_to_renderer(op: StorageOp, store: String, key: String, data: Vec<u8>, shared: Shared) {
858 let request = next_request();
859 WAITING.with_borrow_mut(|waiting| waiting.insert(request, (op, shared)));
860 OUTBOX.with_borrow_mut(|outbox| {
861 outbox.push(RendererCall {
862 request,
863 op,
864 store,
865 key,
866 data,
867 })
868 });
869 if !crate::boundary::in_host_call() {
872 crate::boundary::request_frame_from_worker();
873 }
874}
875
876pub(crate) fn drain_calls(mut emit: impl FnMut(&RendererCall)) {
879 let taken = OUTBOX.with_borrow_mut(std::mem::take);
880 for call in &taken {
881 emit(call);
882 }
883}
884
885pub(crate) fn deliver_answer(request: u32, status: IoStatus, data: &[u8]) -> bool {
889 let Some((op, shared)) = WAITING.with_borrow_mut(|waiting| waiting.remove(&request)) else {
890 return false;
891 };
892 let outcome = answer_from(op, status, data);
893 let awaited = Arc::strong_count(&shared) > 1;
894 complete(&shared, outcome);
895 awaited
896}
897
898fn answer_from(op: StorageOp, status: IoStatus, data: &[u8]) -> Outcome {
899 let said = |fallback: &str| {
900 std::str::from_utf8(data)
901 .ok()
902 .filter(|text| !text.is_empty())
903 .unwrap_or(fallback)
904 .to_owned()
905 };
906 match status {
907 IoStatus::Ok => match op {
908 StorageOp::Read => Ok(Answer::Value(Some(data.to_vec()))),
909 StorageOp::Write | StorageOp::Remove => Ok(Answer::Done),
910 StorageOp::Keys => {
911 let text = std::str::from_utf8(data).map_err(|_| {
912 StorageError::Io("the Renderer sent a key that is not UTF-8".to_owned())
913 })?;
914 let mut keys: Vec<String> = text
915 .split('\0')
916 .filter(|key| !key.is_empty())
917 .map(str::to_owned)
918 .collect();
919 keys.sort();
920 Ok(Answer::Keys(keys))
921 }
922 },
923 IoStatus::NotFound => match op {
924 StorageOp::Read => Ok(Answer::Value(None)),
925 StorageOp::Remove => Ok(Answer::Done),
926 StorageOp::Keys => Ok(Answer::Keys(Vec::new())),
927 StorageOp::Write => Err(StorageError::Io(said(
928 "the store to write to was not found",
929 ))),
930 },
931 IoStatus::QuotaExceeded => Err(StorageError::QuotaExceeded),
932 IoStatus::Unavailable => Err(StorageError::Unavailable(said(
933 "the Renderer has no storage for this application",
934 ))),
935 IoStatus::PermissionDenied => Err(StorageError::Unavailable(said(
936 "the platform refused access to this application's storage",
937 ))),
938 IoStatus::ReadOnly => Err(StorageError::Io(said("the storage is read only"))),
939 IoStatus::Failed => Err(StorageError::Io(said(
940 "the Renderer could not carry it out",
941 ))),
942 IoStatus::TooLarge => {
946 let limit = if op == StorageOp::Read {
947 WEB_ANSWER_LIMIT
948 } else {
949 MAX_VALUE_BYTES
950 };
951 let len = match data {
952 [a, b, c, d] => u32::from_le_bytes([*a, *b, *c, *d]) as usize,
953 _ => limit + 1,
954 };
955 Err(StorageError::TooLarge {
956 len: len.max(limit + 1),
957 limit,
958 })
959 }
960 }
961}