pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("lua error in {chunk}: {source}")]
Lua {
chunk: String,
#[source]
source: Box<mlua::Error>,
},
#[error("cannot configure the Lua state ({stage}): {source}")]
EngineSetup {
stage: &'static str,
#[source]
source: Box<mlua::Error>,
},
#[error("script `{chunk}` exceeded its memory limit of {limit} bytes")]
MemoryLimit {
chunk: String,
limit: usize,
#[source]
source: Box<mlua::Error>,
},
#[error("script `{chunk}` exceeded its instruction limit of {limit}")]
InstructionLimit {
chunk: String,
limit: u64,
},
#[error("failed to install host module `{module}`: {reason}")]
ModuleInstall {
module: String,
reason: String,
},
#[error("host module `{module}` is already registered")]
DuplicateModule {
module: String,
},
#[error("host module `{module}` is not registered")]
ModuleNotFound {
module: String,
},
#[error(
"event `{event}` is not one this host dispatches; {}",
describe_declared(declared)
)]
UnknownEvent {
event: String,
declared: Vec<String>,
},
#[error("{}", describe_reentrant(event.as_deref()))]
Reentrant {
event: Option<String>,
},
#[error("cannot read manifest `{path}`: {source}")]
ManifestRead {
path: String,
#[source]
source: std::io::Error,
},
#[error("cannot parse manifest `{path}`: {reason}")]
ManifestParse {
path: String,
reason: String,
},
#[error("invalid manifest field `{field}`: {reason}")]
ManifestInvalid {
field: &'static str,
reason: String,
},
#[error("manifest refers to `${name}`, which the host did not supply")]
ManifestVariable {
name: String,
},
#[error(
"manifest declares api {requested}; this runtime supports api {}",
join_versions(supported)
)]
UnsupportedApi {
requested: u32,
supported: Vec<u32>,
},
#[error("the ceiling is not a bound: {reason}")]
CeilingUnbounded {
reason: &'static str,
},
#[error("extension `{extension}` was not loaded: {detail}")]
ExtensionDenied {
extension: String,
detail: String,
},
#[error("extension `{extension}` is already loaded")]
DuplicateExtension {
extension: String,
},
#[error("invalid {kind} `{value}`: {reason}")]
InvalidName {
kind: &'static str,
value: String,
reason: &'static str,
},
#[error("cannot read script `{path}`: {source}")]
ScriptRead {
path: String,
#[source]
source: std::io::Error,
},
#[error("module `{module}` resolves outside the script directory `{root}`")]
RequireEscape {
module: String,
root: String,
},
#[error("module `{module}` requires itself, directly or indirectly, under `{root}`")]
RequireCycle {
module: String,
root: String,
},
#[error("{module}.{operation} denied: {detail}")]
Denied {
module: &'static str,
operation: &'static str,
detail: String,
},
#[error("{operation} failed on `{path}`: {source}")]
Io {
operation: &'static str,
path: String,
#[source]
source: std::io::Error,
},
#[error("cannot resolve `{path}` to check it against the policy: {reason}")]
UncheckablePath {
path: String,
reason: &'static str,
},
#[error("path `{path}` is outside `{base}`")]
PathNotRelative {
path: String,
base: String,
},
#[error("cannot resolve path `{path}`: {source}")]
PathResolution {
path: String,
#[source]
source: std::io::Error,
},
#[error("module `{module}` not found under `{root}`")]
RequireNotFound {
module: String,
root: String,
},
}
fn describe_declared(declared: &[String]) -> String {
if declared.is_empty() {
String::from("this runtime dispatches no events")
} else {
format!("declared events: {}", declared.join(", "))
}
}
fn describe_reentrant(event: Option<&str>) -> String {
event.map_or_else(
|| String::from("re-entrant evaluation"),
|event| format!("re-entrant call during handler for event `{event}`"),
)
}
fn join_versions(versions: &[u32]) -> String {
versions
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExhaustedLimit {
Memory,
Instructions,
}
impl Error {
#[must_use]
pub fn lua(chunk: impl Into<String>, source: mlua::Error) -> Self {
Self::Lua {
chunk: chunk.into(),
source: Box::new(source),
}
}
#[must_use]
pub const fn exhausted_limit(&self) -> Option<ExhaustedLimit> {
match self {
Self::MemoryLimit { .. } => Some(ExhaustedLimit::Memory),
Self::InstructionLimit { .. } => Some(ExhaustedLimit::Instructions),
_ => None,
}
}
}
impl From<Error> for mlua::Error {
fn from(value: Error) -> Self {
Self::external(value)
}
}
#[cfg(test)]
mod tests {
use super::Error;
#[test]
fn lua_wraps_chunk_name_into_the_message() {
let err = Error::lua("enforce.lua", mlua::Error::RuntimeError("boom".into()));
assert!(err.to_string().starts_with("lua error in enforce.lua: "));
}
#[test]
fn require_escape_names_both_module_and_root() {
let err = Error::RequireEscape {
module: "../secrets".into(),
root: "/scripts".into(),
};
assert_eq!(
err.to_string(),
"module `../secrets` resolves outside the script directory `/scripts`"
);
}
#[test]
fn converting_into_an_mlua_error_preserves_the_message() {
let err = Error::DuplicateModule {
module: "fs".into(),
};
let text = err.to_string();
let lua: mlua::Error = err.into();
assert!(lua.to_string().contains(&text));
}
#[test]
fn module_not_found_names_the_module() {
let err = Error::ModuleNotFound {
module: "ext".into(),
};
assert_eq!(err.to_string(), "host module `ext` is not registered");
}
#[test]
fn unknown_event_names_what_was_declared() {
let err = Error::UnknownEvent {
event: "tpyo".into(),
declared: vec!["note_saved".into(), "query".into()],
};
assert_eq!(
err.to_string(),
"event `tpyo` is not one this host dispatches; declared events: note_saved, query"
);
}
#[test]
fn unknown_event_with_nothing_declared_says_so() {
let err = Error::UnknownEvent {
event: "x".into(),
declared: Vec::new(),
};
assert_eq!(
err.to_string(),
"event `x` is not one this host dispatches; this runtime dispatches no events"
);
}
#[test]
fn reentrant_names_the_handler_s_event_when_one_is_running() {
let err = Error::Reentrant {
event: Some("query".into()),
};
assert_eq!(
err.to_string(),
"re-entrant call during handler for event `query`"
);
}
#[test]
fn reentrant_with_no_event_describes_a_plain_re_entrant_evaluation() {
let err = Error::Reentrant { event: None };
assert_eq!(err.to_string(), "re-entrant evaluation");
}
#[test]
fn manifest_invalid_names_the_field() {
let err = Error::ManifestInvalid {
field: "capabilities.fs.read",
reason: "`journal` is not absolute after expansion".into(),
};
assert_eq!(
err.to_string(),
"invalid manifest field `capabilities.fs.read`: `journal` is not absolute after expansion"
);
}
#[test]
fn manifest_variable_names_the_variable() {
let err = Error::ManifestVariable {
name: "APP_HOME".into(),
};
assert_eq!(
err.to_string(),
"manifest refers to `$APP_HOME`, which the host did not supply"
);
}
#[test]
fn unsupported_api_lists_the_supported_set() {
let err = Error::UnsupportedApi {
requested: 7,
supported: vec![1],
};
assert_eq!(
err.to_string(),
"manifest declares api 7; this runtime supports api 1"
);
}
#[test]
fn ceiling_unbounded_states_the_reason() {
let err = Error::CeilingUnbounded {
reason: "grants are unrestricted",
};
assert_eq!(
err.to_string(),
"the ceiling is not a bound: grants are unrestricted"
);
}
#[test]
fn extension_denied_names_the_extension_and_the_detail() {
let err = Error::ExtensionDenied {
extension: "journal-indexer".into(),
detail: "fs.read `/` is outside the ceiling".into(),
};
assert_eq!(
err.to_string(),
"extension `journal-indexer` was not loaded: fs.read `/` is outside the ceiling"
);
}
#[test]
fn duplicate_extension_names_the_extension() {
let err = Error::DuplicateExtension {
extension: "journal-indexer".into(),
};
assert_eq!(
err.to_string(),
"extension `journal-indexer` is already loaded"
);
}
}