Skip to main content

wasm_pkg_client/
decoded_component.rs

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    /// Like [`Self::from_publishing_source`] but overrides the derived
39    /// `(package, version)` identity with `package_override` when supplied.
40    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    /// Construct from a registry content stream. Callers already know the
53    /// `(package, version)` identity from the registry listing they followed
54    /// to get here, so we take it as input rather than re-deriving it from
55    /// the wasm metadata.
56    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    /// Check that `self` and `other` are semver-compatible neighbors in the
79    /// same cargo-`^` compatibility range.
80    pub fn semver_check(&self, other: &DecodedComponent) -> Result<(), Error> {
81        // `wit_component::semver_check` is asymmetric: its `new` may add
82        // imports / drop exports relative to its `prev`. To get a symmetric
83        // additive-only gate between two published versions we pass the
84        // newer-in-time release as `prev` and the older as `new`.
85        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                // World removal is considered a breaking change
97                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            // Merge resolves, remap merged resolve, check for incompatibility
105            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    // wit_component::decode_reader is CPU-bound sync work
160    // run it on the blocking pool so we don't stall an async worker thread
161    // see also: https://docs.rs/tokio/latest/tokio/index.html#cpu-bound-tasks-and-blocking-code
162    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
172/// Extract the package name and version from a decoded candidate.
173fn 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            // SAFETY: We just parsed this from wit and should be able to unwrap. If it
190            // isn't a valid identifier, something else is majorly wrong
191            (*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
224/// Borrow the inner `Resolve` and build an index of its worlds. The world
225/// names are borrowed from the `Resolve`, which outlives the returned index.
226fn 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}