wasmer_wasix/runtime/resolver/
in_memory_source.rs1use 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#[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 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 pub fn add(&mut self, summary: PackageSummary) {
74 match summary.pkg.id.clone() {
75 PackageId::Named(ident) => {
76 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 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 pub fn len(&self) -> usize {
128 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}