#
cargo/ops/registry/
mod.rs

    1//! Operations that interact with the [registry web API][1].
    2//!
    3//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html
    4
    5mod info;
    6mod login;
    7mod logout;
    8mod owner;
    9mod publish;
    10mod search;
    11mod yank;
    12
    13use std::collections::HashSet;
    14use std::str;
    15use std::task::Poll;
    16
    17use anyhow::{Context as _, bail, format_err};
    18use cargo_credential::{Operation, Secret};
    19use crates_io::Registry;
    20use url::Url;
    21
    22use crate::core::{Package, PackageId, SourceId};
    23use crate::sources::source::Source;
    24use crate::sources::{RegistrySource, SourceConfigMap};
    25use crate::util::auth;
    26use crate::util::cache_lock::CacheLockMode;
    27use crate::util::context::{GlobalContext, PathAndArgs};
    28use crate::util::errors::CargoResult;
    29use crate::util::network::http::http_handle;
    30
    31pub use self::info::info;
    32pub use self::login::registry_login;
    33pub use self::logout::registry_logout;
    34pub use self::owner::OwnersOptions;
    35pub use self::owner::modify_owners;
    36pub use self::publish::PublishOpts;
    37pub use self::publish::publish;
    38pub use self::search::search;
    39pub use self::yank::yank;
    40
    41pub(crate) use self::publish::prepare_transmit;
    42
    43/// Represents either `--registry` or `--index` argument, which is mutually exclusive.
    44#[derive(Debug, Clone)]
    45pub enum RegistryOrIndex {
    46    Registry(String),
    47    Index(Url),
    48}
    49
    50impl RegistryOrIndex {
    51    fn is_index(&self) -> bool {
    52        matches!(self, RegistryOrIndex::Index(..))
    53    }
    54}
    55
    56/// Registry settings loaded from config files.
    57///
    58/// This is loaded based on the `--registry` flag and the config settings.
    59#[derive(Debug, PartialEq)]
    60pub enum RegistryCredentialConfig {
    61    None,
    62    /// The authentication token.
    63    Token(Secret<String>),
    64    /// Process used for fetching a token.
    65    Process(Vec<PathAndArgs>),
    66    /// Secret Key and subject for Asymmetric tokens.
    67    AsymmetricKey((Secret<String>, Option<String>)),
    68}
    69
    70impl RegistryCredentialConfig {
    71    /// Returns `true` if the credential is [`None`].
    72    ///
    73    /// [`None`]: Self::None
    74    pub fn is_none(&self) -> bool {
    75        matches!(self, Self::None)
    76    }
    77    /// Returns `true` if the credential is [`Token`].
    78    ///
    79    /// [`Token`]: Self::Token
    80    pub fn is_token(&self) -> bool {
    81        matches!(self, Self::Token(..))
    82    }
    83    /// Returns `true` if the credential is [`AsymmetricKey`].
    84    ///
    85    /// [`AsymmetricKey`]: RegistryCredentialConfig::AsymmetricKey
    86    pub fn is_asymmetric_key(&self) -> bool {
    87        matches!(self, Self::AsymmetricKey(..))
    88    }
    89    pub fn as_token(&self) -> Option<Secret<&str>> {
    90        if let Self::Token(v) = self {
    91            Some(v.as_deref())
    92        } else {
    93            None
    94        }
    95    }
    96    pub fn as_process(&self) -> Option<&Vec<PathAndArgs>> {
    97        if let Self::Process(v) = self {
    98            Some(v)
    99        } else {
    100            None
    101        }
    102    }
    103    pub fn as_asymmetric_key(&self) -> Option<&(Secret<String>, Option<String>)> {
    104        if let Self::AsymmetricKey(v) = self {
    105            Some(v)
    106        } else {
    107            None
    108        }
    109    }
    110}
    111
    112/// Returns the `Registry` and `Source` based on command-line and config settings.
    113///
    114/// * `source_ids`: The source IDs for the registry. It contains the original source ID and
    115///   the replacement source ID.
    116/// * `token_from_cmdline`: The token from the command-line. If not set, uses the token
    117///   from the config.
    118/// * `index`: The index URL from the command-line.
    119/// * `registry`: The registry name from the command-line. If neither
    120///   `registry`, or `index` are set, then uses `crates-io`.
    121/// * `force_update`: If `true`, forces the index to be updated.
    122/// * `token_required`: If `true`, the token will be set.
    123fn registry<'gctx>(
    124    gctx: &'gctx GlobalContext,
    125    source_ids: &RegistrySourceIds,
    126    token_from_cmdline: Option<Secret<&str>>,
    127    reg_or_index: Option<&RegistryOrIndex>,
    128    force_update: bool,
    129    token_required: Option<Operation<'_>>,
    130) -> CargoResult<(Registry, RegistrySource<'gctx>)> {
    131    let is_index = reg_or_index.map(|v| v.is_index()).unwrap_or_default();
    132    if is_index && token_required.is_some() && token_from_cmdline.is_none() {
    133        bail!("command-line argument --index requires --token to be specified");
    134    }
    135    if let Some(token) = token_from_cmdline {
    136        auth::cache_token_from_commandline(gctx, &source_ids.original, token);
    137    }
    138
    139    let mut src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), gctx)?;
    140    let cfg = {
    141        let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
    142        // Only update the index if `force_update` is set.
    143        if force_update {
    144            src.invalidate_cache()
    145        }
    146        let cfg = loop {
    147            match src.config()? {
    148                Poll::Pending => src
    149                    .block_until_ready()
    150                    .with_context(|| format!("failed to update {}", source_ids.replacement))?,
    151                Poll::Ready(cfg) => break cfg,
    152            }
    153        };
    154        cfg.expect("remote registries must have config")
    155    };
    156    let api_host = cfg
    157        .api
    158        .ok_or_else(|| format_err!("{} does not support API commands", source_ids.replacement))?;
    159    let token = if token_required.is_some() || cfg.auth_required {
    160        let operation = token_required.unwrap_or(Operation::Read);
    161        Some(auth::auth_token(
    162            gctx,
    163            &source_ids.original,
    164            None,
    165            operation,
    166            vec![],
    167            false,
    168        )?)
    169    } else {
    170        None
    171    };
    172    let handle = http_handle(gctx)?;
    173    Ok((
    174        Registry::new_handle(api_host, token, handle, cfg.auth_required),
    175        src,
    176    ))
    177}
    178
    179/// Gets the `SourceId` for an index or registry setting.
    180///
    181/// The `index` and `reg` values are from the command-line or config settings.
    182/// If both are None, and no source-replacement is configured, returns the source for crates.io.
    183/// If both are None, and source replacement is configured, returns an error.
    184///
    185/// The source for crates.io may be GitHub, index.crates.io, or a test-only registry depending
    186/// on configuration.
    187///
    188/// If `reg` is set, source replacement is not followed.
    189///
    190/// The return value is a pair of `SourceId`s: The first may be a built-in replacement of
    191/// crates.io (such as index.crates.io), while the second is always the original source.
    192pub(crate) fn get_source_id(
    193    gctx: &GlobalContext,
    194    reg_or_index: Option<&RegistryOrIndex>,
    195) -> CargoResult<RegistrySourceIds> {
    196    let sid = get_initial_source_id(gctx, reg_or_index)?;
    197    let (builtin_replacement_sid, replacement_sid) = get_replacement_source_ids(gctx, sid)?;
    198
    199    if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid {
    200        bail!(gen_replacement_error(replacement_sid));
    201    } else {
    202        Ok(RegistrySourceIds {
    203            original: sid,
    204            replacement: builtin_replacement_sid,
    205        })
    206    }
    207}
    208
    209/// Very similar to [`get_source_id`], but is used when the `package_id` is known.
    210fn get_source_id_with_package_id(
    211    gctx: &GlobalContext,
    212    package_id: Option<PackageId>,
    213    reg_or_index: Option<&RegistryOrIndex>,
    214) -> CargoResult<(bool, RegistrySourceIds)> {
    215    let (use_package_source_id, sid) = match (&reg_or_index, package_id) {
    216        (None, Some(package_id)) => (true, package_id.source_id()),
    217        (None, None) => (false, SourceId::crates_io(gctx)?),
    218        (Some(RegistryOrIndex::Index(url)), None) => (false, SourceId::for_registry(url)?),
    219        (Some(RegistryOrIndex::Registry(r)), None) => (false, SourceId::alt_registry(gctx, r)?),
    220        (Some(reg_or_index), Some(package_id)) => {
    221            let sid = get_initial_source_id_from_registry_or_index(gctx, reg_or_index)?;
    222            let package_source_id = package_id.source_id();
    223            // 1. Same registry, use the package's source.
    224            // 2. Use the package's source if the specified registry is a replacement for the package's source.
    225            if sid == package_source_id
    226                || is_replacement_for_package_source(gctx, sid, package_source_id)?
    227            {
    228                (true, package_source_id)
    229            } else {
    230                (false, sid)
    231            }
    232        }
    233    };
    234
    235    let (builtin_replacement_sid, replacement_sid) = get_replacement_source_ids(gctx, sid)?;
    236
    237    if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid {
    238        bail!(gen_replacement_error(replacement_sid));
    239    } else {
    240        Ok((
    241            use_package_source_id,
    242            RegistrySourceIds {
    243                original: sid,
    244                replacement: builtin_replacement_sid,
    245            },
    246        ))
    247    }
    248}
    249
    250fn get_initial_source_id(
    251    gctx: &GlobalContext,
    252    reg_or_index: Option<&RegistryOrIndex>,
    253) -> CargoResult<SourceId> {
    254    match reg_or_index {
    255        None => SourceId::crates_io(gctx),
    256        Some(reg_or_index) => get_initial_source_id_from_registry_or_index(gctx, reg_or_index),
    257    }
    258}
    259
    260fn get_initial_source_id_from_registry_or_index(
    261    gctx: &GlobalContext,
    262    reg_or_index: &RegistryOrIndex,
    263) -> CargoResult<SourceId> {
    264    match reg_or_index {
    265        RegistryOrIndex::Index(url) => SourceId::for_registry(url),
    266        RegistryOrIndex::Registry(r) => SourceId::alt_registry(gctx, r),
    267    }
    268}
    269
    270fn get_replacement_source_ids(
    271    gctx: &GlobalContext,
    272    sid: SourceId,
    273) -> CargoResult<(SourceId, SourceId)> {
    274    let builtin_replacement_sid = SourceConfigMap::empty(gctx)?
    275        .load(sid, &HashSet::new())?
    276        .replaced_source_id();
    277    let replacement_sid = SourceConfigMap::new(gctx)?
    278        .load(sid, &HashSet::new())?
    279        .replaced_source_id();
    280    Ok((builtin_replacement_sid, replacement_sid))
    281}
    282
    283fn is_replacement_for_package_source(
    284    gctx: &GlobalContext,
    285    sid: SourceId,
    286    package_source_id: SourceId,
    287) -> CargoResult<bool> {
    288    let pkg_source_replacement_sid = SourceConfigMap::new(gctx)?
    289        .load(package_source_id, &HashSet::new())?
    290        .replaced_source_id();
    291    Ok(pkg_source_replacement_sid == sid)
    292}
    293
    294fn gen_replacement_error(replacement_sid: SourceId) -> String {
    295    // Neither --registry nor --index was passed and the user has configured source-replacement.
    296    let error_message = if let Some(replacement_name) = replacement_sid.alt_registry_key() {
    297        format!(
    298            "crates-io is replaced with remote registry {};\ninclude `--registry {}` or `--registry crates-io`",
    299            replacement_name, replacement_name
    300        )
    301    } else {
    302        format!(
    303            "crates-io is replaced with non-remote-registry source {};\ninclude `--registry crates-io` to use crates.io",
    304            replacement_sid
    305        )
    306    };
    307
    308    error_message
    309}
    310
    311pub(crate) struct RegistrySourceIds {
    312    /// Use when looking up the auth token, or writing out `Cargo.lock`
    313    pub(crate) original: SourceId,
    314    /// Use when interacting with the source (querying / publishing , etc)
    315    ///
    316    /// The source for crates.io may be replaced by a built-in source for accessing crates.io with
    317    /// the sparse protocol, or a source for the testing framework (when the `replace_crates_io`
    318    /// function is used)
    319    ///
    320    /// User-defined source replacement is not applied.
    321    pub(crate) replacement: SourceId,
    322}
    323
    324/// If this set of packages has an unambiguous publish registry, find it.
    325pub(crate) fn infer_registry(pkgs: &[&Package]) -> CargoResult<Option<RegistryOrIndex>> {
    326    // Ignore "publish = false" packages while inferring the registry.
    327    let publishable_pkgs: Vec<_> = pkgs
    328        .iter()
    329        .filter(|p| p.publish() != &Some(Vec::new()))
    330        .collect();
    331
    332    let Some((first, rest)) = publishable_pkgs.split_first() else {
    333        return Ok(None);
    334    };
    335
    336    // If all packages have the same publish settings, we take that as the default.
    337    if rest.iter().all(|p| p.publish() == first.publish()) {
    338        match publishable_pkgs[0].publish().as_deref() {
    339            Some([unique_pkg_reg]) => {
    340                Ok(Some(RegistryOrIndex::Registry(unique_pkg_reg.to_owned())))
    341            }
    342            None | Some([]) => Ok(None),
    343            Some(regs) => {
    344                let mut regs: Vec<_> = regs.iter().map(|s| format!("\"{}\"", s)).collect();
    345                regs.sort();
    346                regs.dedup();
    347                // unwrap: the match block ensures that there's more than one reg.
    348                let (last_reg, regs) = regs.split_last().unwrap();
    349                bail!(
    350                    "--registry is required to disambiguate between {} or {} registries",
    351                    regs.join(", "),
    352                    last_reg
    353                )
    354            }
    355        }
    356    } else {
    357        let common_regs = publishable_pkgs
    358            .iter()
    359            // `None` means "all registries", so drop them instead of including them
    360            // in the intersection.
    361            .filter_map(|p| p.publish().as_deref())
    362            .map(|p| p.iter().collect::<HashSet<_>>())
    363            .reduce(|xs, ys| xs.intersection(&ys).cloned().collect())
    364            .unwrap_or_default();
    365        if common_regs.is_empty() {
    366            bail!("conflicts between `package.publish` fields in the selected packages");
    367        } else {
    368            bail!("--registry is required because not all `package.publish` settings agree",);
    369        }
    370    }
    371}