Skip to main content

assay/install/
manifest.rs

1//! `Manifest.lua` parser. Reads a Lua return-table declaring extension
2//! binaries + Lua libraries to install.
3//!
4//! Manifest.lua is evaluated in a heavily restricted mlua VM:
5//! - StdLib loaded: `BASE` (always, by mlua) + `STRING` + `TABLE` + `MATH`.
6//! - StdLib withheld: `IO`, `OS`, `COROUTINE`, `UTF8`, `DEBUG`, `PACKAGE`.
7//! - Code-loading primitives nilled: `load`, `loadfile`, `loadstring`,
8//!   `dofile`, `require`.
9//!
10//! A malicious or accidental `os.execute("...")` therefore fails with
11//! "attempt to index a nil value (global 'os')" before any side effect.
12
13use std::collections::HashMap;
14use std::path::Path;
15
16use mlua::{Lua, LuaOptions, LuaSerdeExt, StdLib, Value};
17use serde::Deserialize;
18use thiserror::Error;
19
20#[derive(Debug, Error)]
21pub enum ManifestError {
22    #[error("manifest {path}: lua error: {source}")]
23    Lua {
24        path: String,
25        #[source]
26        source: mlua::Error,
27    },
28
29    #[error("manifest {path}: top-level value is {got}, expected a table")]
30    NotATable { path: String, got: String },
31
32    #[error("manifest {path}: failed to decode: {source}")]
33    Decode {
34        path: String,
35        #[source]
36        source: mlua::Error,
37    },
38}
39
40/// Parsed `Manifest.lua` contents.
41#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct Manifest {
44    /// Self-version pin for the assay binary (advisory). Install warns
45    /// — does not abort — if the running binary differs.
46    pub assay: Option<String>,
47
48    /// Compiled separate binaries (e.g. `assay-engine`).
49    #[serde(default)]
50    pub extensions: Vec<Extension>,
51
52    /// Lua libraries shipped as tarballs (e.g. `sysops`).
53    #[serde(default)]
54    pub libs: Vec<Lib>,
55}
56
57/// One declared extension binary. Per-arch sha256 because the binary
58/// differs per target triple.
59#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct Extension {
62    pub name: String,
63    pub version: String,
64    /// Map from arch token (e.g. `x86_64`, `aarch64`) to sha256 hex.
65    pub sha256: HashMap<String, String>,
66    /// Optional full-URL override. If absent, install resolves to the
67    /// assay-release URL convention.
68    #[serde(default)]
69    pub source: Option<String>,
70}
71
72/// One declared Lua library. Single sha256 because libs are arch-neutral
73/// (pure Lua).
74#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct Lib {
77    pub name: String,
78    pub version: String,
79    pub sha256: String,
80    #[serde(default)]
81    pub source: Option<String>,
82}
83
84/// Parse a `Manifest.lua` source string.
85///
86/// `path` is used only for error messages; the source is evaluated as an
87/// in-memory chunk and the parser performs no filesystem access.
88pub fn parse(source: &str, path: impl AsRef<Path>) -> Result<Manifest, ManifestError> {
89    let path_str = path.as_ref().display().to_string();
90
91    let lua = sandboxed_vm().map_err(|e| ManifestError::Lua {
92        path: path_str.clone(),
93        source: e,
94    })?;
95
96    let chunk = lua.load(source).set_name(path_str.clone());
97    let value: Value = chunk.eval().map_err(|e| ManifestError::Lua {
98        path: path_str.clone(),
99        source: e,
100    })?;
101
102    if !matches!(value, Value::Table(_)) {
103        return Err(ManifestError::NotATable {
104            path: path_str,
105            got: value.type_name().to_string(),
106        });
107    }
108
109    lua.from_value::<Manifest>(value)
110        .map_err(|e| ManifestError::Decode {
111            path: path_str,
112            source: e,
113        })
114}
115
116fn sandboxed_vm() -> mlua::Result<Lua> {
117    let libs = StdLib::STRING | StdLib::TABLE | StdLib::MATH;
118    let lua = Lua::new_with(libs, LuaOptions::default())?;
119
120    // BASE is always loaded by mlua. Strip the primitives that allow
121    // loading arbitrary Lua code from strings or disk.
122    let globals = lua.globals();
123    for name in ["load", "loadfile", "loadstring", "dofile", "require"] {
124        globals.set(name, mlua::Nil)?;
125    }
126
127    Ok(lua)
128}