cargo/sources/
directory.rs

1use std::collections::HashMap;
2use std::fmt::{self, Debug, Formatter};
3use std::path::{Path, PathBuf};
4use std::task::Poll;
5
6use crate::core::{Dependency, Package, PackageId, SourceId};
7use crate::sources::source::MaybePackage;
8use crate::sources::source::QueryKind;
9use crate::sources::source::Source;
10use crate::sources::IndexSummary;
11use crate::sources::PathSource;
12use crate::util::errors::CargoResult;
13use crate::util::GlobalContext;
14
15use anyhow::Context as _;
16use cargo_util::{paths, Sha256};
17use serde::Deserialize;
18
19/// `DirectorySource` contains a number of crates on the file system. It was
20/// designed for representing vendored dependencies for `cargo vendor`.
21///
22/// `DirectorySource` at this moment is just a root directory containing other
23/// directories, which contain the source files of packages. Assumptions would
24/// be made to determine if a directory should be included as a package of a
25/// directory source's:
26///
27/// * Ignore directories starting with dot `.` (tend to be hidden).
28/// * Only when a `Cargo.toml` exists in a directory will it be included as
29///   a package. `DirectorySource` at this time only looks at one level of
30///   directories and never went deeper.
31/// * There must be a [`Checksum`] file `.cargo-checksum.json` file at the same
32///   level of `Cargo.toml` to ensure the integrity when a directory source was
33///   created (usually by `cargo vendor`). A failure to find or parse a single
34///   checksum results in a denial of loading any package in this source.
35/// * Otherwise, there is no other restrction of the name of directories. At
36///   this moment, it is `cargo vendor` that defines the layout and the name of
37///   each directory.
38///
39/// The file tree of a directory source may look like:
40///
41/// ```text
42/// [source root]
43/// ├── a-valid-crate/
44/// │  ├── src/
45/// │  ├── .cargo-checksum.json
46/// │  └── Cargo.toml
47/// ├── .ignored-a-dot-crate/
48/// │  ├── src/
49/// │  ├── .cargo-checksum.json
50/// │  └── Cargo.toml
51/// ├── skipped-no-manifest/
52/// │  ├── src/
53/// │  └── .cargo-checksum.json
54/// └── no-checksum-so-fails-the-entire-source-reading/
55///    └── Cargo.toml
56/// ```
57pub struct DirectorySource<'gctx> {
58    /// The unique identifier of this source.
59    source_id: SourceId,
60    /// The root path of this source.
61    root: PathBuf,
62    /// Packages that this sources has discovered.
63    packages: HashMap<PackageId, (Package, Checksum)>,
64    gctx: &'gctx GlobalContext,
65    updated: bool,
66}
67
68/// The checksum file to ensure the integrity of a package in a directory source.
69///
70/// The file name is simply `.cargo-checksum.json`. The checksum algorithm as
71/// of now is SHA256.
72#[derive(Deserialize)]
73#[serde(rename_all = "kebab-case")]
74struct Checksum {
75    /// Checksum of the package. Normally it is computed from the `.crate` file.
76    package: Option<String>,
77    /// Checksums of each source file.
78    files: HashMap<String, String>,
79}
80
81impl<'gctx> DirectorySource<'gctx> {
82    pub fn new(path: &Path, id: SourceId, gctx: &'gctx GlobalContext) -> DirectorySource<'gctx> {
83        DirectorySource {
84            source_id: id,
85            root: path.to_path_buf(),
86            gctx,
87            packages: HashMap::new(),
88            updated: false,
89        }
90    }
91}
92
93impl<'gctx> Debug for DirectorySource<'gctx> {
94    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
95        write!(f, "DirectorySource {{ root: {:?} }}", self.root)
96    }
97}
98
99impl<'gctx> Source for DirectorySource<'gctx> {
100    fn query(
101        &mut self,
102        dep: &Dependency,
103        kind: QueryKind,
104        f: &mut dyn FnMut(IndexSummary),
105    ) -> Poll<CargoResult<()>> {
106        if !self.updated {
107            return Poll::Pending;
108        }
109        let packages = self.packages.values().map(|p| &p.0);
110        let matches = packages.filter(|pkg| match kind {
111            QueryKind::Exact | QueryKind::RejectedVersions => dep.matches(pkg.summary()),
112            QueryKind::AlternativeNames => true,
113            QueryKind::Normalized => dep.matches(pkg.summary()),
114        });
115        for summary in matches.map(|pkg| pkg.summary().clone()) {
116            f(IndexSummary::Candidate(summary));
117        }
118        Poll::Ready(Ok(()))
119    }
120
121    fn supports_checksums(&self) -> bool {
122        true
123    }
124
125    fn requires_precise(&self) -> bool {
126        true
127    }
128
129    fn source_id(&self) -> SourceId {
130        self.source_id
131    }
132
133    fn block_until_ready(&mut self) -> CargoResult<()> {
134        if self.updated {
135            return Ok(());
136        }
137        self.packages.clear();
138        let entries = self.root.read_dir().with_context(|| {
139            format!(
140                "failed to read root of directory source: {}",
141                self.root.display()
142            )
143        })?;
144
145        for entry in entries {
146            let entry = entry?;
147            let path = entry.path();
148
149            // Ignore hidden/dot directories as they typically don't contain
150            // crates and otherwise may conflict with a VCS
151            // (rust-lang/cargo#3414).
152            if let Some(s) = path.file_name().and_then(|s| s.to_str()) {
153                if s.starts_with('.') {
154                    continue;
155                }
156            }
157
158            // Vendor directories are often checked into a VCS, but throughout
159            // the lifetime of a vendor dir crates are often added and deleted.
160            // Some VCS implementations don't always fully delete the directory
161            // when a dir is removed from a different checkout. Sometimes a
162            // mostly-empty dir is left behind.
163            //
164            // Additionally vendor directories are sometimes accompanied with
165            // readme files and other auxiliary information not too interesting
166            // to Cargo.
167            //
168            // To help handle all this we only try processing folders with a
169            // `Cargo.toml` in them. This has the upside of being pretty
170            // flexible with the contents of vendor directories but has the
171            // downside of accidentally misconfigured vendor directories
172            // silently returning less crates.
173            if !path.join("Cargo.toml").exists() {
174                continue;
175            }
176
177            let mut src = PathSource::new(&path, self.source_id, self.gctx);
178            src.load()?;
179            let mut pkg = src.root_package()?;
180
181            let cksum_file = path.join(".cargo-checksum.json");
182            let cksum = paths::read(&path.join(cksum_file)).with_context(|| {
183                format!(
184                    "failed to load checksum `.cargo-checksum.json` \
185                     of {} v{}",
186                    pkg.package_id().name(),
187                    pkg.package_id().version()
188                )
189            })?;
190            let cksum: Checksum = serde_json::from_str(&cksum).with_context(|| {
191                format!(
192                    "failed to decode `.cargo-checksum.json` of \
193                     {} v{}",
194                    pkg.package_id().name(),
195                    pkg.package_id().version()
196                )
197            })?;
198
199            if let Some(package) = &cksum.package {
200                pkg.manifest_mut()
201                    .summary_mut()
202                    .set_checksum(package.clone());
203            }
204            self.packages.insert(pkg.package_id(), (pkg, cksum));
205        }
206
207        self.updated = true;
208        Ok(())
209    }
210
211    fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
212        self.packages
213            .get(&id)
214            .map(|p| &p.0)
215            .cloned()
216            .map(MaybePackage::Ready)
217            .ok_or_else(|| anyhow::format_err!("failed to find package with id: {}", id))
218    }
219
220    fn finish_download(&mut self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
221        panic!("no downloads to do")
222    }
223
224    fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
225        Ok(pkg.package_id().version().to_string())
226    }
227
228    fn verify(&self, id: PackageId) -> CargoResult<()> {
229        let Some((pkg, cksum)) = self.packages.get(&id) else {
230            anyhow::bail!("failed to find entry for `{}` in directory source", id);
231        };
232
233        for (file, cksum) in cksum.files.iter() {
234            let file = pkg.root().join(file);
235            let actual = Sha256::new()
236                .update_path(&file)
237                .with_context(|| format!("failed to calculate checksum of: {}", file.display()))?
238                .finish_hex();
239            if &*actual != cksum {
240                anyhow::bail!(
241                    "the listed checksum of `{}` has changed:\n\
242                     expected: {}\n\
243                     actual:   {}\n\
244                     \n\
245                     directory sources are not intended to be edited, if \
246                     modifications are required then it is recommended \
247                     that `[patch]` is used with a forked copy of the \
248                     source\
249                     ",
250                    file.display(),
251                    cksum,
252                    actual
253                );
254            }
255        }
256
257        Ok(())
258    }
259
260    fn describe(&self) -> String {
261        format!("directory source `{}`", self.root.display())
262    }
263
264    fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) {}
265
266    fn is_yanked(&mut self, _pkg: PackageId) -> Poll<CargoResult<bool>> {
267        Poll::Ready(Ok(false))
268    }
269
270    fn invalidate_cache(&mut self) {
271        // Directory source has no local cache.
272    }
273
274    fn set_quiet(&mut self, _quiet: bool) {
275        // Directory source does not display status
276    }
277}