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