Lines
100 %
Functions
80 %
Branches
//! Canonical host-fn registry for the 32 server commands.
//!
//! Each domain owns a submodule that mirrors the `server::command::*` shape and
//! holds one file per server command. Commands register their host fn through a
//! per-domain `register` entry point; the top-level `register_all` aggregates
//! every domain so a wasmtime [`Linker`](wasmtime::Linker) gains the full
//! canonical surface in one call.
//! Subsequent commits fill each domain. This commit lays down the directory
//! shape so per-command commits stay focused on one server command at a time.
use wasmtime::{ArrayRef, Linker, Rooted};
use crate::session::SessionData;
/// One stringly-typed wasm host-fn argument as it lands on the
/// `linker.func_wrap_async` closure: nullable because the arg can be
/// absent, `Rooted<ArrayRef>` because the wasm side encodes strings as
/// `(ref null $i8_array)`.
pub(crate) type StringArg = Option<Rooted<ArrayRef>>;
/// Three-arg tuple shape used by `account_set_account_tag` and other
/// host fns that take three string args.
pub(crate) type StringArgTriple = (StringArg, StringArg, StringArg);
pub mod account;
pub mod catch_each;
pub mod commodity;
pub mod config;
pub mod env_io;
mod generated_specs;
pub mod meta;
pub mod raise;
mod render;
pub mod report;
pub mod split;
pub mod ssh_key;
pub mod template;
pub mod transaction;
pub mod user;
pub use generated_specs::all_compiler_specs;
pub use render::{RENDER_NATIVE_ALLOWLIST, link_render, render_compiler_specs};
/// Aggregator: registers every domain's host fns on `linker` in one call.
/// Bound to [`SessionData`] now that `meta` natives consult `ScriptCtx`
/// (user_id) — generic-T register fns from the empty per-domain modules
/// still satisfy this concrete type because they don't touch the Store data.
pub fn link(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
meta::register(linker)?;
env_io::register(linker)?;
raise::register(linker)?;
catch_each::register(linker)?;
account::register(linker)?;
commodity::register(linker)?;
config::register(linker)?;
report::register(linker)?;
split::register(linker)?;
ssh_key::register(linker)?;
template::register(linker)?;
transaction::register(linker)?;
user::register(linker)?;
Ok(())
}
// `all_compiler_specs` lives in `generated_specs` (tangled from
// `doc/scripting/native_reference.org` by `rpc/build.rs`) — see the
// `pub use` import above. Adding or modifying a native means editing
// the org table; cargo build regenerates the Rust.
#[must_use]
pub fn all_registered_commands() -> Vec<&'static str> {
[
account::REGISTERED_COMMANDS,
commodity::REGISTERED_COMMANDS,
config::REGISTERED_COMMANDS,
report::REGISTERED_COMMANDS,
split::REGISTERED_COMMANDS,
ssh_key::REGISTERED_COMMANDS,
transaction::REGISTERED_COMMANDS,
user::REGISTERED_COMMANDS,
]
.iter()
.flat_map(|d| d.iter().copied())
.collect()
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_lists_every_planned_command() {
let names = all_registered_commands();
assert_eq!(names.len(), 36, "command count drifted: {names:?}");
fn registry_uses_kebab_case_only() {
for name in all_registered_commands() {
assert!(
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
"non-kebab-case name: {name}"
);
assert!(!name.starts_with('-'), "leading dash: {name}");
assert!(!name.ends_with('-'), "trailing dash: {name}");
fn registry_has_no_duplicates() {
let mut sorted = names.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
names.len(),
sorted.len(),
"duplicate command names: {names:?}"
fn registry_excludes_add_ssh_key_by_design() {
!names.contains(&"add-ssh-key"),
"add-ssh-key must not be exposed via the eval channel; pubkey upload uses the dedicated ssh-copy-id exec path"
fn each_domain_has_at_least_one_command() {
assert!(!account::REGISTERED_COMMANDS.is_empty());
assert!(!commodity::REGISTERED_COMMANDS.is_empty());
assert!(!config::REGISTERED_COMMANDS.is_empty());
assert!(!report::REGISTERED_COMMANDS.is_empty());
assert!(!split::REGISTERED_COMMANDS.is_empty());
assert!(!ssh_key::REGISTERED_COMMANDS.is_empty());
assert!(!transaction::REGISTERED_COMMANDS.is_empty());
assert!(!user::REGISTERED_COMMANDS.is_empty());
fn link_succeeds_against_session_data_linker() {
let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
.expect("engine");
let mut linker: wasmtime::Linker<crate::session::SessionData> =
wasmtime::Linker::new(&engine);
link(&mut linker).expect("link must succeed");
fn each_empty_server_domain_register_succeeds_in_isolation() {
// Domains that haven't shipped real natives yet keep their `register`
// fns generic over T so empty composition is cheap to verify. As
// domains gain real natives (each one becoming SessionData-specific),
// they migrate out of this loop into dedicated tests.
// All domains now have SessionData-specific natives. Nothing left
// in the generic-T empty-domain loop, but keep the test placeholder
// so the next stub-domain (if any) has an obvious home to slot
// back into.
let _engine = engine;
fn account_register_succeeds_against_session_data() {
account::register(&mut linker).expect("account::register must succeed");
fn commodity_register_succeeds_against_session_data() {
commodity::register(&mut linker).expect("commodity::register must succeed");
fn transaction_register_succeeds_against_session_data() {
transaction::register(&mut linker).expect("transaction::register must succeed");
fn report_register_succeeds_against_session_data() {
report::register(&mut linker).expect("report::register must succeed");
fn user_register_succeeds_against_session_data() {
user::register(&mut linker).expect("user::register must succeed");
fn split_register_succeeds_against_session_data() {
split::register(&mut linker).expect("split::register must succeed");
fn ssh_key_register_succeeds_against_session_data() {
ssh_key::register(&mut linker).expect("ssh_key::register must succeed");
fn config_register_succeeds_against_session_data() {
// config now ships server-backed natives so its register fn is
// SessionData-specific. Verified in isolation alongside the empty
// domains. Async engine required because the host fns are
// registered via func_wrap_async.
config::register(&mut linker).expect("config::register must succeed");