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;
8use std::sync::OnceLock;
9
10use serde::de;
11use serde::ser;
12
13use crate::core::PackageIdSpec;
14use crate::core::SourceId;
15use crate::util::interning::InternedString;
16use crate::util::CargoResult;
17
18static PACKAGE_ID_CACHE: OnceLock<Mutex<HashSet<&'static PackageIdInner>>> = OnceLock::new();
19
20#[derive(Clone, Copy, Eq, PartialOrd, Ord)]
22pub struct PackageId {
23 inner: &'static PackageIdInner,
24}
25
26#[derive(PartialOrd, Eq, Ord)]
27struct PackageIdInner {
28 name: InternedString,
29 version: semver::Version,
30 source_id: SourceId,
31}
32
33impl PartialEq for PackageIdInner {
41 fn eq(&self, other: &Self) -> bool {
42 self.name == other.name
43 && self.version == other.version
44 && self.source_id.full_eq(other.source_id)
45 }
46}
47
48impl Hash for PackageIdInner {
50 fn hash<S: hash::Hasher>(&self, into: &mut S) {
51 self.name.hash(into);
52 self.version.hash(into);
53 self.source_id.full_hash(into);
54 }
55}
56
57impl ser::Serialize for PackageId {
58 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
59 where
60 S: ser::Serializer,
61 {
62 s.collect_str(&format_args!(
63 "{} {} ({})",
64 self.inner.name,
65 self.inner.version,
66 self.inner.source_id.as_url()
67 ))
68 }
69}
70
71impl<'de> de::Deserialize<'de> for PackageId {
72 fn deserialize<D>(d: D) -> Result<PackageId, D::Error>
73 where
74 D: de::Deserializer<'de>,
75 {
76 let string = String::deserialize(d)?;
77
78 let (field, rest) = string
79 .split_once(' ')
80 .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
81 let name = InternedString::new(field);
82
83 let (field, rest) = rest
84 .split_once(' ')
85 .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
86 let version = field.parse().map_err(de::Error::custom)?;
87
88 let url =
89 strip_parens(rest).ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
90 let source_id = SourceId::from_url(url).map_err(de::Error::custom)?;
91
92 Ok(PackageId::new(name, version, source_id))
93 }
94}
95
96fn strip_parens(value: &str) -> Option<&str> {
97 let value = value.strip_prefix('(')?;
98 let value = value.strip_suffix(')')?;
99 Some(value)
100}
101
102impl PartialEq for PackageId {
103 fn eq(&self, other: &PackageId) -> bool {
104 if ptr::eq(self.inner, other.inner) {
105 return true;
106 }
107 self.inner.name == other.inner.name
110 && self.inner.version == other.inner.version
111 && self.inner.source_id == other.inner.source_id
112 }
113}
114
115impl Hash for PackageId {
116 fn hash<S: hash::Hasher>(&self, state: &mut S) {
117 self.inner.name.hash(state);
121 self.inner.version.hash(state);
122 self.inner.source_id.hash(state);
123 }
124}
125
126impl PackageId {
127 pub fn try_new(
128 name: impl Into<InternedString>,
129 version: &str,
130 sid: SourceId,
131 ) -> CargoResult<PackageId> {
132 let v = version.parse()?;
133 Ok(PackageId::new(name.into(), v, sid))
134 }
135
136 pub fn new(name: InternedString, version: semver::Version, source_id: SourceId) -> PackageId {
137 let inner = PackageIdInner {
138 name,
139 version,
140 source_id,
141 };
142 let mut cache = PACKAGE_ID_CACHE
143 .get_or_init(|| Default::default())
144 .lock()
145 .unwrap();
146 let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
147 let inner = Box::leak(Box::new(inner));
148 cache.insert(inner);
149 inner
150 });
151 PackageId { inner }
152 }
153
154 pub fn name(self) -> InternedString {
155 self.inner.name
156 }
157 pub fn version(self) -> &'static semver::Version {
158 &self.inner.version
159 }
160 pub fn source_id(self) -> SourceId {
161 self.inner.source_id
162 }
163
164 pub fn with_source_id(self, source: SourceId) -> PackageId {
165 PackageId::new(self.inner.name, self.inner.version.clone(), source)
166 }
167
168 pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Self {
169 if self.source_id() == to_replace {
170 self.with_source_id(replace_with)
171 } else {
172 self
173 }
174 }
175
176 pub fn stable_hash(self, workspace: &Path) -> PackageIdStableHash<'_> {
183 PackageIdStableHash(self, workspace)
184 }
185
186 pub fn tarball_name(&self) -> String {
188 format!("{}-{}.crate", self.name(), self.version())
189 }
190
191 pub fn to_spec(&self) -> PackageIdSpec {
194 PackageIdSpec::new(String::from(self.name().as_str()))
195 .with_version(self.version().clone().into())
196 .with_url(self.source_id().url().clone())
197 .with_kind(self.source_id().kind().clone())
198 }
199}
200
201pub struct PackageIdStableHash<'a>(PackageId, &'a Path);
202
203impl<'a> Hash for PackageIdStableHash<'a> {
204 fn hash<S: hash::Hasher>(&self, state: &mut S) {
205 self.0.inner.name.hash(state);
206 self.0.inner.version.hash(state);
207 self.0.inner.source_id.stable_hash(self.1, state);
208 }
209}
210
211impl fmt::Display for PackageId {
212 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
213 write!(f, "{} v{}", self.inner.name, self.inner.version)?;
214
215 if !self.inner.source_id.is_crates_io() {
216 write!(f, " ({})", self.inner.source_id)?;
217 }
218
219 Ok(())
220 }
221}
222
223impl fmt::Debug for PackageId {
224 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
225 f.debug_struct("PackageId")
226 .field("name", &self.inner.name)
227 .field("version", &self.inner.version.to_string())
228 .field("source", &self.inner.source_id.to_string())
229 .finish()
230 }
231}
232
233impl fmt::Debug for PackageIdInner {
234 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
235 f.debug_struct("PackageIdInner")
236 .field("name", &self.name)
237 .field("version", &self.version.to_string())
238 .field("source", &self.source_id.to_string())
239 .finish()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::PackageId;
246 use crate::core::SourceId;
247 use crate::sources::CRATES_IO_INDEX;
248 use crate::util::IntoUrl;
249
250 #[test]
251 fn invalid_version_handled_nicely() {
252 let loc = CRATES_IO_INDEX.into_url().unwrap();
253 let repo = SourceId::for_registry(&loc).unwrap();
254
255 assert!(PackageId::try_new("foo", "1.0", repo).is_err());
256 assert!(PackageId::try_new("foo", "1", repo).is_err());
257 assert!(PackageId::try_new("foo", "bar", repo).is_err());
258 assert!(PackageId::try_new("foo", "", repo).is_err());
259 }
260
261 #[test]
262 fn display() {
263 let loc = CRATES_IO_INDEX.into_url().unwrap();
264 let pkg_id =
265 PackageId::try_new("foo", "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap();
266 assert_eq!("foo v1.0.0", pkg_id.to_string());
267 }
268
269 #[test]
270 fn unequal_build_metadata() {
271 let loc = CRATES_IO_INDEX.into_url().unwrap();
272 let repo = SourceId::for_registry(&loc).unwrap();
273 let first = PackageId::try_new("foo", "0.0.1+first", repo).unwrap();
274 let second = PackageId::try_new("foo", "0.0.1+second", repo).unwrap();
275 assert_ne!(first, second);
276 assert_ne!(first.inner, second.inner);
277 }
278}