//! Asset-reference literals: `(asset "icons/logo.png")`, naming a file to be //! resolved against the asset search path. //! //! The form is read from s-expressions, which both surface languages have in //! hand at the point they meet a value — so this is the only parser, and the //! spelling cannot drift between `.gdc` and theme files. //! //! Nothing here knows what *kind* of asset a path names. The form carries a //! path and nothing else; the type of the slot it lands in decides what the //! bytes mean, and the consumer decides how to get them — the typed build //! embeds them with `include_bytes!`, the dev runtime reads them from disk. //! That is what keeps this crate filesystem-free, and what keeps the form //! open to assets that are not images. use crate::diagnostics::Diagnostic; use crate::sexpr::{Sexpr, Span}; /// The head symbol of an asset form. It never collides with the `image` /// widget that draws one: this form is only ever a *value*, and a widget is /// only ever in widget position. pub const ASSET_FORM: &str = "asset"; /// Parse an `(asset "path")` list form. `items` is the whole list including /// the head symbol; returns `None` (not an error) when the head is something /// else, so callers can fall through to other value kinds. pub fn parse_asset_form(items: &[Sexpr], span: Span) -> Option> { if items.first().and_then(Sexpr::as_symbol) != Some(ASSET_FORM) { return None; } let result = match &items[1..] { [path] => match path { Sexpr::Str(text, span) => asset_path(text, *span), other => Err(Diagnostic::new( "an asset path is a string, like `(asset \"icons/logo.png\")`", other.span(), )), }, _ => Err(Diagnostic::new( "`asset` takes exactly one path: `(asset \"icons/logo.png\")`", span, )), }; Some(result) } /// Every rule about what an asset path may say. fn asset_path(text: &str, span: Span) -> Result { if text.is_empty() { return Err(Diagnostic::new("an asset path cannot be empty", span)); } // The form is read straight from the s-expression, so a `.gdc` author's // `{name}` would arrive here as literal characters rather than as the // interpolation they meant. Say so: the bytes are found at build time, // and there is nothing for a runtime value to name. if text.contains('{') || text.contains('}') { return Err(Diagnostic::new( "an asset path is fixed at build time, so it cannot use interpolation", span, )); } // Absolute paths and parent traversal reach outside the search path, // which would make a build depend on the machine it runs on. if text.starts_with('/') { return Err(Diagnostic::new( "an asset path is relative to the asset search path, not absolute", span, )); } if text.split('/').any(|part| part == "..") { return Err(Diagnostic::new( "an asset path cannot climb out of the asset search path with `..`", span, )); } Ok(text.to_owned()) } #[cfg(test)] mod tests;