Coverage Report

Created: 2026-08-15 07:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wasm-tools/crates/wit-parser/src/resolve/fs.rs
Line
Count
Source
1
//! Filesystem operations for [`Resolve`].
2
3
use alloc::format;
4
use std::path::Path;
5
use std::vec::Vec;
6
7
use anyhow::{Context, Result, bail};
8
9
use super::{PackageSources, Resolve};
10
use crate::{SourceMap, UnresolvedPackageGroup};
11
12
/// All the sources used during resolving a directory or path.
13
#[derive(Clone, Debug)]
14
pub struct PackageSourceMap {
15
    inner: PackageSources,
16
}
17
18
impl PackageSourceMap {
19
0
    fn from_single_source(package_id: super::PackageId, source: &Path) -> Result<Self> {
20
0
        let path_str = source
21
0
            .to_str()
22
0
            .ok_or_else(|| anyhow::anyhow!("path is not valid utf-8: {source:?}"))?;
23
0
        Ok(Self {
24
0
            inner: PackageSources::from_single_source(package_id, path_str),
25
0
        })
26
0
    }
27
28
0
    fn from_inner(inner: PackageSources) -> Self {
29
0
        Self { inner }
30
0
    }
31
32
    /// All unique source paths.
33
0
    pub fn paths(&self) -> impl Iterator<Item = &Path> {
34
0
        self.inner.source_names().map(Path::new)
35
0
    }
36
37
    /// Source paths for package
38
0
    pub fn package_paths(&self, id: super::PackageId) -> Option<impl Iterator<Item = &Path>> {
39
0
        self.inner
40
0
            .package_source_names(id)
41
0
            .map(|iter| iter.map(Path::new))
42
0
    }
43
}
44
45
enum ParsedFile {
46
    #[cfg(feature = "decoding")]
47
    Package(super::PackageId),
48
    Unresolved(UnresolvedPackageGroup),
49
}
50
51
impl Resolve {
52
    /// Parse WIT packages from the input `path`.
53
    ///
54
    /// The input `path` can be one of:
55
    ///
56
    /// * A directory containing a WIT package with an optional `deps` directory
57
    ///   for local dependencies. In this case `deps` is parsed first and then
58
    ///   the parent `path` is parsed and returned.
59
    /// * A single standalone WIT file.
60
    /// * A wasm-encoded WIT package as a single file in either the text or
61
    ///   binary format.
62
    ///
63
    /// More information can also be found at [`Resolve::push_dir`] and
64
    /// [`Resolve::push_file`].
65
0
    pub fn push_path(
66
0
        &mut self,
67
0
        path: impl AsRef<Path>,
68
0
    ) -> Result<(super::PackageId, PackageSourceMap)> {
69
0
        self._push_path(path.as_ref())
70
0
    }
71
72
0
    fn _push_path(&mut self, path: &Path) -> Result<(super::PackageId, PackageSourceMap)> {
73
0
        if path.is_dir() {
74
0
            self.push_dir(path).with_context(|| {
75
0
                format!(
76
                    "failed to resolve directory while parsing WIT for path [{}]",
77
0
                    path.display()
78
                )
79
0
            })
80
        } else {
81
0
            let id = self.push_file(path)?;
82
0
            Ok((id, PackageSourceMap::from_single_source(id, path)?))
83
        }
84
0
    }
85
86
    /// Parses the filesystem directory at `path` as a WIT package and returns
87
    /// a fully resolved [`super::PackageId`] list as a result.
88
    ///
89
    /// All `*.wit` files in `path` are parsed as a single package and then
90
    /// inserted into this `Resolve`. The `path` specified may have a `deps`
91
    /// subdirectory which is probed automatically for any other WIT
92
    /// dependencies.
93
    ///
94
    /// The `deps` folder may contain:
95
    ///
96
    /// * `$path/deps/my-package/*.wit` - a directory that may contain multiple
97
    ///   WIT files. The directory is parsed as a single package and then
98
    ///   inserted into this [`Resolve`]. Note that it cannot recursively
99
    ///   contain a `deps` directory.
100
    /// * `$path/deps/my-package.wit` - a single-file WIT package. This is
101
    ///   parsed with [`Resolve::push_file`] and then added to `self` for
102
    ///   name resolution.
103
    /// * `$path/deps/my-package.{wasm,wat}` - a wasm-encoded WIT package either
104
    ///   in the text for binary format.
105
    ///
106
    /// In all cases entries in the `deps` folder are added to `self` first
107
    /// before adding files found in `path` itself. All WIT packages found are
108
    /// candidates for name-based resolution that other packages may use.
109
    ///
110
    /// This function returns a tuple of two values. The first value is a
111
    /// [`super::PackageId`], which represents the main WIT package found within
112
    /// `path`. This argument is useful for passing to [`Resolve::select_world`]
113
    /// for choosing something to bindgen with.
114
    ///
115
    /// The second value returned is a [`PackageSourceMap`], which contains all the sources
116
    /// that were parsed during resolving. This can be useful for:
117
    /// * build systems that want to rebuild bindings whenever one of the files changed
118
    /// * or other tools, which want to identify the sources for the resolved packages
119
0
    pub fn push_dir(
120
0
        &mut self,
121
0
        path: impl AsRef<Path>,
122
0
    ) -> Result<(super::PackageId, PackageSourceMap)> {
123
0
        self._push_dir(path.as_ref())
124
0
    }
125
126
0
    fn _push_dir(&mut self, path: &Path) -> Result<(super::PackageId, PackageSourceMap)> {
127
0
        let top_pkg = self
128
0
            .parse_dir(path)
129
0
            .with_context(|| format!("failed to parse package: {}", path.display()))?;
130
0
        let deps = path.join("deps");
131
0
        let deps = self
132
0
            .parse_deps_dir(&deps)
133
0
            .with_context(|| format!("failed to parse dependency directory: {}", deps.display()))?;
134
135
0
        let (pkg_id, inner) = self.sort_unresolved_packages(top_pkg, deps)?;
136
0
        Ok((pkg_id, PackageSourceMap::from_inner(inner)))
137
0
    }
138
139
    /// Reads `*.wit` files from `path` into a [`SourceMap`] and parses them as
140
    /// an [`UnresolvedPackageGroup`]. See [`Resolve::parse_source_map`] for the
141
    /// failure-path semantics.
142
0
    fn parse_dir(&mut self, path: &Path) -> Result<UnresolvedPackageGroup> {
143
0
        let mut map = SourceMap::default();
144
0
        map.push_dir(path)?;
145
0
        self.parse_source_map(map)
146
0
    }
147
148
0
    fn parse_deps_dir(&mut self, path: &Path) -> Result<Vec<UnresolvedPackageGroup>> {
149
0
        let mut ret = Vec::new();
150
0
        if !path.exists() {
151
0
            return Ok(ret);
152
0
        }
153
0
        let mut entries = path
154
0
            .read_dir()
155
0
            .and_then(|i| i.collect::<std::io::Result<Vec<_>>>())
156
0
            .context("failed to read directory")?;
157
0
        entries.sort_by_key(|e| e.file_name());
158
0
        for dep in entries {
159
0
            let path = dep.path();
160
0
            let pkg = if dep.file_type()?.is_dir() || path.metadata()?.is_dir() {
161
                // If this entry is a directory or a symlink point to a
162
                // directory then always parse it as an `UnresolvedPackage`
163
                // since it's intentional to not support recursive `deps`
164
                // directories.
165
0
                self.parse_dir(&path)
166
0
                    .with_context(|| format!("failed to parse package: {}", path.display()))?
167
            } else {
168
                // If this entry is a file then we may want to ignore it but
169
                // this may also be a standalone WIT file or a `*.wasm` or
170
                // `*.wat` encoded package.
171
0
                let filename = dep.file_name();
172
0
                match Path::new(&filename).extension().and_then(|s| s.to_str()) {
173
0
                    Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? {
174
                        #[cfg(feature = "decoding")]
175
0
                        ParsedFile::Package(_) => continue,
176
0
                        ParsedFile::Unresolved(pkg) => pkg,
177
                    },
178
179
                    // Other files in deps dir are ignored for now to avoid
180
                    // accidentally including things like `.DS_Store` files in
181
                    // the call below to `parse_dir`.
182
0
                    _ => continue,
183
                }
184
            };
185
0
            ret.push(pkg);
186
        }
187
0
        Ok(ret)
188
0
    }
189
190
    /// Parses the contents of `path` from the filesystem and pushes the result
191
    /// into this `Resolve`.
192
    ///
193
    /// The `path` referenced here can be one of:
194
    ///
195
    /// * A WIT file. Note that in this case this single WIT file will be the
196
    ///   entire package and any dependencies it has must already be in `self`.
197
    /// * A WIT package encoded as WebAssembly, either in text or binary form.
198
    ///   In this the package and all of its dependencies are automatically
199
    ///   inserted into `self`.
200
    ///
201
    /// In both situations the `PackageId`s of the resulting resolved packages
202
    /// are returned from this method. The return value is mostly useful in
203
    /// conjunction with [`Resolve::select_world`].
204
0
    pub fn push_file(&mut self, path: impl AsRef<Path>) -> Result<super::PackageId> {
205
0
        match self._push_file(path.as_ref())? {
206
            #[cfg(feature = "decoding")]
207
0
            ParsedFile::Package(id) => Ok(id),
208
0
            ParsedFile::Unresolved(pkg) => Ok(self.push_group(pkg)?),
209
        }
210
0
    }
211
212
0
    fn _push_file(&mut self, path: &Path) -> Result<ParsedFile> {
213
0
        let contents = std::fs::read(path)
214
0
            .with_context(|| format!("failed to read path for WIT [{}]", path.display()))?;
215
216
        // If decoding is enabled at compile time then try to see if this is a
217
        // wasm file.
218
        #[cfg(feature = "decoding")]
219
        {
220
            use crate::decoding::{DecodedWasm, decode};
221
222
            #[cfg(feature = "wat")]
223
            let is_wasm = wat::Detect::from_bytes(&contents).is_wasm();
224
            #[cfg(not(feature = "wat"))]
225
0
            let is_wasm = wasmparser::Parser::is_component(&contents);
226
227
0
            if is_wasm {
228
                #[cfg(feature = "wat")]
229
                let contents = wat::parse_bytes(&contents).map_err(|mut e| {
230
                    e.set_path(path);
231
                    e
232
                })?;
233
234
0
                match decode(&contents)? {
235
                    DecodedWasm::Component(..) => {
236
0
                        bail!("found an actual component instead of an encoded WIT package in wasm")
237
                    }
238
0
                    DecodedWasm::WitPackage(resolve, pkg) => {
239
0
                        let remap = self.merge(resolve)?;
240
0
                        return Ok(ParsedFile::Package(remap.packages[pkg.index()]));
241
                    }
242
                }
243
0
            }
244
        }
245
246
        // If this wasn't a wasm file then assume it's a WIT file.
247
0
        let text = match core::str::from_utf8(&contents) {
248
0
            Ok(s) => s,
249
0
            Err(_) => bail!("input file is not valid utf-8 [{}]", path.display()),
250
        };
251
0
        let mut map = SourceMap::default();
252
0
        map.push(path, text);
253
0
        Ok(ParsedFile::Unresolved(self.parse_source_map(map)?))
254
0
    }
255
256
    /// Parses `contents` as a WIT package and pushes it into this `Resolve`.
257
    ///
258
    /// The `path` provided is used for error messages but otherwise is not
259
    /// read. This method does not touch the filesystem. The `contents` provided
260
    /// are the contents of a WIT package.
261
3.86k
    pub fn push_str(&mut self, path: impl AsRef<Path>, contents: &str) -> Result<super::PackageId> {
262
3.86k
        let path = path
263
3.86k
            .as_ref()
264
3.86k
            .to_str()
265
3.86k
            .ok_or_else(|| anyhow::anyhow!("path is not valid utf-8: {:?}", path.as_ref()))?;
Unexecuted instantiation: <wit_parser::resolve::Resolve>::push_str::<&alloc::string::String>::{closure#0}
Unexecuted instantiation: <wit_parser::resolve::Resolve>::push_str::<_>::{closure#0}
266
3.86k
        self.push_source(path, contents)
267
3.86k
    }
<wit_parser::resolve::Resolve>::push_str::<&alloc::string::String>
Line
Count
Source
261
3.86k
    pub fn push_str(&mut self, path: impl AsRef<Path>, contents: &str) -> Result<super::PackageId> {
262
3.86k
        let path = path
263
3.86k
            .as_ref()
264
3.86k
            .to_str()
265
3.86k
            .ok_or_else(|| anyhow::anyhow!("path is not valid utf-8: {:?}", path.as_ref()))?;
266
3.86k
        self.push_source(path, contents)
267
3.86k
    }
Unexecuted instantiation: <wit_parser::resolve::Resolve>::push_str::<_>
268
}