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