1
//! Config-domain natives. Wraps `server::command::{GetConfig, GetVersion,
2
//! GetBuildDate, SetConfig}`.
3
//!
4
//! `get-version` and `get-build-date` are the first server-command bindings
5
//! the rpc layer ships. Both return a `CmdResult::String(...)` from a body
6
//! that just reads `env!`-baked constants — no DB, no user_id, no real async
7
//! work — so they exercise the marshalling shape without yet needing a
8
//! tokio runtime or pool. The remaining commands wait until DB-touching
9
//! infrastructure (ScriptCtx::pool, sqlx::test fixtures) lands.
10
//!
11
//! `select-column` is deliberately NOT exposed here: it runs caller-supplied
12
//! `field`/`table` against the global admin connection, so a script-callable
13
//! native would let any authenticated session read outside its user boundary.
14
//! It stays a CLI-only escape hatch (`sql selcol`, direct dispatch).
15

            
16
use base64::Engine;
17
use base64::engine::general_purpose::STANDARD as BASE64;
18
use scripting::runtime::{alloc_string_ref, read_string_arg};
19
use server::command::{
20
    CmdError, CmdResult,
21
    config::{GetBuildDate, GetConfig, GetVersion, SetConfig},
22
};
23
use wasmtime::{ArrayRef, Caller, Linker, Rooted};
24

            
25
use crate::session::SessionData;
26

            
27
pub const REGISTERED_COMMANDS: &[&str] =
28
    &["get-config", "get-version", "get-build-date", "set-config"];
29

            
30
5909
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
31
5909
    linker.func_wrap_async(
32
5909
        "nomi",
33
5909
        "config_get_version",
34
        |mut caller: Caller<'_, SessionData>,
35
         ()|
36
         -> Box<
37
            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
38
26
        > {
39
26
            Box::new(async move {
40
26
                let bytes = command_string("get-version", GetVersion::new().run().await)?;
41
26
                Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
42
26
            })
43
26
        },
44
    )?;
45
5909
    linker.func_wrap_async(
46
5909
        "nomi",
47
5909
        "config_get_build_date",
48
        |mut caller: Caller<'_, SessionData>,
49
         ()|
50
         -> Box<
51
            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
52
1
        > {
53
1
            Box::new(async move {
54
1
                let bytes = command_string("get-build-date", GetBuildDate::new().run().await)?;
55
1
                Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
56
1
            })
57
1
        },
58
    )?;
59
5909
    linker.func_wrap_async(
60
5909
        "nomi",
61
5909
        "config_get_config",
62
        |mut caller: Caller<'_, SessionData>,
63
         (name_arg,): (Option<Rooted<ArrayRef>>,)|
64
         -> Box<
65
            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
66
77
        > {
67
77
            Box::new(async move {
68
77
                let user_id = caller.data().ctx().user_id;
69
77
                let name = read_string_arg(&mut caller, name_arg)?;
70
77
                let formatted = run_get_config(user_id, name).await?;
71
75
                Ok(Some(alloc_string_ref(&mut caller, formatted.as_bytes())?))
72
77
            })
73
77
        },
74
    )?;
75
5909
    linker.func_wrap_async(
76
5909
        "nomi",
77
5909
        "config_set_config",
78
        |mut caller: Caller<'_, SessionData>,
79
         (name_arg, value_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
80
25
         -> Box<dyn std::future::Future<Output = wasmtime::Result<i32>> + Send> {
81
25
            Box::new(async move {
82
25
                let user_id = caller.data().ctx().user_id;
83
25
                let name = read_string_arg(&mut caller, name_arg)?;
84
25
                let value = read_string_arg(&mut caller, value_arg)?;
85
25
                run_set_config(user_id, name, value).await
86
25
            })
87
25
        },
88
    )?;
89
5909
    Ok(())
90
5909
}
91

            
92
27
async fn run_set_config(
93
27
    user_id: uuid::Uuid,
94
27
    name_arg: Option<String>,
95
27
    value_arg: Option<String>,
96
27
) -> wasmtime::Result<i32> {
97
26
    let name = match name_arg {
98
26
        Some(s) if !s.is_empty() => s,
99
        _ => {
100
1
            return Err(wasmtime::Error::msg(
101
1
                "set-config: missing or empty :name arg",
102
1
            ));
103
        }
104
    };
105
26
    let value = match value_arg {
106
25
        Some(s) => s,
107
1
        _ => return Err(wasmtime::Error::msg("set-config: missing :value arg")),
108
    };
109
25
    SetConfig::new()
110
25
        .user_id(user_id)
111
25
        .name(name)
112
25
        .value(value)
113
25
        .run()
114
25
        .await
115
25
        .map(|_| 1)
116
25
        .map_err(|err| wasmtime::Error::msg(format!("set-config: {err}")))
117
27
}
118

            
119
/// Returns `(:config-value <val>)` on success, or propagates a
120
/// `wasmtime::Error` on any validation/command failure. The rpc envelope
121
/// layer turns traps into `(:id N :error ...)` responses.
122
79
async fn run_get_config(user_id: uuid::Uuid, name_arg: Option<String>) -> wasmtime::Result<String> {
123
78
    let name = match name_arg {
124
78
        Some(s) if !s.is_empty() => s,
125
        _ => {
126
4
            return Err(wasmtime::Error::msg(
127
4
                "get-config: missing or empty :name arg",
128
4
            ));
129
        }
130
    };
131
75
    match GetConfig::new().user_id(user_id).name(name).run().await {
132
50
        Ok(Some(CmdResult::String(s))) => Ok(format!("(:config-value {})", quote_string(&s))),
133
        Ok(Some(CmdResult::Data(bytes))) => {
134
            Ok(format!("(:config-value #\"{}\")", BASE64.encode(&bytes)))
135
        }
136
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
137
            "get-config: expected String/Data, got {other:?}"
138
        ))),
139
25
        Ok(None) => Ok("(:config-value nil)".to_string()),
140
        Err(err) => Err(wasmtime::Error::msg(format!("get-config: {err}"))),
141
    }
142
79
}
143

            
144
53
fn quote_string(s: &str) -> String {
145
53
    let mut q = String::with_capacity(s.len() + 2);
146
53
    q.push('"');
147
361
    for ch in s.chars() {
148
361
        match ch {
149
1
            '"' => q.push_str("\\\""),
150
1
            '\\' => q.push_str("\\\\"),
151
359
            other => q.push(other),
152
        }
153
    }
154
53
    q.push('"');
155
53
    q
156
53
}
157

            
158
/// Unwraps a `CmdResult::String(_)` result and surfaces every other shape
159
/// (Data/None/Err) as a `wasmtime::Error`. The rpc envelope layer catches
160
/// the trap and renders the `:error` form; callers compose the natives
161
/// expecting a typed `StringRef`, so anything else is a hard failure at
162
/// the point of use.
163
27
fn command_string(
164
27
    name: &str,
165
27
    result: Result<Option<CmdResult>, CmdError>,
166
27
) -> wasmtime::Result<String> {
167
27
    match result {
168
27
        Ok(Some(CmdResult::String(s))) => Ok(s),
169
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
170
            "{name}: expected String result, got {other:?}"
171
        ))),
172
        Ok(None) => Err(wasmtime::Error::msg(format!(
173
            "{name}: command returned no result"
174
        ))),
175
        Err(err) => Err(wasmtime::Error::msg(format!("{name}: {err}"))),
176
    }
177
27
}
178

            
179
#[cfg(test)]
180
mod tests {
181
    use super::*;
182

            
183
    // Arg-validation runs before any DB/user lookup, so a placeholder user id
184
    // is fine here.
185
    const TEST_USER: uuid::Uuid = uuid::Uuid::nil();
186

            
187
    #[tokio::test]
188
1
    async fn run_get_config_no_arg_returns_error() {
189
1
        let err = run_get_config(TEST_USER, None).await.unwrap_err();
190
1
        assert!(err.to_string().contains("missing or empty"));
191
1
    }
192

            
193
    #[tokio::test]
194
1
    async fn run_get_config_empty_arg_returns_error() {
195
1
        let err = run_get_config(TEST_USER, Some(String::new()))
196
1
            .await
197
1
            .unwrap_err();
198
1
        assert!(err.to_string().contains("missing or empty"));
199
1
    }
200

            
201
    #[tokio::test]
202
1
    async fn run_set_config_no_name_emits_error() {
203
1
        let err = run_set_config(TEST_USER, None, Some("v".into()))
204
1
            .await
205
1
            .unwrap_err();
206
1
        assert!(err.to_string().contains(":name"));
207
1
    }
208

            
209
    #[tokio::test]
210
1
    async fn run_set_config_no_value_emits_error() {
211
1
        let err = run_set_config(TEST_USER, Some("k".into()), None)
212
1
            .await
213
1
            .unwrap_err();
214
1
        assert!(err.to_string().contains(":value"));
215
1
    }
216

            
217
    #[test]
218
1
    fn quote_string_escapes_quotes_and_backslashes() {
219
1
        assert_eq!(quote_string("plain"), "\"plain\"");
220
1
        assert_eq!(quote_string("a\"b"), "\"a\\\"b\"");
221
1
        assert_eq!(quote_string("c\\d"), "\"c\\\\d\"");
222
1
    }
223
}