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, WebcHash,
194        inputs::{DistributionInfo, FileSystemMapping, 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-0.1.0.wasmer"
202    ));
203    const COREUTILS_16: &[u8] = include_bytes!(concat!(
204        env!("CARGO_MANIFEST_DIR"),
205        "/../../wasmer-test-files/integration/webc/coreutils-1.0.16-e27dbb4f-2ef2-4b44-b46a-ddd86497c6d7.webc"
206    ));
207    const COREUTILS_11: &[u8] = include_bytes!(concat!(
208        env!("CARGO_MANIFEST_DIR"),
209        "/../../wasmer-test-files/integration/webc/coreutils-1.0.11-9d7746ca-694f-11ed-b932-dead3543c068.webc"
210    ));
211    const BASH: &[u8] = include_bytes!(concat!(
212        env!("CARGO_MANIFEST_DIR"),
213        "/../../wasmer-test-files/integration/webc/bash-1.0.16-f097441a-a80b-4e0d-87d7-684918ef4bb6.webc"
214    ));
215
216    #[test]
217    fn load_a_directory_tree() {
218        let temp = TempDir::new().unwrap();
219        std::fs::write(temp.path().join("python-0.1.0.webc"), PYTHON).unwrap();
220        std::fs::write(temp.path().join("coreutils-1.0.16.webc"), COREUTILS_16).unwrap();
221        std::fs::write(temp.path().join("coreutils-1.0.11.webc"), COREUTILS_11).unwrap();
222        let nested = temp.path().join("nested");
223        std::fs::create_dir(&nested).unwrap();
224        let bash = nested.join("bash-1.0.12.webc");
225        std::fs::write(&bash, BASH).unwrap();
226
227        let source = InMemorySource::from_directory_tree(temp.path()).unwrap();
228
229        assert_eq!(
230            source
231                .named_packages
232                .keys()
233                .map(|k| k.as_str())
234                .collect::<Vec<_>>(),
235            ["python", "sharrattj/bash", "sharrattj/coreutils"]
236        );
237        assert_eq!(source.named_packages["sharrattj/coreutils"].len(), 2);
238        assert_eq!(
239            source.named_packages["sharrattj/bash"][0].summary,
240            PackageSummary {
241                pkg: PackageInfo {
242                    id: PackageId::Named(
243                        NamedPackageId::try_new("sharrattj/bash", "1.0.16").unwrap()
244                    ),
245                    dependencies: vec![Dependency {
246                        alias: "coreutils".to_string(),
247                        pkg: "sharrattj/coreutils@^1.0.16".parse().unwrap()
248                    }],
249                    commands: vec![crate::runtime::resolver::Command {
250                        name: "bash".to_string(),
251                    }],
252                    entrypoint: Some("bash".to_string()),
253                    filesystem: vec![FileSystemMapping {
254                        volume_name: "atom".to_string(),
255                        mount_path: "/".to_string(),
256                        original_path: Some("/".to_string()),
257                        dependency_name: None,
258                    }],
259                },
260                dist: DistributionInfo {
261                    webc: crate::runtime::resolver::utils::url_from_file_path(
262                        bash.canonicalize().unwrap()
263                    )
264                    .unwrap(),
265                    webc_sha256: WebcHash::from_bytes([
266                        161, 101, 23, 194, 244, 92, 186, 213, 143, 33, 200, 128, 238, 23, 185, 174,
267                        180, 195, 144, 145, 78, 17, 227, 159, 118, 64, 83, 153, 0, 205, 253, 215,
268                    ]),
269                },
270            }
271        );
272    }
273}