Skip to main content

cargo/core/
package_id_spec.rs

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/// Some or all of the data required to identify a package:
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///
19/// If any of the optional fields are omitted, then the package ID may be ambiguous, there may be
20/// more than one package/version/url combo that will match. However, often just the name is
21/// sufficient to uniquely define a package ID.
22#[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    /// 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![
39    ///     "https://crates.io/foo#1.2.3",
40    ///     "https://crates.io/foo#bar:1.2.3",
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    /// }
51    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    /// Roughly equivalent to `PackageIdSpec::parse(spec)?.query(i)`
77    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    /// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `Version` and `Url`
87    /// fields filled in.
88    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    /// Tries to convert a valid `Url` to a `PackageIdSpec`.
97    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    /// Checks whether the given `PackageId` matches the `PackageIdSpec`.
160    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    /// 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.
179    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}