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