1
//! Canonical host-fn registry for the 32 server commands.
2
//!
3
//! Each domain owns a submodule that mirrors the `server::command::*` shape and
4
//! holds one file per server command. Commands register their host fn through a
5
//! per-domain `register` entry point; the top-level `register_all` aggregates
6
//! every domain so a wasmtime [`Linker`](wasmtime::Linker) gains the full
7
//! canonical surface in one call.
8
//!
9
//! Subsequent commits fill each domain. This commit lays down the directory
10
//! shape so per-command commits stay focused on one server command at a time.
11

            
12
use wasmtime::{ArrayRef, Linker, Rooted};
13

            
14
use crate::session::SessionData;
15

            
16
/// One stringly-typed wasm host-fn argument as it lands on the
17
/// `linker.func_wrap_async` closure: nullable because the arg can be
18
/// absent, `Rooted<ArrayRef>` because the wasm side encodes strings as
19
/// `(ref null $i8_array)`.
20
pub(crate) type StringArg = Option<Rooted<ArrayRef>>;
21
/// Three-arg tuple shape used by `account_set_account_tag` and other
22
/// host fns that take three string args.
23
pub(crate) type StringArgTriple = (StringArg, StringArg, StringArg);
24

            
25
pub mod account;
26
pub mod catch_each;
27
pub mod commodity;
28
pub mod config;
29
pub mod env_io;
30
mod generated_specs;
31
pub mod meta;
32
pub mod raise;
33
mod render;
34
pub mod report;
35
pub mod split;
36
pub mod ssh_key;
37
pub mod template;
38
pub mod transaction;
39
pub mod user;
40

            
41
pub use generated_specs::all_compiler_specs;
42
pub use render::{RENDER_NATIVE_ALLOWLIST, link_render, render_compiler_specs};
43

            
44
/// Aggregator: registers every domain's host fns on `linker` in one call.
45
/// Bound to [`SessionData`] now that `meta` natives consult `ScriptCtx`
46
/// (user_id) — generic-T register fns from the empty per-domain modules
47
/// still satisfy this concrete type because they don't touch the Store data.
48
2558
pub fn link(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
49
2558
    meta::register(linker)?;
50
2558
    env_io::register(linker)?;
51
2558
    raise::register(linker)?;
52
2558
    catch_each::register(linker)?;
53
2558
    account::register(linker)?;
54
2558
    commodity::register(linker)?;
55
2558
    config::register(linker)?;
56
2558
    report::register(linker)?;
57
2558
    split::register(linker)?;
58
2558
    ssh_key::register(linker)?;
59
2558
    template::register(linker)?;
60
2558
    transaction::register(linker)?;
61
2558
    user::register(linker)?;
62
2558
    Ok(())
63
2558
}
64

            
65
// `all_compiler_specs` lives in `generated_specs` (tangled from
66
// `doc/scripting/native_reference.org` by `rpc/build.rs`) — see the
67
// `pub use` import above. Adding or modifying a native means editing
68
// the org table; cargo build regenerates the Rust.
69

            
70
#[must_use]
71
4
pub fn all_registered_commands() -> Vec<&'static str> {
72
4
    [
73
4
        account::REGISTERED_COMMANDS,
74
4
        commodity::REGISTERED_COMMANDS,
75
4
        config::REGISTERED_COMMANDS,
76
4
        report::REGISTERED_COMMANDS,
77
4
        split::REGISTERED_COMMANDS,
78
4
        ssh_key::REGISTERED_COMMANDS,
79
4
        transaction::REGISTERED_COMMANDS,
80
4
        user::REGISTERED_COMMANDS,
81
4
    ]
82
4
    .iter()
83
32
    .flat_map(|d| d.iter().copied())
84
4
    .collect()
85
4
}
86

            
87
#[cfg(test)]
88
mod tests {
89
    use super::*;
90

            
91
    #[test]
92
1
    fn registry_lists_every_planned_command() {
93
1
        let names = all_registered_commands();
94
1
        assert_eq!(names.len(), 36, "command count drifted: {names:?}");
95
1
    }
96

            
97
    #[test]
98
1
    fn registry_uses_kebab_case_only() {
99
36
        for name in all_registered_commands() {
100
36
            assert!(
101
36
                name.chars()
102
572
                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
103
                "non-kebab-case name: {name}"
104
            );
105
36
            assert!(!name.starts_with('-'), "leading dash: {name}");
106
36
            assert!(!name.ends_with('-'), "trailing dash: {name}");
107
        }
108
1
    }
109

            
110
    #[test]
111
1
    fn registry_has_no_duplicates() {
112
1
        let names = all_registered_commands();
113
1
        let mut sorted = names.clone();
114
1
        sorted.sort_unstable();
115
1
        sorted.dedup();
116
1
        assert_eq!(
117
1
            names.len(),
118
1
            sorted.len(),
119
            "duplicate command names: {names:?}"
120
        );
121
1
    }
122

            
123
    #[test]
124
1
    fn registry_excludes_add_ssh_key_by_design() {
125
1
        let names = all_registered_commands();
126
1
        assert!(
127
1
            !names.contains(&"add-ssh-key"),
128
            "add-ssh-key must not be exposed via the eval channel; pubkey upload uses the dedicated ssh-copy-id exec path"
129
        );
130
1
    }
131

            
132
    #[test]
133
1
    fn each_domain_has_at_least_one_command() {
134
1
        assert!(!account::REGISTERED_COMMANDS.is_empty());
135
1
        assert!(!commodity::REGISTERED_COMMANDS.is_empty());
136
1
        assert!(!config::REGISTERED_COMMANDS.is_empty());
137
1
        assert!(!report::REGISTERED_COMMANDS.is_empty());
138
1
        assert!(!split::REGISTERED_COMMANDS.is_empty());
139
1
        assert!(!ssh_key::REGISTERED_COMMANDS.is_empty());
140
1
        assert!(!transaction::REGISTERED_COMMANDS.is_empty());
141
1
        assert!(!user::REGISTERED_COMMANDS.is_empty());
142
1
    }
143

            
144
    #[test]
145
1
    fn link_succeeds_against_session_data_linker() {
146
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
147
1
            .expect("engine");
148
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
149
1
            wasmtime::Linker::new(&engine);
150
1
        link(&mut linker).expect("link must succeed");
151
1
    }
152

            
153
    #[test]
154
1
    fn each_empty_server_domain_register_succeeds_in_isolation() {
155
        // Domains that haven't shipped real natives yet keep their `register`
156
        // fns generic over T so empty composition is cheap to verify. As
157
        // domains gain real natives (each one becoming SessionData-specific),
158
        // they migrate out of this loop into dedicated tests.
159
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
160
1
            .expect("engine");
161
        // All domains now have SessionData-specific natives. Nothing left
162
        // in the generic-T empty-domain loop, but keep the test placeholder
163
        // so the next stub-domain (if any) has an obvious home to slot
164
        // back into.
165
1
        let _engine = engine;
166
1
    }
167

            
168
    #[test]
169
1
    fn account_register_succeeds_against_session_data() {
170
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
171
1
            .expect("engine");
172
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
173
1
            wasmtime::Linker::new(&engine);
174
1
        account::register(&mut linker).expect("account::register must succeed");
175
1
    }
176

            
177
    #[test]
178
1
    fn commodity_register_succeeds_against_session_data() {
179
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
180
1
            .expect("engine");
181
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
182
1
            wasmtime::Linker::new(&engine);
183
1
        commodity::register(&mut linker).expect("commodity::register must succeed");
184
1
    }
185

            
186
    #[test]
187
1
    fn transaction_register_succeeds_against_session_data() {
188
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
189
1
            .expect("engine");
190
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
191
1
            wasmtime::Linker::new(&engine);
192
1
        transaction::register(&mut linker).expect("transaction::register must succeed");
193
1
    }
194

            
195
    #[test]
196
1
    fn report_register_succeeds_against_session_data() {
197
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
198
1
            .expect("engine");
199
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
200
1
            wasmtime::Linker::new(&engine);
201
1
        report::register(&mut linker).expect("report::register must succeed");
202
1
    }
203

            
204
    #[test]
205
1
    fn user_register_succeeds_against_session_data() {
206
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
207
1
            .expect("engine");
208
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
209
1
            wasmtime::Linker::new(&engine);
210
1
        user::register(&mut linker).expect("user::register must succeed");
211
1
    }
212

            
213
    #[test]
214
1
    fn split_register_succeeds_against_session_data() {
215
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
216
1
            .expect("engine");
217
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
218
1
            wasmtime::Linker::new(&engine);
219
1
        split::register(&mut linker).expect("split::register must succeed");
220
1
    }
221

            
222
    #[test]
223
1
    fn ssh_key_register_succeeds_against_session_data() {
224
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
225
1
            .expect("engine");
226
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
227
1
            wasmtime::Linker::new(&engine);
228
1
        ssh_key::register(&mut linker).expect("ssh_key::register must succeed");
229
1
    }
230

            
231
    #[test]
232
1
    fn config_register_succeeds_against_session_data() {
233
        // config now ships server-backed natives so its register fn is
234
        // SessionData-specific. Verified in isolation alongside the empty
235
        // domains. Async engine required because the host fns are
236
        // registered via func_wrap_async.
237
1
        let engine = crate::wasm::build_engine(crate::wasm::EngineOpts::baseline().with_fuel())
238
1
            .expect("engine");
239
1
        let mut linker: wasmtime::Linker<crate::session::SessionData> =
240
1
            wasmtime::Linker::new(&engine);
241
1
        config::register(&mut linker).expect("config::register must succeed");
242
1
    }
243
}