cargo/sources/
directory.rs1use 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
20pub struct DirectorySource<'gctx> {
59 source_id: SourceId,
61 root: PathBuf,
63 packages: RefCell<HashMap<PackageId, (Package, Checksum)>>,
65 gctx: &'gctx GlobalContext,
66 updated: Cell<bool>,
67}
68
69#[derive(Deserialize)]
74#[serde(rename_all = "kebab-case")]
75struct Checksum {
76 package: Option<String>,
78 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 if let Some(s) = path.file_name().and_then(|s| s.to_str()) {
113 if s.starts_with('.') {
114 continue;
115 }
116 }
117
118 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 }
276
277 fn set_quiet(&mut self, _quiet: bool) {
278 }
280}