cargo/ops/registry/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! Operations that interact with the [registry web API][1].
//!
//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html

mod info;
mod login;
mod logout;
mod owner;
mod publish;
mod search;
mod yank;

use std::collections::HashSet;
use std::str;
use std::task::Poll;

use anyhow::{bail, format_err, Context as _};
use cargo_credential::{Operation, Secret};
use crates_io::Registry;
use url::Url;

use crate::core::{Package, PackageId, SourceId};
use crate::sources::source::Source;
use crate::sources::{RegistrySource, SourceConfigMap};
use crate::util::auth;
use crate::util::cache_lock::CacheLockMode;
use crate::util::context::{GlobalContext, PathAndArgs};
use crate::util::errors::CargoResult;
use crate::util::network::http::http_handle;

pub use self::info::info;
pub use self::login::registry_login;
pub use self::logout::registry_logout;
pub use self::owner::modify_owners;
pub use self::owner::OwnersOptions;
pub use self::publish::publish;
pub use self::publish::PublishOpts;
pub use self::search::search;
pub use self::yank::yank;

pub(crate) use self::publish::prepare_transmit;

/// Represents either `--registry` or `--index` argument, which is mutually exclusive.
#[derive(Debug, Clone)]
pub enum RegistryOrIndex {
    Registry(String),
    Index(Url),
}

impl RegistryOrIndex {
    fn is_index(&self) -> bool {
        matches!(self, RegistryOrIndex::Index(..))
    }
}

/// Registry settings loaded from config files.
///
/// This is loaded based on the `--registry` flag and the config settings.
#[derive(Debug, PartialEq)]
pub enum RegistryCredentialConfig {
    None,
    /// The authentication token.
    Token(Secret<String>),
    /// Process used for fetching a token.
    Process(Vec<PathAndArgs>),
    /// Secret Key and subject for Asymmetric tokens.
    AsymmetricKey((Secret<String>, Option<String>)),
}

impl RegistryCredentialConfig {
    /// Returns `true` if the credential is [`None`].
    ///
    /// [`None`]: Self::None
    pub fn is_none(&self) -> bool {
        matches!(self, Self::None)
    }
    /// Returns `true` if the credential is [`Token`].
    ///
    /// [`Token`]: Self::Token
    pub fn is_token(&self) -> bool {
        matches!(self, Self::Token(..))
    }
    /// Returns `true` if the credential is [`AsymmetricKey`].
    ///
    /// [`AsymmetricKey`]: RegistryCredentialConfig::AsymmetricKey
    pub fn is_asymmetric_key(&self) -> bool {
        matches!(self, Self::AsymmetricKey(..))
    }
    pub fn as_token(&self) -> Option<Secret<&str>> {
        if let Self::Token(v) = self {
            Some(v.as_deref())
        } else {
            None
        }
    }
    pub fn as_process(&self) -> Option<&Vec<PathAndArgs>> {
        if let Self::Process(v) = self {
            Some(v)
        } else {
            None
        }
    }
    pub fn as_asymmetric_key(&self) -> Option<&(Secret<String>, Option<String>)> {
        if let Self::AsymmetricKey(v) = self {
            Some(v)
        } else {
            None
        }
    }
}

/// Returns the `Registry` and `Source` based on command-line and config settings.
///
/// * `source_ids`: The source IDs for the registry. It contains the original source ID and
///   the replacement source ID.
/// * `token_from_cmdline`: The token from the command-line. If not set, uses the token
///   from the config.
/// * `index`: The index URL from the command-line.
/// * `registry`: The registry name from the command-line. If neither
///   `registry`, or `index` are set, then uses `crates-io`.
/// * `force_update`: If `true`, forces the index to be updated.
/// * `token_required`: If `true`, the token will be set.
fn registry<'gctx>(
    gctx: &'gctx GlobalContext,
    source_ids: &RegistrySourceIds,
    token_from_cmdline: Option<Secret<&str>>,
    reg_or_index: Option<&RegistryOrIndex>,
    force_update: bool,
    token_required: Option<Operation<'_>>,
) -> CargoResult<(Registry, RegistrySource<'gctx>)> {
    let is_index = reg_or_index.map(|v| v.is_index()).unwrap_or_default();
    if is_index && token_required.is_some() && token_from_cmdline.is_none() {
        bail!("command-line argument --index requires --token to be specified");
    }
    if let Some(token) = token_from_cmdline {
        auth::cache_token_from_commandline(gctx, &source_ids.original, token);
    }

    let mut src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), gctx)?;
    let cfg = {
        let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
        // Only update the index if `force_update` is set.
        if force_update {
            src.invalidate_cache()
        }
        let cfg = loop {
            match src.config()? {
                Poll::Pending => src
                    .block_until_ready()
                    .with_context(|| format!("failed to update {}", source_ids.replacement))?,
                Poll::Ready(cfg) => break cfg,
            }
        };
        cfg.expect("remote registries must have config")
    };
    let api_host = cfg
        .api
        .ok_or_else(|| format_err!("{} does not support API commands", source_ids.replacement))?;
    let token = if token_required.is_some() || cfg.auth_required {
        let operation = token_required.unwrap_or(Operation::Read);
        Some(auth::auth_token(
            gctx,
            &source_ids.original,
            None,
            operation,
            vec![],
            false,
        )?)
    } else {
        None
    };
    let handle = http_handle(gctx)?;
    Ok((
        Registry::new_handle(api_host, token, handle, cfg.auth_required),
        src,
    ))
}

/// Gets the SourceId for an index or registry setting.
///
/// The `index` and `reg` values are from the command-line or config settings.
/// If both are None, and no source-replacement is configured, returns the source for crates.io.
/// If both are None, and source replacement is configured, returns an error.
///
/// The source for crates.io may be GitHub, index.crates.io, or a test-only registry depending
/// on configuration.
///
/// If `reg` is set, source replacement is not followed.
///
/// The return value is a pair of `SourceId`s: The first may be a built-in replacement of
/// crates.io (such as index.crates.io), while the second is always the original source.
pub(crate) fn get_source_id(
    gctx: &GlobalContext,
    reg_or_index: Option<&RegistryOrIndex>,
) -> CargoResult<RegistrySourceIds> {
    let sid = get_initial_source_id(gctx, reg_or_index)?;
    let (builtin_replacement_sid, replacement_sid) = get_replacement_source_ids(gctx, sid)?;

    if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid {
        bail!(gen_replacement_error(replacement_sid));
    } else {
        Ok(RegistrySourceIds {
            original: sid,
            replacement: builtin_replacement_sid,
        })
    }
}

/// Very similar to [`get_source_id`], but is used when the `package_id` is known.
fn get_source_id_with_package_id(
    gctx: &GlobalContext,
    package_id: Option<PackageId>,
    reg_or_index: Option<&RegistryOrIndex>,
) -> CargoResult<(bool, RegistrySourceIds)> {
    let (use_package_source_id, sid) = match (&reg_or_index, package_id) {
        (None, Some(package_id)) => (true, package_id.source_id()),
        (None, None) => (false, SourceId::crates_io(gctx)?),
        (Some(RegistryOrIndex::Index(url)), None) => (false, SourceId::for_registry(url)?),
        (Some(RegistryOrIndex::Registry(r)), None) => (false, SourceId::alt_registry(gctx, r)?),
        (Some(reg_or_index), Some(package_id)) => {
            let sid = get_initial_source_id_from_registry_or_index(gctx, reg_or_index)?;
            let package_source_id = package_id.source_id();
            // 1. Same registry, use the package's source.
            // 2. Use the package's source if the specified registry is a replacement for the package's source.
            if sid == package_source_id
                || is_replacement_for_package_source(gctx, sid, package_source_id)?
            {
                (true, package_source_id)
            } else {
                (false, sid)
            }
        }
    };

    let (builtin_replacement_sid, replacement_sid) = get_replacement_source_ids(gctx, sid)?;

    if reg_or_index.is_none() && replacement_sid != builtin_replacement_sid {
        bail!(gen_replacement_error(replacement_sid));
    } else {
        Ok((
            use_package_source_id,
            RegistrySourceIds {
                original: sid,
                replacement: builtin_replacement_sid,
            },
        ))
    }
}

fn get_initial_source_id(
    gctx: &GlobalContext,
    reg_or_index: Option<&RegistryOrIndex>,
) -> CargoResult<SourceId> {
    match reg_or_index {
        None => SourceId::crates_io(gctx),
        Some(reg_or_index) => get_initial_source_id_from_registry_or_index(gctx, reg_or_index),
    }
}

fn get_initial_source_id_from_registry_or_index(
    gctx: &GlobalContext,
    reg_or_index: &RegistryOrIndex,
) -> CargoResult<SourceId> {
    match reg_or_index {
        RegistryOrIndex::Index(url) => SourceId::for_registry(url),
        RegistryOrIndex::Registry(r) => SourceId::alt_registry(gctx, r),
    }
}

fn get_replacement_source_ids(
    gctx: &GlobalContext,
    sid: SourceId,
) -> CargoResult<(SourceId, SourceId)> {
    let builtin_replacement_sid = SourceConfigMap::empty(gctx)?
        .load(sid, &HashSet::new())?
        .replaced_source_id();
    let replacement_sid = SourceConfigMap::new(gctx)?
        .load(sid, &HashSet::new())?
        .replaced_source_id();
    Ok((builtin_replacement_sid, replacement_sid))
}

fn is_replacement_for_package_source(
    gctx: &GlobalContext,
    sid: SourceId,
    package_source_id: SourceId,
) -> CargoResult<bool> {
    let pkg_source_replacement_sid = SourceConfigMap::new(gctx)?
        .load(package_source_id, &HashSet::new())?
        .replaced_source_id();
    Ok(pkg_source_replacement_sid == sid)
}

fn gen_replacement_error(replacement_sid: SourceId) -> String {
    // Neither --registry nor --index was passed and the user has configured source-replacement.
    let error_message = if let Some(replacement_name) = replacement_sid.alt_registry_key() {
        format!(
            "crates-io is replaced with remote registry {};\ninclude `--registry {}` or `--registry crates-io`",
            replacement_name, replacement_name
        )
    } else {
        format!(
            "crates-io is replaced with non-remote-registry source {};\ninclude `--registry crates-io` to use crates.io",
            replacement_sid
        )
    };

    error_message
}

pub(crate) struct RegistrySourceIds {
    /// Use when looking up the auth token, or writing out `Cargo.lock`
    pub(crate) original: SourceId,
    /// Use when interacting with the source (querying / publishing , etc)
    ///
    /// The source for crates.io may be replaced by a built-in source for accessing crates.io with
    /// the sparse protocol, or a source for the testing framework (when the replace_crates_io
    /// function is used)
    ///
    /// User-defined source replacement is not applied.
    pub(crate) replacement: SourceId,
}

/// If this set of packages has an unambiguous publish registry, find it.
pub(crate) fn infer_registry(pkgs: &[&Package]) -> CargoResult<Option<RegistryOrIndex>> {
    // Ignore "publish = false" packages while inferring the registry.
    let publishable_pkgs: Vec<_> = pkgs
        .iter()
        .filter(|p| p.publish() != &Some(Vec::new()))
        .collect();

    let Some((first, rest)) = publishable_pkgs.split_first() else {
        return Ok(None);
    };

    // If all packages have the same publish settings, we take that as the default.
    if rest.iter().all(|p| p.publish() == first.publish()) {
        match publishable_pkgs[0].publish().as_deref() {
            Some([unique_pkg_reg]) => {
                Ok(Some(RegistryOrIndex::Registry(unique_pkg_reg.to_owned())))
            }
            None | Some([]) => Ok(None),
            Some(regs) => {
                let mut regs: Vec<_> = regs.iter().map(|s| format!("\"{}\"", s)).collect();
                regs.sort();
                regs.dedup();
                // unwrap: the match block ensures that there's more than one reg.
                let (last_reg, regs) = regs.split_last().unwrap();
                bail!(
                    "--registry is required to disambiguate between {} or {} registries",
                    regs.join(", "),
                    last_reg
                )
            }
        }
    } else {
        let common_regs = publishable_pkgs
            .iter()
            // `None` means "all registries", so drop them instead of including them
            // in the intersection.
            .filter_map(|p| p.publish().as_deref())
            .map(|p| p.iter().collect::<HashSet<_>>())
            .reduce(|xs, ys| xs.intersection(&ys).cloned().collect())
            .unwrap_or_default();
        if common_regs.is_empty() {
            bail!("conflicts between `package.publish` fields in the selected packages");
        } else {
            bail!("--registry is required because not all `package.publish` settings agree",);
        }
    }
}