1 | //! Types related to a time zone. |
2 | |
3 | use std::fs::{self, File}; |
4 | use std::io::{self, Read}; |
5 | use std::path::{Path, PathBuf}; |
6 | use std::{cmp::Ordering, fmt, str}; |
7 | |
8 | use super::rule::{AlternateTime, TransitionRule}; |
9 | use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY}; |
10 | |
11 | /// Time zone |
12 | #[derive (Debug, Clone, Eq, PartialEq)] |
13 | pub(crate) struct TimeZone { |
14 | /// List of transitions |
15 | transitions: Vec<Transition>, |
16 | /// List of local time types (cannot be empty) |
17 | local_time_types: Vec<LocalTimeType>, |
18 | /// List of leap seconds |
19 | leap_seconds: Vec<LeapSecond>, |
20 | /// Extra transition rule applicable after the last transition |
21 | extra_rule: Option<TransitionRule>, |
22 | } |
23 | |
24 | impl TimeZone { |
25 | /// Returns local time zone. |
26 | /// |
27 | /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. |
28 | pub(crate) fn local(env_tz: Option<&str>) -> Result<Self, Error> { |
29 | match env_tz { |
30 | Some(tz) => Self::from_posix_tz(tz), |
31 | None => Self::from_posix_tz("localtime" ), |
32 | } |
33 | } |
34 | |
35 | /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). |
36 | fn from_posix_tz(tz_string: &str) -> Result<Self, Error> { |
37 | if tz_string.is_empty() { |
38 | return Err(Error::InvalidTzString("empty TZ string" )); |
39 | } |
40 | |
41 | if tz_string == "localtime" { |
42 | return Self::from_tz_data(&fs::read("/etc/localtime" )?); |
43 | } |
44 | |
45 | // attributes are not allowed on if blocks in Rust 1.38 |
46 | #[cfg (target_os = "android" )] |
47 | { |
48 | if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) { |
49 | return Self::from_tz_data(&bytes); |
50 | } |
51 | } |
52 | |
53 | let mut chars = tz_string.chars(); |
54 | if chars.next() == Some(':' ) { |
55 | return Self::from_file(&mut find_tz_file(chars.as_str())?); |
56 | } |
57 | |
58 | if let Ok(mut file) = find_tz_file(tz_string) { |
59 | return Self::from_file(&mut file); |
60 | } |
61 | |
62 | // TZ string extensions are not allowed |
63 | let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace()); |
64 | let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?; |
65 | Self::new( |
66 | vec![], |
67 | match rule { |
68 | TransitionRule::Fixed(local_time_type) => vec![local_time_type], |
69 | TransitionRule::Alternate(AlternateTime { std, dst, .. }) => vec![std, dst], |
70 | }, |
71 | vec![], |
72 | Some(rule), |
73 | ) |
74 | } |
75 | |
76 | /// Construct a time zone |
77 | pub(super) fn new( |
78 | transitions: Vec<Transition>, |
79 | local_time_types: Vec<LocalTimeType>, |
80 | leap_seconds: Vec<LeapSecond>, |
81 | extra_rule: Option<TransitionRule>, |
82 | ) -> Result<Self, Error> { |
83 | let new = Self { transitions, local_time_types, leap_seconds, extra_rule }; |
84 | new.as_ref().validate()?; |
85 | Ok(new) |
86 | } |
87 | |
88 | /// Construct a time zone from the contents of a time zone file |
89 | fn from_file(file: &mut File) -> Result<Self, Error> { |
90 | let mut bytes = Vec::new(); |
91 | file.read_to_end(&mut bytes)?; |
92 | Self::from_tz_data(&bytes) |
93 | } |
94 | |
95 | /// Construct a time zone from the contents of a time zone file |
96 | /// |
97 | /// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536). |
98 | pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> { |
99 | parser::parse(bytes) |
100 | } |
101 | |
102 | /// Construct a time zone with the specified UTC offset in seconds |
103 | fn fixed(ut_offset: i32) -> Result<Self, Error> { |
104 | Ok(Self { |
105 | transitions: Vec::new(), |
106 | local_time_types: vec![LocalTimeType::with_offset(ut_offset)?], |
107 | leap_seconds: Vec::new(), |
108 | extra_rule: None, |
109 | }) |
110 | } |
111 | |
112 | /// Construct the time zone associated to UTC |
113 | pub(crate) fn utc() -> Self { |
114 | Self { |
115 | transitions: Vec::new(), |
116 | local_time_types: vec![LocalTimeType::UTC], |
117 | leap_seconds: Vec::new(), |
118 | extra_rule: None, |
119 | } |
120 | } |
121 | |
122 | /// Find the local time type associated to the time zone at the specified Unix time in seconds |
123 | pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> { |
124 | self.as_ref().find_local_time_type(unix_time) |
125 | } |
126 | |
127 | // should we pass NaiveDateTime all the way through to this fn? |
128 | pub(crate) fn find_local_time_type_from_local( |
129 | &self, |
130 | local_time: i64, |
131 | year: i32, |
132 | ) -> Result<crate::LocalResult<LocalTimeType>, Error> { |
133 | self.as_ref().find_local_time_type_from_local(local_time, year) |
134 | } |
135 | |
136 | /// Returns a reference to the time zone |
137 | fn as_ref(&self) -> TimeZoneRef { |
138 | TimeZoneRef { |
139 | transitions: &self.transitions, |
140 | local_time_types: &self.local_time_types, |
141 | leap_seconds: &self.leap_seconds, |
142 | extra_rule: &self.extra_rule, |
143 | } |
144 | } |
145 | } |
146 | |
147 | /// Reference to a time zone |
148 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
149 | pub(crate) struct TimeZoneRef<'a> { |
150 | /// List of transitions |
151 | transitions: &'a [Transition], |
152 | /// List of local time types (cannot be empty) |
153 | local_time_types: &'a [LocalTimeType], |
154 | /// List of leap seconds |
155 | leap_seconds: &'a [LeapSecond], |
156 | /// Extra transition rule applicable after the last transition |
157 | extra_rule: &'a Option<TransitionRule>, |
158 | } |
159 | |
160 | impl<'a> TimeZoneRef<'a> { |
161 | /// Find the local time type associated to the time zone at the specified Unix time in seconds |
162 | pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, Error> { |
163 | let extra_rule = match self.transitions.last() { |
164 | None => match self.extra_rule { |
165 | Some(extra_rule) => extra_rule, |
166 | None => return Ok(&self.local_time_types[0]), |
167 | }, |
168 | Some(last_transition) => { |
169 | let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) { |
170 | Ok(unix_leap_time) => unix_leap_time, |
171 | Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)), |
172 | Err(err) => return Err(err), |
173 | }; |
174 | |
175 | if unix_leap_time >= last_transition.unix_leap_time { |
176 | match self.extra_rule { |
177 | Some(extra_rule) => extra_rule, |
178 | None => { |
179 | // RFC 8536 3.2: |
180 | // "Local time for timestamps on or after the last transition is |
181 | // specified by the TZ string in the footer (Section 3.3) if present |
182 | // and nonempty; otherwise, it is unspecified." |
183 | // |
184 | // Older versions of macOS (1.12 and before?) have TZif file with a |
185 | // missing TZ string, and use the offset given by the last transition. |
186 | return Ok( |
187 | &self.local_time_types[last_transition.local_time_type_index] |
188 | ); |
189 | } |
190 | } |
191 | } else { |
192 | let index = match self |
193 | .transitions |
194 | .binary_search_by_key(&unix_leap_time, Transition::unix_leap_time) |
195 | { |
196 | Ok(x) => x + 1, |
197 | Err(x) => x, |
198 | }; |
199 | |
200 | let local_time_type_index = if index > 0 { |
201 | self.transitions[index - 1].local_time_type_index |
202 | } else { |
203 | 0 |
204 | }; |
205 | return Ok(&self.local_time_types[local_time_type_index]); |
206 | } |
207 | } |
208 | }; |
209 | |
210 | match extra_rule.find_local_time_type(unix_time) { |
211 | Ok(local_time_type) => Ok(local_time_type), |
212 | Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)), |
213 | err => err, |
214 | } |
215 | } |
216 | |
217 | pub(crate) fn find_local_time_type_from_local( |
218 | &self, |
219 | local_time: i64, |
220 | year: i32, |
221 | ) -> Result<crate::LocalResult<LocalTimeType>, Error> { |
222 | // #TODO: this is wrong as we need 'local_time_to_local_leap_time ? |
223 | // but ... does the local time even include leap seconds ?? |
224 | // let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) { |
225 | // Ok(unix_leap_time) => unix_leap_time, |
226 | // Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)), |
227 | // Err(err) => return Err(err), |
228 | // }; |
229 | let local_leap_time = local_time; |
230 | |
231 | // if we have at least one transition, |
232 | // we must check _all_ of them, incase of any Overlapping (LocalResult::Ambiguous) or Skipping (LocalResult::None) transitions |
233 | let offset_after_last = if !self.transitions.is_empty() { |
234 | let mut prev = self.local_time_types[0]; |
235 | |
236 | for transition in self.transitions { |
237 | let after_ltt = self.local_time_types[transition.local_time_type_index]; |
238 | |
239 | // the end and start here refers to where the time starts prior to the transition |
240 | // and where it ends up after. not the temporal relationship. |
241 | let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset); |
242 | let transition_start = transition.unix_leap_time + i64::from(prev.ut_offset); |
243 | |
244 | match transition_start.cmp(&transition_end) { |
245 | Ordering::Greater => { |
246 | // bakwards transition, eg from DST to regular |
247 | // this means a given local time could have one of two possible offsets |
248 | if local_leap_time < transition_end { |
249 | return Ok(crate::LocalResult::Single(prev)); |
250 | } else if local_leap_time >= transition_end |
251 | && local_leap_time <= transition_start |
252 | { |
253 | if prev.ut_offset < after_ltt.ut_offset { |
254 | return Ok(crate::LocalResult::Ambiguous(prev, after_ltt)); |
255 | } else { |
256 | return Ok(crate::LocalResult::Ambiguous(after_ltt, prev)); |
257 | } |
258 | } |
259 | } |
260 | Ordering::Equal => { |
261 | // should this ever happen? presumably we have to handle it anyway. |
262 | if local_leap_time < transition_start { |
263 | return Ok(crate::LocalResult::Single(prev)); |
264 | } else if local_leap_time == transition_end { |
265 | if prev.ut_offset < after_ltt.ut_offset { |
266 | return Ok(crate::LocalResult::Ambiguous(prev, after_ltt)); |
267 | } else { |
268 | return Ok(crate::LocalResult::Ambiguous(after_ltt, prev)); |
269 | } |
270 | } |
271 | } |
272 | Ordering::Less => { |
273 | // forwards transition, eg from regular to DST |
274 | // this means that times that are skipped are invalid local times |
275 | if local_leap_time <= transition_start { |
276 | return Ok(crate::LocalResult::Single(prev)); |
277 | } else if local_leap_time < transition_end { |
278 | return Ok(crate::LocalResult::None); |
279 | } else if local_leap_time == transition_end { |
280 | return Ok(crate::LocalResult::Single(after_ltt)); |
281 | } |
282 | } |
283 | } |
284 | |
285 | // try the next transition, we are fully after this one |
286 | prev = after_ltt; |
287 | } |
288 | |
289 | prev |
290 | } else { |
291 | self.local_time_types[0] |
292 | }; |
293 | |
294 | if let Some(extra_rule) = self.extra_rule { |
295 | match extra_rule.find_local_time_type_from_local(local_time, year) { |
296 | Ok(local_time_type) => Ok(local_time_type), |
297 | Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)), |
298 | err => err, |
299 | } |
300 | } else { |
301 | Ok(crate::LocalResult::Single(offset_after_last)) |
302 | } |
303 | } |
304 | |
305 | /// Check time zone inputs |
306 | fn validate(&self) -> Result<(), Error> { |
307 | // Check local time types |
308 | let local_time_types_size = self.local_time_types.len(); |
309 | if local_time_types_size == 0 { |
310 | return Err(Error::TimeZone("list of local time types must not be empty" )); |
311 | } |
312 | |
313 | // Check transitions |
314 | let mut i_transition = 0; |
315 | while i_transition < self.transitions.len() { |
316 | if self.transitions[i_transition].local_time_type_index >= local_time_types_size { |
317 | return Err(Error::TimeZone("invalid local time type index" )); |
318 | } |
319 | |
320 | if i_transition + 1 < self.transitions.len() |
321 | && self.transitions[i_transition].unix_leap_time |
322 | >= self.transitions[i_transition + 1].unix_leap_time |
323 | { |
324 | return Err(Error::TimeZone("invalid transition" )); |
325 | } |
326 | |
327 | i_transition += 1; |
328 | } |
329 | |
330 | // Check leap seconds |
331 | if !(self.leap_seconds.is_empty() |
332 | || self.leap_seconds[0].unix_leap_time >= 0 |
333 | && self.leap_seconds[0].correction.saturating_abs() == 1) |
334 | { |
335 | return Err(Error::TimeZone("invalid leap second" )); |
336 | } |
337 | |
338 | let min_interval = SECONDS_PER_28_DAYS - 1; |
339 | |
340 | let mut i_leap_second = 0; |
341 | while i_leap_second < self.leap_seconds.len() { |
342 | if i_leap_second + 1 < self.leap_seconds.len() { |
343 | let x0 = &self.leap_seconds[i_leap_second]; |
344 | let x1 = &self.leap_seconds[i_leap_second + 1]; |
345 | |
346 | let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time); |
347 | let abs_diff_correction = |
348 | x1.correction.saturating_sub(x0.correction).saturating_abs(); |
349 | |
350 | if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) { |
351 | return Err(Error::TimeZone("invalid leap second" )); |
352 | } |
353 | } |
354 | i_leap_second += 1; |
355 | } |
356 | |
357 | // Check extra rule |
358 | let (extra_rule, last_transition) = match (&self.extra_rule, self.transitions.last()) { |
359 | (Some(rule), Some(trans)) => (rule, trans), |
360 | _ => return Ok(()), |
361 | }; |
362 | |
363 | let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index]; |
364 | let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) { |
365 | Ok(unix_time) => unix_time, |
366 | Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)), |
367 | Err(err) => return Err(err), |
368 | }; |
369 | |
370 | let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) { |
371 | Ok(rule_local_time_type) => rule_local_time_type, |
372 | Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)), |
373 | Err(err) => return Err(err), |
374 | }; |
375 | |
376 | let check = last_local_time_type.ut_offset == rule_local_time_type.ut_offset |
377 | && last_local_time_type.is_dst == rule_local_time_type.is_dst |
378 | && match (&last_local_time_type.name, &rule_local_time_type.name) { |
379 | (Some(x), Some(y)) => x.equal(y), |
380 | (None, None) => true, |
381 | _ => false, |
382 | }; |
383 | |
384 | if !check { |
385 | return Err(Error::TimeZone( |
386 | "extra transition rule is inconsistent with the last transition" , |
387 | )); |
388 | } |
389 | |
390 | Ok(()) |
391 | } |
392 | |
393 | /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone |
394 | const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, Error> { |
395 | let mut unix_leap_time = unix_time; |
396 | |
397 | let mut i = 0; |
398 | while i < self.leap_seconds.len() { |
399 | let leap_second = &self.leap_seconds[i]; |
400 | |
401 | if unix_leap_time < leap_second.unix_leap_time { |
402 | break; |
403 | } |
404 | |
405 | unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) { |
406 | Some(unix_leap_time) => unix_leap_time, |
407 | None => return Err(Error::OutOfRange("out of range operation" )), |
408 | }; |
409 | |
410 | i += 1; |
411 | } |
412 | |
413 | Ok(unix_leap_time) |
414 | } |
415 | |
416 | /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone |
417 | fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, Error> { |
418 | if unix_leap_time == i64::min_value() { |
419 | return Err(Error::OutOfRange("out of range operation" )); |
420 | } |
421 | |
422 | let index = match self |
423 | .leap_seconds |
424 | .binary_search_by_key(&(unix_leap_time - 1), LeapSecond::unix_leap_time) |
425 | { |
426 | Ok(x) => x + 1, |
427 | Err(x) => x, |
428 | }; |
429 | |
430 | let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 }; |
431 | |
432 | match unix_leap_time.checked_sub(correction as i64) { |
433 | Some(unix_time) => Ok(unix_time), |
434 | None => Err(Error::OutOfRange("out of range operation" )), |
435 | } |
436 | } |
437 | |
438 | /// The UTC time zone |
439 | const UTC: TimeZoneRef<'static> = TimeZoneRef { |
440 | transitions: &[], |
441 | local_time_types: &[LocalTimeType::UTC], |
442 | leap_seconds: &[], |
443 | extra_rule: &None, |
444 | }; |
445 | } |
446 | |
447 | /// Transition of a TZif file |
448 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
449 | pub(super) struct Transition { |
450 | /// Unix leap time |
451 | unix_leap_time: i64, |
452 | /// Index specifying the local time type of the transition |
453 | local_time_type_index: usize, |
454 | } |
455 | |
456 | impl Transition { |
457 | /// Construct a TZif file transition |
458 | pub(super) const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self { |
459 | Self { unix_leap_time, local_time_type_index } |
460 | } |
461 | |
462 | /// Returns Unix leap time |
463 | const fn unix_leap_time(&self) -> i64 { |
464 | self.unix_leap_time |
465 | } |
466 | } |
467 | |
468 | /// Leap second of a TZif file |
469 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
470 | pub(super) struct LeapSecond { |
471 | /// Unix leap time |
472 | unix_leap_time: i64, |
473 | /// Leap second correction |
474 | correction: i32, |
475 | } |
476 | |
477 | impl LeapSecond { |
478 | /// Construct a TZif file leap second |
479 | pub(super) const fn new(unix_leap_time: i64, correction: i32) -> Self { |
480 | Self { unix_leap_time, correction } |
481 | } |
482 | |
483 | /// Returns Unix leap time |
484 | const fn unix_leap_time(&self) -> i64 { |
485 | self.unix_leap_time |
486 | } |
487 | } |
488 | |
489 | /// ASCII-encoded fixed-capacity string, used for storing time zone names |
490 | #[derive (Copy, Clone, Eq, PartialEq)] |
491 | struct TimeZoneName { |
492 | /// Length-prefixed string buffer |
493 | bytes: [u8; 8], |
494 | } |
495 | |
496 | impl TimeZoneName { |
497 | /// Construct a time zone name |
498 | /// |
499 | /// man tzfile(5): |
500 | /// Time zone designations should consist of at least three (3) and no more than six (6) ASCII |
501 | /// characters from the set of alphanumerics, “-”, and “+”. This is for compatibility with |
502 | /// POSIX requirements for time zone abbreviations. |
503 | fn new(input: &[u8]) -> Result<Self, Error> { |
504 | let len = input.len(); |
505 | |
506 | if !(3..=7).contains(&len) { |
507 | return Err(Error::LocalTimeType( |
508 | "time zone name must have between 3 and 7 characters" , |
509 | )); |
510 | } |
511 | |
512 | let mut bytes = [0; 8]; |
513 | bytes[0] = input.len() as u8; |
514 | |
515 | let mut i = 0; |
516 | while i < len { |
517 | let b = input[i]; |
518 | match b { |
519 | b'0' ..=b'9' | b'A' ..=b'Z' | b'a' ..=b'z' | b'+' | b'-' => {} |
520 | _ => return Err(Error::LocalTimeType("invalid characters in time zone name" )), |
521 | } |
522 | |
523 | bytes[i + 1] = b; |
524 | i += 1; |
525 | } |
526 | |
527 | Ok(Self { bytes }) |
528 | } |
529 | |
530 | /// Returns time zone name as a byte slice |
531 | fn as_bytes(&self) -> &[u8] { |
532 | match self.bytes[0] { |
533 | 3 => &self.bytes[1..4], |
534 | 4 => &self.bytes[1..5], |
535 | 5 => &self.bytes[1..6], |
536 | 6 => &self.bytes[1..7], |
537 | 7 => &self.bytes[1..8], |
538 | _ => unreachable!(), |
539 | } |
540 | } |
541 | |
542 | /// Check if two time zone names are equal |
543 | fn equal(&self, other: &Self) -> bool { |
544 | self.bytes == other.bytes |
545 | } |
546 | } |
547 | |
548 | impl AsRef<str> for TimeZoneName { |
549 | fn as_ref(&self) -> &str { |
550 | // SAFETY: ASCII is valid UTF-8 |
551 | unsafe { str::from_utf8_unchecked(self.as_bytes()) } |
552 | } |
553 | } |
554 | |
555 | impl fmt::Debug for TimeZoneName { |
556 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
557 | self.as_ref().fmt(f) |
558 | } |
559 | } |
560 | |
561 | /// Local time type associated to a time zone |
562 | #[derive (Debug, Copy, Clone, Eq, PartialEq)] |
563 | pub(crate) struct LocalTimeType { |
564 | /// Offset from UTC in seconds |
565 | pub(super) ut_offset: i32, |
566 | /// Daylight Saving Time indicator |
567 | is_dst: bool, |
568 | /// Time zone name |
569 | name: Option<TimeZoneName>, |
570 | } |
571 | |
572 | impl LocalTimeType { |
573 | /// Construct a local time type |
574 | pub(super) fn new(ut_offset: i32, is_dst: bool, name: Option<&[u8]>) -> Result<Self, Error> { |
575 | if ut_offset == i32::min_value() { |
576 | return Err(Error::LocalTimeType("invalid UTC offset" )); |
577 | } |
578 | |
579 | let name = match name { |
580 | Some(name) => TimeZoneName::new(name)?, |
581 | None => return Ok(Self { ut_offset, is_dst, name: None }), |
582 | }; |
583 | |
584 | Ok(Self { ut_offset, is_dst, name: Some(name) }) |
585 | } |
586 | |
587 | /// Construct a local time type with the specified UTC offset in seconds |
588 | pub(super) const fn with_offset(ut_offset: i32) -> Result<Self, Error> { |
589 | if ut_offset == i32::min_value() { |
590 | return Err(Error::LocalTimeType("invalid UTC offset" )); |
591 | } |
592 | |
593 | Ok(Self { ut_offset, is_dst: false, name: None }) |
594 | } |
595 | |
596 | /// Returns offset from UTC in seconds |
597 | pub(crate) const fn offset(&self) -> i32 { |
598 | self.ut_offset |
599 | } |
600 | |
601 | /// Returns daylight saving time indicator |
602 | pub(super) const fn is_dst(&self) -> bool { |
603 | self.is_dst |
604 | } |
605 | |
606 | pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None }; |
607 | } |
608 | |
609 | /// Open the TZif file corresponding to a TZ string |
610 | fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> { |
611 | // Don't check system timezone directories on non-UNIX platforms |
612 | #[cfg (not(unix))] |
613 | return Ok(File::open(path)?); |
614 | |
615 | #[cfg (unix)] |
616 | { |
617 | let path: &Path = path.as_ref(); |
618 | if path.is_absolute() { |
619 | return Ok(File::open(path)?); |
620 | } |
621 | |
622 | for folder: &&str in &ZONE_INFO_DIRECTORIES { |
623 | if let Ok(file: File) = File::open(path:PathBuf::from(folder).join(path)) { |
624 | return Ok(file); |
625 | } |
626 | } |
627 | |
628 | Err(Error::Io(io::ErrorKind::NotFound.into())) |
629 | } |
630 | } |
631 | |
632 | // Possible system timezone directories |
633 | #[cfg (unix)] |
634 | const ZONE_INFO_DIRECTORIES: [&str; 4] = |
635 | ["/usr/share/zoneinfo" , "/share/zoneinfo" , "/etc/zoneinfo" , "/usr/share/lib/zoneinfo" ]; |
636 | |
637 | /// Number of seconds in one week |
638 | pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK; |
639 | /// Number of seconds in 28 days |
640 | const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28; |
641 | |
642 | #[cfg (test)] |
643 | mod tests { |
644 | use super::super::Error; |
645 | use super::{LeapSecond, LocalTimeType, TimeZone, TimeZoneName, Transition, TransitionRule}; |
646 | |
647 | #[test ] |
648 | fn test_no_dst() -> Result<(), Error> { |
649 | let tz_string = b"HST10" ; |
650 | let transition_rule = TransitionRule::from_tz_string(tz_string, false)?; |
651 | assert_eq!(transition_rule, LocalTimeType::new(-36000, false, Some(b"HST" ))?.into()); |
652 | Ok(()) |
653 | } |
654 | |
655 | #[test ] |
656 | fn test_error() -> Result<(), Error> { |
657 | assert!(matches!( |
658 | TransitionRule::from_tz_string(b"IST-1GMT0" , false), |
659 | Err(Error::UnsupportedTzString(_)) |
660 | )); |
661 | assert!(matches!( |
662 | TransitionRule::from_tz_string(b"EET-2EEST" , false), |
663 | Err(Error::UnsupportedTzString(_)) |
664 | )); |
665 | |
666 | Ok(()) |
667 | } |
668 | |
669 | #[test ] |
670 | fn test_v1_file_with_leap_seconds() -> Result<(), Error> { |
671 | let bytes = b"TZif \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC \0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0" ; |
672 | |
673 | let time_zone = TimeZone::from_tz_data(bytes)?; |
674 | |
675 | let time_zone_result = TimeZone::new( |
676 | Vec::new(), |
677 | vec![LocalTimeType::new(0, false, Some(b"UTC" ))?], |
678 | vec![ |
679 | LeapSecond::new(78796800, 1), |
680 | LeapSecond::new(94694401, 2), |
681 | LeapSecond::new(126230402, 3), |
682 | LeapSecond::new(157766403, 4), |
683 | LeapSecond::new(189302404, 5), |
684 | LeapSecond::new(220924805, 6), |
685 | LeapSecond::new(252460806, 7), |
686 | LeapSecond::new(283996807, 8), |
687 | LeapSecond::new(315532808, 9), |
688 | LeapSecond::new(362793609, 10), |
689 | LeapSecond::new(394329610, 11), |
690 | LeapSecond::new(425865611, 12), |
691 | LeapSecond::new(489024012, 13), |
692 | LeapSecond::new(567993613, 14), |
693 | LeapSecond::new(631152014, 15), |
694 | LeapSecond::new(662688015, 16), |
695 | LeapSecond::new(709948816, 17), |
696 | LeapSecond::new(741484817, 18), |
697 | LeapSecond::new(773020818, 19), |
698 | LeapSecond::new(820454419, 20), |
699 | LeapSecond::new(867715220, 21), |
700 | LeapSecond::new(915148821, 22), |
701 | LeapSecond::new(1136073622, 23), |
702 | LeapSecond::new(1230768023, 24), |
703 | LeapSecond::new(1341100824, 25), |
704 | LeapSecond::new(1435708825, 26), |
705 | LeapSecond::new(1483228826, 27), |
706 | ], |
707 | None, |
708 | )?; |
709 | |
710 | assert_eq!(time_zone, time_zone_result); |
711 | |
712 | Ok(()) |
713 | } |
714 | |
715 | #[test ] |
716 | fn test_v2_file() -> Result<(), Error> { |
717 | let bytes = b"TZif2 \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT \0HST \0HDT \0HWT \0HPT \0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2 \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT \0HST \0HDT \0HWT \0HPT \0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10 \x0a" ; |
718 | |
719 | let time_zone = TimeZone::from_tz_data(bytes)?; |
720 | |
721 | let time_zone_result = TimeZone::new( |
722 | vec![ |
723 | Transition::new(-2334101314, 1), |
724 | Transition::new(-1157283000, 2), |
725 | Transition::new(-1155436200, 1), |
726 | Transition::new(-880198200, 3), |
727 | Transition::new(-769395600, 4), |
728 | Transition::new(-765376200, 1), |
729 | Transition::new(-712150200, 5), |
730 | ], |
731 | vec![ |
732 | LocalTimeType::new(-37886, false, Some(b"LMT" ))?, |
733 | LocalTimeType::new(-37800, false, Some(b"HST" ))?, |
734 | LocalTimeType::new(-34200, true, Some(b"HDT" ))?, |
735 | LocalTimeType::new(-34200, true, Some(b"HWT" ))?, |
736 | LocalTimeType::new(-34200, true, Some(b"HPT" ))?, |
737 | LocalTimeType::new(-36000, false, Some(b"HST" ))?, |
738 | ], |
739 | Vec::new(), |
740 | Some(TransitionRule::from(LocalTimeType::new(-36000, false, Some(b"HST" ))?)), |
741 | )?; |
742 | |
743 | assert_eq!(time_zone, time_zone_result); |
744 | |
745 | assert_eq!( |
746 | *time_zone.find_local_time_type(-1156939200)?, |
747 | LocalTimeType::new(-34200, true, Some(b"HDT" ))? |
748 | ); |
749 | assert_eq!( |
750 | *time_zone.find_local_time_type(1546300800)?, |
751 | LocalTimeType::new(-36000, false, Some(b"HST" ))? |
752 | ); |
753 | |
754 | Ok(()) |
755 | } |
756 | |
757 | #[test ] |
758 | fn test_no_tz_string() -> Result<(), Error> { |
759 | // Guayaquil from macOS 10.11 |
760 | let bytes = b"TZif \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\x02\0\0\0\0\0\0\0\x01\0\0\0\x02\0\0\0\x08\xb6\xa4B \x18\x01\xff\xff\xb6h \0\0\xff\xff\xb9\xb0\0\x04QMT \0ECT \0\0\0\0\0" ; |
761 | |
762 | let time_zone = TimeZone::from_tz_data(bytes)?; |
763 | dbg!(&time_zone); |
764 | |
765 | let time_zone_result = TimeZone::new( |
766 | vec![Transition::new(-1230749160, 1)], |
767 | vec![ |
768 | LocalTimeType::new(-18840, false, Some(b"QMT" ))?, |
769 | LocalTimeType::new(-18000, false, Some(b"ECT" ))?, |
770 | ], |
771 | Vec::new(), |
772 | None, |
773 | )?; |
774 | |
775 | assert_eq!(time_zone, time_zone_result); |
776 | |
777 | assert_eq!( |
778 | *time_zone.find_local_time_type(-1500000000)?, |
779 | LocalTimeType::new(-18840, false, Some(b"QMT" ))? |
780 | ); |
781 | assert_eq!( |
782 | *time_zone.find_local_time_type(0)?, |
783 | LocalTimeType::new(-18000, false, Some(b"ECT" ))? |
784 | ); |
785 | |
786 | Ok(()) |
787 | } |
788 | |
789 | #[test ] |
790 | fn test_tz_ascii_str() -> Result<(), Error> { |
791 | assert!(matches!(TimeZoneName::new(b"" ), Err(Error::LocalTimeType(_)))); |
792 | assert!(matches!(TimeZoneName::new(b"A" ), Err(Error::LocalTimeType(_)))); |
793 | assert!(matches!(TimeZoneName::new(b"AB" ), Err(Error::LocalTimeType(_)))); |
794 | assert_eq!(TimeZoneName::new(b"CET" )?.as_bytes(), b"CET" ); |
795 | assert_eq!(TimeZoneName::new(b"CHADT" )?.as_bytes(), b"CHADT" ); |
796 | assert_eq!(TimeZoneName::new(b"abcdefg" )?.as_bytes(), b"abcdefg" ); |
797 | assert_eq!(TimeZoneName::new(b"UTC+02" )?.as_bytes(), b"UTC+02" ); |
798 | assert_eq!(TimeZoneName::new(b"-1230" )?.as_bytes(), b"-1230" ); |
799 | assert!(matches!(TimeZoneName::new("−0330" .as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212) |
800 | assert!(matches!(TimeZoneName::new(b" \x00123" ), Err(Error::LocalTimeType(_)))); |
801 | assert!(matches!(TimeZoneName::new(b"12345678" ), Err(Error::LocalTimeType(_)))); |
802 | assert!(matches!(TimeZoneName::new(b"GMT \0\0\0" ), Err(Error::LocalTimeType(_)))); |
803 | |
804 | Ok(()) |
805 | } |
806 | |
807 | #[test ] |
808 | fn test_time_zone() -> Result<(), Error> { |
809 | let utc = LocalTimeType::UTC; |
810 | let cet = LocalTimeType::with_offset(3600)?; |
811 | |
812 | let utc_local_time_types = vec![utc]; |
813 | let fixed_extra_rule = TransitionRule::from(cet); |
814 | |
815 | let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?; |
816 | let time_zone_2 = |
817 | TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?; |
818 | let time_zone_3 = |
819 | TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?; |
820 | let time_zone_4 = TimeZone::new( |
821 | vec![Transition::new(i32::min_value().into(), 0), Transition::new(0, 1)], |
822 | vec![utc, cet], |
823 | Vec::new(), |
824 | Some(fixed_extra_rule), |
825 | )?; |
826 | |
827 | assert_eq!(*time_zone_1.find_local_time_type(0)?, utc); |
828 | assert_eq!(*time_zone_2.find_local_time_type(0)?, cet); |
829 | |
830 | assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc); |
831 | assert_eq!(*time_zone_3.find_local_time_type(0)?, utc); |
832 | |
833 | assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc); |
834 | assert_eq!(*time_zone_4.find_local_time_type(0)?, cet); |
835 | |
836 | let time_zone_err = TimeZone::new( |
837 | vec![Transition::new(0, 0)], |
838 | utc_local_time_types, |
839 | vec![], |
840 | Some(fixed_extra_rule), |
841 | ); |
842 | assert!(time_zone_err.is_err()); |
843 | |
844 | Ok(()) |
845 | } |
846 | |
847 | #[test ] |
848 | fn test_time_zone_from_posix_tz() -> Result<(), Error> { |
849 | #[cfg (unix)] |
850 | { |
851 | // if the TZ var is set, this essentially _overrides_ the |
852 | // time set by the localtime symlink |
853 | // so just ensure that ::local() acts as expected |
854 | // in this case |
855 | if let Ok(tz) = std::env::var("TZ" ) { |
856 | let time_zone_local = TimeZone::local(Some(tz.as_str()))?; |
857 | let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?; |
858 | assert_eq!(time_zone_local, time_zone_local_1); |
859 | } |
860 | |
861 | // `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have |
862 | // a time zone database, like for example some docker containers. |
863 | // In that case skip the test. |
864 | if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC" ) { |
865 | assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); |
866 | } |
867 | } |
868 | |
869 | assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25" ).is_err()); |
870 | assert!(TimeZone::from_posix_tz("" ).is_err()); |
871 | |
872 | Ok(()) |
873 | } |
874 | |
875 | #[test ] |
876 | fn test_leap_seconds() -> Result<(), Error> { |
877 | let time_zone = TimeZone::new( |
878 | Vec::new(), |
879 | vec![LocalTimeType::new(0, false, Some(b"UTC" ))?], |
880 | vec![ |
881 | LeapSecond::new(78796800, 1), |
882 | LeapSecond::new(94694401, 2), |
883 | LeapSecond::new(126230402, 3), |
884 | LeapSecond::new(157766403, 4), |
885 | LeapSecond::new(189302404, 5), |
886 | LeapSecond::new(220924805, 6), |
887 | LeapSecond::new(252460806, 7), |
888 | LeapSecond::new(283996807, 8), |
889 | LeapSecond::new(315532808, 9), |
890 | LeapSecond::new(362793609, 10), |
891 | LeapSecond::new(394329610, 11), |
892 | LeapSecond::new(425865611, 12), |
893 | LeapSecond::new(489024012, 13), |
894 | LeapSecond::new(567993613, 14), |
895 | LeapSecond::new(631152014, 15), |
896 | LeapSecond::new(662688015, 16), |
897 | LeapSecond::new(709948816, 17), |
898 | LeapSecond::new(741484817, 18), |
899 | LeapSecond::new(773020818, 19), |
900 | LeapSecond::new(820454419, 20), |
901 | LeapSecond::new(867715220, 21), |
902 | LeapSecond::new(915148821, 22), |
903 | LeapSecond::new(1136073622, 23), |
904 | LeapSecond::new(1230768023, 24), |
905 | LeapSecond::new(1341100824, 25), |
906 | LeapSecond::new(1435708825, 26), |
907 | LeapSecond::new(1483228826, 27), |
908 | ], |
909 | None, |
910 | )?; |
911 | |
912 | let time_zone_ref = time_zone.as_ref(); |
913 | |
914 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599))); |
915 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600))); |
916 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600))); |
917 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601))); |
918 | |
919 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621))); |
920 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623))); |
921 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624))); |
922 | |
923 | Ok(()) |
924 | } |
925 | |
926 | #[test ] |
927 | fn test_leap_seconds_overflow() -> Result<(), Error> { |
928 | let time_zone_err = TimeZone::new( |
929 | vec![Transition::new(i64::min_value(), 0)], |
930 | vec![LocalTimeType::UTC], |
931 | vec![LeapSecond::new(0, 1)], |
932 | Some(TransitionRule::from(LocalTimeType::UTC)), |
933 | ); |
934 | assert!(time_zone_err.is_err()); |
935 | |
936 | let time_zone = TimeZone::new( |
937 | vec![Transition::new(i64::max_value(), 0)], |
938 | vec![LocalTimeType::UTC], |
939 | vec![LeapSecond::new(0, 1)], |
940 | None, |
941 | )?; |
942 | assert!(matches!( |
943 | time_zone.find_local_time_type(i64::max_value()), |
944 | Err(Error::FindLocalTimeType(_)) |
945 | )); |
946 | |
947 | Ok(()) |
948 | } |
949 | } |
950 | |