cargo/sources/
directory.rs1use crate::util::data_structures::HashMap;
2use std::cell::OnceCell;
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::OnceExt;
13use crate::util::VersionReqMatchMode;
14use crate::util::errors::CargoResult;
15use crate::workspace::{Dependency, Package, PackageId, SourceId};
16
17use anyhow::Context as _;
18use cargo_util::{Sha256, paths};
19use serde::Deserialize;
20
21pub struct DirectorySource<'gctx> {
60 source_id: SourceId,
62 root: PathBuf,
64 packages: OnceCell<HashMap<PackageId, (Package, Checksum)>>,
66 gctx: &'gctx GlobalContext,
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: OnceCell::new(),
89 }
90 }
91
92 fn packages(&self) -> CargoResult<&HashMap<PackageId, (Package, Checksum)>> {
93 self.packages.try_borrow_with(|| self.load_packages())
94 }
95
96 fn load_packages(&self) -> CargoResult<HashMap<PackageId, (Package, Checksum)>> {
97 let mut packages = HashMap::default();
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 packages.insert(pkg.package_id(), (pkg, cksum));
165 }
166
167 Ok(packages)
168 }
169}
170
171impl<'gctx> Debug for DirectorySource<'gctx> {
172 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
173 write!(f, "DirectorySource {{ root: {:?} }}", self.root)
174 }
175}
176
177#[async_trait::async_trait(?Send)]
178impl<'gctx> Source for DirectorySource<'gctx> {
179 async fn query(
180 &self,
181 dep: &Dependency,
182 kind: QueryKind,
183 f: &mut dyn FnMut(IndexSummary),
184 ) -> CargoResult<()> {
185 let packages = self.packages()?;
186 let packages = packages.values().map(|p| &p.0);
187 let matches = packages.filter(|pkg| match kind {
188 QueryKind::Exact | QueryKind::RejectedVersions => {
189 dep.matches(pkg.summary(), VersionReqMatchMode::Default)
190 }
191 QueryKind::AlternativeNames => true,
192 QueryKind::Normalized => dep.matches(pkg.summary(), VersionReqMatchMode::Default),
193 });
194 for summary in matches.map(|pkg| pkg.summary().clone()) {
195 f(IndexSummary::Candidate(summary));
196 }
197 Ok(())
198 }
199
200 fn supports_checksums(&self) -> bool {
201 true
202 }
203
204 fn requires_precise(&self) -> bool {
205 true
206 }
207
208 fn source_id(&self) -> SourceId {
209 self.source_id
210 }
211
212 async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
213 self.packages()?
214 .get(&id)
215 .map(|p| &p.0)
216 .cloned()
217 .map(MaybePackage::Ready)
218 .ok_or_else(|| anyhow::format_err!("failed to find package with id: {}", id))
219 }
220
221 async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
222 panic!("no downloads to do")
223 }
224
225 fn fingerprint(&self, pkg: &Package) -> CargoResult<String> {
226 Ok(pkg.package_id().version().to_string())
227 }
228
229 fn verify(&self, id: PackageId) -> CargoResult<()> {
230 let packages = self.packages()?;
231 let Some((pkg, cksum)) = packages.get(&id) else {
232 anyhow::bail!("failed to find entry for `{}` in directory source", id);
233 };
234
235 for (file, cksum) in cksum.files.iter() {
236 let file = pkg.root().join(file);
237 let actual = Sha256::new()
238 .update_path(&file)
239 .with_context(|| format!("failed to calculate checksum of: {}", file.display()))?
240 .finish_hex();
241 if &*actual != cksum {
242 anyhow::bail!(
243 "the listed checksum of `{}` has changed:\n\
244 expected: {}\n\
245 actual: {}\n\
246 \n\
247 directory sources are not intended to be edited, if \
248 modifications are required then it is recommended \
249 that `[patch]` is used with a forked copy of the \
250 source\
251 ",
252 file.display(),
253 cksum,
254 actual
255 );
256 }
257 }
258
259 Ok(())
260 }
261
262 fn describe(&self) -> String {
263 format!("directory source `{}`", self.root.display())
264 }
265
266 fn invalidate_cache(&self) {
267 }
269
270 fn set_quiet(&mut self, _quiet: bool) {
271 }
273}