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 Fixed(
647 25:59:00,
648 ),
649 ),
650 false,
651 ),
652 )
653 "###);
654 insta::assert_debug_snapshot!(p(b"[-25:59]"), @r###"
655 Some(
656 (
657 TimeZone(
658 Fixed(
659 -25:59:00,
660 ),
661 ),
662 false,
663 ),
664 )
665 "###);
666 }
667
668 #[test]
669 fn ok_empty() {
670 let p = |input| Parser::new().parse(input).unwrap();
671
672 insta::assert_debug_snapshot!(p(b""), @r###"
673 Parsed {
674 value: ParsedAnnotations {
675 input: "",
676 time_zone: None,
677 },
678 input: "",
679 }
680 "###);
681 insta::assert_debug_snapshot!(p(b"blah"), @r###"
682 Parsed {
683 value: ParsedAnnotations {
684 input: "",
685 time_zone: None,
686 },
687 input: "blah",
688 }
689 "###);
690 }
691
692 #[test]
693 fn ok_unsupported() {
694 let p = |input| Parser::new().parse(input).unwrap();
695
696 insta::assert_debug_snapshot!(
697 p(b"[u-ca=chinese]"),
698 @r###"
699 Parsed {
700 value: ParsedAnnotations {
701 input: "[u-ca=chinese]",
702 time_zone: None,
703 },
704 input: "",
705 }
706 "###,
707 );
708 insta::assert_debug_snapshot!(
709 p(b"[u-ca=chinese-japanese]"),
710 @r###"
711 Parsed {
712 value: ParsedAnnotations {
713 input: "[u-ca=chinese-japanese]",
714 time_zone: None,
715 },
716 input: "",
717 }
718 "###,
719 );
720 insta::assert_debug_snapshot!(
721 p(b"[u-ca=chinese-japanese-russian]"),
722 @r###"
723 Parsed {
724 value: ParsedAnnotations {
725 input: "[u-ca=chinese-japanese-russian]",
726 time_zone: None,
727 },
728 input: "",
729 }
730 "###,
731 );
732 }
733
734 #[test]
735 fn ok_iana() {
736 let p = |input| Parser::new().parse(input).unwrap();
737
738 insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
739 Parsed {
740 value: ParsedAnnotations {
741 input: "[America/New_York]",
742 time_zone: Some(
743 Named {
744 critical: false,
745 name: "America/New_York",
746 },
747 ),
748 },
749 input: "",
750 }
751 "###);
752 insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
753 Parsed {
754 value: ParsedAnnotations {
755 input: "[!America/New_York]",
756 time_zone: Some(
757 Named {
758 critical: true,
759 name: "America/New_York",
760 },
761 ),
762 },
763 input: "",
764 }
765 "###);
766 insta::assert_debug_snapshot!(p(b"[UTC]"), @r###"
767 Parsed {
768 value: ParsedAnnotations {
769 input: "[UTC]",
770 time_zone: Some(
771 Named {
772 critical: false,
773 name: "UTC",
774 },
775 ),
776 },
777 input: "",
778 }
779 "###);
780 insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r###"
781 Parsed {
782 value: ParsedAnnotations {
783 input: "[.._foo_../.0+-]",
784 time_zone: Some(
785 Named {
786 critical: false,
787 name: ".._foo_../.0+-",
788 },
789 ),
790 },
791 input: "",
792 }
793 "###);
794 }
795
796 #[test]
797 fn ok_offset() {
798 let p = |input| Parser::new().parse(input).unwrap();
799
800 insta::assert_debug_snapshot!(p(b"[-00]"), @r###"
801 Parsed {
802 value: ParsedAnnotations {
803 input: "[-00]",
804 time_zone: Some(
805 Offset {
806 critical: false,
807 offset: ParsedOffset {
808 kind: Numeric(
809 -00,
810 ),
811 },
812 },
813 ),
814 },
815 input: "",
816 }
817 "###);
818 insta::assert_debug_snapshot!(p(b"[+00]"), @r###"
819 Parsed {
820 value: ParsedAnnotations {
821 input: "[+00]",
822 time_zone: Some(
823 Offset {
824 critical: false,
825 offset: ParsedOffset {
826 kind: Numeric(
827 +00,
828 ),
829 },
830 },
831 ),
832 },
833 input: "",
834 }
835 "###);
836 insta::assert_debug_snapshot!(p(b"[-05]"), @r###"
837 Parsed {
838 value: ParsedAnnotations {
839 input: "[-05]",
840 time_zone: Some(
841 Offset {
842 critical: false,
843 offset: ParsedOffset {
844 kind: Numeric(
845 -05,
846 ),
847 },
848 },
849 ),
850 },
851 input: "",
852 }
853 "###);
854 insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r###"
855 Parsed {
856 value: ParsedAnnotations {
857 input: "[!+05:12]",
858 time_zone: Some(
859 Offset {
860 critical: true,
861 offset: ParsedOffset {
862 kind: Numeric(
863 +05:12,
864 ),
865 },
866 },
867 ),
868 },
869 input: "",
870 }
871 "###);
872 }
873
874 #[test]
875 fn ok_iana_unsupported() {
876 let p = |input| Parser::new().parse(input).unwrap();
877
878 insta::assert_debug_snapshot!(
879 p(b"[America/New_York][u-ca=chinese-japanese-russian]"),
880 @r###"
881 Parsed {
882 value: ParsedAnnotations {
883 input: "[America/New_York][u-ca=chinese-japanese-russian]",
884 time_zone: Some(
885 Named {
886 critical: false,
887 name: "America/New_York",
888 },
889 ),
890 },
891 input: "",
892 }
893 "###,
894 );
895 }
896
897 #[test]
898 fn err_iana() {
899 insta::assert_snapshot!(
900 Parser::new().parse(b"[0/Foo]").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 insta::assert_snapshot!(
904 Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
905 @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"###,
906 );
907 }
908
909 #[test]
910 fn err_offset() {
911 insta::assert_snapshot!(
912 Parser::new().parse(b"[+").unwrap_err(),
913 @r###"failed to parse hours in UTC numeric offset "+": expected two digit hour after sign, but found end of input"###,
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"[-26]").unwrap_err(),
921 @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"###,
922 );
923 insta::assert_snapshot!(
924 Parser::new().parse(b"[+05:12:34]").unwrap_err(),
925 @r###"subminute precision for UTC numeric offset "+05:12:34]" is not enabled in this context (must provide only integral minutes)"###,
926 );
927 insta::assert_snapshot!(
928 Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
929 @r###"subminute precision for UTC numeric offset "+05:12:34.123456789]" is not enabled in this context (must provide only integral minutes)"###,
930 );
931 }
932
933 #[test]
934 fn err_critical_unsupported() {
935 insta::assert_snapshot!(
936 Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
937 @r###"found unsupported RFC 9557 annotation with key "u-ca" with the critical flag ('!') set"###,
938 );
939 }
940
941 #[test]
942 fn err_key_leading_char() {
943 insta::assert_snapshot!(
944 Parser::new().parse(b"[").unwrap_err(),
945 @"expected the start of an RFC 9557 annotation or IANA time zone component name, but found end of input instead",
946 );
947 insta::assert_snapshot!(
948 Parser::new().parse(b"[&").unwrap_err(),
949 @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"###,
950 );
951 insta::assert_snapshot!(
952 Parser::new().parse(b"[Foo][").unwrap_err(),
953 @"expected the start of an RFC 9557 annotation key, but found end of input instead",
954 );
955 insta::assert_snapshot!(
956 Parser::new().parse(b"[Foo][&").unwrap_err(),
957 @r###"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found "&" instead"###,
958 );
959 }
960
961 #[test]
962 fn err_separator() {
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 @"expected an ']' after parsing an RFC 9557 time zone annotation, but found end of input instead",
970 );
971 insta::assert_snapshot!(
972 Parser::new().parse(b"[abc^").unwrap_err(),
973 @r###"expected an ']' after parsing an RFC 9557 time zone annotation, but found "^" 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 @"expected an '=' after parsing an RFC 9557 annotation key, but found end of input instead",
982 );
983 insta::assert_snapshot!(
984 Parser::new().parse(b"[Foo][abc^").unwrap_err(),
985 @r###"expected an '=' after parsing an RFC 9557 annotation key, but found "^" instead"###,
986 );
987 }
988
989 #[test]
990 fn err_value() {
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 @"expected the start of an RFC 9557 annotation value, but found end of input 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 insta::assert_snapshot!(
1004 Parser::new().parse(b"[abc=]").unwrap_err(),
1005 @r###"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found "]" instead"###,
1006 );
1007 }
1008
1009 #[test]
1010 fn err_close() {
1011 insta::assert_snapshot!(
1012 Parser::new().parse(b"[abc=123").unwrap_err(),
1013 @"expected an ']' after parsing an RFC 9557 annotation key and value, but found end of input instead",
1014 );
1015 insta::assert_snapshot!(
1016 Parser::new().parse(b"[abc=123*").unwrap_err(),
1017 @r###"expected an ']' after parsing an RFC 9557 annotation key and value, but found "*" instead"###,
1018 );
1019 }
1020
1021 #[cfg(feature = "std")]
1022 #[test]
1023 fn err_time_zone_db_lookup() {
1024 if crate::tz::db().is_definitively_empty() {
1027 return;
1028 }
1029
1030 let p = |input| {
1031 Parser::new()
1032 .parse(input)
1033 .unwrap()
1034 .value
1035 .to_time_zone_annotation()
1036 .unwrap()
1037 .unwrap()
1038 .to_time_zone()
1039 .unwrap_err()
1040 };
1041
1042 insta::assert_snapshot!(
1043 p(b"[Foo]"),
1044 @"failed to find time zone `Foo` in time zone database",
1045 );
1046 }
1047
1048 #[test]
1049 fn err_repeated_time_zone() {
1050 let p = |input| Parser::new().parse(input).unwrap_err();
1051 insta::assert_snapshot!(
1052 p(b"[america/new_york][america/new_york]"),
1053 @"expected an '=' after parsing an RFC 9557 annotation key, but found / instead (time zone annotations must come first)",
1054 );
1055 }
1056}