1use std::collections::HashMap;
2use std::fmt;
3
4use semver::Version;
5use serde::{de, ser};
6use url::Url;
7
8use crate::core::interning::InternedString;
9use crate::core::PackageId;
10use crate::util::errors::{CargoResult, CargoResultExt};
11use crate::util::{validate_package_name, IntoUrl, ToSemver};
12
13#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
23pub struct PackageIdSpec {
24 name: InternedString,
25 version: Option<Version>,
26 url: Option<Url>,
27}
28
29impl PackageIdSpec {
30 pub fn parse(spec: &str) -> CargoResult<PackageIdSpec> {
52 if spec.contains('/') {
53 if let Ok(url) = spec.into_url() {
54 return PackageIdSpec::from_url(url);
55 }
56 if !spec.contains("://") {
57 if let Ok(url) = Url::parse(&format!("cargo://{}", spec)) {
58 return PackageIdSpec::from_url(url);
59 }
60 }
61 }
62 let mut parts = spec.splitn(2, ':');
63 let name = parts.next().unwrap();
64 let version = match parts.next() {
65 Some(version) => Some(version.to_semver()?),
66 None => None,
67 };
68 validate_package_name(name, "pkgid", "")?;
69 Ok(PackageIdSpec {
70 name: InternedString::new(name),
71 version,
72 url: None,
73 })
74 }
75
76 pub fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
78 where
79 I: IntoIterator<Item = PackageId>,
80 {
81 let spec = PackageIdSpec::parse(spec)
82 .chain_err(|| anyhow::format_err!("invalid package ID specification: `{}`", spec))?;
83 spec.query(i)
84 }
85
86 pub fn from_package_id(package_id: PackageId) -> PackageIdSpec {
89 PackageIdSpec {
90 name: package_id.name(),
91 version: Some(package_id.version().clone()),
92 url: Some(package_id.source_id().url().clone()),
93 }
94 }
95
96 fn from_url(mut url: Url) -> CargoResult<PackageIdSpec> {
98 if url.query().is_some() {
99 anyhow::bail!("cannot have a query string in a pkgid: {}", url)
100 }
101 let frag = url.fragment().map(|s| s.to_owned());
102 url.set_fragment(None);
103 let (name, version) = {
104 let mut path = url
105 .path_segments()
106 .ok_or_else(|| anyhow::format_err!("pkgid urls must have a path: {}", url))?;
107 let path_name = path.next_back().ok_or_else(|| {
108 anyhow::format_err!(
109 "pkgid urls must have at least one path \
110 component: {}",
111 url
112 )
113 })?;
114 match frag {
115 Some(fragment) => {
116 let mut parts = fragment.splitn(2, ':');
117 let name_or_version = parts.next().unwrap();
118 match parts.next() {
119 Some(part) => {
120 let version = part.to_semver()?;
121 (InternedString::new(name_or_version), Some(version))
122 }
123 None => {
124 if name_or_version.chars().next().unwrap().is_alphabetic() {
125 (InternedString::new(name_or_version), None)
126 } else {
127 let version = name_or_version.to_semver()?;
128 (InternedString::new(path_name), Some(version))
129 }
130 }
131 }
132 }
133 None => (InternedString::new(path_name), None),
134 }
135 };
136 Ok(PackageIdSpec {
137 name,
138 version,
139 url: Some(url),
140 })
141 }
142
143 pub fn name(&self) -> InternedString {
144 self.name
145 }
146
147 pub fn version(&self) -> Option<&Version> {
148 self.version.as_ref()
149 }
150
151 pub fn url(&self) -> Option<&Url> {
152 self.url.as_ref()
153 }
154
155 pub fn set_url(&mut self, url: Url) {
156 self.url = Some(url);
157 }
158
159 pub fn matches(&self, package_id: PackageId) -> bool {
161 if self.name() != package_id.name() {
162 return false;
163 }
164
165 if let Some(ref v) = self.version {
166 if v != package_id.version() {
167 return false;
168 }
169 }
170
171 match self.url {
172 Some(ref u) => u == package_id.source_id().url(),
173 None => true,
174 }
175 }
176
177 pub fn query<I>(&self, i: I) -> CargoResult<PackageId>
180 where
181 I: IntoIterator<Item = PackageId>,
182 {
183 let mut ids = i.into_iter().filter(|p| self.matches(*p));
184 let ret = match ids.next() {
185 Some(id) => id,
186 None => anyhow::bail!(
187 "package ID specification `{}` \
188 matched no packages",
189 self
190 ),
191 };
192 return match ids.next() {
193 Some(other) => {
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 );
204 let mut vec = vec![ret, other];
205 vec.extend(ids);
206 minimize(&mut msg, &vec, self);
207 Err(anyhow::format_err!("{}", msg))
208 }
209 None => Ok(ret),
210 };
211
212 fn minimize(msg: &mut String, ids: &[PackageId], spec: &PackageIdSpec) {
213 let mut version_cnt = HashMap::new();
214 for id in ids {
215 *version_cnt.entry(id.version()).or_insert(0) += 1;
216 }
217 for id in ids {
218 if version_cnt[id.version()] == 1 {
219 msg.push_str(&format!("\n {}:{}", spec.name(), id.version()));
220 } else {
221 msg.push_str(&format!("\n {}", PackageIdSpec::from_package_id(*id)));
222 }
223 }
224 }
225 }
226}
227
228impl fmt::Display for PackageIdSpec {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 let mut printed_name = false;
231 match self.url {
232 Some(ref url) => {
233 if url.scheme() == "cargo" {
234 write!(f, "{}{}", url.host().unwrap(), url.path())?;
235 } else {
236 write!(f, "{}", url)?;
237 }
238 if url.path_segments().unwrap().next_back().unwrap() != &*self.name {
239 printed_name = true;
240 write!(f, "#{}", self.name)?;
241 }
242 }
243 None => {
244 printed_name = true;
245 write!(f, "{}", self.name)?
246 }
247 }
248 if let Some(ref v) = self.version {
249 write!(f, "{}{}", if printed_name { ":" } else { "#" }, v)?;
250 }
251 Ok(())
252 }
253}
254
255impl 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
264impl<'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
274#[cfg(test)]
275mod tests {
276 use super::PackageIdSpec;
277 use crate::core::interning::InternedString;
278 use crate::core::{PackageId, SourceId};
279 use crate::util::ToSemver;
280 use url::Url;
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);
287 assert_eq!(parsed.to_string(), spec);
288 }
289
290 ok(
291 "https://crates.io/foo#1.2.3",
292 PackageIdSpec {
293 name: InternedString::new("foo"),
294 version: Some("1.2.3".to_semver().unwrap()),
295 url: Some(Url::parse("https://crates.io/foo").unwrap()),
296 },
297 );
298 ok(
299 "https://crates.io/foo#bar:1.2.3",
300 PackageIdSpec {
301 name: InternedString::new("bar"),
302 version: Some("1.2.3".to_semver().unwrap()),
303 url: Some(Url::parse("https://crates.io/foo").unwrap()),
304 },
305 );
306 ok(
307 "crates.io/foo",
308 PackageIdSpec {
309 name: InternedString::new("foo"),
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 {
317 name: InternedString::new("foo"),
318 version: Some("1.2.3".to_semver().unwrap()),
319 url: Some(Url::parse("cargo://crates.io/foo").unwrap()),
320 },
321 );
322 ok(
323 "crates.io/foo#bar",
324 PackageIdSpec {
325 name: InternedString::new("bar"),
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 {
333 name: InternedString::new("bar"),
334 version: Some("1.2.3".to_semver().unwrap()),
335 url: Some(Url::parse("cargo://crates.io/foo").unwrap()),
336 },
337 );
338 ok(
339 "foo",
340 PackageIdSpec {
341 name: InternedString::new("foo"),
342 version: None,
343 url: None,
344 },
345 );
346 ok(
347 "foo:1.2.3",
348 PackageIdSpec {
349 name: InternedString::new("foo"),
350 version: Some("1.2.3".to_semver().unwrap()),
351 url: None,
352 },
353 );
354 }
355
356 #[test]
357 fn bad_parsing() {
358 assert!(PackageIdSpec::parse("baz:").is_err());
359 assert!(PackageIdSpec::parse("baz:*").is_err());
360 assert!(PackageIdSpec::parse("baz:1.0").is_err());
361 assert!(PackageIdSpec::parse("https://baz:1.0").is_err());
362 assert!(PackageIdSpec::parse("https://#baz:1.0").is_err());
363 }
364
365 #[test]
366 fn matching() {
367 let url = Url::parse("https://example.com").unwrap();
368 let sid = SourceId::for_registry(&url).unwrap();
369 let foo = PackageId::new("foo", "1.2.3", sid).unwrap();
370 let bar = PackageId::new("bar", "1.2.3", sid).unwrap();
371
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));
376 }
377}