1use crate::error::{TzError, TzStringError};
4use crate::timezone::*;
5use crate::utils::*;
6
7use std::num::ParseIntError;
8use std::str::{self, FromStr};
9
10fn parse_int<T: FromStr<Err = ParseIntError>>(bytes: &[u8]) -> Result<T, TzStringError> {
12 Ok(str::from_utf8(bytes)?.parse()?)
13}
14
15fn 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
29fn 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
47fn 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
61fn 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
78fn 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
100fn 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
117fn 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
134fn 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
151pub(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}