1use crate::error::{Error, Result};
11use serde::{Deserialize, Serialize, de, ser};
12use std::{
13 cmp::{Ord, Ordering},
14 fmt,
15 hash::Hash,
16 str::FromStr,
17};
18use url::Url;
19
20#[cfg(any(unix, windows))]
21use std::path::Path;
22
23pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
25pub const CRATES_IO_SPARSE_INDEX: &str = "sparse+https://index.crates.io/";
27
28#[derive(Clone, Debug)]
30pub struct SourceId {
31 url: Url,
33
34 kind: SourceKind,
36
37 precise: Option<String>,
39
40 name: Option<String>,
42}
43
44impl SourceId {
45 fn new(kind: SourceKind, url: Url) -> Result<Self> {
47 Ok(Self {
48 kind,
49 url,
50 precise: None,
51 name: None,
52 })
53 }
54
55 pub fn from_url(string: &str) -> Result<Self> {
66 let mut parts = string.splitn(2, '+');
67 let kind = parts.next().unwrap();
68 let url = parts
69 .next()
70 .ok_or_else(|| Error::Parse(format!("invalid source `{string}`")))?;
71
72 match kind {
73 "git" => {
74 let mut url = url.into_url()?;
75 let mut reference = GitReference::DefaultBranch;
76 for (k, v) in url.query_pairs() {
77 match &k[..] {
78 "branch" | "ref" => reference = GitReference::Branch(v.into_owned()),
80
81 "rev" => reference = GitReference::Rev(v.into_owned()),
82 "tag" => reference = GitReference::Tag(v.into_owned()),
83 _ => {}
84 }
85 }
86 let precise = url.fragment().map(|s| s.to_owned());
87 url.set_fragment(None);
88 url.set_query(None);
89 Ok(Self::for_git(&url, reference)?.with_precise(precise))
90 }
91 "registry" => {
92 let url = url.into_url()?;
93 Ok(SourceId::new(SourceKind::Registry, url)?
94 .with_precise(Some("locked".to_string())))
95 }
96 "sparse" => {
97 let url = url.into_url()?;
98 Ok(SourceId::new(SourceKind::SparseRegistry, url)?
99 .with_precise(Some("locked".to_string())))
100 }
101 "path" => Self::new(SourceKind::Path, url.into_url()?),
102 kind => Err(Error::Parse(format!(
103 "unsupported source protocol: `{kind}` from `{string}`"
104 ))),
105 }
106 }
107
108 #[cfg(any(unix, windows))]
112 pub fn for_path(path: &Path) -> Result<Self> {
113 Self::new(SourceKind::Path, path.into_url()?)
114 }
115
116 pub fn for_git(url: &Url, reference: GitReference) -> Result<Self> {
118 Self::new(SourceKind::Git(reference), url.clone())
119 }
120
121 pub fn for_registry(url: &Url) -> Result<Self> {
123 Self::new(SourceKind::Registry, url.clone())
124 }
125
126 #[cfg(any(unix, windows))]
128 pub fn for_local_registry(path: &Path) -> Result<Self> {
129 Self::new(SourceKind::LocalRegistry, path.into_url()?)
130 }
131
132 #[cfg(any(unix, windows))]
134 pub fn for_directory(path: &Path) -> Result<Self> {
135 Self::new(SourceKind::Directory, path.into_url()?)
136 }
137
138 pub fn url(&self) -> &Url {
140 &self.url
141 }
142
143 pub fn kind(&self) -> &SourceKind {
145 &self.kind
146 }
147
148 pub fn display_index(&self) -> String {
150 if self.is_default_registry() {
151 "crates.io index".to_string()
152 } else {
153 format!("`{}` index", self.url())
154 }
155 }
156
157 pub fn display_registry_name(&self) -> String {
159 if self.is_default_registry() {
160 "crates.io".to_string()
161 } else if let Some(name) = &self.name {
162 name.clone()
163 } else {
164 self.url().to_string()
165 }
166 }
167
168 pub fn is_path(&self) -> bool {
170 self.kind == SourceKind::Path
171 }
172
173 pub fn is_registry(&self) -> bool {
175 matches!(
176 self.kind,
177 SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
178 )
179 }
180
181 pub fn is_remote_registry(&self) -> bool {
186 matches!(self.kind, SourceKind::Registry | SourceKind::SparseRegistry)
187 }
188
189 pub fn is_git(&self) -> bool {
191 matches!(self.kind, SourceKind::Git(_))
192 }
193
194 pub fn precise(&self) -> Option<&str> {
196 self.precise.as_ref().map(AsRef::as_ref)
197 }
198
199 pub fn git_reference(&self) -> Option<&GitReference> {
201 if let SourceKind::Git(s) = &self.kind {
202 Some(s)
203 } else {
204 None
205 }
206 }
207
208 pub fn with_precise(&self, v: Option<String>) -> Self {
210 Self {
211 precise: v,
212 ..self.clone()
213 }
214 }
215
216 pub fn is_default_registry(&self) -> bool {
218 self.kind == SourceKind::Registry && self.url.as_str() == CRATES_IO_INDEX
219 || self.kind == SourceKind::SparseRegistry
220 && self.url.as_str() == &CRATES_IO_SPARSE_INDEX[7..]
221 }
222
223 pub(crate) fn as_url(&self, encoded: bool) -> SourceIdAsUrl<'_> {
225 SourceIdAsUrl { id: self, encoded }
226 }
227}
228
229impl Ord for SourceId {
241 fn cmp(&self, other: &Self) -> Ordering {
242 match self.url.cmp(&other.url) {
243 Ordering::Equal => {}
244 non_eq => return non_eq,
245 }
246
247 match self.name.cmp(&other.name) {
248 Ordering::Equal => {}
249 non_eq => return non_eq,
250 }
251
252 match (&self.kind, &other.kind) {
254 (SourceKind::Git(s), SourceKind::Git(o)) => (s, o),
255 (a, b) => return a.cmp(b),
256 };
257
258 if let (Some(s), Some(o)) = (&self.precise, &other.precise) {
259 return s.cmp(o);
261 }
262
263 Ordering::Equal
264 }
265}
266
267impl PartialOrd for SourceId {
268 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
269 Some(self.cmp(other))
270 }
271}
272
273impl Hash for SourceId {
274 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
275 self.url.hash(state);
276 self.kind.hash(state);
277 self.precise.hash(state);
278 self.name.hash(state);
279 }
280}
281
282impl PartialEq for SourceId {
283 fn eq(&self, other: &Self) -> bool {
284 self.cmp(other) == Ordering::Equal
285 }
286}
287
288impl Eq for SourceId {}
289
290impl Serialize for SourceId {
291 fn serialize<S: ser::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
292 if self.is_path() {
293 None::<String>.serialize(s)
294 } else {
295 s.collect_str(&self.to_string())
296 }
297 }
298}
299
300impl<'de> Deserialize<'de> for SourceId {
301 fn deserialize<D: de::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
302 let string = String::deserialize(d)?;
303 SourceId::from_url(&string).map_err(de::Error::custom)
304 }
305}
306
307impl FromStr for SourceId {
308 type Err = Error;
309
310 fn from_str(s: &str) -> Result<Self> {
311 Self::from_url(s)
312 }
313}
314
315impl fmt::Display for SourceId {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 self.as_url(false).fmt(f)
318 }
319}
320
321impl Default for SourceId {
322 fn default() -> SourceId {
323 SourceId::for_registry(&CRATES_IO_INDEX.into_url().unwrap()).unwrap()
324 }
325}
326
327#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
329#[non_exhaustive]
330pub enum SourceKind {
331 Git(GitReference),
333
334 Path,
336
337 Registry,
339
340 SparseRegistry,
342
343 LocalRegistry,
345
346 #[cfg(any(unix, windows))]
348 Directory,
349}
350
351pub(crate) struct SourceIdAsUrl<'a> {
353 id: &'a SourceId,
354 encoded: bool,
355}
356
357impl fmt::Display for SourceIdAsUrl<'_> {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 match &self.id {
360 SourceId {
361 kind: SourceKind::Path,
362 url,
363 ..
364 } => write!(f, "path+{url}"),
365 SourceId {
366 kind: SourceKind::Git(reference),
367 url,
368 precise,
369 ..
370 } => {
371 write!(f, "git+{url}")?;
372 if let Some(pretty) = reference.pretty_ref(self.encoded) {
374 write!(f, "?{pretty}")?;
375 }
376 if let Some(precise) = precise.as_ref() {
377 write!(f, "#{precise}")?;
378 }
379 Ok(())
380 }
381 SourceId {
382 kind: SourceKind::Registry,
383 url,
384 ..
385 } => write!(f, "registry+{url}"),
386 SourceId {
387 kind: SourceKind::SparseRegistry,
388 url,
389 ..
390 } => write!(f, "sparse+{url}"),
391 SourceId {
392 kind: SourceKind::LocalRegistry,
393 url,
394 ..
395 } => write!(f, "local-registry+{url}"),
396 #[cfg(any(unix, windows))]
397 SourceId {
398 kind: SourceKind::Directory,
399 url,
400 ..
401 } => write!(f, "directory+{url}"),
402 }
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
408pub enum GitReference {
409 DefaultBranch,
411
412 Tag(String),
414
415 Branch(String),
417
418 Rev(String),
420}
421
422impl GitReference {
423 pub fn pretty_ref(&self, url_encoded: bool) -> Option<PrettyRef<'_>> {
426 match self {
427 Self::DefaultBranch => None,
428 _ => Some(PrettyRef {
429 inner: self,
430 url_encoded,
431 }),
432 }
433 }
434}
435
436pub struct PrettyRef<'a> {
438 inner: &'a GitReference,
439 url_encoded: bool,
440}
441
442impl fmt::Display for PrettyRef<'_> {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 let value: &str = match self.inner {
445 GitReference::DefaultBranch => return Ok(()),
446 GitReference::Branch(s) => {
447 write!(f, "branch=")?;
448 s
449 }
450 GitReference::Tag(s) => {
451 write!(f, "tag=")?;
452 s
453 }
454 GitReference::Rev(s) => {
455 write!(f, "rev=")?;
456 s
457 }
458 };
459 if self.url_encoded {
460 for value in url::form_urlencoded::byte_serialize(value.as_bytes()) {
461 write!(f, "{value}")?;
462 }
463 } else {
464 write!(f, "{value}")?;
465 }
466 Ok(())
467 }
468}
469
470trait IntoUrl {
472 fn into_url(self) -> Result<Url>;
474}
475
476impl IntoUrl for &str {
477 fn into_url(self) -> Result<Url> {
478 Url::parse(self).map_err(|s| Error::Parse(format!("invalid url `{self}`: {s}")))
479 }
480}
481
482#[cfg(any(unix, windows))]
483impl IntoUrl for &Path {
484 fn into_url(self) -> Result<Url> {
485 Url::from_file_path(self)
486 .map_err(|_| Error::Parse(format!("invalid path url `{}`", self.display())))
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::SourceId;
493
494 #[test]
495 fn identifies_crates_io() {
496 assert!(SourceId::default().is_default_registry());
497 assert!(
498 SourceId::from_url(super::CRATES_IO_SPARSE_INDEX)
499 .expect("failed to parse sparse URL")
500 .is_default_registry()
501 );
502 }
503}