wac_resolver/
fs.rs

1use super::Error;
2use anyhow::{anyhow, Context, Result};
3use indexmap::IndexMap;
4use miette::SourceSpan;
5use std::{collections::HashMap, fs, path::PathBuf};
6use wac_types::BorrowedPackageKey;
7
8/// Used to resolve packages from the file system.
9pub struct FileSystemPackageResolver {
10    root: PathBuf,
11    overrides: HashMap<String, PathBuf>,
12    error_on_unknown: bool,
13}
14
15impl FileSystemPackageResolver {
16    /// Creates a new file system resolver with the given root directory.
17    pub fn new(
18        root: impl Into<PathBuf>,
19        overrides: HashMap<String, PathBuf>,
20        error_on_unknown: bool,
21    ) -> Self {
22        Self {
23            root: root.into(),
24            overrides,
25            error_on_unknown,
26        }
27    }
28
29    /// Resolves the provided package keys to packages.
30    pub fn resolve<'a>(
31        &self,
32        keys: &IndexMap<BorrowedPackageKey<'a>, SourceSpan>,
33    ) -> Result<IndexMap<BorrowedPackageKey<'a>, Vec<u8>>, Error> {
34        let mut packages = IndexMap::new();
35        for (key, span) in keys.iter() {
36            let path = match self.overrides.get(key.name) {
37                Some(path) if key.version.is_none() => {
38                    if !path.is_file() {
39                        return Err(Error::PackageResolutionFailure {
40                            name: key.name.to_string(),
41                            span: *span,
42                            source: anyhow!(
43                                "local path `{path}` for package `{name}` does not exist",
44                                path = path.display(),
45                                name = key.name
46                            ),
47                        });
48                    }
49
50                    path.clone()
51                }
52                _ => {
53                    let mut path = self.root.clone();
54                    for segment in key.name.split(':') {
55                        path.push(segment);
56                    }
57
58                    if let Some(version) = key.version {
59                        path.push(version.to_string());
60                    }
61
62                    // If the path is not a directory, use a `.wasm` or `.wat` extension
63                    if !path.is_dir() {
64                        append_extension(&mut path, "wasm");
65
66                        #[cfg(feature = "wat")]
67                        {
68                            path.set_extension("wat");
69                            if !path.exists() {
70                                path.set_extension("wasm");
71                            }
72                        }
73                    }
74
75                    path
76                }
77            };
78
79            // First check to see if a directory exists.
80            // If so, then treat it as a textual WIT package.
81            #[cfg(feature = "wit")]
82            {
83                let pkg_res_failure = |e| Error::PackageResolutionFailure {
84                    name: key.name.to_string(),
85                    span: *span,
86                    source: e,
87                };
88                let mut resolve = wit_parser::Resolve::new();
89                let pkg = if path.is_dir() {
90                    log::debug!(
91                        "loading WIT package from directory `{path}`",
92                        path = path.display()
93                    );
94
95                    let (pkg, _) = resolve.push_dir(&path).map_err(pkg_res_failure)?;
96                    Some(pkg)
97                } else if path.extension().and_then(std::ffi::OsStr::to_str) == Some("wit") {
98                    let pkg = resolve.push_file(&path).map_err(pkg_res_failure)?;
99                    Some(pkg)
100                } else {
101                    None
102                };
103                if let Some(pkg) = pkg {
104                    packages.insert(
105                        *key,
106                        wit_component::encode(&resolve, pkg)
107                            .with_context(|| {
108                                format!(
109                                    "failed to encode WIT package from `{path}`",
110                                    path = path.display()
111                                )
112                            })
113                            .map_err(pkg_res_failure)?,
114                    );
115
116                    continue;
117                }
118            }
119
120            if !path.is_file() {
121                log::debug!(
122                    "package `{key}` does not exist at `{path}`",
123                    path = path.display()
124                );
125                if self.error_on_unknown {
126                    return Err(Error::UnknownPackage {
127                        name: key.name.to_string(),
128                        span: *span,
129                    });
130                }
131                continue;
132            }
133
134            log::debug!(
135                "loading package `{key}` from `{path}`",
136                path = path.display()
137            );
138            let bytes = fs::read(&path)
139                .with_context(|| format!("failed to read package `{path}`", path = path.display()))
140                .map_err(|e| Error::PackageResolutionFailure {
141                    name: key.name.to_string(),
142                    span: *span,
143                    source: e,
144                })?;
145
146            #[cfg(feature = "wat")]
147            if path.extension().and_then(std::ffi::OsStr::to_str) == Some("wat") {
148                let bytes = match wat::parse_bytes(&bytes) {
149                    Ok(std::borrow::Cow::Borrowed(_)) => bytes,
150                    Ok(std::borrow::Cow::Owned(wat)) => wat,
151                    Err(mut e) => {
152                        e.set_path(path);
153                        return Err(Error::PackageResolutionFailure {
154                            name: key.name.to_string(),
155                            span: *span,
156                            source: anyhow!(e),
157                        });
158                    }
159                };
160
161                packages.insert(*key, bytes);
162                continue;
163            }
164
165            packages.insert(*key, bytes);
166        }
167
168        Ok(packages)
169    }
170}
171
172impl Default for FileSystemPackageResolver {
173    fn default() -> Self {
174        Self::new("deps", Default::default(), true)
175    }
176}
177
178/// Similar to Path::set_extension except it always appends.
179/// For example "0.0.1" -> "0.0.1.wasm" (instead of to "0.0.wasm").
180fn append_extension(path: &mut PathBuf, extension: &str) {
181    let os_str = path.as_mut_os_string();
182    os_str.push(".");
183    os_str.push(extension)
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn append_extension_if_no_existing_extension() {
192        let mut path = PathBuf::from("a/b/c");
193        append_extension(&mut path, "txt");
194        assert_eq!("a/b/c.txt", path.display().to_string());
195    }
196
197    #[test]
198    fn append_extension_if_existing_extension() {
199        let mut path = PathBuf::from("a/b/0.0.1");
200        append_extension(&mut path, "wasm");
201        assert_eq!("a/b/0.0.1.wasm", path.display().to_string());
202    }
203}