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
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 pub fn len(&self) -> usize {
129 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}