Skip to content

Commit

Permalink
handle ambuiguous and invalid local timestamps
Browse files Browse the repository at this point in the history
  • Loading branch information
esheppa committed May 1, 2022
1 parent 0826d63 commit fd32afd
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 18 deletions.
6 changes: 4 additions & 2 deletions src/offset/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ impl TimeZone for Local {

#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Local>> {
LocalResult::Single(inner::naive_to_local(local, true))
inner::naive_to_local(local, true)
}

fn from_utc_date(&self, utc: &NaiveDate) -> Date<Local> {
Expand All @@ -129,7 +129,9 @@ impl TimeZone for Local {

#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Local> {
inner::naive_to_local(utc, false)
// this is OK to unwrap as getting local time from a UTC
// timestamp is never ambiguous
inner::naive_to_local(utc, false).unwrap()
}
}

Expand Down
135 changes: 135 additions & 0 deletions src/offset/local/tz_info/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ impl TransitionRule {
}
}
}

/// Find the local time type associated to the transition rule at the specified Unix time in seconds
pub(super) fn find_local_time_type_from_local(
&self,
local_time: i64,
year: i32,
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
match self {
TransitionRule::Fixed(local_time_type) => {
Ok(crate::LocalResult::Single(*local_time_type))
}
TransitionRule::Alternate(alternate_time) => {
alternate_time.find_local_time_type_from_local(local_time, year)
}
}
}
}

impl From<LocalTimeType> for TransitionRule {
Expand Down Expand Up @@ -211,6 +227,125 @@ impl AlternateTime {
Ok(&self.std)
}
}

fn find_local_time_type_from_local(
&self,
local_time: i64,
current_year: i32,
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
// Check if the current year is valid for the following computations
if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) {
return Err(Error::OutOfRange("out of range date time"));
}

let dst_start_transition_start =
self.dst_start.unix_time(current_year, 0) + i64::from(self.dst_start_time);
let dst_start_transition_end = self.dst_start.unix_time(current_year, 0)
+ i64::from(self.dst_start_time)
+ i64::from(self.dst.ut_offset)
- i64::from(self.std.ut_offset);

let dst_end_transition_start =
self.dst_end.unix_time(current_year, 0) + i64::from(self.dst_end_time);
let dst_end_transition_end = self.dst_end.unix_time(current_year, 0)
+ i64::from(self.dst_end_time)
+ i64::from(self.std.ut_offset)
- i64::from(self.dst.ut_offset);

match self.std.ut_offset.cmp(&self.dst.ut_offset) {
Ordering::Equal => Ok(crate::LocalResult::Single(self.std)),
Ordering::Less => {
if self.dst_start.transition_date(current_year).0
< self.dst_end.transition_date(current_year).0
{
// northern hemisphere
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
if local_time <= dst_start_transition_start {
Ok(crate::LocalResult::Single(self.std))
} else if local_time > dst_start_transition_start
&& local_time < dst_start_transition_end
{
Ok(crate::LocalResult::None)
} else if local_time >= dst_start_transition_end
&& local_time < dst_end_transition_end
{
Ok(crate::LocalResult::Single(self.dst))
} else if local_time >= dst_end_transition_end
&& local_time <= dst_end_transition_start
{
Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
} else {
Ok(crate::LocalResult::Single(self.std))
}
} else {
// southern hemisphere regular DST
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
if local_time < dst_end_transition_end {
Ok(crate::LocalResult::Single(self.dst))
} else if local_time >= dst_end_transition_end
&& local_time <= dst_end_transition_start
{
Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
} else if local_time > dst_end_transition_end
&& local_time < dst_start_transition_start
{
Ok(crate::LocalResult::Single(self.std))
} else if local_time >= dst_start_transition_start
&& local_time < dst_start_transition_end
{
Ok(crate::LocalResult::None)
} else {
Ok(crate::LocalResult::Single(self.dst))
}
}
}
Ordering::Greater => {
if self.dst_start.transition_date(current_year).0
< self.dst_end.transition_date(current_year).0
{
// southern hemisphere reverse DST
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
if local_time < dst_start_transition_end {
Ok(crate::LocalResult::Single(self.std))
} else if local_time >= dst_start_transition_end
&& local_time <= dst_start_transition_start
{
Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
} else if local_time > dst_start_transition_start
&& local_time < dst_end_transition_start
{
Ok(crate::LocalResult::Single(self.dst))
} else if local_time >= dst_end_transition_start
&& local_time < dst_end_transition_end
{
Ok(crate::LocalResult::None)
} else {
Ok(crate::LocalResult::Single(self.std))
}
} else {
// northern hemisphere reverse DST
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
if local_time <= dst_end_transition_start {
Ok(crate::LocalResult::Single(self.dst))
} else if local_time > dst_end_transition_start
&& local_time < dst_end_transition_end
{
Ok(crate::LocalResult::None)
} else if local_time >= dst_end_transition_end
&& local_time < dst_start_transition_end
{
Ok(crate::LocalResult::Single(self.std))
} else if local_time >= dst_start_transition_end
&& local_time <= dst_start_transition_start
{
Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
} else {
Ok(crate::LocalResult::Single(self.dst))
}
}
}
}
}
}

/// Parse time zone name
Expand Down
102 changes: 100 additions & 2 deletions src/offset/local/tz_info/timezone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::fs::{self, File};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::{fmt, str};
use std::{cmp::Ordering, fmt, str};

use super::rule::{AlternateTime, TransitionRule};
use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY};
Expand All @@ -27,7 +27,11 @@ impl TimeZone {
/// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead.
///
pub(crate) fn local() -> Result<Self, Error> {
Self::from_posix_tz("localtime")
if let Ok(tz) = std::env::var("TZ") {
Self::from_posix_tz(&tz)
} else {
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).
Expand Down Expand Up @@ -114,6 +118,15 @@ impl TimeZone {
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 {
Expand Down Expand Up @@ -188,6 +201,91 @@ impl<'a> TimeZoneRef<'a> {
}
}

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
if !self.transitions.is_empty() {
let mut prev = Some(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.unwrap().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.unwrap()));
} else if local_leap_time >= transition_end
&& local_leap_time <= transition_start
{
if prev.unwrap().ut_offset < after_ltt.ut_offset {
return Ok(crate::LocalResult::Ambiguous(prev.unwrap(), after_ltt));
} else {
return Ok(crate::LocalResult::Ambiguous(after_ltt, prev.unwrap()));
}
}
}
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.unwrap()));
} else if local_leap_time == transition_end {
if prev.unwrap().ut_offset < after_ltt.ut_offset {
return Ok(crate::LocalResult::Ambiguous(prev.unwrap(), after_ltt));
} else {
return Ok(crate::LocalResult::Ambiguous(after_ltt, prev.unwrap()));
}
}
}
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.unwrap()));
} 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 = Some(after_ltt);
}
};

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(self.local_time_types[0]))
}
}

/// Check time zone inputs
fn validate(&self) -> Result<(), Error> {
// Check local time types
Expand Down
54 changes: 40 additions & 14 deletions src/offset/local/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,59 @@ use std::sync::Once;

use super::tz_info::TimeZone;
use super::{DateTime, FixedOffset, Local, NaiveDateTime};
use crate::Utc;
use crate::{Datelike, LocalResult, Utc};

pub(super) fn now() -> DateTime<Local> {
let now = Utc::now();
DateTime::from_utc(now.naive_utc(), offset(now.timestamp()))
let now = Utc::now().naive_utc();
DateTime::from_utc(now, offset(now, false).unwrap())
}

pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime<Local> {
let offset = match local {
true => offset(d.timestamp()),
false => FixedOffset::east(0),
};

DateTime::from_utc(*d - offset, offset)
pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
if local {
match offset(*d, true) {
LocalResult::None => LocalResult::None,
LocalResult::Ambiguous(early, late) => LocalResult::Ambiguous(
DateTime::from_utc(*d - early, early),
DateTime::from_utc(*d - late, late),
),
LocalResult::Single(offset) => {
LocalResult::Single(DateTime::from_utc(*d - offset, offset))
}
}
} else {
LocalResult::Single(DateTime::from_utc(*d, offset(*d, false).unwrap()))
}
}

fn offset(unix: i64) -> FixedOffset {
fn offset(d: NaiveDateTime, local: bool) -> LocalResult<FixedOffset> {
let info = unsafe {
INIT.call_once(|| {
INFO = Some(TimeZone::local().expect("unable to parse localtime info"));
});
INFO.as_ref().unwrap()
};

FixedOffset::east(
info.find_local_time_type(unix).expect("unable to select local time type").offset(),
)
if local {
// we pass through the year as the year of a local point in time must either be valid in that locale, or
// the entire time was skipped in which case we will return LocalResult::None anywa.
match info
.find_local_time_type_from_local(d.timestamp(), d.year())
.expect("unable to select local time type")
{
LocalResult::None => LocalResult::None,
LocalResult::Ambiguous(early, late) => LocalResult::Ambiguous(
FixedOffset::east(early.offset()),
FixedOffset::east(late.offset()),
),
LocalResult::Single(tt) => LocalResult::Single(FixedOffset::east(tt.offset())),
}
} else {
LocalResult::Single(FixedOffset::east(
info.find_local_time_type(d.timestamp())
.expect("unable to select local time type")
.offset(),
))
}
}

static mut INFO: Option<TimeZone> = None;
Expand Down

0 comments on commit fd32afd

Please sign in to comment.