Skip to main content

cargo/sources/
directory.rs

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