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_async;
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<RegistryClient<'gctx>>, 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, 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 = RegistryClient(gctx.http_async()?);
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)?
269        .replaced_source_id();
270    let replacement_sid = SourceConfigMap::new(gctx)?.load(sid)?.replaced_source_id();
271    Ok((builtin_replacement_sid, replacement_sid))
272}
273
274fn is_replacement_for_package_source(
275    gctx: &GlobalContext,
276    sid: SourceId,
277    package_source_id: SourceId,
278) -> CargoResult<bool> {
279    let pkg_source_replacement_sid = SourceConfigMap::new(gctx)?
280        .load(package_source_id)?
281        .replaced_source_id();
282    Ok(pkg_source_replacement_sid == sid)
283}
284
285fn gen_replacement_error(replacement_sid: SourceId) -> String {
286    // Neither --registry nor --index was passed and the user has configured source-replacement.
287    let error_message = if let Some(replacement_name) = replacement_sid.alt_registry_key() {
288        format!(
289            "crates-io is replaced with remote registry {};\ninclude `--registry {}` or `--registry crates-io`",
290            replacement_name, replacement_name
291        )
292    } else {
293        format!(
294            "crates-io is replaced with non-remote-registry source {};\ninclude `--registry crates-io` to use crates.io",
295            replacement_sid
296        )
297    };
298
299    error_message
300}
301
302pub(crate) struct RegistrySourceIds {
303    /// Use when looking up the auth token, or writing out `Cargo.lock`
304    pub(crate) original: SourceId,
305    /// Use when interacting with the source (querying / publishing , etc)
306    ///
307    /// The source for crates.io may be replaced by a built-in source for accessing crates.io with
308    /// the sparse protocol, or a source for the testing framework (when the `replace_crates_io`
309    /// function is used)
310    ///
311    /// User-defined source replacement is not applied.
312    pub(crate) replacement: SourceId,
313}
314
315/// If this set of packages has an unambiguous publish registry, find it.
316pub(crate) fn infer_registry(pkgs: &[&Package]) -> CargoResult<Option<RegistryOrIndex>> {
317    // Ignore "publish = false" packages while inferring the registry.
318    let publishable_pkgs: Vec<_> = pkgs
319        .iter()
320        .filter(|p| p.publish() != &Some(Vec::new()))
321        .collect();
322
323    let Some((first, rest)) = publishable_pkgs.split_first() else {
324        return Ok(None);
325    };
326
327    // If all packages have the same publish settings, we take that as the default.
328    if rest.iter().all(|p| p.publish() == first.publish()) {
329        match publishable_pkgs[0].publish().as_deref() {
330            Some([unique_pkg_reg]) => {
331                Ok(Some(RegistryOrIndex::Registry(unique_pkg_reg.to_owned())))
332            }
333            None | Some([]) => Ok(None),
334            Some(regs) => {
335                let mut regs: Vec<_> = regs.iter().map(|s| format!("\"{}\"", s)).collect();
336                regs.sort();
337                regs.dedup();
338                // unwrap: the match block ensures that there's more than one reg.
339                let (last_reg, regs) = regs.split_last().unwrap();
340                bail!(
341                    "--registry is required to disambiguate between {} or {} registries",
342                    regs.join(", "),
343                    last_reg
344                )
345            }
346        }
347    } else {
348        let common_regs = publishable_pkgs
349            .iter()
350            // `None` means "all registries", so drop them instead of including them
351            // in the intersection.
352            .filter_map(|p| p.publish().as_deref())
353            .map(|p| p.iter().collect::<HashSet<_>>())
354            .reduce(|xs, ys| xs.intersection(&ys).cloned().collect())
355            .unwrap_or_default();
356        if common_regs.is_empty() {
357            bail!("conflicts between `package.publish` fields in the selected packages");
358        } else {
359            bail!("--registry is required because not all `package.publish` settings agree",);
360        }
361    }
362}
363
364struct RegistryClient<'gctx>(&'gctx http_async::Client);
365
366impl<'gctx> crates_io::HttpClient for RegistryClient<'gctx> {
367    type Error = http_async::Error;
368
369    fn request(&self, req: http::Request<Vec<u8>>) -> Result<http::Response<Vec<u8>>, Self::Error> {
370        self.0.request_blocking(req)
371    }
372}