Skip to main content

submilli_engine/stdlib/
uuid.rs

1//! `submilli:uuid` — v4 / v7 generation + validation.
2//!
3//! Pure Rust host functions registered directly under the package name; the
4//! linker resolves user imports (`submilli:uuid#v4`, …) with no Wasm module.
5
6use crate::runtime::effects;
7use crate::runtime::host::{
8    abi_arg, abi_result, intrinsic_string_type, read_string_arg, register_host_fn,
9    register_host_value_async, write_submilli_string_struct,
10};
11use wasmtime::{FuncType, HeapType, Linker, RefType, Val, ValType};
12
13use crate::runtime::StoreData;
14use crate::{PackageDeclaration, Param, Span, Type, ValueKind, ValueSymbol};
15
16pub const MODULE_NAME: &str = "submilli:uuid";
17
18pub fn package_declaration() -> PackageDeclaration {
19    let mut defs = PackageDeclaration::with_package(MODULE_NAME);
20    insert_fn(
21        &mut defs,
22        "v4",
23        Vec::new(),
24        Type::String,
25        "/**\n * Generate a random UUID v4 (RFC 4122).\n * Returns the canonical lowercase hyphenated form.\n */",
26    );
27    insert_fn(
28        &mut defs,
29        "v7",
30        Vec::new(),
31        Type::String,
32        "/**\n * Generate a time-ordered UUID v7 (RFC 9562).\n * Sortable / index-friendly; prefer over `v4` when the destination is a sorted store.\n * Returns the canonical lowercase hyphenated form.\n */",
33    );
34    insert_fn(
35        &mut defs,
36        "validate",
37        vec![Param::new("string", Type::String)],
38        Type::Boolean,
39        "/**\n * Returns `true` if `string` is a valid UUID (any version), `false` otherwise.\n * @param string The candidate UUID text.\n */",
40    );
41    defs
42}
43
44fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
45    defs.values.insert(
46        name.to_string(),
47        ValueSymbol {
48            name: name.to_string(),
49            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
50            declaration_span: Span::at(crate::FileId::UUID),
51            kind: ValueKind::Function {
52                generics: Vec::new(),
53                params,
54                ret,
55                type_predicate: None,
56                doc: crate::doc(crate::FileId::UUID, doc),
57            },
58        },
59    );
60}
61
62pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
63    let engine = linker.engine().clone();
64    let string = ValType::Ref(RefType::new(
65        false,
66        HeapType::ConcreteStruct(intrinsic_string_type(&engine)?),
67    ));
68
69    let gen_ty = FuncType::new(&engine, [], [string.clone()]);
70    register_host_value_async(
71        linker,
72        MODULE_NAME,
73        crate::mangle::package_symbol(MODULE_NAME, "v4"),
74        gen_ty.clone(),
75        |caller, _params| {
76            Box::pin(async move {
77                let value = effects::execute(caller, "uuid.v4", (), |_, _, ()| {
78                    Box::pin(async { Ok(::uuid::Uuid::new_v4().to_string()) })
79                })
80                .await?;
81                let st = write_submilli_string_struct(caller, &value)?;
82                Ok(Val::AnyRef(Some(st.to_anyref())))
83            })
84        },
85    )?;
86
87    register_host_value_async(
88        linker,
89        MODULE_NAME,
90        crate::mangle::package_symbol(MODULE_NAME, "v7"),
91        gen_ty,
92        |caller, _params| {
93            Box::pin(async move {
94                let value = effects::execute(caller, "uuid.v7", (), |_, _, ()| {
95                    Box::pin(async { Ok(::uuid::Uuid::now_v7().to_string()) })
96                })
97                .await?;
98                let st = write_submilli_string_struct(caller, &value)?;
99                Ok(Val::AnyRef(Some(st.to_anyref())))
100            })
101        },
102    )?;
103
104    let validate_ty = FuncType::new(&engine, [string], [ValType::I32]);
105    register_host_fn(
106        linker,
107        MODULE_NAME,
108        crate::mangle::package_symbol(MODULE_NAME, "validate"),
109        validate_ty,
110        |caller, params, results| {
111            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "uuid.validate")?;
112            *abi_result(results, 0)? = Val::I32(i32::from(::uuid::Uuid::parse_str(&s).is_ok()));
113            Ok(())
114        },
115    )?;
116
117    Ok(())
118}
119
120#[cfg(test)]
121mod tests {
122    use std::sync::Arc;
123
124    use crate::compile_script;
125    use crate::runtime::security::{CheckOutcome, SecurityCheck};
126    use crate::runtime::{
127        RuntimeConfig, StoreData, Vfs, dispatch_main_async, install_runtime_async,
128    };
129
130    struct RecordingCheck {
131        seen: std::sync::Mutex<Vec<(String, String)>>,
132    }
133    impl SecurityCheck for RecordingCheck {
134        fn check(
135            &self,
136            caller: &str,
137            capability: &str,
138            _context: &serde_json::Value,
139        ) -> CheckOutcome {
140            self.seen
141                .lock()
142                .unwrap()
143                .push((caller.to_string(), capability.to_string()));
144            CheckOutcome::Allow { rule: None }
145        }
146    }
147
148    /// A `uuid.*` call followed by an `fs.*` call must record `caller="main"` —
149    /// host-fn packages leave the caller stack untouched, so the fs check sees
150    /// the user package on top.
151    #[tokio::test]
152    async fn uuid_call_leaves_caller_attribution_intact() {
153        let source = r#"
154            import { v4 } from "submilli:uuid";
155            import { writeText } from "submilli:fs";
156            function main(): void {
157                const _id = v4();
158                writeText("/x.txt", "hi");
159            }
160        "#;
161        let recording = Arc::new(RecordingCheck {
162            seen: std::sync::Mutex::new(Vec::new()),
163        });
164        let compiled =
165            compile_script(source, "test.subm", crate::FileId(0), &[], &[]).expect("compile clean");
166        let cfg = RuntimeConfig::default();
167        let engine = cfg.engine().expect("engine");
168        let mut data = StoreData::with_vfs("test-run".into(), Vfs::tempdir().expect("tempdir"));
169        data.security_check = recording.clone();
170        let mut store = cfg.store(&engine, data).expect("store");
171        let module = wasmtime::Module::new(&engine, &compiled.wasm).expect("module");
172        let mut linker = wasmtime::Linker::<StoreData>::new(&engine);
173        install_runtime_async(&mut linker, &mut store)
174            .await
175            .expect("install");
176        let inst = linker
177            .instantiate_async(&mut store, &module)
178            .await
179            .expect("instantiate");
180        dispatch_main_async(&mut store, &inst)
181            .await
182            .expect("main ran");
183
184        let seen = recording.seen.lock().unwrap().clone();
185        assert!(
186            !seen.is_empty(),
187            "the fs.* call should have been recorded by RecordingCheck"
188        );
189        for (caller, capability) in &seen {
190            assert_eq!(
191                caller, "main",
192                "expected caller=main for capability {capability}; got {caller}"
193            );
194        }
195    }
196}