]>
Commit | Line | Data |
---|---|---|
bc60f64b | 1 | use std::collections::HashMap; |
83d877e9 | 2 | use std::fmt; |
bc60f64b | 3 | |
ebca5190 | 4 | use anyhow::{bail, Context as _}; |
83d877e9 | 5 | use semver::Version; |
5415a341 | 6 | use serde::{de, ser}; |
32562e92 | 7 | use url::Url; |
83d877e9 | 8 | |
04ddd4d0 | 9 | use crate::core::PackageId; |
ebca5190 | 10 | use crate::util::errors::CargoResult; |
7f73a6c7 | 11 | use crate::util::interning::InternedString; |
b04c7fb8 | 12 | use crate::util::lev_distance; |
2415a298 | 13 | use 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 | 25 | pub struct PackageIdSpec { |
77a7e3fe | 26 | name: InternedString, |
83d877e9 AC |
27 | version: Option<Version>, |
28 | url: Option<Url>, | |
29 | } | |
30 | ||
31 | impl 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 | 282 | impl 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 |
305 | impl 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 | ||
314 | impl<'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)] |
325 | mod 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 | } |