#
cargo/sources/
directory.rs

    1use 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
    19/// `DirectorySource` contains a number of crates on the file system. It was
    20/// designed for representing vendored dependencies for `cargo vendor`.
    21///
    22/// `DirectorySource` at this moment is just a root directory containing other
    23/// directories, which contain the source files of packages. Assumptions would
    24/// be made to determine if a directory should be included as a package of a
    25/// directory source's:
    26///
    27/// * Ignore directories starting with dot `.` (tend to be hidden).
    28/// * Only when a `Cargo.toml` exists in a directory will it be included as
    29///   a package. `DirectorySource` at this time only looks at one level of
    30///   directories and never went deeper.
    31/// * There must be a [`Checksum`] file `.cargo-checksum.json` file at the same
    32///   level of `Cargo.toml` to ensure the integrity when a directory source was
    33///   created (usually by `cargo vendor`). A failure to find or parse a single
    34///   checksum results in a denial of loading any package in this source.
    35/// * Otherwise, there is no other restrction of the name of directories. At
    36///   this moment, it is `cargo vendor` that defines the layout and the name of
    37///   each directory.
    38///
    39/// The file tree of a directory source may look like:
    40///
    41/// ```text
    42/// [source root]
    43/// ├── a-valid-crate/
    44/// │  ├── src/
    45/// │  ├── .cargo-checksum.json
    46/// │  └── Cargo.toml
    47/// ├── .ignored-a-dot-crate/
    48/// │  ├── src/
    49/// │  ├── .cargo-checksum.json
    50/// │  └── Cargo.toml
    51/// ├── skipped-no-manifest/
    52/// │  ├── src/
    53/// │  └── .cargo-checksum.json
    54/// └── no-checksum-so-fails-the-entire-source-reading/
    55///    └── Cargo.toml
    56/// ```
    57pub struct DirectorySource<'gctx> {
    58    /// The unique identifier of this source.
    59    source_id: SourceId,
    60    /// The root path of this source.
    61    root: PathBuf,
    62    /// Packages that this sources has discovered.
    63    packages: HashMap<PackageId, (Package, Checksum)>,
    64    gctx: &'gctx GlobalContext,
    65    updated: bool,
    66}
    67
    68/// The checksum file to ensure the integrity of a package in a directory source.
    69///
    70/// The file name is simply `.cargo-checksum.json`. The checksum algorithm as
    71/// of now is SHA256.
    72#[derive(Deserialize)]
    73#[serde(rename_all = "kebab-case")]
    74struct Checksum {
    75    /// Checksum of the package. Normally it is computed from the `.crate` file.
    76    package: Option<String>,
    77    /// Checksums of each source file.
    78    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            // Ignore hidden/dot directories as they typically don't contain
    150            // crates and otherwise may conflict with a VCS
    151            // (rust-lang/cargo#3414).
    152            if let Some(s) = path.file_name().and_then(|s| s.to_str()) {
    153                if s.starts_with('.') {
    154                    continue;
    155                }
    156            }
    157
    158            // Vendor directories are often checked into a VCS, but throughout
    159            // the lifetime of a vendor dir crates are often added and deleted.
    160            // Some VCS implementations don't always fully delete the directory
    161            // when a dir is removed from a different checkout. Sometimes a
    162            // mostly-empty dir is left behind.
    163            //
    164            // Additionally vendor directories are sometimes accompanied with
    165            // readme files and other auxiliary information not too interesting
    166            // to Cargo.
    167            //
    168            // To help handle all this we only try processing folders with a
    169            // `Cargo.toml` in them. This has the upside of being pretty
    170            // flexible with the contents of vendor directories but has the
    171            // downside of accidentally misconfigured vendor directories
    172            // silently returning less crates.
    173            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        // Directory source has no local cache.
    272    }
    273
    274    fn set_quiet(&mut self, _quiet: bool) {
    275        // Directory source does not display status
    276    }
    277}