Skip to content

Commit

Permalink
Dynamically load localtime_rs and friends from libc on Android.
Browse files Browse the repository at this point in the history
These functions were added in Android API level 35, and provide a better
interface to getting timezones, with no environment variable access to
worry about.
  • Loading branch information
qwandor committed Jun 19, 2023
1 parent 66addc7 commit f5b9836
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ iana-time-zone = { version = "0.1.45", optional = true, features = ["fallback"]

[target.'cfg(target_os = "android")'.dependencies]
libc = { version = "0.2.146", optional = true }
once_cell = "1.18.0"

[dev-dependencies]
serde_json = { version = "1" }
Expand Down
107 changes: 95 additions & 12 deletions src/offset/local/android.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,87 @@
use super::{FixedOffset, NaiveDateTime};
use crate::traits::{Datelike, Timelike};
use crate::LocalResult;
use libc::{localtime_r, mktime, time_t};
use libc::{c_char, c_void, dlsym, localtime_r, mktime, time_t, tm, RTLD_DEFAULT};
use once_cell::sync::Lazy;
use std::io;
use std::mem;
use std::mem::{self, transmute};
use std::ops::Deref;
use std::ptr::null;

enum Timezone {}

#[allow(non_camel_case_types)]
type timezone_t = *mut Timezone;

/// Timezone access functions added in API level 35. These are prefereable to `localtime_r` /
/// `mktime` if they are available, as they don't access environment variables and so avoid any
/// potential thread-safety issues with them.
struct TimezoneFunctions {
localtime_rz: unsafe extern "C" fn(timezone_t, *const time_t, *mut tm) -> *mut tm,
mktime_z: unsafe extern "C" fn(timezone_t, *mut tm) -> time_t,
tzalloc: unsafe extern "C" fn(*const c_char) -> timezone_t,
tzfree: unsafe extern "C" fn(timezone_t),
}

impl TimezoneFunctions {
/// Loads the functions dynamically from libc if they are available, or returns `None` if any of
/// the functions is not available.
fn load() -> Option<Self> {
// Safe because we give dlsym valid arguments, check all the return values for nulls, and
// cast the function pointers to the correct types as defined in Bionic libc.
unsafe {
let localtime_rz = dlsym(RTLD_DEFAULT, b"localtime_rz\0".as_ptr());
let mktime_z = dlsym(RTLD_DEFAULT, b"mktime_z\0".as_ptr());
let tzalloc = dlsym(RTLD_DEFAULT, b"tzalloc\0".as_ptr());
let tzfree = dlsym(RTLD_DEFAULT, b"tzfree\0".as_ptr());
if localtime_rz.is_null() || mktime_z.is_null() || tzalloc.is_null() || tzfree.is_null()
{
return None;
}
Some(Self {
localtime_rz: transmute::<*mut c_void, _>(localtime_rz),
mktime_z: transmute::<*mut c_void, _>(mktime_z),
tzalloc: transmute::<*mut c_void, _>(tzalloc),
tzfree: transmute::<*mut c_void, _>(tzfree),
})
}
}
}

static TZ_FUNCTIONS: Lazy<Option<TimezoneFunctions>> = Lazy::new(|| TimezoneFunctions::load());

pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
let time = utc.timestamp() as time_t;
// Safe because an all-zero `struct tm` is valid.
let mut result = unsafe { mem::zeroed() };
// Safe because localtime_r only accesses the pointers passed to it during the call. It does
// also try to read the `TZ` environment variable, but we can assume it's not set on Android as
// the timezone is read from a system property instead.
unsafe {
if localtime_r(&time, &mut result).is_null() {
panic!("localtime_r failed: {}", io::Error::last_os_error());

if let Some(tz_functions) = TZ_FUNCTIONS.deref() {
// Safe because:
// - tzalloc accepts a null pointer to use the current system timezone.
// - localtime_rz only accesses the pointers passed to it during the call, which are valid
// at that point.
// - tzfree is only called after the timezone_t is no longer used.
unsafe {
let timezone = (tz_functions.tzalloc)(null());
if timezone.is_null() {
panic!("tzalloc failed: {}", io::Error::last_os_error());
}
if (tz_functions.localtime_rz)(timezone, &time, &mut result).is_null() {
panic!("localtime_rz failed: {}", io::Error::last_os_error());
}
(tz_functions.tzfree)(timezone);
}
} else {
// Safe because localtime_r only accesses the pointers passed to it during the call. It does
// also try to read the `TZ` environment variable, but we can assume it's not set on Android
// as the timezone is read from a system property instead.
unsafe {
if localtime_r(&time, &mut result).is_null() {
panic!("localtime_r failed: {}", io::Error::last_os_error());
}
}
}

LocalResult::Single(
FixedOffset::east_opt(
result.tm_gmtoff.try_into().expect("localtime_r returned invalid UTC offset"),
Expand Down Expand Up @@ -68,9 +132,28 @@ fn mktime_with_dst(local: &NaiveDateTime, isdst: i32) -> (time_t, i32) {
tm_gmtoff: 0,
tm_zone: null(),
};
// Safe because mktime only accesses struct it is passed during the call, and doesn't store the
// pointer to access later. It does also try to read the `TZ` environment variable, but we can
// assume it's not set on Android as the timezone is read from a system property instead.
let timestamp = unsafe { mktime(&mut tm) };
let timestamp;
if let Some(tz_functions) = TZ_FUNCTIONS.deref() {
// Safe because:
// - tzalloc accepts a null pointer to use the current system timezone.
// - mktime only accesses the struct tm it is passed during the call, and doesn't store the
// pointer to access later. The tm_zone it sets may only be valid as long as the timezone
// is, but that's fine as we don't access tm_zone.
// - tzfree is only called after the timezone_t is no longer used.
unsafe {
let timezone = (tz_functions.tzalloc)(null());
if timezone.is_null() {
panic!("tzalloc failed: {}", io::Error::last_os_error());
}
timestamp = (tz_functions.mktime_z)(timezone, &mut tm);
(tz_functions.tzfree)(timezone);
}
} else {
// Safe because mktime only accesses the struct it is passed during the call, and doesn't
// store the pointer to access later. It does also try to read the `TZ` environment
// variable, but we can assume it's not set on Android as the timezone is read from a system
// property instead.
timestamp = unsafe { mktime(&mut tm) };
}
(timestamp, tm.tm_isdst)
}

0 comments on commit f5b9836

Please sign in to comment.