Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ base64 = "0.22"
bytes = "1.11"
docker_credential = "1.3.2"
etcetera = "0.11"
futures-concurrency = "7.7"
futures-util = "0.3.30"
oci-client = { version = "0.16", default-features = false, features = [
"rustls-tls",
Expand All @@ -22,7 +23,7 @@ oci-wasm = { version = "0.4", default-features = false, features = [
"rustls-tls",
] }
rcgen = "0.14.8"
semver = "1.0.23"
semver = "1.0.28"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
Expand Down
4 changes: 3 additions & 1 deletion crates/wasm-pkg-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ base64 = { workspace = true }
bytes = { workspace = true }
docker_credential = { workspace = true }
etcetera = { workspace = true }
futures-concurrency = { workspace = true }
futures-util = { workspace = true, features = ["io"] }
oci-client = { workspace = true }
oci-wasm = { workspace = true }
Expand All @@ -46,7 +47,8 @@ warg-crypto = "0.9.2"
wasm-metadata = { workspace = true }
warg-protocol = "0.9.2"
wasm-pkg-common = { workspace = true, features = ["registry-config"] }
wit-component = { workspace = true }
wit-component = { workspace = true, features = ["semver-check"] }
wit-parser = { workspace = true }

[dev-dependencies]
rcgen = { workspace = true }
Expand Down
193 changes: 193 additions & 0 deletions crates/wasm-pkg-client/src/decoded_component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use crate::{ContentStream, PublishingSource};
use futures_util::TryStreamExt;
use std::io::Read;
use tokio::io::AsyncSeekExt;
use tokio_util::io::{StreamReader, SyncIoBridge};
use wasm_pkg_common::{
package::{PackageRef, Version},
Error,
};
use wit_component::DecodedWasm;

pub struct DecodedComponent {
version: Version,
package_ref: PackageRef,
decoded_wasm: DecodedWasm,
}

impl DecodedComponent {
pub async fn from_publishing_source(
data: PublishingSource,
) -> Result<(PublishingSource, DecodedComponent), Error> {
let (reader, decoded_wasm) = decode(SyncIoBridge::new(data)).await?;
let (package_ref, version) = extract_package_version(&decoded_wasm)?;

let mut data = reader.into_inner();
data.rewind().await?;

Ok((
data,
DecodedComponent {
version,
package_ref,
decoded_wasm,
},
))
}

/// Like [`Self::from_publishing_source`] but overrides the derived
/// `(package, version)` identity with `package_override` when supplied.
pub async fn from_publishing_source_with_package(
data: PublishingSource,
package_override: Option<(PackageRef, Version)>,
) -> Result<(PublishingSource, DecodedComponent), Error> {
let (data, mut decoded) = Self::from_publishing_source(data).await?;
if let Some((p, v)) = package_override {
decoded.package_ref = p;
decoded.version = v;
}
Ok((data, decoded))
}

/// Construct from a registry content stream. Callers already know the
/// `(package, version)` identity from the registry listing they followed
/// to get here, so we take it as input rather than re-deriving it from
/// the wasm metadata.
pub async fn from_content_stream(
stream: ContentStream,
package_ref: PackageRef,
version: Version,
) -> Result<DecodedComponent, Error> {
let reader = SyncIoBridge::new(StreamReader::new(stream.map_err(std::io::Error::other)));
let (_reader, decoded_wasm) = decode(reader).await?;
Ok(DecodedComponent {
version,
package_ref,
decoded_wasm,
})
}

pub fn version(&self) -> &Version {
&self.version
}

pub fn package(&self) -> &PackageRef {
&self.package_ref
}

/// Check that `self` and `other` are semver-compatible neighbors in the
/// same cargo-`^` compatibility range.
pub fn semver_check(&self, other: &DecodedComponent) -> Result<(), Error> {
// `wit_component::semver_check` is asymmetric: its `new` may add
// imports / drop exports relative to its `prev`. To get a symmetric
// additive-only gate between two published versions we pass the
// newer-in-time release as `prev` and the older as `new`.
let (older, newer) = if self.version < other.version {
(self, other)
} else {
(other, self)
};

let (prev_resolve, prev_world) = extract_resolve_and_world_id(&newer.decoded_wasm)?;
let (new_resolve, new_world) = extract_resolve_and_world_id(&older.decoded_wasm)?;

// Merge resolves, remap merged resolve, check for incompatibility
let mut merged = prev_resolve.clone();
let new_world = merged
.merge(new_resolve.clone())
.and_then(|remap| remap.map_world(new_world, None))
.map_err(Error::InvalidComponent)?;

wit_component::semver_check(merged, prev_world, new_world).map_err(|e| {
Error::SemverIncompatible {
previous: older.version.clone(),
new: newer.version.clone(),
source: e,
}
})
}
}

async fn decode<R>(reader: R) -> Result<(R, DecodedWasm), Error>
where
R: Read + Send + 'static,
{
tokio::task::spawn_blocking(move || {
let mut reader = reader;
let decoded_wasm =
wit_component::decode_reader(&mut reader).map_err(Error::InvalidComponent)?;
Ok::<_, Error>((reader, decoded_wasm))
})
.await
.map_err(|e| Error::IoError(std::io::Error::other(e)))?
}

/// Extract the package name and version from a decoded candidate.
fn extract_package_version(decoded: &DecodedWasm) -> Result<(PackageRef, Version), Error> {
let resolve = decoded.resolve();
let package_id = match decoded {
wit_component::DecodedWasm::Component(_, world_id) => {
resolve.worlds[*world_id].package.ok_or_else(|| {
crate::Error::InvalidComponent(anyhow::anyhow!(
"component world or package not found"
))
})?
}
wit_component::DecodedWasm::WitPackage(_, pkg) => *pkg,
};
let (package, version) = resolve
.package_names
.iter()
.find_map(|(pkg, id)| {
// SAFETY: We just parsed this from wit and should be able to unwrap. If it
// isn't a valid identifier, something else is majorly wrong
(*id == package_id).then(|| {
(
PackageRef::new(
pkg.namespace.clone().try_into().unwrap(),
pkg.name.clone().try_into().unwrap(),
),
pkg.version.clone(),
)
})
})
.ok_or_else(|| {
crate::Error::InvalidComponent(anyhow::anyhow!(
"component package {package_id:?} not found"
))
})?;

let version = version.ok_or_else(|| {
crate::Error::InvalidComponent(anyhow::anyhow!(
"component package version not found in the Wasm binary\n\
\n\
The Wasm file was built without a version in the WIT `package` statement.\n\
Add a version to the `package` statement in your .wit file, e.g.:\n\
\n\
\tpackage example:my-package@1.0.0;\n\
\n\
Alternatively, specify the package and version explicitly with the --package flag:\n\
\n\
\twkg publish <file> --package <namespace>:<name>@<version>"
))
})?;
Ok((package, version))
}

/// Borrow the inner `wit_parser::Resolve` and resolve a concrete `WorldId`.
/// For a decoded component the world is fixed; for a WIT package we ask
/// `Resolve::select_world` to pick one — deferred until needed so a
/// multi-world WIT package can publish its first version unambiguously.
fn extract_resolve_and_world_id(
decoded: &DecodedWasm,
) -> Result<(&wit_parser::Resolve, wit_parser::WorldId), Error> {
match decoded {
DecodedWasm::Component(resolve, world_id) => Ok((resolve, *world_id)),
DecodedWasm::WitPackage(resolve, pkg) => {
let world_id = resolve
.select_world(&[*pkg], None)
.map_err(Error::InvalidPackage)?;
Ok((resolve, world_id))
}
}
}
Loading
Loading