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