Skip to main content

cargo/ops/
registry.rs

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