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