1 use std::collections::{BTreeMap, HashSet};
2 use std::fs::File;
3 use std::io::{self, BufRead};
4 use std::iter::repeat;
5 use std::path::PathBuf;
6 use std::str;
7 use std::time::Duration;
8 use std::{cmp, env};
9 
10 use anyhow::{bail, format_err, Context as _};
11 use cargo_util::paths;
12 use crates_io::{self, NewCrate, NewCrateDependency, Registry};
13 use curl::easy::{Easy, InfoType, SslOpt, SslVersion};
14 use log::{log, Level};
15 use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
16 
17 use crate::core::dependency::DepKind;
18 use crate::core::manifest::ManifestMetadata;
19 use crate::core::resolver::CliFeatures;
20 use crate::core::source::Source;
21 use crate::core::{Package, SourceId, Workspace};
22 use crate::ops;
23 use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_IO_REGISTRY};
24 use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange};
25 use crate::util::errors::CargoResult;
26 use crate::util::important_paths::find_root_manifest_for_wd;
27 use crate::util::validate_package_name;
28 use crate::util::IntoUrl;
29 use crate::{drop_print, drop_println, version};
30 
31 mod auth;
32 
33 /// Registry settings loaded from config files.
34 ///
35 /// This is loaded based on the `--registry` flag and the config settings.
36 #[derive(Debug)]
37 pub struct RegistryConfig {
38     /// The index URL. If `None`, use crates.io.
39     pub index: Option<String>,
40     /// The authentication token.
41     pub token: Option<String>,
42     /// Process used for fetching a token.
43     pub credential_process: Option<(PathBuf, Vec<String>)>,
44 }
45 
46 pub struct PublishOpts<'cfg> {
47     pub config: &'cfg Config,
48     pub token: Option<String>,
49     pub index: Option<String>,
50     pub verify: bool,
51     pub allow_dirty: bool,
52     pub jobs: Option<u32>,
53     pub to_publish: ops::Packages,
54     pub targets: Vec<String>,
55     pub dry_run: bool,
56     pub registry: Option<String>,
57     pub cli_features: CliFeatures,
58 }
59 
publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()>60 pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
61     let specs = opts.to_publish.to_package_id_specs(ws)?;
62     let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
63 
64     let (pkg, cli_features) = pkgs.pop().unwrap();
65 
66     let mut publish_registry = opts.registry.clone();
67     if let Some(ref allowed_registries) = *pkg.publish() {
68         if publish_registry.is_none() && allowed_registries.len() == 1 {
69             // If there is only one allowed registry, push to that one directly,
70             // even though there is no registry specified in the command.
71             let default_registry = &allowed_registries[0];
72             if default_registry != CRATES_IO_REGISTRY {
73                 // Don't change the registry for crates.io and don't warn the user.
74                 // crates.io will be defaulted even without this.
75                 opts.config.shell().note(&format!(
76                     "Found `{}` as only allowed registry. Publishing to it automatically.",
77                     default_registry
78                 ))?;
79                 publish_registry = Some(default_registry.clone());
80             }
81         }
82 
83         let reg_name = publish_registry
84             .clone()
85             .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string());
86         if !allowed_registries.contains(&reg_name) {
87             bail!(
88                 "`{}` cannot be published.\n\
89                  The registry `{}` is not listed in the `publish` value in Cargo.toml.",
90                 pkg.name(),
91                 reg_name
92             );
93         }
94     }
95 
96     let (mut registry, _reg_cfg, reg_id) = registry(
97         opts.config,
98         opts.token.clone(),
99         opts.index.clone(),
100         publish_registry,
101         true,
102         !opts.dry_run,
103     )?;
104     verify_dependencies(pkg, &registry, reg_id)?;
105 
106     // Prepare a tarball, with a non-suppressible warning if metadata
107     // is missing since this is being put online.
108     let tarball = ops::package_one(
109         ws,
110         pkg,
111         &ops::PackageOpts {
112             config: opts.config,
113             verify: opts.verify,
114             list: false,
115             check_metadata: true,
116             allow_dirty: opts.allow_dirty,
117             to_package: ops::Packages::Default,
118             targets: opts.targets.clone(),
119             jobs: opts.jobs,
120             cli_features: cli_features,
121         },
122     )?
123     .unwrap();
124 
125     opts.config
126         .shell()
127         .status("Uploading", pkg.package_id().to_string())?;
128     transmit(
129         opts.config,
130         pkg,
131         tarball.file(),
132         &mut registry,
133         reg_id,
134         opts.dry_run,
135     )?;
136 
137     Ok(())
138 }
139 
verify_dependencies( pkg: &Package, registry: &Registry, registry_src: SourceId, ) -> CargoResult<()>140 fn verify_dependencies(
141     pkg: &Package,
142     registry: &Registry,
143     registry_src: SourceId,
144 ) -> CargoResult<()> {
145     for dep in pkg.dependencies().iter() {
146         if super::check_dep_has_version(dep, true)? {
147             continue;
148         }
149         // TomlManifest::prepare_for_publish will rewrite the dependency
150         // to be just the `version` field.
151         if dep.source_id() != registry_src {
152             if !dep.source_id().is_registry() {
153                 // Consider making SourceId::kind a public type that we can
154                 // exhaustively match on. Using match can help ensure that
155                 // every kind is properly handled.
156                 panic!("unexpected source kind for dependency {:?}", dep);
157             }
158             // Block requests to send to crates.io with alt-registry deps.
159             // This extra hostname check is mostly to assist with testing,
160             // but also prevents someone using `--index` to specify
161             // something that points to crates.io.
162             if registry_src.is_default_registry() || registry.host_is_crates_io() {
163                 bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
164                        registries. `{}` needs to be published to crates.io before publishing this crate.\n\
165                        (crate `{}` is pulled from {})",
166                       dep.package_name(),
167                       dep.package_name(),
168                       dep.source_id());
169             }
170         }
171     }
172     Ok(())
173 }
174 
transmit( config: &Config, pkg: &Package, tarball: &File, registry: &mut Registry, registry_id: SourceId, dry_run: bool, ) -> CargoResult<()>175 fn transmit(
176     config: &Config,
177     pkg: &Package,
178     tarball: &File,
179     registry: &mut Registry,
180     registry_id: SourceId,
181     dry_run: bool,
182 ) -> CargoResult<()> {
183     let deps = pkg
184         .dependencies()
185         .iter()
186         .filter(|dep| {
187             // Skip dev-dependency without version.
188             dep.is_transitive() || dep.specified_req()
189         })
190         .map(|dep| {
191             // If the dependency is from a different registry, then include the
192             // registry in the dependency.
193             let dep_registry_id = match dep.registry_id() {
194                 Some(id) => id,
195                 None => SourceId::crates_io(config)?,
196             };
197             // In the index and Web API, None means "from the same registry"
198             // whereas in Cargo.toml, it means "from crates.io".
199             let dep_registry = if dep_registry_id != registry_id {
200                 Some(dep_registry_id.url().to_string())
201             } else {
202                 None
203             };
204 
205             Ok(NewCrateDependency {
206                 optional: dep.is_optional(),
207                 default_features: dep.uses_default_features(),
208                 name: dep.package_name().to_string(),
209                 features: dep.features().iter().map(|s| s.to_string()).collect(),
210                 version_req: dep.version_req().to_string(),
211                 target: dep.platform().map(|s| s.to_string()),
212                 kind: match dep.kind() {
213                     DepKind::Normal => "normal",
214                     DepKind::Build => "build",
215                     DepKind::Development => "dev",
216                 }
217                 .to_string(),
218                 registry: dep_registry,
219                 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
220             })
221         })
222         .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
223     let manifest = pkg.manifest();
224     let ManifestMetadata {
225         ref authors,
226         ref description,
227         ref homepage,
228         ref documentation,
229         ref keywords,
230         ref readme,
231         ref repository,
232         ref license,
233         ref license_file,
234         ref categories,
235         ref badges,
236         ref links,
237     } = *manifest.metadata();
238     let readme_content = readme
239         .as_ref()
240         .map(|readme| {
241             paths::read(&pkg.root().join(readme))
242                 .with_context(|| format!("failed to read `readme` file for package `{}`", pkg))
243         })
244         .transpose()?;
245     if let Some(ref file) = *license_file {
246         if !pkg.root().join(file).exists() {
247             bail!("the license file `{}` does not exist", file)
248         }
249     }
250 
251     // Do not upload if performing a dry run
252     if dry_run {
253         config.shell().warn("aborting upload due to dry run")?;
254         return Ok(());
255     }
256 
257     let string_features = match manifest.original().features() {
258         Some(features) => features
259             .iter()
260             .map(|(feat, values)| {
261                 (
262                     feat.to_string(),
263                     values.iter().map(|fv| fv.to_string()).collect(),
264                 )
265             })
266             .collect::<BTreeMap<String, Vec<String>>>(),
267         None => BTreeMap::new(),
268     };
269 
270     let warnings = registry
271         .publish(
272             &NewCrate {
273                 name: pkg.name().to_string(),
274                 vers: pkg.version().to_string(),
275                 deps,
276                 features: string_features,
277                 authors: authors.clone(),
278                 description: description.clone(),
279                 homepage: homepage.clone(),
280                 documentation: documentation.clone(),
281                 keywords: keywords.clone(),
282                 categories: categories.clone(),
283                 readme: readme_content,
284                 readme_file: readme.clone(),
285                 repository: repository.clone(),
286                 license: license.clone(),
287                 license_file: license_file.clone(),
288                 badges: badges.clone(),
289                 links: links.clone(),
290                 v: None,
291             },
292             tarball,
293         )
294         .with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
295 
296     if !warnings.invalid_categories.is_empty() {
297         let msg = format!(
298             "the following are not valid category slugs and were \
299              ignored: {}. Please see https://crates.io/category_slugs \
300              for the list of all category slugs. \
301              ",
302             warnings.invalid_categories.join(", ")
303         );
304         config.shell().warn(&msg)?;
305     }
306 
307     if !warnings.invalid_badges.is_empty() {
308         let msg = format!(
309             "the following are not valid badges and were ignored: {}. \
310              Either the badge type specified is unknown or a required \
311              attribute is missing. Please see \
312              https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
313              for valid badge types and their required attributes.",
314             warnings.invalid_badges.join(", ")
315         );
316         config.shell().warn(&msg)?;
317     }
318 
319     if !warnings.other.is_empty() {
320         for msg in warnings.other {
321             config.shell().warn(&msg)?;
322         }
323     }
324 
325     Ok(())
326 }
327 
328 /// Returns the index and token from the config file for the given registry.
329 ///
330 /// `registry` is typically the registry specified on the command-line. If
331 /// `None`, `index` is set to `None` to indicate it should use crates.io.
registry_configuration( config: &Config, registry: Option<&str>, ) -> CargoResult<RegistryConfig>332 pub fn registry_configuration(
333     config: &Config,
334     registry: Option<&str>,
335 ) -> CargoResult<RegistryConfig> {
336     let err_both = |token_key: &str, proc_key: &str| {
337         Err(format_err!(
338             "both `{TOKEN_KEY}` and `{PROC_KEY}` \
339              were specified in the config\n\
340              Only one of these values may be set, remove one or the other to proceed.",
341             TOKEN_KEY = token_key,
342             PROC_KEY = proc_key,
343         ))
344     };
345     // `registry.default` is handled in command-line parsing.
346     let (index, token, process) = match registry {
347         Some(registry) => {
348             validate_package_name(registry, "registry name", "")?;
349             let index = Some(config.get_registry_index(registry)?.to_string());
350             let token_key = format!("registries.{}.token", registry);
351             let token = config.get_string(&token_key)?.map(|p| p.val);
352             let process = if config.cli_unstable().credential_process {
353                 let mut proc_key = format!("registries.{}.credential-process", registry);
354                 let mut process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
355                 if process.is_none() && token.is_none() {
356                     // This explicitly ignores the global credential-process if
357                     // the token is set, as that is "more specific".
358                     proc_key = String::from("registry.credential-process");
359                     process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
360                 } else if process.is_some() && token.is_some() {
361                     return err_both(&token_key, &proc_key);
362                 }
363                 process
364             } else {
365                 None
366             };
367             (index, token, process)
368         }
369         None => {
370             // Use crates.io default.
371             config.check_registry_index_not_set()?;
372             let token = config.get_string("registry.token")?.map(|p| p.val);
373             let process = if config.cli_unstable().credential_process {
374                 let process =
375                     config.get::<Option<config::PathAndArgs>>("registry.credential-process")?;
376                 if token.is_some() && process.is_some() {
377                     return err_both("registry.token", "registry.credential-process");
378                 }
379                 process
380             } else {
381                 None
382             };
383             (None, token, process)
384         }
385     };
386 
387     let credential_process =
388         process.map(|process| (process.path.resolve_program(config), process.args));
389 
390     Ok(RegistryConfig {
391         index,
392         token,
393         credential_process,
394     })
395 }
396 
397 /// Returns the `Registry` and `Source` based on command-line and config settings.
398 ///
399 /// * `token`: The token from the command-line. If not set, uses the token
400 ///   from the config.
401 /// * `index`: The index URL from the command-line. This is ignored if
402 ///   `registry` is set.
403 /// * `registry`: The registry name from the command-line. If neither
404 ///   `registry`, or `index` are set, then uses `crates-io`, honoring
405 ///   `[source]` replacement if defined.
406 /// * `force_update`: If `true`, forces the index to be updated.
407 /// * `validate_token`: If `true`, the token must be set.
registry( config: &Config, token: Option<String>, index: Option<String>, registry: Option<String>, force_update: bool, validate_token: bool, ) -> CargoResult<(Registry, RegistryConfig, SourceId)>408 fn registry(
409     config: &Config,
410     token: Option<String>,
411     index: Option<String>,
412     registry: Option<String>,
413     force_update: bool,
414     validate_token: bool,
415 ) -> CargoResult<(Registry, RegistryConfig, SourceId)> {
416     if index.is_some() && registry.is_some() {
417         // Otherwise we would silently ignore one or the other.
418         bail!("both `--index` and `--registry` should not be set at the same time");
419     }
420     // Parse all configuration options
421     let reg_cfg = registry_configuration(config, registry.as_deref())?;
422     let opt_index = reg_cfg.index.as_ref().or_else(|| index.as_ref());
423     let sid = get_source_id(config, opt_index, registry.as_ref())?;
424     if !sid.is_remote_registry() {
425         bail!(
426             "{} does not support API commands.\n\
427              Check for a source-replacement in .cargo/config.",
428             sid
429         );
430     }
431     let api_host = {
432         let _lock = config.acquire_package_cache_lock()?;
433         let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
434         // Only update the index if the config is not available or `force` is set.
435         let cfg = src.config();
436         let mut updated_cfg = || {
437             src.update()
438                 .with_context(|| format!("failed to update {}", sid))?;
439             src.config()
440         };
441 
442         let cfg = if force_update {
443             updated_cfg()?
444         } else {
445             cfg.or_else(|_| updated_cfg())?
446         };
447 
448         cfg.and_then(|cfg| cfg.api)
449             .ok_or_else(|| format_err!("{} does not support API commands", sid))?
450     };
451     let token = if validate_token {
452         if index.is_some() {
453             if token.is_none() {
454                 bail!("command-line argument --index requires --token to be specified");
455             }
456             token
457         } else {
458             // Check `is_default_registry` so that the crates.io index can
459             // change config.json's "api" value, and this won't affect most
460             // people. It will affect those using source replacement, but
461             // hopefully that's a relatively small set of users.
462             if token.is_none()
463                 && reg_cfg.token.is_some()
464                 && registry.is_none()
465                 && !sid.is_default_registry()
466                 && !crates_io::is_url_crates_io(&api_host)
467             {
468                 config.shell().warn(
469                     "using `registry.token` config value with source \
470                         replacement is deprecated\n\
471                         This may become a hard error in the future; \
472                         see <https://github.com/rust-lang/cargo/issues/xxx>.\n\
473                         Use the --token command-line flag to remove this warning.",
474                 )?;
475                 reg_cfg.token.clone()
476             } else {
477                 let token = auth::auth_token(
478                     config,
479                     token.as_deref(),
480                     reg_cfg.token.as_deref(),
481                     reg_cfg.credential_process.as_ref(),
482                     registry.as_deref(),
483                     &api_host,
484                 )?;
485                 Some(token)
486             }
487         }
488     } else {
489         None
490     };
491     let handle = http_handle(config)?;
492     Ok((Registry::new_handle(api_host, token, handle), reg_cfg, sid))
493 }
494 
495 /// Creates a new HTTP handle with appropriate global configuration for cargo.
http_handle(config: &Config) -> CargoResult<Easy>496 pub fn http_handle(config: &Config) -> CargoResult<Easy> {
497     let (mut handle, timeout) = http_handle_and_timeout(config)?;
498     timeout.configure(&mut handle)?;
499     Ok(handle)
500 }
501 
http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)>502 pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> {
503     if config.frozen() {
504         bail!(
505             "attempting to make an HTTP request, but --frozen was \
506              specified"
507         )
508     }
509     if !config.network_allowed() {
510         bail!("can't make HTTP request in the offline mode")
511     }
512 
513     // The timeout option for libcurl by default times out the entire transfer,
514     // but we probably don't want this. Instead we only set timeouts for the
515     // connect phase as well as a "low speed" timeout so if we don't receive
516     // many bytes in a large-ish period of time then we time out.
517     let mut handle = Easy::new();
518     let timeout = configure_http_handle(config, &mut handle)?;
519     Ok((handle, timeout))
520 }
521 
needs_custom_http_transport(config: &Config) -> CargoResult<bool>522 pub fn needs_custom_http_transport(config: &Config) -> CargoResult<bool> {
523     Ok(http_proxy_exists(config)?
524         || *config.http_config()? != Default::default()
525         || env::var_os("HTTP_TIMEOUT").is_some())
526 }
527 
528 /// Configure a libcurl http handle with the defaults options for Cargo
configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout>529 pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout> {
530     let http = config.http_config()?;
531     if let Some(proxy) = http_proxy(config)? {
532         handle.proxy(&proxy)?;
533     }
534     if let Some(cainfo) = &http.cainfo {
535         let cainfo = cainfo.resolve_path(config);
536         handle.cainfo(&cainfo)?;
537     }
538     if let Some(check) = http.check_revoke {
539         handle.ssl_options(SslOpt::new().no_revoke(!check))?;
540     }
541 
542     if let Some(user_agent) = &http.user_agent {
543         handle.useragent(user_agent)?;
544     } else {
545         handle.useragent(&format!("cargo {}", version()))?;
546     }
547 
548     fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
549         let version = match s {
550             "default" => SslVersion::Default,
551             "tlsv1" => SslVersion::Tlsv1,
552             "tlsv1.0" => SslVersion::Tlsv10,
553             "tlsv1.1" => SslVersion::Tlsv11,
554             "tlsv1.2" => SslVersion::Tlsv12,
555             "tlsv1.3" => SslVersion::Tlsv13,
556             _ => bail!(
557                 "Invalid ssl version `{}`,\
558                  choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'.",
559                 s
560             ),
561         };
562         Ok(version)
563     }
564     if let Some(ssl_version) = &http.ssl_version {
565         match ssl_version {
566             SslVersionConfig::Single(s) => {
567                 let version = to_ssl_version(s.as_str())?;
568                 handle.ssl_version(version)?;
569             }
570             SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
571                 let min_version = min
572                     .as_ref()
573                     .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
574                 let max_version = max
575                     .as_ref()
576                     .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
577                 handle.ssl_min_max_version(min_version, max_version)?;
578             }
579         }
580     }
581 
582     if let Some(true) = http.debug {
583         handle.verbose(true)?;
584         log::debug!("{:#?}", curl::Version::get());
585         handle.debug_function(|kind, data| {
586             let (prefix, level) = match kind {
587                 InfoType::Text => ("*", Level::Debug),
588                 InfoType::HeaderIn => ("<", Level::Debug),
589                 InfoType::HeaderOut => (">", Level::Debug),
590                 InfoType::DataIn => ("{", Level::Trace),
591                 InfoType::DataOut => ("}", Level::Trace),
592                 InfoType::SslDataIn | InfoType::SslDataOut => return,
593                 _ => return,
594             };
595             match str::from_utf8(data) {
596                 Ok(s) => {
597                     for mut line in s.lines() {
598                         if line.starts_with("Authorization:") {
599                             line = "Authorization: [REDACTED]";
600                         } else if line[..line.len().min(10)].eq_ignore_ascii_case("set-cookie") {
601                             line = "set-cookie: [REDACTED]";
602                         }
603                         log!(level, "http-debug: {} {}", prefix, line);
604                     }
605                 }
606                 Err(_) => {
607                     log!(
608                         level,
609                         "http-debug: {} ({} bytes of data)",
610                         prefix,
611                         data.len()
612                     );
613                 }
614             }
615         })?;
616     }
617 
618     HttpTimeout::new(config)
619 }
620 
621 #[must_use]
622 pub struct HttpTimeout {
623     pub dur: Duration,
624     pub low_speed_limit: u32,
625 }
626 
627 impl HttpTimeout {
new(config: &Config) -> CargoResult<HttpTimeout>628     pub fn new(config: &Config) -> CargoResult<HttpTimeout> {
629         let config = config.http_config()?;
630         let low_speed_limit = config.low_speed_limit.unwrap_or(10);
631         let seconds = config
632             .timeout
633             .or_else(|| env::var("HTTP_TIMEOUT").ok().and_then(|s| s.parse().ok()))
634             .unwrap_or(30);
635         Ok(HttpTimeout {
636             dur: Duration::new(seconds, 0),
637             low_speed_limit,
638         })
639     }
640 
configure(&self, handle: &mut Easy) -> CargoResult<()>641     pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
642         // The timeout option for libcurl by default times out the entire
643         // transfer, but we probably don't want this. Instead we only set
644         // timeouts for the connect phase as well as a "low speed" timeout so
645         // if we don't receive many bytes in a large-ish period of time then we
646         // time out.
647         handle.connect_timeout(self.dur)?;
648         handle.low_speed_time(self.dur)?;
649         handle.low_speed_limit(self.low_speed_limit)?;
650         Ok(())
651     }
652 }
653 
654 /// Finds an explicit HTTP proxy if one is available.
655 ///
656 /// Favor cargo's `http.proxy`, then git's `http.proxy`. Proxies specified
657 /// via environment variables are picked up by libcurl.
http_proxy(config: &Config) -> CargoResult<Option<String>>658 fn http_proxy(config: &Config) -> CargoResult<Option<String>> {
659     let http = config.http_config()?;
660     if let Some(s) = &http.proxy {
661         return Ok(Some(s.clone()));
662     }
663     if let Ok(cfg) = git2::Config::open_default() {
664         if let Ok(s) = cfg.get_string("http.proxy") {
665             return Ok(Some(s));
666         }
667     }
668     Ok(None)
669 }
670 
671 /// Determine if an http proxy exists.
672 ///
673 /// Checks the following for existence, in order:
674 ///
675 /// * cargo's `http.proxy`
676 /// * git's `http.proxy`
677 /// * `http_proxy` env var
678 /// * `HTTP_PROXY` env var
679 /// * `https_proxy` env var
680 /// * `HTTPS_PROXY` env var
http_proxy_exists(config: &Config) -> CargoResult<bool>681 fn http_proxy_exists(config: &Config) -> CargoResult<bool> {
682     if http_proxy(config)?.is_some() {
683         Ok(true)
684     } else {
685         Ok(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]
686             .iter()
687             .any(|v| env::var(v).is_ok()))
688     }
689 }
690 
registry_login( config: &Config, token: Option<String>, reg: Option<String>, ) -> CargoResult<()>691 pub fn registry_login(
692     config: &Config,
693     token: Option<String>,
694     reg: Option<String>,
695 ) -> CargoResult<()> {
696     let (registry, reg_cfg, _) = registry(config, token.clone(), None, reg.clone(), false, false)?;
697 
698     let token = match token {
699         Some(token) => token,
700         None => {
701             drop_println!(
702                 config,
703                 "please paste the API Token found on {}/me below",
704                 registry.host()
705             );
706             let mut line = String::new();
707             let input = io::stdin();
708             input
709                 .lock()
710                 .read_line(&mut line)
711                 .with_context(|| "failed to read stdin")?;
712             // Automatically remove `cargo login` from an inputted token to
713             // allow direct pastes from `registry.host()`/me.
714             line.replace("cargo login", "").trim().to_string()
715         }
716     };
717 
718     if let Some(old_token) = &reg_cfg.token {
719         if old_token == &token {
720             config.shell().status("Login", "already logged in")?;
721             return Ok(());
722         }
723     }
724 
725     auth::login(
726         config,
727         token,
728         reg_cfg.credential_process.as_ref(),
729         reg.as_deref(),
730         registry.host(),
731     )?;
732 
733     config.shell().status(
734         "Login",
735         format!(
736             "token for `{}` saved",
737             reg.as_ref().map_or(CRATES_IO_DOMAIN, String::as_str)
738         ),
739     )?;
740     Ok(())
741 }
742 
registry_logout(config: &Config, reg: Option<String>) -> CargoResult<()>743 pub fn registry_logout(config: &Config, reg: Option<String>) -> CargoResult<()> {
744     let (registry, reg_cfg, _) = registry(config, None, None, reg.clone(), false, false)?;
745     let reg_name = reg.as_deref().unwrap_or(CRATES_IO_DOMAIN);
746     if reg_cfg.credential_process.is_none() && reg_cfg.token.is_none() {
747         config.shell().status(
748             "Logout",
749             format!("not currently logged in to `{}`", reg_name),
750         )?;
751         return Ok(());
752     }
753     auth::logout(
754         config,
755         reg_cfg.credential_process.as_ref(),
756         reg.as_deref(),
757         registry.host(),
758     )?;
759     config.shell().status(
760         "Logout",
761         format!(
762             "token for `{}` has been removed from local storage",
763             reg_name
764         ),
765     )?;
766     Ok(())
767 }
768 
769 pub struct OwnersOptions {
770     pub krate: Option<String>,
771     pub token: Option<String>,
772     pub index: Option<String>,
773     pub to_add: Option<Vec<String>>,
774     pub to_remove: Option<Vec<String>>,
775     pub list: bool,
776     pub registry: Option<String>,
777 }
778 
modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()>779 pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> {
780     let name = match opts.krate {
781         Some(ref name) => name.clone(),
782         None => {
783             let manifest_path = find_root_manifest_for_wd(config.cwd())?;
784             let ws = Workspace::new(&manifest_path, config)?;
785             ws.current()?.package_id().name().to_string()
786         }
787     };
788 
789     let (mut registry, _, _) = registry(
790         config,
791         opts.token.clone(),
792         opts.index.clone(),
793         opts.registry.clone(),
794         true,
795         true,
796     )?;
797 
798     if let Some(ref v) = opts.to_add {
799         let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
800         let msg = registry.add_owners(&name, &v).with_context(|| {
801             format!(
802                 "failed to invite owners to crate `{}` on registry at {}",
803                 name,
804                 registry.host()
805             )
806         })?;
807 
808         config.shell().status("Owner", msg)?;
809     }
810 
811     if let Some(ref v) = opts.to_remove {
812         let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
813         config
814             .shell()
815             .status("Owner", format!("removing {:?} from crate {}", v, name))?;
816         registry.remove_owners(&name, &v).with_context(|| {
817             format!(
818                 "failed to remove owners from crate `{}` on registry at {}",
819                 name,
820                 registry.host()
821             )
822         })?;
823     }
824 
825     if opts.list {
826         let owners = registry.list_owners(&name).with_context(|| {
827             format!(
828                 "failed to list owners of crate `{}` on registry at {}",
829                 name,
830                 registry.host()
831             )
832         })?;
833         for owner in owners.iter() {
834             drop_print!(config, "{}", owner.login);
835             match (owner.name.as_ref(), owner.email.as_ref()) {
836                 (Some(name), Some(email)) => drop_println!(config, " ({} <{}>)", name, email),
837                 (Some(s), None) | (None, Some(s)) => drop_println!(config, " ({})", s),
838                 (None, None) => drop_println!(config),
839             }
840         }
841     }
842 
843     Ok(())
844 }
845 
yank( config: &Config, krate: Option<String>, version: Option<String>, token: Option<String>, index: Option<String>, undo: bool, reg: Option<String>, ) -> CargoResult<()>846 pub fn yank(
847     config: &Config,
848     krate: Option<String>,
849     version: Option<String>,
850     token: Option<String>,
851     index: Option<String>,
852     undo: bool,
853     reg: Option<String>,
854 ) -> CargoResult<()> {
855     let name = match krate {
856         Some(name) => name,
857         None => {
858             let manifest_path = find_root_manifest_for_wd(config.cwd())?;
859             let ws = Workspace::new(&manifest_path, config)?;
860             ws.current()?.package_id().name().to_string()
861         }
862     };
863     let version = match version {
864         Some(v) => v,
865         None => bail!("a version must be specified to yank"),
866     };
867 
868     let (mut registry, _, _) = registry(config, token, index, reg, true, true)?;
869 
870     if undo {
871         config
872             .shell()
873             .status("Unyank", format!("{}:{}", name, version))?;
874         registry.unyank(&name, &version).with_context(|| {
875             format!(
876                 "failed to undo a yank from the registry at {}",
877                 registry.host()
878             )
879         })?;
880     } else {
881         config
882             .shell()
883             .status("Yank", format!("{}:{}", name, version))?;
884         registry
885             .yank(&name, &version)
886             .with_context(|| format!("failed to yank from the registry at {}", registry.host()))?;
887     }
888 
889     Ok(())
890 }
891 
892 /// Gets the SourceId for an index or registry setting.
893 ///
894 /// The `index` and `reg` values are from the command-line or config settings.
895 /// If both are None, returns the source for crates.io.
get_source_id( config: &Config, index: Option<&String>, reg: Option<&String>, ) -> CargoResult<SourceId>896 fn get_source_id(
897     config: &Config,
898     index: Option<&String>,
899     reg: Option<&String>,
900 ) -> CargoResult<SourceId> {
901     match (reg, index) {
902         (Some(r), _) => SourceId::alt_registry(config, r),
903         (_, Some(i)) => SourceId::for_registry(&i.into_url()?),
904         _ => {
905             let map = SourceConfigMap::new(config)?;
906             let src = map.load(SourceId::crates_io(config)?, &HashSet::new())?;
907             Ok(src.replaced_source_id())
908         }
909     }
910 }
911 
search( query: &str, config: &Config, index: Option<String>, limit: u32, reg: Option<String>, ) -> CargoResult<()>912 pub fn search(
913     query: &str,
914     config: &Config,
915     index: Option<String>,
916     limit: u32,
917     reg: Option<String>,
918 ) -> CargoResult<()> {
919     fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
920         // We should truncate at grapheme-boundary and compute character-widths,
921         // yet the dependencies on unicode-segmentation and unicode-width are
922         // not worth it.
923         let mut chars = s.chars();
924         let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
925         if chars.next().is_some() {
926             prefix.push('…');
927         }
928         prefix
929     }
930 
931     let (mut registry, _, source_id) = registry(config, None, index, reg, false, false)?;
932     let (crates, total_crates) = registry.search(query, limit).with_context(|| {
933         format!(
934             "failed to retrieve search results from the registry at {}",
935             registry.host()
936         )
937     })?;
938 
939     let names = crates
940         .iter()
941         .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version))
942         .collect::<Vec<String>>();
943 
944     let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default();
945 
946     let description_length = cmp::max(80, 128 - description_margin);
947 
948     let descriptions = crates.iter().map(|krate| {
949         krate
950             .description
951             .as_ref()
952             .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length))
953     });
954 
955     for (name, description) in names.into_iter().zip(descriptions) {
956         let line = match description {
957             Some(desc) => {
958                 let space = repeat(' ')
959                     .take(description_margin - name.len())
960                     .collect::<String>();
961                 name + &space + "# " + &desc
962             }
963             None => name,
964         };
965         drop_println!(config, "{}", line);
966     }
967 
968     let search_max_limit = 100;
969     if total_crates > limit && limit < search_max_limit {
970         drop_println!(
971             config,
972             "... and {} crates more (use --limit N to see more)",
973             total_crates - limit
974         );
975     } else if total_crates > limit && limit >= search_max_limit {
976         let extra = if source_id.is_default_registry() {
977             format!(
978                 " (go to https://crates.io/search?q={} to see more)",
979                 percent_encode(query.as_bytes(), NON_ALPHANUMERIC)
980             )
981         } else {
982             String::new()
983         };
984         drop_println!(
985             config,
986             "... and {} crates more{}",
987             total_crates - limit,
988             extra
989         );
990     }
991 
992     Ok(())
993 }
994