Skip to main content
KeyOS API Reference

fs/
lib.rs

1// SPDX-FileCopyrightText: 2023 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! Filesystem server API.
5//!
6//! The primary client handle is [`FileSystem`]. Use [`use_api!`] in app code to
7//! define local aliases for [`FileSystem`], [`File`], and [`Dir`] with the
8//! app's generated permissions type:
9//!
10//! ```rust,ignore
11//! fs::use_api!();
12//!
13//! let fs = FileSystem::default();
14//! let file = fs.open_file("state.json", fs::Location::AppData, fs::OpenFlags::read_only())?;
15//! ```
16//!
17//! The types in [`messages`] describe the wire protocol between the API handle
18//! and the filesystem server. Most app code should use [`FileSystem`] and the
19//! returned [`File`] / [`Dir`] handles directly.
20
21use std::io::{Read, Seek, Write};
22
23use num_derive::{FromPrimitive, ToPrimitive};
24use num_traits::{FromPrimitive, ToPrimitive};
25use server::{permission_set, wrapped_scalar, CheckedConn, CheckedPermissions, MessageAllowed};
26use xous::{DropDeallocate, MemoryRange};
27
28pub mod adapter;
29pub mod error;
30mod flags;
31pub mod messages;
32
33pub use error::Error;
34use messages::*;
35
36// Enough space for the typical FAT32 cluster read (64 sectors of 512)
37pub const FILE_BUFFER_SIZE: usize = 64 * 512;
38// The server rejects any single async read, write or copy longer than this.
39pub const MAX_ASYNC_LEN: usize = 1024 * 1024;
40pub const BLOCK_SIZE: u64 = 512;
41pub const SYSTEM_STATE_ROOT: &str = "state";
42// Staging names durable_file_write appends to the path it is given.
43const DURABLE_SCRATCH_SUFFIX: &str = ".tmp";
44const DURABLE_STAGED_SUFFIX: &str = ".new";
45
46/// Defines local filesystem API aliases with generated permissions.
47///
48/// The zero-argument form expands to:
49///
50/// - `FileSystem = fs::FileSystem<FileSystemPermissions>`
51/// - `File = fs::File<FileSystemPermissions>`
52/// - `Dir = fs::Dir<FileSystemPermissions>`
53///
54/// Use this from an app crate before constructing a [`FileSystem`] handle.
55#[macro_export]
56macro_rules! use_api {
57    ($fs:path, $server:path) => {
58        mod fs_permissions {
59            use fs::messages::*;
60            pub use $fs as fs;
61            use $server as server;
62            #[derive(Clone, Default, Debug, server::Permissions)]
63            #[server_name = "os/fs"]
64            pub struct FileSystemPermissions;
65        }
66        type FileSystem = fs_permissions::fs::FileSystem<fs_permissions::FileSystemPermissions>;
67        type File = fs_permissions::fs::File<fs_permissions::FileSystemPermissions>;
68        type Dir = fs_permissions::fs::Dir<fs_permissions::FileSystemPermissions>;
69    };
70    () => {
71        fs::use_api!(fs, server);
72    };
73}
74
75/// Client handle for the filesystem server.
76///
77/// This is the filesystem crate's high-level API surface. It owns a checked
78/// connection to `os/fs`, enforces location access checks, and creates [`File`]
79/// and [`Dir`] handles for file and directory operations.
80#[derive(Debug, Default, Clone)]
81pub struct FileSystem<P: CheckedPermissions> {
82    conn: CheckedConn<P>,
83    read_access_granted: flags::AccessFlags,
84    write_access_granted: flags::AccessFlags,
85}
86
87#[cfg(keyos)]
88permission_set!(
89    /// Permissions [`FileSystem::map_file`] requires. The device sends one map
90    /// message; the hosted build has no page-mirroring syscall and reads the file
91    /// instead, so there it needs the file read messages.
92    pub trait MapFilePermissions { MapFileMessage }
93);
94
95#[cfg(not(keyos))]
96permission_set!(pub trait MapFilePermissions { OpenFileMessage, CloseFile, ReadFile });
97
98permission_set!(
99    /// Permissions [`FileSystem::durable_file_write`] and [`FileSystem::durable_file_read`] require.
100    pub trait DurableFilePermissions {
101        OpenFileMessage, CloseFile, ReadFile, SeekFile, WriteFile, TruncateFile, Flush, Rename,
102        Remove
103    }
104);
105
106impl<P: CheckedPermissions> FileSystem<P> {
107    pub fn open_file(
108        &self,
109        path: impl Into<String>,
110        location: Location,
111        flags: OpenFlags,
112    ) -> Result<File<P>, Error>
113    where
114        P: MessageAllowed<OpenFileMessage>,
115        P: MessageAllowed<CloseFile>,
116    {
117        if flags.read {
118            self.ensure_read_access(location)?;
119        }
120        if flags.write {
121            self.ensure_write_access(location)?;
122        }
123        Ok(File {
124            handle: self.conn.send_blocking_archive(OpenFileMessage {
125                path: path.into(),
126                location,
127                flags,
128            })?,
129            work_buf: DropDeallocate::new(
130                xous::map_memory(None, None, FILE_BUFFER_SIZE, xous::MemoryFlags::W)
131                    .map_err(|_| Error::FileNotOpen)?,
132            ),
133            conn: self.conn.clone(),
134        })
135    }
136
137    pub fn open_dir(&self, path: impl Into<String>, location: Location) -> Result<Dir<P>, Error>
138    where
139        P: MessageAllowed<OpenDirMessage>,
140        P: MessageAllowed<CloseDir>,
141    {
142        self.ensure_read_access(location)?;
143        Ok(Dir {
144            handle: self.conn.send_blocking_archive(OpenDirMessage { path: path.into(), location })?,
145            conn: self.conn.clone(),
146        })
147    }
148
149    pub fn create_dir(&self, path: impl Into<String>, location: Location) -> Result<Dir<P>, Error>
150    where
151        P: MessageAllowed<CreateDirMessage>,
152        P: MessageAllowed<CloseDir>,
153    {
154        self.ensure_write_access(location)?;
155        Ok(Dir {
156            handle: self.conn.send_blocking_archive(CreateDirMessage { path: path.into(), location })?,
157            conn: self.conn.clone(),
158        })
159    }
160
161    pub fn create_dir_async(
162        &self,
163        path: impl Into<String>,
164        location: Location,
165    ) -> Result<CreateDirMessage, Error>
166    where
167        P: MessageAllowed<CreateDirMessage>,
168        P: MessageAllowed<CloseDir>,
169    {
170        self.ensure_write_access(location)?;
171        Ok(CreateDirMessage { path: path.into(), location })
172    }
173
174    pub fn ensure_parent_dir_exists(&self, path: &str, location: Location) -> Result<(), Error>
175    where
176        P: MessageAllowed<CreateDirMessage>,
177        P: MessageAllowed<CloseDir>,
178    {
179        ensure_parent_dir_exists_impl(|dir| self.create_dir(dir, location).map(|_| ()), path)
180    }
181
182    pub fn remove(&self, path: impl Into<String>, location: Location) -> Result<(), Error>
183    where
184        P: MessageAllowed<Remove>,
185    {
186        let path = path.into();
187        self.ensure_write_access(location)?;
188        self.conn.send_blocking_archive(Remove { path, location })
189    }
190
191    pub fn remove_async(&self, path: impl Into<String>, location: Location) -> Result<Remove, Error>
192    where
193        P: MessageAllowed<Remove>,
194    {
195        let path = path.into();
196        self.ensure_write_access(location)?;
197        Ok(Remove { path, location })
198    }
199
200    /// Copy a source file/directory to a destination directory. If source is a directory, the copy is
201    /// recursive.
202    ///
203    /// Optionally, the copied file/directory can be renamed by providing the `rename` argument.
204    ///
205    /// The destination directory must be empty; copying into a non-empty directory returns
206    /// [`Error::FileAlreadyExists`].
207    pub fn atomic_copy(
208        &self,
209        src: impl Into<String>,
210        dest_dir: impl Into<String>,
211        rename: Option<impl Into<String>>,
212        location: Location,
213    ) -> Result<(), Error>
214    where
215        P: MessageAllowed<AtomicCopy>,
216    {
217        self.ensure_read_access(location)?;
218        self.ensure_write_access(location)?;
219
220        let src = src.into();
221        let dest_dir = dest_dir.into();
222        let rename = rename.map(|s| s.into());
223        self.conn.send_blocking_archive(AtomicCopy { src, dest_dir, rename, location })
224    }
225
226    pub fn metadata(&self, path: impl Into<String>, location: Location) -> Result<Metadata, Error>
227    where
228        P: MessageAllowed<GetMetadata>,
229    {
230        self.ensure_read_access(location)?;
231        self.conn.send_blocking_archive(GetMetadata::Path { path: path.into(), location })
232    }
233
234    pub fn rename(
235        &self,
236        from: impl Into<String>,
237        to: impl Into<String>,
238        location: Location,
239    ) -> Result<(), Error>
240    where
241        P: MessageAllowed<Rename>,
242    {
243        self.ensure_write_access(location)?;
244        self.conn.send_blocking_archive(Rename { from: from.into(), to: to.into(), location })
245    }
246
247    pub fn rename_async(
248        &self,
249        from: impl Into<String>,
250        to: impl Into<String>,
251        location: Location,
252    ) -> Result<Rename, Error>
253    where
254        P: MessageAllowed<Rename>,
255    {
256        self.ensure_write_access(location)?;
257        Ok(Rename { from: from.into(), to: to.into(), location })
258    }
259
260    /// Replace the contents of `path` with `data`, so that a crash at any point during the
261    /// write leaves [`FileSystem::durable_file_read`] either the complete old contents or
262    /// the complete new ones.
263    ///
264    /// `<path>.tmp` and `<path>.new` belong to this file and must not be used for anything
265    /// else. Parent directories must already exist.
266    pub fn durable_file_write(&self, path: &str, location: Location, data: &[u8]) -> Result<(), Error>
267    where
268        P: DurableFilePermissions,
269    {
270        let scratch = format!("{path}{DURABLE_SCRATCH_SUFFIX}");
271        let staged = format!("{path}{DURABLE_STAGED_SUFFIX}");
272
273        // Settle what a crash left staged: adopt it if `path` is gone, drop it otherwise.
274        // Either way `staged` ends up free for the rename below.
275        let _ = self.rename(&staged, path, location);
276        let _ = self.remove(&staged, location);
277
278        {
279            let mut file = self.open_file(&scratch, location, OpenFlags::CREATE)?;
280            file.overwrite(data)?;
281            file.flush().map_err(|_| Error::Io)?;
282        }
283        // The rename is the commit: `staged` can only exist as a whole file, while `scratch`
284        // may be a half-written one that nothing in the bytes marks as incomplete.
285        self.rename(&scratch, &staged, location)?;
286        let _ = self.remove(path, location);
287        self.rename(&staged, path, location)
288    }
289
290    /// Read a file written by [`FileSystem::durable_file_write`].
291    ///
292    /// # Errors
293    ///
294    /// [`Error::FileNotFound`] if no generation of the file exists.
295    pub fn durable_file_read(&self, path: &str, location: Location) -> Result<Vec<u8>, Error>
296    where
297        P: DurableFilePermissions,
298    {
299        let read = |path: &str| -> Result<Vec<u8>, Error> {
300            let mut file = self.open_file(path, location, OpenFlags::READ_ONLY)?;
301            let mut data = Vec::new();
302            file.read_to_end(&mut data).map_err(|_| Error::Io)?;
303            Ok(data)
304        };
305
306        match read(path) {
307            // `path` is missing only while a write is between removing it and renaming the
308            // staged copy over it. Any other failure is about `path` itself, and answering it
309            // with a FileNotFound from the staged name would read as "never written".
310            Err(Error::FileNotFound) => read(&format!("{path}{DURABLE_STAGED_SUFFIX}")),
311            result => result,
312        }
313    }
314
315    pub fn map_file(&self, location: Location, path: impl Into<String>) -> Result<xous::MemoryRange, Error>
316    where
317        P: MapFilePermissions,
318    {
319        self.ensure_read_access(location)?;
320        if !location.is_mappable() {
321            return Err(Error::AccessDenied);
322        }
323
324        #[cfg(keyos)]
325        {
326            let result = self.conn.send_blocking_archive(MapFileMessage { path: path.into(), location })?;
327            Ok(unsafe { xous::MemoryRange::new(result.addr, result.size).unwrap() })
328        }
329
330        // No hosted equivalent of mirror_memory_to_pid, so read the file into a
331        // page-aligned buffer (leaked like the device's mapping, never unmapped)
332        // and hand back a range over it.
333        #[cfg(not(keyos))]
334        {
335            let mut file = self.open_file(path, location, OpenFlags::READ_ONLY)?;
336            let mut bytes = Vec::new();
337            file.read_to_end(&mut bytes).map_err(|_| Error::Io)?;
338            if bytes.is_empty() {
339                return Err(Error::FileNotFound);
340            }
341
342            let mut buffer = xous::map_memory(None, None, bytes.len(), xous::MemoryFlags::W)
343                .map_err(|_| Error::OutOfMemory)?;
344            buffer.as_slice_mut()[..bytes.len()].copy_from_slice(&bytes);
345            Ok(buffer)
346        }
347    }
348
349    pub fn register_app_resources(
350        &self,
351        app_id: xous::AppId,
352        root: AppResourcesRoot,
353        app_dir: impl Into<String>,
354    ) -> Result<(), Error>
355    where
356        P: MessageAllowed<RegisterAppResources>,
357    {
358        self.conn.send_blocking_archive(RegisterAppResources { app_id, root, app_dir: app_dir.into() })
359    }
360
361    /// Remove all AppData belonging to `app_id`, not just the caller's own. Wipes
362    /// the app's persisted data (including any stored seed) so it does not survive a
363    /// reinstall.
364    pub fn remove_app_data(&self, app_id: xous::AppId) -> Result<(), Error>
365    where
366        P: MessageAllowed<RemoveAppData>,
367    {
368        self.conn.send_blocking_archive(RemoveAppData { app_id })
369    }
370
371    fn ensure_read_access(&self, location: Location) -> Result<(), Error> {
372        if self.read_access_granted.contains(location) {
373            return Ok(());
374        }
375        match location {
376            Location::CommonAssets | Location::AppData | Location::AppResources => return Ok(()),
377            Location::System => self.conn.unchecked().try_send_blocking_scalar(GetSystemReadAccess)?,
378            Location::SystemAppData => {
379                self.conn.unchecked().try_send_blocking_scalar(GetSystemAppDataReadAccess)?
380            }
381            Location::EncryptedRoot => {
382                self.conn.unchecked().try_send_blocking_scalar(GetEncryptedRootReadAccess)?
383            }
384            Location::Usb => self.conn.unchecked().try_send_blocking_scalar(GetUsbReadAccess)?,
385            Location::User => self.conn.unchecked().try_send_blocking_scalar(GetUserReadAccess)?,
386            Location::Airlock => self.conn.unchecked().try_send_blocking_scalar(GetAirlockReadAccess)?,
387            Location::Boot => self.conn.unchecked().try_send_blocking_scalar(GetBootReadAccess)?,
388        };
389        self.read_access_granted.insert(location);
390        Ok(())
391    }
392
393    fn ensure_write_access(&self, location: Location) -> Result<(), Error> {
394        if self.write_access_granted.contains(location) {
395            return Ok(());
396        }
397        match location {
398            Location::CommonAssets | Location::AppResources => return Err(Error::AccessDenied)?,
399            Location::AppData => return Ok(()),
400            Location::System => self.conn.unchecked().try_send_blocking_scalar(GetSystemWriteAccess)?,
401            Location::SystemAppData => {
402                self.conn.unchecked().try_send_blocking_scalar(GetSystemAppDataWriteAccess)?
403            }
404            Location::EncryptedRoot => {
405                self.conn.unchecked().try_send_blocking_scalar(GetEncryptedRootWriteAccess)?
406            }
407            Location::Usb => self.conn.unchecked().try_send_blocking_scalar(GetUsbWriteAccess)?,
408            Location::User => self.conn.unchecked().try_send_blocking_scalar(GetUserWriteAccess)?,
409            Location::Airlock => self.conn.unchecked().try_send_blocking_scalar(GetAirlockWriteAccess)?,
410            Location::Boot => self.conn.unchecked().try_send_blocking_scalar(GetBootWriteAccess)?,
411        };
412        self.write_access_granted.insert(location);
413        Ok(())
414    }
415
416    pub fn read_blocks(
417        &mut self,
418        location: Location,
419        block_index: u32,
420        block_count: usize,
421        buf: MemoryRange,
422    ) -> Result<usize, Error>
423    where
424        P: MessageAllowed<ReadBlocks>,
425    {
426        self.conn.lend_mut(ReadBlocks { buf, block_index, block_count, location })
427    }
428
429    pub fn write_blocks(
430        &mut self,
431        location: Location,
432        block_index: u32,
433        block_count: usize,
434        buf: MemoryRange,
435    ) -> Result<usize, Error>
436    where
437        P: MessageAllowed<WriteBlocks>,
438    {
439        self.conn.lend_mut(WriteBlocks { buf, block_index, block_count, location })
440    }
441
442    pub fn flush(&mut self, location: Location) -> Result<(), Error>
443    where
444        P: MessageAllowed<FlushFs>,
445    {
446        self.conn.try_send_blocking_scalar(FlushFs(location))?
447    }
448
449    pub fn block_count(&self, location: Location) -> Result<usize, Error>
450    where
451        P: MessageAllowed<BlockCount>,
452    {
453        self.conn.try_send_blocking_scalar(BlockCount(location))?
454    }
455
456    pub fn format_encrypted_volume(&self)
457    where
458        P: MessageAllowed<FormatEncryptedVolume>,
459    {
460        self.conn.send_blocking_scalar(FormatEncryptedVolume);
461    }
462
463    pub fn subscribe_filesystem_events<S>(&self, listener: &mut server::ServerContext<S>, location: Location)
464    where
465        S: server::Server + server::ScalarEventHandler<FileSystemEvent>,
466        P: MessageAllowed<SubscribeFilesystemEvent>,
467    {
468        self.conn.subscribe_scalar_infallible(SubscribeFilesystemEvent(location), listener)
469    }
470
471    pub fn wait_for_filesystem(&self, location: Location)
472    where
473        P: 'static,
474        P: MessageAllowed<SubscribeFilesystemEvent>,
475    {
476        server::listen(WaitForFs(self.clone(), location));
477    }
478
479    pub fn mount_airlock(&mut self) -> Result<(), Error>
480    where
481        P: MessageAllowed<MountAirlock>,
482    {
483        self.conn.send_blocking_scalar(MountAirlock(true))
484    }
485
486    pub fn unmount_airlock(&mut self) -> Result<(), Error>
487    where
488        P: MessageAllowed<MountAirlock>,
489    {
490        self.conn.send_blocking_scalar(MountAirlock(false))
491    }
492
493    pub fn format_airlock(&mut self) -> Result<(), Error>
494    where
495        P: MessageAllowed<FormatAirlock>,
496    {
497        self.conn.send_blocking_scalar(FormatAirlock)
498    }
499}
500
501#[derive(Debug)]
502pub struct File<P: CheckedPermissions + MessageAllowed<CloseFile>> {
503    handle: FileHandle,
504    conn: CheckedConn<P>,
505    work_buf: DropDeallocate,
506}
507
508impl<P: CheckedPermissions + MessageAllowed<CloseFile>> File<P> {
509    pub fn truncate(&mut self) -> Result<(), Error>
510    where
511        P: MessageAllowed<TruncateFile>,
512    {
513        self.conn.send_blocking_archive(TruncateFile(self.handle))
514    }
515
516    // if len is less than the current file size, the file is truncated
517    // if len is greater than the current file size, the file is extended with empty bytes
518    // the file's cursor does not move
519    pub fn set_len(&mut self, len: u64) -> Result<(), Error>
520    where
521        P: MessageAllowed<SetLen>,
522    {
523        self.conn.send_blocking_archive(SetLen { handle: self.handle, len })
524    }
525
526    pub fn metadata(&self) -> Result<Metadata, Error>
527    where
528        P: MessageAllowed<GetMetadata>,
529    {
530        self.conn.send_blocking_archive(GetMetadata::Handle { handle: self.handle })
531    }
532
533    pub fn set_mtime(&mut self, datetime: DateTime) -> Result<(), Error>
534    where
535        P: MessageAllowed<SetMtime>,
536    {
537        self.conn.send_blocking_archive(SetMtime { handle: self.handle, datetime })
538    }
539
540    /// Prepare a message that can be sent with slint_keyos_platform::async_archive
541    /// Less efficient than a regular read()
542    /// May return less bytes than requested.
543    /// Returns an empty buffer on EOF
544    pub fn async_read(&mut self, read_len: usize) -> AsyncRead { AsyncRead { handle: self.handle, read_len } }
545
546    /// Prepare a message that can be sent with slint_keyos_platform::async_archive
547    /// Less efficient than a regular write()
548    /// The actual bytes written is returned, may be less than the buffer size
549    pub fn async_write(&mut self, buffer: Vec<u8>) -> AsyncWrite {
550        AsyncWrite { handle: self.handle, buffer }
551    }
552
553    /// Prepare a message that can be sent with slint_keyos_platform::async_scalar
554    /// Efficiently copies between two open files.
555    /// Returns the numebr of actually copied bytes.
556    /// Returns Ok(0) on EOF
557    pub fn async_copy_block_to(&mut self, to: &mut Self, len: usize) -> AsyncCopyBlock {
558        AsyncCopyBlock { from: self.handle, to: to.handle, len }
559    }
560
561    pub fn copy_block_to(&mut self, to: &mut Self, len: usize) -> Result<usize, Error>
562    where
563        P: MessageAllowed<AsyncCopyBlock>,
564    {
565        self.conn.send_blocking_scalar(AsyncCopyBlock { from: self.handle, to: to.handle, len })
566    }
567
568    pub fn overwrite(&mut self, buf: &[u8]) -> Result<(), Error>
569    where
570        P: MessageAllowed<SeekFile>,
571        P: MessageAllowed<WriteFile>,
572        P: MessageAllowed<TruncateFile>,
573        P: MessageAllowed<Flush>,
574    {
575        self.seek(std::io::SeekFrom::Start(0))?;
576        self.write_all(buf)?;
577        self.truncate()?;
578        Ok(())
579    }
580
581    pub fn copy_to(&mut self, to: &mut Self) -> Result<(), Error>
582    where
583        P: MessageAllowed<SeekFile>,
584        P: MessageAllowed<WriteFile>,
585        P: MessageAllowed<TruncateFile>,
586        P: MessageAllowed<AsyncCopyBlock>,
587    {
588        to.seek(std::io::SeekFrom::Start(0))?;
589        while self.copy_block_to(to, MAX_ASYNC_LEN)? > 0 {}
590        to.truncate()?;
591        Ok(())
592    }
593}
594
595impl<P: CheckedPermissions + MessageAllowed<CloseFile>> Read for File<P>
596where
597    P: MessageAllowed<ReadFile>,
598{
599    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
600        #[cfg(keyos)]
601        if (buf.as_ptr() as usize) & (xous::keyos::PAGE_SIZE - 1) == 0 && buf.len() >= xous::keyos::PAGE_SIZE
602        {
603            let read_len = (buf.len() & !(xous::keyos::PAGE_SIZE - 1)).min(FILE_BUFFER_SIZE);
604            return Ok(self.conn.lend_mut(ReadFile {
605                buf: unsafe { xous::MemoryRange::new(buf.as_ptr() as usize, read_len).unwrap() },
606                handle: self.handle,
607                read_len,
608            })?);
609        }
610
611        let read_len = buf.len().min(FILE_BUFFER_SIZE);
612
613        let result = self.conn.lend_mut(ReadFile { buf: *self.work_buf, handle: self.handle, read_len })?;
614        buf[..result].copy_from_slice(&self.work_buf.as_slice()[..result]);
615        Ok(result)
616    }
617}
618
619impl<P: CheckedPermissions + MessageAllowed<CloseFile>> Seek for File<P>
620where
621    P: MessageAllowed<SeekFile>,
622{
623    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
624        self.conn.send_blocking_archive(SeekFile { file: self.handle, pos: pos.into() }).map_err(Into::into)
625    }
626}
627
628impl<P: CheckedPermissions + MessageAllowed<CloseFile>> Write for File<P>
629where
630    P: MessageAllowed<WriteFile>,
631    P: MessageAllowed<Flush>,
632{
633    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
634        #[cfg(keyos)]
635        if (buf.as_ptr() as usize) & (xous::keyos::PAGE_SIZE - 1) == 0 && buf.len() >= xous::keyos::PAGE_SIZE
636        {
637            let write_len = (buf.len() & !(xous::keyos::PAGE_SIZE - 1)).min(FILE_BUFFER_SIZE);
638            return Ok(self.conn.lend_mut(WriteFile {
639                buf: unsafe { xous::MemoryRange::new(buf.as_ptr() as usize, write_len).unwrap() },
640                handle: self.handle,
641                write_len,
642            })?);
643        }
644
645        let buf_len = buf.len().min(FILE_BUFFER_SIZE);
646
647        self.work_buf.as_slice_mut()[..buf_len].copy_from_slice(&buf[..buf_len]);
648        let result =
649            self.conn.lend_mut(WriteFile { buf: *self.work_buf, handle: self.handle, write_len: buf_len })?;
650        Ok(result)
651    }
652
653    fn flush(&mut self) -> std::io::Result<()> {
654        self.conn.try_send_blocking_scalar(Flush(self.handle)).map_err(|_| std::io::ErrorKind::Other)??;
655        Ok(())
656    }
657}
658
659impl<P: CheckedPermissions + MessageAllowed<CloseFile>> Drop for File<P> {
660    fn drop(&mut self) {
661        if let Err(e) = self.conn.try_send_blocking_scalar(CloseFile(self.handle)) {
662            log::error!("Failed to close file: {:?}", e);
663        }
664    }
665}
666
667#[derive(Debug)]
668pub struct Dir<P: CheckedPermissions + MessageAllowed<CloseDir>> {
669    handle: DirHandle,
670    conn: CheckedConn<P>,
671}
672
673impl<P: CheckedPermissions + MessageAllowed<CloseDir>> Dir<P> {
674    pub fn next_entry(&self) -> Result<Option<DirEntry>, Error>
675    where
676        P: MessageAllowed<NextEntry>,
677    {
678        self.conn.send_blocking_archive(NextEntry(self.handle))
679    }
680
681    pub fn next_entry_async(&self) -> NextEntry { NextEntry(self.handle) }
682
683    pub fn pick_next_filename(&self, filename: impl Into<String>, pad: Option<usize>) -> Result<String, Error>
684    where
685        P: MessageAllowed<NextEntry>,
686    {
687        let filename: String = filename.into();
688
689        // Name can't include subdirectories
690        if filename.contains('/') {
691            return Err(Error::InvalidPath);
692        }
693
694        let pad = pad.unwrap_or(3);
695
696        // Allow getting the next directory name
697        let (basename, ext) = match filename.rsplit_once('.') {
698            Some((base, ext)) => (base, Some(format!(".{}", ext))),
699            None => (filename.as_str(), None),
700        };
701
702        let mut highest = 0u32;
703        let prefix = format!("{}-", basename);
704
705        while let Some(entry) = self.next_entry().ok().flatten() {
706            let name = entry.name;
707
708            // If this entry starts with a match to our filename, get the rest, else ignore
709            let remainder = match name.strip_prefix(&prefix) {
710                Some(r) => r,
711                None => continue,
712            };
713
714            // If this entry has the same extension, get the remaining number, else ignore
715            let num = match &ext {
716                Some(e) => match remainder.strip_suffix(e) {
717                    Some(n) => n,
718                    None => continue,
719                },
720                None => remainder,
721            };
722
723            if let Ok(n) = num.parse::<u32>() {
724                highest = highest.max(n);
725            }
726        }
727
728        // Example: account.txt => account-001.txt
729        let number = highest + 1;
730        Ok(format!("{}-{number:0pad$}{}", basename, ext.clone().unwrap_or_default()))
731    }
732}
733
734impl<P: CheckedPermissions + MessageAllowed<CloseDir>> Drop for Dir<P> {
735    fn drop(&mut self) {
736        if let Err(e) = self.conn.try_send_blocking_scalar(CloseDir(self.handle)) {
737            log::error!("Failed to close dir: {:?}", e);
738        }
739    }
740}
741
742// WaitForFs helper for subscribing to filesystem events
743pub struct WaitForFs<P: CheckedPermissions>(pub FileSystem<P>, pub Location);
744
745impl<P: CheckedPermissions> server::ServerMessages for WaitForFs<P> {
746    const NAME: &'static str = "";
747
748    fn messages() -> &'static [server::MessageDef<Self>]
749    where
750        Self: Sized,
751    {
752        &[]
753    }
754}
755
756impl<P: CheckedPermissions> server::Server for WaitForFs<P>
757where
758    P: MessageAllowed<SubscribeFilesystemEvent>,
759{
760    fn on_start(&mut self, context: &mut server::ServerContext<Self>) {
761        self.0.subscribe_filesystem_events(context, self.1);
762    }
763}
764
765impl<P: CheckedPermissions> server::ScalarEventHandler<FileSystemEvent> for WaitForFs<P>
766where
767    P: MessageAllowed<SubscribeFilesystemEvent>,
768{
769    fn handle(
770        &mut self,
771        msg: FileSystemEvent,
772        _sender: xous::PID,
773        context: &mut server::ServerContext<Self>,
774    ) {
775        if msg.location == self.1 && msg.event_type == FileSystemEventType::Mounted {
776            context.shutdown();
777        }
778    }
779}
780
781// ==================== Data types ====================
782
783#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
784pub struct FileHandle(pub u32);
785
786wrapped_scalar!(FileHandle);
787
788impl FileHandle {
789    pub fn new(id: u32, location: Location) -> Self {
790        Self(((location.to_usize().unwrap() as u32) << 24) | (id & 0x00FF_FFFF))
791    }
792
793    pub fn id(self) -> u32 { self.0 & 0x00FF_FFFF }
794
795    pub fn location(self) -> Result<Location, Error> {
796        Location::from_usize((self.0 >> 24) as usize).ok_or(Error::FileNotOpen)
797    }
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
801pub struct DirHandle(pub u32);
802
803wrapped_scalar!(DirHandle);
804
805impl DirHandle {
806    pub fn new(id: u32, location: Location) -> Self {
807        Self(((location.to_usize().unwrap() as u32) << 24) | (id & 0x00FF_FFFF))
808    }
809
810    pub fn id(self) -> u32 { self.0 & 0x00FF_FFFF }
811
812    pub fn location(self) -> Result<Location, Error> {
813        Location::from_usize((self.0 >> 24) as usize).ok_or(Error::FileNotOpen)
814    }
815}
816
817#[derive(
818    Debug,
819    Clone,
820    Copy,
821    PartialEq,
822    Eq,
823    Hash,
824    rkyv::Archive,
825    rkyv::Serialize,
826    rkyv::Deserialize,
827    FromPrimitive,
828    ToPrimitive,
829)]
830pub enum Location {
831    /// KeyOS common assets directory root.
832    /// Read-only. Available to all apps.
833    /// `<system-volume>/common`
834    CommonAssets = 1,
835
836    /// Currently running KeyOS app's RW data directory.
837    /// Available to all apps.
838    /// `<encrypted>/appdata/<app-id>/`
839    AppData,
840
841    /// Privileged access to System Volume.
842    /// `<system-volume>/`
843    System,
844
845    /// Privileged access to the whole encrypted partition
846    /// `<encrypted>/`
847    EncryptedRoot,
848
849    /// Privileged access to the Boot Volume. Should only be used by firmware upgrade/recovery
850    Boot,
851
852    /// Externally connected USB drive
853    Usb,
854
855    /// Encrypted user files
856    /// `<encrypted>/user`
857    User,
858
859    /// Virtual partition used to share files on USB.
860    Airlock,
861
862    /// Per-app unencrypted state directory on System Volume.
863    /// `<system-volume>/state/<app-id>/`
864    SystemAppData,
865
866    /// Currently running KeyOS app's read-only bundle resources directory.
867    /// Registered by app-manager before launch.
868    /// `<system-volume>/keyos/apps/<app-name>/resources/` for built-in apps, or
869    /// `<system-volume>/keyos/sideloaded-apps/<app-id>/resources/` for sideloaded apps.
870    AppResources,
871}
872
873impl Location {
874    /// Whether [`FileSystem::map_file`] accepts this location.
875    pub fn is_mappable(self) -> bool {
876        match self {
877            Location::CommonAssets => true,
878            Location::Boot
879            | Location::System
880            | Location::SystemAppData
881            | Location::EncryptedRoot
882            | Location::AppData
883            | Location::AppResources
884            | Location::User
885            | Location::Airlock
886            | Location::Usb => false,
887        }
888    }
889}
890
891impl server::AsScalar<1> for Location {
892    fn as_scalar(&self) -> [u32; 1] { [self.to_u32().unwrap()] }
893}
894
895impl server::FromScalar<1> for Location {
896    fn from_scalar([value]: [u32; 1]) -> Self { Self::from_u32(value).unwrap_or(Location::AppData) }
897}
898
899#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
900pub struct DirEntry {
901    pub name: String,
902    pub len: u64,
903    pub modified: DateTime,
904    pub is_dir: bool,
905    pub is_file: bool,
906}
907
908#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
909pub struct OpenFlags {
910    pub read: bool,
911    pub write: bool,
912    pub create: bool,
913}
914
915impl OpenFlags {
916    pub const CREATE: Self = Self { read: true, write: true, create: true };
917    pub const READ_ONLY: Self = Self { read: true, write: false, create: false };
918    pub const READ_WRITE: Self = Self { read: true, write: true, create: false };
919}
920
921#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
922pub enum SeekFrom {
923    Start(u64),
924    End(i64),
925    Current(i64),
926}
927
928impl From<SeekFrom> for std::io::SeekFrom {
929    fn from(from: SeekFrom) -> Self {
930        match from {
931            SeekFrom::Start(offset) => std::io::SeekFrom::Start(offset),
932            SeekFrom::End(offset) => std::io::SeekFrom::End(offset),
933            SeekFrom::Current(offset) => std::io::SeekFrom::Current(offset),
934        }
935    }
936}
937
938impl From<std::io::SeekFrom> for SeekFrom {
939    fn from(from: std::io::SeekFrom) -> Self {
940        match from {
941            std::io::SeekFrom::Start(offset) => SeekFrom::Start(offset),
942            std::io::SeekFrom::End(offset) => SeekFrom::End(offset),
943            std::io::SeekFrom::Current(offset) => SeekFrom::Current(offset),
944        }
945    }
946}
947
948#[derive(Debug, Clone)]
949pub struct FileSystemEvent {
950    pub location: Location,
951    pub event_type: FileSystemEventType,
952}
953
954#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
955pub enum FileSystemEventType {
956    Mounted,
957    Unmounted,
958    Error,
959}
960
961impl server::AsScalar<2> for FileSystemEvent {
962    fn as_scalar(&self) -> [u32; 2] {
963        let [location] = self.location.as_scalar();
964        [location, self.event_type.to_u32().unwrap()]
965    }
966}
967
968impl server::FromScalar<2> for FileSystemEvent {
969    fn from_scalar([location, event_type]: [u32; 2]) -> Self {
970        Self {
971            location: Location::from_scalar([location]),
972            event_type: FileSystemEventType::from_u32(event_type).unwrap_or(FileSystemEventType::Mounted),
973        }
974    }
975}
976
977#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
978pub struct MappedFileInTheirSpace {
979    pub addr: usize,
980    pub size: usize,
981}
982
983#[derive(Debug, Clone, Copy, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
984pub struct Metadata {
985    pub created: DateTime,
986    pub accessed: Date,
987    pub modified: DateTime,
988    pub size: u64,
989    pub is_dir: bool,
990}
991
992#[derive(Debug, Clone, Copy, Eq, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
993pub struct Date {
994    pub year: u16,
995    pub month: u16,
996    pub day: u16,
997}
998
999impl Ord for Date {
1000    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1001        self.year
1002            .cmp(&other.year)
1003            .then_with(|| self.month.cmp(&other.month))
1004            .then_with(|| self.day.cmp(&other.day))
1005    }
1006}
1007
1008impl PartialOrd for Date {
1009    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
1010}
1011
1012#[derive(Debug, Clone, Copy, Eq, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
1013pub struct Time {
1014    pub hour: u16,
1015    pub min: u16,
1016    pub sec: u16,
1017    pub millis: u16,
1018}
1019
1020impl Ord for Time {
1021    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1022        self.hour
1023            .cmp(&other.hour)
1024            .then_with(|| self.min.cmp(&other.min))
1025            .then_with(|| self.sec.cmp(&other.sec))
1026            .then_with(|| self.millis.cmp(&other.millis))
1027    }
1028}
1029
1030impl PartialOrd for Time {
1031    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
1032}
1033
1034#[derive(Debug, Clone, Copy, Eq, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
1035pub struct DateTime {
1036    pub date: Date,
1037    pub time: Time,
1038}
1039
1040impl Ord for DateTime {
1041    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1042        self.date.cmp(&other.date).then_with(|| self.time.cmp(&other.time))
1043    }
1044}
1045
1046impl PartialOrd for DateTime {
1047    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
1048}
1049
1050pub(crate) fn ensure_parent_dir_exists_impl(
1051    mut create_dir: impl FnMut(&str) -> Result<(), Error>,
1052    path: &str,
1053) -> Result<(), Error> {
1054    fn recurse(create_dir: &mut impl FnMut(&str) -> Result<(), Error>, path: &str) -> Result<(), Error> {
1055        if let Some(parent) = path.rsplit_once('/').map(|(parent, _)| parent) {
1056            if !parent.is_empty() {
1057                match create_dir(parent) {
1058                    Ok(_) | Err(Error::FileAlreadyExists) => {}
1059                    Err(Error::FileNotFound) => {
1060                        recurse(create_dir, parent)?;
1061                        match create_dir(parent) {
1062                            Ok(_) | Err(Error::FileAlreadyExists) => {}
1063                            Err(e) => return Err(e),
1064                        }
1065                    }
1066                    Err(e) => return Err(e),
1067                }
1068            }
1069        }
1070        Ok(())
1071    }
1072
1073    recurse(&mut create_dir, path)
1074}