asset.rs
raw
//! 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<Result<String, Diagnostic>> {
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<String, Diagnostic> {
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;