Skip to main content

wasmer_wasix/runtime/resolver/
in_memory_source.rs

1use std::{
2    collections::{BTreeMap, HashMap, VecDeque},
3    fs::File,
4    path::{Path, PathBuf},
5};
6
7use anyhow::{Context, Error};
8use wasmer_config::package::{NamedPackageId, PackageHash, PackageId, PackageIdent, PackageSource};
9
10use crate::runtime::resolver::{PackageSummary, QueryError, Source};
11
12/// A [`Source`] that tracks packages in memory.
13///
14/// Primarily used during testing.
15#[derive(Debug, Default, Clone, PartialEq, Eq)]
16pub struct InMemorySource {
17    named_packages: BTreeMap<String, Vec<NamedPackageSummary>>,
18    hash_packages: HashMap<PackageHash, PackageSummary>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22struct NamedPackageSummary {
23    ident: NamedPackageId,
24    summary: PackageSummary,
25}
26
27impl InMemorySource {
28    pub fn new() -> Self {
29        InMemorySource::default()
30    }
31
32    /// Recursively walk a directory, adding all valid WEBC files to the source.
33    pub fn from_directory_tree(dir: impl Into<PathBuf>) -> Result<Self, Error> {
34        let mut source = InMemorySource::default();
35
36        let mut to_check: VecDeque<PathBuf> = VecDeque::new();
37        to_check.push_back(dir.into());
38
39        fn process_entry(
40            path: &Path,
41            source: &mut InMemorySource,
42            to_check: &mut VecDeque<PathBuf>,
43        ) -> Result<(), Error> {
44            let metadata = std::fs::metadata(path).context("Unable to get filesystem metadata")?;
45
46            if metadata.is_dir() {
47                for entry in path.read_dir().context("Unable to read the directory")? {
48                    to_check.push_back(entry?.path());
49                }
50            } else if metadata.is_file() {
51                let f = File::open(path).context("Unable to open the file")?;
52                if webc::detect(f).is_ok() {
53                    source
54                        .add_webc(path)
55                        .with_context(|| format!("Unable to load \"{}\"", path.display()))?;
56                }
57            }
58
59            Ok(())
60        }
61
62        while let Some(path) = to_check.pop_front() {
63            process_entry(&path, &mut source, &mut to_check)
64                .with_context(|| format!("Unable to add entries from \"{}\"", path.display()))?;
65        }
66
67        Ok(source)
68    }
69
70    /// Add a new [`PackageSummary`] to the [`InMemorySource`].
71    ///
72    /// Named packages are also made accessible by their hash.
73    pub fn add(&mut self, summary: PackageSummary) {
74        match summary.pkg.id.clone() {
75            PackageId::Named(ident) => {
76                // Also add the package as a hashed package.
77                let pkg_hash = PackageHash::Sha256(wasmer_config::hash::Sha256Hash(
78                    summary.dist.webc_sha256.as_bytes(),
79                ));
80                self.hash_packages
81                    .entry(pkg_hash)
82                    .or_insert_with(|| summary.clone());
83
84                // Add the named package.
85                let summaries = self
86                    .named_packages
87                    .entry(ident.full_name.clone())
88                    .or_default();
89                summaries.push(NamedPackageSummary { ident, summary });
90                summaries
91                    .sort_by(|left, right| left.ident.version.cmp_precedence(&right.ident.version));
92                summaries.dedup_by(|left, right| left.ident.version == right.ident.version);
93            }
94            PackageId::Hash(hash) => {
95                self.hash_packages.insert(hash, summary);
96            }
97        }
98    }
99
100    pub fn add_webc(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
101        let summary = PackageSummary::from_webc_file(path)?;
102        self.add(summary);
103
104        Ok(())
105    }
106
107    pub fn get(&self, id: &PackageId) -> Option<&PackageSummary> {
108        match id {
109            PackageId::Named(ident) => {
110                self.named_packages
111                    .get(&ident.full_name)
112                    .and_then(|summaries| {
113                        summaries
114                            .iter()
115                            .find(|s| s.ident.version == ident.version)
116                            .map(|s| &s.summary)
117                    })
118            }
119            PackageId::Hash(hash) => self.hash_packages.get(hash),
120        }
121    }
122
123    pub fn is_empty(&self) -> bool {
124        self.named_packages.is_empty() && self.hash_packages.is_empty()
125    }
126
127    /// Returns the number of packages in the source.
128    pub fn len(&self) -> usize {
129        // Only need to count the hash packages,
130        // as the named packages are also always added as hashed.
131        self.hash_packages.len()
132    }
133}
134
135#[async_trait::async_trait]
136impl Source for InMemorySource {
137    #[tracing::instrument(level = "debug", skip_all, fields(%package))]
138    async fn query(&self, package: &PackageSource) -> Result<Vec<PackageSummary>, QueryError> {
139        match package {
140            PackageSource::Ident(PackageIdent::Named(named)) => {
141                match self.named_packages.get(&named.full_name()) {
142                    Some(summaries) => {
143                        let matches: Vec<_> = summaries
144                            .iter()
145                            .filter(|summary| {
146                                named.version_or_default().matches(&summary.ident.version)
147                            })
148                            .map(|n| n.summary.clone())
149                            .collect();
150
151                        tracing::trace!(
152                            matches = ?matches
153                                .iter()
154                                .map(|summary| summary.pkg.id.to_string())
155                                .collect::<Vec<_>>(),
156                            "package resolution matches",
157                        );
158
159                        if matches.is_empty() {
160                            return Err(QueryError::NoMatches {
161                                query: package.clone(),
162                                archived_versions: Vec::new(),
163                            });
164                        }
165
166                        Ok(matches)
167                    }
168                    None => Err(QueryError::NotFound {
169                        query: package.clone(),
170                    }),
171                }
172            }
173            PackageSource::Ident(PackageIdent::Hash(hash)) => self
174                .hash_packages
175                .get(hash)
176                .map(|x| vec![x.clone()])
177                .ok_or_else(|| QueryError::NoMatches {
178                    query: package.clone(),
179                    archived_versions: Vec::new(),
180                }),
181            PackageSource::Url(_) | PackageSource::Path(_) => Err(QueryError::Unsupported {
182                query: package.clone(),
183            }),
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use tempfile::TempDir;
191
192    use crate::runtime::resolver::{
193        Dependency,
194        inputs::{DistributionInfo, PackageInfo},
195    };
196
197    use super::*;
198
199    const PYTHON: &[u8] = include_bytes!(concat!(
200        env!("CARGO_MANIFEST_DIR"),
201        "/../../wasmer-test-files/examples/python--python@3.13.5.webc"
202    ));
203    const COREUTILS_24: &[u8] = include_bytes!(concat!(
204        env!("CARGO_MANIFEST_DIR"),
205        "/../../wasmer-test-files/integration/webc/wasmer--coreutils@1.0.24.webc"
206    ));
207    const COREUTILS_25: &[u8] = include_bytes!(concat!(
208        env!("CARGO_MANIFEST_DIR"),
209        "/../../wasmer-test-files/integration/webc/wasmer--coreutils@1.0.25.webc"
210    ));
211    const BASH: &[u8] = include_bytes!(concat!(
212        env!("CARGO_MANIFEST_DIR"),
213        "/../../wasmer-test-files/integration/webc/wasmer--bash@1.0.25.webc"
214    ));
215
216    #[test]
217    fn load_a_directory_tree() {
218        let temp = TempDir::new().unwrap();
219        let nested = temp.path().join("nested");
220        std::fs::create_dir(&nested).unwrap();
221
222        let bash = nested.join("bash-1.0.25.webc");
223        let fixtures = [
224            (temp.path().join("python.webc"), PYTHON),
225            (temp.path().join("coreutils-1.0.24.webc"), COREUTILS_24),
226            (temp.path().join("coreutils-1.0.25.webc"), COREUTILS_25),
227            (bash.clone(), BASH),
228        ];
229        for (path, bytes) in &fixtures {
230            std::fs::write(path, bytes).unwrap();
231        }
232        std::fs::write(temp.path().join("not-a-webc.txt"), b"ignored").unwrap();
233
234        let source = InMemorySource::from_directory_tree(temp.path()).unwrap();
235
236        assert!(source.named_packages.is_empty());
237        assert_eq!(source.len(), fixtures.len());
238        for (path, bytes) in fixtures {
239            let webc_hash = crate::runtime::resolver::WebcHash::sha256(bytes);
240            let package_hash = PackageHash::from_sha256_bytes(webc_hash.0);
241            let summary = source.get(&PackageId::Hash(package_hash)).unwrap();
242
243            assert_eq!(summary.dist.webc_sha256, webc_hash);
244            assert_eq!(
245                summary.dist.webc,
246                crate::runtime::resolver::utils::url_from_file_path(path.canonicalize().unwrap())
247                    .unwrap(),
248            );
249        }
250
251        let bash_hash = crate::runtime::resolver::WebcHash::parse_hex(
252            "059606d132e2e6bc1afe3b432ee64dcb1b1b059815c8bb213cf3b24798ef21e1",
253        )
254        .unwrap();
255        let bash_id = PackageId::Hash(PackageHash::from_sha256_bytes(bash_hash.0));
256        assert_eq!(
257            source.get(&bash_id).unwrap(),
258            &PackageSummary {
259                pkg: PackageInfo {
260                    id: bash_id.clone(),
261                    dependencies: vec![Dependency {
262                        alias: "wasmer/coreutils".to_string(),
263                        pkg: "wasmer/coreutils@^1.0.19".parse().unwrap(),
264                    }],
265                    commands: vec![
266                        crate::runtime::resolver::Command {
267                            name: "bash".to_string(),
268                        },
269                        crate::runtime::resolver::Command {
270                            name: "sh".to_string(),
271                        },
272                    ],
273                    entrypoint: Some("bash".to_string()),
274                    filesystem: vec![],
275                },
276                dist: DistributionInfo {
277                    webc: crate::runtime::resolver::utils::url_from_file_path(
278                        bash.canonicalize().unwrap(),
279                    )
280                    .unwrap(),
281                    webc_sha256: bash_hash,
282                },
283            },
284        );
285    }
286}