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.sort_by(|left, right| left.ident.version.cmp(&right.ident.version));
91                summaries.dedup_by(|left, right| left.ident.version == right.ident.version);
92            }
93            PackageId::Hash(hash) => {
94                self.hash_packages.insert(hash, summary);
95            }
96        }
97    }
98
99    pub fn add_webc(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
100        let summary = PackageSummary::from_webc_file(path)?;
101        self.add(summary);
102
103        Ok(())
104    }
105
106    pub fn get(&self, id: &PackageId) -> Option<&PackageSummary> {
107        match id {
108            PackageId::Named(ident) => {
109                self.named_packages
110                    .get(&ident.full_name)
111                    .and_then(|summaries| {
112                        summaries
113                            .iter()
114                            .find(|s| s.ident.version == ident.version)
115                            .map(|s| &s.summary)
116                    })
117            }
118            PackageId::Hash(hash) => self.hash_packages.get(hash),
119        }
120    }
121
122    pub fn is_empty(&self) -> bool {
123        self.named_packages.is_empty() && self.hash_packages.is_empty()
124    }
125
126    /// Returns the number of packages in the source.
127    pub fn len(&self) -> usize {
128        // Only need to count the hash packages,
129        // as the named packages are also always added as hashed.
130        self.hash_packages.len()
131    }
132}
133
134#[async_trait::async_trait]
135impl Source for InMemorySource {
136    #[tracing::instrument(level = "debug", skip_all, fields(%package))]
137    async fn query(&self, package: &PackageSource) -> Result<Vec<PackageSummary>, QueryError> {
138        match package {
139            PackageSource::Ident(PackageIdent::Named(named)) => {
140                match self.named_packages.get(&named.full_name()) {
141                    Some(summaries) => {
142                        let matches: Vec<_> = summaries
143                            .iter()
144                            .filter(|summary| {
145                                named.version_or_default().matches(&summary.ident.version)
146                            })
147                            .map(|n| n.summary.clone())
148                            .collect();
149
150                        tracing::trace!(
151                            matches = ?matches
152                                .iter()
153                                .map(|summary| summary.pkg.id.to_string())
154                                .collect::<Vec<_>>(),
155                            "package resolution matches",
156                        );
157
158                        if matches.is_empty() {
159                            return Err(QueryError::NoMatches {
160                                query: package.clone(),
161                                archived_versions: Vec::new(),
162                            });
163                        }
164
165                        Ok(matches)
166                    }
167                    None => Err(QueryError::NotFound {
168                        query: package.clone(),
169                    }),
170                }
171            }
172            PackageSource::Ident(PackageIdent::Hash(hash)) => self
173                .hash_packages
174                .get(hash)
175                .map(|x| vec![x.clone()])
176                .ok_or_else(|| QueryError::NoMatches {
177                    query: package.clone(),
178                    archived_versions: Vec::new(),
179                }),
180            PackageSource::Url(_) | PackageSource::Path(_) => Err(QueryError::Unsupported {
181                query: package.clone(),
182            }),
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use tempfile::TempDir;
190
191    use crate::runtime::resolver::{
192        Dependency, WebcHash,
193        inputs::{DistributionInfo, FileSystemMapping, PackageInfo},
194    };
195
196    use super::*;
197
198    const PYTHON: &[u8] = include_bytes!(concat!(
199        env!("CARGO_MANIFEST_DIR"),
200        "/../../wasmer-test-files/examples/python-0.1.0.wasmer"
201    ));
202    const COREUTILS_16: &[u8] = include_bytes!(concat!(
203        env!("CARGO_MANIFEST_DIR"),
204        "/../../wasmer-test-files/integration/webc/coreutils-1.0.16-e27dbb4f-2ef2-4b44-b46a-ddd86497c6d7.webc"
205    ));
206    const COREUTILS_11: &[u8] = include_bytes!(concat!(
207        env!("CARGO_MANIFEST_DIR"),
208        "/../../wasmer-test-files/integration/webc/coreutils-1.0.11-9d7746ca-694f-11ed-b932-dead3543c068.webc"
209    ));
210    const BASH: &[u8] = include_bytes!(concat!(
211        env!("CARGO_MANIFEST_DIR"),
212        "/../../wasmer-test-files/integration/webc/bash-1.0.16-f097441a-a80b-4e0d-87d7-684918ef4bb6.webc"
213    ));
214
215    #[test]
216    fn load_a_directory_tree() {
217        let temp = TempDir::new().unwrap();
218        std::fs::write(temp.path().join("python-0.1.0.webc"), PYTHON).unwrap();
219        std::fs::write(temp.path().join("coreutils-1.0.16.webc"), COREUTILS_16).unwrap();
220        std::fs::write(temp.path().join("coreutils-1.0.11.webc"), COREUTILS_11).unwrap();
221        let nested = temp.path().join("nested");
222        std::fs::create_dir(&nested).unwrap();
223        let bash = nested.join("bash-1.0.12.webc");
224        std::fs::write(&bash, BASH).unwrap();
225
226        let source = InMemorySource::from_directory_tree(temp.path()).unwrap();
227
228        assert_eq!(
229            source
230                .named_packages
231                .keys()
232                .map(|k| k.as_str())
233                .collect::<Vec<_>>(),
234            ["python", "sharrattj/bash", "sharrattj/coreutils"]
235        );
236        assert_eq!(source.named_packages["sharrattj/coreutils"].len(), 2);
237        assert_eq!(
238            source.named_packages["sharrattj/bash"][0].summary,
239            PackageSummary {
240                pkg: PackageInfo {
241                    id: PackageId::Named(
242                        NamedPackageId::try_new("sharrattj/bash", "1.0.16").unwrap()
243                    ),
244                    dependencies: vec![Dependency {
245                        alias: "coreutils".to_string(),
246                        pkg: "sharrattj/coreutils@^1.0.16".parse().unwrap()
247                    }],
248                    commands: vec![crate::runtime::resolver::Command {
249                        name: "bash".to_string(),
250                    }],
251                    entrypoint: Some("bash".to_string()),
252                    filesystem: vec![FileSystemMapping {
253                        volume_name: "atom".to_string(),
254                        mount_path: "/".to_string(),
255                        original_path: Some("/".to_string()),
256                        dependency_name: None,
257                    }],
258                },
259                dist: DistributionInfo {
260                    webc: crate::runtime::resolver::utils::url_from_file_path(
261                        bash.canonicalize().unwrap()
262                    )
263                    .unwrap(),
264                    webc_sha256: WebcHash::from_bytes([
265                        161, 101, 23, 194, 244, 92, 186, 213, 143, 33, 200, 128, 238, 23, 185, 174,
266                        180, 195, 144, 145, 78, 17, 227, 159, 118, 64, 83, 153, 0, 205, 253, 215,
267                    ]),
268                },
269            }
270        );
271    }
272}