use crate::{
error::{fmt::rfc9557::Error as E, Error},
fmt::{
offset::{self, ParsedOffset},
temporal::{TimeZoneAnnotation, TimeZoneAnnotationKind},
Parsed,
},
util::parse,
};
#[derive(Debug)]
pub(crate) struct ParsedAnnotations<'i> {
time_zone: Option<ParsedTimeZone<'i>>,
}
impl<'i> ParsedAnnotations<'i> {
pub(crate) fn none() -> ParsedAnnotations<'static> {
ParsedAnnotations { time_zone: None }
}
pub(crate) fn to_time_zone_annotation(
&self,
) -> Result<Option<TimeZoneAnnotation<'i>>, Error> {
let Some(ref parsed) = self.time_zone else { return Ok(None) };
Ok(Some(parsed.to_time_zone_annotation()?))
}
}
impl<'i> core::fmt::Display for ParsedAnnotations<'i> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
if let Some(ref tz) = self.time_zone {
core::fmt::Display::fmt(tz, f)?;
}
Ok(())
}
}
#[derive(Debug)]
enum ParsedTimeZone<'i> {
Named {
critical: bool,
name: &'i str,
},
Offset {
critical: bool,
offset: ParsedOffset,
},
}
impl<'i> ParsedTimeZone<'i> {
pub(crate) fn to_time_zone_annotation(
&self,
) -> Result<TimeZoneAnnotation<'i>, Error> {
let (kind, critical) = match *self {
ParsedTimeZone::Named { name, critical } => {
let kind = TimeZoneAnnotationKind::from(name);
(kind, critical)
}
ParsedTimeZone::Offset { ref offset, critical } => {
let kind = TimeZoneAnnotationKind::Offset(offset.to_offset()?);
(kind, critical)
}
};
Ok(TimeZoneAnnotation { kind, critical })
}
}
impl<'i> core::fmt::Display for ParsedTimeZone<'i> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
ParsedTimeZone::Named { critical, name } => {
f.write_str("[")?;
if critical {
f.write_str("!")?;
}
f.write_str(name)?;
f.write_str("]")
}
ParsedTimeZone::Offset { critical, ref offset } => {
f.write_str("[")?;
if critical {
f.write_str("!")?;
}
core::fmt::Display::fmt(offset, f)?;
f.write_str("]")
}
}
}
}
#[derive(Debug)]
pub(crate) struct Parser {
_priv: (),
}
impl Parser {
pub(crate) const fn new() -> Parser {
Parser { _priv: () }
}
pub(crate) fn parse<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
let Parsed { value: time_zone, mut input } =
self.parse_time_zone_annotation(input)?;
loop {
let Parsed { value: did_consume, input: unconsumed } =
self.parse_annotation(input)?;
if !did_consume {
break;
}
input = unconsumed;
}
let value = ParsedAnnotations { time_zone };
Ok(Parsed { value, input })
}
fn parse_time_zone_annotation<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, Option<ParsedTimeZone<'i>>>, Error> {
let unconsumed = input;
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: None, input: unconsumed });
};
if first != b'[' {
return Ok(Parsed { value: None, input: unconsumed });
}
input = tail;
let mut critical = false;
if let Some(tail) = input.strip_prefix(b"!") {
critical = true;
input = tail;
}
if input.starts_with(b"+") || input.starts_with(b"-") {
const P: offset::Parser =
offset::Parser::new().zulu(false).subminute(false);
let Parsed { value: offset, input } = P.parse(input)?;
let Parsed { input, .. } =
self.parse_tz_annotation_close(input)?;
let value = Some(ParsedTimeZone::Offset { critical, offset });
return Ok(Parsed { value, input });
}
let mkiana = parse::slicer(input);
let Parsed { mut input, .. } =
self.parse_tz_annotation_iana_name(input)?;
if input.starts_with(b"=") {
return Ok(Parsed { value: None, input: unconsumed });
}
while let Some(tail) = input.strip_prefix(b"/") {
input = tail;
let Parsed { input: unconsumed, .. } =
self.parse_tz_annotation_iana_name(input)?;
input = unconsumed;
}
let iana_name = core::str::from_utf8(mkiana(input)).expect("ASCII");
let time_zone =
Some(ParsedTimeZone::Named { critical, name: iana_name });
let Parsed { input, .. } = self.parse_tz_annotation_close(input)?;
Ok(Parsed { value: time_zone, input })
}
fn parse_annotation<'i>(
&self,
mut input: &'i [u8],
) -> Result<Parsed<'i, bool>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Ok(Parsed { value: false, input });
};
if first != b'[' {
return Ok(Parsed { value: false, input });
}
input = tail;
let mut critical = false;
if let Some(tail) = input.strip_prefix(b"!") {
critical = true;
input = tail;
}
let Parsed { input, .. } = self.parse_annotation_key(input)?;
let Parsed { input, .. } = self.parse_annotation_separator(input)?;
let Parsed { input, .. } = self.parse_annotation_values(input)?;
let Parsed { input, .. } = self.parse_annotation_close(input)?;
if critical {
return Err(Error::from(E::UnsupportedAnnotationCritical));
}
Ok(Parsed { value: true, input })
}
fn parse_tz_annotation_iana_name<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, &'i [u8]>, Error> {
let mkname = parse::slicer(input);
let Parsed { mut input, .. } =
self.parse_tz_annotation_leading_char(input)?;
loop {
let Parsed { value: did_consume, input: unconsumed } =
self.parse_tz_annotation_char(input);
if !did_consume {
break;
}
input = unconsumed;
}
Ok(Parsed { value: mkname(input), input })
}
fn parse_annotation_key<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, &'i [u8]>, Error> {
let mkkey = parse::slicer(input);
let Parsed { mut input, .. } =
self.parse_annotation_key_leading_char(input)?;
loop {
let Parsed { value: did_consume, input: unconsumed } =
self.parse_annotation_key_char(input);
if !did_consume {
break;
}
input = unconsumed;
}
Ok(Parsed { value: mkkey(input), input })
}
fn parse_annotation_values<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Parsed { mut input, .. } = self.parse_annotation_value(input)?;
while let Some(tail) = input.strip_prefix(b"-") {
input = tail;
let Parsed { input: unconsumed, .. } =
self.parse_annotation_value(input)?;
input = unconsumed;
}
Ok(Parsed { value: (), input })
}
fn parse_annotation_value<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, &'i [u8]>, Error> {
let mkvalue = parse::slicer(input);
let Parsed { mut input, .. } =
self.parse_annotation_value_leading_char(input)?;
loop {
let Parsed { value: did_consume, input: unconsumed } =
self.parse_annotation_value_char(input);
if !did_consume {
break;
}
input = unconsumed;
}
let value = mkvalue(input);
Ok(Parsed { value, input })
}
fn parse_tz_annotation_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotation));
};
if !matches!(first, b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(Error::from(E::UnexpectedByteAnnotation {
byte: first,
}));
}
Ok(Parsed { value: (), input: tail })
}
fn parse_tz_annotation_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
};
if !matches!(
first,
b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
) {
return Parsed { value: false, input };
}
Parsed { value: true, input: tail }
}
fn parse_annotation_key_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationKey));
};
if !matches!(first, b'_' | b'a'..=b'z') {
return Err(Error::from(E::UnexpectedByteAnnotationKey {
byte: first,
}));
}
Ok(Parsed { value: (), input: tail })
}
fn parse_annotation_key_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
};
if !matches!(first, b'_' | b'-' | b'0'..=b'9' | b'a'..=b'z') {
return Parsed { value: false, input };
}
Parsed { value: true, input: tail }
}
fn parse_annotation_value_leading_char<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationValue));
};
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
return Err(Error::from(E::UnexpectedByteAnnotationValue {
byte: first,
}));
}
Ok(Parsed { value: (), input: tail })
}
fn parse_annotation_value_char<'i>(
&self,
input: &'i [u8],
) -> Parsed<'i, bool> {
let Some((&first, tail)) = input.split_first() else {
return Parsed { value: false, input };
};
if !matches!(first, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z') {
return Parsed { value: false, input };
}
Parsed { value: true, input: tail }
}
fn parse_annotation_separator<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationSeparator));
};
if first != b'=' {
return Err(Error::from(if first == b'/' {
E::UnexpectedSlashAnnotationSeparator
} else {
E::UnexpectedByteAnnotationSeparator { byte: first }
}));
}
Ok(Parsed { value: (), input: tail })
}
fn parse_annotation_close<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputAnnotationClose));
};
if first != b']' {
return Err(Error::from(E::UnexpectedByteAnnotationClose {
byte: first,
}));
}
Ok(Parsed { value: (), input: tail })
}
fn parse_tz_annotation_close<'i>(
&self,
input: &'i [u8],
) -> Result<Parsed<'i, ()>, Error> {
let Some((&first, tail)) = input.split_first() else {
return Err(Error::from(E::EndOfInputTzAnnotationClose));
};
if first != b']' {
return Err(Error::from(E::UnexpectedByteTzAnnotationClose {
byte: first,
}));
}
Ok(Parsed { value: (), input: tail })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ok_time_zone() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |input| {
Parser::new()
.parse(input)
.unwrap()
.value
.to_time_zone_annotation()
.unwrap()
.map(|ann| (ann.to_time_zone().unwrap(), ann.is_critical()))
};
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r###"
Some(
(
TimeZone(
TZif(
"America/New_York",
),
),
false,
),
)
"###);
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r###"
Some(
(
TimeZone(
TZif(
"America/New_York",
),
),
true,
),
)
"###);
insta::assert_debug_snapshot!(p(b"[america/new_york]"), @r###"
Some(
(
TimeZone(
TZif(
"America/New_York",
),
),
false,
),
)
"###);
insta::assert_debug_snapshot!(p(b"[+25:59]"), @r###"
Some(
(
TimeZone(
25:59:00,
),
false,
),
)
"###);
insta::assert_debug_snapshot!(p(b"[-25:59]"), @r###"
Some(
(
TimeZone(
-25:59:00,
),
false,
),
)
"###);
}
#[test]
fn ok_empty() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b""), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: None,
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"blah"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: None,
},
input: "blah",
}
"#);
}
#[test]
fn ok_unsupported() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(
p(b"[u-ca=chinese]"),
@r#"
Parsed {
value: ParsedAnnotations {
time_zone: None,
},
input: "",
}
"#,
);
insta::assert_debug_snapshot!(
p(b"[u-ca=chinese-japanese]"),
@r#"
Parsed {
value: ParsedAnnotations {
time_zone: None,
},
input: "",
}
"#,
);
insta::assert_debug_snapshot!(
p(b"[u-ca=chinese-japanese-russian]"),
@r#"
Parsed {
value: ParsedAnnotations {
time_zone: None,
},
input: "",
}
"#,
);
}
#[test]
fn ok_iana() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b"[America/New_York]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Named {
critical: false,
name: "America/New_York",
},
),
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"[!America/New_York]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Named {
critical: true,
name: "America/New_York",
},
),
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"[UTC]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Named {
critical: false,
name: "UTC",
},
),
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"[.._foo_../.0+-]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Named {
critical: false,
name: ".._foo_../.0+-",
},
),
},
input: "",
}
"#);
}
#[test]
fn ok_offset() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(p(b"[-00]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Offset {
critical: false,
offset: ParsedOffset {
kind: Numeric(
-00,
),
},
},
),
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"[+00]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Offset {
critical: false,
offset: ParsedOffset {
kind: Numeric(
+00,
),
},
},
),
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"[-05]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Offset {
critical: false,
offset: ParsedOffset {
kind: Numeric(
-05,
),
},
},
),
},
input: "",
}
"#);
insta::assert_debug_snapshot!(p(b"[!+05:12]"), @r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Offset {
critical: true,
offset: ParsedOffset {
kind: Numeric(
+05:12,
),
},
},
),
},
input: "",
}
"#);
}
#[test]
fn ok_iana_unsupported() {
let p = |input| Parser::new().parse(input).unwrap();
insta::assert_debug_snapshot!(
p(b"[America/New_York][u-ca=chinese-japanese-russian]"),
@r#"
Parsed {
value: ParsedAnnotations {
time_zone: Some(
Named {
critical: false,
name: "America/New_York",
},
),
},
input: "",
}
"#,
);
}
#[test]
fn err_iana() {
insta::assert_snapshot!(
Parser::new().parse(b"[0/Foo]").unwrap_err(),
@"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",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo/0Bar]").unwrap_err(),
@"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",
);
}
#[test]
fn err_offset() {
insta::assert_snapshot!(
Parser::new().parse(b"[+").unwrap_err(),
@"failed to parse hours in UTC numeric offset: expected two digit hour after sign, but found end of input",
);
insta::assert_snapshot!(
Parser::new().parse(b"[+26]").unwrap_err(),
@"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",
);
insta::assert_snapshot!(
Parser::new().parse(b"[-26]").unwrap_err(),
@"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",
);
insta::assert_snapshot!(
Parser::new().parse(b"[+05:12:34]").unwrap_err(),
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
);
insta::assert_snapshot!(
Parser::new().parse(b"[+05:12:34.123456789]").unwrap_err(),
@"subminute precision for UTC numeric offset is not enabled in this context (must provide only integral minutes)",
);
}
#[test]
fn err_critical_unsupported() {
insta::assert_snapshot!(
Parser::new().parse(b"[!u-ca=chinese]").unwrap_err(),
@"found unsupported RFC 9557 annotation with the critical flag (`!`) set",
);
}
#[test]
fn err_key_leading_char() {
insta::assert_snapshot!(
Parser::new().parse(b"[").unwrap_err(),
@"expected the start of an RFC 9557 annotation or IANA time zone component name, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[&").unwrap_err(),
@"expected ASCII alphabetic byte (or underscore or period) at the start of an RFC 9557 annotation or time zone component name, but found `&` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][").unwrap_err(),
@"expected the start of an RFC 9557 annotation key, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][&").unwrap_err(),
@"expected lowercase alphabetic byte (or underscore) at the start of an RFC 9557 annotation key, but found `&` instead",
);
}
#[test]
fn err_separator() {
insta::assert_snapshot!(
Parser::new().parse(b"[abc").unwrap_err(),
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[_abc").unwrap_err(),
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc^").unwrap_err(),
@"expected an `]` after parsing an RFC 9557 time zone annotation, but found `^` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][abc").unwrap_err(),
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][_abc").unwrap_err(),
@"expected an `=` after parsing an RFC 9557 annotation key, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[Foo][abc^").unwrap_err(),
@"expected an `=` after parsing an RFC 9557 annotation key, but found `^` instead",
);
}
#[test]
fn err_value() {
insta::assert_snapshot!(
Parser::new().parse(b"[abc=").unwrap_err(),
@"expected the start of an RFC 9557 annotation value, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[_abc=").unwrap_err(),
@"expected the start of an RFC 9557 annotation value, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=^").unwrap_err(),
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `^` instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=]").unwrap_err(),
@"expected alphanumeric ASCII byte at the start of an RFC 9557 annotation value, but found `]` instead",
);
}
#[test]
fn err_close() {
insta::assert_snapshot!(
Parser::new().parse(b"[abc=123").unwrap_err(),
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found end of input instead",
);
insta::assert_snapshot!(
Parser::new().parse(b"[abc=123*").unwrap_err(),
@"expected an `]` after parsing an RFC 9557 annotation key and value, but found `*` instead",
);
}
#[cfg(feature = "std")]
#[test]
fn err_time_zone_db_lookup() {
if crate::tz::db().is_definitively_empty() {
return;
}
let p = |input| {
Parser::new()
.parse(input)
.unwrap()
.value
.to_time_zone_annotation()
.unwrap()
.unwrap()
.to_time_zone()
.unwrap_err()
};
insta::assert_snapshot!(
p(b"[Foo]"),
@"failed to find time zone `Foo` in time zone database",
);
}
#[test]
fn err_repeated_time_zone() {
let p = |input| Parser::new().parse(input).unwrap_err();
insta::assert_snapshot!(
p(b"[america/new_york][america/new_york]"),
@"expected an `=` after parsing an RFC 9557 annotation key, but found `/` instead (time zone annotations must come first)",
);
}
}