| use crate::util::data_structures::HashMap; |
| use std::cell::{Cell, RefCell}; |
| use std::fmt::{self, Debug, Formatter}; |
| use std::path::{Path, PathBuf}; |
| |
| use crate::sources::IndexSummary; |
| use crate::sources::PathSource; |
| use crate::sources::source::MaybePackage; |
| use crate::sources::source::QueryKind; |
| use crate::sources::source::Source; |
| use crate::util::GlobalContext; |
| use crate::util::errors::CargoResult; |
| use crate::workspace::{Dependency, Package, PackageId, SourceId}; |
| |
| use anyhow::Context as _; |
| use cargo_util::{Sha256, paths}; |
| use serde::Deserialize; |
| |
| /// `DirectorySource` contains a number of crates on the file system. It was |
| /// designed for representing vendored dependencies for `cargo vendor`. |
| /// |
| /// `DirectorySource` at this moment is just a root directory containing other |
| /// directories, which contain the source files of packages. Assumptions would |
| /// be made to determine if a directory should be included as a package of a |
| /// directory source's: |
| /// |
| /// * Ignore directories starting with dot `.` (tend to be hidden). |
| /// * Only when a `Cargo.toml` exists in a directory will it be included as |
| /// a package. `DirectorySource` at this time only looks at one level of |
| /// directories and never went deeper. |
| /// * There must be a [`Checksum`] file `.cargo-checksum.json` file at the same |
| /// level of `Cargo.toml` to ensure the integrity when a directory source was |
| /// created (usually by `cargo vendor`). A failure to find or parse a single |
| /// checksum results in a denial of loading any package in this source. |
| /// * Otherwise, there is no other restriction of the name of directories. At |
| /// this moment, it is `cargo vendor` that defines the layout and the name of |
| /// each directory. |
| /// |
| /// The file tree of a directory source may look like: |
| /// |
| /// ```text |
| /// [source root] |
| /// ├── a-valid-crate/ |
| /// │ ├── src/ |
| /// │ ├── .cargo-checksum.json |
| /// │ └── Cargo.toml |
| /// ├── .ignored-a-dot-crate/ |
| /// │ ├── src/ |
| /// │ ├── .cargo-checksum.json |
| /// │ └── Cargo.toml |
| /// ├── skipped-no-manifest/ |
| /// │ ├── src/ |
| /// │ └── .cargo-checksum.json |
| /// └── no-checksum-so-fails-the-entire-source-reading/ |
| /// └── Cargo.toml |
| /// ``` |
| pub struct DirectorySource<'gctx> { |
| /// The unique identifier of this source. |
| source_id: SourceId, |
| /// The root path of this source. |
| root: PathBuf, |
| /// Packages that this sources has discovered. |
| packages: RefCell<HashMap<PackageId, (Package, Checksum)>>, |
| gctx: &'gctx GlobalContext, |
| updated: Cell<bool>, |
| } |
| |
| /// The checksum file to ensure the integrity of a package in a directory source. |
| /// |
| /// The file name is simply `.cargo-checksum.json`. The checksum algorithm as |
| /// of now is SHA256. |
| #[derive(Deserialize)] |
| #[serde(rename_all = "kebab-case")] |
| struct Checksum { |
| /// Checksum of the package. Normally it is computed from the `.crate` file. |
| package: Option<String>, |
| /// Checksums of each source file. |
| files: HashMap<String, String>, |
| } |
| |
| impl<'gctx> DirectorySource<'gctx> { |
| pub fn new(path: &Path, id: SourceId, gctx: &'gctx GlobalContext) -> DirectorySource<'gctx> { |
| DirectorySource { |
| source_id: id, |
| root: path.to_path_buf(), |
| gctx, |
| packages: RefCell::new(HashMap::default()), |
| updated: Cell::new(false), |
| } |
| } |
| |
| fn update(&self) -> CargoResult<()> { |
| if self.updated.get() { |
| return Ok(()); |
| } |
| self.packages.borrow_mut().clear(); |
| let entries = self.root.read_dir().with_context(|| { |
| format!( |
| "failed to read root of directory source: {}", |
| self.root.display() |
| ) |
| })?; |
| |
| for entry in entries { |
| let entry = entry?; |
| let path = entry.path(); |
| |
| // Ignore hidden/dot directories as they typically don't contain |
| // crates and otherwise may conflict with a VCS |
| // (rust-lang/cargo#3414). |
| if let Some(s) = path.file_name().and_then(|s| s.to_str()) { |
| if s.starts_with('.') { |
| continue; |
| } |
| } |
| |
| // Vendor directories are often checked into a VCS, but throughout |
| // the lifetime of a vendor dir crates are often added and deleted. |
| // Some VCS implementations don't always fully delete the directory |
| // when a dir is removed from a different checkout. Sometimes a |
| // mostly-empty dir is left behind. |
| // |
| // Additionally vendor directories are sometimes accompanied with |
| // readme files and other auxiliary information not too interesting |
| // to Cargo. |
| // |
| // To help handle all this we only try processing folders with a |
| // `Cargo.toml` in them. This has the upside of being pretty |
| // flexible with the contents of vendor directories but has the |
| // downside of accidentally misconfigured vendor directories |
| // silently returning less crates. |
| if !path.join("Cargo.toml").exists() { |
| continue; |
| } |
| |
| let mut src = PathSource::new(&path, self.source_id, self.gctx); |
| src.load()?; |
| let mut pkg = src.root_package()?; |
| |
| let cksum_file = path.join(".cargo-checksum.json"); |
| let cksum = paths::read(&path.join(cksum_file)).with_context(|| { |
| format!( |
| "failed to load checksum `.cargo-checksum.json` \ |
| of {} v{}", |
| pkg.package_id().name(), |
| pkg.package_id().version() |
| ) |
| })?; |
| let cksum: Checksum = serde_json::from_str(&cksum).with_context(|| { |
| format!( |
| "failed to decode `.cargo-checksum.json` of \ |
| {} v{}", |
| pkg.package_id().name(), |
| pkg.package_id().version() |
| ) |
| })?; |
| |
| if let Some(package) = &cksum.package { |
| pkg.manifest_mut() |
| .summary_mut() |
| .set_checksum(package.clone()); |
| } |
| self.packages |
| .borrow_mut() |
| .insert(pkg.package_id(), (pkg, cksum)); |
| } |
| |
| self.updated.set(true); |
| Ok(()) |
| } |
| } |
| |
| impl<'gctx> Debug for DirectorySource<'gctx> { |
| fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
| write!(f, "DirectorySource {{ root: {:?} }}", self.root) |
| } |
| } |
| |
| #[async_trait::async_trait(?Send)] |
| impl<'gctx> Source for DirectorySource<'gctx> { |
| async fn query( |
| &self, |
| dep: &Dependency, |
| kind: QueryKind, |
| f: &mut dyn FnMut(IndexSummary), |
| ) -> CargoResult<()> { |
| if !self.updated.get() { |
| self.update()?; |
| } |
| let packages = self.packages.borrow(); |
| let packages = packages.values().map(|p| &p.0); |
| let matches = packages.filter(|pkg| match kind { |
| QueryKind::Exact | QueryKind::RejectedVersions => dep.matches(pkg.summary()), |
| QueryKind::AlternativeNames => true, |
| QueryKind::Normalized => dep.matches(pkg.summary()), |
| }); |
| for summary in matches.map(|pkg| pkg.summary().clone()) { |
| f(IndexSummary::Candidate(summary)); |
| } |
| Ok(()) |
| } |
| |
| fn supports_checksums(&self) -> bool { |
| true |
| } |
| |
| fn requires_precise(&self) -> bool { |
| true |
| } |
| |
| fn source_id(&self) -> SourceId { |
| self.source_id |
| } |
| |
| async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> { |
| self.packages |
| .borrow() |
| .get(&id) |
| .map(|p| &p.0) |
| .cloned() |
| .map(MaybePackage::Ready) |
| .ok_or_else(|| anyhow::format_err!("failed to find package with id: {}", id)) |
| } |
| |
| async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> { |
| panic!("no downloads to do") |
| } |
| |
| fn fingerprint(&self, pkg: &Package) -> CargoResult<String> { |
| Ok(pkg.package_id().version().to_string()) |
| } |
| |
| fn verify(&self, id: PackageId) -> CargoResult<()> { |
| let packages = self.packages.borrow_mut(); |
| let Some((pkg, cksum)) = packages.get(&id) else { |
| anyhow::bail!("failed to find entry for `{}` in directory source", id); |
| }; |
| |
| for (file, cksum) in cksum.files.iter() { |
| let file = pkg.root().join(file); |
| let actual = Sha256::new() |
| .update_path(&file) |
| .with_context(|| format!("failed to calculate checksum of: {}", file.display()))? |
| .finish_hex(); |
| if &*actual != cksum { |
| anyhow::bail!( |
| "the listed checksum of `{}` has changed:\n\ |
| expected: {}\n\ |
| actual: {}\n\ |
| \n\ |
| directory sources are not intended to be edited, if \ |
| modifications are required then it is recommended \ |
| that `[patch]` is used with a forked copy of the \ |
| source\ |
| ", |
| file.display(), |
| cksum, |
| actual |
| ); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn describe(&self) -> String { |
| format!("directory source `{}`", self.root.display()) |
| } |
| |
| fn invalidate_cache(&self) { |
| // Directory source has no local cache. |
| } |
| |
| fn set_quiet(&mut self, _quiet: bool) { |
| // Directory source does not display status |
| } |
| } |