1use std::fmt;
2
3use semver::Version;
4use serde::{de, ser};
5use url::Url;
6
7use crate::core::GitReference;
8use crate::core::PartialVersion;
9use crate::core::PartialVersionError;
10use crate::core::SourceKind;
11use crate::manifest::PackageName;
12use crate::restricted_names::NameValidationError;
13
14type Result<T> = std::result::Result<T, PackageIdSpecError>;
15
16#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
26pub struct PackageIdSpec {
27 name: String,
28 version: Option<PartialVersion>,
29 url: Option<Url>,
30 kind: Option<SourceKind>,
31}
32
33impl PackageIdSpec {
34 pub fn new(name: String) -> Self {
35 Self {
36 name,
37 version: None,
38 url: None,
39 kind: None,
40 }
41 }
42
43 pub fn with_version(mut self, version: PartialVersion) -> Self {
44 self.version = Some(version);
45 self
46 }
47
48 pub fn with_url(mut self, url: Url) -> Self {
49 self.url = Some(url);
50 self
51 }
52
53 pub fn with_kind(mut self, kind: SourceKind) -> Self {
54 self.kind = Some(kind);
55 self
56 }
57
58 pub fn parse(spec: &str) -> Result<PackageIdSpec> {
82 if spec.contains("://") {
83 if let Ok(url) = Url::parse(spec) {
84 return PackageIdSpec::from_url(url);
85 }
86 } else if spec.contains('/') || spec.contains('\\') {
87 let abs = std::env::current_dir().unwrap_or_default().join(spec);
88 if abs.exists() {
89 let maybe_url = Url::from_file_path(abs)
90 .map_or_else(|_| "a file:// URL".to_string(), |url| url.to_string());
91 return Err(ErrorKind::MaybeFilePath {
92 spec: spec.into(),
93 maybe_url,
94 }
95 .into());
96 }
97 }
98 let (name, version) = match parse_spec(spec)? {
99 Some((name, ver)) => (name, Some(ver)),
100 None => (spec.to_owned(), None),
101 };
102 PackageName::new(&name)?;
103 Ok(PackageIdSpec {
104 name: String::from(name),
105 version,
106 url: None,
107 kind: None,
108 })
109 }
110
111 fn from_url(mut url: Url) -> Result<PackageIdSpec> {
113 let mut kind = None;
114 if let Some((kind_str, scheme)) = url.scheme().split_once('+') {
115 match kind_str {
116 "git" => {
117 let git_ref = GitReference::from_query(url.query_pairs());
118 url.set_query(None);
119 kind = Some(SourceKind::Git(git_ref));
120 url = strip_url_protocol(&url)?;
121 }
122 "registry" => {
123 if url.query().is_some() {
124 return Err(ErrorKind::UnexpectedQueryString(url).into());
125 }
126 kind = Some(SourceKind::Registry);
127 url = strip_url_protocol(&url)?;
128 }
129 "sparse" => {
130 if url.query().is_some() {
131 return Err(ErrorKind::UnexpectedQueryString(url).into());
132 }
133 kind = Some(SourceKind::SparseRegistry);
134 }
137 "path" => {
138 if url.query().is_some() {
139 return Err(ErrorKind::UnexpectedQueryString(url).into());
140 }
141 if scheme != "file" {
142 return Err(ErrorKind::UnsupportedPathPlusScheme(scheme.into()).into());
143 }
144 kind = Some(SourceKind::Path);
145 url = strip_url_protocol(&url)?;
146 }
147 "builtin" => {
148 if url.query().is_some() {
149 return Err(ErrorKind::UnexpectedQueryString(url).into());
150 }
151 if scheme != "builtin" {
152 return Err(ErrorKind::UnsupportedBuiltinScheme(scheme.into()).into());
153 }
154 kind = Some(SourceKind::Builtin);
155 url = strip_url_protocol(&url)?;
156 }
157 kind => return Err(ErrorKind::UnsupportedProtocol(kind.into()).into()),
158 }
159 } else if url.scheme() == "builtin" {
160 if url.query().is_some() {
161 return Err(ErrorKind::UnexpectedQueryString(url).into());
162 }
163 kind = Some(SourceKind::Builtin)
164 } else if url.query().is_some() {
165 return Err(ErrorKind::UnexpectedQueryString(url).into());
166 }
167
168 let frag = url.fragment().map(|s| s.to_owned());
169 url.set_fragment(None);
170
171 if kind == Some(SourceKind::Builtin) {
172 if url.host_str() != Some(".") {
175 return Err(ErrorKind::InvalidHostname.into());
176 }
177 if url.path() != "" {
178 return Err(ErrorKind::InvalidPath.into());
179 }
180 let Some(frag) = frag else {
181 return Err(ErrorKind::FragmentRequired.into());
182 };
183
184 if frag.is_empty() {
185 return Err(ErrorKind::EmptyFragment.into());
186 }
187 let name = match parse_spec(&frag)? {
188 Some((name, ver)) => {
189 if !ver.matches(&Version::new(0, 0, 0)) {
190 return Err(ErrorKind::InvalidVersion(ver.to_string()).into());
191 }
192 name
193 }
194 None => frag,
195 };
196
197 PackageName::new(&name)?;
198 let version = PartialVersion {
199 major: 0,
200 minor: Some(0),
201 patch: Some(0),
202 pre: None,
203 build: None,
204 };
205 return Ok(PackageIdSpec {
206 name,
207 version: Some(version),
208 url: Some(url),
209 kind,
210 });
211 }
212
213 let (name, version) = {
214 let Some(path_name) = url.path_segments().and_then(|mut p| p.next_back()) else {
215 return Err(ErrorKind::MissingUrlPath(url).into());
216 };
217 match frag {
218 Some(fragment) => match parse_spec(&fragment)? {
219 Some((name, ver)) => (name, Some(ver)),
220 None => {
221 let Some(f) = fragment.chars().next() else {
222 return Err(PackageIdSpecError(ErrorKind::EmptyFragment));
223 };
224
225 if f.is_alphabetic() {
226 (String::from(fragment.as_str()), None)
227 } else {
228 let version = fragment.parse::<PartialVersion>()?;
229 (String::from(path_name), Some(version))
230 }
231 }
232 },
233 None => (String::from(path_name), None),
234 }
235 };
236 PackageName::new(&name)?;
237 Ok(PackageIdSpec {
238 name,
239 version,
240 url: Some(url),
241 kind,
242 })
243 }
244
245 pub fn name(&self) -> &str {
246 self.name.as_str()
247 }
248
249 pub fn version(&self) -> Option<Version> {
251 self.version.as_ref().and_then(|v| v.to_version())
252 }
253
254 pub fn partial_version(&self) -> Option<&PartialVersion> {
255 self.version.as_ref()
256 }
257
258 pub fn url(&self) -> Option<&Url> {
259 self.url.as_ref()
260 }
261
262 pub fn set_url(&mut self, url: Url) {
263 self.url = Some(url);
264 }
265
266 pub fn kind(&self) -> Option<&SourceKind> {
267 self.kind.as_ref()
268 }
269
270 pub fn set_kind(&mut self, kind: SourceKind) {
271 self.kind = Some(kind);
272 }
273}
274
275fn parse_spec(spec: &str) -> Result<Option<(String, PartialVersion)>> {
276 let Some((name, ver)) = spec
277 .rsplit_once('@')
278 .or_else(|| spec.rsplit_once(':').filter(|(n, _)| !n.ends_with(':')))
279 else {
280 return Ok(None);
281 };
282 let name = name.to_owned();
283 let ver = ver.parse::<PartialVersion>()?;
284 Ok(Some((name, ver)))
285}
286
287fn strip_url_protocol(url: &Url) -> Result<Url> {
288 let raw = url.to_string();
290 let (_, rest) = raw
291 .split_once('+')
292 .ok_or_else(|| ErrorKind::InvalidPkgIdUrl(raw.clone()))?;
293 rest.parse()
294 .map_err(|_| ErrorKind::InvalidPkgIdUrl(raw).into())
295}
296
297impl fmt::Display for PackageIdSpec {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 let mut printed_name = false;
300 match self.url {
301 Some(ref url) => {
302 if let Some(protocol) = self.kind.as_ref().and_then(SourceKind::protocol) {
303 write!(f, "{protocol}+")?;
304 }
305 write!(f, "{}", url)?;
306 if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() {
307 if let Some(pretty) = git_ref.pretty_ref(true) {
308 write!(f, "?{}", pretty)?;
309 }
310 }
311 if self.kind() == Some(&SourceKind::Builtin)
312 || url.path_segments().unwrap().next_back().unwrap() != &*self.name
313 {
314 printed_name = true;
315 write!(f, "#{}", self.name)?;
316 }
317 }
318 None => {
319 printed_name = true;
320 write!(f, "{}", self.name)?;
321 }
322 }
323 if let Some(ref v) = self.version
324 && self.kind != Some(SourceKind::Builtin)
325 {
326 write!(f, "{}{}", if printed_name { "@" } else { "#" }, v)?;
327 }
328 Ok(())
329 }
330}
331
332impl ser::Serialize for PackageIdSpec {
333 fn serialize<S>(&self, s: S) -> std::result::Result<S::Ok, S::Error>
334 where
335 S: ser::Serializer,
336 {
337 self.to_string().serialize(s)
338 }
339}
340
341impl<'de> de::Deserialize<'de> for PackageIdSpec {
342 fn deserialize<D>(d: D) -> std::result::Result<PackageIdSpec, D::Error>
343 where
344 D: de::Deserializer<'de>,
345 {
346 let string = String::deserialize(d)?;
347 PackageIdSpec::parse(&string).map_err(de::Error::custom)
348 }
349}
350
351#[cfg(feature = "unstable-schema")]
352impl schemars::JsonSchema for PackageIdSpec {
353 fn schema_name() -> std::borrow::Cow<'static, str> {
354 "PackageIdSpec".into()
355 }
356 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
357 <String as schemars::JsonSchema>::json_schema(generator)
358 }
359}
360
361#[derive(Debug, thiserror::Error)]
362#[error(transparent)]
363pub struct PackageIdSpecError(#[from] ErrorKind);
364
365impl From<PartialVersionError> for PackageIdSpecError {
366 fn from(value: PartialVersionError) -> Self {
367 ErrorKind::PartialVersion(value).into()
368 }
369}
370
371impl From<NameValidationError> for PackageIdSpecError {
372 fn from(value: NameValidationError) -> Self {
373 ErrorKind::NameValidation(value).into()
374 }
375}
376
377#[non_exhaustive]
379#[derive(Debug, thiserror::Error)]
380enum ErrorKind {
381 #[error("unsupported source protocol: {0}")]
382 UnsupportedProtocol(String),
383
384 #[error("`path+{0}` is unsupported; `path+file` and `file` schemes are supported")]
385 UnsupportedPathPlusScheme(String),
386
387 #[error("cannot have a query string in a pkgid: {0}")]
388 UnexpectedQueryString(Url),
389
390 #[error("pkgid urls must have at least one path component: {0}")]
391 MissingUrlPath(Url),
392
393 #[error("package ID specification `{spec}` looks like a file path, maybe try {maybe_url}")]
394 MaybeFilePath { spec: String, maybe_url: String },
395
396 #[error("pkgid url cannot have an empty fragment")]
397 EmptyFragment,
398
399 #[error("builtin package ID specifications must specify the package name in the fragment")]
400 FragmentRequired,
401
402 #[error("version `{0}` is invalid for builtin packages, which are unversioned (or `0.0.0`)")]
403 InvalidVersion(String),
404
405 #[error("only `.` is permitted as a hostname for builtin package ID specifications")]
406 InvalidHostname,
407
408 #[error("only empty paths are permitted for builtin package ID specifications")]
409 InvalidPath,
410
411 #[error(
412 "`builtin+{0}` is unsupported - only the `builtin` protocol is supported for `builtin` kinds"
413 )]
414 UnsupportedBuiltinScheme(String),
415
416 #[error(transparent)]
417 NameValidation(#[from] crate::restricted_names::NameValidationError),
418
419 #[error(transparent)]
420 PartialVersion(#[from] crate::core::PartialVersionError),
421
422 #[error("invalid pkgid url: {0}")]
423 InvalidPkgIdUrl(String),
424}
425
426#[cfg(test)]
427mod tests {
428 use super::ErrorKind;
429 use super::PackageIdSpec;
430 use crate::core::{GitReference, SourceKind};
431 use url::Url;
432
433 #[track_caller]
434 fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) {
435 let parsed = PackageIdSpec::parse(spec).unwrap();
436 assert_eq!(parsed, expected);
437 let rendered = parsed.to_string();
438 assert_eq!(rendered, expected_rendered);
439 let reparsed = PackageIdSpec::parse(&rendered).unwrap();
440 assert_eq!(reparsed, expected);
441 }
442
443 macro_rules! err {
444 ($spec:expr, $expected:pat) => {
445 let err = PackageIdSpec::parse($spec).unwrap_err();
446 let kind = err.0;
447 assert!(
448 matches!(kind, $expected),
449 "`{}` parse error mismatch, got {kind:?}",
450 $spec
451 );
452 };
453 }
454
455 #[test]
456 fn good_parsing() {
457 ok(
458 "https://crates.io/foo",
459 PackageIdSpec {
460 name: String::from("foo"),
461 version: None,
462 url: Some(Url::parse("https://crates.io/foo").unwrap()),
463 kind: None,
464 },
465 "https://crates.io/foo",
466 );
467 ok(
468 "https://crates.io/foo#1.2.3",
469 PackageIdSpec {
470 name: String::from("foo"),
471 version: Some("1.2.3".parse().unwrap()),
472 url: Some(Url::parse("https://crates.io/foo").unwrap()),
473 kind: None,
474 },
475 "https://crates.io/foo#1.2.3",
476 );
477 ok(
478 "https://crates.io/foo#1.2",
479 PackageIdSpec {
480 name: String::from("foo"),
481 version: Some("1.2".parse().unwrap()),
482 url: Some(Url::parse("https://crates.io/foo").unwrap()),
483 kind: None,
484 },
485 "https://crates.io/foo#1.2",
486 );
487 ok(
488 "https://crates.io/foo#bar:1.2.3",
489 PackageIdSpec {
490 name: String::from("bar"),
491 version: Some("1.2.3".parse().unwrap()),
492 url: Some(Url::parse("https://crates.io/foo").unwrap()),
493 kind: None,
494 },
495 "https://crates.io/foo#bar@1.2.3",
496 );
497 ok(
498 "https://crates.io/foo#bar@1.2.3",
499 PackageIdSpec {
500 name: String::from("bar"),
501 version: Some("1.2.3".parse().unwrap()),
502 url: Some(Url::parse("https://crates.io/foo").unwrap()),
503 kind: None,
504 },
505 "https://crates.io/foo#bar@1.2.3",
506 );
507 ok(
508 "https://crates.io/foo#bar@1.2",
509 PackageIdSpec {
510 name: String::from("bar"),
511 version: Some("1.2".parse().unwrap()),
512 url: Some(Url::parse("https://crates.io/foo").unwrap()),
513 kind: None,
514 },
515 "https://crates.io/foo#bar@1.2",
516 );
517 ok(
518 "registry+https://crates.io/foo#bar@1.2",
519 PackageIdSpec {
520 name: String::from("bar"),
521 version: Some("1.2".parse().unwrap()),
522 url: Some(Url::parse("https://crates.io/foo").unwrap()),
523 kind: Some(SourceKind::Registry),
524 },
525 "registry+https://crates.io/foo#bar@1.2",
526 );
527 ok(
528 "sparse+https://crates.io/foo#bar@1.2",
529 PackageIdSpec {
530 name: String::from("bar"),
531 version: Some("1.2".parse().unwrap()),
532 url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()),
533 kind: Some(SourceKind::SparseRegistry),
534 },
535 "sparse+https://crates.io/foo#bar@1.2",
536 );
537 ok(
538 "foo",
539 PackageIdSpec {
540 name: String::from("foo"),
541 version: None,
542 url: None,
543 kind: None,
544 },
545 "foo",
546 );
547 ok(
548 "foo::bar",
549 PackageIdSpec {
550 name: String::from("foo::bar"),
551 version: None,
552 url: None,
553 kind: None,
554 },
555 "foo::bar",
556 );
557 ok(
558 "foo:1.2.3",
559 PackageIdSpec {
560 name: String::from("foo"),
561 version: Some("1.2.3".parse().unwrap()),
562 url: None,
563 kind: None,
564 },
565 "foo@1.2.3",
566 );
567 ok(
568 "foo::bar:1.2.3",
569 PackageIdSpec {
570 name: String::from("foo::bar"),
571 version: Some("1.2.3".parse().unwrap()),
572 url: None,
573 kind: None,
574 },
575 "foo::bar@1.2.3",
576 );
577 ok(
578 "foo@1.2.3",
579 PackageIdSpec {
580 name: String::from("foo"),
581 version: Some("1.2.3".parse().unwrap()),
582 url: None,
583 kind: None,
584 },
585 "foo@1.2.3",
586 );
587 ok(
588 "foo::bar@1.2.3",
589 PackageIdSpec {
590 name: String::from("foo::bar"),
591 version: Some("1.2.3".parse().unwrap()),
592 url: None,
593 kind: None,
594 },
595 "foo::bar@1.2.3",
596 );
597 ok(
598 "foo@1.2",
599 PackageIdSpec {
600 name: String::from("foo"),
601 version: Some("1.2".parse().unwrap()),
602 url: None,
603 kind: None,
604 },
605 "foo@1.2",
606 );
607
608 ok(
610 "regex",
611 PackageIdSpec {
612 name: String::from("regex"),
613 version: None,
614 url: None,
615 kind: None,
616 },
617 "regex",
618 );
619 ok(
620 "regex@1.4",
621 PackageIdSpec {
622 name: String::from("regex"),
623 version: Some("1.4".parse().unwrap()),
624 url: None,
625 kind: None,
626 },
627 "regex@1.4",
628 );
629 ok(
630 "regex@1.4.3",
631 PackageIdSpec {
632 name: String::from("regex"),
633 version: Some("1.4.3".parse().unwrap()),
634 url: None,
635 kind: None,
636 },
637 "regex@1.4.3",
638 );
639 ok(
640 "https://github.com/rust-lang/crates.io-index#regex",
641 PackageIdSpec {
642 name: String::from("regex"),
643 version: None,
644 url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
645 kind: None,
646 },
647 "https://github.com/rust-lang/crates.io-index#regex",
648 );
649 ok(
650 "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
651 PackageIdSpec {
652 name: String::from("regex"),
653 version: Some("1.4.3".parse().unwrap()),
654 url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
655 kind: None,
656 },
657 "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
658 );
659 ok(
660 "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
661 PackageIdSpec {
662 name: String::from("regex"),
663 version: Some("1.4.3".parse().unwrap()),
664 url: Some(
665 Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(),
666 ),
667 kind: Some(SourceKind::SparseRegistry),
668 },
669 "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
670 );
671 ok(
672 "https://github.com/rust-lang/cargo#0.52.0",
673 PackageIdSpec {
674 name: String::from("cargo"),
675 version: Some("0.52.0".parse().unwrap()),
676 url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
677 kind: None,
678 },
679 "https://github.com/rust-lang/cargo#0.52.0",
680 );
681 ok(
682 "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
683 PackageIdSpec {
684 name: String::from("cargo-platform"),
685 version: Some("0.1.2".parse().unwrap()),
686 url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
687 kind: None,
688 },
689 "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
690 );
691 ok(
692 "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
693 PackageIdSpec {
694 name: String::from("regex"),
695 version: Some("1.4.3".parse().unwrap()),
696 url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
697 kind: None,
698 },
699 "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
700 );
701 ok(
702 "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
703 PackageIdSpec {
704 name: String::from("regex"),
705 version: Some("1.4.3".parse().unwrap()),
706 url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
707 kind: Some(SourceKind::Git(GitReference::DefaultBranch)),
708 },
709 "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
710 );
711 ok(
712 "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
713 PackageIdSpec {
714 name: String::from("regex"),
715 version: Some("1.4.3".parse().unwrap()),
716 url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
717 kind: Some(SourceKind::Git(GitReference::Branch("dev".to_owned()))),
718 },
719 "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
720 );
721 ok(
722 "file:///path/to/my/project/foo",
723 PackageIdSpec {
724 name: String::from("foo"),
725 version: None,
726 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
727 kind: None,
728 },
729 "file:///path/to/my/project/foo",
730 );
731 ok(
732 "file:///path/to/my/project/foo::bar",
733 PackageIdSpec {
734 name: String::from("foo::bar"),
735 version: None,
736 url: Some(Url::parse("file:///path/to/my/project/foo::bar").unwrap()),
737 kind: None,
738 },
739 "file:///path/to/my/project/foo::bar",
740 );
741 ok(
742 "file:///path/to/my/project/foo#1.1.8",
743 PackageIdSpec {
744 name: String::from("foo"),
745 version: Some("1.1.8".parse().unwrap()),
746 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
747 kind: None,
748 },
749 "file:///path/to/my/project/foo#1.1.8",
750 );
751 ok(
752 "path+file:///path/to/my/project/foo#1.1.8",
753 PackageIdSpec {
754 name: String::from("foo"),
755 version: Some("1.1.8".parse().unwrap()),
756 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
757 kind: Some(SourceKind::Path),
758 },
759 "path+file:///path/to/my/project/foo#1.1.8",
760 );
761 ok(
762 "path+file:///path/to/my/project/foo#bar",
763 PackageIdSpec {
764 name: String::from("bar"),
765 version: None,
766 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
767 kind: Some(SourceKind::Path),
768 },
769 "path+file:///path/to/my/project/foo#bar",
770 );
771 ok(
772 "path+file:///path/to/my/project/foo#foo::bar",
773 PackageIdSpec {
774 name: String::from("foo::bar"),
775 version: None,
776 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
777 kind: Some(SourceKind::Path),
778 },
779 "path+file:///path/to/my/project/foo#foo::bar",
780 );
781 ok(
782 "path+file:///path/to/my/project/foo#bar:1.1.8",
783 PackageIdSpec {
784 name: String::from("bar"),
785 version: Some("1.1.8".parse().unwrap()),
786 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
787 kind: Some(SourceKind::Path),
788 },
789 "path+file:///path/to/my/project/foo#bar@1.1.8",
790 );
791 ok(
792 "path+file:///path/to/my/project/foo#foo::bar:1.1.8",
793 PackageIdSpec {
794 name: String::from("foo::bar"),
795 version: Some("1.1.8".parse().unwrap()),
796 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
797 kind: Some(SourceKind::Path),
798 },
799 "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
800 );
801 ok(
802 "path+file:///path/to/my/project/foo#bar@1.1.8",
803 PackageIdSpec {
804 name: String::from("bar"),
805 version: Some("1.1.8".parse().unwrap()),
806 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
807 kind: Some(SourceKind::Path),
808 },
809 "path+file:///path/to/my/project/foo#bar@1.1.8",
810 );
811 ok(
812 "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
813 PackageIdSpec {
814 name: String::from("foo::bar"),
815 version: Some("1.1.8".parse().unwrap()),
816 url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
817 kind: Some(SourceKind::Path),
818 },
819 "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
820 );
821 ok(
822 "builtin://.#core",
823 PackageIdSpec {
824 name: String::from("core"),
825 version: Some("0.0.0".parse().unwrap()),
826 url: Some(Url::parse("builtin://.").unwrap()),
827 kind: Some(SourceKind::Builtin),
828 },
829 "builtin://.#core",
830 );
831 ok(
832 "builtin+builtin://.#core",
833 PackageIdSpec {
834 name: String::from("core"),
835 version: Some("0.0.0".parse().unwrap()),
836 url: Some(Url::parse("builtin://.").unwrap()),
837 kind: Some(SourceKind::Builtin),
838 },
839 "builtin://.#core",
840 );
841 ok(
842 "builtin+builtin://.#core@0.0.0",
843 PackageIdSpec {
844 name: String::from("core"),
845 version: Some("0.0.0".parse().unwrap()),
846 url: Some(Url::parse("builtin://.").unwrap()),
847 kind: Some(SourceKind::Builtin),
848 },
849 "builtin://.#core",
850 );
851 ok(
852 "builtin+builtin://.#core@0",
853 PackageIdSpec {
854 name: String::from("core"),
855 version: Some("0.0.0".parse().unwrap()),
856 url: Some(Url::parse("builtin://.").unwrap()),
857 kind: Some(SourceKind::Builtin),
858 },
859 "builtin://.#core",
860 );
861 }
862
863 #[test]
864 fn bad_parsing() {
865 err!("baz:", ErrorKind::PartialVersion(_));
866 err!("baz:*", ErrorKind::PartialVersion(_));
867 err!("baz@", ErrorKind::PartialVersion(_));
868 err!("baz@*", ErrorKind::PartialVersion(_));
869 err!("baz@^1.0", ErrorKind::PartialVersion(_));
870 err!("https://baz:1.0", ErrorKind::NameValidation(_));
871 err!("https://#baz:1.0", ErrorKind::NameValidation(_));
872 err!(
873 "foobar+https://github.com/rust-lang/crates.io-index",
874 ErrorKind::UnsupportedProtocol(_)
875 );
876 err!(
877 "path+https://github.com/rust-lang/crates.io-index",
878 ErrorKind::UnsupportedPathPlusScheme(_)
879 );
880
881 err!(
883 "file:///path/to/my/project/foo?branch=dev",
884 ErrorKind::UnexpectedQueryString(_)
885 );
886 err!(
887 "path+file:///path/to/my/project/foo?branch=dev",
888 ErrorKind::UnexpectedQueryString(_)
889 );
890 err!(
891 "registry+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
892 ErrorKind::UnexpectedQueryString(_)
893 );
894 err!(
895 "sparse+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
896 ErrorKind::UnexpectedQueryString(_)
897 );
898 err!("@1.2.3", ErrorKind::NameValidation(_));
899 err!("registry+https://github.com", ErrorKind::NameValidation(_));
900 err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_));
901 err!("https://example.com/foo#", ErrorKind::EmptyFragment);
902 err!("git++://x:", ErrorKind::InvalidPkgIdUrl(_));
903 err!(
904 "git++https://github.com/rust-lang/cargo",
905 ErrorKind::InvalidPkgIdUrl(_)
906 );
907 err!("git+https://", ErrorKind::InvalidPkgIdUrl(_));
908 err!("builtin://.", ErrorKind::FragmentRequired);
909 err!(
910 "builtin+builtin://.#core@0.1.0",
911 ErrorKind::InvalidVersion(_)
912 );
913 err!("builtin://.#", ErrorKind::EmptyFragment);
914 err!("builtin://.#0.0.0", ErrorKind::NameValidation(_));
915 err!("builtin://wrong#core", ErrorKind::InvalidHostname);
916 err!("builtin://./invalid/path#core", ErrorKind::InvalidPath);
917 err!(
918 "builtin+invalid://.",
919 ErrorKind::UnsupportedBuiltinScheme(_)
920 );
921 err!(
922 "builtin://.?query=test#core",
923 ErrorKind::UnexpectedQueryString(_)
924 );
925 err!(
926 "builtin+builtin://.?query=test#core",
927 ErrorKind::UnexpectedQueryString(_)
928 );
929 }
930}