174 lines
5.8 KiB
Rust
174 lines
5.8 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 {
|
||
|
|
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<WitDependency>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Encoded package bytes together with metadata derived from those bytes.
|
||
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||
|
|
pub struct BuiltWitPackage {
|
||
|
|
pub metadata: WitPackageMetadata,
|
||
|
|
pub bytes: Vec<u8>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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<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.
|
||
|
|
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(_)));
|
||
|
|
}
|
||
|
|
}
|