]> git.proxmox.com Git - cargo.git/blame - src/cargo/core/package_id_spec.rs
bump version to 0.66.0+pve1-1~bpo11+pve1
[cargo.git] / src / cargo / core / package_id_spec.rs
CommitLineData
bc60f64b 1use std::collections::HashMap;
83d877e9 2use std::fmt;
bc60f64b 3
ebca5190 4use anyhow::{bail, Context as _};
83d877e9 5use semver::Version;
5415a341 6use serde::{de, ser};
32562e92 7use url::Url;
83d877e9 8
04ddd4d0 9use crate::core::PackageId;
ebca5190 10use crate::util::errors::CargoResult;
7f73a6c7 11use crate::util::interning::InternedString;
b04c7fb8 12use crate::util::lev_distance;
2415a298 13use crate::util::{validate_package_name, IntoUrl, ToSemver};
83d877e9 14
e82e9c19 15/// Some or all of the data required to identify a package:
7868945b
RD
16///
17/// 1. the package name (a `String`, required)
18/// 2. the package version (a `Version`, optional)
19/// 3. the package source (a `Url`, optional)
20///
f7c91ba6 21/// If any of the optional fields are omitted, then the package ID may be ambiguous, there may be
7868945b 22/// more than one package/version/url combo that will match. However, often just the name is
f7c91ba6 23/// sufficient to uniquely define a package ID.
5415a341 24#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
83d877e9 25pub struct PackageIdSpec {
77a7e3fe 26 name: InternedString,
83d877e9
AC
27 version: Option<Version>,
28 url: Option<Url>,
29}
30
31impl PackageIdSpec {
7868945b
RD
32 /// Parses a spec string and returns a `PackageIdSpec` if the string was valid.
33 ///
34 /// # Examples
35 /// Some examples of valid strings
36 ///
37 /// ```
38 /// use cargo::core::PackageIdSpec;
39 ///
40 /// let specs = vec![
c5d23046 41 /// "https://crates.io/foo",
0c3851c0
JC
42 /// "https://crates.io/foo#1.2.3",
43 /// "https://crates.io/foo#bar:1.2.3",
cdec3838 44 /// "https://crates.io/foo#bar@1.2.3",
7868945b
RD
45 /// "foo",
46 /// "foo:1.2.3",
cdec3838 47 /// "foo@1.2.3",
7868945b
RD
48 /// ];
49 /// for spec in specs {
50 /// assert!(PackageIdSpec::parse(spec).is_ok());
51 /// }
83d877e9 52 pub fn parse(spec: &str) -> CargoResult<PackageIdSpec> {
1bbc0569 53 if spec.contains("://") {
930134c7 54 if let Ok(url) = spec.into_url() {
f1062ce3 55 return PackageIdSpec::from_url(url);
83d877e9 56 }
1bbc0569
WL
57 } else if spec.contains('/') || spec.contains('\\') {
58 let abs = std::env::current_dir().unwrap_or_default().join(spec);
59 if abs.exists() {
60 let maybe_url = Url::from_file_path(abs)
61 .map_or_else(|_| "a file:// URL".to_string(), |url| url.to_string());
62 bail!(
63 "package ID specification `{}` looks like a file path, \
64 maybe try {}",
65 spec,
66 maybe_url
67 );
83d877e9
AC
68 }
69 }
cdec3838 70 let mut parts = spec.splitn(2, [':', '@']);
83d877e9
AC
71 let name = parts.next().unwrap();
72 let version = match parts.next() {
cffd5b24 73 Some(version) => Some(version.to_semver()?),
83d877e9
AC
74 None => None,
75 };
080f0b34 76 validate_package_name(name, "pkgid", "")?;
83d877e9 77 Ok(PackageIdSpec {
77a7e3fe 78 name: InternedString::new(name),
0247dc42 79 version,
83d877e9
AC
80 url: None,
81 })
82 }
83
7868945b 84 /// Roughly equivalent to `PackageIdSpec::parse(spec)?.query(i)`
dae87a26 85 pub fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
1e682848 86 where
dae87a26 87 I: IntoIterator<Item = PackageId>,
bc60f64b 88 {
a22b3ac2 89 let i: Vec<_> = i.into_iter().collect();
ebca5190 90 let spec = PackageIdSpec::parse(spec).with_context(|| {
a22b3ac2 91 let suggestion = lev_distance::closest_msg(spec, i.iter(), |id| id.name().as_str());
9099b491 92 format!("invalid package ID specification: `{}`{}", spec, suggestion)
a22b3ac2 93 })?;
bc60f64b
AC
94 spec.query(i)
95 }
96
7868945b
RD
97 /// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `Version` and `Url`
98 /// fields filled in.
dae87a26 99 pub fn from_package_id(package_id: PackageId) -> PackageIdSpec {
83d877e9 100 PackageIdSpec {
77a7e3fe 101 name: package_id.name(),
7a2facba
AC
102 version: Some(package_id.version().clone()),
103 url: Some(package_id.source_id().url().clone()),
83d877e9
AC
104 }
105 }
106
7868945b 107 /// Tries to convert a valid `Url` to a `PackageIdSpec`.
83d877e9 108 fn from_url(mut url: Url) -> CargoResult<PackageIdSpec> {
32562e92 109 if url.query().is_some() {
b04c7fb8 110 bail!("cannot have a query string in a pkgid: {}", url)
83d877e9 111 }
32562e92
SS
112 let frag = url.fragment().map(|s| s.to_owned());
113 url.set_fragment(None);
83d877e9 114 let (name, version) = {
e5a11190
E
115 let mut path = url
116 .path_segments()
3a18c89a 117 .ok_or_else(|| anyhow::format_err!("pkgid urls must have a path: {}", url))?;
e95044e3 118 let path_name = path.next_back().ok_or_else(|| {
3a18c89a 119 anyhow::format_err!(
1e682848
AC
120 "pkgid urls must have at least one path \
121 component: {}",
122 url
123 )
82655b46 124 })?;
83d877e9
AC
125 match frag {
126 Some(fragment) => {
cdec3838 127 let mut parts = fragment.splitn(2, [':', '@']);
83d877e9
AC
128 let name_or_version = parts.next().unwrap();
129 match parts.next() {
130 Some(part) => {
c7de4859 131 let version = part.to_semver()?;
77a7e3fe 132 (InternedString::new(name_or_version), Some(version))
83d877e9
AC
133 }
134 None => {
1e682848 135 if name_or_version.chars().next().unwrap().is_alphabetic() {
77a7e3fe 136 (InternedString::new(name_or_version), None)
83d877e9 137 } else {
c7de4859 138 let version = name_or_version.to_semver()?;
77a7e3fe 139 (InternedString::new(path_name), Some(version))
83d877e9
AC
140 }
141 }
142 }
143 }
77a7e3fe 144 None => (InternedString::new(path_name), None),
83d877e9
AC
145 }
146 };
147 Ok(PackageIdSpec {
0247dc42
E
148 name,
149 version,
83d877e9
AC
150 url: Some(url),
151 })
152 }
153
77a7e3fe
E
154 pub fn name(&self) -> InternedString {
155 self.name
1e682848 156 }
8a6f10a7 157
1e682848
AC
158 pub fn version(&self) -> Option<&Version> {
159 self.version.as_ref()
160 }
8a6f10a7 161
1e682848
AC
162 pub fn url(&self) -> Option<&Url> {
163 self.url.as_ref()
164 }
83d877e9 165
0d038a90
AC
166 pub fn set_url(&mut self, url: Url) {
167 self.url = Some(url);
168 }
169
4a64d05e 170 /// Checks whether the given `PackageId` matches the `PackageIdSpec`.
dae87a26 171 pub fn matches(&self, package_id: PackageId) -> bool {
77a7e3fe 172 if self.name() != package_id.name() {
1e682848
AC
173 return false;
174 }
83d877e9 175
23591fe5
LL
176 if let Some(ref v) = self.version {
177 if v != package_id.version() {
178 return false;
179 }
83d877e9
AC
180 }
181
182 match self.url {
7a2facba 183 Some(ref u) => u == package_id.source_id().url(),
1e682848 184 None => true,
83d877e9
AC
185 }
186 }
bc60f64b 187
7868945b
RD
188 /// Checks a list of `PackageId`s to find 1 that matches this `PackageIdSpec`. If 0, 2, or
189 /// more are found, then this returns an error.
dae87a26 190 pub fn query<I>(&self, i: I) -> CargoResult<PackageId>
1e682848 191 where
dae87a26 192 I: IntoIterator<Item = PackageId>,
bc60f64b 193 {
b04c7fb8
EH
194 let all_ids: Vec<_> = i.into_iter().collect();
195 let mut ids = all_ids.iter().copied().filter(|&id| self.matches(id));
bc60f64b
AC
196 let ret = match ids.next() {
197 Some(id) => id,
b04c7fb8
EH
198 None => {
199 let mut suggestion = String::new();
200 let try_spec = |spec: PackageIdSpec, suggestion: &mut String| {
201 let try_matches: Vec<_> = all_ids
202 .iter()
203 .copied()
204 .filter(|&id| spec.matches(id))
205 .collect();
206 if !try_matches.is_empty() {
207 suggestion.push_str("\nDid you mean one of these?\n");
208 minimize(suggestion, &try_matches, self);
209 }
210 };
211 if self.url.is_some() {
212 try_spec(
213 PackageIdSpec {
214 name: self.name,
215 version: self.version.clone(),
216 url: None,
217 },
218 &mut suggestion,
219 );
220 }
221 if suggestion.is_empty() && self.version.is_some() {
222 try_spec(
223 PackageIdSpec {
224 name: self.name,
225 version: None,
226 url: None,
227 },
228 &mut suggestion,
229 );
230 }
231 if suggestion.is_empty() {
232 suggestion.push_str(&lev_distance::closest_msg(
233 &self.name,
234 all_ids.iter(),
235 |id| id.name().as_str(),
236 ));
237 }
238
239 bail!(
240 "package ID specification `{}` did not match any packages{}",
241 self,
242 suggestion
243 );
244 }
bc60f64b
AC
245 };
246 return match ids.next() {
247 Some(other) => {
1e682848
AC
248 let mut msg = format!(
249 "There are multiple `{}` packages in \
250 your project, and the specification \
251 `{}` is ambiguous.\n\
252 Please re-run this command \
253 with `-p <spec>` where `<spec>` is one \
254 of the following:",
255 self.name(),
256 self
257 );
bc60f64b
AC
258 let mut vec = vec![ret, other];
259 vec.extend(ids);
23591fe5 260 minimize(&mut msg, &vec, self);
3a18c89a 261 Err(anyhow::format_err!("{}", msg))
bc60f64b 262 }
1e682848 263 None => Ok(ret),
bc60f64b
AC
264 };
265
dae87a26 266 fn minimize(msg: &mut String, ids: &[PackageId], spec: &PackageIdSpec) {
bc60f64b 267 let mut version_cnt = HashMap::new();
23591fe5 268 for id in ids {
bc60f64b
AC
269 *version_cnt.entry(id.version()).or_insert(0) += 1;
270 }
23591fe5 271 for id in ids {
bc60f64b 272 if version_cnt[id.version()] == 1 {
cdec3838 273 msg.push_str(&format!("\n {}@{}", spec.name(), id.version()));
bc60f64b 274 } else {
1e682848 275 msg.push_str(&format!("\n {}", PackageIdSpec::from_package_id(*id)));
bc60f64b
AC
276 }
277 }
278 }
279 }
83d877e9
AC
280}
281
213afc02 282impl fmt::Display for PackageIdSpec {
b8b7faee 283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83d877e9
AC
284 let mut printed_name = false;
285 match self.url {
286 Some(ref url) => {
1bbc0569 287 write!(f, "{}", url)?;
77a7e3fe 288 if url.path_segments().unwrap().next_back().unwrap() != &*self.name {
83d877e9 289 printed_name = true;
82655b46 290 write!(f, "#{}", self.name)?;
83d877e9
AC
291 }
292 }
1e682848
AC
293 None => {
294 printed_name = true;
cdec3838 295 write!(f, "{}", self.name)?;
1e682848 296 }
83d877e9 297 }
23591fe5 298 if let Some(ref v) = self.version {
cdec3838 299 write!(f, "{}{}", if printed_name { "@" } else { "#" }, v)?;
83d877e9
AC
300 }
301 Ok(())
302 }
303}
304
5415a341
EH
305impl ser::Serialize for PackageIdSpec {
306 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
307 where
308 S: ser::Serializer,
309 {
310 self.to_string().serialize(s)
311 }
312}
313
314impl<'de> de::Deserialize<'de> for PackageIdSpec {
315 fn deserialize<D>(d: D) -> Result<PackageIdSpec, D::Error>
316 where
317 D: de::Deserializer<'de>,
318 {
319 let string = String::deserialize(d)?;
320 PackageIdSpec::parse(&string).map_err(de::Error::custom)
321 }
322}
323
83d877e9
AC
324#[cfg(test)]
325mod tests {
32562e92 326 use super::PackageIdSpec;
04ddd4d0 327 use crate::core::{PackageId, SourceId};
7f73a6c7 328 use crate::util::interning::InternedString;
cffd5b24 329 use crate::util::ToSemver;
e5a11190 330 use url::Url;
83d877e9
AC
331
332 #[test]
333 fn good_parsing() {
cdec3838
EP
334 #[track_caller]
335 fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) {
83d877e9
AC
336 let parsed = PackageIdSpec::parse(spec).unwrap();
337 assert_eq!(parsed, expected);
cdec3838 338 assert_eq!(parsed.to_string(), expected_rendered);
83d877e9
AC
339 }
340
1e682848 341 ok(
1bbc0569 342 "https://crates.io/foo",
1e682848 343 PackageIdSpec {
77a7e3fe 344 name: InternedString::new("foo"),
1e682848 345 version: None,
1bbc0569 346 url: Some(Url::parse("https://crates.io/foo").unwrap()),
1e682848 347 },
cdec3838 348 "https://crates.io/foo",
1e682848
AC
349 );
350 ok(
1bbc0569 351 "https://crates.io/foo#1.2.3",
1e682848 352 PackageIdSpec {
77a7e3fe 353 name: InternedString::new("foo"),
cffd5b24 354 version: Some("1.2.3".to_semver().unwrap()),
1bbc0569 355 url: Some(Url::parse("https://crates.io/foo").unwrap()),
1e682848 356 },
cdec3838 357 "https://crates.io/foo#1.2.3",
1e682848
AC
358 );
359 ok(
1bbc0569 360 "https://crates.io/foo#bar:1.2.3",
1e682848 361 PackageIdSpec {
77a7e3fe 362 name: InternedString::new("bar"),
cffd5b24 363 version: Some("1.2.3".to_semver().unwrap()),
1bbc0569 364 url: Some(Url::parse("https://crates.io/foo").unwrap()),
1e682848 365 },
cdec3838
EP
366 "https://crates.io/foo#bar@1.2.3",
367 );
368 ok(
369 "https://crates.io/foo#bar@1.2.3",
370 PackageIdSpec {
371 name: InternedString::new("bar"),
372 version: Some("1.2.3".to_semver().unwrap()),
373 url: Some(Url::parse("https://crates.io/foo").unwrap()),
374 },
375 "https://crates.io/foo#bar@1.2.3",
1e682848
AC
376 );
377 ok(
378 "foo",
379 PackageIdSpec {
77a7e3fe 380 name: InternedString::new("foo"),
1e682848
AC
381 version: None,
382 url: None,
383 },
cdec3838 384 "foo",
1e682848
AC
385 );
386 ok(
387 "foo:1.2.3",
388 PackageIdSpec {
77a7e3fe 389 name: InternedString::new("foo"),
cffd5b24 390 version: Some("1.2.3".to_semver().unwrap()),
1e682848
AC
391 url: None,
392 },
cdec3838
EP
393 "foo@1.2.3",
394 );
395 ok(
396 "foo@1.2.3",
397 PackageIdSpec {
398 name: InternedString::new("foo"),
399 version: Some("1.2.3".to_semver().unwrap()),
400 url: None,
401 },
402 "foo@1.2.3",
1e682848 403 );
83d877e9
AC
404 }
405
406 #[test]
407 fn bad_parsing() {
408 assert!(PackageIdSpec::parse("baz:").is_err());
b43e6dd2 409 assert!(PackageIdSpec::parse("baz:*").is_err());
83d877e9 410 assert!(PackageIdSpec::parse("baz:1.0").is_err());
cdec3838
EP
411 assert!(PackageIdSpec::parse("baz@").is_err());
412 assert!(PackageIdSpec::parse("baz@*").is_err());
413 assert!(PackageIdSpec::parse("baz@1.0").is_err());
0c3851c0
JC
414 assert!(PackageIdSpec::parse("https://baz:1.0").is_err());
415 assert!(PackageIdSpec::parse("https://#baz:1.0").is_err());
83d877e9
AC
416 }
417
418 #[test]
419 fn matching() {
0c3851c0 420 let url = Url::parse("https://example.com").unwrap();
dc7422b6 421 let sid = SourceId::for_registry(&url).unwrap();
e5a11190
E
422 let foo = PackageId::new("foo", "1.2.3", sid).unwrap();
423 let bar = PackageId::new("bar", "1.2.3", sid).unwrap();
83d877e9 424
dae87a26
E
425 assert!(PackageIdSpec::parse("foo").unwrap().matches(foo));
426 assert!(!PackageIdSpec::parse("foo").unwrap().matches(bar));
427 assert!(PackageIdSpec::parse("foo:1.2.3").unwrap().matches(foo));
428 assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
cdec3838
EP
429 assert!(PackageIdSpec::parse("foo@1.2.3").unwrap().matches(foo));
430 assert!(!PackageIdSpec::parse("foo@1.2.2").unwrap().matches(foo));
83d877e9
AC
431 }
432}