//! The `Store` port on the storage server, through two grants: the //! signed-in user's folder (their apps in `apps//`, each app's data in //! `data//`, their decisions in `permissions/`) and, read-only, the //! system image's apps (`/apps//`), which every user shares. use libkos::fs::Storage; use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest}; use types::address::VirtualAddress; use types::bounded::CapSlot; use crate::policy::{MAX_MANIFEST, Store}; const APPS: &str = "apps"; const DATA: &str = "data"; const DECISIONS: &str = "permissions"; const MANIFEST: &str = "manifest.toml"; /// Apps `find` looks through. pub const MAX_APPS: usize = 32; const LONGEST_PATH: usize = 160; const COPY_CHUNK: usize = 4096; /// `parts` (the empty ones skipped) joined with `/`, in `buf`. fn path<'a>(buf: &'a mut [u8; LONGEST_PATH], parts: &[&str]) -> Option<&'a [u8]> { let mut len = 0; for p in parts.iter().filter(|p| !p.is_empty()) { let sep = usize::from(len > 0); let end = len + sep + p.len(); let room = buf.get_mut(len..end)?; room[..sep].copy_from_slice(&b"/"[..sep]); room[sep..].copy_from_slice(p.as_bytes()); len = end; } Some(&buf[..len]) } /// All of the file at `path` into `buf`, as text. fn read_text<'a>(fs: &mut Storage, path: &[u8], buf: &'a mut [u8]) -> Option<&'a str> { let h = fs.open(path).ok()?; let size = fs.file_size(h).ok().filter(|s| *s <= buf.len()); let read = size.and_then(|s| fs.read(h, &mut buf[..s]).ok()); let _ = fs.close(h); let n = read.filter(|r| Some(*r) == size)?; core::str::from_utf8(&buf[..n]).ok() } /// Copies the file at `from` (in `src`) to `to` (in `dst`), replacing /// what an earlier, failed attempt left there. fn copy(src: &mut Storage, from: &[u8], dst: &mut Storage, to: &[u8]) -> bool { let Ok(input) = src.open(from) else { return false }; let _ = dst.delete(to); let Ok(output) = dst.create(to) else { let _ = src.close(input); return false; }; let mut chunk = [0u8; COPY_CHUNK]; let ok = loop { match src.read(input, &mut chunk) { Ok(0) => break true, Ok(n) if dst.write(output, &chunk[..n]) == Ok(n) => {} _ => break false, } }; let _ = src.close(input); let _ = dst.close(output); ok } /// Where an app is installed: on the system image, or by the user. #[derive(Clone, Copy, PartialEq, Eq)] enum Shelf { System, User, } pub struct DiskStore { /// The signed-in user's folder. user: Storage, /// The system image's apps, read-only. system: Storage, /// Where an offered app folder's grant is mapped while it is read. source_va: VirtualAddress, } impl DiskStore { pub fn new(user: Storage, system: Storage, source_va: VirtualAddress) -> Self { Self { user, system, source_va } } /// The storage holding `shelf`, and its apps' folder in it. fn on(&mut self, shelf: Shelf) -> (&mut Storage, &'static str) { match shelf { Shelf::System => (&mut self.system, ""), Shelf::User => (&mut self.user, APPS), } } /// The ids of the installed apps (folder names), the system's first. pub fn ids(&mut self, mut each: impl FnMut(AppId)) { for shelf in [Shelf::System, Shelf::User] { let (fs, apps) = self.on(shelf); let _ = fs.list_dir(apps.as_bytes(), |e| { if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) { each(id); } }); } } /// Which shelf holds `id`'s manifest; the system's wins, so a user /// cannot stand in for a system app. fn shelf(&mut self, id: &AppId) -> Option { [Shelf::System, Shelf::User].into_iter().find(|shelf| { let (fs, apps) = self.on(*shelf); let mut p = [0u8; LONGEST_PATH]; path(&mut p, &[apps, id.as_str(), MANIFEST]).is_some_and(|p| fs.open(p).and_then(|h| fs.close(h)).is_ok()) }) } /// A read-only grant to `app`'s binary, for init to load it from. pub fn binary_grant(&mut self, app: &Manifest) -> Option { let shelf = self.shelf(&app.id)?; let (fs, apps) = self.on(shelf); let mut p = [0u8; LONGEST_PATH]; fs.grant(path(&mut p, &[apps, app.id.as_str(), app.binary.as_str()])?, false).ok() } /// A read-write grant to `id`'s own data folder, for the app. pub fn data_grant(&mut self, id: &AppId) -> Option { let mut p = [0u8; LONGEST_PATH]; self.user.grant(path(&mut p, &[DATA, id.as_str()])?, true).ok() } fn with_source(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option { let mut src = Storage::attach(source, self.source_va).ok()?; let out = f(&mut src); src.detach(); Some(out) } } impl Store for DiskStore { /// A read-only storage grant to the offered app's folder. type Source = CapSlot; fn source_manifest<'a>(&mut self, source: &CapSlot, buf: &'a mut [u8]) -> Option<&'a str> { let mut text = [0u8; MAX_MANIFEST]; let n = self.with_source(*source, |src| read_text(src, MANIFEST.as_bytes(), &mut text).map(str::len))??; buf.get_mut(..n)?.copy_from_slice(&text[..n]); core::str::from_utf8(&buf[..n]).ok() } fn is_installed(&mut self, id: &AppId) -> bool { self.shelf(id).is_some() } fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool { let id = app.id.as_str(); let (mut folder, mut to_manifest, mut to_binary) = ([0u8; LONGEST_PATH], [0u8; LONGEST_PATH], [0u8; LONGEST_PATH]); let (Some(folder), Some(to_manifest), Some(to_binary)) = ( path(&mut folder, &[APPS, id]), path(&mut to_manifest, &[APPS, id, MANIFEST]), path(&mut to_binary, &[APPS, id, app.binary.as_str()]), ) else { return false; }; // There already if an earlier attempt failed part way; an app // counts as installed only once its manifest is in. let _ = self.user.mkdir(folder); let fs = &mut self.user; let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| { // The binary first: an app folder with a manifest is installed. let ok = copy(&mut src, app.binary.as_str().as_bytes(), fs, to_binary) && copy(&mut src, MANIFEST.as_bytes(), fs, to_manifest); src.detach(); ok }); if copied != Some(true) { return false; } let mut data = [0u8; LONGEST_PATH]; // Kept from an earlier install if there is one: the app's data. !data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.user.mkdir(d).is_ok() || self.user.list_dir(d, |_| {}).is_ok()) } fn manifest(&mut self, id: &AppId) -> Option { let shelf = self.shelf(id)?; let (fs, apps) = self.on(shelf); let mut p = [0u8; LONGEST_PATH]; let p = path(&mut p, &[apps, id.as_str(), MANIFEST])?; let mut text = [0u8; MAX_MANIFEST]; let m = Manifest::parse(read_text(fs, p, &mut text)?).ok()?; // A manifest in the wrong folder is no app. (m.id == *id).then_some(m) } fn find(&mut self, command: &str) -> Option { let mut ids = [None; MAX_APPS]; let mut n = 0; self.ids(|id| { if let Some(slot) = ids.get_mut(n) { *slot = Some(id); n += 1; } }); ids.into_iter().flatten().find(|id| self.manifest(id).is_some_and(|m| m.command.as_str() == command)) } fn decision(&mut self, id: &AppId) -> Option { let mut p = [0u8; LONGEST_PATH]; let p = path(&mut p, &[DECISIONS, id.as_str()])?; let mut text = [0u8; MAX_DECISION_TEXT]; Decision::parse(read_text(&mut self.user, p, &mut text)?).ok() } fn record(&mut self, id: &AppId, decision: &Decision) -> bool { let mut p = [0u8; LONGEST_PATH]; let Some(p) = path(&mut p, &[DECISIONS, id.as_str()]) else { return false }; let mut text = [0u8; MAX_DECISION_TEXT]; let n = decision.write(&mut text); // Replaced whole: there is no truncate. let _ = self.user.delete(p); let Ok(h) = self.user.create(p) else { return false }; let written = self.user.write(h, &text[..n]) == Ok(n); let _ = self.user.close(h); written } }