cargo/sources/
directory.rs1use std::cell::{Cell, RefCell};
2use std::collections::HashMap;
3use std::fmt::{self, Debug, Formatter};
4use std::path::{Path, PathBuf};
5
6use crate::core::{Dependency, Package, PackageId, SourceId};
7use crate::sources::IndexSummary;
8use crate::sources::PathSource;
9use crate::sources::source::MaybePackage;
10use crate::sources::source::QueryKind;
11use crate::sources::source::Source;
12use crate::util::GlobalContext;
13use crate::util::errors::CargoResult;
14
15use anyhow::Context as _;
16use cargo_util::{Sha256, paths};
17use serde::Deserialize;
18
19pub struct DirectorySource<'gctx> {
58 source_id: SourceId,
60 root: PathBuf,
62 packages: RefCell<HashMap<PackageId, (Package, Checksum)>>,
64 gctx: &'gctx GlobalContext,
65 updated: Cell<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: RefCell::new(HashMap::new()),
88 updated: Cell::new(false),
89 }
90 }
91
92 fn update(&self) -> CargoResult<()> {
93 if self.updated.get() {
94 return Ok(());
95 }
96 self.packages.borrow_mut().clear();
97 let entries = self.root.read_dir().with_context(|| {
98 format!(
99 "failed to read root of directory source: {}",
100 self.root.display()
101 )
102 })?;
103
104 for entry in entries {
105 let entry = entry?;
106 let path = entry.path();
107
108 if let Some(s) = path.file_name().and_then(|s| s.to_str()) {
112 if s.starts_with('.') {
113 continue;
114 }
115 }
116
117 if !path.join("Cargo.toml").exists() {
133 continue;
134 }
135
136 let mut src = PathSource::new(&path, self.source_id, self.gctx);
137 src.load()?;
138 let mut pkg = src.root_package()?;
139
140 let cksum_file = path.join(".cargo-checksum.json");
141 let cksum = paths::read(&path.join(cksum_file)).with_context(|| {
142 format!(
143 "failed to load checksum `.cargo-checksum.json` \
144 of {} v{}",
145 pkg.package_id().name(),
146 pkg.package_id().version()
147 )
148 })?;
149 let cksum: Checksum = serde_json::from_str(&cksum).with_context(|| {
150 format!(
151 "failed to decode `.cargo-checksum.json` of \
152 {} v{}",
153 pkg.package_id().name(),
154 pkg.package_id().version()
155 )
156 })?;
157
158 if let Some(package) = &cksum.package {
159 pkg.manifest_mut()
160 .summary_mut()
161 .set_checksum(package.clone());
162 }
163 self.packages
164 .borrow_mut()
165 .insert(pkg.package_id(), (pkg, cksum));
166 }
167
168 self.updated.set(true);
169 Ok(())
170 }
171}
172
173impl<'gctx> Debug for DirectorySource<'gctx> {
174 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
175 write!(f, "DirectorySource {{ root: {:?} }}", self.root)
176 }
177}
178
179#[async_trait::async_trait(?Send)]
180impl<'gctx> Source for DirectorySource<'gctx> {
181 async fn query(
182 &self,
183 dep: &Dependency,
184 kind: QueryKind,
185 f: &mut dyn FnMut(IndexSummary),
186 ) -> CargoResult<()> {
187 if !self.updated.get() {
188 self.update()?;
189 }
190 let packages = self.packages.borrow();
191 let packages = packages.values().map(|p| &p.0);
192 let matches = packages.filter(|pkg| match kind {
193 QueryKind::Exact | QueryKind::RejectedVersions => dep.matches(pkg.summary()),
194 QueryKind::AlternativeNames => true,
195 QueryKind::Normalized => dep.matches(pkg.summary()),
196 });
197 for summary in matches.map(|pkg| pkg.summary().clone()) {
198 f(IndexSummary::Candidate(summary));
199 }
200 Ok(())
201 }
202
203 fn supports_checksums(&self) -> bool {
204 true
205 }
206
207 fn requires_precise(&self) -> bool {
208 true
209 }
210
211 fn source_id(&self) -> SourceId {
212 self.source_id
213 }
214
215 fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
216 self.packages
217 .borrow()
218 .get(&id)
219 .map(|p| &p.0)
220 .cloned()
221 .map(MaybePackage::Ready)
222 .ok_or_else(|| anyhow::format_err!("failed to find package with id: {}", id))
223 }
224
225 fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
226 panic!("no downloads to do")
227 }
228
229 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
230 Ok(pkg.package_id().version().to_string())
231 }
232
233 fn verify(&self, id: PackageId) -> CargoResult<()> {
234 let packages = self.packages.borrow_mut();
235 let Some((pkg, cksum)) = packages.get(&id) else {
236 anyhow::bail!("failed to find entry for `{}` in directory source", id);
237 };
238
239 for (file, cksum) in cksum.files.iter() {
240 let file = pkg.root().join(file);
241 let actual = Sha256::new()
242 .update_path(&file)
243 .with_context(|| format!("failed to calculate checksum of: {}", file.display()))?
244 .finish_hex();
245 if &*actual != cksum {
246 anyhow::bail!(
247 "the listed checksum of `{}` has changed:\n\
248 expected: {}\n\
249 actual: {}\n\
250 \n\
251 directory sources are not intended to be edited, if \
252 modifications are required then it is recommended \
253 that `[patch]` is used with a forked copy of the \
254 source\
255 ",
256 file.display(),
257 cksum,
258 actual
259 );
260 }
261 }
262
263 Ok(())
264 }
265
266 fn describe(&self) -> String {
267 format!("directory source `{}`", self.root.display())
268 }
269
270 fn add_to_yanked_whitelist(&self, _pkgs: &[PackageId]) {}
271
272 async fn is_yanked(&self, _pkg: PackageId) -> CargoResult<bool> {
273 Ok(false)
274 }
275
276 fn invalidate_cache(&self) {
277 }
279
280 fn set_quiet(&mut self, _quiet: bool) {
281 }
283}