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