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