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
23const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
25const 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(Self::new(SourceKind::Registry, url)?.with_precise(Some("locked".to_string())))
94 }
95 "sparse" => {
96 let url = url.into_url()?;
97 Ok(Self::new(SourceKind::SparseRegistry, url)?
98 .with_precise(Some("locked".to_string())))
99 }
100 "path" => Self::new(SourceKind::Path, url.into_url()?),
101 kind => Err(Error::Parse(format!(
102 "unsupported source protocol: `{kind}` from `{string}`"
103 ))),
104 }
105 }
106
107 #[cfg(any(unix, windows))]
111 pub fn for_path(path: &Path) -> Result<Self> {
112 Self::new(SourceKind::Path, path.into_url()?)
113 }
114
115 pub fn for_git(url: &Url, reference: GitReference) -> Result<Self> {
117 Self::new(SourceKind::Git(reference), url.clone())
118 }
119
120 pub fn for_registry(url: &Url) -> Result<Self> {
122 Self::new(SourceKind::Registry, url.clone())
123 }
124
125 #[cfg(any(unix, windows))]
127 pub fn for_local_registry(path: &Path) -> Result<Self> {
128 Self::new(SourceKind::LocalRegistry, path.into_url()?)
129 }
130
131 #[cfg(any(unix, windows))]
133 pub fn for_directory(path: &Path) -> Result<Self> {
134 Self::new(SourceKind::Directory, path.into_url()?)
135 }
136
137 pub fn url(&self) -> &Url {
139 &self.url
140 }
141
142 pub fn kind(&self) -> &SourceKind {
144 &self.kind
145 }
146
147 pub fn display_index(&self) -> String {
149 if self.is_default_registry() {
150 "crates.io index".to_string()
151 } else {
152 format!("`{}` index", self.url())
153 }
154 }
155
156 pub fn display_registry_name(&self) -> String {
158 if self.is_default_registry() {
159 "crates.io".to_string()
160 } else if let Some(name) = &self.name {
161 name.clone()
162 } else {
163 self.url().to_string()
164 }
165 }
166
167 pub fn is_path(&self) -> bool {
169 self.kind == SourceKind::Path
170 }
171
172 pub fn is_registry(&self) -> bool {
174 matches!(
175 self.kind,
176 SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
177 )
178 }
179
180 pub fn is_remote_registry(&self) -> bool {
185 matches!(self.kind, SourceKind::Registry | SourceKind::SparseRegistry)
186 }
187
188 pub fn is_git(&self) -> bool {
190 matches!(self.kind, SourceKind::Git(_))
191 }
192
193 pub fn precise(&self) -> Option<&str> {
195 self.precise.as_ref().map(AsRef::as_ref)
196 }
197
198 pub fn git_reference(&self) -> Option<&GitReference> {
200 if let SourceKind::Git(s) = &self.kind {
201 Some(s)
202 } else {
203 None
204 }
205 }
206
207 pub fn with_precise(&self, v: Option<String>) -> Self {
209 Self {
210 precise: v,
211 ..self.clone()
212 }
213 }
214
215 pub fn is_default_registry(&self) -> bool {
217 self.kind == SourceKind::Registry && self.url.as_str() == CRATES_IO_INDEX
218 || self.kind == SourceKind::SparseRegistry
219 && self.url.as_str() == &CRATES_IO_SPARSE_INDEX[7..]
220 }
221
222 pub(crate) fn as_url(&self, encoded: bool) -> SourceIdAsUrl<'_> {
224 SourceIdAsUrl { id: self, encoded }
225 }
226}
227
228impl Ord for SourceId {
240 fn cmp(&self, other: &Self) -> Ordering {
241 match self.url.cmp(&other.url) {
242 Ordering::Equal => {}
243 non_eq => return non_eq,
244 }
245
246 match self.name.cmp(&other.name) {
247 Ordering::Equal => {}
248 non_eq => return non_eq,
249 }
250
251 match (&self.kind, &other.kind) {
253 (SourceKind::Git(s), SourceKind::Git(o)) => (s, o),
254 (a, b) => return a.cmp(b),
255 };
256
257 if let (Some(s), Some(o)) = (&self.precise, &other.precise) {
258 return s.cmp(o);
260 }
261
262 Ordering::Equal
263 }
264}
265
266impl PartialOrd for SourceId {
267 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
268 Some(self.cmp(other))
269 }
270}
271
272impl Hash for SourceId {
273 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
274 self.url.hash(state);
275 self.kind.hash(state);
276 self.precise.hash(state);
277 self.name.hash(state);
278 }
279}
280
281impl PartialEq for SourceId {
282 fn eq(&self, other: &Self) -> bool {
283 self.cmp(other) == Ordering::Equal
284 }
285}
286
287impl Eq for SourceId {}
288
289impl Serialize for SourceId {
290 fn serialize<S: ser::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
291 if self.is_path() {
292 None::<String>.serialize(s)
293 } else {
294 s.collect_str(&self.to_string())
295 }
296 }
297}
298
299impl<'de> Deserialize<'de> for SourceId {
300 fn deserialize<D: de::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
301 let string = String::deserialize(d)?;
302 Self::from_url(&string).map_err(de::Error::custom)
303 }
304}
305
306impl FromStr for SourceId {
307 type Err = Error;
308
309 fn from_str(s: &str) -> Result<Self> {
310 Self::from_url(s)
311 }
312}
313
314impl fmt::Display for SourceId {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 self.as_url(false).fmt(f)
317 }
318}
319
320impl Default for SourceId {
321 fn default() -> Self {
322 Self::for_registry(&CRATES_IO_INDEX.into_url().unwrap()).unwrap()
323 }
324}
325
326#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
328#[non_exhaustive]
329pub enum SourceKind {
330 Git(GitReference),
332
333 Path,
335
336 Registry,
338
339 SparseRegistry,
341
342 LocalRegistry,
344
345 #[cfg(any(unix, windows))]
347 Directory,
348}
349
350pub(crate) struct SourceIdAsUrl<'a> {
352 id: &'a SourceId,
353 encoded: bool,
354}
355
356impl fmt::Display for SourceIdAsUrl<'_> {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 match &self.id {
359 SourceId {
360 kind: SourceKind::Path,
361 url,
362 ..
363 } => write!(f, "path+{url}"),
364 SourceId {
365 kind: SourceKind::Git(reference),
366 url,
367 precise,
368 ..
369 } => {
370 write!(f, "git+{url}")?;
371 if let Some(pretty) = reference.pretty_ref(self.encoded) {
373 write!(f, "?{pretty}")?;
374 }
375 if let Some(precise) = precise.as_ref() {
376 write!(f, "#{precise}")?;
377 }
378 Ok(())
379 }
380 SourceId {
381 kind: SourceKind::Registry,
382 url,
383 ..
384 } => write!(f, "registry+{url}"),
385 SourceId {
386 kind: SourceKind::SparseRegistry,
387 url,
388 ..
389 } => write!(f, "sparse+{url}"),
390 SourceId {
391 kind: SourceKind::LocalRegistry,
392 url,
393 ..
394 } => write!(f, "local-registry+{url}"),
395 #[cfg(any(unix, windows))]
396 SourceId {
397 kind: SourceKind::Directory,
398 url,
399 ..
400 } => write!(f, "directory+{url}"),
401 }
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
407pub enum GitReference {
408 DefaultBranch,
410
411 Tag(String),
413
414 Branch(String),
416
417 Rev(String),
419}
420
421impl GitReference {
422 pub fn pretty_ref(&self, url_encoded: bool) -> Option<impl fmt::Display + '_> {
425 match self {
426 Self::DefaultBranch => None,
427 _ => Some(PrettyRef {
428 inner: self,
429 url_encoded,
430 }),
431 }
432 }
433}
434
435struct PrettyRef<'a> {
437 inner: &'a GitReference,
438 url_encoded: bool,
439}
440
441impl fmt::Display for PrettyRef<'_> {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 let value: &str = match self.inner {
444 GitReference::DefaultBranch => return Ok(()),
445 GitReference::Branch(s) => {
446 write!(f, "branch=")?;
447 s
448 }
449 GitReference::Tag(s) => {
450 write!(f, "tag=")?;
451 s
452 }
453 GitReference::Rev(s) => {
454 write!(f, "rev=")?;
455 s
456 }
457 };
458 if self.url_encoded {
459 for value in url::form_urlencoded::byte_serialize(value.as_bytes()) {
460 write!(f, "{value}")?;
461 }
462 } else {
463 write!(f, "{value}")?;
464 }
465 Ok(())
466 }
467}
468
469trait IntoUrl {
471 fn into_url(self) -> Result<Url>;
473}
474
475impl IntoUrl for &str {
476 fn into_url(self) -> Result<Url> {
477 Url::parse(self).map_err(|s| Error::Parse(format!("invalid url `{self}`: {s}")))
478 }
479}
480
481#[cfg(any(unix, windows))]
482impl IntoUrl for &Path {
483 fn into_url(self) -> Result<Url> {
484 Url::from_file_path(self)
485 .map_err(|_| Error::Parse(format!("invalid path url `{}`", self.display())))
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::SourceId;
492
493 #[test]
494 fn identifies_crates_io() {
495 assert!(SourceId::default().is_default_registry());
496 assert!(
497 SourceId::from_url(super::CRATES_IO_SPARSE_INDEX)
498 .expect("failed to parse sparse URL")
499 .is_default_registry()
500 );
501 }
502}