handle missing /etc/localtime on some unix platforms (#756)

* msrv -> 1.38

* default to UTC  when iana-time-zone errors and /etc/localtime is missing, support android

fix function name
This commit is contained in:
Eric Sheppard 2022-08-09 23:43:30 +10:00 committed by GitHub
parent 5edf4d0cc7
commit 557bcd5f44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 70 additions and 49 deletions

View File

@ -31,11 +31,11 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
rust_version: nightly rust_version: nightly
- os: ubuntu-20.04 - os: ubuntu-20.04
rust_version: 1.32.0 rust_version: 1.38.0
- os: macos-latest - os: macos-latest
rust_version: 1.32.0 rust_version: 1.38.0
- os: windows-latest - os: windows-latest
rust_version: 1.32.0 rust_version: 1.38.0
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

View File

@ -39,11 +39,11 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
rust_version: nightly rust_version: nightly
- os: ubuntu-20.04 - os: ubuntu-20.04
rust_version: 1.32.0 rust_version: 1.38.0
- os: macos-latest - os: macos-latest
rust_version: 1.32.0 rust_version: 1.38.0
- os: windows-latest - os: windows-latest
rust_version: 1.32.0 rust_version: 1.38.0
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

View File

@ -24,7 +24,7 @@ default = ["clock", "std", "oldtime"]
alloc = [] alloc = []
libc = [] libc = []
std = [] std = []
clock = ["std", "winapi"] clock = ["std", "winapi", "iana-time-zone"]
oldtime = ["time"] oldtime = ["time"]
wasmbind = [] # TODO: empty feature to avoid breaking change in 0.4.20, can be removed later wasmbind = [] # TODO: empty feature to avoid breaking change in 0.4.20, can be removed later
unstable-locales = ["pure-rust-locales", "alloc"] unstable-locales = ["pure-rust-locales", "alloc"]
@ -45,6 +45,9 @@ rkyv = {version = "0.7", optional = true}
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2" }
js-sys = { version = "0.3" } # contains FFI bindings for the JS Date API js-sys = { version = "0.3" } # contains FFI bindings for the JS Date API
[target.'cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "solaris")))'.dependencies]
iana-time-zone = { version = "0.1.41", optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.0", features = ["std", "minwinbase", "minwindef", "timezoneapi"], optional = true } winapi = { version = "0.3.0", features = ["std", "minwinbase", "minwindef", "timezoneapi"], optional = true }

View File

@ -29,7 +29,7 @@ meaningful in the github actions feature matrix UI.
runv cargo --version runv cargo --version
if [[ ${RUST_VERSION:-} != 1.32.0 ]]; then if [[ ${RUST_VERSION:-} != 1.38.0 ]]; then
if [[ ${WASM:-} == yes_wasm ]]; then if [[ ${WASM:-} == yes_wasm ]]; then
test_wasm test_wasm
elif [[ ${WASM:-} == wasm_simple ]]; then elif [[ ${WASM:-} == wasm_simple ]]; then
@ -49,7 +49,7 @@ meaningful in the github actions feature matrix UI.
else else
test_regular UTC0 test_regular UTC0
fi fi
elif [[ ${RUST_VERSION:-} == 1.32.0 ]]; then elif [[ ${RUST_VERSION:-} == 1.38.0 ]]; then
test_132 test_132
else else
echo "ERROR: didn't run any tests" echo "ERROR: didn't run any tests"

View File

@ -1 +1 @@
msrv = "1.32" msrv = "1.38"

View File

@ -529,7 +529,7 @@ fn format_inner<'a>(
}; };
if let Some(v) = v { if let Some(v) = v {
if (spec == &Year || spec == &IsoYear) && !(0 <= v && v < 10_000) { if (spec == &Year || spec == &IsoYear) && !(0..10_000).contains(&v) {
// non-four-digit years require an explicit sign as per ISO 8601 // non-four-digit years require an explicit sign as per ISO 8601
match *pad { match *pad {
Pad::None => write!(result, "{:+}", v), Pad::None => write!(result, "{:+}", v),

View File

@ -232,7 +232,7 @@ impl Parsed {
/// given hour number in 12-hour clocks. /// given hour number in 12-hour clocks.
#[inline] #[inline]
pub fn set_hour12(&mut self, value: i64) -> ParseResult<()> { pub fn set_hour12(&mut self, value: i64) -> ParseResult<()> {
if value < 1 || value > 12 { if !(1..=12).contains(&value) {
return Err(OUT_OF_RANGE); return Err(OUT_OF_RANGE);
} }
set_if_consistent(&mut self.hour_mod_12, value as u32 % 12) set_if_consistent(&mut self.hour_mod_12, value as u32 % 12)

View File

@ -48,7 +48,7 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)
let mut n = 0i64; let mut n = 0i64;
for (i, c) in bytes.iter().take(max).cloned().enumerate() { for (i, c) in bytes.iter().take(max).cloned().enumerate() {
// cloned() = copied() // cloned() = copied()
if c < b'0' || b'9' < c { if !(b'0'..=b'9').contains(&c) {
if i < min { if i < min {
return Err(INVALID); return Err(INVALID);
} else { } else {
@ -79,7 +79,7 @@ pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, i64)> {
let v = v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)?; let v = v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)?;
// if there are more than 9 digits, skip next digits. // if there are more than 9 digits, skip next digits.
let s = s.trim_left_matches(|c: char| '0' <= c && c <= '9'); let s = s.trim_left_matches(|c: char| ('0'..='9').contains(&c));
Ok((s, v)) Ok((s, v))
} }

View File

@ -1887,7 +1887,7 @@ impl fmt::Debug for NaiveDate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let year = self.year(); let year = self.year();
let mdf = self.mdf(); let mdf = self.mdf();
if 0 <= year && year <= 9999 { if (0..=9999).contains(&year) {
write!(f, "{:04}-{:02}-{:02}", year, mdf.month(), mdf.day()) write!(f, "{:04}-{:02}-{:02}", year, mdf.month(), mdf.day())
} else { } else {
// ISO 8601 requires the explicit sign for out-of-range years // ISO 8601 requires the explicit sign for out-of-range years

View File

@ -297,7 +297,7 @@ impl Of {
pub(super) fn valid(&self) -> bool { pub(super) fn valid(&self) -> bool {
let Of(of) = *self; let Of(of) = *self;
let ol = of >> 3; let ol = of >> 3;
MIN_OL <= ol && ol <= MAX_OL (MIN_OL..=MAX_OL).contains(&ol)
} }
#[inline] #[inline]

View File

@ -135,7 +135,7 @@ impl fmt::Debug for IsoWeek {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let year = self.year(); let year = self.year();
let week = self.week(); let week = self.week();
if 0 <= year && year <= 9999 { if (0..=9999).contains(&year) {
write!(f, "{:04}-W{:02}", year, week) write!(f, "{:04}-W{:02}", year, week)
} else { } else {
// ISO 8601 requires the explicit sign for out-of-range years // ISO 8601 requires the explicit sign for out-of-range years

View File

@ -586,7 +586,7 @@ impl NaiveTime {
secs += 1; secs += 1;
} }
debug_assert!(-86_400 <= secs && secs < 2 * 86_400); debug_assert!(-86_400 <= secs && secs < 2 * 86_400);
debug_assert!(0 <= frac && frac < 1_000_000_000); debug_assert!((0..1_000_000_000).contains(&frac));
if secs < 0 { if secs < 0 {
secs += 86_400; secs += 86_400;
@ -595,7 +595,7 @@ impl NaiveTime {
secs -= 86_400; secs -= 86_400;
morerhssecs += 86_400; morerhssecs += 86_400;
} }
debug_assert!(0 <= secs && secs < 86_400); debug_assert!((0..86_400).contains(&secs));
(NaiveTime { secs: secs as u32, frac: frac as u32 }, morerhssecs) (NaiveTime { secs: secs as u32, frac: frac as u32 }, morerhssecs)
} }

View File

@ -365,13 +365,13 @@ fn parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error> {
fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> { fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> {
let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
if hour < 0 || hour > 24 { if !(0..=24).contains(&hour) {
return Err(Error::InvalidTzString("invalid offset hour")); return Err(Error::InvalidTzString("invalid offset hour"));
} }
if minute < 0 || minute > 59 { if !(0..=59).contains(&minute) {
return Err(Error::InvalidTzString("invalid offset minute")); return Err(Error::InvalidTzString("invalid offset minute"));
} }
if second < 0 || second > 59 { if !(0..=59).contains(&second) {
return Err(Error::InvalidTzString("invalid offset second")); return Err(Error::InvalidTzString("invalid offset second"));
} }
@ -382,13 +382,13 @@ fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> {
fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error> { fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error> {
let (hour, minute, second) = parse_hhmmss(cursor)?; let (hour, minute, second) = parse_hhmmss(cursor)?;
if hour < 0 || hour > 24 { if !(0..=24).contains(&hour) {
return Err(Error::InvalidTzString("invalid day time hour")); return Err(Error::InvalidTzString("invalid day time hour"));
} }
if minute < 0 || minute > 59 { if !(0..=59).contains(&minute) {
return Err(Error::InvalidTzString("invalid day time minute")); return Err(Error::InvalidTzString("invalid day time minute"));
} }
if second < 0 || second > 59 { if !(0..=59).contains(&second) {
return Err(Error::InvalidTzString("invalid day time second")); return Err(Error::InvalidTzString("invalid day time second"));
} }
@ -402,10 +402,10 @@ fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error> {
if hour < -167 || hour > 167 { if hour < -167 || hour > 167 {
return Err(Error::InvalidTzString("invalid day time hour")); return Err(Error::InvalidTzString("invalid day time hour"));
} }
if minute < 0 || minute > 59 { if !(0..=59).contains(&minute) {
return Err(Error::InvalidTzString("invalid day time minute")); return Err(Error::InvalidTzString("invalid day time minute"));
} }
if second < 0 || second > 59 { if !(0..=59).contains(&second) {
return Err(Error::InvalidTzString("invalid day time second")); return Err(Error::InvalidTzString("invalid day time second"));
} }
@ -496,7 +496,7 @@ impl RuleDay {
/// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
fn julian_1(julian_day_1: u16) -> Result<Self, Error> { fn julian_1(julian_day_1: u16) -> Result<Self, Error> {
if julian_day_1 < 1 || julian_day_1 > 365 { if !(1..=365).contains(&julian_day_1) {
return Err(Error::TransitionRule("invalid rule day julian day")); return Err(Error::TransitionRule("invalid rule day julian day"));
} }
@ -514,11 +514,11 @@ impl RuleDay {
/// Construct a transition rule day represented by a month, a month week and a week day /// Construct a transition rule day represented by a month, a month week and a week day
fn month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error> { fn month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error> {
if month < 1 || month > 12 { if !(1..=12).contains(&month) {
return Err(Error::TransitionRule("invalid rule day month")); return Err(Error::TransitionRule("invalid rule day month"));
} }
if week < 1 || week > 5 { if !(1..=5).contains(&week) {
return Err(Error::TransitionRule("invalid rule day week")); return Err(Error::TransitionRule("invalid rule day week"));
} }

View File

@ -89,7 +89,7 @@ impl TimeZone {
/// Construct a time zone from the contents of a time zone file /// 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). /// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536).
pub(super) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> { pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> {
parser::parse(bytes) parser::parse(bytes)
} }
@ -104,7 +104,7 @@ impl TimeZone {
} }
/// Construct the time zone associated to UTC /// Construct the time zone associated to UTC
fn utc() -> Self { pub(crate) fn utc() -> Self {
Self { Self {
transitions: Vec::new(), transitions: Vec::new(),
local_time_types: vec![LocalTimeType::UTC], local_time_types: vec![LocalTimeType::UTC],
@ -482,7 +482,7 @@ impl TimeZoneName {
fn new(input: &[u8]) -> Result<Self, Error> { fn new(input: &[u8]) -> Result<Self, Error> {
let len = input.len(); let len = input.len();
if len < 3 || len > 7 { if !(3..=7).contains(&len) {
return Err(Error::LocalTimeType( return Err(Error::LocalTimeType(
"time zone name must have between 3 and 7 characters", "time zone name must have between 3 and 7 characters",
)); ));
@ -816,15 +816,6 @@ mod tests {
let time_zone_local = TimeZone::local()?; let time_zone_local = TimeZone::local()?;
let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?; let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?;
assert_eq!(time_zone_local, time_zone_local_1); assert_eq!(time_zone_local, time_zone_local_1);
} else {
let time_zone_local = TimeZone::local()?;
let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?;
let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?;
let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?;
assert_eq!(time_zone_local, time_zone_local_1);
assert_eq!(time_zone_local, time_zone_local_2);
assert_eq!(time_zone_local, time_zone_local_3);
} }
let time_zone_utc = TimeZone::from_posix_tz("UTC")?; let time_zone_utc = TimeZone::from_posix_tz("UTC")?;

View File

@ -47,12 +47,19 @@ impl Default for Source {
// to that in `naive_to_local` // to that in `naive_to_local`
match env::var_os("TZ") { match env::var_os("TZ") {
Some(ref s) if s.to_str().is_some() => Source::Environment, Some(ref s) if s.to_str().is_some() => Source::Environment,
Some(_) | None => Source::LocalTime { Some(_) | None => match fs::symlink_metadata("/etc/localtime") {
mtime: fs::symlink_metadata("/etc/localtime") Ok(data) => Source::LocalTime {
.expect("localtime should exist") // we have to pick a sensible default when the mtime fails
.modified() // by picking SystemTime::now() we raise the probability of
.unwrap(), // the cache being invalidated if/when the mtime starts working
last_checked: SystemTime::now(), mtime: data.modified().unwrap_or_else(|_| SystemTime::now()),
last_checked: SystemTime::now(),
},
Err(_) => {
// as above, now() should be a better default than some constant
// TODO: see if we can improve caching in the case where the fallback is a valid timezone
Source::LocalTime { mtime: SystemTime::now(), last_checked: SystemTime::now() }
}
}, },
} }
} }
@ -89,10 +96,30 @@ struct Cache {
source: Source, source: Source,
} }
#[cfg(target_os = "android")]
const TZDB_LOCATION: &str = " /system/usr/share/zoneinfo";
#[allow(dead_code)] // keeps the cfg simpler
#[cfg(not(target_os = "android"))]
const TZDB_LOCATION: &str = "/usr/share/zoneinfo";
#[cfg(any(target_os = "emscripten", target_os = "wasi", target_os = "solaris"))]
fn fallback_timezone() -> Option<TimeZone> {
Some(TimeZone::utc())
}
#[cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "solaris")))]
fn fallback_timezone() -> Option<TimeZone> {
let tz_name = iana_time_zone::get_timezone().ok()?;
let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).ok()?;
TimeZone::from_tz_data(&bytes).ok()
}
impl Default for Cache { impl Default for Cache {
fn default() -> Cache { fn default() -> Cache {
// default to UTC if no local timezone can be found
Cache { Cache {
zone: TimeZone::local().expect("unable to parse localtime info"), zone: TimeZone::local().ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc),
source: Source::default(), source: Source::default(),
} }
} }