1use crate::{ContentStream, PublishingSource};
2use futures_util::TryStreamExt;
3use std::io::Read;
4use tokio::io::AsyncSeekExt;
5use tokio_util::io::{StreamReader, SyncIoBridge};
6use wasm_pkg_common::{
7 Error,
8 package::{PackageRef, Version},
9};
10use wit_component::DecodedWasm;
11
12pub struct DecodedComponent {
13 version: Version,
14 package_ref: PackageRef,
15 decoded_wasm: DecodedWasm,
16}
17
18impl DecodedComponent {
19 pub async fn from_publishing_source(
20 data: PublishingSource,
21 ) -> Result<(PublishingSource, DecodedComponent), Error> {
22 let (reader, decoded_wasm) = decode(SyncIoBridge::new(data)).await?;
23 let (package_ref, version) = extract_package_version(&decoded_wasm)?;
24
25 let mut data = reader.into_inner();
26 data.rewind().await?;
27
28 Ok((
29 data,
30 DecodedComponent {
31 version,
32 package_ref,
33 decoded_wasm,
34 },
35 ))
36 }
37
38 pub async fn from_publishing_source_with_package(
41 data: PublishingSource,
42 package_override: Option<(PackageRef, Version)>,
43 ) -> Result<(PublishingSource, DecodedComponent), Error> {
44 let (data, mut decoded) = Self::from_publishing_source(data).await?;
45 if let Some((p, v)) = package_override {
46 decoded.package_ref = p;
47 decoded.version = v;
48 }
49 Ok((data, decoded))
50 }
51
52 pub async fn from_content_stream(
57 stream: ContentStream,
58 package_ref: PackageRef,
59 version: Version,
60 ) -> Result<DecodedComponent, Error> {
61 let reader = SyncIoBridge::new(StreamReader::new(stream.map_err(std::io::Error::other)));
62 let (_reader, decoded_wasm) = decode(reader).await?;
63 Ok(DecodedComponent {
64 version,
65 package_ref,
66 decoded_wasm,
67 })
68 }
69
70 pub fn version(&self) -> &Version {
71 &self.version
72 }
73
74 pub fn package(&self) -> &PackageRef {
75 &self.package_ref
76 }
77
78 pub fn semver_check(&self, other: &DecodedComponent) -> Result<(), Error> {
81 let (older, newer) = if self.version < other.version {
86 (self, other)
87 } else {
88 (other, self)
89 };
90
91 let (prev_resolve, prev_worlds) = extract_resolve_and_worlds(&newer.decoded_wasm);
92 let (new_resolve, new_worlds) = extract_resolve_and_worlds(&older.decoded_wasm);
93
94 for (name, new_world) in new_worlds {
95 let Some(&prev_world) = prev_worlds.get(&name) else {
96 return Err(Error::SemverIncompatible {
98 previous: older.version.clone(),
99 new: newer.version.clone(),
100 source: anyhow::anyhow!("world `{name}` was removed"),
101 });
102 };
103
104 let mut merged = prev_resolve.clone();
106 let new_world = match merged
107 .merge(new_resolve.clone())
108 .map(|remap| remap.map_world(new_world, wit_parser::Span::default()))
109 {
110 Ok(Ok(w)) => w,
111 Ok(Err(e)) => {
112 return Err(Error::InvalidComponent(anyhow::format_err!(
113 "failed to remap merged worlds: {}",
114 e.kind()
115 )));
116 }
117 Err(e) => {
118 return Err(Error::InvalidComponent(e));
119 }
120 };
121
122 wit_component::semver_check(merged, prev_world, new_world).map_err(|e| {
123 Error::SemverIncompatible {
124 previous: older.version.clone(),
125 new: newer.version.clone(),
126 source: e.context(format!("world `{name}`")),
127 }
128 })?;
129 }
130
131 Ok(())
132 }
133}
134
135impl PartialEq for DecodedComponent {
136 fn eq(&self, other: &Self) -> bool {
137 self.package_ref == other.package_ref && self.version == other.version
138 }
139}
140
141impl Eq for DecodedComponent {}
142
143impl PartialOrd for DecodedComponent {
144 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
145 Some(self.cmp(other))
146 }
147}
148
149impl Ord for DecodedComponent {
150 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
151 (&self.package_ref, &self.version).cmp(&(&other.package_ref, &other.version))
152 }
153}
154
155async fn decode<R>(reader: R) -> Result<(R, DecodedWasm), Error>
156where
157 R: Read + Send + 'static,
158{
159 tokio::task::spawn_blocking(move || {
163 let mut reader = reader;
164 let decoded_wasm =
165 wit_component::decode_reader(&mut reader).map_err(Error::InvalidComponent)?;
166 Ok::<_, Error>((reader, decoded_wasm))
167 })
168 .await
169 .map_err(|e| Error::IoError(std::io::Error::other(e)))?
170}
171
172fn extract_package_version(decoded: &DecodedWasm) -> Result<(PackageRef, Version), Error> {
174 let resolve = decoded.resolve();
175 let package_id = match decoded {
176 wit_component::DecodedWasm::Component(_, world_id) => {
177 resolve.worlds[*world_id].package.ok_or_else(|| {
178 crate::Error::InvalidComponent(anyhow::anyhow!(
179 "component world or package not found"
180 ))
181 })?
182 }
183 wit_component::DecodedWasm::WitPackage(_, pkg) => *pkg,
184 };
185 let (package, version) = resolve
186 .package_names
187 .iter()
188 .find_map(|(pkg, id)| {
189 (*id == package_id).then(|| {
192 (
193 PackageRef::new(
194 pkg.namespace.clone().try_into().unwrap(),
195 pkg.name.clone().try_into().unwrap(),
196 ),
197 pkg.version.clone(),
198 )
199 })
200 })
201 .ok_or_else(|| {
202 crate::Error::InvalidComponent(anyhow::anyhow!(
203 "component package {package_id:?} not found"
204 ))
205 })?;
206
207 let version = version.ok_or_else(|| {
208 crate::Error::InvalidComponent(anyhow::anyhow!(
209 "component package version not found in the Wasm binary\n\
210 \n\
211 The Wasm file was built without a version in the WIT `package` statement.\n\
212 Add a version to the `package` statement in your .wit file, e.g.:\n\
213 \n\
214 \tpackage example:my-package@1.0.0;\n\
215 \n\
216 Alternatively, specify the package and version explicitly with the --package flag:\n\
217 \n\
218 \twkg publish <file> --package <namespace>:<name>@<version>"
219 ))
220 })?;
221 Ok((package, version))
222}
223
224fn extract_resolve_and_worlds(
227 decoded: &DecodedWasm,
228) -> (
229 &wit_parser::Resolve,
230 std::collections::HashMap<&str, wit_parser::WorldId>,
231) {
232 match decoded {
233 DecodedWasm::Component(resolve, world_id) => {
234 let name = resolve.worlds[*world_id].name.as_str();
235 (resolve, std::iter::once((name, *world_id)).collect())
236 }
237 DecodedWasm::WitPackage(resolve, pkg) => {
238 let worlds = resolve.packages[*pkg]
239 .worlds
240 .iter()
241 .map(|(name, id)| (name.as_str(), *id))
242 .collect();
243 (resolve, worlds)
244 }
245 }
246}