1#![allow(missing_docs)]
3#![deny(missing_debug_implementations)]
4
5pub mod annotations;
6
7use std::{collections::BTreeMap, str::FromStr};
8
9use anyhow::{Context, Error};
10use base64::Engine;
11use indexmap::IndexMap;
12use serde::{Deserialize, Serialize, de::DeserializeOwned};
13use url::Url;
14
15use crate::metadata::annotations::{FileSystemMappings, Wapm};
16
17#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct Manifest {
20 #[serde(skip, default)]
23 pub origin: Option<String>,
24 #[serde(default, rename = "use", skip_serializing_if = "IndexMap::is_empty")]
26 pub use_map: IndexMap<String, UrlOrManifest>,
27 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
29 pub package: IndexMap<String, Annotation>,
30 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
32 pub atoms: IndexMap<String, Atom>,
33 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
35 pub commands: IndexMap<String, Command>,
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub bindings: Vec<Binding>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub entrypoint: Option<String>,
42}
43
44impl Manifest {
45 pub fn package_annotation<T>(&self, name: &str) -> Result<Option<T>, anyhow::Error>
46 where
47 T: DeserializeOwned,
48 {
49 if let Some(value) = self.package.get(name) {
50 let annotation = value.deserialized().map_err(|e| {
51 anyhow::anyhow!("Failed to deserialize package annotation '{}': {}", name, e)
52 })?;
53 return Ok(Some(annotation));
54 }
55
56 Ok(None)
57 }
58
59 pub fn atom_signature(&self, atom_name: &str) -> Result<AtomSignature, anyhow::Error> {
60 self.atoms
61 .get(atom_name)
62 .ok_or_else(|| anyhow::anyhow!("failed to get atom: {}", atom_name))?
63 .signature
64 .parse()
65 }
66}
67
68impl Manifest {
70 pub fn wapm(&self) -> Result<Option<Wapm>, anyhow::Error> {
72 self.package_annotation(Wapm::KEY)
73 }
74
75 pub fn filesystem(&self) -> Result<Option<FileSystemMappings>, anyhow::Error> {
78 self.package_annotation(FileSystemMappings::KEY)
79 }
80
81 pub fn update_filesystem(&mut self, mapping: FileSystemMappings) -> Result<(), anyhow::Error> {
82 if let Some(value) = self.package.get_mut(FileSystemMappings::KEY) {
83 let new_value = ciborium::value::Value::serialized(&mapping)
84 .map_err(|e| anyhow::anyhow!("Failed to serialize filesystem mappings: {}", e))?;
85 *value = new_value;
86
87 Ok(())
88 } else {
89 anyhow::bail!("failed to get file system mappings");
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
95#[serde(rename_all = "kebab-case")]
96pub enum BindingsExtended {
97 Wit(WitBindings),
98 Wai(WaiBindings),
99}
100
101impl BindingsExtended {
102 pub fn metadata_paths(&self) -> Vec<&str> {
103 match self {
104 BindingsExtended::Wit(w) => w.metadata_paths(),
105 BindingsExtended::Wai(w) => w.metadata_paths(),
106 }
107 }
108
109 pub fn module(&self) -> &str {
111 match self {
112 BindingsExtended::Wit(wit) => &wit.module,
113 BindingsExtended::Wai(wai) => &wai.module,
114 }
115 }
116
117 pub fn exports(&self) -> Option<&str> {
119 match self {
120 BindingsExtended::Wit(wit) => Some(&wit.exports),
121 BindingsExtended::Wai(wai) => wai.exports.as_deref(),
122 }
123 }
124
125 pub fn imports(&self) -> Vec<String> {
127 match self {
128 BindingsExtended::Wit(_) => Vec::new(),
129 BindingsExtended::Wai(wai) => wai.imports.clone(),
130 }
131 }
132}
133
134#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
135pub struct WitBindings {
136 pub exports: String,
137 pub module: String,
138}
139
140impl WitBindings {
141 pub fn metadata_paths(&self) -> Vec<&str> {
142 vec![&self.exports]
143 }
144}
145
146#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
147pub struct WaiBindings {
148 pub exports: Option<String>,
149 pub module: String,
150 pub imports: Vec<String>,
151}
152
153impl WaiBindings {
154 pub fn metadata_paths(&self) -> Vec<&str> {
155 let mut paths: Vec<&str> = Vec::new();
156
157 if let Some(export) = &self.exports {
158 paths.push(export);
159 }
160 for import in &self.imports {
161 paths.push(import);
162 }
163
164 paths
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
169pub struct Binding {
170 pub name: String,
171 pub kind: String,
172 pub annotations: ciborium::Value,
173}
174
175impl Binding {
176 pub fn new_wit(name: String, kind: String, wit: WitBindings) -> Self {
177 Self {
178 name,
179 kind,
180 annotations: ciborium::Value::serialized(&BindingsExtended::Wit(wit)).unwrap(),
181 }
182 }
183
184 pub fn get_bindings(&self) -> Option<BindingsExtended> {
185 self.annotations.deserialized().ok()
186 }
187
188 pub fn get_wai_bindings(&self) -> Option<WaiBindings> {
189 match self.get_bindings() {
190 Some(BindingsExtended::Wai(wai)) => Some(wai),
191 _ => None,
192 }
193 }
194
195 pub fn get_wit_bindings(&self) -> Option<WitBindings> {
196 match self.get_bindings() {
197 Some(BindingsExtended::Wit(wit)) => Some(wit),
198 _ => None,
199 }
200 }
201}
202
203#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
205pub struct ManifestWithoutAtomSignatures {
206 #[serde(skip, default)]
207 pub origin: Option<String>,
208 #[serde(default, rename = "use", skip_serializing_if = "IndexMap::is_empty")]
209 pub use_map: IndexMap<String, UrlOrManifest>,
210 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
211 pub package: IndexMap<String, Annotation>,
212 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
215 pub atoms: IndexMap<String, AtomWithoutSignature>,
216 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
217 pub commands: IndexMap<String, Command>,
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub bindings: Vec<Binding>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub entrypoint: Option<String>,
223}
224
225impl ManifestWithoutAtomSignatures {
226 pub fn to_manifest(
229 &self,
230 atom_signatures: &BTreeMap<String, String>,
231 ) -> Result<Manifest, Error> {
232 let mut atoms = IndexMap::new();
233 for (k, v) in self.atoms.iter() {
234 let signature = atom_signatures
235 .get(k)
236 .with_context(|| format!("Could not find signature for atom {k:?}"))?;
237 atoms.insert(
238 k.clone(),
239 Atom {
240 kind: v.kind.clone(),
241 signature: signature.clone(),
242 annotations: v.annotations.clone(),
243 },
244 );
245 }
246 Ok(Manifest {
247 origin: self.origin.clone(),
248 use_map: self.use_map.clone(),
249 package: self.package.clone(),
250 atoms,
251 bindings: self.bindings.clone(),
252 commands: self.commands.clone(),
253 entrypoint: self.entrypoint.clone(),
254 })
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
261#[serde(untagged)]
262#[allow(clippy::large_enum_variant)]
263pub enum UrlOrManifest {
264 Url(Url),
266 Manifest(Manifest),
268 RegistryDependentUrl(String),
270}
271
272impl UrlOrManifest {
273 pub fn is_manifest(&self) -> bool {
274 matches!(self, UrlOrManifest::Manifest(_))
275 }
276
277 pub fn is_url(&self) -> bool {
278 matches!(self, UrlOrManifest::Url(_))
279 }
280}
281
282pub type Annotation = ciborium::Value;
284
285#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
287pub struct AtomWithoutSignature {
288 pub kind: Url,
290 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
292 pub annotations: IndexMap<String, Annotation>,
293}
294
295#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
297pub struct Atom {
298 pub kind: Url,
300 pub signature: String,
302 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
304 pub annotations: IndexMap<String, Annotation>,
305}
306
307#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
308pub enum AtomSignature {
309 Sha256([u8; 32]),
310}
311
312impl AtomSignature {
313 pub fn as_bytes(&self) -> &[u8] {
314 match self {
315 AtomSignature::Sha256(hash) => hash.as_slice(),
316 }
317 }
318}
319
320impl Atom {
321 pub fn annotation<T>(&self, name: &str) -> Result<Option<T>, anyhow::Error>
322 where
323 T: DeserializeOwned,
324 {
325 if let Some(value) = self.annotations.get(name) {
326 let annotation = value.deserialized().map_err(|e| {
327 anyhow::anyhow!("Failed to deserialize annotation '{}': {}", name, e)
328 })?;
329 return Ok(Some(annotation));
330 }
331
332 Ok(None)
333 }
334
335 pub fn wasm(&self) -> Result<Option<annotations::Wasm>, anyhow::Error> {
336 self.annotation(annotations::Wasm::KEY)
337 }
338}
339
340impl std::fmt::Display for AtomSignature {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 match self {
343 AtomSignature::Sha256(bytes) => {
344 let encoded = base64::prelude::BASE64_STANDARD.encode(bytes);
345 write!(f, "sha256:{encoded}")
346 }
347 }
348 }
349}
350
351impl FromStr for AtomSignature {
352 type Err = anyhow::Error;
353
354 fn from_str(s: &str) -> Result<Self, Self::Err> {
355 let base64_encoded = s
356 .strip_prefix("sha256:")
357 .ok_or_else(|| anyhow::Error::msg("malformed atom signature"))?;
358
359 let hash = base64::prelude::BASE64_STANDARD
360 .decode(base64_encoded)
361 .context("malformed base64 encoded hash")?;
362
363 let hash: [u8; 32] = hash
364 .as_slice()
365 .try_into()
366 .context("sha256 hash must be 32 bytes")?;
367
368 Ok(Self::Sha256(hash))
369 }
370}
371
372#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
374pub struct Command {
375 pub runner: String,
379 pub annotations: IndexMap<String, Annotation>,
382}
383
384impl Command {
385 pub fn annotation<T>(&self, name: &str) -> Result<Option<T>, anyhow::Error>
386 where
387 T: DeserializeOwned,
388 {
389 if let Some(value) = self.annotations.get(name) {
390 let annotation = value.deserialized().map_err(|e| {
391 anyhow::anyhow!("Failed to deserialize annotation '{}': {}", name, e)
392 })?;
393 return Ok(Some(annotation));
394 }
395
396 Ok(None)
397 }
398}
399
400impl Command {
402 pub fn wasi(&self) -> Result<Option<annotations::Wasi>, anyhow::Error> {
403 self.annotation(annotations::Wasi::KEY)
404 }
405
406 pub fn wcgi(&self) -> Result<Option<annotations::Wcgi>, anyhow::Error> {
407 self.annotation(annotations::Wcgi::KEY)
408 }
409
410 pub fn emscripten(&self) -> Result<Option<annotations::Emscripten>, anyhow::Error> {
411 self.annotation(annotations::Emscripten::KEY)
412 }
413
414 pub fn atom(&self) -> Result<Option<annotations::Atom>, anyhow::Error> {
415 if let Some(annotations) = self.annotation(annotations::Atom::KEY)? {
417 return Ok(Some(annotations));
418 }
419
420 #[allow(deprecated)]
422 let atom = if let Ok(Some(annotations::Wasi { atom, .. })) = self.wasi() {
423 Some(atom)
424 } else if let Ok(Some(annotations::Emscripten { atom, .. })) = self.emscripten() {
425 atom
426 } else {
427 None
428 };
429 if let Some(atom) = atom {
430 match atom.split_once(':') {
431 Some((dependency, module)) => {
432 if module.contains(':') {
433 return Err(anyhow::anyhow!("Invalid format"));
434 }
435
436 return Ok(Some(annotations::Atom::new(
437 module,
438 Some(dependency.to_string()),
439 )));
440 }
441 None => return Ok(Some(annotations::Atom::new(atom.to_string(), None))),
442 }
443 }
444
445 Ok(None)
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::metadata::annotations::Wasm;
453
454 #[test]
455 fn deserialize_extended_wai_bindings() {
456 let json = serde_json::json!({
457 "wai": {
458 "exports": "interface.wai",
459 "module": "my-module",
460 "imports": ["browser.wai", "fs.wai"],
461 }
462 });
463 let bindings = BindingsExtended::deserialize(json).unwrap();
464
465 assert_eq!(
466 bindings,
467 BindingsExtended::Wai(WaiBindings {
468 exports: Some("interface.wai".to_string()),
469 module: "my-module".to_string(),
470 imports: vec!["browser.wai".to_string(), "fs.wai".to_string(),]
471 })
472 );
473 }
474
475 #[test]
476 fn deserialize_extended_wit_bindings() {
477 let json = serde_json::json!({
478 "wit": {
479 "exports": "interface.wit",
480 "module": "my-module",
481 }
482 });
483 let bindings = BindingsExtended::deserialize(json).unwrap();
484
485 assert_eq!(
486 bindings,
487 BindingsExtended::Wit(WitBindings {
488 exports: "interface.wit".to_string(),
489 module: "my-module".to_string(),
490 })
491 );
492 }
493
494 #[test]
495 fn atom_with_wasm_features() {
496 use indexmap::IndexMap;
497 use url::Url;
498
499 let mut annotations = IndexMap::new();
501 let mut wasm_features = Wasm::default();
502 wasm_features.enable_exceptions();
503 wasm_features.enable_simd();
504 wasm_features.add_feature("multiple-returns");
505
506 let wasm_value = ciborium::value::Value::serialized(&wasm_features).unwrap();
508 annotations.insert(Wasm::KEY.to_string(), wasm_value);
509
510 let atom = Atom {
511 kind: Url::parse("https://webc.org/kind/wasm").unwrap(),
512 signature: "sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=".to_string(),
513 annotations,
514 };
515
516 let retrieved_features = atom.wasm().unwrap().unwrap();
518 assert_eq!(retrieved_features.features.len(), 3);
519 assert!(retrieved_features.has_exceptions());
520 assert!(retrieved_features.has_simd());
521 assert!(retrieved_features.has_feature("multiple-returns"));
522 assert!(!retrieved_features.has_threads());
523
524 let simple_wasm = Wasm::with_features(&[Wasm::BULK_MEMORY, Wasm::REFERENCE_TYPES]);
526 assert!(simple_wasm.has_bulk_memory());
527 assert!(simple_wasm.has_reference_types());
528 assert!(!simple_wasm.has_simd());
529
530 let json = serde_json::to_string(&atom).unwrap();
532 let deserialized_atom: Atom = serde_json::from_str(&json).unwrap();
533
534 let deserialized_features = deserialized_atom.wasm().unwrap().unwrap();
536 assert_eq!(deserialized_features.features.len(), 3);
537 assert!(deserialized_features.has_exceptions());
538 assert!(deserialized_features.has_simd());
539 assert!(deserialized_features.has_feature("multiple-returns"));
540 }
541
542 #[test]
543 fn manifest_with_atom_wasm_features() {
544 use annotations::Wasm;
545
546 let mut manifest = serde_json::from_value::<Manifest>(serde_json::json!({
548 "package": {
549 "wapm": {
550 "name": "wiqar/cowsay",
551 "readme": {
552 "path": "README.md",
553 "volume": "metadata"
554 },
555 "version": "0.3.0",
556 "repository": "https://github.com/wapm-packages/cowsay",
557 "description": "cowsay is a program that generates ASCII pictures of a cow with a message"
558 }
559 },
560 "atoms": {
561 "cowsay": {
562 "kind": "https://webc.org/kind/wasm",
563 "signature": "sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo="
564 }
565 },
566 "commands": {
567 "cowsay": {
568 "runner": "https://webc.org/runner/wasi",
569 "annotations": {
570 "wasi": {
571 "atom": "cowsay",
572 "package": null,
573 "main_args": null
574 }
575 }
576 },
577 "cowthink": {
578 "runner": "https://webc.org/runner/wasi",
579 "annotations": {
580 "wasi": {
581 "atom": "cowsay",
582 "package": null,
583 "main_args": null
584 }
585 }
586 }
587 }
588 })).unwrap();
589
590 let cowsay_atom = manifest.atoms.get_mut("cowsay").unwrap();
592
593 let mut wasm_features = Wasm::default();
595 wasm_features.enable_exceptions();
596 wasm_features.enable_multi_value();
597 wasm_features.enable_bulk_memory();
598
599 let wasm_value = ciborium::value::Value::serialized(&wasm_features).unwrap();
601 cowsay_atom.annotations = IndexMap::new();
602 cowsay_atom
603 .annotations
604 .insert(Wasm::KEY.to_string(), wasm_value);
605
606 let json = serde_json::to_string(&manifest).unwrap();
608 let deserialized_manifest: Manifest = serde_json::from_str(&json).unwrap();
609
610 let atom = deserialized_manifest.atoms.get("cowsay").unwrap();
612 let wasm = atom.wasm().unwrap().unwrap();
613 assert!(wasm.has_exceptions());
614 assert!(wasm.has_multi_value());
615 assert!(wasm.has_bulk_memory());
616 assert!(!wasm.has_simd());
617
618 let expected_manifest = serde_json::from_value::<Manifest>(serde_json::json!({
620 "package": {
621 "wapm": {
622 "name": "wiqar/cowsay",
623 "readme": {
624 "path": "README.md",
625 "volume": "metadata"
626 },
627 "version": "0.3.0",
628 "repository": "https://github.com/wapm-packages/cowsay",
629 "description": "cowsay is a program that generates ASCII pictures of a cow with a message"
630 }
631 },
632 "atoms": {
633 "cowsay": {
634 "kind": "https://webc.org/kind/wasm",
635 "signature": "sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=",
636 "annotations": {
637 "wasm": {
638 "features": ["exception-handling", "multi-value", "bulk-memory"]
639 }
640 }
641 }
642 },
643 "commands": {
644 "cowsay": {
645 "runner": "https://webc.org/runner/wasi",
646 "annotations": {
647 "wasi": {
648 "atom": "cowsay",
649 "package": null,
650 "main_args": null
651 }
652 }
653 },
654 "cowthink": {
655 "runner": "https://webc.org/runner/wasi",
656 "annotations": {
657 "wasi": {
658 "atom": "cowsay",
659 "package": null,
660 "main_args": null
661 }
662 }
663 }
664 }
665 })).unwrap();
666
667 let expected_atom = expected_manifest.atoms.get("cowsay").unwrap();
671 let expected_wasm = expected_atom.wasm().unwrap().unwrap();
672 assert_eq!(expected_wasm.features.len(), 3);
673 assert!(expected_wasm.has_exceptions());
674 assert!(expected_wasm.has_multi_value());
675 assert!(expected_wasm.has_bulk_memory());
676 }
677}