//! 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 { /// Package identity in `namespace:name` form. pub name: String, /// Exact semantic version encoded by the dependency package. pub version: String, } /// Metadata derived from a binary WIT package. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct WitPackageMetadata { /// Metadata response schema used by the Registry API. pub schema_version: u32, /// Package identity derived from binary WIT contents. pub name: String, /// Explicit semantic version derived from binary WIT contents. pub version: String, /// SHA-256 digest of the complete binary WIT artifact. pub sha256: String, /// Exact direct dependencies embedded in the package. pub dependencies: Vec, } /// Encoded package bytes together with metadata derived from those bytes. #[derive(Clone, Debug, Eq, PartialEq)] pub struct BuiltWitPackage { /// Metadata derived by decoding the bytes after encoding. pub metadata: WitPackageMetadata, /// Standard Component Model binary WIT package. pub bytes: Vec, } /// Errors raised while parsing WIT source or decoding a binary WIT package. #[derive(Debug, Error)] pub enum WitPackageError { /// WIT source could not be parsed or resolved. #[error("failed to parse WIT package at {path}: {message}")] Source { /// Source file or directory passed to the parser. path: PathBuf, /// Parser or dependency-resolution detail. message: String, }, /// A resolved WIT package could not be encoded. #[error("failed to encode WIT package: {0}")] Encode(String), /// Input bytes are not a standard binary WIT package. #[error("invalid binary WIT package: {0}")] Decode(String), /// The package identity omits the version required by the Registry. #[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`]. /// /// # Errors /// /// Returns an error when WIT parsing, dependency resolution, binary encoding, /// or post-encode inspection fails. 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. /// /// # Errors /// /// Returns an error if the bytes are malformed, contain a Component instead of /// a WIT package, or any package in the direct dependency set lacks a version. 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(_))); } }