Skip to main content

plat/
guarded.rs

1// guarded -- the public file functions: a name with a scheme goes to its vfs provider
2// (vfs.rs), any other name is checked against the app's permissions (policy.rs) and then
3// handed to the OS call (win_sys.rs, linux_sys.rs, web_files.rs: the *_raw functions, which
4// the platform layer uses for its own system files)
5//
6// trash(path) moves into the trash (trash.rs) and counts as writing the path.
7// A refused path looks like a missing or unreadable one: 0 / None / false.
8// File and FileLock hold an OS file (also behind a folder mount: os_path) or a stream of a
9// mount (Provider::open: MemFs, Bundle, ZipFs, Overlay, Sub; other providers: the whole file,
10// read at the start and written back by sync and drop); a FileLock on a mount without OS
11// files holds inside this process only.
12// AtomicFile is write_file in pieces: a sibling (OS: OsAtomic of win_sys / linux_sys /
13// web_files / android_files; mounts: "NAME.tmp-.." in the mount) put in place by commit,
14// removed when dropped; content:// (no rename over a document): the bytes in memory, written
15// in place by commit (aio.rs builds the async writes and copies on it).
16
17use alloc::boxed::Box;
18use alloc::string::String;
19use alloc::sync::Arc;
20use alloc::vec::Vec;
21
22use rt::sync::SpinLock;
23
24use crate::policy::{fs_allowed, FS_READ, FS_WRITE};
25use crate::vfs::{self, Opened, Provider, Route, Stream};
26use crate::{OsFile, OsFileLock, FO_READ, FO_RW, FO_WRITE};
27
28/// 0 missing, 1 file, 2 directory  [plat_file_exists]
29pub fn file_exists(path: &str) -> u32 {
30    match vfs::route(path) {
31        Route::Os => if fs_allowed(path, FS_READ) { crate::file_exists_raw(path) } else { 0 },
32        Route::Vfs(p, rest) => p.kind(&rest),
33        Route::Bad => 0,
34    }
35}
36
37/// Some(0) only for a missing path, Some(1/2) for file/directory, None for other errors.
38pub fn file_exists_checked(path: &str) -> Option<u32> {
39    match vfs::route(path) {
40        Route::Os => if fs_allowed(path, FS_READ) { crate::file_exists_checked_raw(path) } else { None },
41        Route::Vfs(p, rest) => Some(p.kind(&rest)),
42        Route::Bad => None,
43    }
44}
45
46/// create a directory and its missing parents; an existing directory is fine  [plat_mkdir]
47pub fn mkdir(path: &str) -> bool {
48    match vfs::route(path) {
49        Route::Os => fs_allowed(path, FS_WRITE) && crate::mkdir_raw(path),
50        Route::Vfs(p, rest) => p.mkdir(&rest),
51        Route::Bad => false,
52    }
53}
54
55/// a file or an empty directory  [plat_delete]
56pub fn delete(path: &str) -> bool {
57    match vfs::route(path) {
58        Route::Os => fs_allowed(path, FS_WRITE) && crate::delete_raw(path),
59        Route::Vfs(p, rest) => p.delete(&rest),
60        Route::Bad => false,
61    }
62}
63
64/// move a file or folder into the trash (Windows: the recycle bin; Linux: the freedesktop
65/// trash) -> false: it stays where it was, never deleted for good (trash.rs).  Counts as
66/// writing the path; a vfs name needs a folder mount.
67pub fn trash(path: &str) -> bool {
68    match vfs::route(path) {
69        Route::Os => fs_allowed(path, FS_WRITE) && crate::trash::trash_raw(path),
70        Route::Vfs(p, rest) => !rest.is_empty() && p.os_path(&rest).is_some_and(|o| fs_allowed(&o, FS_WRITE) && crate::trash::trash_raw(&o)),
71        Route::Bad => false,
72    }
73}
74
75/// rename, replacing an existing target (both names on the OS or in the same mount)  [plat_rename]
76pub fn rename(old: &str, new: &str) -> bool {
77    match (vfs::route(old), vfs::route(new)) {
78        (Route::Os, Route::Os) => fs_allowed(old, FS_WRITE) && fs_allowed(new, FS_WRITE) && crate::rename_raw(old, new),
79        (Route::Vfs(p, a), Route::Vfs(q, b)) if Arc::ptr_eq(&p, &q) => p.rename(&a, &b),
80        _ => false,
81    }
82}
83
84/// f(name, is_dir, size_lo) for every entry except "." and ".."  [plat_dir_list]
85pub fn dir_list(path: &str, f: &mut dyn FnMut(&str, bool, u32)) -> bool {
86    match vfs::route(path) {
87        Route::Os => fs_allowed(path, FS_READ) && crate::dir_list_raw(path, f),
88        Route::Vfs(p, rest) => p.list(&rest, &mut |n, d, s| f(n, d, s as u32)),
89        Route::Bad => false,
90    }
91}
92
93/// the contents of a file  [plat_file_read]
94pub fn read_file(path: &str) -> Option<Vec<u8>> {
95    match vfs::route(path) {
96        Route::Os => if fs_allowed(path, FS_READ) { crate::read_file_raw(path) } else { None },
97        Route::Vfs(p, rest) => p.read(&rest),
98        Route::Bad => None,
99    }
100}
101
102/// Write an exclusively created sibling, flush it, then replace path (a mount: its own way of
103/// replacing a file; missing folders are created). [plat_file_write]
104pub fn write_file(path: &str, data: &[u8]) -> bool {
105    match vfs::route(path) {
106        Route::Os => fs_allowed(path, FS_WRITE) && crate::write_file_raw(path, data),
107        Route::Vfs(p, rest) => !rest.is_empty() && p.write(&rest, data),
108        Route::Bad => false,
109    }
110}
111
112// ---------------------------------------------------------------- streaming files
113
114/// an open file (closed when dropped): an OS file, or a stream of a vfs mount
115pub struct File(Inner);
116
117enum Inner {
118    Os(OsFile),
119    Vfs(Box<dyn Stream>),
120}
121
122fn mode_ok(mode: u32) -> bool {
123    matches!(mode, FO_READ | FO_WRITE | FO_RW)
124}
125
126// a checked OS file
127fn os_open(path: &str, mode: u32) -> Option<File> {
128    if mode_ok(mode) && fs_allowed(path, if mode == FO_READ { FS_READ } else { FS_WRITE }) {
129        OsFile::open_raw(path, mode).map(|f| File(Inner::Os(f)))
130    } else {
131        None
132    }
133}
134
135/// (vfs) File::open on a provider: its own stream, its OS file, else the whole file
136pub(crate) fn open_in(p: &Arc<dyn Provider>, path: &str, mode: u32) -> Option<File> {
137    if !mode_ok(mode) {
138        return None;
139    }
140    match p.open(path, mode) {
141        Opened::File(s) => Some(File(Inner::Vfs(s))),
142        Opened::Refused => None,
143        Opened::Generic => match p.os_path(path) {
144            Some(o) => os_open(&o, mode),
145            None => whole(p, path, mode),
146        },
147    }
148}
149
150// a provider without streams: the file read at the start, written back by sync and drop
151fn whole(p: &Arc<dyn Provider>, path: &str, mode: u32) -> Option<File> {
152    let data = match (mode, p.kind(path)) {
153        (_, 2) => return None,
154        (FO_READ, 1) | (FO_RW, 1) => p.read(path)?,
155        (FO_READ, _) => return None,
156        _ => Vec::new(),
157    };
158    // FO_WRITE truncates, FO_RW creates; an existing file is written back once, which also
159    // tells a read-only provider
160    if mode != FO_READ && (path.is_empty() || !p.write(path, &data)) {
161        return None;
162    }
163    let back = (mode != FO_READ).then(|| (p.clone(), String::from(path)));
164    Some(File(Inner::Vfs(Box::new(Whole { writable: back.is_some(), back, st: SpinLock::new((data, false)) }))))
165}
166
167/// a file kept in memory: written back to back (provider, path) when it changed; without
168/// back read-only, or an anonymous file (open_temp on a mount without OS files)
169struct Whole {
170    back: Option<(Arc<dyn Provider>, String)>,
171    writable: bool,
172    // the bytes, changed since the last write-back
173    st: SpinLock<(Vec<u8>, bool)>,
174}
175
176impl Whole {
177    fn anon() -> Whole {
178        Whole { back: None, writable: true, st: SpinLock::new((Vec::new(), false)) }
179    }
180}
181
182impl Stream for Whole {
183    fn read_at(&self, off: u64, buf: &mut [u8]) -> Option<usize> {
184        let g = self.st.lock();
185        let off = usize::try_from(off).unwrap_or(usize::MAX).min(g.0.len());
186        let n = (g.0.len() - off).min(buf.len());
187        buf[..n].copy_from_slice(&g.0[off..off + n]);
188        Some(n)
189    }
190
191    fn write_at(&self, off: u64, buf: &[u8]) -> Option<usize> {
192        if !self.writable {
193            return None;
194        }
195        let mut g = self.st.lock();
196        let n = vfs::write_vec(&mut g.0, off, buf)?;
197        g.1 = true;
198        Some(n)
199    }
200
201    fn size(&self) -> Option<u64> {
202        Some(self.st.lock().0.len() as u64)
203    }
204
205    fn set_len(&self, len: u64) -> bool {
206        if !self.writable {
207            return false;
208        }
209        let mut g = self.st.lock();
210        g.1 = true;
211        vfs::set_len_vec(&mut g.0, len)
212    }
213
214    fn sync(&self) -> bool {
215        let Some((p, path)) = &self.back else { return true };
216        let mut g = self.st.lock();
217        if !g.1 {
218            return true;
219        }
220        let ok = p.write(path, &g.0);
221        g.1 &= !ok;
222        ok
223    }
224}
225
226impl Drop for Whole {
227    fn drop(&mut self) {
228        self.sync();
229    }
230}
231
232impl File {
233    /// FO_READ (existing, read only), FO_WRITE (create / truncate), FO_RW (open or create);
234    /// on every mount (vfs.rs)
235    pub fn open(path: &str, mode: u32) -> Option<File> {
236        match vfs::route(path) {
237            Route::Os => os_open(path, mode),
238            Route::Vfs(p, rest) => open_in(&p, &rest, mode),
239            Route::Bad => None,
240        }
241    }
242
243    /// Create a named file exclusively; never open an existing path (a mount without OS
244    /// files: exclusive inside this process).
245    #[cfg(any(windows, target_os = "linux"))]
246    pub fn create_new(path: &str) -> Option<File> {
247        let new_os = |o: &str| if fs_allowed(o, FS_WRITE) { OsFile::create_new_raw(o).map(|f| File(Inner::Os(f))) } else { None };
248        match vfs::route(path) {
249            Route::Os => new_os(path),
250            Route::Vfs(p, rest) => match p.os_path(&rest) {
251                Some(o) => new_os(&o),
252                None => {
253                    let _g = NEW_LOCK.lock();
254                    if rest.is_empty() || p.kind(&rest) != 0 { None } else { open_in(&p, &rest, FO_WRITE) }
255                }
256            },
257            Route::Bad => None,
258        }
259    }
260
261    /// a new file without a lasting name in dir (None = temp_dir) that disappears when it is
262    /// closed (also after a crash); a mount without OS files: a file in memory  [plat_file_open_temp]
263    pub fn open_temp(dir: Option<&str>) -> Option<File> {
264        let temp_os = |d: &str| if fs_allowed(d, FS_WRITE) { OsFile::open_temp_raw(Some(d)).map(|f| File(Inner::Os(f))) } else { None };
265        let Some(d) = dir else { return OsFile::open_temp_raw(None).map(|f| File(Inner::Os(f))) };
266        match vfs::route(d) {
267            Route::Os => temp_os(d),
268            Route::Vfs(p, rest) => match p.os_path(&rest) {
269                Some(o) => temp_os(&o),
270                None => (p.kind(&rest) == 2).then(|| File(Inner::Vfs(Box::new(Whole::anon())))),
271            },
272            Route::Bad => None,
273        }
274    }
275
276    /// bytes read at off (fewer at the end of the file), None on an error before any byte
277    pub fn read_at(&self, off: u64, buf: &mut [u8]) -> Option<usize> {
278        match &self.0 {
279            Inner::Os(f) => f.read_at(off, buf),
280            Inner::Vfs(s) => s.read_at(off, buf),
281        }
282    }
283
284    /// bytes written at off (the file grows as needed), None on an error before any byte
285    pub fn write_at(&self, off: u64, buf: &[u8]) -> Option<usize> {
286        match &self.0 {
287            Inner::Os(f) => f.write_at(off, buf),
288            Inner::Vfs(s) => s.write_at(off, buf),
289        }
290    }
291
292    pub fn size(&self) -> Option<u64> {
293        match &self.0 {
294            Inner::Os(f) => f.size(),
295            Inner::Vfs(s) => s.size(),
296        }
297    }
298
299    /// write cached data to the disk (a mount: where its readers find it)
300    pub fn sync(&self) -> bool {
301        match &self.0 {
302            Inner::Os(f) => f.sync(),
303            Inner::Vfs(s) => s.sync(),
304        }
305    }
306
307    /// cut or extend the file to `len` bytes
308    pub fn set_len(&self, len: u64) -> bool {
309        match &self.0 {
310            #[cfg(any(windows, target_os = "linux", target_os = "android"))]
311            Inner::Os(f) => f.set_len(len),
312            #[cfg(not(any(windows, target_os = "linux", target_os = "android")))]
313            Inner::Os(_) => false,
314            Inner::Vfs(s) => s.set_len(len),
315        }
316    }
317}
318
319impl Stream for File {
320    fn read_at(&self, off: u64, buf: &mut [u8]) -> Option<usize> {
321        File::read_at(self, off, buf)
322    }
323    fn write_at(&self, off: u64, buf: &[u8]) -> Option<usize> {
324        File::write_at(self, off, buf)
325    }
326    fn size(&self) -> Option<u64> {
327        File::size(self)
328    }
329    fn set_len(&self, len: u64) -> bool {
330        File::set_len(self, len)
331    }
332    fn sync(&self) -> bool {
333        File::sync(self)
334    }
335}
336
337// ---------------------------------------------------------------- atomic streaming writes
338
339/// The streaming form of write_file: the bytes go into a sibling of the target, commit puts
340/// it in place in one step (readers see the old file or all of the new one); dropped without
341/// commit, the sibling is removed and the target is unchanged.  Checked and routed like
342/// write_file when it is created.  OS paths: an exclusive "NAME.tmp-PID-SEQ-TIME" beside the
343/// file, fsync and rename (Linux keeps the old file's access bits; the browser: in memory until
344/// commit).  Mounts: a "NAME.tmp-.." file of the mount, renamed by the provider (one without
345/// rename gets the whole file written); a provider whose rename cannot replace a file
346/// (`atomic_rename() == false`, content://) gets the bytes written in place at commit.
347pub struct AtomicFile(Option<AtomicInner>);
348
349enum AtomicInner {
350    Os(crate::OsAtomic),
351    Vfs { p: Arc<dyn Provider>, tmp: String, dst: String, f: Option<File> },
352    // a provider without rename over a file (content://): the bytes, written at commit
353    InPlace { p: Arc<dyn Provider>, dst: String, buf: SpinLock<Vec<u8>> },
354}
355
356static ATOMIC_SEQ: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
357
358impl AtomicFile {
359    /// a writer of path (None: refused, a folder, an invalid name, or the sibling could not be
360    /// created)
361    pub fn create(path: &str) -> Option<AtomicFile> {
362        match vfs::route(path) {
363            Route::Os => {
364                if !fs_allowed(path, FS_WRITE) {
365                    return None;
366                }
367                crate::OsAtomic::begin(path).map(|a| AtomicFile(Some(AtomicInner::Os(a))))
368            }
369            Route::Vfs(p, rest) => {
370                if rest.is_empty() || p.kind(&rest) == 2 {
371                    return None;
372                }
373                if !p.atomic_rename() {
374                    return Some(AtomicFile(Some(AtomicInner::InPlace { p, dst: rest, buf: SpinLock::new(Vec::new()) })));
375                }
376                if let Some((dir, _)) = rest.rsplit_once('/') {
377                    p.mkdir(dir);
378                }
379                for _ in 0..16 {
380                    let seq = ATOMIC_SEQ.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
381                    let tmp = alloc::format!("{rest}.tmp-{seq:x}-{:x}", crate::time_us() as u32);
382                    if p.kind(&tmp) != 0 {
383                        continue;
384                    }
385                    let f = open_in(&p, &tmp, FO_WRITE)?;
386                    return Some(AtomicFile(Some(AtomicInner::Vfs { p, tmp, dst: rest, f: Some(f) })));
387                }
388                None
389            }
390            Route::Bad => None,
391        }
392    }
393
394    /// bytes written at off (the file grows as needed), None on an error
395    pub fn write_at(&self, off: u64, data: &[u8]) -> Option<usize> {
396        match self.0.as_ref()? {
397            AtomicInner::Os(a) => a.write_at(off, data),
398            AtomicInner::Vfs { f, .. } => f.as_ref()?.write_at(off, data),
399            AtomicInner::InPlace { buf, .. } => vfs::write_vec(&mut buf.lock(), off, data),
400        }
401    }
402
403    /// put the written file in place -> false: the target is unchanged, the sibling gone
404    pub fn commit(mut self) -> bool {
405        match self.0.take() {
406            None => false,
407            Some(AtomicInner::Os(a)) => a.commit(),
408            Some(AtomicInner::Vfs { p, tmp, dst, f }) => {
409                let ok = f.is_some_and(|f| f.sync());
410                // a provider without rename: the whole file under the new name
411                let ok = ok && (p.rename(&tmp, &dst) || p.read(&tmp).is_some_and(|d| p.write(&dst, &d)));
412                if p.kind(&tmp) == 1 {
413                    p.delete(&tmp);
414                }
415                ok
416            }
417            Some(AtomicInner::InPlace { p, dst, buf }) => p.write(&dst, &buf.lock()),
418        }
419    }
420}
421
422impl Drop for AtomicFile {
423    fn drop(&mut self) {
424        if let Some(AtomicInner::Vfs { p, tmp, f, .. }) = &mut self.0 {
425            drop(f.take());
426            p.delete(tmp); // not committed: the sibling goes (an OsAtomic removes its own)
427        }
428    }
429}
430
431// create_new on mounts without OS files: one check-and-create at a time
432#[cfg(any(windows, target_os = "linux"))]
433static NEW_LOCK: SpinLock<()> = SpinLock::new(());
434
435// the FileLocks of mounts without OS files: provider (address) and path
436static VFS_LOCKS: SpinLock<Vec<(usize, String)>> = SpinLock::new(Vec::new());
437
438/// Nonblocking exclusive sidecar lock; drop releases it.  All cooperating writers must lock
439/// the same path before checking and replacing data.  OS files: across processes (Windows: an
440/// exclusive handle, Linux: flock); a mount without OS files: inside this process.
441pub struct FileLock(LockInner);
442
443enum LockInner {
444    Os { _lock: OsFileLock },
445    Vfs { p: Arc<dyn Provider>, path: String },
446}
447
448impl FileLock {
449    /// Open or create without truncating. None means busy, invalid path, refused or I/O failure.
450    pub fn acquire(path: &str) -> Option<Self> {
451        let os = |o: &str| if fs_allowed(o, FS_WRITE) { OsFileLock::acquire_raw(o).map(|l| FileLock(LockInner::Os { _lock: l })) } else { None };
452        let (p, rest) = match vfs::route(path) {
453            Route::Os => return os(path),
454            Route::Vfs(p, rest) => match p.os_path(&rest) {
455                Some(o) => return os(&o),
456                None => (p, rest),
457            },
458            Route::Bad => return None,
459        };
460        let key = Arc::as_ptr(&p) as *const () as usize;
461        let mut g = VFS_LOCKS.lock();
462        if rest.is_empty() || g.iter().any(|(k, n)| *k == key && *n == rest) {
463            return None;
464        }
465        match p.kind(&rest) {
466            1 => {}
467            0 if p.write(&rest, &[]) => {}
468            _ => return None,
469        }
470        g.push((key, rest.clone()));
471        Some(FileLock(LockInner::Vfs { p, path: rest }))
472    }
473}
474
475impl Drop for FileLock {
476    fn drop(&mut self) {
477        if let LockInner::Vfs { p, path } = &self.0 {
478            let key = Arc::as_ptr(p) as *const () as usize;
479            VFS_LOCKS.lock().retain(|(k, n)| !(*k == key && n == path));
480        }
481    }
482}