Skip to main content

cargo/core/
package_id_spec.rs

1use std::collections::HashMap;
2
3use anyhow::{Context as _, bail};
4
5use crate::core::PackageId;
6use crate::core::PackageIdSpec;
7use crate::util::edit_distance;
8use crate::util::errors::CargoResult;
9
10pub trait PackageIdSpecQuery {
11    /// Roughly equivalent to `PackageIdSpec::parse(spec)?.query(i)`
12    fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
13    where
14        I: IntoIterator<Item = PackageId>;
15
16    /// Checks whether the given `PackageId` matches the `PackageIdSpec`.
17    fn matches(&self, package_id: PackageId) -> bool;
18
19    /// Checks a list of `PackageId`s to find 1 that matches this `PackageIdSpec`. If 0, 2, or
20    /// more are found, then this returns an error.
21    fn query<I>(&self, i: I) -> CargoResult<PackageId>
22    where
23        I: IntoIterator<Item = PackageId>;
24}
25
26impl PackageIdSpecQuery for PackageIdSpec {
27    fn query_str<I>(spec: &str, i: I) -> CargoResult<PackageId>
28    where
29        I: IntoIterator<Item = PackageId>,
30    {
31        let i: Vec<_> = i.into_iter().collect();
32        let spec = PackageIdSpec::parse(spec).with_context(|| {
33            let suggestion =
34                edit_distance::closest_msg(spec, i.iter(), |id| id.name().as_str(), "package");
35            format!("invalid package ID specification: `{}`{}", spec, suggestion)
36        })?;
37        spec.query(i)
38    }
39
40    fn matches(&self, package_id: PackageId) -> bool {
41        if self.name() != package_id.name().as_str() {
42            return false;
43        }
44
45        if let Some(ref v) = self.partial_version() {
46            if !v.matches(package_id.version()) {
47                return false;
48            }
49        }
50
51        if let Some(u) = &self.url() {
52            if *u != package_id.source_id().url() {
53                return false;
54            }
55        }
56
57        if let Some(k) = &self.kind() {
58            if *k != package_id.source_id().kind() {
59                return false;
60            }
61        }
62
63        true
64    }
65
66    fn query<I>(&self, i: I) -> CargoResult<PackageId>
67    where
68        I: IntoIterator<Item = PackageId>,
69    {
70        let all_ids: Vec<_> = i.into_iter().collect();
71        let mut ids = all_ids.iter().copied().filter(|&id| self.matches(id));
72        let Some(ret) = ids.next() else {
73            let mut suggestion = String::new();
74            let try_spec = |spec: PackageIdSpec, suggestion: &mut String| {
75                let try_matches: Vec<_> = all_ids
76                    .iter()
77                    .copied()
78                    .filter(|&id| spec.matches(id))
79                    .collect();
80                if !try_matches.is_empty() {
81                    suggestion.push_str("\nhelp: there are similar package ID specifications:\n");
82                    minimize(suggestion, &try_matches, self);
83                }
84            };
85            if self.url().is_some() {
86                let spec = PackageIdSpec::new(self.name().to_owned());
87                let spec = if let Some(version) = self.partial_version().cloned() {
88                    spec.with_version(version)
89                } else {
90                    spec
91                };
92                try_spec(spec, &mut suggestion);
93            }
94            if suggestion.is_empty() && self.version().is_some() {
95                try_spec(PackageIdSpec::new(self.name().to_owned()), &mut suggestion);
96            }
97            if suggestion.is_empty() {
98                suggestion.push_str(&edit_distance::closest_msg(
99                    self.name(),
100                    all_ids.iter(),
101                    |id| id.name().as_str(),
102                    "package",
103                ));
104            }
105
106            bail!(
107                "package ID specification `{}` did not match any packages{}",
108                self,
109                suggestion
110            );
111        };
112        return match ids.next() {
113            Some(other) => {
114                let mut msg = format!(
115                    "specificationm `{self}` is ambiguous
116help: re-run this command with one of the following specifications",
117                );
118                let mut vec = vec![ret, other];
119                vec.extend(ids);
120                minimize(&mut msg, &vec, self);
121                Err(anyhow::format_err!("{}", msg))
122            }
123            None => Ok(ret),
124        };
125
126        fn minimize(msg: &mut String, ids: &[PackageId], spec: &PackageIdSpec) {
127            let mut version_cnt = HashMap::new();
128            for id in ids {
129                *version_cnt.entry(id.version()).or_insert(0) += 1;
130            }
131            for id in ids {
132                if version_cnt[id.version()] == 1 {
133                    msg.push_str(&format!("\n  {}@{}", spec.name(), id.version()));
134                } else {
135                    msg.push_str(&format!("\n  {}", id.to_spec()));
136                }
137            }
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::PackageIdSpec;
145    use super::PackageIdSpecQuery;
146    use crate::core::{PackageId, SourceId};
147    use url::Url;
148
149    #[test]
150    fn matching() {
151        let url = Url::parse("https://example.com").unwrap();
152        let sid = SourceId::for_registry(&url).unwrap();
153
154        let foo = PackageId::try_new("foo", "1.2.3", sid).unwrap();
155        assert!(PackageIdSpec::parse("foo").unwrap().matches(foo));
156        assert!(!PackageIdSpec::parse("bar").unwrap().matches(foo));
157        assert!(PackageIdSpec::parse("foo:1.2.3").unwrap().matches(foo));
158        assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo));
159        assert!(PackageIdSpec::parse("foo@1.2.3").unwrap().matches(foo));
160        assert!(!PackageIdSpec::parse("foo@1.2.2").unwrap().matches(foo));
161        assert!(PackageIdSpec::parse("foo@1.2").unwrap().matches(foo));
162        assert!(
163            PackageIdSpec::parse("https://example.com#foo@1.2")
164                .unwrap()
165                .matches(foo)
166        );
167        assert!(
168            !PackageIdSpec::parse("https://bob.com#foo@1.2")
169                .unwrap()
170                .matches(foo)
171        );
172        assert!(
173            PackageIdSpec::parse("registry+https://example.com#foo@1.2")
174                .unwrap()
175                .matches(foo)
176        );
177        assert!(
178            !PackageIdSpec::parse("git+https://example.com#foo@1.2")
179                .unwrap()
180                .matches(foo)
181        );
182
183        let meta = PackageId::try_new("meta", "1.2.3+hello", sid).unwrap();
184        assert!(PackageIdSpec::parse("meta").unwrap().matches(meta));
185        assert!(PackageIdSpec::parse("meta@1").unwrap().matches(meta));
186        assert!(PackageIdSpec::parse("meta@1.2").unwrap().matches(meta));
187        assert!(PackageIdSpec::parse("meta@1.2.3").unwrap().matches(meta));
188        assert!(
189            !PackageIdSpec::parse("meta@1.2.3-alpha.0")
190                .unwrap()
191                .matches(meta)
192        );
193        assert!(
194            PackageIdSpec::parse("meta@1.2.3+hello")
195                .unwrap()
196                .matches(meta)
197        );
198        assert!(
199            !PackageIdSpec::parse("meta@1.2.3+bye")
200                .unwrap()
201                .matches(meta)
202        );
203
204        let pre = PackageId::try_new("pre", "1.2.3-alpha.0", sid).unwrap();
205        assert!(PackageIdSpec::parse("pre").unwrap().matches(pre));
206        assert!(!PackageIdSpec::parse("pre@1").unwrap().matches(pre));
207        assert!(!PackageIdSpec::parse("pre@1.2").unwrap().matches(pre));
208        assert!(!PackageIdSpec::parse("pre@1.2.3").unwrap().matches(pre));
209        assert!(
210            PackageIdSpec::parse("pre@1.2.3-alpha.0")
211                .unwrap()
212                .matches(pre)
213        );
214        assert!(
215            !PackageIdSpec::parse("pre@1.2.3-alpha.1")
216                .unwrap()
217                .matches(pre)
218        );
219        assert!(
220            !PackageIdSpec::parse("pre@1.2.3-beta.0")
221                .unwrap()
222                .matches(pre)
223        );
224        assert!(
225            !PackageIdSpec::parse("pre@1.2.3+hello")
226                .unwrap()
227                .matches(pre)
228        );
229        assert!(
230            !PackageIdSpec::parse("pre@1.2.3-alpha.0+hello")
231                .unwrap()
232                .matches(pre)
233        );
234    }
235}