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::{bail, format_err, Context as _};
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::modify_owners;
35pub use self::owner::OwnersOptions;
36pub use self::publish::publish;
37pub use self::publish::PublishOpts;
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}