tz/parse/
tz_string.rs

1//! Functions used for parsing a TZ string.
2
3use crate::error::{TzError, TzStringError};
4use crate::timezone::*;
5use crate::utils::*;
6
7use std::num::ParseIntError;
8use std::str::{self, FromStr};
9
10/// Parse integer from a slice of bytes
11fn parse_int<T: FromStr<Err = ParseIntError>>(bytes: &[u8]) -> Result<T, TzStringError> {
12    Ok(str::from_utf8(bytes)?.parse()?)
13}
14
15/// Parse time zone designation
16fn parse_time_zone_designation<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], TzStringError> {
17    let unquoted = if cursor.remaining().first() == Some(&b'<') {
18        cursor.read_exact(1)?;
19        let unquoted = cursor.read_until(|&x| x == b'>')?;
20        cursor.read_exact(1)?;
21        unquoted
22    } else {
23        cursor.read_while(u8::is_ascii_alphabetic)?
24    };
25
26    Ok(unquoted)
27}
28
29/// Parse hours, minutes and seconds
30fn parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), TzStringError> {
31    let hour = parse_int(cursor.read_while(u8::is_ascii_digit)?)?;
32
33    let mut minute = 0;
34    let mut second = 0;
35
36    if cursor.read_optional_tag(b":")? {
37        minute = parse_int(cursor.read_while(u8::is_ascii_digit)?)?;
38
39        if cursor.read_optional_tag(b":")? {
40            second = parse_int(cursor.read_while(u8::is_ascii_digit)?)?;
41        }
42    }
43
44    Ok((hour, minute, second))
45}
46
47/// Parse signed hours, minutes and seconds
48fn parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), TzStringError> {
49    let mut sign = 1;
50    if let Some(&c @ b'+') | Some(&c @ b'-') = cursor.remaining().first() {
51        cursor.read_exact(1)?;
52        if c == b'-' {
53            sign = -1;
54        }
55    }
56
57    let (hour, minute, second) = parse_hhmmss(cursor)?;
58    Ok((sign, hour, minute, second))
59}
60
61/// Parse time zone offset
62fn parse_offset(cursor: &mut Cursor) -> Result<i32, TzStringError> {
63    let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
64
65    if !(0..=24).contains(&hour) {
66        return Err(TzStringError::InvalidTzString("invalid offset hour"));
67    }
68    if !(0..=59).contains(&minute) {
69        return Err(TzStringError::InvalidTzString("invalid offset minute"));
70    }
71    if !(0..=59).contains(&second) {
72        return Err(TzStringError::InvalidTzString("invalid offset second"));
73    }
74
75    Ok(sign * (hour * 3600 + minute * 60 + second))
76}
77
78/// Parse transition rule day
79fn parse_rule_day(cursor: &mut Cursor) -> Result<RuleDay, TzError> {
80    match cursor.remaining().first() {
81        Some(b'J') => {
82            cursor.read_exact(1)?;
83            Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(parse_int(cursor.read_while(u8::is_ascii_digit)?)?)?))
84        }
85        Some(b'M') => {
86            cursor.read_exact(1)?;
87
88            let month = parse_int(cursor.read_while(u8::is_ascii_digit)?)?;
89            cursor.read_tag(b".")?;
90            let week = parse_int(cursor.read_while(u8::is_ascii_digit)?)?;
91            cursor.read_tag(b".")?;
92            let week_day = parse_int(cursor.read_while(u8::is_ascii_digit)?)?;
93
94            Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?))
95        }
96        _ => Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(parse_int(cursor.read_while(u8::is_ascii_digit)?)?)?)),
97    }
98}
99
100/// Parse transition rule time
101fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, TzStringError> {
102    let (hour, minute, second) = parse_hhmmss(cursor)?;
103
104    if !(0..=24).contains(&hour) {
105        return Err(TzStringError::InvalidTzString("invalid day time hour"));
106    }
107    if !(0..=59).contains(&minute) {
108        return Err(TzStringError::InvalidTzString("invalid day time minute"));
109    }
110    if !(0..=59).contains(&second) {
111        return Err(TzStringError::InvalidTzString("invalid day time second"));
112    }
113
114    Ok(hour * 3600 + minute * 60 + second)
115}
116
117/// Parse transition rule time with TZ string extensions
118fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, TzStringError> {
119    let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
120
121    if !(-167..=167).contains(&hour) {
122        return Err(TzStringError::InvalidTzString("invalid day time hour"));
123    }
124    if !(0..=59).contains(&minute) {
125        return Err(TzStringError::InvalidTzString("invalid day time minute"));
126    }
127    if !(0..=59).contains(&second) {
128        return Err(TzStringError::InvalidTzString("invalid day time second"));
129    }
130
131    Ok(sign * (hour * 3600 + minute * 60 + second))
132}
133
134/// Parse transition rule
135fn parse_rule_block(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(RuleDay, i32), TzError> {
136    let date = parse_rule_day(cursor)?;
137
138    let time = if cursor.read_optional_tag(b"/")? {
139        if use_string_extensions {
140            parse_rule_time_extended(cursor)?
141        } else {
142            parse_rule_time(cursor)?
143        }
144    } else {
145        2 * 3600
146    };
147
148    Ok((date, time))
149}
150
151/// Parse a POSIX TZ string containing a time zone description, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
152///
153/// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used.
154///
155pub(crate) fn parse_posix_tz(tz_string: &[u8], use_string_extensions: bool) -> Result<TransitionRule, TzError> {
156    let mut cursor = Cursor::new(tz_string);
157
158    let std_time_zone = Some(parse_time_zone_designation(&mut cursor)?);
159    let std_offset = parse_offset(&mut cursor)?;
160
161    if cursor.is_empty() {
162        return Ok(TransitionRule::Fixed(LocalTimeType::new(-std_offset, false, std_time_zone)?));
163    }
164
165    let dst_time_zone = Some(parse_time_zone_designation(&mut cursor)?);
166
167    let dst_offset = match cursor.remaining().first() {
168        Some(&b',') => std_offset - 3600,
169        Some(_) => parse_offset(&mut cursor)?,
170        None => return Err(TzStringError::UnsupportedTzString("DST start and end rules must be provided").into()),
171    };
172
173    if cursor.is_empty() {
174        return Err(TzStringError::UnsupportedTzString("DST start and end rules must be provided").into());
175    }
176
177    cursor.read_tag(b",")?;
178    let (dst_start, dst_start_time) = parse_rule_block(&mut cursor, use_string_extensions)?;
179
180    cursor.read_tag(b",")?;
181    let (dst_end, dst_end_time) = parse_rule_block(&mut cursor, use_string_extensions)?;
182
183    if !cursor.is_empty() {
184        return Err(TzStringError::InvalidTzString("remaining data after parsing TZ string").into());
185    }
186
187    Ok(TransitionRule::Alternate(AlternateTime::new(
188        LocalTimeType::new(-std_offset, false, std_time_zone)?,
189        LocalTimeType::new(-dst_offset, true, dst_time_zone)?,
190        dst_start,
191        dst_start_time,
192        dst_end,
193        dst_end_time,
194    )?))
195}
196
197#[cfg(test)]
198mod test {
199    use super::*;
200
201    #[test]
202    fn test_no_dst() -> Result<(), TzError> {
203        let tz_string = b"HST10";
204
205        let transition_rule = parse_posix_tz(tz_string, false)?;
206        let transition_rule_result = TransitionRule::Fixed(LocalTimeType::new(-36000, false, Some(b"HST"))?);
207
208        assert_eq!(transition_rule, transition_rule_result);
209
210        Ok(())
211    }
212
213    #[test]
214    fn test_quoted() -> Result<(), TzError> {
215        let tz_string = b"<-03>+3<+03>-3,J1,J365";
216
217        let transition_rule = parse_posix_tz(tz_string, false)?;
218
219        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
220            LocalTimeType::new(-10800, false, Some(b"-03"))?,
221            LocalTimeType::new(10800, true, Some(b"+03"))?,
222            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?),
223            7200,
224            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
225            7200,
226        )?);
227
228        assert_eq!(transition_rule, transition_rule_result);
229
230        Ok(())
231    }
232
233    #[test]
234    fn test_full() -> Result<(), TzError> {
235        let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00";
236
237        let transition_rule = parse_posix_tz(tz_string, false)?;
238
239        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
240            LocalTimeType::new(43200, false, Some(b"NZST"))?,
241            LocalTimeType::new(46800, true, Some(b"NZDT"))?,
242            RuleDay::MonthWeekDay(MonthWeekDay::new(10, 1, 0)?),
243            7200,
244            RuleDay::MonthWeekDay(MonthWeekDay::new(3, 3, 0)?),
245            7200,
246        )?);
247
248        assert_eq!(transition_rule, transition_rule_result);
249
250        Ok(())
251    }
252
253    #[test]
254    fn test_negative_dst() -> Result<(), TzError> {
255        let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1";
256
257        let transition_rule = parse_posix_tz(tz_string, false)?;
258
259        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
260            LocalTimeType::new(3600, false, Some(b"IST"))?,
261            LocalTimeType::new(0, true, Some(b"GMT"))?,
262            RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?),
263            7200,
264            RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?),
265            3600,
266        )?);
267
268        assert_eq!(transition_rule, transition_rule_result);
269
270        Ok(())
271    }
272
273    #[test]
274    fn test_negative_hour() -> Result<(), TzError> {
275        let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1";
276
277        assert!(parse_posix_tz(tz_string, false).is_err());
278
279        let transition_rule = parse_posix_tz(tz_string, true)?;
280
281        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
282            LocalTimeType::new(-10800, false, Some(b"-03"))?,
283            LocalTimeType::new(-7200, true, Some(b"-02"))?,
284            RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?),
285            -7200,
286            RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?),
287            -3600,
288        )?);
289
290        assert_eq!(transition_rule, transition_rule_result);
291
292        Ok(())
293    }
294
295    #[test]
296    fn test_all_year_dst() -> Result<(), TzError> {
297        let tz_string = b"EST5EDT,0/0,J365/25";
298
299        assert!(parse_posix_tz(tz_string, false).is_err());
300
301        let transition_rule = parse_posix_tz(tz_string, true)?;
302
303        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
304            LocalTimeType::new(-18000, false, Some(b"EST"))?,
305            LocalTimeType::new(-14400, true, Some(b"EDT"))?,
306            RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?),
307            0,
308            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
309            90000,
310        )?);
311
312        assert_eq!(transition_rule, transition_rule_result);
313
314        Ok(())
315    }
316
317    #[test]
318    fn test_min_dst_offset() -> Result<(), TzError> {
319        let tz_string = b"STD24:59:59DST,J1,J365";
320
321        let transition_rule = parse_posix_tz(tz_string, false)?;
322
323        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
324            LocalTimeType::new(-89999, false, Some(b"STD"))?,
325            LocalTimeType::new(-86399, true, Some(b"DST"))?,
326            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?),
327            7200,
328            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
329            7200,
330        )?);
331
332        assert_eq!(transition_rule, transition_rule_result);
333
334        Ok(())
335    }
336
337    #[test]
338    fn test_max_dst_offset() -> Result<(), TzError> {
339        let tz_string = b"STD-24:59:59DST,J1,J365";
340
341        let transition_rule = parse_posix_tz(tz_string, false)?;
342
343        let transition_rule_result = TransitionRule::Alternate(AlternateTime::new(
344            LocalTimeType::new(89999, false, Some(b"STD"))?,
345            LocalTimeType::new(93599, true, Some(b"DST"))?,
346            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?),
347            7200,
348            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
349            7200,
350        )?);
351
352        assert_eq!(transition_rule, transition_rule_result);
353
354        Ok(())
355    }
356
357    #[test]
358    fn test_error() -> Result<(), TzError> {
359        assert!(matches!(parse_posix_tz(b"IST-1GMT0", false), Err(TzError::TzStringError(TzStringError::UnsupportedTzString(_)))));
360        assert!(matches!(parse_posix_tz(b"EET-2EEST", false), Err(TzError::TzStringError(TzStringError::UnsupportedTzString(_)))));
361
362        Ok(())
363    }
364}