Skip to main content

cargo/core/
package_id.rs

1use std::collections::HashSet;
2use std::fmt::{self, Formatter};
3use std::hash;
4use std::hash::Hash;
5use std::path::Path;
6use std::ptr;
7use std::sync::Mutex;
8
9use serde::de;
10use serde::ser;
11
12use crate::core::interning::InternedString;
13use crate::core::source::SourceId;
14use crate::util::{CargoResult, ToSemver};
15
16lazy_static::lazy_static! {
17    static ref PACKAGE_ID_CACHE: Mutex<HashSet<&'static PackageIdInner>> =
18        Mutex::new(HashSet::new());
19}
20
21/// Identifier for a specific version of a package in a specific source.
22#[derive(Clone, Copy, Eq, PartialOrd, Ord)]
23pub struct PackageId {
24    inner: &'static PackageIdInner,
25}
26
27#[derive(PartialOrd, Eq, Ord)]
28struct PackageIdInner {
29    name: InternedString,
30    version: semver::Version,
31    source_id: SourceId,
32}
33
34// Custom equality that uses full equality of SourceId, rather than its custom equality.
35impl PartialEq for PackageIdInner {
36    fn eq(&self, other: &Self) -> bool {
37        self.name == other.name
38            && self.version == other.version
39            && self.source_id.full_eq(other.source_id)
40    }
41}
42
43// Custom hash that is coherent with the custom equality above.
44impl Hash for PackageIdInner {
45    fn hash<S: hash::Hasher>(&self, into: &mut S) {
46        self.name.hash(into);
47        self.version.hash(into);
48        self.source_id.full_hash(into);
49    }
50}
51
52impl ser::Serialize for PackageId {
53    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
54    where
55        S: ser::Serializer,
56    {
57        s.collect_str(&format_args!(
58            "{} {} ({})",
59            self.inner.name,
60            self.inner.version,
61            self.inner.source_id.into_url()
62        ))
63    }
64}
65
66impl<'de> de::Deserialize<'de> for PackageId {
67    fn deserialize<D>(d: D) -> Result<PackageId, D::Error>
68    where
69        D: de::Deserializer<'de>,
70    {
71        let string = String::deserialize(d)?;
72        let mut s = string.splitn(3, ' ');
73        let name = s.next().unwrap();
74        let name = InternedString::new(name);
75        let version = match s.next() {
76            Some(s) => s,
77            None => return Err(de::Error::custom("invalid serialized PackageId")),
78        };
79        let version = version.to_semver().map_err(de::Error::custom)?;
80        let url = match s.next() {
81            Some(s) => s,
82            None => return Err(de::Error::custom("invalid serialized PackageId")),
83        };
84        let url = if url.starts_with('(') && url.ends_with(')') {
85            &url[1..url.len() - 1]
86        } else {
87            return Err(de::Error::custom("invalid serialized PackageId"));
88        };
89        let source_id = SourceId::from_url(url).map_err(de::Error::custom)?;
90
91        Ok(PackageId::pure(name, version, source_id))
92    }
93}
94
95impl PartialEq for PackageId {
96    fn eq(&self, other: &PackageId) -> bool {
97        if ptr::eq(self.inner, other.inner) {
98            return true;
99        }
100        self.inner.name == other.inner.name
101            && self.inner.version == other.inner.version
102            && self.inner.source_id == other.inner.source_id
103    }
104}
105
106impl Hash for PackageId {
107    fn hash<S: hash::Hasher>(&self, state: &mut S) {
108        self.inner.name.hash(state);
109        self.inner.version.hash(state);
110        self.inner.source_id.hash(state);
111    }
112}
113
114impl PackageId {
115    pub fn new<T: ToSemver>(
116        name: impl Into<InternedString>,
117        version: T,
118        sid: SourceId,
119    ) -> CargoResult<PackageId> {
120        let v = version.to_semver()?;
121        Ok(PackageId::pure(name.into(), v, sid))
122    }
123
124    pub fn pure(name: InternedString, version: semver::Version, source_id: SourceId) -> PackageId {
125        let inner = PackageIdInner {
126            name,
127            version,
128            source_id,
129        };
130        let mut cache = PACKAGE_ID_CACHE.lock().unwrap();
131        let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
132            let inner = Box::leak(Box::new(inner));
133            cache.insert(inner);
134            inner
135        });
136        PackageId { inner }
137    }
138
139    pub fn name(self) -> InternedString {
140        self.inner.name
141    }
142    pub fn version(self) -> &'static semver::Version {
143        &self.inner.version
144    }
145    pub fn source_id(self) -> SourceId {
146        self.inner.source_id
147    }
148
149    pub fn with_precise(self, precise: Option<String>) -> PackageId {
150        PackageId::pure(
151            self.inner.name,
152            self.inner.version.clone(),
153            self.inner.source_id.with_precise(precise),
154        )
155    }
156
157    pub fn with_source_id(self, source: SourceId) -> PackageId {
158        PackageId::pure(self.inner.name, self.inner.version.clone(), source)
159    }
160
161    pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Self {
162        if self.source_id() == to_replace {
163            self.with_source_id(replace_with)
164        } else {
165            self
166        }
167    }
168
169    pub fn stable_hash(self, workspace: &Path) -> PackageIdStableHash<'_> {
170        PackageIdStableHash(self, workspace)
171    }
172}
173
174pub struct PackageIdStableHash<'a>(PackageId, &'a Path);
175
176impl<'a> Hash for PackageIdStableHash<'a> {
177    fn hash<S: hash::Hasher>(&self, state: &mut S) {
178        self.0.inner.name.hash(state);
179        self.0.inner.version.hash(state);
180        self.0.inner.source_id.stable_hash(self.1, state);
181    }
182}
183
184impl fmt::Display for PackageId {
185    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
186        write!(f, "{} v{}", self.inner.name, self.inner.version)?;
187
188        if !self.inner.source_id.is_default_registry() {
189            write!(f, " ({})", self.inner.source_id)?;
190        }
191
192        Ok(())
193    }
194}
195
196impl fmt::Debug for PackageId {
197    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
198        f.debug_struct("PackageId")
199            .field("name", &self.inner.name)
200            .field("version", &self.inner.version.to_string())
201            .field("source", &self.inner.source_id.to_string())
202            .finish()
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::PackageId;
209    use crate::core::source::SourceId;
210    use crate::sources::CRATES_IO_INDEX;
211    use crate::util::IntoUrl;
212
213    #[test]
214    fn invalid_version_handled_nicely() {
215        let loc = CRATES_IO_INDEX.into_url().unwrap();
216        let repo = SourceId::for_registry(&loc).unwrap();
217
218        assert!(PackageId::new("foo", "1.0", repo).is_err());
219        assert!(PackageId::new("foo", "1", repo).is_err());
220        assert!(PackageId::new("foo", "bar", repo).is_err());
221        assert!(PackageId::new("foo", "", repo).is_err());
222    }
223
224    #[test]
225    fn debug() {
226        let loc = CRATES_IO_INDEX.into_url().unwrap();
227        let pkg_id = PackageId::new("foo", "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap();
228        assert_eq!(
229            r#"PackageId { name: "foo", version: "1.0.0", source: "registry `https://github.com/rust-lang/crates.io-index`" }"#,
230            format!("{:?}", pkg_id)
231        );
232
233        let expected = r#"
234PackageId {
235    name: "foo",
236    version: "1.0.0",
237    source: "registry `https://github.com/rust-lang/crates.io-index`",
238}
239"#
240        .trim();
241
242        // Can be removed once trailing commas in Debug have reached the stable
243        // channel.
244        let expected_without_trailing_comma = r#"
245PackageId {
246    name: "foo",
247    version: "1.0.0",
248    source: "registry `https://github.com/rust-lang/crates.io-index`"
249}
250"#
251        .trim();
252
253        let actual = format!("{:#?}", pkg_id);
254        if actual.ends_with(",\n}") {
255            assert_eq!(actual, expected);
256        } else {
257            assert_eq!(actual, expected_without_trailing_comma);
258        }
259    }
260
261    #[test]
262    fn display() {
263        let loc = CRATES_IO_INDEX.into_url().unwrap();
264        let pkg_id = PackageId::new("foo", "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap();
265        assert_eq!("foo v1.0.0", pkg_id.to_string());
266    }
267}