cargo/sources/
directory.rs

1use std::collections::HashMap;
2use std::fmt::{self, Debug, Formatter};
3use std::path::{Path, PathBuf};
4
5use serde::Deserialize;
6
7use crate::core::source::MaybePackage;
8use crate::core::{Dependency, Package, PackageId, Source, SourceId, Summary};
9use crate::sources::PathSource;
10use crate::util::errors::{CargoResult, CargoResultExt};
11use crate::util::paths;
12use crate::util::{Config, Sha256};
13
14pub struct DirectorySource<'cfg> {
15    source_id: SourceId,
16    root: PathBuf,
17    packages: HashMap<PackageId, (Package, Checksum)>,
18    config: &'cfg Config,
19}
20
21#[derive(Deserialize)]
22struct Checksum {
23    package: Option<String>,
24    files: HashMap<String, String>,
25}
26
27impl<'cfg> DirectorySource<'cfg> {
28    pub fn new(path: &Path, id: SourceId, config: &'cfg Config) -> DirectorySource<'cfg> {
29        DirectorySource {
30            source_id: id,
31            root: path.to_path_buf(),
32            config,
33            packages: HashMap::new(),
34        }
35    }
36}
37
38impl<'cfg> Debug for DirectorySource<'cfg> {
39    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
40        write!(f, "DirectorySource {{ root: {:?} }}", self.root)
41    }
42}
43
44impl<'cfg> Source for DirectorySource<'cfg> {
45    fn query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
46        let packages = self.packages.values().map(|p| &p.0);
47        let matches = packages.filter(|pkg| dep.matches(pkg.summary()));
48        for summary in matches.map(|pkg| pkg.summary().clone()) {
49            f(summary);
50        }
51        Ok(())
52    }
53
54    fn fuzzy_query(&mut self, _dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
55        let packages = self.packages.values().map(|p| &p.0);
56        for summary in packages.map(|pkg| pkg.summary().clone()) {
57            f(summary);
58        }
59        Ok(())
60    }
61
62    fn supports_checksums(&self) -> bool {
63        true
64    }
65
66    fn requires_precise(&self) -> bool {
67        true
68    }
69
70    fn source_id(&self) -> SourceId {
71        self.source_id
72    }
73
74    fn update(&mut self) -> CargoResult<()> {
75        self.packages.clear();
76        let entries = self.root.read_dir().chain_err(|| {
77            format!(
78                "failed to read root of directory source: {}",
79                self.root.display()
80            )
81        })?;
82
83        for entry in entries {
84            let entry = entry?;
85            let path = entry.path();
86
87            // Ignore hidden/dot directories as they typically don't contain
88            // crates and otherwise may conflict with a VCS
89            // (rust-lang/cargo#3414).
90            if let Some(s) = path.file_name().and_then(|s| s.to_str()) {
91                if s.starts_with('.') {
92                    continue;
93                }
94            }
95
96            // Vendor directories are often checked into a VCS, but throughout
97            // the lifetime of a vendor dir crates are often added and deleted.
98            // Some VCS implementations don't always fully delete the directory
99            // when a dir is removed from a different checkout. Sometimes a
100            // mostly-empty dir is left behind.
101            //
102            // Additionally vendor directories are sometimes accompanied with
103            // readme files and other auxiliary information not too interesting
104            // to Cargo.
105            //
106            // To help handle all this we only try processing folders with a
107            // `Cargo.toml` in them. This has the upside of being pretty
108            // flexible with the contents of vendor directories but has the
109            // downside of accidentally misconfigured vendor directories
110            // silently returning less crates.
111            if !path.join("Cargo.toml").exists() {
112                continue;
113            }
114
115            let mut src = PathSource::new(&path, self.source_id, self.config);
116            src.update()?;
117            let mut pkg = src.root_package()?;
118
119            let cksum_file = path.join(".cargo-checksum.json");
120            let cksum = paths::read(&path.join(cksum_file)).chain_err(|| {
121                format!(
122                    "failed to load checksum `.cargo-checksum.json` \
123                     of {} v{}",
124                    pkg.package_id().name(),
125                    pkg.package_id().version()
126                )
127            })?;
128            let cksum: Checksum = serde_json::from_str(&cksum).chain_err(|| {
129                format!(
130                    "failed to decode `.cargo-checksum.json` of \
131                     {} v{}",
132                    pkg.package_id().name(),
133                    pkg.package_id().version()
134                )
135            })?;
136
137            if let Some(package) = &cksum.package {
138                pkg.manifest_mut()
139                    .summary_mut()
140                    .set_checksum(package.clone());
141            }
142            self.packages.insert(pkg.package_id(), (pkg, cksum));
143        }
144
145        Ok(())
146    }
147
148    fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
149        self.packages
150            .get(&id)
151            .map(|p| &p.0)
152            .cloned()
153            .map(MaybePackage::Ready)
154            .ok_or_else(|| anyhow::format_err!("failed to find package with id: {}", id))
155    }
156
157    fn finish_download(&mut self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
158        panic!("no downloads to do")
159    }
160
161    fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
162        Ok(pkg.package_id().version().to_string())
163    }
164
165    fn verify(&self, id: PackageId) -> CargoResult<()> {
166        let (pkg, cksum) = match self.packages.get(&id) {
167            Some(&(ref pkg, ref cksum)) => (pkg, cksum),
168            None => anyhow::bail!("failed to find entry for `{}` in directory source", id),
169        };
170
171        for (file, cksum) in cksum.files.iter() {
172            let file = pkg.root().join(file);
173            let actual = Sha256::new()
174                .update_path(&file)
175                .chain_err(|| format!("failed to calculate checksum of: {}", file.display()))?
176                .finish_hex();
177            if &*actual != cksum {
178                anyhow::bail!(
179                    "the listed checksum of `{}` has changed:\n\
180                     expected: {}\n\
181                     actual:   {}\n\
182                     \n\
183                     directory sources are not intended to be edited, if \
184                     modifications are required then it is recommended \
185                     that [replace] is used with a forked copy of the \
186                     source\
187                     ",
188                    file.display(),
189                    cksum,
190                    actual
191                );
192            }
193        }
194
195        Ok(())
196    }
197
198    fn describe(&self) -> String {
199        format!("directory source `{}`", self.root.display())
200    }
201
202    fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) {}
203
204    fn is_yanked(&mut self, _pkg: PackageId) -> CargoResult<bool> {
205        Ok(false)
206    }
207}