cargo_update/ops/mod.rs
1//! Main functions doing actual work.
2//!
3//! Use `installed_registry_packages()` to list the installed packages,
4//! then use `intersect_packages()` to confirm which ones should be updated,
5//! poll the packages' latest versions by calling `RegistryPackage::pull_version()` on them,
6//! continue with doing whatever you wish.
7
8
9use git2::{self, ErrorCode as GitErrorCode, Config as GitConfig, Error as GitError, Cred as GitCred, RemoteCallbacks, CredentialType, FetchOptions,
10 ProxyOptions, Repository, Blob, Tree, Oid};
11use curl::easy::{WriteError as CurlWriteError, Handler as CurlHandler, SslOpt as CurlSslOpt, Easy2 as CurlEasy, List as CurlList};
12use semver::{VersionReq as SemverReq, Version as Semver};
13#[cfg(target_vendor = "apple")]
14use security_framework::os::macos::keychain::SecKeychain;
15#[cfg(target_os = "windows")]
16use windows::Win32::Security::Credentials as WinCred;
17use std::io::{self, ErrorKind as IoErrorKind, BufWriter, BufReader, BufRead, Write};
18use std::collections::{BTreeMap, BTreeSet};
19use std::{slice, cmp, env, mem, str, fs};
20use chrono::{FixedOffset, DateTime, Utc};
21use curl::multi::{Multi as CurlMulti, Easy2Handle as CurlEasyHandle};
22use std::process::{Command, Stdio};
23use std::ffi::{OsString, OsStr};
24use std::path::{PathBuf, Path};
25use std::hash::{Hasher, Hash};
26use std::iter::FromIterator;
27#[cfg(target_os = "windows")]
28use windows::core::PCSTR;
29use std::time::Duration;
30#[cfg(all(unix, not(target_vendor = "apple")))]
31use std::sync::LazyLock;
32use serde_json as json;
33use std::borrow::Cow;
34use std::sync::Mutex;
35#[cfg(any(target_os = "windows", all(unix, not(target_vendor = "apple"))))]
36use std::ptr;
37use url::Url;
38use toml;
39use hex;
40
41mod config;
42
43pub use self::config::*;
44
45
46// cargo-audit 0.17.5 (registry+https://github.com/rust-lang/crates.io-index)
47// cargo-audit 0.17.5 (sparse+https://index.crates.io/)
48// -> (name, version, registry)
49// ("cargo-audit", "0.17.5", "https://github.com/rust-lang/crates.io-index")
50// ("cargo-audit", "0.17.5", "https://index.crates.io/")
51fn parse_registry_package_ident(ident: &str) -> Option<(&str, &str, &str)> {
52 let mut idx = ident.splitn(3, ' ');
53 let (name, version, mut reg) = (idx.next()?, idx.next()?, idx.next()?);
54 reg = reg.strip_prefix('(')?.strip_suffix(')')?;
55 Some((name, version, reg.strip_prefix("registry+").or_else(|| reg.strip_prefix("sparse+"))?))
56}
57// alacritty 0.1.0 (git+https://github.com/jwilm/alacritty#eb231b3e70b87875df4bdd1974d5e94704024d70)
58// chattium-oxide-client 0.1.0
59// (git+https://github.com/nabijaczleweli/chattium-oxide-client?branch=master#108a7b94f0e0dcb2a875f70fc0459d5a682df14c)
60// -> (name, url, sha)
61// ("alacritty", "https://github.com/jwilm/alacritty", "eb231b3e70b87875df4bdd1974d5e94704024d70")
62// ("chattium-oxide-client", "https://github.com/nabijaczleweli/chattium-oxide-client?branch=master",
63// "108a7b94f0e0dcb2a875f70fc0459d5a682df14c")
64fn parse_git_package_ident(ident: &str) -> Option<(&str, &str, &str)> {
65 let mut idx = ident.splitn(3, ' ');
66 let (name, _, blob) = (idx.next()?, idx.next()?, idx.next()?);
67 let (url, sha) = blob.strip_prefix("(git+")?.strip_suffix(')')?.split_once('#')?;
68 if sha.len() != 40 {
69 return None;
70 }
71 Some((name, url, sha))
72}
73
74
75/// A representation of a package from the main [`crates.io`](https://crates.io) repository.
76///
77/// The newest version of a package is pulled from [`crates.io`](https://crates.io) via `pull_version()`.
78///
79/// The `parse()` function parses the format used in `$HOME/.cargo/.crates.toml`.
80///
81/// # Examples
82///
83/// ```
84/// # extern crate cargo_update;
85/// # extern crate semver;
86/// # use cargo_update::ops::RegistryPackage;
87/// # use semver::Version as Semver;
88/// # fn main() {
89/// let package_s = "racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)";
90/// let mut package = RegistryPackage::parse(package_s, vec!["racer.exe".to_string()]).unwrap();
91/// assert_eq!(package,
92/// RegistryPackage {
93/// name: "racer".to_string(),
94/// registry: "https://github.com/rust-lang/crates.io-index".into(),
95/// version: Some(Semver::parse("1.2.10").unwrap()),
96/// newest_version: None,
97/// alternative_version: None,
98/// max_version: None,
99/// executables: vec!["racer.exe".to_string()],
100/// });
101///
102/// # /*
103/// package.pull_version(®istry_tree, ®istry);
104/// # */
105/// # package.newest_version = Some(Semver::parse("1.2.11").unwrap());
106/// assert!(package.newest_version.is_some());
107/// # }
108/// ```
109#[derive(Debug, Clone, Hash, PartialEq, Eq)]
110pub struct RegistryPackage {
111 /// The package's name.
112 ///
113 /// Go to `https://crates.io/crates/{name}` to get the crate info, if available on the main repository.
114 pub name: String,
115 /// The registry the package is available from.
116 ///
117 /// Can be a name from ~/.cargo/config.
118 ///
119 /// The main repository is `https://github.com/rust-lang/crates.io-index`, or `sparse+https://index.crates.io/`.
120 pub registry: Cow<'static, str>,
121 /// The package's locally installed version.
122 pub version: Option<Semver>,
123 /// The latest version of the package, available at [`crates.io`](https://crates.io), if in main repository.
124 ///
125 /// `None` by default, acquire via `RegistryPackage::pull_version()`.
126 pub newest_version: Option<Semver>,
127 /// If present, the alternative newest version not chosen because of unfulfilled requirements like (not) being a prerelease.
128 pub alternative_version: Option<Semver>,
129 /// User-bounded maximum version to update up to.
130 pub max_version: Option<Semver>,
131 /// Executables currently installed for this package.
132 pub executables: Vec<String>,
133}
134
135/// A representation of a package a remote git repository.
136///
137/// The newest commit is pulled from that repo via `pull_version()`.
138///
139/// The `parse()` function parses the format used in `$HOME/.cargo/.crates.toml`.
140///
141/// # Examples
142///
143/// ```
144/// # extern crate cargo_update;
145/// # extern crate git2;
146/// # use cargo_update::ops::GitRepoPackage;
147/// # fn main() {
148/// let package_s = "alacritty 0.1.0 (git+https://github.com/jwilm/alacritty#eb231b3e70b87875df4bdd1974d5e94704024d70)";
149/// let mut package = GitRepoPackage::parse(package_s, vec!["alacritty".to_string()]).unwrap();
150/// assert_eq!(package,
151/// GitRepoPackage {
152/// name: "alacritty".to_string(),
153/// url: "https://github.com/jwilm/alacritty".to_string(),
154/// branch: None,
155/// id: git2::Oid::from_str("eb231b3e70b87875df4bdd1974d5e94704024d70").unwrap(),
156/// newest_id: Err(git2::Error::from_str("")),
157/// executables: vec!["alacritty".to_string()],
158/// });
159///
160/// # /*
161/// package.pull_version(®istry_tree, ®istry);
162/// # */
163/// # package.newest_id = git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa");
164/// assert!(package.newest_id.is_ok());
165/// # }
166/// ```
167#[derive(Debug, PartialEq)]
168pub struct GitRepoPackage {
169 /// The package's name.
170 pub name: String,
171 /// The remote git repo URL.
172 pub url: String,
173 /// The installed branch, or `None` for default.
174 pub branch: Option<String>,
175 /// The package's locally installed version's object hash.
176 pub id: Oid,
177 /// The latest version of the package available at the main [`crates.io`](https://crates.io) repository.
178 ///
179 /// `None` by default, acquire via `GitRepoPackage::pull_version()`.
180 pub newest_id: Result<Oid, GitError>,
181 /// Executables currently installed for this package.
182 pub executables: Vec<String>,
183}
184impl Hash for GitRepoPackage {
185 fn hash<H: Hasher>(&self, state: &mut H) {
186 self.name.hash(state);
187 self.url.hash(state);
188 self.branch.hash(state);
189 self.id.hash(state);
190 match &self.newest_id {
191 Ok(nid) => nid.hash(state),
192 Err(err) => {
193 err.raw_code().hash(state);
194 err.raw_class().hash(state);
195 err.message().hash(state);
196 }
197 }
198 self.executables.hash(state);
199 }
200}
201
202
203impl RegistryPackage {
204 /// Try to decypher a package descriptor into a `RegistryPackage`.
205 ///
206 /// Will return `None` if the given package descriptor is invalid.
207 ///
208 /// In the returned instance, `newest_version` is always `None`, get it via `RegistryPackage::pull_version()`.
209 ///
210 /// The executable list is used as-is.
211 ///
212 /// # Examples
213 ///
214 /// Main repository packages:
215 ///
216 /// ```
217 /// # extern crate cargo_update;
218 /// # extern crate semver;
219 /// # use cargo_update::ops::RegistryPackage;
220 /// # use semver::Version as Semver;
221 /// # fn main() {
222 /// let package_s = "racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)";
223 /// assert_eq!(RegistryPackage::parse(package_s, vec!["racer.exe".to_string()]).unwrap(),
224 /// RegistryPackage {
225 /// name: "racer".to_string(),
226 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
227 /// version: Some(Semver::parse("1.2.10").unwrap()),
228 /// newest_version: None,
229 /// alternative_version: None,
230 /// max_version: None,
231 /// executables: vec!["racer.exe".to_string()],
232 /// });
233 ///
234 /// let package_s = "cargo-outdated 0.2.0 (registry+file:///usr/local/share/cargo)";
235 /// assert_eq!(RegistryPackage::parse(package_s, vec!["cargo-outdated".to_string()]).unwrap(),
236 /// RegistryPackage {
237 /// name: "cargo-outdated".to_string(),
238 /// registry: "file:///usr/local/share/cargo".into(),
239 /// version: Some(Semver::parse("0.2.0").unwrap()),
240 /// newest_version: None,
241 /// alternative_version: None,
242 /// max_version: None,
243 /// executables: vec!["cargo-outdated".to_string()],
244 /// });
245 /// # }
246 /// ```
247 ///
248 /// Git repository:
249 ///
250 /// ```
251 /// # use cargo_update::ops::RegistryPackage;
252 /// let package_s = "treesize 0.2.1 (git+https://github.com/melak47/treesize-rs#v0.2.1)";
253 /// assert!(RegistryPackage::parse(package_s, vec!["treesize".to_string()]).is_none());
254 /// ```
255 pub fn parse(what: &str, executables: Vec<String>) -> Option<RegistryPackage> {
256 parse_registry_package_ident(what).map(|(name, version, registry)| {
257 RegistryPackage {
258 name: name.to_string(),
259 registry: registry.to_string().into(),
260 version: Some(Semver::parse(version).unwrap()),
261 newest_version: None,
262 alternative_version: None,
263 max_version: None,
264 executables: executables,
265 }
266 })
267 }
268
269 fn want_to_install_prerelease(&self, version_to_install: &Semver, install_prereleases: Option<bool>) -> bool {
270 if install_prereleases.unwrap_or(false) {
271 return true;
272 }
273
274 // otherwise only want to install prerelease if the current version is a prerelease with the same maj.min.patch
275 self.version
276 .as_ref()
277 .map(|cur| {
278 cur.is_prerelease() && cur.major == version_to_install.major && cur.minor == version_to_install.minor && cur.patch == version_to_install.patch
279 })
280 .unwrap_or(false)
281 }
282
283 /// Read the version list for this crate off the specified repository tree and set the latest and alternative versions.
284 ///
285 /// Returns `false` if the package wasn't in the given registry
286 pub fn pull_version(&mut self, registry: &RegistryTree, registry_parent: &Registry, install_prereleases: Option<bool>,
287 released_after: Option<DateTime<Utc>>) -> bool {
288 self.newest_version = None;
289 self.alternative_version = None;
290
291 let Some(vers) = self.versions(registry, registry_parent) else { return false };
292
293 let mut vers = vers.iter()
294 .rev()
295 .filter(|(_, dt)| match (dt, released_after) {
296 (_, None) => true,
297 (None, Some(_)) => false,
298 (Some(dt), Some(ra)) => ra > *dt,
299 })
300 .map(|(v, _)| v);
301 if let Some(newest) = vers.next() {
302 self.newest_version = Some(newest.clone());
303
304 if self.newest_version.as_ref().unwrap().is_prerelease() &&
305 !self.want_to_install_prerelease(self.newest_version.as_ref().unwrap(), install_prereleases) {
306 if let Some(newest_nonpre) = vers.find(|v| !v.is_prerelease()) {
307 mem::swap(&mut self.alternative_version, &mut self.newest_version);
308 self.newest_version = Some(newest_nonpre.clone());
309 }
310 }
311 }
312 true
313 }
314
315 fn versions<'r>(&mut self, registry: &RegistryTree, registry_parent: &'r Registry) -> Option<Cow<'r, [(Semver, Option<DateTime<FixedOffset>>)]>> {
316 match (registry, registry_parent) {
317 (RegistryTree::Git(registry), Registry::Git(registry_parent)) => {
318 let mut vers_git = crate_versions(find_package_data(&self.name, registry, registry_parent)?.content())
319 .map_err(|e| format!("package {}: {}", self.name, e))
320 .unwrap();
321 vers_git.sort();
322 Some(Cow::Owned(vers_git))
323 }
324 (RegistryTree::Sparse, Registry::Sparse(registry_parent)) => registry_parent.get(&self.name).map(Cow::from),
325 _ => unreachable!(),
326 }
327 }
328
329 /// Check whether this package needs to be installed
330 ///
331 /// # Examples
332 ///
333 /// ```
334 /// # extern crate cargo_update;
335 /// # extern crate semver;
336 /// # use semver::{VersionReq as SemverReq, Version as Semver};
337 /// # use cargo_update::ops::RegistryPackage;
338 /// # use std::str::FromStr;
339 /// # fn main() {
340 /// assert!(RegistryPackage {
341 /// name: "racer".to_string(),
342 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
343 /// version: Some(Semver::parse("1.7.2").unwrap()),
344 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
345 /// alternative_version: None,
346 /// max_version: None,
347 /// executables: vec!["racer".to_string()],
348 /// }.needs_update(None, None, false));
349 /// assert!(RegistryPackage {
350 /// name: "racer".to_string(),
351 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
352 /// version: None,
353 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
354 /// alternative_version: None,
355 /// max_version: None,
356 /// executables: vec!["racer".to_string()],
357 /// }.needs_update(None, None, false));
358 /// assert!(RegistryPackage {
359 /// name: "racer".to_string(),
360 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
361 /// version: Some(Semver::parse("2.0.7").unwrap()),
362 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
363 /// alternative_version: None,
364 /// max_version: None,
365 /// executables: vec!["racer".to_string()],
366 /// }.needs_update(None, None, true));
367 /// assert!(!RegistryPackage {
368 /// name: "racer".to_string(),
369 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
370 /// version: Some(Semver::parse("2.0.6").unwrap()),
371 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
372 /// alternative_version: None,
373 /// max_version: None,
374 /// executables: vec!["racer".to_string()],
375 /// }.needs_update(None, None, false));
376 /// assert!(!RegistryPackage {
377 /// name: "racer".to_string(),
378 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
379 /// version: Some(Semver::parse("2.0.6").unwrap()),
380 /// newest_version: None,
381 /// alternative_version: None,
382 /// max_version: None,
383 /// executables: vec!["racer".to_string()],
384 /// }.needs_update(None, None, false));
385 ///
386 /// let req = SemverReq::from_str("^1.7").unwrap();
387 /// assert!(RegistryPackage {
388 /// name: "racer".to_string(),
389 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
390 /// version: Some(Semver::parse("1.7.2").unwrap()),
391 /// newest_version: Some(Semver::parse("1.7.3").unwrap()),
392 /// alternative_version: None,
393 /// max_version: None,
394 /// executables: vec!["racer".to_string()],
395 /// }.needs_update(Some(&req), None, false));
396 /// assert!(RegistryPackage {
397 /// name: "racer".to_string(),
398 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
399 /// version: None,
400 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
401 /// alternative_version: None,
402 /// max_version: None,
403 /// executables: vec!["racer".to_string()],
404 /// }.needs_update(Some(&req), None, false));
405 /// assert!(!RegistryPackage {
406 /// name: "racer".to_string(),
407 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
408 /// version: Some(Semver::parse("1.7.2").unwrap()),
409 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
410 /// alternative_version: None,
411 /// max_version: None,
412 /// executables: vec!["racer".to_string()],
413 /// }.needs_update(Some(&req), None, false));
414 ///
415 /// assert!(!RegistryPackage {
416 /// name: "cargo-audit".to_string(),
417 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
418 /// version: None,
419 /// newest_version: Some(Semver::parse("0.9.0-beta2").unwrap()),
420 /// alternative_version: None,
421 /// max_version: None,
422 /// executables: vec!["racer".to_string()],
423 /// }.needs_update(Some(&req), None, false));
424 /// assert!(RegistryPackage {
425 /// name: "cargo-audit".to_string(),
426 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
427 /// version: None,
428 /// newest_version: Some(Semver::parse("0.9.0-beta2").unwrap()),
429 /// alternative_version: None,
430 /// max_version: None,
431 /// executables: vec!["racer".to_string()],
432 /// }.needs_update(Some(&req), Some(true), false));
433 /// # }
434 /// ```
435 pub fn needs_update(&self, req: Option<&SemverReq>, install_prereleases: Option<bool>, downdate: bool) -> bool {
436 fn criterion(fromver: &Semver, tover: &Semver, downdate: bool) -> bool {
437 if downdate {
438 fromver != tover
439 } else {
440 fromver < tover
441 }
442 }
443
444 let update_to_version = self.update_to_version();
445
446 (req.into_iter().zip(self.version.as_ref()).map(|(sr, cv)| !sr.matches(cv)).next().unwrap_or(true) ||
447 req.into_iter().zip(update_to_version).map(|(sr, uv)| sr.matches(uv)).next().unwrap_or(true)) &&
448 update_to_version.map(|upd_v| {
449 (!upd_v.is_prerelease() || self.want_to_install_prerelease(upd_v, install_prereleases)) &&
450 (self.version.is_none() || criterion(self.version.as_ref().unwrap(), upd_v, downdate))
451 })
452 .unwrap_or(false)
453 }
454
455 /// Get package version to update to, or `None` if the crate has no newest version (was yanked)
456 ///
457 /// # Examples
458 ///
459 /// ```
460 /// # extern crate cargo_update;
461 /// # extern crate semver;
462 /// # use cargo_update::ops::RegistryPackage;
463 /// # use semver::Version as Semver;
464 /// # fn main() {
465 /// assert_eq!(RegistryPackage {
466 /// name: "racer".to_string(),
467 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
468 /// version: Some(Semver::parse("1.7.2").unwrap()),
469 /// newest_version: Some(Semver::parse("2.0.6").unwrap()),
470 /// alternative_version: None,
471 /// max_version: Some(Semver::parse("2.0.5").unwrap()),
472 /// executables: vec!["racer".to_string()],
473 /// }.update_to_version(),
474 /// Some(&Semver::parse("2.0.5").unwrap()));
475 /// assert_eq!(RegistryPackage {
476 /// name: "gutenberg".to_string(),
477 /// registry: "https://github.com/rust-lang/crates.io-index".into(),
478 /// version: Some(Semver::parse("0.0.7").unwrap()),
479 /// newest_version: None,
480 /// alternative_version: None,
481 /// max_version: None,
482 /// executables: vec!["gutenberg".to_string()],
483 /// }.update_to_version(),
484 /// None);
485 /// # }
486 /// ```
487 pub fn update_to_version(&self) -> Option<&Semver> {
488 self.newest_version.as_ref().map(|new_v| cmp::min(new_v, self.max_version.as_ref().unwrap_or(new_v)))
489 }
490}
491
492impl GitRepoPackage {
493 /// Try to decypher a package descriptor into a `GitRepoPackage`.
494 ///
495 /// Will return `None` if:
496 ///
497 /// * the given package descriptor is invalid, or
498 /// * the package descriptor is not from a git repository.
499 ///
500 /// In the returned instance, `newest_version` is always `None`, get it via `GitRepoPackage::pull_version()`.
501 ///
502 /// The executable list is used as-is.
503 ///
504 /// # Examples
505 ///
506 /// Remote git repo packages:
507 ///
508 /// ```
509 /// # extern crate cargo_update;
510 /// # extern crate git2;
511 /// # use cargo_update::ops::GitRepoPackage;
512 /// # fn main() {
513 /// let package_s = "alacritty 0.1.0 (git+https://github.com/jwilm/alacritty#eb231b3e70b87875df4bdd1974d5e94704024d70)";
514 /// assert_eq!(GitRepoPackage::parse(package_s, vec!["alacritty".to_string()]).unwrap(),
515 /// GitRepoPackage {
516 /// name: "alacritty".to_string(),
517 /// url: "https://github.com/jwilm/alacritty".to_string(),
518 /// branch: None,
519 /// id: git2::Oid::from_str("eb231b3e70b87875df4bdd1974d5e94704024d70").unwrap(),
520 /// newest_id: Err(git2::Error::from_str("")),
521 /// executables: vec!["alacritty".to_string()],
522 /// });
523 ///
524 /// let package_s = "chattium-oxide-client 0.1.0 \
525 /// (git+https://github.com/nabijaczleweli/chattium-oxide-client\
526 /// ?branch=master#108a7b94f0e0dcb2a875f70fc0459d5a682df14c)";
527 /// assert_eq!(GitRepoPackage::parse(package_s, vec!["chattium-oxide-client.exe".to_string()]).unwrap(),
528 /// GitRepoPackage {
529 /// name: "chattium-oxide-client".to_string(),
530 /// url: "https://github.com/nabijaczleweli/chattium-oxide-client".to_string(),
531 /// branch: Some("master".to_string()),
532 /// id: git2::Oid::from_str("108a7b94f0e0dcb2a875f70fc0459d5a682df14c").unwrap(),
533 /// newest_id: Err(git2::Error::from_str("")),
534 /// executables: vec!["chattium-oxide-client.exe".to_string()],
535 /// });
536 /// # }
537 /// ```
538 ///
539 /// Main repository package:
540 ///
541 /// ```
542 /// # use cargo_update::ops::GitRepoPackage;
543 /// let package_s = "racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)";
544 /// assert!(GitRepoPackage::parse(package_s, vec!["racer".to_string()]).is_none());
545 /// ```
546 pub fn parse(what: &str, executables: Vec<String>) -> Option<GitRepoPackage> {
547 parse_git_package_ident(what).map(|(name, url, sha)| {
548 let mut url = Url::parse(url).unwrap();
549 let branch = url.query_pairs().find(|&(ref name, _)| name == "branch").map(|(_, value)| value.to_string());
550 url.set_query(None);
551 GitRepoPackage {
552 name: name.to_string(),
553 url: url.into(),
554 branch: branch,
555 id: Oid::from_str(sha).unwrap(),
556 newest_id: Err(GitError::from_str("")),
557 executables: executables,
558 }
559 })
560 }
561
562 /// Clone the repo and check what the latest commit's hash is.
563 pub fn pull_version<Pt: AsRef<Path>, Pg: AsRef<Path>>(&mut self, temp_dir: Pt, git_db_dir: Pg, http_proxy: Option<&str>, fork_git: bool) {
564 self.pull_version_impl(temp_dir.as_ref(), git_db_dir.as_ref(), http_proxy, fork_git)
565 }
566
567 fn pull_version_impl(&mut self, temp_dir: &Path, git_db_dir: &Path, http_proxy: Option<&str>, fork_git: bool) {
568 let clone_dir = find_git_db_repo(git_db_dir, &self.url).unwrap_or_else(|| temp_dir.join(&self.name));
569 if !clone_dir.exists() {
570 self.newest_id = if fork_git {
571 Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
572 .args(&["ls-remote", "--", &self.url, self.branch.as_ref().map(String::as_str).unwrap_or("HEAD")])
573 .arg(&clone_dir)
574 .stderr(Stdio::inherit())
575 .output()
576 .ok()
577 .filter(|s| s.status.success())
578 .map(|s| s.stdout)
579 .and_then(|o| String::from_utf8(o).ok())
580 .and_then(|o| o.split('\t').next().and_then(|o| Oid::from_str(o).ok()))
581 .ok_or(GitError::from_str(""))
582 } else {
583 with_authentication(&self.url, |creds| {
584 git2::Remote::create_detached(self.url.clone()).and_then(|mut r| {
585 let mut cb = RemoteCallbacks::new();
586 cb.credentials(|a, b, c| creds(a, b, c));
587 r.connect_auth(git2::Direction::Fetch,
588 Some(cb),
589 http_proxy.map(|http_proxy| proxy_options_from_proxy_url(&self.url, http_proxy)))
590 .and_then(|rc| {
591 rc.list()?
592 .into_iter()
593 .find(|rh| match self.branch.as_ref() {
594 Some(b) => {
595 if rh.name().starts_with("refs/heads/") {
596 rh.name()["refs/heads/".len()..] == b[..]
597 } else if rh.name().starts_with("refs/tags/") {
598 rh.name()["refs/tags/".len()..] == b[..]
599 } else {
600 false
601 }
602 }
603 None => rh.name() == "HEAD",
604 })
605 .map(|rh| rh.oid())
606 .ok_or(git2::Error::from_str(""))
607 })
608 })
609 })
610 };
611 if self.newest_id.is_ok() {
612 return;
613 }
614 }
615
616 let repo = self.pull_version_repo(&clone_dir, http_proxy, fork_git);
617
618 self.newest_id = repo.and_then(|r| r.head().and_then(|h| h.target().ok_or_else(|| GitError::from_str("HEAD not a direct reference"))));
619 }
620
621 fn pull_version_fresh_clone(&self, clone_dir: &Path, http_proxy: Option<&str>, fork_git: bool) -> Result<Repository, GitError> {
622 if fork_git {
623 Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
624 .arg("clone")
625 .args(self.branch.as_ref().map(|_| "-b"))
626 .args(self.branch.as_ref())
627 .args(&["--bare", "--", &self.url])
628 .arg(clone_dir)
629 .status()
630 .map_err(|e| GitError::from_str(&e.to_string()))
631 .and_then(|e| if e.success() {
632 Repository::open(clone_dir)
633 } else {
634 Err(GitError::from_str(&e.to_string()))
635 })
636 } else {
637 with_authentication(&self.url, |creds| {
638 let mut bldr = git2::build::RepoBuilder::new();
639
640 let mut cb = RemoteCallbacks::new();
641 cb.credentials(|a, b, c| creds(a, b, c));
642 bldr.fetch_options(fetch_options_from_proxy_url_and_callbacks(&self.url, http_proxy, cb));
643 if let Some(ref b) = self.branch.as_ref() {
644 bldr.branch(b);
645 }
646
647 bldr.bare(true);
648 bldr.clone(&self.url, &clone_dir)
649 })
650 }
651 }
652
653 fn pull_version_repo(&self, clone_dir: &Path, http_proxy: Option<&str>, fork_git: bool) -> Result<Repository, GitError> {
654 if let Ok(r) = Repository::open(clone_dir) {
655 // If `Repository::open` is successful, both `clone_dir` exists *and* points to a valid repository.
656 //
657 // Fetch the specified or default branch, reset it to the remote HEAD.
658
659 let (branch, tofetch) = match self.branch.as_ref() {
660 Some(b) => {
661 // Cargo doesn't point the HEAD at the chosen (via "--branch") branch when installing
662 // https://github.com/nabijaczleweli/cargo-update/issues/143
663 r.set_head(&format!("refs/heads/{}", b)).map_err(|e| panic!("Couldn't set HEAD to chosen branch {}: {}", b, e)).unwrap();
664 (Cow::from(b), Cow::from(b))
665 }
666
667 None => {
668 match r.find_reference("HEAD")
669 .map_err(|e| panic!("No HEAD in {}: {}", clone_dir.display(), e))
670 .unwrap()
671 .symbolic_target() {
672 Some(ht) => (ht["refs/heads/".len()..].to_string().into(), "+HEAD:refs/remotes/origin/HEAD".into()),
673 None => {
674 // Versions up to v4.0.0 (well, 59be1c0de283dabce320a860a3d533d00910a6a9, but who's counting)
675 // called r.set_head("FETCH_HEAD"), which made HEAD a direct SHA reference.
676 // This is obviously problematic when trying to read the default branch, and these checkouts can persist
677 // (https://github.com/nabijaczleweli/cargo-update/issues/139#issuecomment-665847290);
678 // yeeting them shouldn't be a problem, since that's what we *would* do anyway,
679 // and we set up for the non-pessimised path in later runs.
680 fs::remove_dir_all(clone_dir).unwrap();
681 return self.pull_version_fresh_clone(clone_dir, http_proxy, fork_git);
682 }
683 }
684
685 }
686 };
687
688 let mut remote = "origin";
689 r.find_remote("origin")
690 .or_else(|_| {
691 remote = &self.url;
692 r.remote_anonymous(&self.url)
693 })
694 .and_then(|mut rm| if fork_git {
695 Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
696 .arg("-C")
697 .arg(r.path())
698 .args(&["fetch", remote, &tofetch])
699 .status()
700 .map_err(|e| GitError::from_str(&e.to_string()))
701 .and_then(|e| if e.success() {
702 Ok(())
703 } else {
704 Err(GitError::from_str(&e.to_string()))
705 })
706 } else {
707 with_authentication(&self.url, |creds| {
708 let mut cb = RemoteCallbacks::new();
709 cb.credentials(|a, b, c| creds(a, b, c));
710
711 rm.fetch(&[&tofetch[..]],
712 Some(&mut fetch_options_from_proxy_url_and_callbacks(&self.url, http_proxy, cb)),
713 None)
714 })
715 })
716 .map_err(|e| panic!("Fetching {} from {}: {}", clone_dir.display(), self.url, e))
717 .unwrap();
718 r.branch(&branch,
719 &r.find_reference("FETCH_HEAD")
720 .map_err(|e| panic!("No FETCH_HEAD in {}: {}", clone_dir.display(), e))
721 .unwrap()
722 .peel_to_commit()
723 .map_err(|e| panic!("FETCH_HEAD not a commit in {}: {}", clone_dir.display(), e))
724 .unwrap(),
725 true)
726 .map_err(|e| panic!("Setting local branch {} in {}: {}", branch, clone_dir.display(), e))
727 .unwrap();
728 Ok(r)
729 } else {
730 // If we could not open the repository either it does not exist, or exists but is invalid,
731 // in which case remove it to trigger a fresh clone.
732 let _ = fs::remove_dir_all(&clone_dir).or_else(|_| fs::remove_file(&clone_dir));
733
734 self.pull_version_fresh_clone(clone_dir, http_proxy, fork_git)
735 }
736 }
737
738 /// Check whether this package needs to be installed
739 ///
740 /// # Examples
741 ///
742 /// ```
743 /// # extern crate cargo_update;
744 /// # extern crate git2;
745 /// # use cargo_update::ops::GitRepoPackage;
746 /// # fn main() {
747 /// assert!(GitRepoPackage {
748 /// name: "alacritty".to_string(),
749 /// url: "https://github.com/jwilm/alacritty".to_string(),
750 /// branch: None,
751 /// id: git2::Oid::from_str("eb231b3e70b87875df4bdd1974d5e94704024d70").unwrap(),
752 /// newest_id: git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa"),
753 /// executables: vec!["alacritty".to_string()],
754 /// }.needs_update());
755 /// assert!(!GitRepoPackage {
756 /// name: "alacritty".to_string(),
757 /// url: "https://github.com/jwilm/alacritty".to_string(),
758 /// branch: None,
759 /// id: git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa").unwrap(),
760 /// newest_id: git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa"),
761 /// executables: vec!["alacritty".to_string()],
762 /// }.needs_update());
763 /// # }
764 /// ```
765 pub fn needs_update(&self) -> bool {
766 self.newest_id.is_ok() && self.id != *self.newest_id.as_ref().unwrap()
767 }
768}
769
770
771/// One of elements with which to filter required packages.
772#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
773pub struct PackageFilterElement(pub bool, pub PackageFilterElementValue);
774#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
775pub enum PackageFilterElementValue {
776 /// Requires toolchain to be specified to the specified toolchain.
777 ///
778 /// Parsed name: `"toolchain"`.
779 Toolchain(String),
780 /// Matches package with name matching this glob.
781 ///
782 /// Parsed name: `"name"`. Glob parsed with `str::split('*')`.
783 Name(Vec<String>),
784}
785
786impl PackageFilterElement {
787 /// Parse one filter specifier into up to one package filter
788 ///
789 /// # Examples
790 ///
791 /// ```
792 /// # use cargo_update::ops::{PackageFilterElement, PackageFilterElementValue};
793 /// assert_eq!(PackageFilterElement::parse("toolchain=nightly"),
794 /// Ok(PackageFilterElement(false, PackageFilterElementValue::Toolchain("nightly".to_string()))));
795 /// assert_eq!(PackageFilterElement::parse("!name=cargo*"),
796 /// Ok(PackageFilterElement(true, PackageFilterElementValue::Name(vec!["cargo".to_string(), "".to_string()]))));
797 ///
798 /// assert!(PackageFilterElement::parse("capitalism").is_err());
799 /// assert!(PackageFilterElement::parse("communism=good").is_err());
800 /// ```
801 pub fn parse(from: &str) -> Result<PackageFilterElement, String> {
802 let (negate, from) = match from.strip_prefix("!") {
803 Some(from) => (true, from),
804 None => (false, from),
805 };
806 let (key, value) = from.split_at(from.find('=').ok_or_else(|| format!(r#"Filter string "{}" does not contain the key/value separator "=""#, from))?);
807 let value = &value[1..];
808
809 Ok(PackageFilterElement(negate,
810 match key {
811 "toolchain" => PackageFilterElementValue::Toolchain(value.to_string()),
812 "name" => PackageFilterElementValue::Name(value.split('*').map(str::to_string).collect()),
813 _ => return Err(format!(r#"Unrecognised filter key "{}""#, key)),
814 }))
815 }
816
817 /// Check if the specified package config matches this filter element.
818 ///
819 /// # Examples
820 ///
821 /// ```
822 /// # use cargo_update::ops::{PackageFilterElement, PackageFilterElementValue, ConfigOperation, PackageConfig};
823 /// assert!(PackageFilterElement(false, PackageFilterElementValue::Toolchain("nightly".to_string()))
824 /// .matches("treesize", &PackageConfig::from(&[ConfigOperation::SetToolchain("nightly".to_string())])));
825 ///
826 /// assert!(!PackageFilterElement(false, PackageFilterElementValue::Toolchain("nightly".to_string()))
827 /// .matches("treesize", &PackageConfig::from(&[])));
828 ///
829 /// let notcargo = PackageFilterElement::parse("!name=cargo*").unwrap();
830 /// assert!(notcargo.matches("treesize", &PackageConfig::from(&[])));
831 /// assert!(!notcargo.matches("cargo-update", &PackageConfig::from(&[])));
832 /// ```
833 pub fn matches(&self, name: &str, cfg: &PackageConfig) -> bool {
834 self.0 ^
835 match self.1 {
836 PackageFilterElementValue::Toolchain(ref chain) => Some(chain) == cfg.toolchain.as_ref(),
837 PackageFilterElementValue::Name(ref glob) => {
838 match &glob[..] {
839 [] => true /* unreachable */,
840 [exact] => exact == name,
841 [front, back] => name.starts_with(front) && name.ends_with(back),
842 [front, chunks @ .., back] => matchglob(name, front, chunks, back),
843 }
844 }
845 }
846 }
847}
848
849fn matchglob(name: &str, front: &str, glob: &[String], back: &str) -> bool {
850 let Some(name) = name.strip_prefix(front) else { return false };
851 let Some(mut name) = name.strip_suffix(back) else { return false };
852 for chunk in glob {
853 let Some(idx) = name.find(chunk) else { return false };
854 name = &name[idx + chunk.len()..];
855 }
856 true
857}
858
859/// `cargo` configuration, as obtained from `.cargo/config[.toml]`
860#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
861pub struct CargoConfig {
862 pub net_git_fetch_with_cli: bool,
863 /// https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html#cargos-sparse-protocol
864 /// https://doc.rust-lang.org/stable/cargo/reference/registry-index.html#sparse-protocol
865 pub registries_crates_io_protocol_sparse: bool,
866 pub http: HttpCargoConfig,
867 pub sparse_registries: SparseRegistryConfig,
868}
869
870#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
871pub struct HttpCargoConfig {
872 pub cainfo: Option<PathBuf>,
873 pub check_revoke: bool,
874}
875
876/// https://github.com/nabijaczleweli/cargo-update/issues/300
877#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
878pub struct SparseRegistryConfig {
879 pub global_credential_providers: Vec<SparseRegistryAuthProvider>,
880 pub crates_io_credential_provider: Option<SparseRegistryAuthProvider>,
881 pub crates_io_token_env: Option<String>,
882 pub crates_io_token: Option<String>,
883 pub registry_tokens_env: BTreeMap<CargoConfigEnvironmentNormalisedString, String>,
884 pub registry_tokens: BTreeMap<String, String>,
885 pub credential_aliases: BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>,
886}
887
888impl SparseRegistryConfig {
889 pub fn credential_provider(&self, v: toml::Value) -> Option<SparseRegistryAuthProvider> {
890 SparseRegistryConfig::credential_provider_impl(&self.credential_aliases, v)
891 }
892
893 fn credential_provider_impl(credential_aliases: &BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>, v: toml::Value)
894 -> Option<SparseRegistryAuthProvider> {
895 match v {
896 toml::Value::String(s) => Some(CargoConfig::string_provider(s, &credential_aliases)),
897 toml::Value::Array(a) => Some(SparseRegistryAuthProvider::from_config(CargoConfig::string_array(a))),
898 _ => None,
899 }
900 }
901}
902
903/// https://doc.rust-lang.org/cargo/reference/registry-authentication.html
904#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
905pub enum SparseRegistryAuthProvider {
906 /// The default; does not read `CARGO_REGISTRY_TOKEN` or `CARGO_REGISTRIES_{}_TOKEN` environment variables.
907 TokenNoEnvironment,
908 /// `cargo:token`
909 Token,
910 /// `cargo:wincred` (Win32)
911 Wincred,
912 /// `cargo:macos-keychain` (Apple)
913 MacosKeychain,
914 /// `cargo:libsecret`; this `dlopen()`s `libsecret-1.so.0` on non-Apple UNIX.
915 Libsecret,
916 /// `cargo:token-from-stdout prog arg arg`
917 TokenFromStdout(Vec<String>),
918 /// Not `cargo:`-prefixed
919 ///
920 /// https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html
921 ///
922 /// We do *not* care about `"cache"`, `"expiration"`, or `"operation_independent"`,
923 /// always behaving as-if `"never"`/`_`/`true`.
924 ///
925 /// We don't provide the optional `{"registry": {"headers": ...}}` field.
926 Provider(Vec<String>),
927}
928
929impl SparseRegistryAuthProvider {
930 /// Parses a `["cargo:token-from-stdout", "whatever"]`-style entry
931 pub fn from_config(mut toks: Vec<String>) -> SparseRegistryAuthProvider {
932 match toks.get(0).map(String::as_str).unwrap_or("") {
933 "cargo:token" => SparseRegistryAuthProvider::Token,
934 "cargo:wincred" => SparseRegistryAuthProvider::Wincred,
935 "cargo:macos-keychain" => SparseRegistryAuthProvider::MacosKeychain,
936 "cargo:libsecret" => SparseRegistryAuthProvider::Libsecret,
937 "cargo:token-from-stdout" => {
938 toks.remove(0);
939 SparseRegistryAuthProvider::TokenFromStdout(toks)
940 }
941 _ => SparseRegistryAuthProvider::Provider(toks),
942 }
943 }
944}
945
946/// https://doc.rust-lang.org/cargo/reference/config.html#environment-variables
947#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
948pub struct CargoConfigEnvironmentNormalisedString(pub String);
949impl CargoConfigEnvironmentNormalisedString {
950 /// `tr a-z.- A-Z__`
951 pub fn normalise(mut s: String) -> CargoConfigEnvironmentNormalisedString {
952 s.make_ascii_uppercase();
953 while let Some(i) = s.find(['.', '-']) {
954 s.replace_range(i..i + 1, "_");
955 }
956 CargoConfigEnvironmentNormalisedString(s)
957 }
958}
959
960impl CargoConfig {
961 pub fn load(crates_file: &Path) -> CargoConfig {
962 let mut cfg = fs::read_to_string(crates_file.with_file_name("config"))
963 .or_else(|_| fs::read_to_string(crates_file.with_file_name("config.toml")))
964 .ok()
965 .and_then(|s| s.parse::<toml::Value>().ok());
966 let mut creds = fs::read_to_string(crates_file.with_file_name("credentials"))
967 .or_else(|_| fs::read_to_string(crates_file.with_file_name("credentials.toml")))
968 .ok()
969 .and_then(|s| s.parse::<toml::Value>().ok());
970
971 let credential_aliases = None.or_else(|| match cfg.as_mut()?.as_table_mut()?.remove("credential-alias")? {
972 toml::Value::Table(t) => Some(t),
973 _ => None,
974 })
975 .unwrap_or_default()
976 .into_iter()
977 .flat_map(|(k, v)| {
978 match v {
979 toml::Value::String(s) => Some(s.split(' ').map(String::from).collect()),
980 toml::Value::Array(a) => Some(CargoConfig::string_array(a)),
981 _ => None,
982 }
983 .map(|v| (CargoConfigEnvironmentNormalisedString::normalise(k), v))
984 })
985 .chain(env::vars_os()
986 .map(|(k, v)| (k.into_encoded_bytes(), v))
987 .filter(|(k, _)| k.starts_with(b"CARGO_CREDENTIAL_ALIAS_"))
988 .filter(|(k, _)| k["CARGO_CREDENTIAL_ALIAS_".len()..].iter().all(|&b| !(b.is_ascii_lowercase() || b == b'.' || b == b'-')))
989 .flat_map(|(mut k, v)| {
990 let k = String::from_utf8(k.drain("CARGO_CREDENTIAL_ALIAS_".len()..).collect()).ok()?;
991 let v = v.into_string().ok()?;
992 Some((CargoConfigEnvironmentNormalisedString(k), v.split(' ').map(String::from).collect()))
993 }))
994 .collect();
995
996 CargoConfig {
997 net_git_fetch_with_cli: env::var("CARGO_NET_GIT_FETCH_WITH_CLI")
998 .ok()
999 .and_then(|e| if e.is_empty() {
1000 Some(toml::Value::String(String::new()))
1001 } else {
1002 e.parse::<toml::Value>().ok()
1003 })
1004 .or_else(|| {
1005 cfg.as_mut()?
1006 .as_table_mut()?
1007 .get_mut("net")?
1008 .as_table_mut()?
1009 .remove("git-fetch-with-cli")
1010 })
1011 .map(CargoConfig::truthy)
1012 .unwrap_or(false),
1013 registries_crates_io_protocol_sparse: env::var("CARGO_REGISTRIES_CRATES_IO_PROTOCOL")
1014 .map(|s| s == "sparse")
1015 .ok()
1016 .or_else(|| {
1017 Some(cfg.as_mut()?
1018 .as_table_mut()?
1019 .get_mut("registries")?
1020 .as_table_mut()?
1021 .get_mut("crates-io")?
1022 .as_table_mut()?
1023 .remove("protocol")?
1024 .as_str()? == "sparse")
1025 })
1026 // // Horrifically expensive (82-93ms end-to-end) and largely unnecessary
1027 // .or_else(|| {
1028 // let mut l = String::new();
1029 // // let before = std::time::Instant::now();
1030 // BufReader::new(Command::new(cargo).arg("version").stdout(Stdio::piped()).spawn().ok()?.stdout?).read_line(&mut l).ok()?;
1031 // // let after = std::time::Instant::now();
1032 //
1033 // // cargo 1.63.0 (fd9c4297c 2022-07-01)
1034 // Some(Semver::parse(l.split_whitespace().nth(1)?).ok()? >= Semver::new(1, 70, 0))
1035 // })
1036 // .unwrap_or(false),
1037 .unwrap_or(true),
1038 http: HttpCargoConfig {
1039 cainfo: env::var_os("CARGO_HTTP_CAINFO")
1040 .map(PathBuf::from)
1041 .or_else(|| {
1042 CargoConfig::string(cfg.as_mut()?
1043 .as_table_mut()?
1044 .get_mut("http")?
1045 .as_table_mut()?
1046 .remove("cainfo")?)
1047 .map(PathBuf::from)
1048 }),
1049 check_revoke: env::var("CARGO_HTTP_CHECK_REVOKE")
1050 .ok()
1051 .map(toml::Value::String)
1052 .or_else(|| {
1053 cfg.as_mut()?
1054 .as_table_mut()?
1055 .get_mut("http")?
1056 .as_table_mut()?
1057 .remove("check-revoke")
1058 })
1059 .map(CargoConfig::truthy)
1060 .unwrap_or(cfg!(target_os = "windows")),
1061 },
1062 sparse_registries: SparseRegistryConfig {
1063 // Supposedly this is CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS but they don't specify how they serialise arrays so
1064 global_credential_providers: None.or_else(|| {
1065 CargoConfig::string_array_v(cfg.as_mut()?
1066 .as_table_mut()?
1067 .get_mut("registry")?
1068 .as_table_mut()?
1069 .remove("global-credential-providers")?)
1070 })
1071 .map(|a| a.into_iter().map(|s| CargoConfig::string_provider(s, &credential_aliases)).collect())
1072 .unwrap_or_else(|| vec![SparseRegistryAuthProvider::TokenNoEnvironment]),
1073 crates_io_credential_provider: env::var("CARGO_REGISTRY_CREDENTIAL_PROVIDER")
1074 .ok()
1075 .map(toml::Value::String)
1076 .or_else(|| {
1077 cfg.as_mut()?
1078 .as_table_mut()?
1079 .get_mut("registry")?
1080 .as_table_mut()?
1081 .remove("credential-provider")
1082 })
1083 .and_then(|v| SparseRegistryConfig::credential_provider_impl(&credential_aliases, v)),
1084 crates_io_token_env: env::var("CARGO_REGISTRY_TOKEN").ok(),
1085 crates_io_token: None.or_else(|| {
1086 CargoConfig::string(creds.as_mut()?
1087 .as_table_mut()?
1088 .get_mut("registry")?
1089 .as_table_mut()?
1090 .remove("token")?)
1091 })
1092 .or_else(|| {
1093 CargoConfig::string(cfg.as_mut()?
1094 .as_table_mut()?
1095 .get_mut("registry")?
1096 .as_table_mut()?
1097 .remove("token")?)
1098 }),
1099 registry_tokens_env: env::vars_os()
1100 .map(|(k, v)| (k.into_encoded_bytes(), v))
1101 .filter(|(k, _)| k.starts_with(b"CARGO_REGISTRIES_") && k.ends_with(b"_TOKEN"))
1102 .filter(|(k, _)| {
1103 k["CARGO_REGISTRIES_".len()..k.len() - b"_TOKEN".len()].iter().all(|&b| !(b.is_ascii_lowercase() || b == b'.' || b == b'-'))
1104 })
1105 .flat_map(|(mut k, v)| {
1106 let k = String::from_utf8(k.drain("CARGO_REGISTRIES_".len()..k.len() - b"_TOKEN".len()).collect()).ok()?;
1107 Some((CargoConfigEnvironmentNormalisedString(k), v.into_string().ok()?))
1108 })
1109 .collect(),
1110 registry_tokens: cfg.as_mut()
1111 .into_iter()
1112 .chain(creds.as_mut())
1113 .flat_map(|c| {
1114 c.as_table_mut()?
1115 .get_mut("registries")?
1116 .as_table_mut()
1117 })
1118 .flat_map(|r| r.into_iter().flat_map(|(name, v)| Some((name.clone(), CargoConfig::string(v.as_table_mut()?.remove("token")?)?))))
1119 .collect(),
1120 credential_aliases: credential_aliases,
1121 },
1122 }
1123 }
1124
1125 fn truthy(v: toml::Value) -> bool {
1126 match v {
1127 toml::Value::String(ref s) if s == "" => false,
1128 toml::Value::Float(0.) => false,
1129 toml::Value::Integer(0) |
1130 toml::Value::Boolean(false) => false,
1131 _ => true,
1132 }
1133 }
1134
1135 fn string(v: toml::Value) -> Option<String> {
1136 match v {
1137 toml::Value::String(s) => Some(s),
1138 _ => None,
1139 }
1140 }
1141
1142 fn string_array(a: Vec<toml::Value>) -> Vec<String> {
1143 a.into_iter().flat_map(CargoConfig::string).collect()
1144 }
1145
1146 fn string_array_v(v: toml::Value) -> Option<Vec<String>> {
1147 match v {
1148 toml::Value::Array(s) => Some(CargoConfig::string_array(s)),
1149 _ => None,
1150 }
1151 }
1152
1153 fn string_provider(s: String, credential_aliases: &BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>) -> SparseRegistryAuthProvider {
1154 match credential_aliases.get(&CargoConfigEnvironmentNormalisedString::normalise(s.clone())) {
1155 Some(av) => SparseRegistryAuthProvider::Provider(av.clone()),
1156 None => {
1157 SparseRegistryAuthProvider::from_config(if s.contains(' ') {
1158 s.split(' ').map(String::from).collect()
1159 } else {
1160 vec![s]
1161 })
1162 }
1163 }
1164 }
1165}
1166
1167
1168/// [Follow `install.root`](https://github.com/nabijaczleweli/cargo-update/issues/23) in the `config` or `config.toml` file
1169/// in the cargo directory specified.
1170///
1171/// # Examples
1172///
1173/// ```
1174/// # use cargo_update::ops::crates_file_in;
1175/// # use std::env::temp_dir;
1176/// # let cargo_dir = temp_dir();
1177/// let cargo_dir = crates_file_in(&cargo_dir);
1178/// # let _ = cargo_dir;
1179/// ```
1180pub fn crates_file_in(cargo_dir: &Path) -> PathBuf {
1181 crates_file_in_impl(cargo_dir, BTreeSet::new())
1182}
1183fn crates_file_in_impl<'cd>(cargo_dir: &'cd Path, mut seen: BTreeSet<&'cd Path>) -> PathBuf {
1184 if !seen.insert(cargo_dir) {
1185 panic!("Cargo config install.root loop at {:?} (saw {:?})", cargo_dir.display(), seen);
1186 }
1187
1188 let mut config_file = cargo_dir.join("config");
1189 let mut config_data = fs::read_to_string(&config_file);
1190 if config_data.is_err() {
1191 config_file.set_file_name("config.toml");
1192 config_data = fs::read_to_string(&config_file);
1193 }
1194 if let Ok(config_data) = config_data {
1195 if let Some(idir) = toml::from_str::<toml::Value>(&config_data)
1196 .unwrap()
1197 .get("install")
1198 .and_then(|t| t.as_table())
1199 .and_then(|t| t.get("root"))
1200 .and_then(|t| t.as_str()) {
1201 return crates_file_in_impl(Path::new(idir), seen);
1202 }
1203 }
1204
1205 config_file.set_file_name(".crates.toml");
1206 config_file
1207}
1208
1209fn installed_packages_table(crates_file: &Path) -> Option<toml::Table> {
1210 let crates_data = fs::read_to_string(crates_file).ok()?;
1211 Some(toml::from_str::<toml::Value>(&crates_data).unwrap().get_mut("v1")?.as_table_mut().map(mem::take).unwrap())
1212}
1213
1214/// List the installed packages at the specified location that originate
1215/// from the a cargo registry.
1216///
1217/// If the `.crates.toml` file doesn't exist an empty vector is returned.
1218///
1219/// This also deduplicates packages and assumes the latest version as the correct one to work around
1220/// [#44](https://github.com/nabijaczleweli/cargo-update/issues/44) a.k.a.
1221/// [rust-lang/cargo#4321](https://github.com/rust-lang/cargo/issues/4321).
1222///
1223/// # Examples
1224///
1225/// ```
1226/// # use cargo_update::ops::installed_registry_packages;
1227/// # use std::env::temp_dir;
1228/// # let cargo_dir = temp_dir().join(".crates.toml");
1229/// let packages = installed_registry_packages(&cargo_dir);
1230/// for package in &packages {
1231/// println!("{} v{}", package.name, package.version.as_ref().unwrap());
1232/// }
1233/// ```
1234pub fn installed_registry_packages(crates_file: &Path) -> Vec<RegistryPackage> {
1235 let mut res = Vec::<RegistryPackage>::new();
1236 for pkg in installed_packages_table(crates_file)
1237 .into_iter()
1238 .flatten()
1239 .flat_map(|(s, x)| CargoConfig::string_array_v(x).and_then(|x| RegistryPackage::parse(&s, x))) {
1240 if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
1241 if saved.version.is_none() || saved.version.as_ref().unwrap() < pkg.version.as_ref().unwrap() {
1242 saved.version = pkg.version;
1243 }
1244 continue;
1245 }
1246
1247 res.push(pkg);
1248 }
1249 res
1250}
1251
1252/// List the installed packages at the specified location that originate
1253/// from a remote git repository.
1254///
1255/// If the `.crates.toml` file doesn't exist an empty vector is returned.
1256///
1257/// This also deduplicates packages and assumes the latest-mentioned version as the most correct.
1258///
1259/// # Examples
1260///
1261/// ```
1262/// # use cargo_update::ops::installed_git_repo_packages;
1263/// # use std::env::temp_dir;
1264/// # let cargo_dir = temp_dir().join(".crates.toml");
1265/// let packages = installed_git_repo_packages(&cargo_dir);
1266/// for package in &packages {
1267/// println!("{} v{}", package.name, package.id);
1268/// }
1269/// ```
1270pub fn installed_git_repo_packages(crates_file: &Path) -> Vec<GitRepoPackage> {
1271 let mut res = Vec::<GitRepoPackage>::new();
1272 for pkg in installed_packages_table(crates_file)
1273 .into_iter()
1274 .flatten()
1275 .flat_map(|(s, x)| CargoConfig::string_array_v(x).and_then(|x| GitRepoPackage::parse(&s, x))) {
1276 if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
1277 saved.id = pkg.id;
1278 continue;
1279 }
1280
1281 res.push(pkg);
1282 }
1283 res
1284}
1285
1286/// Filter out the installed packages not specified to be updated and add the packages you specify to install,
1287/// if they aren't already installed via git.
1288///
1289/// List installed packages with `installed_registry_packages()`.
1290///
1291/// # Examples
1292///
1293/// ```
1294/// # use cargo_update::ops::{RegistryPackage, intersect_packages};
1295/// # fn installed_registry_packages(_: &()) {}
1296/// # let cargo_dir = ();
1297/// # let packages_to_update = [("racer".to_string(), None,
1298/// # "registry+https://github.com/rust-lang/crates.io-index".into()),
1299/// # ("cargo-outdated".to_string(), None,
1300/// # "registry+https://github.com/rust-lang/crates.io-index".into())];
1301/// let mut installed_packages = installed_registry_packages(&cargo_dir);
1302/// # let mut installed_packages =
1303/// # vec![RegistryPackage::parse("cargo-outdated 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
1304/// # vec!["cargo-outdated".to_string()]).unwrap(),
1305/// # RegistryPackage::parse("racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1306/// # vec!["racer.exe".to_string()]).unwrap(),
1307/// # RegistryPackage::parse("rustfmt 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1308/// # vec!["rustfmt".to_string(), "cargo-format".to_string()]).unwrap()];
1309/// installed_packages = intersect_packages(&installed_packages, &packages_to_update, false, &[]);
1310/// # assert_eq!(&installed_packages,
1311/// # &[RegistryPackage::parse("cargo-outdated 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
1312/// # vec!["cargo-outdated".to_string()]).unwrap(),
1313/// # RegistryPackage::parse("racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1314/// # vec!["racer.exe".to_string()]).unwrap()]);
1315/// ```
1316pub fn intersect_packages(installed: &[RegistryPackage], to_update: &[(String, Option<Semver>, Cow<'static, str>)], allow_installs: bool,
1317 installed_git: &[GitRepoPackage])
1318 -> Vec<RegistryPackage> {
1319 installed.iter()
1320 .filter(|p| to_update.iter().any(|u| p.name == u.0))
1321 .cloned()
1322 .map(|p| RegistryPackage { max_version: to_update.iter().find(|u| p.name == u.0).and_then(|u| u.1.clone()), ..p })
1323 .chain(to_update.iter()
1324 .filter(|p| allow_installs && !installed.iter().any(|i| i.name == p.0) && !installed_git.iter().any(|i| i.name == p.0))
1325 .map(|p| {
1326 RegistryPackage {
1327 name: p.0.clone(),
1328 registry: p.2.clone(),
1329 version: None,
1330 newest_version: None,
1331 alternative_version: None,
1332 max_version: p.1.clone(),
1333 executables: vec![],
1334 }
1335 }))
1336 .collect()
1337}
1338
1339/// Parse the raw crate descriptor from the repository into a collection of `Semver`s and publication times (if present).
1340///
1341/// # Examples
1342///
1343/// ```
1344/// # use cargo_update::ops::crate_versions;
1345/// # use std::fs;
1346/// # let desc_path = "test-data/checksums-versions.json";
1347/// # let package = "checksums";
1348/// let versions = crate_versions(&fs::read(desc_path).unwrap()).expect(package);
1349///
1350/// println!("Released versions of checksums:");
1351/// for (ver, pubtime) in &versions {
1352/// match pubtime {
1353/// None => println!(" {}", ver),
1354/// Some(pubtime) => println!(" {} ({})", ver, pubtime),
1355/// }
1356/// }
1357/// ```
1358pub fn crate_versions(buf: &[u8]) -> Result<Vec<(Semver, Option<DateTime<FixedOffset>>)>, Cow<'static, str>> {
1359 buf.split_inclusive(|&b| b == b'\n').map(crate_version_line).flat_map(Result::transpose).collect()
1360}
1361fn crate_version_line(line: &[u8]) -> Result<Option<(Semver, Option<DateTime<FixedOffset>>)>, Cow<'static, str>> {
1362 if line == b"\n" {
1363 return Ok(None);
1364 }
1365 match json::from_slice(line).map_err(|e| e.to_string())? {
1366 json::Value::Object(o) => {
1367 if matches!(o.get("yanked"), Some(&json::Value::Bool(true))) {
1368 return Ok(None);
1369 }
1370
1371 let v = match o.get("vers").ok_or("no \"vers\" key")? {
1372 json::Value::String(ref v) => Semver::parse(&v).map_err(|e| e.to_string())?,
1373 _ => return Err("\"vers\" not string".into()),
1374 };
1375
1376 let pt = match o.get("pubtime") {
1377 None => None,
1378 Some(json::Value::String(ref pt)) => Some(DateTime::parse_from_rfc3339(pt).map_err(|e| e.to_string())?),
1379 Some(_) => return Err("\"pubtime\" not string".into()),
1380 };
1381
1382 Ok(Some((v, pt)))
1383 }
1384 _ => Err("line not object".into()),
1385 }
1386}
1387
1388/// Get the location of the registry index corresponding ot the given URL; if not present – make it and its parents.
1389///
1390/// As odd as it may be, this [can happen (if rarely) and is a supported
1391/// configuration](https://github.com/nabijaczleweli/cargo-update/issues/150).
1392///
1393/// Sparse registries do nothing and return a meaningless value.
1394///
1395/// # Examples
1396///
1397/// ```
1398/// # #[cfg(all(target_pointer_width="64", target_endian="little"))] // github.com/nabijaczleweli/cargo-update/issues/235
1399/// # {
1400/// # use cargo_update::ops::assert_index_path;
1401/// # use std::env::temp_dir;
1402/// # use std::path::Path;
1403/// # let cargo_dir = temp_dir().join("cargo_update-doctest").join("assert_index_path-0");
1404/// # let idx_dir = cargo_dir.join("registry").join("index").join("github.com-1ecc6299db9ec823");
1405/// let index = assert_index_path(&cargo_dir, "https://github.com/rust-lang/crates.io-index", false).unwrap();
1406///
1407/// // Use find_package_data() to look for packages
1408/// # assert_eq!(index, idx_dir);
1409/// # assert_eq!(assert_index_path(&cargo_dir, "https://index.crates.io/", true).unwrap(), Path::new("/ENOENT"));
1410/// # }
1411/// ```
1412pub fn assert_index_path(cargo_dir: &Path, registry_url: &str, sparse: bool) -> Result<PathBuf, Cow<'static, str>> {
1413 if sparse {
1414 return Ok(PathBuf::from("/ENOENT"));
1415 }
1416
1417 let path = cargo_dir.join("registry").join("index").join(registry_shortname(registry_url));
1418 match path.metadata() {
1419 Ok(meta) => {
1420 if meta.is_dir() {
1421 Ok(path)
1422 } else {
1423 Err(format!("{} (index directory for {}) not a directory", path.display(), registry_url).into())
1424 }
1425 }
1426 Err(ref e) if e.kind() == IoErrorKind::NotFound => {
1427 fs::create_dir_all(&path).map_err(|e| format!("Couldn't create {} (index directory for {}): {}", path.display(), registry_url, e))?;
1428 Ok(path)
1429 }
1430 Err(e) => Err(format!("Couldn't read {} (index directory for {}): {}", path.display(), registry_url, e).into()),
1431 }
1432}
1433
1434/// Opens or initialises a git repository at `registry`, or returns a blank sparse registry.
1435///
1436/// Error type distinguishes init error from open error.
1437pub fn open_index_repository(registry: &Path, sparse: bool) -> Result<Registry, (bool, GitError)> {
1438 match sparse {
1439 false => {
1440 Repository::open(®istry).map(Registry::Git).or_else(|e| if e.code() == GitErrorCode::NotFound {
1441 Repository::init(®istry).map(Registry::Git).map_err(|e| (true, e))
1442 } else {
1443 Err((false, e))
1444 })
1445 }
1446 true => Ok(Registry::Sparse(BTreeMap::new())),
1447 }
1448}
1449
1450#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
1451pub struct SparseRegistryAuthProviderBundle<'sr>(pub Cow<'sr, [SparseRegistryAuthProvider]>,
1452 pub &'sr OsStr,
1453 pub &'sr str,
1454 pub Cow<'sr, str>,
1455 pub Option<&'sr str>,
1456 pub Option<&'sr str>);
1457impl<'sr> SparseRegistryAuthProviderBundle<'sr> {
1458 pub fn try(&self) -> Option<Cow<'sr, str>> {
1459 let SparseRegistryAuthProviderBundle(providers, install_cargo, repo_name, repo_url, token_env, token) = self;
1460 providers.iter()
1461 .rev()
1462 .find_map(|p| match p {
1463 SparseRegistryAuthProvider::TokenNoEnvironment => token.map(Cow::from),
1464 SparseRegistryAuthProvider::Token => token_env.or(*token).map(Cow::from),
1465 SparseRegistryAuthProvider::Wincred => {
1466 #[allow(unused_mut)]
1467 let mut ret = None;
1468 #[cfg(target_os="windows")]
1469 unsafe {
1470 let mut cred = ptr::null_mut();
1471 if WinCred::CredReadA(PCSTR(format!("cargo-registry:{}\0", repo_url).as_ptr()),
1472 WinCred::CRED_TYPE_GENERIC,
1473 None,
1474 &mut cred)
1475 .is_ok() {
1476 ret = str::from_utf8(slice::from_raw_parts((*cred).CredentialBlob, (*cred).CredentialBlobSize as usize))
1477 .map(str::to_string)
1478 .map(Cow::from)
1479 .ok();
1480 WinCred::CredFree(cred as _);
1481 }
1482 }
1483 ret
1484 }
1485 SparseRegistryAuthProvider::MacosKeychain => {
1486 #[allow(unused_mut, unused_assignments)]
1487 let mut ret = None;
1488 #[cfg(target_vendor = "apple")]
1489 {
1490 ret = SecKeychain::default()
1491 .and_then(|k| k.find_generic_password(&format!("cargo-registry:{}", repo_url), ""))
1492 .ok()
1493 .and_then(|(p, _)| str::from_utf8(&*p).map(str::to_string).map(Cow::from).ok());
1494 }
1495 ret
1496 }
1497 SparseRegistryAuthProvider::Libsecret => {
1498 #[allow(unused_mut)]
1499 let mut ret = None;
1500 #[cfg(all(unix, not(target_vendor = "apple")))]
1501 #[allow(non_camel_case_types)]
1502 unsafe {
1503 #[repr(C)]
1504 struct SecretSchemaAttribute {
1505 name: *const u8,
1506 flags: libc::c_int, // SECRET_SCHEMA_ATTRIBUTE_STRING = 0
1507 }
1508 #[repr(C)]
1509 struct SecretSchema {
1510 name: *const u8,
1511 flags: libc::c_int,
1512 attributes: [SecretSchemaAttribute; 32],
1513 reserved: libc::c_int,
1514 reserved1: *const (),
1515 reserved2: *const (),
1516 reserved3: *const (),
1517 reserved4: *const (),
1518 reserved5: *const (),
1519 reserved6: *const (),
1520 reserved7: *const (),
1521 }
1522 unsafe impl Sync for SecretSchema {}
1523 type secret_password_lookup_sync_t = extern "C" fn(*const SecretSchema, *mut (), *mut (), ...) -> *mut u8;
1524 type secret_password_free_t = extern "C" fn(*mut u8);
1525
1526 static LIBSECRET: LazyLock<Option<(secret_password_lookup_sync_t, secret_password_free_t)>> = LazyLock::new(|| unsafe {
1527 let libsecret = libc::dlopen(b"libsecret-1.so.0\0".as_ptr() as _, libc::RTLD_LAZY);
1528 if libsecret.is_null() {
1529 return None;
1530 }
1531 let lookup = libc::dlsym(libsecret, b"secret_password_lookup_sync\0".as_ptr() as _);
1532 let free = libc::dlsym(libsecret, b"secret_password_free\0".as_ptr() as _);
1533 if lookup.is_null() || free.is_null() {
1534 libc::dlclose(libsecret);
1535 return None;
1536 }
1537 Some((mem::transmute(lookup), mem::transmute(free)))
1538 });
1539 static SCHEMA: SecretSchema = unsafe {
1540 let mut schema: SecretSchema = mem::zeroed();
1541 schema.name = b"org.rust-lang.cargo.registry\0".as_ptr() as _;
1542 schema.attributes[0].name = b"url\0".as_ptr() as _;
1543 schema
1544 };
1545
1546 if let Some((lookup, free)) = *LIBSECRET {
1547 let pass = lookup(&SCHEMA,
1548 ptr::null_mut(),
1549 ptr::null_mut(),
1550 b"url\0".as_ptr(),
1551 format!("{}\0", repo_url).as_ptr(),
1552 ptr::null() as *const u8);
1553 if !pass.is_null() {
1554 ret = str::from_utf8(slice::from_raw_parts(pass, libc::strlen(pass as _))).map(str::to_string).map(Cow::from).ok();
1555 free(pass);
1556 }
1557 }
1558 }
1559 ret
1560 }
1561 SparseRegistryAuthProvider::TokenFromStdout(args) => {
1562 Command::new(&args[0])
1563 .args(&args[1..])
1564 .env("CARGO", install_cargo)
1565 .env("CARGO_REGISTRY_INDEX_URL", &repo_url[..])
1566 .env("CARGO_REGISTRY_NAME_OPT", repo_name)
1567 .stdin(Stdio::inherit())
1568 .stderr(Stdio::inherit())
1569 .output()
1570 .ok()
1571 .filter(|o| o.status.success())
1572 .map(|o| o.stdout)
1573 .and_then(|o| String::from_utf8(o).ok())
1574 .map(|mut o| {
1575 o.replace_range(o.rfind(|c| c != '\n').unwrap_or(o.len()) + 1..o.len(), "");
1576 o.replace_range(0..o.find(|c| c != '\n').unwrap_or(0), "");
1577 o.into()
1578 })
1579 }
1580 SparseRegistryAuthProvider::Provider(args) => {
1581 Command::new(&args[0])
1582 .arg("--cargo-plugin")
1583 .stdin(Stdio::piped())
1584 .stdout(Stdio::piped())
1585 .spawn()
1586 .ok()
1587 .and_then(|mut child| {
1588 let mut stdin = BufWriter::new(child.stdin.take().unwrap());
1589 let mut stdout = BufReader::new(child.stdout.take().unwrap());
1590
1591 let mut l = String::new();
1592 stdout.read_line(&mut l).map_err(|_| child.kill()).ok()?;
1593 {
1594 let mut hello: json::Value = json::from_str(&l).map_err(|_| child.kill()).ok()?;
1595 hello.as_object_mut()
1596 .and_then(|h| h.remove("v"))
1597 .and_then(|mut v| v.as_array_mut().filter(|vs| vs.contains(&json::Value::Number(1.into()))).map(drop))
1598 .ok_or_else(|| child.kill())
1599 .ok()?;
1600 }
1601
1602 let req = json::Value::Object({
1603 let mut kv = json::Map::new();
1604 kv.insert("v".to_string(), json::Value::Number(1.into()));
1605 kv.insert("registry".to_string(),
1606 json::Value::Object({
1607 let mut kv = json::Map::new();
1608 kv.insert("index-url".to_string(), json::Value::String(repo_url.to_string()));
1609 kv.insert("name".to_string(), json::Value::String(repo_name.to_string()));
1610 kv
1611 }));
1612 kv.insert("kind".to_string(), json::Value::String("get".to_string()));
1613 kv.insert("operation".to_string(), json::Value::String("read".to_string()));
1614 kv.insert("args".to_string(),
1615 json::Value::Array(args.into_iter().skip(1).cloned().map(json::Value::String).collect()));
1616 kv
1617 });
1618 json::to_writer(&mut stdin, &req).map_err(|_| child.kill()).ok()?;
1619 stdin.write_all(b"\n").map_err(|_| child.kill()).ok()?;
1620 stdin.flush().map_err(|_| child.kill()).ok()?;
1621
1622 l.clear();
1623 stdout.read_line(&mut l).map_err(|_| child.kill()).ok()?;
1624 let mut res: json::Value = json::from_str(&l).map_err(|_| child.kill()).ok()?;
1625 match res.as_object_mut()
1626 .and_then(|h| h.remove("Ok"))
1627 .and_then(|mut ok| ok.as_object_mut().and_then(|ok| ok.remove("token"))) {
1628 Some(json::Value::String(tok)) => Some(tok.into()),
1629 Some(_) => {
1630 let _ = child.kill();
1631 None
1632 }
1633 None => {
1634 let _ = io::stderr()
1635 .write_all(b"\n")
1636 .ok()
1637 .and_then(|_| json::to_writer(&mut io::stderr(), &res).ok().and_then(|_| io::stderr().write_all(b"\n").ok()));
1638 None
1639 }
1640 }
1641 })
1642 }
1643 })
1644 }
1645}
1646
1647/// Collect everything needed to get an authentication token for the given registry.
1648pub fn auth_providers<'sr>(crates_file: &Path, cargo: &'sr OsStr, sparse_registries: &'sr SparseRegistryConfig, sparse: bool, repo_name: &'sr str,
1649 repo_url: &'sr str)
1650 -> SparseRegistryAuthProviderBundle<'sr> {
1651 if !sparse {
1652 return SparseRegistryAuthProviderBundle(vec![].into(), cargo, "!sparse", "!sparse".into(), None, None);
1653 }
1654
1655 if repo_name == "crates-io" {
1656 let ret = match sparse_registries.crates_io_credential_provider.as_ref() {
1657 Some(prov) => slice::from_ref(prov).into(),
1658 None => sparse_registries.global_credential_providers[..].into(),
1659 };
1660 return SparseRegistryAuthProviderBundle(ret,
1661 cargo,
1662 repo_name,
1663 format!("sparse+{}", repo_url).into(),
1664 sparse_registries.crates_io_token_env.as_deref(),
1665 sparse_registries.crates_io_token.as_deref());
1666 }
1667
1668 // Supposedly this is
1669 // format!("CARGO_REGISTRIES_{}_CREDENTIAL_PROVIDER",
1670 // CargoConfigEnvironmentNormalisedString::normalise(repo_name.to_string()).0)
1671 // but they don't specify how they serialise arrays so
1672 let ret: Cow<'sr, [SparseRegistryAuthProvider]> = match fs::read_to_string(crates_file.with_file_name("config"))
1673 .or_else(|_| fs::read_to_string(crates_file.with_file_name("config.toml")))
1674 .ok()
1675 .and_then(|s| s.parse::<toml::Value>().ok())
1676 .and_then(|mut c| {
1677 sparse_registries.credential_provider(c.as_table_mut()?
1678 .remove("registries")?
1679 .as_table_mut()?
1680 .remove(repo_name)?
1681 .as_table_mut()?
1682 .remove("credential-provider")?)
1683 }) {
1684 Some(prov) => vec![prov].into(),
1685 None => sparse_registries.global_credential_providers[..].into(),
1686 };
1687 let token_env = if ret.contains(&SparseRegistryAuthProvider::Token) {
1688 sparse_registries.registry_tokens_env.get(&CargoConfigEnvironmentNormalisedString::normalise(repo_name.to_string())).map(String::as_str)
1689 } else {
1690 None
1691 };
1692 SparseRegistryAuthProviderBundle(ret,
1693 cargo,
1694 repo_name,
1695 format!("sparse+{}", repo_url).into(),
1696 token_env,
1697 sparse_registries.registry_tokens.get(repo_name).map(String::as_str))
1698}
1699
1700/// Update the specified index repository from the specified URL.
1701///
1702/// Historically, `cargo search` was used, first of an
1703/// [empty string](https://github.com/nabijaczleweli/cargo-update/commit/aa090b4a38a486654cd73b173c3f49f6a56aa059#diff-639fbc4ef05b315af92b4d836c31b023R24),
1704/// then a [ZWNJ](https://github.com/nabijaczleweli/cargo-update/commit/aeccbd6252a2ddc90dc796117cefe327fbd7fb58#diff-639fbc4ef05b315af92b4d836c31b023R48)
1705/// ([why?](https://github.com/nabijaczleweli/cargo-update/commit/08a7111831c6397b7d67a51f9b77bee0a3bbbed4#diff-639fbc4ef05b315af92b4d836c31b023R47)).
1706///
1707/// The need for this in-house has first emerged with [#93](https://github.com/nabijaczleweli/cargo-update/issues/93): since
1708/// [`cargo` v1.29.0-nightly](https://github.com/rust-lang/cargo/pull/5621/commits/5e680f2849e44ce9dfe44416c3284a3b30747e74),
1709/// the registry was no longer updated.
1710/// So a [two-year-old `cargo` issue](https://github.com/rust-lang/cargo/issues/3377#issuecomment-417950125) was dug up,
1711/// asking for a `cargo update-registry` command, followed by a [PR](https://github.com/rust-lang/cargo/pull/5961) implementing
1712/// this.
1713/// Up to this point, there was no good substitute: `cargo install lazy_static`, the poster-child of replacements errored out
1714/// and left garbage in the console, making it unsuitable.
1715///
1716/// But then, a [man of steel eyes and hawk will](https://github.com/Eh2406) has emerged, seemingly from nowhere, remarking:
1717///
1718/// > [21:09] Eh2406:
1719/// https://github.com/rust-lang/cargo/blob/1ee1ef0ea7ab47d657ca675e3b1bd2fcd68b5aab/src/cargo/sources/registry/remote.rs#L204
1720/// <br />
1721/// > [21:10] Eh2406: looks like it is a git fetch of "refs/heads/master:refs/remotes/origin/master"<br />
1722/// > [21:11] Eh2406: You are already poking about in cargos internal representation of the index, is this so much more?
1723///
1724/// It, well, isn't. And with some `cargo` maintainers being firmly against blind-merging that `cargo update-registry` PR,
1725/// here I go recycling <del>the same old song</del> that implementation (but simpler, and badlier).
1726///
1727/// Honourable mentions:
1728/// * [**@joshtriplett**](https://github.com/joshtriplett), for being a bastion for the people and standing with me in
1729/// advocacy for `cargo update-registry`
1730/// (NB: it was *his* issue from 2016 requesting it, funny how things turn around)
1731/// * [**@alexcrichton**](https://github.com/alexcrichton), for not getting overly too fed up with me while managing that PR
1732/// and producing a brilliant
1733/// argument list for doing it in-house (as well as suggesting I write another crate for this)
1734/// * And lastly, because mostly, [**@Eh2406**](https://github.com/Eh2406), for swooping in and saving me in my hour of
1735/// <del>need</del> not having a good replacement.
1736///
1737/// Most of this would have been impossible, of course, without the [`rust-lang` Discord server](https://discord.gg/rust-lang),
1738/// so shoutout to whoever convinced people that Discord is actually good.
1739///
1740/// Sometimes, however, even this isn't enough (see https://github.com/nabijaczleweli/cargo-update/issues/163),
1741/// hence `fork_git`, which actually runs `$GIT` (default: `git`).
1742///
1743/// # Sparse indices
1744///
1745/// Have a `.cache` under the obvious path, then the usual `ca/rg/cargo-update`, but *the file is different than the standard
1746/// format*: it starts with a ^A or ^C (I'm assuming these are versions, and if I looked at more files I would also've seen
1747/// ^C), then Some Binary Data, then the ETag(?), then {NUL, version, NUL, usual JSON blob line} repeats.
1748///
1749/// I do not wanna be touching that shit. Just suck off all the files.<br />
1750/// Shoulda stored the blobs verbatim and used `If-Modified-Since`. Too me.
1751///
1752/// Only in this mode is the package list used.
1753pub fn update_index<W: Write, A: AsRef<str>, I: Iterator<Item = A>>(index_repo: &mut Registry, repo_url: &str, packages: I, http_proxy: Option<&str>,
1754 fork_git: bool, http: &HttpCargoConfig, auth_token: Option<&str>, out: &mut W)
1755 -> Result<(), String> {
1756 write!(out,
1757 " {} registry '{}'{}",
1758 ["Updating", "Polling"][matches!(index_repo, Registry::Sparse(_)) as usize],
1759 repo_url,
1760 ["\n", ""][matches!(index_repo, Registry::Sparse(_)) as usize]).and_then(|_| out.flush())
1761 .map_err(|e| format!("failed to write updating message: {}", e))?;
1762 match index_repo {
1763 Registry::Git(index_repo) => {
1764 if fork_git {
1765 Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git"))).arg("-C")
1766 .arg(index_repo.path())
1767 .args(&["fetch", "-f", repo_url, "HEAD:refs/remotes/origin/HEAD"])
1768 .status()
1769 .map_err(|e| e.to_string())
1770 .and_then(|e| if e.success() {
1771 Ok(())
1772 } else {
1773 Err(e.to_string())
1774 })?;
1775 } else {
1776 index_repo.remote_anonymous(repo_url)
1777 .and_then(|mut r| {
1778 with_authentication(repo_url, |creds| {
1779 let mut cb = RemoteCallbacks::new();
1780 cb.credentials(|a, b, c| creds(a, b, c));
1781
1782 r.fetch(&["HEAD:refs/remotes/origin/HEAD"],
1783 Some(&mut fetch_options_from_proxy_url_and_callbacks(repo_url, http_proxy, cb)),
1784 None)
1785 })
1786 })
1787 .map_err(|e| e.message().to_string())?;
1788 }
1789 }
1790 Registry::Sparse(registry) => {
1791 let mut sucker = CurlMulti::new();
1792 sucker.pipelining(true, true).map_err(|e| format!("pipelining: {}", e))?;
1793
1794 let writussy = Mutex::new(&mut *out);
1795 let mut conns: Vec<_> = Result::from_iter(packages.map(|pkg| {
1796 sucker.add2(CurlEasy::new(SparseHandler(pkg.as_ref().to_string(), Err("init".into()), &writussy, Some(b'.'))))
1797 .map(|h| (Some(h), Ok(())))
1798 .map_err(|e| format!("add2: {}", e))
1799 }))?;
1800 const ATTEMPTS: u8 = 4;
1801 for attempt in 0..ATTEMPTS {
1802 if conns.is_empty() {
1803 break;
1804 }
1805 std::thread::sleep(Duration::from_secs((1 << attempt) - 1));
1806
1807 for c in &mut conns {
1808 let mut conn = sucker.remove2(c.0.take().unwrap()).map_err(|e| format!("remove2: {}", e))?;
1809 conn.get_mut().1 = Ok((vec![], vec![]));
1810 conn.get_mut().3.get_or_insert(b'0' + attempt);
1811 conn.reset();
1812
1813 conn.url(&split_package_path(&conn.get_ref().0).into_iter().fold(repo_url.to_string(), |mut u, s| {
1814 if !u.ends_with('/') {
1815 u.push('/');
1816 }
1817 u.push_str(&s);
1818 u
1819 }))
1820 .map_err(|e| format!("url: {}", e))?;
1821 if let Some(auth_token) = auth_token.as_ref() {
1822 let mut headers = CurlList::new();
1823 headers.append(&format!("Authorization: {}", auth_token)).map_err(|e| format!("append: {}", e))?;
1824 conn.http_headers(headers).map_err(|e| format!("http_headers: {}", e))?;
1825 }
1826 if let Some(http_proxy) = http_proxy {
1827 conn.proxy(http_proxy).map_err(|e| format!("proxy: {}", e))?;
1828 }
1829 conn.pipewait(true).map_err(|e| format!("pipewait: {}", e))?;
1830 conn.progress(true).map_err(|e| format!("progress: {}", e))?;
1831 if let Some(cainfo) = http.cainfo.as_ref() {
1832 conn.cainfo(cainfo).map_err(|e| format!("cainfo: {}", e))?;
1833 }
1834 conn.ssl_options(CurlSslOpt::new().no_revoke(!http.check_revoke)).map_err(|e| format!("ssl_options: {}", e))?;
1835 c.0 = Some(sucker.add2(conn).map_err(|e| format!("add2: {}", e))?);
1836 }
1837 while sucker.perform().map_err(|e| format!("perform: {}", e))? > 0 {
1838 sucker.wait(&mut [], Duration::from_millis(200)).map_err(|e| format!("wait: {}", e))?;
1839 }
1840
1841 sucker.messages(|m| {
1842 for c in &mut conns {
1843 // Yes, a linear search; this is much faster than adding 2+n sets of CURLINFO_PRIVATE calls
1844 if let Some(err) = m.result_for2(&c.0.as_ref().unwrap()) {
1845 c.1 = err;
1846 }
1847 }
1848 });
1849
1850 let mut retainer = |c: &mut (Option<CurlEasyHandle<SparseHandler<'_, '_, _>>>, Result<(), _>)| {
1851 let pkg = mem::take(&mut c.0.as_mut().unwrap().get_mut().0);
1852 match c.0.as_mut().unwrap().response_code().map_err(|e| format!("response_code: {}", e))? {
1853 0 | 200 => {
1854 let (mut resp, buf) =
1855 mem::replace(&mut c.0.as_mut().unwrap().get_mut().1, Err("taken".into())).map_err(|e| format!("package {}: {}", pkg, e))?;
1856 mem::replace(&mut c.1, Ok(())).map_err(|e| format!("package {}: {}", pkg, e))?;
1857 if !buf.is_empty() {
1858 if c.0.as_ref().unwrap().get_ref().3.is_some() {
1859 return Err(format!("package {}: {} bytes of trailing garbage", pkg, buf.len()))?;
1860 } else {
1861 // Some broken registries (kellnr <https://github.com/nabijaczleweli/cargo-update/pull/341>)
1862 // return a complete response that's 1 byte too short;
1863 // if curl told us we managed to read the whole response, try pretending to append it back
1864 resp.extend(crate_version_line(&buf).map_err(|e| format!("package {}: trailing garbage as crate version: {}", pkg, e))?);
1865 }
1866 }
1867 resp.sort();
1868 sucker.remove2(c.0.take().unwrap()).map_err(|e| format!("remove2: {}", e))?;
1869 registry.insert(pkg, resp);
1870 Ok(false)
1871 }
1872 404 => {
1873 sucker.remove2(c.0.take().unwrap()).map_err(|e| format!("remove2: {}", e))?;
1874 Ok(false)
1875 }
1876 rc @ 410 | rc @ 451 => Err(format!("package {} doesn't exist: HTTP {}", pkg, rc)),
1877 rc @ 408 | rc @ 429 | rc @ 503 | rc @ 504 => {
1878 if attempt == ATTEMPTS - 1 {
1879 Err(format!("package {}: HTTP {} after {} attempts", pkg, rc, ATTEMPTS))
1880 } else {
1881 c.0.as_mut().unwrap().get_mut().0 = pkg;
1882 Ok(true)
1883 }
1884 }
1885 rc => Err(format!("package {}: HTTP {}", pkg, rc)),
1886 }
1887 };
1888 let mut err = Ok(());
1889 conns.retain_mut(|c| {
1890 if err.is_err() {
1891 return false;
1892 }
1893 match retainer(c) {
1894 Ok(r) => r,
1895 Err(e) => {
1896 if let Ok(mut out) = writussy.lock() {
1897 let _ = writeln!(out);
1898 }
1899 err = Err(e);
1900 false
1901 }
1902 }
1903 });
1904 err?;
1905 }
1906 if let Ok(mut out) = writussy.lock() {
1907 let _ = writeln!(out);
1908 };
1909 }
1910 }
1911 writeln!(out).map_err(|e| format!("failed to write post-update newline: {}", e))?;
1912
1913 Ok(())
1914}
1915// TODO: Mutex wants to be nonpoison
1916struct SparseHandler<'m, 'w: 'm, W: Write>(String,
1917 Result<(Vec<(Semver, Option<DateTime<FixedOffset>>)>, Vec<u8>), Cow<'static, str>>,
1918 &'m Mutex<&'w mut W>,
1919 Option<u8>);
1920
1921impl<'m, 'w: 'm, W: Write> CurlHandler for SparseHandler<'m, 'w, W> {
1922 fn write(&mut self, data: &[u8]) -> Result<usize, CurlWriteError> {
1923 let mut consumed = 0;
1924 self.1 = mem::replace(&mut self.1, Err("write".into())).and_then(|(mut vers, mut buf)| {
1925 for l in data.split_inclusive(|&b| b == b'\n') {
1926 if !l.ends_with(b"\n") {
1927 buf.extend(l);
1928 consumed += l.len();
1929 continue;
1930 }
1931
1932 let line = if buf.is_empty() {
1933 l
1934 } else {
1935 buf.extend(l);
1936 &buf[..]
1937 };
1938 vers.extend(crate_version_line(line)?);
1939 buf.clear();
1940 consumed += l.len();
1941 }
1942 Ok((vers, buf))
1943 });
1944 Ok(consumed)
1945 }
1946
1947 fn progress(&mut self, dltotal: f64, dlnow: f64, _: f64, _: f64) -> bool {
1948 if dltotal != 0.0 && dltotal == dlnow {
1949 if let Some(status) = self.3.take() {
1950 if let Ok(mut out) = self.2.lock() {
1951 let _ = out.write_all(&[status]).and_then(|_| out.flush());
1952 }
1953 }
1954 }
1955 true
1956 }
1957}
1958
1959
1960/// Either an open git repository with a git registry, or a map of (package, sorted versions), populated by
1961/// [`update_index()`](fn.update_index.html)
1962pub enum Registry {
1963 Git(Repository),
1964 Sparse(BTreeMap<String, Vec<(Semver, Option<DateTime<FixedOffset>>)>>),
1965}
1966
1967/// A git tree corresponding to the latest revision of a git registry.
1968pub enum RegistryTree<'a> {
1969 Git(Tree<'a>),
1970 Sparse,
1971}
1972
1973/// Get `FETCH_HEAD` or `origin/HEAD`, then unwrap it to the tree it points to.
1974pub fn parse_registry_head(registry_repo: &Registry) -> Result<RegistryTree<'_>, GitError> {
1975 match registry_repo {
1976 Registry::Git(registry_repo) => {
1977 registry_repo.revparse_single("FETCH_HEAD")
1978 .or_else(|_| registry_repo.revparse_single("origin/HEAD"))
1979 .map(|h| h.as_commit().unwrap().tree().unwrap())
1980 .map(RegistryTree::Git)
1981 }
1982 Registry::Sparse(_) => Ok(RegistryTree::Sparse),
1983 }
1984}
1985
1986
1987fn proxy_options_from_proxy_url<'a>(repo_url: &str, proxy_url: &str) -> ProxyOptions<'a> {
1988 let mut prx = ProxyOptions::new();
1989 let mut url = Cow::from(proxy_url);
1990
1991 // Cargo allows [protocol://]host[:port], but git needs the protocol, try to crudely add it here if missing;
1992 // confer https://github.com/nabijaczleweli/cargo-update/issues/144.
1993 if Url::parse(proxy_url).is_err() {
1994 if let Ok(rurl) = Url::parse(repo_url) {
1995 let replacement_proxy_url = format!("{}://{}", rurl.scheme(), proxy_url);
1996 if Url::parse(&replacement_proxy_url).is_ok() {
1997 url = Cow::from(replacement_proxy_url);
1998 }
1999 }
2000 }
2001
2002 prx.url(&url);
2003 prx
2004}
2005
2006fn fetch_options_from_proxy_url_and_callbacks<'a>(repo_url: &str, proxy_url: Option<&str>, callbacks: RemoteCallbacks<'a>) -> FetchOptions<'a> {
2007 let mut ret = FetchOptions::new();
2008 if let Some(proxy_url) = proxy_url {
2009 ret.proxy_options(proxy_options_from_proxy_url(repo_url, proxy_url));
2010 }
2011 ret.remote_callbacks(callbacks);
2012 ret
2013}
2014
2015/// Get the URL to update index from, whether it's "sparse", and the cargo name for it from the config file parallel to the
2016/// specified crates file
2017///
2018/// First gets the source name corresponding to the given URL, if appropriate,
2019/// then chases the `source.$SRCNAME.replace-with` chain,
2020/// then retrieves the URL from `source.$SRCNAME.registry` of the final source.
2021///
2022/// Prepopulates with `source.crates-io.registry = "https://github.com/rust-lang/crates.io-index"`,
2023/// as specified in the book
2024///
2025/// If `registries_crates_io_protocol_sparse`, `https://github.com/rust-lang/crates.io-index` is replaced with
2026/// `sparse+https://index.crates.io/`.
2027///
2028/// Consult [#107](https://github.com/nabijaczleweli/cargo-update/issues/107) and
2029/// the Cargo Book for details: https://doc.rust-lang.org/cargo/reference/source-replacement.html,
2030/// https://doc.rust-lang.org/cargo/reference/registries.html.
2031pub fn get_index_url(crates_file: &Path, registry: &str, registries_crates_io_protocol_sparse: bool)
2032 -> Result<(Cow<'static, str>, bool, Cow<'static, str>), Cow<'static, str>> {
2033 let mut config_file = crates_file.with_file_name("config");
2034 let config = if let Ok(cfg) = fs::read_to_string(&config_file).or_else(|_| {
2035 config_file.set_file_name("config.toml");
2036 fs::read_to_string(&config_file)
2037 }) {
2038 toml::from_str::<toml::Value>(&cfg).map_err(|e| format!("{} not TOML: {}", config_file.display(), e))?
2039 } else {
2040 if registry == "https://github.com/rust-lang/crates.io-index" {
2041 if registries_crates_io_protocol_sparse {
2042 return Ok(("https://index.crates.io/".into(), true, "crates-io".into()));
2043 } else {
2044 return Ok((registry.to_string().into(), false, "crates-io".into()));
2045 }
2046 } else {
2047 Err(format!("Non-crates.io registry specified and no config file found at {} or {}. \
2048 Due to a Cargo limitation we will not be able to install from there \
2049 until it's given a [source.NAME] in that file!",
2050 config_file.with_file_name("config").display(),
2051 config_file.display()))?
2052 }
2053 };
2054
2055 let mut replacements = BTreeMap::new();
2056 let mut registries = BTreeMap::new();
2057 let mut cur_source = Cow::from(registry);
2058
2059 // Special case, always present
2060 registries.insert("crates-io",
2061 Cow::from(if registries_crates_io_protocol_sparse {
2062 "sparse+https://index.crates.io/"
2063 } else {
2064 "https://github.com/rust-lang/crates.io-index"
2065 }));
2066 if cur_source == "https://github.com/rust-lang/crates.io-index" || cur_source == "sparse+https://index.crates.io/" {
2067 cur_source = "crates-io".into();
2068 }
2069
2070 if let Some(source) = config.get("source") {
2071 for (name, v) in source.as_table().ok_or("source not table")? {
2072 if let Some(replacement) = v.get("replace-with") {
2073 replacements.insert(&name[..],
2074 replacement.as_str().ok_or_else(|| format!("source.{}.replacement not string", name))?);
2075 }
2076
2077 if let Some(url) = v.get("registry") {
2078 let url = url.as_str().ok_or_else(|| format!("source.{}.registry not string", name))?.to_string().into();
2079 if cur_source == url {
2080 cur_source = name.into();
2081 }
2082
2083 registries.insert(&name[..], url);
2084 }
2085 }
2086 }
2087
2088 if let Some(registries_tabls) = config.get("registries") {
2089 let table = registries_tabls.as_table().ok_or("registries is not a table")?;
2090 for (name, url) in table.iter().flat_map(|(name, val)| val.as_table()?.get("index")?.as_str().map(|v| (name, v))) {
2091 if cur_source == url.strip_prefix("sparse+").unwrap_or(url) {
2092 cur_source = name.into()
2093 }
2094 registries.insert(name, url.into());
2095 }
2096 }
2097
2098 if Url::parse(&cur_source).is_ok() {
2099 Err(format!("Non-crates.io registry specified and {} couldn't be found in the config file at {}. \
2100 Due to a Cargo limitation we will not be able to install from there \
2101 until it's given a [source.NAME] in that file!",
2102 cur_source,
2103 config_file.display()))?
2104 }
2105
2106 while let Some(repl) = replacements.get(&cur_source[..]) {
2107 cur_source = Cow::from(&repl[..]);
2108 }
2109
2110 registries.get(&cur_source[..])
2111 .map(|reg| (reg.strip_prefix("sparse+").unwrap_or(reg).to_string().into(), reg.starts_with("sparse+"), cur_source.to_string().into()))
2112 .ok_or_else(|| {
2113 format!("Couldn't find appropriate source URL for {} in {} (resolved to {:?})",
2114 registry,
2115 config_file.display(),
2116 cur_source)
2117 .into()
2118 })
2119}
2120
2121/// Based on
2122/// https://github.com/rust-lang/cargo/blob/bb28e71202260180ecff658cd0fa0c7ba86d0296/src/cargo/sources/git/utils.rs#L344
2123/// and
2124/// https://github.com/rust-lang/cargo/blob/5102de2b7de997b03181063417f20874a06a67c0/src/cargo/sources/git/utils.rs#L644,
2125/// then
2126/// https://github.com/rust-lang/cargo/blob/5102de2b7de997b03181063417f20874a06a67c0/src/cargo/sources/git/utils.rs#L437
2127/// (see that link for full comments)
2128fn with_authentication<T, F>(url: &str, mut f: F) -> Result<T, GitError>
2129 where F: FnMut(&mut git2::Credentials) -> Result<T, GitError>
2130{
2131 let cfg = GitConfig::open_default().unwrap();
2132
2133 let mut cred_helper = git2::CredentialHelper::new(url);
2134 cred_helper.config(&cfg);
2135
2136 let mut ssh_username_requested = false;
2137 let mut cred_helper_bad = None;
2138 let mut ssh_agent_attempts = Vec::new();
2139 let mut any_attempts = false;
2140 let mut tried_ssh_key = false;
2141
2142 let mut res = f(&mut |url, username, allowed| {
2143 any_attempts = true;
2144
2145 if allowed.contains(CredentialType::USERNAME) {
2146 ssh_username_requested = true;
2147
2148 Err(GitError::from_str("username to be tried later"))
2149 } else if allowed.contains(CredentialType::SSH_KEY) && !tried_ssh_key {
2150 tried_ssh_key = true;
2151
2152 let username = username.unwrap();
2153 ssh_agent_attempts.push(username.to_string());
2154
2155 GitCred::ssh_key_from_agent(username)
2156 } else if allowed.contains(CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none() {
2157 let ret = GitCred::credential_helper(&cfg, url, username);
2158 cred_helper_bad = Some(ret.is_err());
2159 ret
2160 } else if allowed.contains(CredentialType::DEFAULT) {
2161 GitCred::default()
2162 } else {
2163 Err(GitError::from_str("no authentication available"))
2164 }
2165 });
2166
2167 if ssh_username_requested {
2168 // NOTE: this is the only divergence from the original cargo code: we also try cfg["user.name"]
2169 // see https://github.com/nabijaczleweli/cargo-update/issues/110#issuecomment-533091965 for explanation
2170 for uname in cred_helper.username
2171 .into_iter()
2172 .chain(cfg.get_string("user.name"))
2173 .chain(["USERNAME", "USER"].iter().flat_map(env::var))
2174 .chain(Some("git".to_string())) {
2175 let mut ssh_attempts = 0;
2176
2177 res = f(&mut |_, _, allowed| {
2178 if allowed.contains(CredentialType::USERNAME) {
2179 return GitCred::username(&uname);
2180 } else if allowed.contains(CredentialType::SSH_KEY) {
2181 ssh_attempts += 1;
2182 if ssh_attempts == 1 {
2183 ssh_agent_attempts.push(uname.to_string());
2184 return GitCred::ssh_key_from_agent(&uname);
2185 }
2186 }
2187
2188 Err(GitError::from_str("no authentication available"))
2189 });
2190
2191 if ssh_attempts != 2 {
2192 break;
2193 }
2194 }
2195 }
2196
2197 if res.is_ok() || !any_attempts {
2198 res
2199 } else {
2200 let err = res.err().map(|e| format!("{}: ", e)).unwrap_or_default();
2201
2202 let mut msg = format!("{}failed to authenticate when downloading repository {}", err, url);
2203 if !ssh_agent_attempts.is_empty() {
2204 msg.push_str(" (tried ssh-agent, but none of the following usernames worked: ");
2205 for (i, uname) in ssh_agent_attempts.into_iter().enumerate() {
2206 if i != 0 {
2207 msg.push_str(", ");
2208 }
2209 msg.push('\"');
2210 msg.push_str(&uname);
2211 msg.push('\"');
2212 }
2213 msg.push(')');
2214 }
2215
2216 if let Some(failed_cred_helper) = cred_helper_bad {
2217 msg.push_str(" (tried to find username+password via ");
2218 if failed_cred_helper {
2219 msg.push_str("git's credential.helper support, but failed)");
2220 } else {
2221 msg.push_str("credential.helper, but found credentials were incorrect)");
2222 }
2223 }
2224
2225 Err(GitError::from_str(&msg))
2226 }
2227}
2228
2229
2230/// Split and lower-case `cargo-update` into `[ca, rg, cargo-update]`, `jot` into `[3, j, jot]`, &c.
2231pub fn split_package_path(cratename: &str) -> Vec<Cow<'_, str>> {
2232 let mut elems = Vec::new();
2233 if cratename.is_empty() {
2234 panic!("0-length cratename");
2235 }
2236 if cratename.len() <= 3 {
2237 elems.push(["1", "2", "3"][cratename.len() - 1].into())
2238 }
2239 match cratename.len() {
2240 1 | 2 => {}
2241 3 => elems.push(lcase(&cratename[0..1])),
2242 _ => {
2243 elems.push(lcase(&cratename[0..2]));
2244 elems.push(lcase(&cratename[2..4]));
2245 }
2246 }
2247 elems.push(lcase(cratename));
2248 elems
2249}
2250
2251fn lcase(s: &str) -> Cow<'_, str> {
2252 if s.bytes().any(|b| b.is_ascii_uppercase()) {
2253 s.to_ascii_lowercase().into()
2254 } else {
2255 s.into()
2256 }
2257}
2258
2259/// Find package data in the specified cargo git index tree.
2260pub fn find_package_data<'t>(cratename: &str, registry: &Tree<'t>, registry_parent: &'t Repository) -> Option<Blob<'t>> {
2261 let elems = split_package_path(cratename);
2262
2263 let ent = registry.get_name(&elems[0])?;
2264 let obj = ent.to_object(registry_parent).ok()?;
2265 let ent = obj.as_tree()?.get_name(&elems[1])?;
2266 let obj = ent.to_object(registry_parent).ok()?;
2267 let obj = if elems.len() == 3 {
2268 let ent = obj.as_tree()?.get_name(&elems[2])?;
2269 ent.to_object(registry_parent).ok()?
2270 } else {
2271 obj
2272 };
2273 obj.into_blob().ok()
2274}
2275
2276/// Check if there's a proxy specified to be used.
2277///
2278/// Look for `http.proxy` key in the `config` file parallel to the specified crates file.
2279///
2280/// Then look for `git`'s `http.proxy`.
2281///
2282/// Then for the `http_proxy`, `HTTP_PROXY`, `https_proxy`, and `HTTPS_PROXY` environment variables, in that order.
2283///
2284/// Based on Cargo's [`http_proxy_exists()` and
2285/// `http_proxy()`](https://github.com/rust-lang/cargo/blob/eebd1da3a89e9c7788d109b3e615e1e25dc2cfcd/src/cargo/ops/registry.rs)
2286///
2287/// If a proxy is specified, but an empty string, treat it as unspecified.
2288///
2289/// # Examples
2290///
2291/// ```
2292/// # use cargo_update::ops::find_proxy;
2293/// # use std::env::temp_dir;
2294/// # let crates_file = temp_dir().join(".crates.toml");
2295/// match find_proxy(&crates_file) {
2296/// Some(proxy) => println!("Proxy found at {}", proxy),
2297/// None => println!("No proxy detected"),
2298/// }
2299/// ```
2300pub fn find_proxy(crates_file: &Path) -> Option<String> {
2301 if let Ok(crates_file) = fs::read_to_string(crates_file) {
2302 if let Some(toml::Value::String(proxy)) =
2303 toml::from_str::<toml::Value>(&crates_file)
2304 .unwrap()
2305 .get_mut("http")
2306 .and_then(|t| t.as_table_mut())
2307 .and_then(|t| t.remove("proxy")) {
2308 if !proxy.is_empty() {
2309 return Some(proxy);
2310 }
2311 }
2312 }
2313
2314 if let Ok(cfg) = GitConfig::open_default() {
2315 if let Ok(proxy) = cfg.get_string("http.proxy") {
2316 if !proxy.is_empty() {
2317 return Some(proxy);
2318 }
2319 }
2320 }
2321
2322 ["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"].iter().flat_map(env::var).filter(|proxy| !proxy.is_empty()).next()
2323}
2324
2325/// Find the bare git repository in the specified directory for the specified crate
2326///
2327/// The db directory is usually `$HOME/.cargo/git/db/`
2328///
2329/// The resulting paths are children of this directory in the format
2330/// [`{last_url_segment || "_empty"}-{hash(url)}`]
2331/// (https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/sources/git/source.rs#L62-L73)
2332pub fn find_git_db_repo(git_db_dir: &Path, url: &str) -> Option<PathBuf> {
2333 let path = git_db_dir.join(format!("{}-{}",
2334 match Url::parse(url)
2335 .ok()?
2336 .path_segments()
2337 .and_then(|mut segs| segs.next_back())
2338 .unwrap_or("") {
2339 "" => "_empty",
2340 url => url,
2341 },
2342 cargo_hash(url)));
2343
2344 if path.is_dir() { Some(path) } else { None }
2345}
2346
2347
2348/// The short filesystem name for the repository, as used by `cargo`
2349///
2350/// Must be equivalent to
2351/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/sources/registry/mod.rs#L387-L402
2352/// and
2353/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/util/hex.rs
2354///
2355/// For main repository it's `github.com-1ecc6299db9ec823`
2356pub fn registry_shortname(url: &str) -> String {
2357 struct RegistryHash<'u>(&'u str);
2358 impl<'u> Hash for RegistryHash<'u> {
2359 fn hash<S: Hasher>(&self, hasher: &mut S) {
2360 SourceKind::Registry.hash(hasher);
2361 self.0.hash(hasher);
2362 }
2363 }
2364
2365 format!("{}-{}",
2366 Url::parse(url).map_err(|e| format!("{} not an URL: {}", url, e)).unwrap().host_str().unwrap_or(""),
2367 cargo_hash(RegistryHash(url)))
2368}
2369
2370/// Stolen from and equivalent to `short_hash()` from
2371/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/util/hex.rs
2372#[allow(deprecated)]
2373pub fn cargo_hash<T: Hash>(whom: T) -> String {
2374 use std::hash::SipHasher;
2375
2376 let mut hasher = SipHasher::new_with_keys(0, 0);
2377 whom.hash(&mut hasher);
2378 let hash = hasher.finish();
2379 hex::encode(&[(hash >> 0) as u8,
2380 (hash >> 8) as u8,
2381 (hash >> 16) as u8,
2382 (hash >> 24) as u8,
2383 (hash >> 32) as u8,
2384 (hash >> 40) as u8,
2385 (hash >> 48) as u8,
2386 (hash >> 56) as u8])
2387}
2388
2389/// These two are stolen verbatim from
2390/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/core/source/source_id.rs#L48-L73
2391/// in order to match our hash with
2392/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/core/source/source_id.rs#L510
2393#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2394#[allow(unused)]
2395enum SourceKind {
2396 Git(GitReference),
2397 Path,
2398 Registry,
2399 LocalRegistry,
2400 Directory,
2401}
2402
2403#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2404#[allow(unused)]
2405enum GitReference {
2406 Tag(String),
2407 Branch(String),
2408 Rev(String),
2409}
2410
2411
2412trait SemverExt {
2413 fn is_prerelease(&self) -> bool;
2414}
2415impl SemverExt for Semver {
2416 fn is_prerelease(&self) -> bool {
2417 !self.pre.is_empty()
2418 }
2419}