//! An app's manifest: who it is and what it asks for. use crate::Error; use crate::capability::{Capabilities, Capability}; use crate::fields::{Accent, AppId, Command, FileName, Text, Version}; use crate::toml::{self, Entry, Value}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Manifest { pub id: AppId, pub name: Text<64>, pub version: Version, pub binary: FileName, /// What the shell runs it as. pub command: Command, pub author: Option>, pub icon: Option, /// Denied = not installed. pub required: Capabilities, /// The owner decides each. pub optional: Capabilities, pub accent: Option, } /// Fields as they are read, before the required ones are known present. #[derive(Default)] struct Draft { id: Option, name: Option>, version: Option, binary: Option, command: Option, author: Option>, icon: Option, required: Option, optional: Option, accent: Option, } /// Sets `slot` once; a second value for the same key is an error. fn set(slot: &mut Option, value: Option, line: usize) -> Result<(), Error> { if slot.is_some() { return Err(Error::Duplicate { line }); } *slot = Some(value.ok_or(Error::Invalid { line })?); Ok(()) } fn text<'a>(value: Value<'a>) -> Option<&'a str> { match value { Value::Str(s) => Some(s), Value::List(_) => None, } } /// A list of capability names, each known and listed once. pub(crate) fn capabilities(value: Value, line: usize) -> Result { let Value::List(list) = value else { return Err(Error::Invalid { line }); }; let mut set = Capabilities::NONE; for name in list.iter() { let c = Capability::from_name(name).ok_or(Error::Unknown { line })?; if set.contains(c) { return Err(Error::Duplicate { line }); } set = set.with(c); } Ok(set) } impl Draft { fn take(&mut self, e: Entry) -> Result<(), Error> { let l = e.line; let s = text(e.value); match (e.table, e.key) { ("app", "id") => set(&mut self.id, s.and_then(AppId::new), l), ("app", "name") => set(&mut self.name, s.and_then(Text::new), l), ("app", "version") => set(&mut self.version, s.and_then(Version::parse), l), ("app", "binary") => set(&mut self.binary, s.and_then(FileName::new), l), ("app", "command") => set(&mut self.command, s.and_then(Command::new), l), ("app", "author") => set(&mut self.author, s.and_then(Text::new), l), ("app", "icon") => set(&mut self.icon, s.and_then(FileName::new), l), ("capabilities", "required") => { set(&mut self.required, Some(capabilities(e.value, l)?), l) } ("capabilities", "optional") => { set(&mut self.optional, Some(capabilities(e.value, l)?), l) } ("appearance", "accent") => set(&mut self.accent, s.and_then(Accent::parse), l), _ => Err(Error::Unknown { line: l }), } } } impl Manifest { pub fn parse(text: &str) -> Result { let mut d = Draft::default(); toml::parse(text, |e| d.take(e))?; let binary = d.binary.ok_or(Error::Missing("binary"))?; let command = match d.command { Some(c) => c, // A binary name is a command name when it fits the stricter rules. None => Command::new(binary.as_str()).ok_or(Error::Missing("command"))?, }; let required = d.required.unwrap_or_default(); let optional = d.optional.unwrap_or_default(); if !required.intersect(optional).is_empty() { return Err(Error::Conflict); } Ok(Self { id: d.id.ok_or(Error::Missing("id"))?, name: d.name.ok_or(Error::Missing("name"))?, version: d.version.ok_or(Error::Missing("version"))?, binary, command, author: d.author, icon: d.icon, required, optional, accent: d.accent, }) } /// Everything it asks for. pub fn requested(&self) -> Capabilities { self.required.union(self.optional) } }