202 lines
7.0 KiB
Rust
202 lines
7.0 KiB
Rust
//! 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<WitDependency>,
|
|
}
|
|
|
|
/// 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<u8>,
|
|
}
|
|
|
|
/// 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<Path>) -> Result<BuiltWitPackage, WitPackageError> {
|
|
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<WitPackageMetadata, WitPackageError> {
|
|
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<WitPackageMetadata, WitPackageError> {
|
|
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::<Result<Vec<_>, 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(_)));
|
|
}
|
|
}
|