webc 5.0.0-rc.1

WebContainer implementation for wapm.io
Documentation
pub use indexmap::IndexMap;
pub use url::Url;

use std::{collections::BTreeMap, fmt};

use serde::{Deserialize, Serialize};

use crate::v1::Error;

/// Manifest of the file, see spec `ยง2.3.1`
#[derive(Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct Manifest {
    /// If this manifest was vendored from an external source, where did it originally
    /// come from? Necessary for vendoring dependencies.
    #[serde(skip, default)]
    pub origin: Option<String>,
    /// Dependencies of this file (internal or external)
    #[serde(default, rename = "use", skip_serializing_if = "IndexMap::is_empty")]
    pub use_map: IndexMap<String, UrlOrManifest>,
    /// Package version, author, license, etc. information
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub package: IndexMap<String, Annotation>,
    /// Atoms (executable files) in this container
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub atoms: IndexMap<String, Atom>,
    /// Commands that this container can execute (empty for library-only containers)
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub commands: IndexMap<String, Command>,
    /// Binding descriptions of this manifest
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub bindings: Vec<Binding>,
    /// Entrypoint (default command) to lookup in `self.commands` when invoking `wasmer run file.webc`
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub entrypoint: Option<String>,
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BindingsExtended {
    Wit(WitBindings),
    Wai(WaiBindings),
}

impl BindingsExtended {
    pub fn metadata_paths(&self) -> Vec<&str> {
        match self {
            BindingsExtended::Wit(w) => w.metadata_paths(),
            BindingsExtended::Wai(w) => w.metadata_paths(),
        }
    }

    /// The WebAssembly module these bindings annotate.
    pub fn module(&self) -> &str {
        match self {
            BindingsExtended::Wit(wit) => &wit.module,
            BindingsExtended::Wai(wai) => &wai.module,
        }
    }

    /// The URI pointing to the exports file exposed by these bindings.
    pub fn exports(&self) -> Option<&str> {
        match self {
            BindingsExtended::Wit(wit) => Some(&wit.exports),
            BindingsExtended::Wai(wai) => wai.exports.as_deref(),
        }
    }

    /// The URI pointing to the exports file exposed by these bindings.
    pub fn imports(&self) -> Vec<String> {
        match self {
            BindingsExtended::Wit(_) => Vec::new(),
            BindingsExtended::Wai(wai) => wai.imports.clone(),
        }
    }
}

#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct WitBindings {
    pub exports: String,
    pub module: String,
}

impl WitBindings {
    pub fn metadata_paths(&self) -> Vec<&str> {
        vec![&self.exports]
    }
}

#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct WaiBindings {
    pub exports: Option<String>,
    pub module: String,
    pub imports: Vec<String>,
}

impl WaiBindings {
    pub fn metadata_paths(&self) -> Vec<&str> {
        let mut paths: Vec<&str> = Vec::new();

        if let Some(export) = &self.exports {
            paths.push(export);
        }
        for import in &self.imports {
            paths.push(import);
        }

        paths
    }
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Binding {
    pub name: String,
    pub kind: String,
    pub annotations: serde_cbor::Value,
}

impl Binding {
    pub fn new_wit(name: String, kind: String, wit: WitBindings) -> Self {
        Self {
            name,
            kind,
            annotations: serde_cbor::from_slice(
                &serde_cbor::to_vec(&BindingsExtended::Wit(wit)).unwrap(),
            )
            .unwrap(),
        }
    }

    pub fn get_bindings(&self) -> Option<BindingsExtended> {
        serde_cbor::from_slice(&serde_cbor::to_vec(&self.annotations).ok()?).ok()
    }

    pub fn get_wai_bindings(&self) -> Option<WaiBindings> {
        match self.get_bindings() {
            Some(BindingsExtended::Wai(wai)) => Some(wai),
            _ => None,
        }
    }

    pub fn get_wit_bindings(&self) -> Option<WitBindings> {
        match self.get_bindings() {
            Some(BindingsExtended::Wit(wit)) => Some(wit),
            _ => None,
        }
    }
}

/// Same as `Manifest`, but doesn't require the `atom.signature`
#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ManifestWithoutAtomSignatures {
    #[serde(skip, default)]
    pub origin: Option<String>,
    #[serde(default, rename = "use", skip_serializing_if = "IndexMap::is_empty")]
    pub use_map: IndexMap<String, UrlOrManifest>,
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub package: IndexMap<String, Annotation>,
    /// Atoms do not require a ".signature" field to be valid
    /// (SHA1 signature is calculated during packaging)
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub atoms: IndexMap<String, AtomWithoutSignature>,
    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
    pub commands: IndexMap<String, Command>,
    /// Binding descriptions of this manifest
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub bindings: Vec<Binding>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub entrypoint: Option<String>,
}

impl ManifestWithoutAtomSignatures {
    /// Returns the manifest with the "atom.signature" field filled out.
    /// `atom_signatures` is a map between the atom name and the SHA256 hash
    pub fn to_manifest(
        &self,
        atom_signatures: &BTreeMap<String, String>,
    ) -> Result<Manifest, Error> {
        let mut atoms = IndexMap::new();
        for (k, v) in self.atoms.iter() {
            let signature = atom_signatures
                .get(k)
                .ok_or(Error(format!("Could not find signature for atom {k:?}")))?;
            atoms.insert(
                k.clone(),
                Atom {
                    kind: v.kind.clone(),
                    signature: signature.clone(),
                },
            );
        }
        Ok(Manifest {
            origin: self.origin.clone(),
            use_map: self.use_map.clone(),
            package: self.package.clone(),
            atoms,
            bindings: self.bindings.clone(),
            commands: self.commands.clone(),
            entrypoint: self.entrypoint.clone(),
        })
    }
}

/// Dependency of this file, encoded as either a URL or a
/// manifest (in case of vendored dependencies)
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum UrlOrManifest {
    /// External dependency
    Url(Url),
    /// Internal dependency (volume name = `user/package@version`)
    Manifest(Manifest),
    /// Registry-dependent dependency in a forma a la "namespace/package@version"
    RegistryDependentUrl(String),
}

impl UrlOrManifest {
    pub fn is_manifest(&self) -> bool {
        matches!(self, UrlOrManifest::Manifest(_))
    }

    pub fn is_url(&self) -> bool {
        matches!(self, UrlOrManifest::Url(_))
    }
}

impl fmt::Debug for UrlOrManifest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            UrlOrManifest::Url(u) => write!(f, "{u}"),
            UrlOrManifest::Manifest(m) => m.fmt(f),
            UrlOrManifest::RegistryDependentUrl(s) => write!(f, "{s}"),
        }
    }
}

impl fmt::Debug for Manifest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let json = serde_json::to_string_pretty(self).unwrap_or_default();
        write!(f, "{json}")
    }
}

impl fmt::Debug for ManifestWithoutAtomSignatures {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let json = serde_json::to_string_pretty(self).unwrap_or_default();
        write!(f, "{json}")
    }
}

/// Annotation = free-form metadata to be used by the atom
pub type Annotation = serde_cbor::Value;

/// Executable file, stored in the `.atoms` section of the file
#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AtomWithoutSignature {
    /// URL of the kind of the atom, usually `"webc.org/kind/wasm"`
    pub kind: Url,
}

/// Executable file, stored in the `.atoms` section of the file
#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Atom {
    /// URL of the kind of the atom, usually `"webc.org/kind/wasm"`
    pub kind: Url,
    /// Signature hash of the atom, usually `"sha256:xxxxx"`
    pub signature: String,
}

impl fmt::Debug for Atom {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Atom {{\r\n\tkind: {},\r\n\tsignature:{}\r\n}}",
            self.kind, self.signature
        )
    }
}

/// Command that can be run by a given implementation
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Command {
    /// User-defined string specifying the type of runner to use to invoke the command
    ///
    /// The default value for this field is `https://webc.org/runners/wasi@unstable_`
    pub runner: String,
    /// User-defined map of free-form CBOR values that the runner can use as metadata
    /// when invoking the command
    pub annotations: IndexMap<String, Annotation>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deserialize_extended_wai_bindings() {
        let json = serde_json::json!({
            "wai": {
                "exports": "interface.wai",
                "module": "my-module",
                "imports": ["browser.wai", "fs.wai"],
            }
        });
        let bindings = BindingsExtended::deserialize(json).unwrap();

        assert_eq!(
            bindings,
            BindingsExtended::Wai(WaiBindings {
                exports: Some("interface.wai".to_string()),
                module: "my-module".to_string(),
                imports: vec!["browser.wai".to_string(), "fs.wai".to_string(),]
            })
        );
    }

    #[test]
    fn deserialize_extended_wit_bindings() {
        let json = serde_json::json!({
            "wit": {
                "exports": "interface.wit",
                "module": "my-module",
            }
        });
        let bindings = BindingsExtended::deserialize(json).unwrap();

        assert_eq!(
            bindings,
            BindingsExtended::Wit(WitBindings {
                exports: "interface.wit".to_string(),
                module: "my-module".to_string(),
            })
        );
    }
}