//! Standard binary WIT package encoding and inspection. //! //! Package identity is derived from the encoded WIT itself. Registry clients //! never submit a separate package name, version, or dependency manifest that //! could disagree with the artifact. use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; use wit_parser::{ Resolve, decoding::{DecodedWasm, decode}, }; /// Metadata schema returned by the Wasmeld WIT Registry API. pub const WIT_PACKAGE_SCHEMA_VERSION: u32 = 1; /// One exact, direct WIT package dependency. #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(deny_unknown_fields)] pub struct WitDependency { pub name: String, pub version: String, } /// Metadata derived from a binary WIT package. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct WitPackageMetadata { pub schema_version: u32, pub name: String, pub version: String, pub sha256: String, pub dependencies: Vec, } /// Encoded package bytes together with metadata derived from those bytes. #[derive(Clone, Debug, Eq, PartialEq)] pub struct BuiltWitPackage { pub metadata: WitPackageMetadata, pub bytes: Vec, } /// Errors raised while parsing WIT source or decoding a binary WIT package. #[derive(Debug, Error)] pub enum WitPackageError { #[error("failed to parse WIT package at {path}: {message}")] Source { path: PathBuf, message: String }, #[error("failed to encode WIT package: {0}")] Encode(String), #[error("invalid binary WIT package: {0}")] Decode(String), #[error("WIT package {0} must declare an explicit semantic version")] MissingVersion(String), } /// Parses a WIT source path and encodes its main package as binary WIT. /// /// The package must declare an explicit semantic version. Dependencies /// available through the source path are encoded as Component Model package /// references and reported in [`WitPackageMetadata::dependencies`]. pub fn build_wit_package(path: impl AsRef) -> Result { let path = path.as_ref(); let mut resolve = Resolve::default(); let (package, _) = resolve .push_path(path) .map_err(|error| WitPackageError::Source { path: path.to_path_buf(), message: error.to_string(), })?; let bytes = wit_component::encode(&resolve, package) .map_err(|error| WitPackageError::Encode(error.to_string()))?; let metadata = metadata_from_resolve(&resolve, package, &bytes)?; Ok(BuiltWitPackage { metadata, bytes }) } /// Decodes binary WIT and derives its identity, direct dependencies, and digest. /// /// Ordinary WebAssembly Components are rejected even though both formats use a /// Component Model binary container. pub fn inspect_wit_package(bytes: &[u8]) -> Result { let decoded = decode(bytes).map_err(|error| WitPackageError::Decode(error.to_string()))?; let DecodedWasm::WitPackage(resolve, package) = decoded else { return Err(WitPackageError::Decode( "artifact is a WebAssembly Component, not a binary WIT package".to_owned(), )); }; metadata_from_resolve(&resolve, package, bytes) } /// Returns the lowercase SHA-256 digest used as the Registry artifact identity. pub fn wit_package_sha256(bytes: &[u8]) -> String { format!("{:x}", Sha256::digest(bytes)) } fn metadata_from_resolve( resolve: &Resolve, package: wit_parser::PackageId, bytes: &[u8], ) -> Result { let package_name = &resolve.packages[package].name; let name = format!("{}:{}", package_name.namespace, package_name.name); let version = package_name .version .as_ref() .map(ToString::to_string) .ok_or_else(|| WitPackageError::MissingVersion(name.clone()))?; let mut dependencies = resolve .package_direct_deps(package) .map(|dependency| { let dependency = &resolve.packages[dependency].name; let dependency_name = format!("{}:{}", dependency.namespace, dependency.name); let dependency_version = dependency .version .as_ref() .map(ToString::to_string) .ok_or_else(|| WitPackageError::MissingVersion(dependency_name.clone()))?; Ok(WitDependency { name: dependency_name, version: dependency_version, }) }) .collect::, WitPackageError>>()?; dependencies.sort(); dependencies.dedup(); Ok(WitPackageMetadata { schema_version: WIT_PACKAGE_SCHEMA_VERSION, name, version, sha256: wit_package_sha256(bytes), dependencies, }) } #[cfg(test)] mod tests { use std::fs; use tempfile::tempdir; use super::*; #[test] fn builds_and_inspects_a_binary_wit_package() { let source = tempdir().unwrap(); fs::write( source.path().join("package.wit"), "package wasmeld:clock@1.2.3;\ninterface clock { now: func() -> u64; }\n", ) .unwrap(); let package = build_wit_package(source.path()).unwrap(); let inspected = inspect_wit_package(&package.bytes).unwrap(); assert_eq!(package.metadata, inspected); assert_eq!(inspected.name, "wasmeld:clock"); assert_eq!(inspected.version, "1.2.3"); assert!(inspected.dependencies.is_empty()); assert_eq!(inspected.sha256.len(), 64); } #[test] fn rejects_a_component_as_a_wit_package() { let error = inspect_wit_package(b"not wasm").unwrap_err(); assert!(matches!(error, WitPackageError::Decode(_))); } }