1use crate::error::{Error, Result};
11use serde::{de, ser, Deserialize, Serialize};
12use std::{fmt, str::FromStr};
13use url::Url;
14
15#[cfg(any(unix, windows))]
16use std::path::Path;
17
18pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
20pub const CRATES_IO_SPARSE_INDEX: &str = "sparse+https://index.crates.io/";
22
23pub const DEFAULT_BRANCH: &str = "master";
25
26#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
28pub struct SourceId {
29 url: Url,
31
32 kind: SourceKind,
34
35 precise: Option<String>,
37
38 name: Option<String>,
40}
41
42#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
44#[non_exhaustive]
45pub enum SourceKind {
46 Git(GitReference),
48
49 Path,
51
52 Registry,
54
55 SparseRegistry,
57
58 LocalRegistry,
60
61 #[cfg(any(unix, windows))]
63 Directory,
64}
65
66impl SourceId {
67 fn new(kind: SourceKind, url: Url) -> Result<Self> {
69 Ok(Self {
70 kind,
71 url,
72 precise: None,
73 name: None,
74 })
75 }
76
77 pub fn normalize_git_source_for_dependency(&self) -> Self {
83 if let SourceKind::Git(GitReference::Rev(_abbrev)) = &self.kind {
84 if let Some(full) = &self.precise {
85 let mut url = self.url.clone();
86 url.set_fragment(None);
87 return Self {
88 kind: SourceKind::Git(GitReference::Rev(full.clone())),
89 precise: None,
90 url,
91 name: self.name.clone(),
92 };
93 }
94 } else if let SourceKind::Git(reference) = &self.kind {
95 if self.precise.is_some() {
96 return Self {
97 kind: SourceKind::Git(reference.clone()),
98 precise: None,
99 url: self.url.clone(),
100 name: self.name.clone(),
101 };
102 }
103 }
104 self.clone()
105 }
106
107 pub fn from_url(string: &str) -> Result<Self> {
118 let mut parts = string.splitn(2, '+');
119 let kind = parts.next().unwrap();
120 let url = parts
121 .next()
122 .ok_or_else(|| Error::Parse(format!("invalid source `{string}`")))?;
123
124 match kind {
125 "git" => {
126 let mut url = url.into_url()?;
127 let mut reference = GitReference::Branch(DEFAULT_BRANCH.to_string());
128 for (k, v) in url.query_pairs() {
129 match &k[..] {
130 "branch" | "ref" => reference = GitReference::Branch(v.into_owned()),
132
133 "rev" => reference = GitReference::Rev(v.into_owned()),
134 "tag" => reference = GitReference::Tag(v.into_owned()),
135 _ => {}
136 }
137 }
138 let precise = url.fragment().map(|s| s.to_owned());
139 url.set_fragment(None);
140 url.set_query(None);
141 Ok(Self::for_git(&url, reference)?.with_precise(precise))
142 }
143 "registry" => {
144 let url = url.into_url()?;
145 Ok(SourceId::new(SourceKind::Registry, url)?
146 .with_precise(Some("locked".to_string())))
147 }
148 "sparse" => {
149 let url = url.into_url()?;
150 Ok(SourceId::new(SourceKind::SparseRegistry, url)?
151 .with_precise(Some("locked".to_string())))
152 }
153 "path" => Self::new(SourceKind::Path, url.into_url()?),
154 kind => Err(Error::Parse(format!(
155 "unsupported source protocol: `{kind}` from `{string}`"
156 ))),
157 }
158 }
159
160 #[cfg(any(unix, windows))]
164 pub fn for_path(path: &Path) -> Result<Self> {
165 Self::new(SourceKind::Path, path.into_url()?)
166 }
167
168 pub fn for_git(url: &Url, reference: GitReference) -> Result<Self> {
170 Self::new(SourceKind::Git(reference), url.clone())
171 }
172
173 pub fn for_registry(url: &Url) -> Result<Self> {
175 Self::new(SourceKind::Registry, url.clone())
176 }
177
178 #[cfg(any(unix, windows))]
180 pub fn for_local_registry(path: &Path) -> Result<Self> {
181 Self::new(SourceKind::LocalRegistry, path.into_url()?)
182 }
183
184 #[cfg(any(unix, windows))]
186 pub fn for_directory(path: &Path) -> Result<Self> {
187 Self::new(SourceKind::Directory, path.into_url()?)
188 }
189
190 pub fn url(&self) -> &Url {
192 &self.url
193 }
194
195 pub fn kind(&self) -> &SourceKind {
197 &self.kind
198 }
199
200 pub fn display_index(&self) -> String {
202 if self.is_default_registry() {
203 "crates.io index".to_string()
204 } else {
205 format!("`{}` index", self.url())
206 }
207 }
208
209 pub fn display_registry_name(&self) -> String {
211 if self.is_default_registry() {
212 "crates.io".to_string()
213 } else if let Some(name) = &self.name {
214 name.clone()
215 } else {
216 self.url().to_string()
217 }
218 }
219
220 pub fn is_path(&self) -> bool {
222 self.kind == SourceKind::Path
223 }
224
225 pub fn is_registry(&self) -> bool {
227 matches!(
228 self.kind,
229 SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
230 )
231 }
232
233 pub fn is_remote_registry(&self) -> bool {
238 matches!(self.kind, SourceKind::Registry | SourceKind::SparseRegistry)
239 }
240
241 pub fn is_git(&self) -> bool {
243 matches!(self.kind, SourceKind::Git(_))
244 }
245
246 pub fn precise(&self) -> Option<&str> {
248 self.precise.as_ref().map(AsRef::as_ref)
249 }
250
251 pub fn git_reference(&self) -> Option<&GitReference> {
253 if let SourceKind::Git(ref s) = self.kind {
254 Some(s)
255 } else {
256 None
257 }
258 }
259
260 pub fn with_precise(&self, v: Option<String>) -> Self {
262 Self {
263 precise: v,
264 ..self.clone()
265 }
266 }
267
268 pub fn is_default_registry(&self) -> bool {
270 self.kind == SourceKind::Registry && self.url.as_str() == CRATES_IO_INDEX
271 || self.kind == SourceKind::SparseRegistry
272 && self.url.as_str() == &CRATES_IO_SPARSE_INDEX[7..]
273 }
274
275 pub(crate) fn as_url(&self, encoded: bool) -> SourceIdAsUrl<'_> {
277 SourceIdAsUrl { id: self, encoded }
278 }
279}
280
281impl Default for SourceId {
282 fn default() -> SourceId {
283 SourceId::for_registry(&CRATES_IO_INDEX.into_url().unwrap()).unwrap()
284 }
285}
286
287impl FromStr for SourceId {
288 type Err = Error;
289
290 fn from_str(s: &str) -> Result<Self> {
291 Self::from_url(s)
292 }
293}
294
295impl fmt::Display for SourceId {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 self.as_url(false).fmt(f)
298 }
299}
300
301pub(crate) struct SourceIdAsUrl<'a> {
303 id: &'a SourceId,
304 encoded: bool,
305}
306
307impl<'a> fmt::Display for SourceIdAsUrl<'a> {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 match self.id {
310 SourceId {
311 kind: SourceKind::Path,
312 ref url,
313 ..
314 } => write!(f, "path+{url}"),
315 SourceId {
316 kind: SourceKind::Git(ref reference),
317 ref url,
318 ref precise,
319 ..
320 } => {
321 write!(f, "git+{url}")?;
322 if let Some(pretty) = reference.pretty_ref(self.encoded) {
324 write!(f, "?{pretty}")?;
325 }
326 if let Some(precise) = precise.as_ref() {
327 write!(f, "#{precise}")?;
328 }
329 Ok(())
330 }
331 SourceId {
332 kind: SourceKind::Registry,
333 ref url,
334 ..
335 } => write!(f, "registry+{url}"),
336 SourceId {
337 kind: SourceKind::SparseRegistry,
338 ref url,
339 ..
340 } => write!(f, "sparse+{url}"),
341 SourceId {
342 kind: SourceKind::LocalRegistry,
343 ref url,
344 ..
345 } => write!(f, "local-registry+{url}"),
346 #[cfg(any(unix, windows))]
347 SourceId {
348 kind: SourceKind::Directory,
349 ref url,
350 ..
351 } => write!(f, "directory+{url}"),
352 }
353 }
354}
355
356impl Serialize for SourceId {
357 fn serialize<S: ser::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
358 if self.is_path() {
359 None::<String>.serialize(s)
360 } else {
361 s.collect_str(&self.to_string())
362 }
363 }
364}
365
366impl<'de> Deserialize<'de> for SourceId {
367 fn deserialize<D: de::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
368 let string = String::deserialize(d)?;
369 SourceId::from_url(&string).map_err(de::Error::custom)
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
375pub enum GitReference {
376 Tag(String),
378
379 Branch(String),
381
382 Rev(String),
384}
385
386impl GitReference {
387 pub fn pretty_ref(&self, url_encoded: bool) -> Option<PrettyRef<'_>> {
390 match self {
391 GitReference::Branch(ref s) if *s == DEFAULT_BRANCH => None,
392 _ => Some(PrettyRef {
393 inner: self,
394 url_encoded,
395 }),
396 }
397 }
398}
399
400pub struct PrettyRef<'a> {
402 inner: &'a GitReference,
403 url_encoded: bool,
404}
405
406impl<'a> fmt::Display for PrettyRef<'a> {
407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 let value: &str = match self.inner {
409 GitReference::Branch(s) => {
410 write!(f, "branch=")?;
411 s
412 }
413 GitReference::Tag(s) => {
414 write!(f, "tag=")?;
415 s
416 }
417 GitReference::Rev(s) => {
418 write!(f, "rev=")?;
419 s
420 }
421 };
422 if self.url_encoded {
423 for value in url::form_urlencoded::byte_serialize(value.as_bytes()) {
424 write!(f, "{value}")?;
425 }
426 } else {
427 write!(f, "{value}")?;
428 }
429 Ok(())
430 }
431}
432
433trait IntoUrl {
435 fn into_url(self) -> Result<Url>;
437}
438
439impl<'a> IntoUrl for &'a str {
440 fn into_url(self) -> Result<Url> {
441 Url::parse(self).map_err(|s| Error::Parse(format!("invalid url `{self}`: {s}")))
442 }
443}
444
445#[cfg(any(unix, windows))]
446impl<'a> IntoUrl for &'a Path {
447 fn into_url(self) -> Result<Url> {
448 Url::from_file_path(self)
449 .map_err(|_| Error::Parse(format!("invalid path url `{}`", self.display())))
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::SourceId;
456
457 #[test]
458 fn identifies_crates_io() {
459 assert!(SourceId::default().is_default_registry());
460 assert!(SourceId::from_url(super::CRATES_IO_SPARSE_INDEX)
461 .expect("failed to parse sparse URL")
462 .is_default_registry());
463 }
464}