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