1use crate::{
98 error::{err, Error},
99 fmt::{
100 offset::{self, ParsedOffset},
101 temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind},
102 Parsed,
103 },
104 util::{escape, parse},
105};
106
107#[derive(Debug)]
114pub(crate) struct ParsedAnnotations<'i> {
115 #[allow(dead_code)]
119 input: escape::Bytes<'i>,
120 time_zone: Option<ParsedTimeZone<'i>>,
122 }
126
127impl<'i> ParsedAnnotations<'i> {
128 pub(crate) fn none() -> ParsedAnnotations<'static> {
130 ParsedAnnotations { input: escape::Bytes(&[]), time_zone: None }
131 }
132
133 pub(crate) fn to_time_zone_annotation(
139 &self,
140 ) -> Result<Option<TimeZoneAnnotation<'i>>, Error> {
141 let Some(ref parsed) = self.time_zone else { return Ok(None) };
142 Ok(Some(parsed.to_time_zone_annotation()?))
143 }
144}
145
146#[derive(Debug)]
148enum ParsedTimeZone<'i> {
149 Named {
151 critical: bool,
153 name: &'i str,
155 },
156 Offset {
158 critical: bool,
160 offset: ParsedOffset,
162 },
163}
164
165impl<'i> ParsedTimeZone<'i> {
166 pub(crate) fn to_time_zone_annotation(
174 &self,
175 ) -> Result<TimeZoneAnnotation<'i>, Error> {
176 let (kind, critical) = match *self {
177 ParsedTimeZone::Named { name, critical } => {
178 let kind = TimeZoneAnnotationKind::from(name);
179 (kind, critical)
180 }
181 ParsedTimeZone::Offset { ref offset, critical } => {
182 let kind = TimeZoneAnnotationKind::Offset(offset.to_offset()?);
183 (kind, critical)
184 }
185 };
186 Ok(TimeZoneAnnotation { kind, critical })
187 }
188}
189
190#[derive(Debug)]
192pub(crate) struct Parser {
193 _priv: (),
195}
196
197impl Parser {
198 pub(crate) const fn new() -> Parser {
200 Parser { _priv: () }
201 }
202
203 pub(crate) fn parse<'i>(
212 &self,
213 input: &'i [u8],
214 ) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
215 let mkslice = parse::slicer(input);
216
217 let Parsed { value: time_zone, mut input } =
218 self.parse_time_zone_annotation(input)?;
219 loop {
220 let Parsed { value: did_consume, input: unconsumed } =
225 self.parse_annotation(input)?;
226 if !did_consume {
227 break;
228 }
229 input = unconsumed;
230 }
231
232 let value = ParsedAnnotations {
233 input: escape::Bytes(mkslice(input)),
234 time_zone,
235 };
236 Ok(Parsed { value, input })
237 }
238
239 fn parse_time_zone_annotation<'i>(
240 &self,
241 mut input: &'i [u8],
242 ) -> Result<Parsed<'i, Option<ParsedTimeZone<'i>>>, Error> {
243 let unconsumed = input;
244 if input.is_empty() || input[0] != b'[' {
245 return Ok(Parsed { value: None, input: unconsumed });
246 }
247 input = &input[1..];
248
249 let critical = input.starts_with(b"!");
250 if critical {
251 input = &input[1..];
252 }
253
254 if input.starts_with(b"+") || input.starts_with(b"-") {
259 const P: offset::Parser =
260 offset::Parser::new().zulu(false).subminute(false);
261
262 let Parsed { value: offset, input } = P.parse(input)?;
263 let Parsed { input, .. } =
264 self.parse_tz_annotation_close(input)?;
265 let value = Some(ParsedTimeZone::Offset { critical, offset });
266 return Ok(Parsed { value, input });
267 }
268
269 let mkiana = parse::slicer(input);
276 let Parsed { mut input, .. } =
277 self.parse_tz_annotation_iana_name(input)?;
278 if input.starts_with(b"=") {
283 return Ok(Parsed { value: None, input: unconsumed });
286 }
287 while input.starts_with(b"/") {
288 input = &input[1..];
289 let Parsed { input: unconsumed, .. } =
290 self.parse_tz_annotation_iana_name(input)?;
291 input = unconsumed;
292 }
293 let iana_name = core::str::from_utf8(mkiana(input)).expect("ASCII");
298 let time_zone =
299 Some(ParsedTimeZone::Named { critical, name: iana_name });
300 let Parsed { input, .. } = self.parse_tz_annotation_close(input)?;
302 Ok(Parsed { value: time_zone, input })
303 }
304
305 fn parse_annotation<'i>(
306 &self,
307 mut input: &'i [u8],
308 ) -> Result<Parsed<'i, bool>, Error> {
309 if input.is_empty() || input[0] != b'[' {
310 return Ok(Parsed { value: false, input });
311 }
312 input = &input[1..];
313
314 let critical = input.starts_with(b"!");
315 if critical {
316 input = &input[1..];
317 }
318
319 let Parsed { value: key, input } = self.parse_annotation_key(input)?;
320 let Parsed { input, .. } = self.parse_annotation_separator(input)?;
321 let Parsed { input, .. } = self.parse_annotation_values(input)?;
322 let Parsed { input, .. } = self.parse_annotation_close(input)?;
323
324 if critical {
329 return Err(err!(
330 "found unsupported RFC 9557 annotation with key {key:?} \
331 with the critical flag ('!') set",
332 key = escape::Bytes(key),
333 ));
334 }
335
336 Ok(Parsed { value: true, input })
337 }
338
339 fn parse_tz_annotation_iana_name<'i>(
340 &self,
341 input: &'i [u8],
342 ) -> Result<Parsed<'i, &'i [u8]>, Error> {
343 let mkname = parse::slicer(input);
344 let Parsed { mut input, .. } =
345 self.parse_tz_annotation_leading_char(input)?;
346 loop {
347 let Parsed { value: did_consume, input: unconsumed } =
348 self.parse_tz_annotation_char(input);
349 if !did_consume {
350 break;
351 }
352 input = unconsumed;
353 }
354 Ok(Parsed { value: mkname(input), input })
355 }
356
357 fn parse_annotation_key<'i>(
358 &self,
359 input: &'i [u8],
360 ) -> Result<Parsed<'i, &'i [u8]>, Error> {
361 let mkkey = parse::slicer(input);
362 let Parsed { mut input, .. } =
363 self.parse_annotation_key_leading_char(input)?;
364 loop {
365 let Parsed { value: did_consume, input: unconsumed } =
366 self.parse_annotation_key_char(input);
367 if !did_consume {
368 break;
369 }
370 input = unconsumed;
371 }
372 Ok(Parsed { value: mkkey(input), input })
373 }
374
375 fn parse_annotation_values<'i>(
380 &self,
381 input: &'i [u8],
382 ) -> Result<Parsed<'i, ()>, Error> {
383 let Parsed { mut input, .. } = self.parse_annotation_value(input)?;
384 while input.starts_with(b"-") {
385 input = &input[1..];
386 let Parsed { input: unconsumed, .. } =
387 self.parse_annotation_value(input)?;
388 input = unconsumed;
389 }
390 Ok(Parsed { value: (), input })
391 }
392
393 fn parse_annotation_value<'i>(
394 &self,
395 input: &'i [u8],
396 ) -> Result<Parsed<'i, &'i [u8]>, Error> {
397 let mkvalue = parse::slicer(input);
398 let Parsed { mut input, .. } =
399 self.parse_annotation_value_leading_char(input)?;
400 loop {
401 let Parsed { value: did_consume, input: unconsumed } =
402 self.parse_annotation_value_char(input);
403 if !did_consume {
404 break;
405 }
406 input = unconsumed;
407 }
408 let value = mkvalue(input);
409 Ok(Parsed { value, input })
410 }
411
412 fn parse_tz_annotation_leading_char<'i>(
413 &self,
414 input: &'i [u8],
415 ) -> Result<Parsed<'i, ()>, Error> {
416 if input.is_empty() {
417 return Err(err!(
418 "expected the start of an RFC 9557 annotation or IANA \
419 time zone component name, but found end of input instead",
420 ));
421 }
422 if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
423 return Err(err!(
424 "expected ASCII alphabetic byte (or underscore or period) \
425 at the start of an RFC 9557 annotation or time zone \
426 component name, but found {:?} instead",
427 escape::Byte(input[0]),
428 ));
429 }
430 Ok(Parsed { value: (), input: &input[1..] })
431 }
432
433 fn parse_tz_annotation_char<'i>(
434 &self,
435 input: &'i [u8],
436 ) -> Parsed<'i, bool> {
437 let is_tz_annotation_char = |byte| {
438 matches!(
439 byte,
440 b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
441 )
442 };
443 if input.is_empty() || !is_tz_annotation_char(input[0]) {
444 return Parsed { value: false, input };
445 }
446 Parsed { value: true, input: &input[1..] }
447 }
448
449 fn parse_annotation_key_leading_char<'i>(
450 &self,
451 input: &'i [u8],
452 ) -> Result<Parsed<'i, ()>, Error> {
453 if input.is_empty() {
454 return Err(err!(
455 "expected the start of an RFC 9557 annotation key, \
456 but found end of input instead",
457 ));
458 }
459 if !matches!(input[0], b'_' | b'a'..=b'z') {
460 return Err(err!(
461 "expected lowercase alphabetic byte (or underscore) \
462 at the start of an RFC 9557 annotation key, \
463 but found {:?} instead",
464 escape::Byte(input[0]),
465 ));
466 }
467 Ok(Parsed { value: (), input: &input[1..] })
468 }
469
470 fn parse_annotation_key_char<'i>(
471 &self,
472 input: &'i [u8],
473 ) -> Parsed<'i, bool> {
474 let is_annotation_key_char =
475 |byte| matches!(byte, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z');
476 if input.is_empty() || !is_annotation_key_char(input[0]) {
477 return Parsed { value: false, input };
478 }
479 Parsed { value: true, input: &input[1..] }
480 }
481
482 fn parse_annotation_value_leading_char<'i>(
483 &self,
484 input: &'i [u8],
485 ) -> Result<Parsed<'i, ()>, Error> {
486 if input.is_empty() {
487 return Err(err!(
488 "expected the start of an RFC 9557 annotation value, \
489 but found end of input instead",
490 ));
491 }
492 if !matches!(input[0], b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
493 return Err(err!(
494 "expected alphanumeric ASCII byte \
495 at the start of an RFC 9557 annotation value, \
496 but found {:?} instead",
497 escape::Byte(input[0]),
498 ));
499 }
500 Ok(Parsed { value: (), input: &input[1..] })
501 }
502
503 fn parse_annotation_value_char<'i>(
504 &self,
505 input: &'i [u8],
506 ) -> Parsed<'i, bool> {
507 let is_annotation_value_char =
508 |byte| matches!(byte, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
509 if input.is_empty() || !is_annotation_value_char(input[0]) {
510 return Parsed { value: false, input };
511 }
512 Parsed { value: true, input: &input[1..] }
513 }
514
515 fn parse_annotation_separator<'i>(
516 &self,
517 input: &'i [u8],
518 ) -> Result<Parsed<'i, ()>, Error> {
519 if input.is_empty() {
520 return Err(err!(
521 "expected an '=' after parsing an RFC 9557 annotation key, \
522 but found end of input instead",
523 ));
524 }
525 if input[0] != b'=' {
526 return Err(if input[0] == b'/' {
529 err!(
530 "expected an '=' after parsing an RFC 9557 annotation \
531 key, but found / instead (time zone annotations must \
532 come first)",
533 )
534 } else {
535 err!(
536 "expected an '=' after parsing an RFC 9557 annotation \
537 key, but found {:?} instead",
538 escape::Byte(input[0]),
539 )
540 });
541 }
542 Ok(Parsed { value: (), input: &input[1..] })
543 }
544
545 fn parse_annotation_close<'i>(
546 &self,
547 input: &'i [u8],
548 ) -> Result<Parsed<'i, ()>, Error> {
549 if input.is_empty() {
550 return Err(err!(
551 "expected an ']' after parsing an RFC 9557 annotation key \
552 and value, but found end of input instead",
553 ));
554 }
555 if input[0] != b']' {
556 return Err(err!(
557 "expected an ']' after parsing an RFC 9557 annotation key \
558 and value, but found {:?} instead",
559 escape::Byte(input[0]),
560 ));
561 }
562 Ok(Parsed { value: (), input: &input[1..] })
563 }
564
565 fn parse_tz_annotation_close<'i>(
566 &self,
567 input: &'i [u8],
568 ) -> Result<Parsed<'i, ()>, Error> {
569 if input.is_empty() {
570 return Err(err!(
571 "expected an ']' after parsing an RFC 9557 time zone \
572 annotation, but found end of input instead",
573 ));
574 }
575 if input[0] != b']' {
576 return Err(err!(
577 "expected an ']' after parsing an RFC 9557 time zone \
578 annotation, but found {:?} instead",
579 escape::Byte(input[0]),
580 ));
581 }
582 Ok(Parsed { value: (), input: &input[1..] })
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
591 fn ok_time_zone() {
592 if crate::tz::db().is_definitively_empty() {
593 return;
594 }
595
596 let p = |input| {
597 Parser::new()
598 .parse(input)
599 .unwrap()
600 .value
601 .to_time_zone_annotation()
602 .unwrap()
603 .map(|ann| (ann.to_time_zone().unwrap(), ann.is_critical()))
604 };
605
606 insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
607 Some(
608 (
609 TimeZone(
610 TZif(
611 "America/New_York",
612 ),
613 ),
614 false,
615 ),
616 )
617 "###);
618 insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
619 Some(
620 (
621 TimeZone(
622 TZif(
623 "America/New_York",
624 ),
625 ),
626 true,
627 ),
628 )
629 "###);
630 insta::assert_debug_snapshot!(p(b"[america/new_york]"), @r###"
631 Some(
632 (
633 TimeZone(
634 TZif(
635 "America/New_York",
636 ),
637 ),
638 false,
639 ),
640 )
641 "###);
642 insta::assert_debug_snapshot!(p(b"[+25:59]"), @r###"
643 Some(
644 (
645 TimeZone(
646 25:59:00,
647 ),
648 false,
649 ),
650 )
651 "###);
652 insta::assert_debug_snapshot!(p(b"[-25:59]"), @r###"
653 Some(
654 (
655 TimeZone(
656 -25:59:00,
657 ),
658 false,
659 ),
660 )
661 "###);
662 }
663
664 #[test]
665 fn ok_empty() {
666 let p = |input| Parser::new().parse(input).unwrap();
667
668 insta::assert_debug_snapshot!(p(b""), @r###"
669 Parsed {
670 value: ParsedAnnotations {
671 input: "",
672 time_zone: None,
673 },
674 input: "",
675 }
676 "###);
677 insta::assert_debug_snapshot!(p(b"blah"), @r###"
678 Parsed {
679 value: ParsedAnnotations {
680 input: "",
681 time_zone: None,
682 },
683 input: "blah",
684 }
685 "###);
686 }
687
688 #[test]
689 fn ok_unsupported() {
690 let p = |input| Parser::new().parse(input).unwrap();
691
692 insta::assert_debug_snapshot!(
693 p(b"[u-ca=chinese]"),
694 @r###"
695 Parsed {
696 value: ParsedAnnotations {
697 input: "[u-ca=chinese]",
698 time_zone: None,
699 },
700 input: "",
701 }
702 "###,
703 );
704 insta::assert_debug_snapshot!(
705 p(b"[u-ca=chinese-japanese]"),
706 @r###"
707 Parsed {
708 value: ParsedAnnotations {
709 input: "[u-ca=chinese-japanese]",
710 time_zone: None,
711 },
712 input: "",
713 }
714 "###,
715 );
716 insta::assert_debug_snapshot!(
717 p(b"[u-ca=chinese-japanese-russian]"),
718 @r###"
719 Parsed {
720 value: ParsedAnnotations {
721 input: "[u-ca=chinese-japanese-russian]",
722 time_zone: None,
723 },
724 input: "",
725 }
726 "###,
727 );
728 }
729
730 #[test]
731 fn ok_iana() {
732 let p = |input| Parser::new().parse(input).unwrap();
733
734 insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
735 Parsed {
736 value: ParsedAnnotations {
737 input: "[America/New_York]",
738 time_zone: Some(
739 Named {
740 critical: false,
741 name: "America/New_York",
742 },
743 ),
744 },
745 input: "",
746 }
747 "###);
748 insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
749 Parsed {
750 value: ParsedAnnotations {
751 input: "[!America/New_York]",
752 time_zone: Some(
753 Named {
754 critical: true,
755 name: "America/New_York",
756 },
757 ),
758 },
759 input: "",
760 }
761 "###);
762 insta::assert_debug_snapshot!(p(b"[UTC]"), @r###"
763 Parsed {
764 value: ParsedAnnotations {
765 input: "[UTC]",
766 time_zone: Some(
767 Named {
768 critical: false,
769 name: "UTC",
770 },
771 ),
772 },
773 input: "",
774 }
775 "###);
776 insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r###"
777 Parsed {
778 value: ParsedAnnotations {
779 input: "[.._foo_../.0+-]",
780 time_zone: Some(
781 Named {
782 critical: false,
783 name: ".._foo_../.0+-",
784 },
785 ),
786 },
787 input: "",
788 }
789 "###);
790 }
791
792 #[test]
793 fn ok_offset() {
794 let p = |input| Parser::new().parse(input).unwrap();
795
796 insta::assert_debug_snapshot!(p(b"[-00]"), @r###"
797 Parsed {
798 value: ParsedAnnotations {
799 input: "[-00]",
800 time_zone: Some(
801 Offset {
802 critical: false,
803 offset: ParsedOffset {
804 kind: Numeric(
805 -00,
806 ),
807 },
808 },
809 ),
810 },
811 input: "",
812 }
813 "###);
814 insta::assert_debug_snapshot!(p(b"[+00]"), @r###"
815 Parsed {
816 value: ParsedAnnotations {
817 input: "[+00]",
818 time_zone: Some(
819 Offset {
820 critical: false,
821 offset: ParsedOffset {
822 kind: Numeric(
823 +00,
824 ),
825 },
826 },
827 ),
828 },
829 input: "",
830 }
831 "###);
832 insta::assert_debug_snapshot!(p(b"[-05]"), @r###"
833 Parsed {
834 value: ParsedAnnotations {
835 input: "[-05]",
836 time_zone: Some(
837 Offset {
838 critical: false,
839 offset: ParsedOffset {
840 kind: Numeric(
841 -05,
842 ),
843 },
844 },
845 ),
846 },
847 input: "",
848 }
849 "###);
850 insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r###"
851 Parsed {
852 value: ParsedAnnotations {
853 input: "[!+05:12]",
854 time_zone: Some(
855 Offset {
856 critical: true,
857 offset: ParsedOffset {
858 kind: Numeric(
859 +05:12,
860 ),
861 },
862 },
863 ),
864 },
865 input: "",
866 }
867 "###);
868 }
869
870 #[test]
871 fn ok_iana_unsupported() {
872 let p = |input| Parser::new().parse(input).unwrap();
873
874 insta::assert_debug_snapshot!(
875 p(b"[America/New_York][u-ca=chinese-japanese-russian]"),
876 @r###"
877 Parsed {
878 value: ParsedAnnotations {
879 input: "[America/New_York][u-ca=chinese-japanese-russian]",
880 time_zone: Some(
881 Named {
882 critical: false,
883 name: "America/New_York",
884 },
885 ),
886 },
887 input: "",
888 }
889 "###,
890 );
891 }
892
893 #[test]
894 fn err_iana() {
895 insta::assert_snapshot!(
896 Parser::new().parse(b"[0/Foo]").unwrap_err(),
897 @r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
898 );
899 insta::assert_snapshot!(
900 Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
901 @r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "0" instead"###,
902 );
903 }
904
905 #[test]
906 fn err_offset() {
907 insta::assert_snapshot!(
908 Parser::new().parse(b"[+").unwrap_err(),
909 @r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###,
910 );
911 insta::assert_snapshot!(
912 Parser::new().parse(b"[+26]").unwrap_err(),
913 @r###"failed to parse hours in UTC numeric offset "+26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
914 );
915 insta::assert_snapshot!(
916 Parser::new().parse(b"[-26]").unwrap_err(),
917 @r###"failed to parse hours in UTC numeric offset "-26]": offset hours are not valid: parameter 'hours' with value 26 is not in the required range of 0..=25"###,
918 );
919 insta::assert_snapshot!(
920 Parser::new().parse(b"[+05:12:34]").unwrap_err(),
921 @r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###,
922 );
923 insta::assert_snapshot!(
924 Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
925 @r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###,
926 );
927 }
928
929 #[test]
930 fn err_critical_unsupported() {
931 insta::assert_snapshot!(
932 Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
933 @r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###,
934 );
935 }
936
937 #[test]
938 fn err_key_leading_char() {
939 insta::assert_snapshot!(
940 Parser::new().parse(b"[").unwrap_err(),
941 @"expected the start of an RFC 9557 annotation or IANA time zone component name, but found end of input instead",
942 );
943 insta::assert_snapshot!(
944 Parser::new().parse(b"[&").unwrap_err(),
945 @r###"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found "&" instead"###,
946 );
947 insta::assert_snapshot!(
948 Parser::new().parse(b"[Foo][").unwrap_err(),
949 @"expected the start of an RFC 9557 annotation key, but found end of input instead",
950 );
951 insta::assert_snapshot!(
952 Parser::new().parse(b"[Foo][&").unwrap_err(),
953 @r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###,
954 );
955 }
956
957 #[test]
958 fn err_separator() {
959 insta::assert_snapshot!(
960 Parser::new().parse(b"[abc").unwrap_err(),
961 @"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
962 );
963 insta::assert_snapshot!(
964 Parser::new().parse(b"[_abc").unwrap_err(),
965 @"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
966 );
967 insta::assert_snapshot!(
968 Parser::new().parse(b"[abc^").unwrap_err(),
969 @r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" instead"###,
970 );
971 insta::assert_snapshot!(
972 Parser::new().parse(b"[Foo][abc").unwrap_err(),
973 @"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
974 );
975 insta::assert_snapshot!(
976 Parser::new().parse(b"[Foo][_abc").unwrap_err(),
977 @"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
978 );
979 insta::assert_snapshot!(
980 Parser::new().parse(b"[Foo][abc^").unwrap_err(),
981 @r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###,
982 );
983 }
984
985 #[test]
986 fn err_value() {
987 insta::assert_snapshot!(
988 Parser::new().parse(b"[abc=").unwrap_err(),
989 @"expected the start of an RFC 9557 annotation value, but found end of input instead",
990 );
991 insta::assert_snapshot!(
992 Parser::new().parse(b"[_abc=").unwrap_err(),
993 @"expected the start of an RFC 9557 annotation value, but found end of input instead",
994 );
995 insta::assert_snapshot!(
996 Parser::new().parse(b"[abc=^").unwrap_err(),
997 @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "^" instead"###,
998 );
999 insta::assert_snapshot!(
1000 Parser::new().parse(b"[abc=]").unwrap_err(),
1001 @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###,
1002 );
1003 }
1004
1005 #[test]
1006 fn err_close() {
1007 insta::assert_snapshot!(
1008 Parser::new().parse(b"[abc=123").unwrap_err(),
1009 @"expected an ']' after parsing an RFC 9557 annotation key and value, but found end of input instead",
1010 );
1011 insta::assert_snapshot!(
1012 Parser::new().parse(b"[abc=123*").unwrap_err(),
1013 @r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###,
1014 );
1015 }
1016
1017 #[cfg(feature = "std")]
1018 #[test]
1019 fn err_time_zone_db_lookup() {
1020 if crate::tz::db().is_definitively_empty() {
1023 return;
1024 }
1025
1026 let p = |input| {
1027 Parser::new()
1028 .parse(input)
1029 .unwrap()
1030 .value
1031 .to_time_zone_annotation()
1032 .unwrap()
1033 .unwrap()
1034 .to_time_zone()
1035 .unwrap_err()
1036 };
1037
1038 insta::assert_snapshot!(
1039 p(b"[Foo]"),
1040 @"failed to find time zone `Foo` in time zone database",
1041 );
1042 }
1043
1044 #[test]
1045 fn err_repeated_time_zone() {
1046 let p = |input| Parser::new().parse(input).unwrap_err();
1047 insta::assert_snapshot!(
1048 p(b"[america/new_york][america/new_york]"),
1049 @"expected an '=' after parsing an RFC 9557 annotation key, but found / instead (time zone annotations must come first)",
1050 );
1051 }
1052}