Skip to content

Commit

Permalink
Method to get local UTC offset at a given datetime (#217)
Browse files Browse the repository at this point in the history
This implementation should work on Windows, Mac, Linux, and Solaris. As
Redox is *nix, it should work there as well.

`OffsetDateTime::now_local()` returns a value with the local offset.

`UtcOffset::local_offset_at(OffsetDateTime)` and
`UtcOffset::current_local_offset()` have also been implemented.

This necessarily requires syscalls, and as such
`#![forbid(unsafe_code)]` has been changed to `#![deny(unsafe_code)]`.
It must be explicitly `#[allow]`ed in every location it is used, along
with a general message describing why the usage is safe.

Co-authored-by: Hroi Sigurdsson <hroi@asdf.dk>
  • Loading branch information
jhpratt and hroi committed Feb 22, 2020
1 parent 73f55ce commit 5f1c492
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -32,6 +32,13 @@ Versioning].
- `NumericalDuration` has been implemented for `f32` and `f64`.
`NumericalStdDuration` and `NumericalStdDurationShort` have been implemented
for `f64` only.
- `UtcOffset::local_offset_at(OffsetDateTime)`, which will obtain the system's
local offset at the provided moment in time.
- `OffsetDateTime::now_local()` is equivalent to calling
`OffsetDateTime::now().to_offset(UtcOffset::local_offset_at(OffsetDateTime::now()))`
(but more efficient).
- `UtcOffset::current_local_offset()` will return the equivalent of
`OffsetDateTime::now_local().offset()`.

### Changed

Expand Down
8 changes: 7 additions & 1 deletion Cargo.toml
Expand Up @@ -18,7 +18,7 @@ all-features = true
default = ["deprecated", "std"]
deprecated = []
panicking-api = []
std = []
std = ["libc", "winapi"]

# Internal usage. This is used when building for docs.rs and time-rs.github.io.
# This feature should never be used by external users. It will likely be
Expand All @@ -33,3 +33,9 @@ time-macros = { version = "0.1", path = "time-macros" }

[workspace]
members = ["time-macros", "time-macros-impl"]

[target.'cfg(unix)'.dependencies]
libc = { version = "0.2", optional = true }

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["minwinbase", "minwindef", "timezoneapi"], optional = true }
2 changes: 1 addition & 1 deletion src/lib.rs
Expand Up @@ -141,8 +141,8 @@

#![cfg_attr(feature = "__doc", feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![deny(
unsafe_code, // Used when interacting with system APIs
anonymous_parameters,
rust_2018_idioms,
trivial_casts,
Expand Down
30 changes: 26 additions & 4 deletions src/offset_date_time.rs
Expand Up @@ -40,10 +40,7 @@ pub struct OffsetDateTime {
}

impl OffsetDateTime {
/// Create a new `OffsetDateTime` with the current date and time.
///
/// This currently returns an offset of UTC, though this behavior will
/// change once a way to obtain the local offset is implemented.
/// Create a new `OffsetDateTime` with the current date and time in UTC.
///
/// ```rust
/// # use time::{OffsetDateTime, offset};
Expand All @@ -57,6 +54,21 @@ impl OffsetDateTime {
SystemTime::now().into()
}

/// Create a new `OffsetDateTime` with the current date and time in the
/// local offset.
///
/// ```rust
/// # use time::{OffsetDateTime, offset};
/// assert!(OffsetDateTime::now_local().year() >= 2019);
/// ```
#[inline(always)]
#[cfg(feature = "std")]
#[cfg_attr(feature = "__doc", doc(cfg(feature = "std")))]
pub fn now_local() -> Self {
let t = Self::now();
t.to_offset(UtcOffset::local_offset_at(t))
}

/// Convert the `OffsetDateTime` from the current `UtcOffset` to the
/// provided `UtcOffset`.
///
Expand Down Expand Up @@ -960,6 +972,16 @@ mod test {
assert_eq!(OffsetDateTime::now().offset(), offset!(UTC));
}

#[test]
#[cfg(feature = "std")]
fn now_local() {
assert!(OffsetDateTime::now().year() >= 2019);
assert_eq!(
OffsetDateTime::now_local().offset(),
UtcOffset::current_local_offset()
);
}

#[test]
fn to_offset() {
assert_eq!(
Expand Down
196 changes: 196 additions & 0 deletions src/utc_offset.rs
@@ -1,5 +1,7 @@
#[cfg(not(feature = "std"))]
use crate::alloc_prelude::*;
#[cfg(feature = "std")]
use crate::OffsetDateTime;
use crate::{
format::{parse, ParseError, ParseResult, ParsedItems},
DeferredFormat, Duration,
Expand Down Expand Up @@ -197,6 +199,35 @@ impl UtcOffset {
pub(crate) const fn as_duration(self) -> Duration {
Duration::seconds(self.seconds as i64)
}

/// Obtain the system's UTC offset at a known moment in time. If the offset
/// cannot be determined, UTC is returned.
///
/// ```rust,no_run
/// # use time::{UtcOffset, OffsetDateTime};
/// let unix_epoch = OffsetDateTime::unix_epoch();
/// let local_offset = UtcOffset::local_offset_at(unix_epoch);
/// println!("{}", local_offset.format("%z"));
/// ```
#[inline(always)]
#[cfg(feature = "std")]
pub fn local_offset_at(datetime: OffsetDateTime) -> Self {
try_local_offset_at(datetime).unwrap_or(Self::UTC)
}

/// Obtain the system's current UTC offset. If the offset cannot be
/// determined, UTC is returned.
///
/// ```rust,no_run
/// # use time::UtcOffset;
/// let local_offset = UtcOffset::current_local_offset();
/// println!("{}", local_offset.format("%z"));
/// ```
#[inline(always)]
#[cfg(feature = "std")]
pub fn current_local_offset() -> Self {
OffsetDateTime::now_local().offset()
}
}

/// Methods that allow parsing and formatting the `UtcOffset`.
Expand Down Expand Up @@ -256,6 +287,171 @@ impl Display for UtcOffset {
}
}

/// Attempt to obtain the system's UTC offset. If the offset cannot be
/// determined, `None` is returned.
#[cfg(feature = "std")]
#[allow(clippy::too_many_lines)] //
fn try_local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
use core::{convert::TryInto, mem};

#[cfg(target_family = "unix")]
{
/// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on
/// any error.
fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
extern "C" {
fn tzset();
}

let timestamp = timestamp.try_into().ok()?;

// Safety: Plain old data.
#[allow(unsafe_code)]
let mut tm = unsafe { mem::zeroed() };

// Update timezone information from system. `localtime_r` does not
// do this for us.
//
// Safety: tzset is thread-safe.
#[allow(unsafe_code)]
unsafe {
tzset();
}

// Safety: We are calling a system API, which mutates the `tm`
// variable. If a null pointer is returned, an error occurred.
#[allow(unsafe_code)]
let tm_ptr = unsafe { libc::localtime_r(&timestamp, &mut tm) };

if tm_ptr.is_null() {
None
} else {
Some(tm)
}
}

let tm = timestamp_to_tm(datetime.timestamp())?;

// `tm_gmtoff` extension
#[cfg(not(target_os = "solaris"))]
{
tm.tm_gmtoff.try_into().ok().map(UtcOffset::seconds)
}

// No `tm_gmtoff` extension
#[cfg(target_os = "solaris")]
{
use crate::Date;
use core::convert::TryFrom;

let mut tm = tm;
if tm.tm_sec == 60 {
// Leap seconds are not currently supported.
tm.tm_sec = 59;
}

let local_timestamp =
Date::try_from_yo(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1)
.ok()?
.try_with_hms(
tm.tm_hour.try_into().ok()?,
tm.tm_min.try_into().ok()?,
tm.tm_sec.try_into().ok()?,
)
.ok()?
.assume_utc()
.timestamp();

(local_timestamp - datetime.timestamp())
.try_into()
.ok()
.map(UtcOffset::seconds)
}
}

#[cfg(target_family = "windows")]
{
use crate::offset;
use winapi::{
shared::minwindef::FILETIME,
um::{
minwinbase::SYSTEMTIME,
timezoneapi::{SystemTimeToFileTime, SystemTimeToTzSpecificLocalTime},
},
};

/// Convert a `SYSTEMTIME` to a `FILETIME`. Returns `None` if any error
/// occurred.
fn systemtime_to_filetime(systime: &SYSTEMTIME) -> Option<FILETIME> {
// Safety: We only read `ft` if it is properly initialized.
#[allow(unsafe_code, deprecated)]
let mut ft = unsafe { mem::uninitialized() };

// Safety: `SystemTimeToFileTime` is thread-safe.
#[allow(unsafe_code)]
{
if 0 == unsafe { SystemTimeToFileTime(systime, &mut ft) } {
// failed
None
} else {
Some(ft)
}
}
}

/// Convert a `FILETIME` to an `i64`, representing a number of seconds.
fn filetime_to_secs(filetime: &FILETIME) -> i64 {
/// FILETIME represents 100-nanosecond intervals
const FT_TO_SECS: i64 = 10_000_000;
((filetime.dwHighDateTime as i64) << 32 | filetime.dwLowDateTime as i64) / FT_TO_SECS
}

/// Convert an `OffsetDateTime` to a `SYSTEMTIME`.
fn offset_to_systemtime(datetime: OffsetDateTime) -> SYSTEMTIME {
let (month, day_of_month) = datetime.to_offset(offset!(UTC)).month_day();
SYSTEMTIME {
wYear: datetime.year() as u16,
wMonth: month as u16,
wDay: day_of_month as u16,
wDayOfWeek: 0, // ignored
wHour: datetime.hour() as u16,
wMinute: datetime.minute() as u16,
wSecond: datetime.second() as u16,
wMilliseconds: datetime.millisecond(),
}
}

// This function falls back to UTC if any system call fails.
let systime_utc = offset_to_systemtime(datetime.to_offset(offset!(UTC)));

// Safety: `local_time` is only read if it is properly initialized, and
// `SystemTimeToTzSpecificLocalTime` is thread-safe.
#[allow(unsafe_code)]
let systime_local = unsafe {
#[allow(deprecated)]
let mut local_time = mem::uninitialized();
if 0 == SystemTimeToTzSpecificLocalTime(
core::ptr::null(), // use system's current timezone
&systime_utc,
&mut local_time,
) {
// call failed
return None;
} else {
local_time
}
};

// Convert SYSTEMTIMEs to FILETIMEs so we can perform arithmetic on them.
let ft_system = systemtime_to_filetime(&systime_utc)?;
let ft_local = systemtime_to_filetime(&systime_local)?;

let diff_secs = filetime_to_secs(&ft_local) - filetime_to_secs(&ft_system);

diff_secs.try_into().ok().map(UtcOffset::seconds)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down

0 comments on commit 5f1c492

Please sign in to comment.