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