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 {
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 #[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
802fn 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 matches!(code, 39 | 112 | 1295)
815 } else if cfg!(any(target_os = "macos", target_os = "ios")) {
816 matches!(code, 28 | 69)
818 } else {
819 matches!(code, 28 | 122)
821 }
822}
823
824pub(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 static OUTBOX: std::cell::RefCell<Vec<RendererCall>> = const { std::cell::RefCell::new(Vec::new()) };
838 static WAITING: std::cell::RefCell<HashMap<u32, (StorageOp, Shared)>> =
840 std::cell::RefCell::new(HashMap::new());
841 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 let id = next.get() & !crate::picked_file::REQUEST_BIT;
851 next.set(id.wrapping_add(1));
852 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 if !crate::boundary::in_host_call() {
875 crate::boundary::request_frame_from_worker();
876 }
877}
878
879pub(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
888pub(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 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}