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