Skip to main content

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