cargo/sources/
directory.rs1use 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
19pub struct DirectorySource<'gctx> {
58 source_id: SourceId,
60 root: PathBuf,
62 packages: HashMap<PackageId, (Package, Checksum)>,
64 gctx: &'gctx GlobalContext,
65 updated: bool,
66}
67
68#[derive(Deserialize)]
73#[serde(rename_all = "kebab-case")]
74struct Checksum {
75 package: Option<String>,
77 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 if let Some(s) = path.file_name().and_then(|s| s.to_str()) {
153 if s.starts_with('.') {
154 continue;
155 }
156 }
157
158 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 }
273
274 fn set_quiet(&mut self, _quiet: bool) {
275 }
277}