1use base64::Engine;
12use base64::engine::general_purpose::STANDARD as BASE64;
13use scripting::runtime::{alloc_string_ref, read_string_arg};
14use server::command::{
15 CmdError, CmdResult,
16 config::{GetBuildDate, GetConfig, GetVersion, SetConfig},
17};
18use wasmtime::{ArrayRef, Caller, Linker, Rooted};
19
20use crate::session::SessionData;
21
22pub const REGISTERED_COMMANDS: &[&str] = &[
23 "get-config",
24 "get-version",
25 "get-build-date",
26 "set-config",
27 "select-column",
28];
29
30pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
31 linker.func_wrap_async(
32 "nomi",
33 "config_get_version",
34 |mut caller: Caller<'_, SessionData>,
35 ()|
36 -> Box<
37 dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
38 > {
39 Box::new(async move {
40 let bytes = command_string("get-version", GetVersion::new().run().await)?;
41 Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
42 })
43 },
44 )?;
45 linker.func_wrap_async(
46 "nomi",
47 "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 > {
53 Box::new(async move {
54 let bytes = command_string("get-build-date", GetBuildDate::new().run().await)?;
55 Ok(Some(alloc_string_ref(&mut caller, bytes.as_bytes())?))
56 })
57 },
58 )?;
59 linker.func_wrap_async(
60 "nomi",
61 "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 > {
67 Box::new(async move {
68 let user_id = caller.data().ctx().user_id;
69 let name = read_string_arg(&mut caller, name_arg)?;
70 let formatted = run_get_config(user_id, name).await;
71 Ok(Some(alloc_string_ref(&mut caller, formatted.as_bytes())?))
72 })
73 },
74 )?;
75 linker.func_wrap_async(
76 "nomi",
77 "config_set_config",
78 |mut caller: Caller<'_, SessionData>,
79 (name_arg, value_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
80 -> Box<dyn std::future::Future<Output = wasmtime::Result<i32>> + Send> {
81 Box::new(async move {
82 let user_id = caller.data().ctx().user_id;
83 let name = read_string_arg(&mut caller, name_arg)?;
84 let value = read_string_arg(&mut caller, value_arg)?;
85 run_set_config(user_id, name, value).await
86 })
87 },
88 )?;
89 Ok(())
90}
91
92async fn run_set_config(
100 user_id: uuid::Uuid,
101 name_arg: Option<String>,
102 value_arg: Option<String>,
103) -> wasmtime::Result<i32> {
104 let name = match name_arg {
105 Some(s) if !s.is_empty() => s,
106 _ => {
107 return Err(wasmtime::Error::msg(
108 "set-config: missing or empty :name arg",
109 ));
110 }
111 };
112 let value = match value_arg {
113 Some(s) => s,
114 _ => return Err(wasmtime::Error::msg("set-config: missing :value arg")),
115 };
116 SetConfig::new()
117 .user_id(user_id)
118 .name(name)
119 .value(value)
120 .run()
121 .await
122 .map(|_| 1)
123 .map_err(|err| wasmtime::Error::msg(format!("set-config: {err}")))
124}
125
126async fn run_get_config(user_id: uuid::Uuid, name_arg: Option<String>) -> String {
127 let name = match name_arg {
128 Some(s) if !s.is_empty() => s,
129 _ => return "(:error \"get-config: missing or empty :name arg\")".into(),
130 };
131 match GetConfig::new().user_id(user_id).name(name).run().await {
132 Ok(Some(CmdResult::String(s))) => format!("(:config-value {})", quote_string(&s)),
133 Ok(Some(CmdResult::Data(bytes))) => {
134 format!("(:config-value #\"{}\")", BASE64.encode(&bytes))
135 }
136 Ok(Some(other)) => {
137 format!("(:error \"get-config: expected String/Data, got {other:?}\")")
138 }
139 Ok(None) => "(:config-value nil)".to_string(),
140 Err(err) => format!("(:error \"get-config: {err}\")"),
141 }
142}
143
144fn quote_string(s: &str) -> String {
145 let mut q = String::with_capacity(s.len() + 2);
146 q.push('"');
147 for ch in s.chars() {
148 match ch {
149 '"' => q.push_str("\\\""),
150 '\\' => q.push_str("\\\\"),
151 other => q.push(other),
152 }
153 }
154 q.push('"');
155 q
156}
157
158fn command_string(
164 name: &str,
165 result: Result<Option<CmdResult>, CmdError>,
166) -> wasmtime::Result<String> {
167 match result {
168 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}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 const TEST_USER: uuid::Uuid = uuid::Uuid::nil();
186
187 #[tokio::test]
188 async fn run_get_config_no_arg_emits_error() {
189 let out = run_get_config(TEST_USER, None).await;
190 assert!(out.contains("(:error"));
191 assert!(out.contains("missing or empty"));
192 }
193
194 #[tokio::test]
195 async fn run_get_config_empty_arg_emits_error() {
196 let out = run_get_config(TEST_USER, Some(String::new())).await;
197 assert!(out.contains("(:error"));
198 assert!(out.contains("missing or empty"));
199 }
200
201 #[tokio::test]
202 async fn run_set_config_no_name_emits_error() {
203 let err = run_set_config(TEST_USER, None, Some("v".into()))
204 .await
205 .unwrap_err();
206 assert!(err.to_string().contains(":name"));
207 }
208
209 #[tokio::test]
210 async fn run_set_config_no_value_emits_error() {
211 let err = run_set_config(TEST_USER, Some("k".into()), None)
212 .await
213 .unwrap_err();
214 assert!(err.to_string().contains(":value"));
215 }
216
217 #[test]
218 fn quote_string_escapes_quotes_and_backslashes() {
219 assert_eq!(quote_string("plain"), "\"plain\"");
220 assert_eq!(quote_string("a\"b"), "\"a\\\"b\"");
221 assert_eq!(quote_string("c\\d"), "\"c\\\\d\"");
222 }
223}