Skip to main content

submilli_engine/stdlib/git/
install.rs

1//! Explicit Repository method registrations and Wasm value conversion.
2use super::{GitCall, MODULE_NAME};
3use crate::runtime::StoreData;
4use crate::runtime::fuel;
5use crate::runtime::host::{
6    abi_arg, read_string_arg, register_host_value_async, write_submilli_string_struct,
7    write_submilli_uint8array_struct,
8};
9use crate::runtime::intrinsic_types::{IntrinsicTypes, build_intrinsic_types};
10use crate::stdlib::session::value;
11use serde::de::DeserializeOwned;
12use serde_json::Value;
13use wasmtime::{Caller, FuncType, HeapType, Linker, RefType, Result, Val, ValType, bail};
14
15pub fn install(linker: &mut Linker<StoreData>) -> Result<()> {
16    let engine = linker.engine().clone();
17    let intr = build_intrinsic_types(&engine)?;
18    install_repository_reads(linker, &engine, &intr)?;
19    install_repository_writes(linker, &engine, &intr)
20}
21
22fn install_repository_reads(
23    linker: &mut Linker<StoreData>,
24    engine: &wasmtime::Engine,
25    intr: &IntrinsicTypes,
26) -> Result<()> {
27    let argument = ValType::Ref(RefType::new(
28        true,
29        HeapType::ConcreteStruct(intr.object.clone()),
30    ));
31    let receiver = ValType::Ref(RefType::new(
32        false,
33        HeapType::ConcreteStruct(intr.object.clone()),
34    ));
35    let object = ValType::Ref(RefType::new(
36        false,
37        HeapType::ConcreteStruct(intr.object_shape.clone()),
38    ));
39    let bytes = ValType::Ref(RefType::new(
40        false,
41        HeapType::ConcreteStruct(intr.uint8_array.clone()),
42    ));
43    let array = ValType::Ref(RefType::new(
44        false,
45        HeapType::ConcreteStruct(intr.array.clone()),
46    ));
47    let repository = crate::mangle::package_symbol(MODULE_NAME, "Repository");
48    register_host_value_async(
49        linker,
50        MODULE_NAME,
51        crate::mangle::extend(&repository, "status"),
52        FuncType::new(engine, [receiver.clone()], [object.clone()]),
53        |caller, params| {
54            Box::pin(async move {
55                let call = GitCall::new(caller, "status")?;
56                let arguments = ();
57                let path = repository_path(caller, abi_arg(params, 0)?)?;
58                let result = call.execute(caller, path, arguments, super::status).await?;
59                encode_json(caller, &result, call.max_bytes)
60            })
61        },
62    )?;
63    register_host_value_async(
64        linker,
65        MODULE_NAME,
66        crate::mangle::extend(&repository, "log"),
67        FuncType::new(
68            engine,
69            [receiver.clone(), argument.clone()],
70            [object.clone()],
71        ),
72        |caller, params| {
73            Box::pin(async move {
74                prepare_history_cache(caller)?;
75                let call = GitCall::new(caller, "log")?;
76                let arguments = read_argument(
77                    caller,
78                    abi_arg(params, 1)?,
79                    Some(serde_json::json!({})),
80                    &call,
81                )
82                .await?;
83                let path = repository_path(caller, abi_arg(params, 0)?)?;
84                let result = call.execute(caller, path, arguments, super::log).await?;
85                encode_json(caller, &result, call.max_bytes)
86            })
87        },
88    )?;
89    register_host_value_async(
90        linker,
91        MODULE_NAME,
92        crate::mangle::extend(&repository, "diff"),
93        FuncType::new(
94            engine,
95            [receiver.clone(), argument.clone()],
96            [object.clone()],
97        ),
98        |caller, params| {
99            Box::pin(async move {
100                let call = GitCall::new(caller, "diff")?;
101                let arguments = read_argument(
102                    caller,
103                    abi_arg(params, 1)?,
104                    Some(serde_json::json!({})),
105                    &call,
106                )
107                .await?;
108                let path = repository_path(caller, abi_arg(params, 0)?)?;
109                let result = call.execute(caller, path, arguments, super::diff).await?;
110                encode_json(caller, &result, call.max_bytes)
111            })
112        },
113    )?;
114    register_host_value_async(
115        linker,
116        MODULE_NAME,
117        crate::mangle::extend(&repository, "show"),
118        FuncType::new(
119            engine,
120            [receiver.clone(), argument.clone(), argument.clone()],
121            [bytes.clone()],
122        ),
123        |caller, params| {
124            Box::pin(async move {
125                let call = GitCall::new(caller, "show")?;
126                let arguments = super::ShowArguments {
127                    revision: read_argument(caller, abi_arg(params, 1)?, None, &call).await?,
128                    path: read_argument(caller, abi_arg(params, 2)?, None, &call).await?,
129                };
130                let path = repository_path(caller, abi_arg(params, 0)?)?;
131                let result = call.execute(caller, path, arguments, super::show).await?;
132                Ok(Val::AnyRef(Some(
133                    write_submilli_uint8array_struct(caller, &result.0)?.to_anyref(),
134                )))
135            })
136        },
137    )?;
138    register_host_value_async(
139        linker,
140        MODULE_NAME,
141        crate::mangle::extend(&repository, "branches"),
142        FuncType::new(engine, [receiver.clone()], [array.clone()]),
143        |caller, params| {
144            Box::pin(async move {
145                let call = GitCall::new(caller, "branches")?;
146                let arguments = ();
147                let path = repository_path(caller, abi_arg(params, 0)?)?;
148                let result = call
149                    .execute(caller, path, arguments, super::branches)
150                    .await?;
151                encode_json(caller, &result, call.max_bytes)
152            })
153        },
154    )?;
155    register_host_value_async(
156        linker,
157        MODULE_NAME,
158        crate::mangle::extend(&repository, "remotes"),
159        FuncType::new(engine, [receiver.clone()], [array.clone()]),
160        |caller, params| {
161            Box::pin(async move {
162                let call = GitCall::new(caller, "remotes")?;
163                let arguments = ();
164                let path = repository_path(caller, abi_arg(params, 0)?)?;
165                let result = call
166                    .execute(caller, path, arguments, super::remotes)
167                    .await?;
168                encode_json(caller, &result, call.max_bytes)
169            })
170        },
171    )?;
172    Ok(())
173}
174
175fn install_repository_writes(
176    linker: &mut Linker<StoreData>,
177    engine: &wasmtime::Engine,
178    intr: &IntrinsicTypes,
179) -> Result<()> {
180    let argument = ValType::Ref(RefType::new(
181        true,
182        HeapType::ConcreteStruct(intr.object.clone()),
183    ));
184    let receiver = ValType::Ref(RefType::new(
185        false,
186        HeapType::ConcreteStruct(intr.object.clone()),
187    ));
188    let object = ValType::Ref(RefType::new(
189        false,
190        HeapType::ConcreteStruct(intr.object_shape.clone()),
191    ));
192    // Class method slots carry a value even when their source return type is void.
193    let completion = argument.clone();
194    let repository = crate::mangle::package_symbol(MODULE_NAME, "Repository");
195    register_host_value_async(
196        linker,
197        MODULE_NAME,
198        crate::mangle::extend(&repository, "add"),
199        FuncType::new(
200            engine,
201            [receiver.clone(), argument.clone()],
202            [completion.clone()],
203        ),
204        |caller, params| {
205            Box::pin(async move {
206                invalidate_history_cache(caller)?;
207                let call = GitCall::new(caller, "add")?;
208                let arguments = read_argument(caller, abi_arg(params, 1)?, None, &call).await?;
209                let path = repository_path(caller, abi_arg(params, 0)?)?;
210                call.execute(caller, path, arguments, super::add).await?;
211                crate::runtime::prelude::undefined::value(caller)
212            })
213        },
214    )?;
215    register_host_value_async(
216        linker,
217        MODULE_NAME,
218        crate::mangle::extend(&repository, "commit"),
219        FuncType::new(
220            engine,
221            [receiver.clone(), argument.clone()],
222            [ValType::Ref(RefType::new(
223                false,
224                intr.string.clone().into(),
225            ))],
226        ),
227        |caller, params| {
228            Box::pin(async move {
229                invalidate_history_cache(caller)?;
230                let call = GitCall::new(caller, "commit")?;
231                let arguments = read_argument(caller, abi_arg(params, 1)?, None, &call).await?;
232                let path = repository_path(caller, abi_arg(params, 0)?)?;
233                let result = call.execute(caller, path, arguments, super::commit).await?;
234                Ok(Val::AnyRef(Some(
235                    write_submilli_string_struct(caller, &result)?.to_anyref(),
236                )))
237            })
238        },
239    )?;
240    register_host_value_async(
241        linker,
242        MODULE_NAME,
243        crate::mangle::extend(&repository, "createBranch"),
244        FuncType::new(
245            engine,
246            [receiver.clone(), argument.clone(), argument.clone()],
247            [completion.clone()],
248        ),
249        |caller, params| {
250            Box::pin(async move {
251                invalidate_history_cache(caller)?;
252                let call = GitCall::new(caller, "createBranch")?;
253                let arguments = super::BranchArguments {
254                    name: read_argument(caller, abi_arg(params, 1)?, None, &call).await?,
255                    start: read_argument(
256                        caller,
257                        abi_arg(params, 2)?,
258                        Some(String::from("HEAD")),
259                        &call,
260                    )
261                    .await?,
262                };
263                let path = repository_path(caller, abi_arg(params, 0)?)?;
264                call.execute(caller, path, arguments, super::create_branch)
265                    .await?;
266                crate::runtime::prelude::undefined::value(caller)
267            })
268        },
269    )?;
270    register_host_value_async(
271        linker,
272        MODULE_NAME,
273        crate::mangle::extend(&repository, "switchBranch"),
274        FuncType::new(
275            engine,
276            [receiver.clone(), argument.clone()],
277            [completion.clone()],
278        ),
279        |caller, params| {
280            Box::pin(async move {
281                invalidate_history_cache(caller)?;
282                let call = GitCall::new(caller, "switchBranch")?;
283                let arguments = read_argument(caller, abi_arg(params, 1)?, None, &call).await?;
284                let path = repository_path(caller, abi_arg(params, 0)?)?;
285                call.execute(caller, path, arguments, super::switch_branch)
286                    .await?;
287                crate::runtime::prelude::undefined::value(caller)
288            })
289        },
290    )?;
291    register_host_value_async(
292        linker,
293        MODULE_NAME,
294        crate::mangle::extend(&repository, "addRemote"),
295        FuncType::new(
296            engine,
297            [receiver.clone(), argument.clone(), argument.clone()],
298            [completion.clone()],
299        ),
300        |caller, params| {
301            Box::pin(async move {
302                let call = GitCall::new(caller, "addRemote")?;
303                let arguments = super::RemoteArguments {
304                    name: read_argument(caller, abi_arg(params, 1)?, None, &call).await?,
305                    url: read_argument(caller, abi_arg(params, 2)?, None, &call).await?,
306                };
307                let path = repository_path(caller, abi_arg(params, 0)?)?;
308                call.execute(caller, path, arguments, super::add_remote)
309                    .await?;
310                crate::runtime::prelude::undefined::value(caller)
311            })
312        },
313    )?;
314    register_host_value_async(
315        linker,
316        MODULE_NAME,
317        crate::mangle::extend(&repository, "setRemoteUrl"),
318        FuncType::new(
319            engine,
320            [receiver.clone(), argument.clone(), argument.clone()],
321            [completion.clone()],
322        ),
323        |caller, params| {
324            Box::pin(async move {
325                let call = GitCall::new(caller, "setRemoteUrl")?;
326                let arguments = super::RemoteArguments {
327                    name: read_argument(caller, abi_arg(params, 1)?, None, &call).await?,
328                    url: read_argument(caller, abi_arg(params, 2)?, None, &call).await?,
329                };
330                let path = repository_path(caller, abi_arg(params, 0)?)?;
331                call.execute(caller, path, arguments, super::set_remote_url)
332                    .await?;
333                crate::runtime::prelude::undefined::value(caller)
334            })
335        },
336    )?;
337    register_host_value_async(
338        linker,
339        MODULE_NAME,
340        crate::mangle::extend(&repository, "fetch"),
341        FuncType::new(
342            engine,
343            [receiver.clone(), argument.clone(), argument.clone()],
344            [object.clone()],
345        ),
346        |caller, params| {
347            Box::pin(async move {
348                invalidate_history_cache(caller)?;
349                let call = GitCall::new(caller, "fetch")?;
350                let arguments = super::FetchArguments {
351                    remote: read_argument(
352                        caller,
353                        abi_arg(params, 1)?,
354                        Some(String::from("origin")),
355                        &call,
356                    )
357                    .await?,
358                    branch: read_argument(caller, abi_arg(params, 2)?, Some(String::new()), &call)
359                        .await?,
360                };
361                let path = repository_path(caller, abi_arg(params, 0)?)?;
362                let result = call.execute(caller, path, arguments, super::fetch).await?;
363                encode_json(caller, &result, call.max_bytes)
364            })
365        },
366    )?;
367    register_host_value_async(
368        linker,
369        MODULE_NAME,
370        crate::mangle::extend(&repository, "pull"),
371        FuncType::new(
372            engine,
373            [receiver.clone(), argument.clone(), argument.clone()],
374            [object.clone()],
375        ),
376        |caller, params| {
377            Box::pin(async move {
378                invalidate_history_cache(caller)?;
379                let call = GitCall::new(caller, "pull")?;
380                let arguments = super::FetchArguments {
381                    remote: read_argument(
382                        caller,
383                        abi_arg(params, 1)?,
384                        Some(String::from("origin")),
385                        &call,
386                    )
387                    .await?,
388                    branch: read_argument(caller, abi_arg(params, 2)?, Some(String::new()), &call)
389                        .await?,
390                };
391                let path = repository_path(caller, abi_arg(params, 0)?)?;
392                let result = call.execute(caller, path, arguments, super::pull).await?;
393                encode_json(caller, &result, call.max_bytes)
394            })
395        },
396    )?;
397    Ok(())
398}
399
400pub(super) async fn open_repository(
401    caller: &mut Caller<'_, StoreData>,
402    params: &[Val],
403) -> Result<Val> {
404    let call = GitCall::new(caller, "open")?;
405    let path: String = read_argument(caller, abi_arg(params, 0)?, None, &call).await?;
406    let path = normalize_repository_path(caller, &path)?;
407    call.execute(caller, path.clone(), (), super::open).await?;
408    Ok(Val::AnyRef(Some(
409        write_submilli_string_struct(caller, &path)?.to_anyref(),
410    )))
411}
412
413pub(super) async fn init_repository(
414    caller: &mut Caller<'_, StoreData>,
415    params: &[Val],
416) -> Result<Val> {
417    invalidate_history_cache(caller)?;
418    let call = GitCall::new(caller, "init")?;
419    let path: String = read_argument(caller, abi_arg(params, 0)?, None, &call).await?;
420    let options = read_argument(
421        caller,
422        abi_arg(params, 1)?,
423        Some(serde_json::json!({})),
424        &call,
425    )
426    .await?;
427    let path = normalize_repository_path(caller, &path)?;
428    call.execute(caller, path.clone(), options, super::init)
429        .await?;
430    Ok(Val::AnyRef(Some(
431        write_submilli_string_struct(caller, &path)?.to_anyref(),
432    )))
433}
434
435pub(super) async fn clone_repository(
436    caller: &mut Caller<'_, StoreData>,
437    params: &[Val],
438) -> Result<Val> {
439    invalidate_history_cache(caller)?;
440    let call = GitCall::new(caller, "clone")?;
441    let url = read_argument(caller, abi_arg(params, 0)?, None, &call).await?;
442    let path: String = read_argument(caller, abi_arg(params, 1)?, None, &call).await?;
443    let options = read_argument(
444        caller,
445        abi_arg(params, 2)?,
446        Some(serde_json::json!({})),
447        &call,
448    )
449    .await?;
450    let path = normalize_repository_path(caller, &path)?;
451    call.execute(
452        caller,
453        path.clone(),
454        super::CloneArguments { url, options },
455        super::clone_repository,
456    )
457    .await?;
458    Ok(Val::AnyRef(Some(
459        write_submilli_string_struct(caller, &path)?.to_anyref(),
460    )))
461}
462
463async fn read_argument<T: DeserializeOwned>(
464    caller: &mut Caller<'_, StoreData>,
465    argument: &Val,
466    default: Option<T>,
467    call: &GitCall,
468) -> Result<T> {
469    if crate::runtime::prelude::undefined::is_undefined(caller, argument)? {
470        return default.ok_or_else(|| {
471            wasmtime::Error::msg(format!(
472                "git: undefined is not accepted for this argument to {}",
473                call.operation
474            ))
475        });
476    }
477    // Serializing a guest value may run callbacks. Keep that work outside the effect
478    // and within the same deadline as the worker it will start.
479    let units = super::before_deadline(call.deadline, value::serialize(caller, argument)).await??;
480    if units.len() as u64 * 2 > call.max_bytes {
481        bail!("git: argument size limit exceeded");
482    }
483    fuel::charge(&mut *caller, fuel::PARSE, units.len() as u64)?;
484    let text = String::from_utf16(&units)
485        .map_err(|_| wasmtime::Error::msg("git: arguments must be valid Unicode"))?;
486    Ok(serde_json::from_str(&text)?)
487}
488
489fn repository_path(caller: &mut Caller<'_, StoreData>, receiver: &Val) -> Result<String> {
490    let field = super::class::path(caller, receiver)?;
491    let path = read_string_arg(caller, &field, "git repository")?;
492    normalize_repository_path(caller, &path)
493}
494
495fn normalize_repository_path(caller: &Caller<'_, StoreData>, path: &str) -> Result<String> {
496    let path = crate::runtime::fs::guest_normalize(caller.data().vfs.cwd(), path)?;
497    if path
498        .split('/')
499        .any(|part| part.eq_ignore_ascii_case(".git") || super::stage::is_reserved_stage_name(part))
500    {
501        bail!("git: repository path targets protected metadata");
502    }
503    Ok(path)
504}
505
506fn prepare_history_cache(caller: &mut Caller<'_, StoreData>) -> Result<()> {
507    if caller.data().git.is_none() {
508        bail!("submilli:git is disabled; configure the blueprint git block");
509    }
510    if caller.data().git_history.is_none() {
511        let cache = super::log_cache::Cache::new(&caller.data().tenant_limits)?;
512        caller.data_mut().git_history = Some(std::sync::Arc::new(cache));
513    }
514    Ok(())
515}
516
517fn invalidate_history_cache(caller: &Caller<'_, StoreData>) -> Result<()> {
518    if let Some(cache) = &caller.data().git_history {
519        cache.invalidate()?;
520    }
521    Ok(())
522}
523
524fn encode_json(caller: &mut Caller<'_, StoreData>, result: &Value, max_bytes: u64) -> Result<Val> {
525    let text = serde_json::to_string(result)?;
526    if text.len() as u64 > max_bytes {
527        bail!("git: result limit exceeded");
528    }
529    fuel::charge(&mut *caller, fuel::PARSE, 2 * text.len() as u64)?;
530    value::deserialize(caller, &text.encode_utf16().collect::<Vec<_>>())
531}