#![allow(missing_docs)]
#![deny(missing_debug_implementations)]
pub mod annotations;
use std::{collections::BTreeMap, str::FromStr};
use anyhow::{Context, Error};
use base64::Engine;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use url::Url;
use crate::metadata::annotations::{FileSystemMappings, Wapm};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Manifest {
#[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>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub atoms: IndexMap<String, Atom>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub commands: IndexMap<String, Command>,
#[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 Manifest {
pub fn package_annotation<T>(&self, name: &str) -> Result<Option<T>, anyhow::Error>
where
T: DeserializeOwned,
{
if let Some(value) = self.package.get(name) {
let annotation = value.deserialized().map_err(|e| {
anyhow::anyhow!("Failed to deserialize package annotation '{}': {}", name, e)
})?;
return Ok(Some(annotation));
}
Ok(None)
}
pub fn atom_signature(&self, atom_name: &str) -> Result<AtomSignature, anyhow::Error> {
self.atoms
.get(atom_name)
.ok_or_else(|| anyhow::anyhow!("failed to get atom: {}", atom_name))?
.signature
.parse()
}
}
impl Manifest {
pub fn wapm(&self) -> Result<Option<Wapm>, anyhow::Error> {
self.package_annotation(Wapm::KEY)
}
pub fn filesystem(&self) -> Result<Option<FileSystemMappings>, anyhow::Error> {
self.package_annotation(FileSystemMappings::KEY)
}
pub fn update_filesystem(&mut self, mapping: FileSystemMappings) -> Result<(), anyhow::Error> {
if let Some(value) = self.package.get_mut(FileSystemMappings::KEY) {
let new_value = ciborium::value::Value::serialized(&mapping)
.map_err(|e| anyhow::anyhow!("Failed to serialize filesystem mappings: {}", e))?;
*value = new_value;
Ok(())
} else {
anyhow::bail!("failed to get file system mappings");
}
}
}
#[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(),
}
}
pub fn module(&self) -> &str {
match self {
BindingsExtended::Wit(wit) => &wit.module,
BindingsExtended::Wai(wai) => &wai.module,
}
}
pub fn exports(&self) -> Option<&str> {
match self {
BindingsExtended::Wit(wit) => Some(&wit.exports),
BindingsExtended::Wai(wai) => wai.exports.as_deref(),
}
}
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, serde::Serialize, serde::Deserialize)]
pub struct Binding {
pub name: String,
pub kind: String,
pub annotations: ciborium::Value,
}
impl Binding {
pub fn new_wit(name: String, kind: String, wit: WitBindings) -> Self {
Self {
name,
kind,
annotations: ciborium::Value::serialized(&BindingsExtended::Wit(wit)).unwrap(),
}
}
pub fn get_bindings(&self) -> Option<BindingsExtended> {
self.annotations.deserialized().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,
}
}
}
#[derive(Default, Debug, 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>,
#[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>,
#[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 {
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)
.with_context(|| format!("Could not find signature for atom {k:?}"))?;
atoms.insert(
k.clone(),
Atom {
kind: v.kind.clone(),
signature: signature.clone(),
annotations: v.annotations.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(),
})
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum UrlOrManifest {
Url(Url),
Manifest(Manifest),
RegistryDependentUrl(String),
}
impl UrlOrManifest {
pub fn is_manifest(&self) -> bool {
matches!(self, UrlOrManifest::Manifest(_))
}
pub fn is_url(&self) -> bool {
matches!(self, UrlOrManifest::Url(_))
}
}
pub type Annotation = ciborium::Value;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct AtomWithoutSignature {
pub kind: Url,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub annotations: IndexMap<String, Annotation>,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Atom {
pub kind: Url,
pub signature: String,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub annotations: IndexMap<String, Annotation>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AtomSignature {
Sha256([u8; 32]),
}
impl AtomSignature {
pub fn as_bytes(&self) -> &[u8] {
match self {
AtomSignature::Sha256(hash) => hash.as_slice(),
}
}
}
impl Atom {
pub fn annotation<T>(&self, name: &str) -> Result<Option<T>, anyhow::Error>
where
T: DeserializeOwned,
{
if let Some(value) = self.annotations.get(name) {
let annotation = value.deserialized().map_err(|e| {
anyhow::anyhow!("Failed to deserialize annotation '{}': {}", name, e)
})?;
return Ok(Some(annotation));
}
Ok(None)
}
pub fn wasm(&self) -> Result<Option<annotations::Wasm>, anyhow::Error> {
self.annotation(annotations::Wasm::KEY)
}
}
impl std::fmt::Display for AtomSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AtomSignature::Sha256(bytes) => {
let encoded = base64::prelude::BASE64_STANDARD.encode(bytes);
write!(f, "sha256:{encoded}")
}
}
}
}
impl FromStr for AtomSignature {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let base64_encoded = s
.strip_prefix("sha256:")
.ok_or_else(|| anyhow::Error::msg("malformed atom signature"))?;
let hash = base64::prelude::BASE64_STANDARD
.decode(base64_encoded)
.context("malformed base64 encoded hash")?;
let hash: [u8; 32] = hash
.as_slice()
.try_into()
.context("sha256 hash must be 32 bytes")?;
Ok(Self::Sha256(hash))
}
}
#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Command {
pub runner: String,
pub annotations: IndexMap<String, Annotation>,
}
impl Command {
pub fn annotation<T>(&self, name: &str) -> Result<Option<T>, anyhow::Error>
where
T: DeserializeOwned,
{
if let Some(value) = self.annotations.get(name) {
let annotation = value.deserialized().map_err(|e| {
anyhow::anyhow!("Failed to deserialize annotation '{}': {}", name, e)
})?;
return Ok(Some(annotation));
}
Ok(None)
}
}
impl Command {
pub fn wasi(&self) -> Result<Option<annotations::Wasi>, anyhow::Error> {
self.annotation(annotations::Wasi::KEY)
}
pub fn wcgi(&self) -> Result<Option<annotations::Wcgi>, anyhow::Error> {
self.annotation(annotations::Wcgi::KEY)
}
pub fn emscripten(&self) -> Result<Option<annotations::Emscripten>, anyhow::Error> {
self.annotation(annotations::Emscripten::KEY)
}
pub fn atom(&self) -> Result<Option<annotations::Atom>, anyhow::Error> {
if let Some(annotations) = self.annotation(annotations::Atom::KEY)? {
return Ok(Some(annotations));
}
#[allow(deprecated)]
let atom = if let Ok(Some(annotations::Wasi { atom, .. })) = self.wasi() {
Some(atom)
} else if let Ok(Some(annotations::Emscripten { atom, .. })) = self.emscripten() {
atom
} else {
None
};
if let Some(atom) = atom {
match atom.split_once(':') {
Some((dependency, module)) => {
if module.contains(':') {
return Err(anyhow::anyhow!("Invalid format"));
}
return Ok(Some(annotations::Atom::new(
module,
Some(dependency.to_string()),
)));
}
None => return Ok(Some(annotations::Atom::new(atom.to_string(), None))),
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::annotations::Wasm;
#[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(),
})
);
}
#[test]
fn atom_with_wasm_features() {
use indexmap::IndexMap;
use url::Url;
let mut annotations = IndexMap::new();
let mut wasm_features = Wasm::default();
wasm_features.enable_exceptions();
wasm_features.enable_simd();
wasm_features.add_feature("multiple-returns");
let wasm_value = ciborium::value::Value::serialized(&wasm_features).unwrap();
annotations.insert(Wasm::KEY.to_string(), wasm_value);
let atom = Atom {
kind: Url::parse("https://webc.org/kind/wasm").unwrap(),
signature: "sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=".to_string(),
annotations,
};
let retrieved_features = atom.wasm().unwrap().unwrap();
assert_eq!(retrieved_features.features.len(), 3);
assert!(retrieved_features.has_exceptions());
assert!(retrieved_features.has_simd());
assert!(retrieved_features.has_feature("multiple-returns"));
assert!(!retrieved_features.has_threads());
let simple_wasm = Wasm::with_features(&[Wasm::BULK_MEMORY, Wasm::REFERENCE_TYPES]);
assert!(simple_wasm.has_bulk_memory());
assert!(simple_wasm.has_reference_types());
assert!(!simple_wasm.has_simd());
let json = serde_json::to_string(&atom).unwrap();
let deserialized_atom: Atom = serde_json::from_str(&json).unwrap();
let deserialized_features = deserialized_atom.wasm().unwrap().unwrap();
assert_eq!(deserialized_features.features.len(), 3);
assert!(deserialized_features.has_exceptions());
assert!(deserialized_features.has_simd());
assert!(deserialized_features.has_feature("multiple-returns"));
}
#[test]
fn manifest_with_atom_wasm_features() {
use annotations::Wasm;
let mut manifest = serde_json::from_value::<Manifest>(serde_json::json!({
"package": {
"wapm": {
"name": "wiqar/cowsay",
"readme": {
"path": "README.md",
"volume": "metadata"
},
"version": "0.3.0",
"repository": "https://github.com/wapm-packages/cowsay",
"description": "cowsay is a program that generates ASCII pictures of a cow with a message"
}
},
"atoms": {
"cowsay": {
"kind": "https://webc.org/kind/wasm",
"signature": "sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo="
}
},
"commands": {
"cowsay": {
"runner": "https://webc.org/runner/wasi",
"annotations": {
"wasi": {
"atom": "cowsay",
"package": null,
"main_args": null
}
}
},
"cowthink": {
"runner": "https://webc.org/runner/wasi",
"annotations": {
"wasi": {
"atom": "cowsay",
"package": null,
"main_args": null
}
}
}
}
})).unwrap();
let cowsay_atom = manifest.atoms.get_mut("cowsay").unwrap();
let mut wasm_features = Wasm::default();
wasm_features.enable_exceptions();
wasm_features.enable_multi_value();
wasm_features.enable_bulk_memory();
let wasm_value = ciborium::value::Value::serialized(&wasm_features).unwrap();
cowsay_atom.annotations = IndexMap::new();
cowsay_atom
.annotations
.insert(Wasm::KEY.to_string(), wasm_value);
let json = serde_json::to_string(&manifest).unwrap();
let deserialized_manifest: Manifest = serde_json::from_str(&json).unwrap();
let atom = deserialized_manifest.atoms.get("cowsay").unwrap();
let wasm = atom.wasm().unwrap().unwrap();
assert!(wasm.has_exceptions());
assert!(wasm.has_multi_value());
assert!(wasm.has_bulk_memory());
assert!(!wasm.has_simd());
let expected_manifest = serde_json::from_value::<Manifest>(serde_json::json!({
"package": {
"wapm": {
"name": "wiqar/cowsay",
"readme": {
"path": "README.md",
"volume": "metadata"
},
"version": "0.3.0",
"repository": "https://github.com/wapm-packages/cowsay",
"description": "cowsay is a program that generates ASCII pictures of a cow with a message"
}
},
"atoms": {
"cowsay": {
"kind": "https://webc.org/kind/wasm",
"signature": "sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=",
"annotations": {
"wasm": {
"features": ["exception-handling", "multi-value", "bulk-memory"]
}
}
}
},
"commands": {
"cowsay": {
"runner": "https://webc.org/runner/wasi",
"annotations": {
"wasi": {
"atom": "cowsay",
"package": null,
"main_args": null
}
}
},
"cowthink": {
"runner": "https://webc.org/runner/wasi",
"annotations": {
"wasi": {
"atom": "cowsay",
"package": null,
"main_args": null
}
}
}
}
})).unwrap();
let expected_atom = expected_manifest.atoms.get("cowsay").unwrap();
let expected_wasm = expected_atom.wasm().unwrap().unwrap();
assert_eq!(expected_wasm.features.len(), 3);
assert!(expected_wasm.has_exceptions());
assert!(expected_wasm.has_multi_value());
assert!(expected_wasm.has_bulk_memory());
}
}