1use std::collections::{HashMap, HashSet};
4use std::rc::Rc;
5
6pub use crate::core::{Promise, PromiseState, Value};
7use crate::kernel::{parse, Form};
8
9const MANIFEST_FIELDS: &[&str] = &[
10 "root",
11 "namespace",
12 "identity",
13 "version",
14 "provider",
15 "module",
16 "abi",
17 "exports",
18 "capabilities",
19 "host-calls",
20 "callbacks",
21 "handles",
22 "targets",
23 "assets",
24];
25const EXPORT_FIELDS: &[&str] = &["args", "returns", "async", "wasm/export", "operation"];
26const HANDLE_FIELDS: &[&str] = &["tag", "release"];
27const TARGET_FIELDS: &[&str] = &["provider", "runtime"];
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum WasmAbi {
31 CoreV1,
32 HtaV1,
33 MemoryV1,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ExtensionExport {
38 pub arguments: Vec<String>,
39 pub returns: String,
40 pub asynchronous: bool,
41 pub raw_export: Option<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ExtensionCallback {
46 pub arguments: Vec<String>,
47 pub returns: String,
48}
49
50impl ExtensionExport {
51 pub fn raw_name<'a>(&'a self, public_name: &'a str) -> &'a str {
52 self.raw_export.as_deref().unwrap_or(public_name)
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ExtensionTarget {
58 pub provider: String,
59 pub runtime: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ExtensionManifest {
64 pub namespace: String,
65 pub root: Option<String>,
66 pub identity: Option<String>,
67 pub version: String,
68 pub provider: String,
69 pub module: Option<String>,
70 pub abi: WasmAbi,
71 pub targets: HashMap<String, ExtensionTarget>,
72 pub assets: Vec<String>,
73 pub exports: Vec<(String, ExtensionExport)>,
74 pub operations: HashMap<String, String>,
75 pub capabilities: Vec<String>,
76 pub host_calls: HashMap<String, Vec<String>>,
77 pub host_call_capabilities: HashMap<String, Vec<String>>,
78 pub callbacks: HashMap<String, ExtensionCallback>,
79 pub handle_tags: HashMap<String, String>,
80 pub handle_releases: HashMap<String, String>,
81}
82
83impl ExtensionManifest {
84 pub fn parse(source: &str, origin: &str) -> Result<Self, String> {
85 let form = parse(source)
86 .map_err(|error| malformed(origin, format!("cannot parse manifest: {error}")))?;
87 let entries = map(&form, origin, "manifest")?;
88 reject_unknown(entries, MANIFEST_FIELDS, origin, "manifest")?;
89 let namespace = named_string(entries, "namespace", origin)?;
90 if !valid_namespace(&namespace) {
91 return Err(malformed(
92 origin,
93 "namespace must be a qualified lower-case symbol",
94 ));
95 }
96 let root = optional(entries, "root")
97 .map(|form| string(form, origin, "root").map(str::to_owned))
98 .transpose()?;
99 if let Some(root) = root.as_deref() {
100 safe_relative(root, None, origin, "root")?;
101 }
102 let identity = optional(entries, "identity")
103 .map(|form| string(form, origin, "identity").map(str::to_owned))
104 .transpose()?;
105 if identity
106 .as_deref()
107 .is_some_and(|value| !valid_identity(value))
108 {
109 return Err(malformed(
110 origin,
111 "identity must be an owner/name coordinate",
112 ));
113 }
114 let version = named_string(entries, "version", origin)?;
115 let provider = named_keyword(entries, "provider", origin)?;
116 let module = optional(entries, "module")
117 .map(|form| string(form, origin, "module").map(str::to_owned))
118 .transpose()?;
119 let targets = optional(entries, "targets")
120 .map_or_else(|| Ok(HashMap::new()), |form| parse_targets(form, origin))?;
121 let assets = optional(entries, "assets")
122 .map_or_else(|| Ok(Vec::new()), |form| parse_assets(form, origin))?;
123 match provider.as_str() {
124 "wasm" if module.is_some() && targets.is_empty() => {
125 safe_relative(module.as_deref().unwrap(), Some(".wasm"), origin, "module")?;
126 }
127 "hta" if module.is_none() && !targets.is_empty() => {}
128 "wasm" => {
129 return Err(malformed(
130 origin,
131 "WASM providers require :module and cannot declare :targets",
132 ))
133 }
134 "hta" => {
135 return Err(malformed(
136 origin,
137 "HTA providers require :targets and cannot declare :module",
138 ))
139 }
140 _ => {
141 return Err(malformed(
142 origin,
143 format!("unsupported provider :{provider}"),
144 ))
145 }
146 }
147 let abi = match named_keyword(entries, "abi", origin)?.as_str() {
148 "core.v1" => WasmAbi::CoreV1,
149 "hta.v1" => WasmAbi::HtaV1,
150 "memory.v1" => WasmAbi::MemoryV1,
151 value => return Err(malformed(origin, format!("unsupported WASM ABI :{value}"))),
152 };
153 if provider == "hta" && abi != WasmAbi::HtaV1 {
154 return Err(malformed(origin, "HTA providers require :abi :hta.v1"));
155 }
156 let (exports, operations) = parse_exports(required(entries, "exports", origin)?, origin)?;
157 let capabilities = keyword_vector(
158 required(entries, "capabilities", origin)?,
159 origin,
160 "capabilities",
161 )?;
162 let (host_calls, host_call_capabilities) = optional(entries, "host-calls").map_or_else(
163 || Ok((HashMap::new(), HashMap::new())),
164 |form| parse_host_calls(form, origin),
165 )?;
166 let callbacks = optional(entries, "callbacks")
167 .map_or_else(|| Ok(HashMap::new()), |form| parse_callbacks(form, origin))?;
168 let (handle_tags, handle_releases) = optional(entries, "handles").map_or_else(
169 || Ok((HashMap::new(), HashMap::new())),
170 |form| parse_handles(form, origin),
171 )?;
172 Ok(Self {
173 namespace,
174 root,
175 identity,
176 version,
177 provider,
178 module,
179 targets,
180 assets,
181 abi,
182 exports,
183 operations,
184 capabilities,
185 host_calls,
186 host_call_capabilities,
187 callbacks,
188 handle_tags,
189 handle_releases,
190 })
191 }
192
193 pub fn permits_host_call(&self, service: &str, method: &str) -> bool {
194 self.host_calls.get(service).map_or(false, |methods| {
195 methods.iter().any(|candidate| candidate == method)
196 })
197 }
198
199 pub fn host_call_capabilities(&self, service: &str, method: &str) -> &[String] {
200 self.host_call_capabilities
201 .get(&format!("{service}/{method}"))
202 .map(Vec::as_slice)
203 .unwrap_or(&[])
204 }
205}
206
207pub trait WasmExtensionProvider {
208 fn supports(&self, abi: WasmAbi) -> bool;
209
210 fn capabilities(&self) -> Vec<String> {
211 Vec::new()
212 }
213
214 fn start(&self, manifest: &ExtensionManifest) -> Result<(), String>;
215
216 fn invoke(
217 &self,
218 manifest: &ExtensionManifest,
219 export: &str,
220 arguments: &[Value],
221 ) -> Result<Value, String>;
222
223 fn cancel(&self, manifest: &ExtensionManifest, request: u64) -> Result<(), String>;
224
225 fn release(&self, _manifest: &ExtensionManifest, _handle: &Value) -> Result<(), String> {
226 Err("extension/release-unsupported: provider has no HTA handle boundary".into())
227 }
228
229 fn shutdown(&self, manifest: &ExtensionManifest);
230}
231
232struct ExtensionSession {
233 manifest: ExtensionManifest,
234 provider: Rc<dyn WasmExtensionProvider>,
235}
236
237impl Drop for ExtensionSession {
238 fn drop(&mut self) {
239 self.provider.shutdown(&self.manifest);
240 }
241}
242
243#[derive(Clone)]
244pub struct ExtensionBinding {
245 pub namespace: String,
246 pub name: String,
247 pub specification: ExtensionExport,
248 session: Rc<ExtensionSession>,
249}
250
251impl ExtensionBinding {
252 pub fn invoke(&self, arguments: &[Value]) -> Result<Value, String> {
253 if arguments.len() != self.specification.arguments.len() {
254 return Err(format!(
255 "extension/arity: {}/{} expects {} arguments, got {}",
256 self.namespace,
257 self.name,
258 self.specification.arguments.len(),
259 arguments.len()
260 ));
261 }
262 let result = self
263 .session
264 .provider
265 .invoke(&self.session.manifest, &self.name, arguments)?;
266 if self.specification.asynchronous && !matches!(result, Value::Promise(_)) {
267 return Err(format!(
268 "extension/protocol: asynchronous export {}/{} must return a promise",
269 self.namespace, self.name
270 ));
271 }
272 Ok(result)
273 }
274
275 pub fn release(&self, handle: &Value) -> Result<(), String> {
276 self.session
277 .provider
278 .release(&self.session.manifest, handle)
279 .map_err(|error| format!("extension/release: {error}"))
280 }
281}
282
283pub struct WasmExtension {
284 manifest: ExtensionManifest,
285 provider: Rc<dyn WasmExtensionProvider>,
286 session: Option<Rc<ExtensionSession>>,
287}
288
289impl WasmExtension {
290 pub fn new<P: WasmExtensionProvider + 'static>(
291 manifest: ExtensionManifest,
292 provider: P,
293 ) -> Result<Self, String> {
294 if !provider.supports(manifest.abi) {
295 return Err(format!(
296 "extension/unsupported: provider does not support {:?}",
297 manifest.abi
298 ));
299 }
300 Ok(Self {
301 manifest,
302 provider: Rc::new(provider),
303 session: None,
304 })
305 }
306
307 pub fn namespace(&self) -> &str {
308 &self.manifest.namespace
309 }
310
311 pub fn require(&mut self) -> Result<Vec<ExtensionBinding>, String> {
312 if self.session.is_none() {
313 let available = self
314 .provider
315 .capabilities()
316 .into_iter()
317 .collect::<HashSet<_>>();
318 let missing = self
319 .manifest
320 .capabilities
321 .iter()
322 .chain(self.manifest.host_call_capabilities.values().flatten())
323 .find(|capability| !available.contains(*capability));
324 if let Some(capability) = missing {
325 return Err(format!(
326 "extension/denied: {} requires capability :{}",
327 self.manifest.namespace, capability
328 ));
329 }
330 self.provider
331 .start(&self.manifest)
332 .map_err(|error| format!("extension/start: {error}"))?;
333 self.session = Some(Rc::new(ExtensionSession {
334 manifest: self.manifest.clone(),
335 provider: self.provider.clone(),
336 }));
337 }
338 let session = self.session.as_ref().expect("session started").clone();
339 Ok(self
340 .manifest
341 .exports
342 .iter()
343 .map(|(name, specification)| ExtensionBinding {
344 namespace: self.manifest.namespace.clone(),
345 name: name.clone(),
346 specification: specification.clone(),
347 session: session.clone(),
348 })
349 .collect())
350 }
351
352 pub fn cancel(&self, request: u64) -> Result<(), String> {
353 let session = self.session.as_ref().ok_or_else(|| {
354 format!(
355 "extension/not-started: namespace has not been required: {}",
356 self.manifest.namespace
357 )
358 })?;
359 session
360 .provider
361 .cancel(&session.manifest, request)
362 .map_err(|error| format!("extension/cancel: {error}"))
363 }
364}
365
366fn parse_targets(form: &Form, origin: &str) -> Result<HashMap<String, ExtensionTarget>, String> {
367 map(form, origin, "targets")?
368 .iter()
369 .map(|(host, specification)| {
370 let host = keyword(host, origin, "target host")?.to_owned();
371 if host != "node" && host != "browser" {
372 return Err(malformed(origin, format!("unsupported target :{host}")));
373 }
374 let entries = map(specification, origin, "target")?;
375 reject_unknown(entries, TARGET_FIELDS, origin, &format!("target {host}"))?;
376 let provider = named_string(entries, "provider", origin)?;
377 safe_relative(&provider, Some(".mjs"), origin, "target provider")?;
378 let runtime = named_keyword(entries, "runtime", origin)?;
379 let compatible = (host == "node" && runtime == "process")
380 || (host == "browser" && runtime == "web-worker");
381 if !compatible {
382 return Err(malformed(
383 origin,
384 format!("target {host} has incompatible runtime :{runtime}"),
385 ));
386 }
387 Ok((host, ExtensionTarget { provider, runtime }))
388 })
389 .collect()
390}
391
392fn parse_assets(form: &Form, origin: &str) -> Result<Vec<String>, String> {
393 let mut seen = HashSet::new();
394 vector(form, origin, "assets")?
395 .iter()
396 .map(|value| {
397 let value = string(value, origin, "asset")?.to_owned();
398 safe_relative(&value, None, origin, "asset")?;
399 if !seen.insert(value.clone()) {
400 return Err(malformed(origin, format!("duplicate asset {value}")));
401 }
402 Ok(value)
403 })
404 .collect()
405}
406
407fn safe_relative(
408 value: &str,
409 suffix: Option<&str>,
410 origin: &str,
411 field: &str,
412) -> Result<(), String> {
413 let unsafe_path = value.is_empty()
414 || value.starts_with('/')
415 || value.contains('\\')
416 || value.bytes().any(|byte| byte == 0)
417 || value.contains(':')
418 || value
419 .split('/')
420 .any(|part| part.is_empty() || part == "." || part == "..");
421 if unsafe_path || suffix.is_some_and(|suffix| !value.ends_with(suffix)) {
422 return Err(malformed(
423 origin,
424 format!("{field} must be a safe relative package file"),
425 ));
426 }
427 Ok(())
428}
429
430fn parse_exports(
431 form: &Form,
432 origin: &str,
433) -> Result<(Vec<(String, ExtensionExport)>, HashMap<String, String>), String> {
434 let entries = map(form, origin, "exports")?;
435 if entries.is_empty() {
436 return Err(malformed(origin, "exports cannot be empty"));
437 }
438 let mut operations = HashMap::new();
439 let exports = entries
440 .iter()
441 .map(|(name, specification)| {
442 let name = string(name, origin, "export name")?.to_owned();
443 let specification = map(specification, origin, "export specification")?;
444 reject_unknown(
445 specification,
446 EXPORT_FIELDS,
447 origin,
448 &format!("export {name}"),
449 )?;
450 let arguments = wire_vector(
451 required(specification, "args", origin)?,
452 origin,
453 "export args",
454 )?;
455 let returns = wire_type(
456 required(specification, "returns", origin)?,
457 origin,
458 "export returns",
459 )?;
460 let asynchronous = match optional(specification, "async") {
461 None => false,
462 Some(Form::Bool(value)) => *value,
463 Some(_) => return Err(malformed(origin, "export async must be boolean")),
464 };
465 let raw_export = optional(specification, "wasm/export")
466 .map(|form| string(form, origin, "export wasm/export").map(str::to_owned))
467 .transpose()?;
468 if let Some(operation) = optional(specification, "operation") {
469 let operation = string(operation, origin, "export operation")?.to_owned();
470 if operations.insert(name.clone(), operation).is_some() {
471 return Err(malformed(origin, format!("duplicate export {name}")));
472 }
473 }
474 Ok((
475 name,
476 ExtensionExport {
477 arguments,
478 returns,
479 asynchronous,
480 raw_export,
481 },
482 ))
483 })
484 .collect::<Result<Vec<_>, _>>()?;
485 Ok((exports, operations))
486}
487
488fn parse_host_calls(
489 form: &Form,
490 origin: &str,
491) -> Result<(HashMap<String, Vec<String>>, HashMap<String, Vec<String>>), String> {
492 let mut host_calls = HashMap::new();
493 let mut host_call_capabilities = HashMap::new();
494 for (service, specification) in map(form, origin, "host-calls")? {
495 let service = string(service, origin, "host-call service")?.to_owned();
496 let (methods, capabilities) = match specification {
497 Form::Vector(_) => (
498 vector(specification, origin, "host-call methods")?
499 .iter()
500 .map(|method| string(method, origin, "host-call method").map(str::to_owned))
501 .collect::<Result<Vec<_>, _>>()?,
502 Vec::new(),
503 ),
504 Form::Map(entries) => {
505 reject_unknown(entries, &["methods", "capabilities"], origin, "host-call")?;
506 let methods = vector(
507 required(entries, "methods", origin)?,
508 origin,
509 "host-call methods",
510 )?
511 .iter()
512 .map(|method| string(method, origin, "host-call method").map(str::to_owned))
513 .collect::<Result<Vec<_>, _>>()?;
514 let capabilities = optional(entries, "capabilities")
515 .map(|form| keyword_vector(form, origin, "host-call capabilities"))
516 .transpose()?
517 .unwrap_or_default();
518 (methods, capabilities)
519 }
520 _ => {
521 return Err(malformed(
522 origin,
523 "host-call specification must be a vector or map",
524 ))
525 }
526 };
527 if host_calls
528 .insert(service.clone(), methods.clone())
529 .is_some()
530 {
531 return Err(malformed(
532 origin,
533 format!("duplicate host-call service {service}"),
534 ));
535 }
536 for method in methods {
537 host_call_capabilities.insert(format!("{service}/{method}"), capabilities.clone());
538 }
539 }
540 Ok((host_calls, host_call_capabilities))
541}
542
543fn parse_handles(
544 form: &Form,
545 origin: &str,
546) -> Result<(HashMap<String, String>, HashMap<String, String>), String> {
547 let mut tags = HashMap::new();
548 let mut releases = HashMap::new();
549 for (type_name, specification) in map(form, origin, "handles")? {
550 let type_name = string(type_name, origin, "handle type")?.to_owned();
551 let specification = map(specification, origin, "handle specification")?;
552 reject_unknown(
553 specification,
554 HANDLE_FIELDS,
555 origin,
556 &format!("handle {type_name}"),
557 )?;
558 let tag = match required(specification, "tag", origin)? {
559 Form::Symbol(tag) if valid_tag(tag) => tag.clone(),
560 _ => return Err(malformed(origin, "handle tag must be a lower-case symbol")),
561 };
562 tags.insert(type_name.clone(), tag);
563 if let Some(release) = optional(specification, "release") {
564 releases.insert(
565 type_name,
566 string(release, origin, "handle release")?.to_owned(),
567 );
568 }
569 }
570 Ok((tags, releases))
571}
572
573fn parse_callbacks(
574 form: &Form,
575 origin: &str,
576) -> Result<HashMap<String, ExtensionCallback>, String> {
577 let mut callbacks = HashMap::new();
578 for (name, specification) in map(form, origin, "callbacks")? {
579 let name = string(name, origin, "callback name")?.to_owned();
580 let entries = map(specification, origin, "callback specification")?;
581 reject_unknown(entries, &["args", "returns", "reentrant"], origin, "callback")?;
582 let arguments = wire_vector(required(entries, "args", origin)?, origin, "callback args")?;
583 let returns = wire_type(
584 required(entries, "returns", origin)?,
585 origin,
586 "callback returns",
587 )?;
588 if matches!(optional(entries, "reentrant"), Some(Form::Bool(true))) {
589 return Err(malformed(
590 origin,
591 format!("callback {name} cannot be reentrant"),
592 ));
593 }
594 if optional(entries, "reentrant").is_some_and(|form| !matches!(form, Form::Bool(_))) {
595 return Err(malformed(origin, "callback reentrant must be boolean"));
596 }
597 if callbacks
598 .insert(name.clone(), ExtensionCallback { arguments, returns })
599 .is_some()
600 {
601 return Err(malformed(origin, format!("duplicate callback {name}")));
602 }
603 }
604 Ok(callbacks)
605}
606
607fn valid_identity(value: &str) -> bool {
608 value
609 .split_once("/")
610 .is_some_and(|(owner, name)| valid_component(owner) && valid_tag(name))
611}
612
613fn valid_namespace(value: &str) -> bool {
614 value.contains('.') && value.split('.').all(valid_component)
615}
616
617fn valid_tag(value: &str) -> bool {
618 value.split('.').all(valid_component)
619}
620
621fn valid_component(value: &str) -> bool {
622 !value.is_empty()
623 && value
624 .chars()
625 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
626}
627
628fn map<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a [(Form, Form)], String> {
629 match form {
630 Form::Map(entries) => Ok(entries),
631 _ => Err(malformed(origin, format!("{field} must be a map"))),
632 }
633}
634
635fn vector<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a [Form], String> {
636 match form {
637 Form::Vector(values) => Ok(values),
638 _ => Err(malformed(origin, format!("{field} must be a vector"))),
639 }
640}
641
642fn string<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a str, String> {
643 match form {
644 Form::String(value) if !value.is_empty() => Ok(value),
645 _ => Err(malformed(
646 origin,
647 format!("{field} must be a non-empty string"),
648 )),
649 }
650}
651
652fn keyword<'a>(form: &'a Form, origin: &str, field: &str) -> Result<&'a str, String> {
653 match form {
654 Form::Keyword(value) => Ok(value),
655 _ => Err(malformed(origin, format!("{field} must be a keyword"))),
656 }
657}
658
659fn keyword_vector(form: &Form, origin: &str, field: &str) -> Result<Vec<String>, String> {
660 vector(form, origin, field)?
661 .iter()
662 .map(|form| keyword(form, origin, field).map(str::to_owned))
663 .collect()
664}
665
666fn wire_vector(form: &Form, origin: &str, field: &str) -> Result<Vec<String>, String> {
667 vector(form, origin, field)?
668 .iter()
669 .map(|form| wire_type(form, origin, field))
670 .collect()
671}
672
673fn wire_type(form: &Form, origin: &str, field: &str) -> Result<String, String> {
674 match form {
675 Form::Keyword(value) => Ok(value.clone()),
676 Form::Vector(_) => Ok(form.to_string()),
677 _ => Err(malformed(origin, format!("{field} must be a type form"))),
678 }
679}
680
681fn key(form: &Form) -> Option<&str> {
682 match form {
683 Form::Keyword(value) | Form::Symbol(value) | Form::String(value) => Some(value),
684 _ => None,
685 }
686}
687
688fn required<'a>(entries: &'a [(Form, Form)], name: &str, origin: &str) -> Result<&'a Form, String> {
689 optional(entries, name)
690 .ok_or_else(|| malformed(origin, format!("missing required field {name}")))
691}
692
693fn optional<'a>(entries: &'a [(Form, Form)], name: &str) -> Option<&'a Form> {
694 entries
695 .iter()
696 .find(|(candidate, _)| key(candidate) == Some(name))
697 .map(|(_, value)| value)
698}
699
700fn named_string(entries: &[(Form, Form)], name: &str, origin: &str) -> Result<String, String> {
701 string(required(entries, name, origin)?, origin, name).map(str::to_owned)
702}
703
704fn named_keyword(entries: &[(Form, Form)], name: &str, origin: &str) -> Result<String, String> {
705 keyword(required(entries, name, origin)?, origin, name).map(str::to_owned)
706}
707
708fn reject_unknown(
709 entries: &[(Form, Form)],
710 allowed: &[&str],
711 origin: &str,
712 scope: &str,
713) -> Result<(), String> {
714 let mut seen = HashSet::new();
715 for (candidate, _) in entries {
716 let Some(name) = key(candidate) else {
717 return Err(malformed(origin, format!("{scope} keys must be named")));
718 };
719 if !allowed.contains(&name) {
720 return Err(malformed(origin, format!("unknown {scope} field: {name}")));
721 }
722 if !seen.insert(name) {
723 return Err(malformed(
724 origin,
725 format!("duplicate {scope} field: {name}"),
726 ));
727 }
728 }
729 Ok(())
730}
731
732fn malformed(origin: &str, message: impl AsRef<str>) -> String {
733 format!("extension/malformed {origin}: {}", message.as_ref())
734}
735
736#[cfg(test)]
737mod tests {
738 use super::{ExtensionManifest, WasmAbi};
739
740 const MANIFEST: &str = r#"
741 {:namespace "crypto.hash"
742 :identity "hara/crypto.hash"
743 :version "0.1.0"
744 :provider :wasm
745 :module "hash.wasm"
746 :abi :hta.v1
747 :exports {"digest" {:args [:bytes] :returns :bytes :async true}}
748 :host-calls {"crypto.random" ["fill"]}
749 :handles {"digest" {:tag crypto}}
750 :capabilities [:random]}"#;
751
752 const ALIASED_MANIFEST: &str = r#"
753 {:namespace "math.scalar"
754 :version "0.1.0"
755 :provider :wasm
756 :module "math.wasm"
757 :abi :core.v1
758 :exports {"sum" {:wasm/export "add_i64"
759 :args [:i64 :i64]
760 :returns :i64}}
761 :capabilities []}"#;
762
763 const MEMORY_MANIFEST: &str = r#"
764 {:namespace "codec.echo"
765 :version "0.1.0"
766 :provider :wasm
767 :module "echo.wasm"
768 :abi :memory.v1
769 :exports {"echo" {:wasm/export "echo_bytes"
770 :args [:bytes]
771 :returns :bytes}}
772 :capabilities []}"#;
773
774 #[test]
775 fn parses_the_wasm_manifest_contract() {
776 let manifest = ExtensionManifest::parse(MANIFEST, "fixture").unwrap();
777 assert_eq!(manifest.namespace, "crypto.hash");
778 assert_eq!(manifest.identity.as_deref(), Some("hara/crypto.hash"));
779 assert_eq!(manifest.abi, WasmAbi::HtaV1);
780 assert!(manifest.exports[0].1.asynchronous);
781 assert_eq!(manifest.exports[0].1.raw_name("digest"), "digest");
782 assert_eq!(manifest.handle_tags["digest"], "crypto");
783 assert!(manifest.permits_host_call("crypto.random", "fill"));
784 }
785
786 #[test]
787 fn parses_memory_v1_manifests_without_downgrading_the_abi() {
788 let manifest = ExtensionManifest::parse(MEMORY_MANIFEST, "fixture").unwrap();
789 assert_eq!(manifest.abi, WasmAbi::MemoryV1);
790 assert_eq!(manifest.exports[0].1.raw_name("echo"), "echo_bytes");
791 }
792
793 #[test]
794 fn preserves_public_and_raw_export_names() {
795 let manifest = ExtensionManifest::parse(ALIASED_MANIFEST, "fixture").unwrap();
796 assert_eq!(manifest.exports[0].0, "sum");
797 assert_eq!(manifest.exports[0].1.raw_name("sum"), "add_i64");
798 }
799
800 #[test]
801 fn rejects_non_wasm_unknown_duplicate_unsafe_and_unsupported_manifests() {
802 for source in [
803 MANIFEST.replace(":wasm", ":pod"),
804 MANIFEST.replace(":capabilities [:random]", ":capabilities [] :extra true"),
805 MANIFEST.replace(":version \"0.1.0\"", ":version \"0.1.0\" :version \"2\""),
806 MANIFEST.replace("hash.wasm", "../hash.wasm"),
807 MANIFEST.replace(":hta.v1", ":unknown-v1"),
808 ] {
809 assert!(ExtensionManifest::parse(&source, "fixture")
810 .unwrap_err()
811 .starts_with("extension/malformed fixture:"));
812 }
813 }
814
815 #[test]
816 fn hta_targets_name_provider_implementations() {
817 let source = r#"
818{:namespace "demo.hta"
819 :version "1"
820 :provider :hta
821 :abi :hta.v1
822 :targets {:node {:provider "node/provider.mjs" :runtime :process}
823 :browser {:provider "browser/provider.mjs" :runtime :web-worker}}
824 :exports {"open" {:args [] :returns :value}}
825 :capabilities []}"#;
826 let manifest = ExtensionManifest::parse(source, "fixture").unwrap();
827 assert_eq!(manifest.targets["browser"].provider, "browser/provider.mjs");
828 assert!(ExtensionManifest::parse(
829 &source.replace(":provider \"browser/provider.mjs\"", ":module \"browser/worker.mjs\""),
830 "fixture"
831 )
832 .is_err());
833 }
834}