blob: 02b6f341e230063240f93cc24c229e992a8c0f4e [file] [log] [blame]
//! Types related to a time zone.
use std::fs::{self, File};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::{cmp::Ordering, fmt, str};
use super::rule::{AlternateTime, TransitionRule};
use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY};
/// Time zone
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct TimeZone {
/// List of transitions
transitions: Vec<Transition>,
/// List of local time types (cannot be empty)
local_time_types: Vec<LocalTimeType>,
/// List of leap seconds
leap_seconds: Vec<LeapSecond>,
/// Extra transition rule applicable after the last transition
extra_rule: Option<TransitionRule>,
}
impl TimeZone {
/// Returns local time zone.
///
/// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead.
///
pub(crate) fn local(env_tz: Option<&str>) -> Result<Self, Error> {
match env_tz {
Some(tz) => Self::from_posix_tz(tz),
None => Self::from_posix_tz("localtime"),
}
}
/// 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).
fn from_posix_tz(tz_string: &str) -> Result<Self, Error> {
if tz_string.is_empty() {
return Err(Error::InvalidTzString("empty TZ string"));
}
if tz_string == "localtime" {
return Self::from_tz_data(&fs::read("/etc/localtime")?);
}
// attributes are not allowed on if blocks in Rust 1.38
#[cfg(target_os = "android")]
{
if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) {
return Self::from_tz_data(&bytes);
}
}
let mut chars = tz_string.chars();
if chars.next() == Some(':') {
return Self::from_file(&mut find_tz_file(chars.as_str())?);
}
if let Ok(mut file) = find_tz_file(tz_string) {
return Self::from_file(&mut file);
}
// TZ string extensions are not allowed
let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace());
let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?;
Self::new(
vec![],
match rule {
TransitionRule::Fixed(local_time_type) => vec![local_time_type],
TransitionRule::Alternate(AlternateTime { std, dst, .. }) => vec![std, dst],
},
vec![],
Some(rule),
)
}
/// Construct a time zone
pub(super) fn new(
transitions: Vec<Transition>,
local_time_types: Vec<LocalTimeType>,
leap_seconds: Vec<LeapSecond>,
extra_rule: Option<TransitionRule>,
) -> Result<Self, Error> {
let new = Self { transitions, local_time_types, leap_seconds, extra_rule };
new.as_ref().validate()?;
Ok(new)
}
/// Construct a time zone from the contents of a time zone file
fn from_file(file: &mut File) -> Result<Self, Error> {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Self::from_tz_data(&bytes)
}
/// Construct a time zone from the contents of a time zone file
///
/// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536).
pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> {
parser::parse(bytes)
}
/// Construct a time zone with the specified UTC offset in seconds
fn fixed(ut_offset: i32) -> Result<Self, Error> {
Ok(Self {
transitions: Vec::new(),
local_time_types: vec![LocalTimeType::with_offset(ut_offset)?],
leap_seconds: Vec::new(),
extra_rule: None,
})
}
/// Construct the time zone associated to UTC
pub(crate) fn utc() -> Self {
Self {
transitions: Vec::new(),
local_time_types: vec![LocalTimeType::UTC],
leap_seconds: Vec::new(),
extra_rule: None,
}
}
/// Find the local time type associated to the time zone at the specified Unix time in seconds
pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
self.as_ref().find_local_time_type(unix_time)
}
// should we pass NaiveDateTime all the way through to this fn?
pub(crate) fn find_local_time_type_from_local(
&self,
local_time: i64,
year: i32,
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
self.as_ref().find_local_time_type_from_local(local_time, year)
}
/// Returns a reference to the time zone
fn as_ref(&self) -> TimeZoneRef {
TimeZoneRef {
transitions: &self.transitions,
local_time_types: &self.local_time_types,
leap_seconds: &self.leap_seconds,
extra_rule: &self.extra_rule,
}
}
}
/// Reference to a time zone
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct TimeZoneRef<'a> {
/// List of transitions
transitions: &'a [Transition],
/// List of local time types (cannot be empty)
local_time_types: &'a [LocalTimeType],
/// List of leap seconds
leap_seconds: &'a [LeapSecond],
/// Extra transition rule applicable after the last transition
extra_rule: &'a Option<TransitionRule>,
}
impl<'a> TimeZoneRef<'a> {
/// Find the local time type associated to the time zone at the specified Unix time in seconds
pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, Error> {
let extra_rule = match self.transitions.last() {
None => match self.extra_rule {
Some(extra_rule) => extra_rule,
None => return Ok(&self.local_time_types[0]),
},
Some(last_transition) => {
let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) {
Ok(unix_leap_time) => unix_leap_time,
Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
Err(err) => return Err(err),
};
if unix_leap_time >= last_transition.unix_leap_time {
match self.extra_rule {
Some(extra_rule) => extra_rule,
None => {
// RFC 8536 3.2:
// "Local time for timestamps on or after the last transition is
// specified by the TZ string in the footer (Section 3.3) if present
// and nonempty; otherwise, it is unspecified."
//
// Older versions of macOS (1.12 and before?) have TZif file with a
// missing TZ string, and use the offset given by the last transition.
return Ok(
&self.local_time_types[last_transition.local_time_type_index]
);
}
}
} else {
let index = match self
.transitions
.binary_search_by_key(&unix_leap_time, Transition::unix_leap_time)
{
Ok(x) => x + 1,
Err(x) => x,
};
let local_time_type_index = if index > 0 {
self.transitions[index - 1].local_time_type_index
} else {
0
};
return Ok(&self.local_time_types[local_time_type_index]);
}
}
};
match extra_rule.find_local_time_type(unix_time) {
Ok(local_time_type) => Ok(local_time_type),
Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
err => err,
}
}
pub(crate) fn find_local_time_type_from_local(
&self,
local_time: i64,
year: i32,
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
// #TODO: this is wrong as we need 'local_time_to_local_leap_time ?
// but ... does the local time even include leap seconds ??
// let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) {
// Ok(unix_leap_time) => unix_leap_time,
// Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
// Err(err) => return Err(err),
// };
let local_leap_time = local_time;
// if we have at least one transition,
// we must check _all_ of them, incase of any Overlapping (LocalResult::Ambiguous) or Skipping (LocalResult::None) transitions
let offset_after_last = if !self.transitions.is_empty() {
let mut prev = self.local_time_types[0];
for transition in self.transitions {
let after_ltt = self.local_time_types[transition.local_time_type_index];
// the end and start here refers to where the time starts prior to the transition
// and where it ends up after. not the temporal relationship.
let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset);
let transition_start = transition.unix_leap_time + i64::from(prev.ut_offset);
match transition_start.cmp(&transition_end) {
Ordering::Greater => {
// bakwards transition, eg from DST to regular
// this means a given local time could have one of two possible offsets
if local_leap_time < transition_end {
return Ok(crate::LocalResult::Single(prev));
} else if local_leap_time >= transition_end
&& local_leap_time <= transition_start
{
if prev.ut_offset < after_ltt.ut_offset {
return Ok(crate::LocalResult::Ambiguous(prev, after_ltt));
} else {
return Ok(crate::LocalResult::Ambiguous(after_ltt, prev));
}
}
}
Ordering::Equal => {
// should this ever happen? presumably we have to handle it anyway.
if local_leap_time < transition_start {
return Ok(crate::LocalResult::Single(prev));
} else if local_leap_time == transition_end {
if prev.ut_offset < after_ltt.ut_offset {
return Ok(crate::LocalResult::Ambiguous(prev, after_ltt));
} else {
return Ok(crate::LocalResult::Ambiguous(after_ltt, prev));
}
}
}
Ordering::Less => {
// forwards transition, eg from regular to DST
// this means that times that are skipped are invalid local times
if local_leap_time <= transition_start {
return Ok(crate::LocalResult::Single(prev));
} else if local_leap_time < transition_end {
return Ok(crate::LocalResult::None);
} else if local_leap_time == transition_end {
return Ok(crate::LocalResult::Single(after_ltt));
}
}
}
// try the next transition, we are fully after this one
prev = after_ltt;
}
prev
} else {
self.local_time_types[0]
};
if let Some(extra_rule) = self.extra_rule {
match extra_rule.find_local_time_type_from_local(local_time, year) {
Ok(local_time_type) => Ok(local_time_type),
Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
err => err,
}
} else {
Ok(crate::LocalResult::Single(offset_after_last))
}
}
/// Check time zone inputs
fn validate(&self) -> Result<(), Error> {
// Check local time types
let local_time_types_size = self.local_time_types.len();
if local_time_types_size == 0 {
return Err(Error::TimeZone("list of local time types must not be empty"));
}
// Check transitions
let mut i_transition = 0;
while i_transition < self.transitions.len() {
if self.transitions[i_transition].local_time_type_index >= local_time_types_size {
return Err(Error::TimeZone("invalid local time type index"));
}
if i_transition + 1 < self.transitions.len()
&& self.transitions[i_transition].unix_leap_time
>= self.transitions[i_transition + 1].unix_leap_time
{
return Err(Error::TimeZone("invalid transition"));
}
i_transition += 1;
}
// Check leap seconds
if !(self.leap_seconds.is_empty()
|| self.leap_seconds[0].unix_leap_time >= 0
&& self.leap_seconds[0].correction.saturating_abs() == 1)
{
return Err(Error::TimeZone("invalid leap second"));
}
let min_interval = SECONDS_PER_28_DAYS - 1;
let mut i_leap_second = 0;
while i_leap_second < self.leap_seconds.len() {
if i_leap_second + 1 < self.leap_seconds.len() {
let x0 = &self.leap_seconds[i_leap_second];
let x1 = &self.leap_seconds[i_leap_second + 1];
let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time);
let abs_diff_correction =
x1.correction.saturating_sub(x0.correction).saturating_abs();
if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) {
return Err(Error::TimeZone("invalid leap second"));
}
}
i_leap_second += 1;
}
// Check extra rule
let (extra_rule, last_transition) = match (&self.extra_rule, self.transitions.last()) {
(Some(rule), Some(trans)) => (rule, trans),
_ => return Ok(()),
};
let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index];
let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) {
Ok(unix_time) => unix_time,
Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)),
Err(err) => return Err(err),
};
let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) {
Ok(rule_local_time_type) => rule_local_time_type,
Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)),
Err(err) => return Err(err),
};
let check = last_local_time_type.ut_offset == rule_local_time_type.ut_offset
&& last_local_time_type.is_dst == rule_local_time_type.is_dst
&& match (&last_local_time_type.name, &rule_local_time_type.name) {
(Some(x), Some(y)) => x.equal(y),
(None, None) => true,
_ => false,
};
if !check {
return Err(Error::TimeZone(
"extra transition rule is inconsistent with the last transition",
));
}
Ok(())
}
/// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone
const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, Error> {
let mut unix_leap_time = unix_time;
let mut i = 0;
while i < self.leap_seconds.len() {
let leap_second = &self.leap_seconds[i];
if unix_leap_time < leap_second.unix_leap_time {
break;
}
unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) {
Some(unix_leap_time) => unix_leap_time,
None => return Err(Error::OutOfRange("out of range operation")),
};
i += 1;
}
Ok(unix_leap_time)
}
/// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone
fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, Error> {
if unix_leap_time == i64::min_value() {
return Err(Error::OutOfRange("out of range operation"));
}
let index = match self
.leap_seconds
.binary_search_by_key(&(unix_leap_time - 1), LeapSecond::unix_leap_time)
{
Ok(x) => x + 1,
Err(x) => x,
};
let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 };
match unix_leap_time.checked_sub(correction as i64) {
Some(unix_time) => Ok(unix_time),
None => Err(Error::OutOfRange("out of range operation")),
}
}
/// The UTC time zone
const UTC: TimeZoneRef<'static> = TimeZoneRef {
transitions: &[],
local_time_types: &[LocalTimeType::UTC],
leap_seconds: &[],
extra_rule: &None,
};
}
/// Transition of a TZif file
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) struct Transition {
/// Unix leap time
unix_leap_time: i64,
/// Index specifying the local time type of the transition
local_time_type_index: usize,
}
impl Transition {
/// Construct a TZif file transition
pub(super) const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self {
Self { unix_leap_time, local_time_type_index }
}
/// Returns Unix leap time
const fn unix_leap_time(&self) -> i64 {
self.unix_leap_time
}
}
/// Leap second of a TZif file
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) struct LeapSecond {
/// Unix leap time
unix_leap_time: i64,
/// Leap second correction
correction: i32,
}
impl LeapSecond {
/// Construct a TZif file leap second
pub(super) const fn new(unix_leap_time: i64, correction: i32) -> Self {
Self { unix_leap_time, correction }
}
/// Returns Unix leap time
const fn unix_leap_time(&self) -> i64 {
self.unix_leap_time
}
}
/// ASCII-encoded fixed-capacity string, used for storing time zone names
#[derive(Copy, Clone, Eq, PartialEq)]
struct TimeZoneName {
/// Length-prefixed string buffer
bytes: [u8; 8],
}
impl TimeZoneName {
/// Construct a time zone name
///
/// man tzfile(5):
/// Time zone designations should consist of at least three (3) and no more than six (6) ASCII
/// characters from the set of alphanumerics, “-”, and “+”. This is for compatibility with
/// POSIX requirements for time zone abbreviations.
fn new(input: &[u8]) -> Result<Self, Error> {
let len = input.len();
if !(3..=7).contains(&len) {
return Err(Error::LocalTimeType(
"time zone name must have between 3 and 7 characters",
));
}
let mut bytes = [0; 8];
bytes[0] = input.len() as u8;
let mut i = 0;
while i < len {
let b = input[i];
match b {
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-' => {}
_ => return Err(Error::LocalTimeType("invalid characters in time zone name")),
}
bytes[i + 1] = b;
i += 1;
}
Ok(Self { bytes })
}
/// Returns time zone name as a byte slice
fn as_bytes(&self) -> &[u8] {
match self.bytes[0] {
3 => &self.bytes[1..4],
4 => &self.bytes[1..5],
5 => &self.bytes[1..6],
6 => &self.bytes[1..7],
7 => &self.bytes[1..8],
_ => unreachable!(),
}
}
/// Check if two time zone names are equal
fn equal(&self, other: &Self) -> bool {
self.bytes == other.bytes
}
}
impl AsRef<str> for TimeZoneName {
fn as_ref(&self) -> &str {
// SAFETY: ASCII is valid UTF-8
unsafe { str::from_utf8_unchecked(self.as_bytes()) }
}
}
impl fmt::Debug for TimeZoneName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.as_ref().fmt(f)
}
}
/// Local time type associated to a time zone
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct LocalTimeType {
/// Offset from UTC in seconds
pub(super) ut_offset: i32,
/// Daylight Saving Time indicator
is_dst: bool,
/// Time zone name
name: Option<TimeZoneName>,
}
impl LocalTimeType {
/// Construct a local time type
pub(super) fn new(ut_offset: i32, is_dst: bool, name: Option<&[u8]>) -> Result<Self, Error> {
if ut_offset == i32::min_value() {
return Err(Error::LocalTimeType("invalid UTC offset"));
}
let name = match name {
Some(name) => TimeZoneName::new(name)?,
None => return Ok(Self { ut_offset, is_dst, name: None }),
};
Ok(Self { ut_offset, is_dst, name: Some(name) })
}
/// Construct a local time type with the specified UTC offset in seconds
pub(super) const fn with_offset(ut_offset: i32) -> Result<Self, Error> {
if ut_offset == i32::min_value() {
return Err(Error::LocalTimeType("invalid UTC offset"));
}
Ok(Self { ut_offset, is_dst: false, name: None })
}
/// Returns offset from UTC in seconds
pub(crate) const fn offset(&self) -> i32 {
self.ut_offset
}
/// Returns daylight saving time indicator
pub(super) const fn is_dst(&self) -> bool {
self.is_dst
}
pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None };
}
/// Open the TZif file corresponding to a TZ string
fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> {
// Don't check system timezone directories on non-UNIX platforms
#[cfg(not(unix))]
return Ok(File::open(path)?);
#[cfg(unix)]
{
let path = path.as_ref();
if path.is_absolute() {
return Ok(File::open(path)?);
}
for folder in &ZONE_INFO_DIRECTORIES {
if let Ok(file) = File::open(PathBuf::from(folder).join(path)) {
return Ok(file);
}
}
Err(Error::Io(io::ErrorKind::NotFound.into()))
}
}
// Possible system timezone directories
#[cfg(unix)]
const ZONE_INFO_DIRECTORIES: [&str; 4] =
["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"];
/// Number of seconds in one week
pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK;
/// Number of seconds in 28 days
const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28;
#[cfg(test)]
mod tests {
use super::super::Error;
use super::{LeapSecond, LocalTimeType, TimeZone, TimeZoneName, Transition, TransitionRule};
#[test]
fn test_no_dst() -> Result<(), Error> {
let tz_string = b"HST10";
let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
assert_eq!(transition_rule, LocalTimeType::new(-36000, false, Some(b"HST"))?.into());
Ok(())
}
#[test]
fn test_error() -> Result<(), Error> {
assert!(matches!(
TransitionRule::from_tz_string(b"IST-1GMT0", false),
Err(Error::UnsupportedTzString(_))
));
assert!(matches!(
TransitionRule::from_tz_string(b"EET-2EEST", false),
Err(Error::UnsupportedTzString(_))
));
Ok(())
}
#[test]
fn test_v1_file_with_leap_seconds() -> Result<(), Error> {
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";
let time_zone = TimeZone::from_tz_data(bytes)?;
let time_zone_result = TimeZone::new(
Vec::new(),
vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
vec![
LeapSecond::new(78796800, 1),
LeapSecond::new(94694401, 2),
LeapSecond::new(126230402, 3),
LeapSecond::new(157766403, 4),
LeapSecond::new(189302404, 5),
LeapSecond::new(220924805, 6),
LeapSecond::new(252460806, 7),
LeapSecond::new(283996807, 8),
LeapSecond::new(315532808, 9),
LeapSecond::new(362793609, 10),
LeapSecond::new(394329610, 11),
LeapSecond::new(425865611, 12),
LeapSecond::new(489024012, 13),
LeapSecond::new(567993613, 14),
LeapSecond::new(631152014, 15),
LeapSecond::new(662688015, 16),
LeapSecond::new(709948816, 17),
LeapSecond::new(741484817, 18),
LeapSecond::new(773020818, 19),
LeapSecond::new(820454419, 20),
LeapSecond::new(867715220, 21),
LeapSecond::new(915148821, 22),
LeapSecond::new(1136073622, 23),
LeapSecond::new(1230768023, 24),
LeapSecond::new(1341100824, 25),
LeapSecond::new(1435708825, 26),
LeapSecond::new(1483228826, 27),
],
None,
)?;
assert_eq!(time_zone, time_zone_result);
Ok(())
}
#[test]
fn test_v2_file() -> Result<(), Error> {
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";
let time_zone = TimeZone::from_tz_data(bytes)?;
let time_zone_result = TimeZone::new(
vec![
Transition::new(-2334101314, 1),
Transition::new(-1157283000, 2),
Transition::new(-1155436200, 1),
Transition::new(-880198200, 3),
Transition::new(-769395600, 4),
Transition::new(-765376200, 1),
Transition::new(-712150200, 5),
],
vec![
LocalTimeType::new(-37886, false, Some(b"LMT"))?,
LocalTimeType::new(-37800, false, Some(b"HST"))?,
LocalTimeType::new(-34200, true, Some(b"HDT"))?,
LocalTimeType::new(-34200, true, Some(b"HWT"))?,
LocalTimeType::new(-34200, true, Some(b"HPT"))?,
LocalTimeType::new(-36000, false, Some(b"HST"))?,
],
Vec::new(),
Some(TransitionRule::from(LocalTimeType::new(-36000, false, Some(b"HST"))?)),
)?;
assert_eq!(time_zone, time_zone_result);
assert_eq!(
*time_zone.find_local_time_type(-1156939200)?,
LocalTimeType::new(-34200, true, Some(b"HDT"))?
);
assert_eq!(
*time_zone.find_local_time_type(1546300800)?,
LocalTimeType::new(-36000, false, Some(b"HST"))?
);
Ok(())
}
#[test]
fn test_no_tz_string() -> Result<(), Error> {
// Guayaquil from macOS 10.11
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";
let time_zone = TimeZone::from_tz_data(bytes)?;
dbg!(&time_zone);
let time_zone_result = TimeZone::new(
vec![Transition::new(-1230749160, 1)],
vec![
LocalTimeType::new(-18840, false, Some(b"QMT"))?,
LocalTimeType::new(-18000, false, Some(b"ECT"))?,
],
Vec::new(),
None,
)?;
assert_eq!(time_zone, time_zone_result);
assert_eq!(
*time_zone.find_local_time_type(-1500000000)?,
LocalTimeType::new(-18840, false, Some(b"QMT"))?
);
assert_eq!(
*time_zone.find_local_time_type(0)?,
LocalTimeType::new(-18000, false, Some(b"ECT"))?
);
Ok(())
}
#[test]
fn test_tz_ascii_str() -> Result<(), Error> {
assert!(matches!(TimeZoneName::new(b""), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"A"), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"AB"), Err(Error::LocalTimeType(_))));
assert_eq!(TimeZoneName::new(b"CET")?.as_bytes(), b"CET");
assert_eq!(TimeZoneName::new(b"CHADT")?.as_bytes(), b"CHADT");
assert_eq!(TimeZoneName::new(b"abcdefg")?.as_bytes(), b"abcdefg");
assert_eq!(TimeZoneName::new(b"UTC+02")?.as_bytes(), b"UTC+02");
assert_eq!(TimeZoneName::new(b"-1230")?.as_bytes(), b"-1230");
assert!(matches!(TimeZoneName::new("−0330".as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212)
assert!(matches!(TimeZoneName::new(b"\x00123"), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"12345678"), Err(Error::LocalTimeType(_))));
assert!(matches!(TimeZoneName::new(b"GMT\0\0\0"), Err(Error::LocalTimeType(_))));
Ok(())
}
#[test]
fn test_time_zone() -> Result<(), Error> {
let utc = LocalTimeType::UTC;
let cet = LocalTimeType::with_offset(3600)?;
let utc_local_time_types = vec![utc];
let fixed_extra_rule = TransitionRule::from(cet);
let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?;
let time_zone_2 =
TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?;
let time_zone_3 =
TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?;
let time_zone_4 = TimeZone::new(
vec![Transition::new(i32::min_value().into(), 0), Transition::new(0, 1)],
vec![utc, cet],
Vec::new(),
Some(fixed_extra_rule),
)?;
assert_eq!(*time_zone_1.find_local_time_type(0)?, utc);
assert_eq!(*time_zone_2.find_local_time_type(0)?, cet);
assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc);
assert_eq!(*time_zone_3.find_local_time_type(0)?, utc);
assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc);
assert_eq!(*time_zone_4.find_local_time_type(0)?, cet);
let time_zone_err = TimeZone::new(
vec![Transition::new(0, 0)],
utc_local_time_types,
vec![],
Some(fixed_extra_rule),
);
assert!(time_zone_err.is_err());
Ok(())
}
#[test]
fn test_time_zone_from_posix_tz() -> Result<(), Error> {
#[cfg(unix)]
{
// if the TZ var is set, this essentially _overrides_ the
// time set by the localtime symlink
// so just ensure that ::local() acts as expected
// in this case
if let Ok(tz) = std::env::var("TZ") {
let time_zone_local = TimeZone::local(Some(tz.as_str()))?;
let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?;
assert_eq!(time_zone_local, time_zone_local_1);
}
// `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have
// a time zone database, like for example some docker containers.
// In that case skip the test.
if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC") {
assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0);
}
}
assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err());
assert!(TimeZone::from_posix_tz("").is_err());
Ok(())
}
#[test]
fn test_leap_seconds() -> Result<(), Error> {
let time_zone = TimeZone::new(
Vec::new(),
vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
vec![
LeapSecond::new(78796800, 1),
LeapSecond::new(94694401, 2),
LeapSecond::new(126230402, 3),
LeapSecond::new(157766403, 4),
LeapSecond::new(189302404, 5),
LeapSecond::new(220924805, 6),
LeapSecond::new(252460806, 7),
LeapSecond::new(283996807, 8),
LeapSecond::new(315532808, 9),
LeapSecond::new(362793609, 10),
LeapSecond::new(394329610, 11),
LeapSecond::new(425865611, 12),
LeapSecond::new(489024012, 13),
LeapSecond::new(567993613, 14),
LeapSecond::new(631152014, 15),
LeapSecond::new(662688015, 16),
LeapSecond::new(709948816, 17),
LeapSecond::new(741484817, 18),
LeapSecond::new(773020818, 19),
LeapSecond::new(820454419, 20),
LeapSecond::new(867715220, 21),
LeapSecond::new(915148821, 22),
LeapSecond::new(1136073622, 23),
LeapSecond::new(1230768023, 24),
LeapSecond::new(1341100824, 25),
LeapSecond::new(1435708825, 26),
LeapSecond::new(1483228826, 27),
],
None,
)?;
let time_zone_ref = time_zone.as_ref();
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599)));
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600)));
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600)));
assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601)));
assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621)));
assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623)));
assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624)));
Ok(())
}
#[test]
fn test_leap_seconds_overflow() -> Result<(), Error> {
let time_zone_err = TimeZone::new(
vec![Transition::new(i64::min_value(), 0)],
vec![LocalTimeType::UTC],
vec![LeapSecond::new(0, 1)],
Some(TransitionRule::from(LocalTimeType::UTC)),
);
assert!(time_zone_err.is_err());
let time_zone = TimeZone::new(
vec![Transition::new(i64::max_value(), 0)],
vec![LocalTimeType::UTC],
vec![LeapSecond::new(0, 1)],
None,
)?;
assert!(matches!(
time_zone.find_local_time_type(i64::max_value()),
Err(Error::FindLocalTimeType(_))
));
Ok(())
}
}