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#[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
34impl 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
43impl 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 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}