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