From 557bcd5f44417aaa77b9e31018b0b103f8396ef1 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Tue, 9 Aug 2022 23:43:30 +1000 Subject: [PATCH] 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 --- .github/workflows/test-release.yml | 6 ++-- .github/workflows/test.yml | 6 ++-- Cargo.toml | 5 +++- ci/github.sh | 4 +-- clippy.toml | 2 +- src/format/mod.rs | 2 +- src/format/parsed.rs | 2 +- src/format/scan.rs | 4 +-- src/naive/date.rs | 2 +- src/naive/internals.rs | 2 +- src/naive/isoweek.rs | 2 +- src/naive/time/mod.rs | 4 +-- src/offset/local/tz_info/rule.rs | 22 +++++++-------- src/offset/local/tz_info/timezone.rs | 15 ++-------- src/offset/local/unix.rs | 41 +++++++++++++++++++++++----- 15 files changed, 70 insertions(+), 49 deletions(-) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index d2d29cb350..67de1c1a18 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -31,11 +31,11 @@ jobs: - os: ubuntu-latest rust_version: nightly - os: ubuntu-20.04 - rust_version: 1.32.0 + rust_version: 1.38.0 - os: macos-latest - rust_version: 1.32.0 + rust_version: 1.38.0 - os: windows-latest - rust_version: 1.32.0 + rust_version: 1.38.0 runs-on: ${{ matrix.os }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f043ab07c5..7c66f3a607 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,11 +39,11 @@ jobs: - os: ubuntu-latest rust_version: nightly - os: ubuntu-20.04 - rust_version: 1.32.0 + rust_version: 1.38.0 - os: macos-latest - rust_version: 1.32.0 + rust_version: 1.38.0 - os: windows-latest - rust_version: 1.32.0 + rust_version: 1.38.0 runs-on: ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index e2cbaf4599..391f26f68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ default = ["clock", "std", "oldtime"] alloc = [] libc = [] std = [] -clock = ["std", "winapi"] +clock = ["std", "winapi", "iana-time-zone"] oldtime = ["time"] wasmbind = [] # TODO: empty feature to avoid breaking change in 0.4.20, can be removed later unstable-locales = ["pure-rust-locales", "alloc"] @@ -45,6 +45,9 @@ rkyv = {version = "0.7", optional = true} wasm-bindgen = { version = "0.2" } 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] winapi = { version = "0.3.0", features = ["std", "minwinbase", "minwindef", "timezoneapi"], optional = true } diff --git a/ci/github.sh b/ci/github.sh index 73d4b86042..31dbf5d53d 100755 --- a/ci/github.sh +++ b/ci/github.sh @@ -29,7 +29,7 @@ meaningful in the github actions feature matrix UI. runv cargo --version - if [[ ${RUST_VERSION:-} != 1.32.0 ]]; then + if [[ ${RUST_VERSION:-} != 1.38.0 ]]; then if [[ ${WASM:-} == yes_wasm ]]; then test_wasm elif [[ ${WASM:-} == wasm_simple ]]; then @@ -49,7 +49,7 @@ meaningful in the github actions feature matrix UI. else test_regular UTC0 fi - elif [[ ${RUST_VERSION:-} == 1.32.0 ]]; then + elif [[ ${RUST_VERSION:-} == 1.38.0 ]]; then test_132 else echo "ERROR: didn't run any tests" diff --git a/clippy.toml b/clippy.toml index 22d09a5a01..749c3b58a5 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.32" +msrv = "1.38" diff --git a/src/format/mod.rs b/src/format/mod.rs index 2089c4c06a..695ede79ce 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -529,7 +529,7 @@ fn format_inner<'a>( }; 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 match *pad { Pad::None => write!(result, "{:+}", v), diff --git a/src/format/parsed.rs b/src/format/parsed.rs index 3a72cb3d59..6fcdd60e82 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -232,7 +232,7 @@ impl Parsed { /// given hour number in 12-hour clocks. #[inline] pub fn set_hour12(&mut self, value: i64) -> ParseResult<()> { - if value < 1 || value > 12 { + if !(1..=12).contains(&value) { return Err(OUT_OF_RANGE); } set_if_consistent(&mut self.hour_mod_12, value as u32 % 12) diff --git a/src/format/scan.rs b/src/format/scan.rs index 9a40903436..7334a3b2ed 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -48,7 +48,7 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64) let mut n = 0i64; for (i, c) in bytes.iter().take(max).cloned().enumerate() { // cloned() = copied() - if c < b'0' || b'9' < c { + if !(b'0'..=b'9').contains(&c) { if i < min { return Err(INVALID); } 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)?; // 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)) } diff --git a/src/naive/date.rs b/src/naive/date.rs index 134b0506bd..dfe3598319 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -1887,7 +1887,7 @@ impl fmt::Debug for NaiveDate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let year = self.year(); let mdf = self.mdf(); - if 0 <= year && year <= 9999 { + if (0..=9999).contains(&year) { write!(f, "{:04}-{:02}-{:02}", year, mdf.month(), mdf.day()) } else { // ISO 8601 requires the explicit sign for out-of-range years diff --git a/src/naive/internals.rs b/src/naive/internals.rs index 43b769df39..f5f0bc9f50 100644 --- a/src/naive/internals.rs +++ b/src/naive/internals.rs @@ -297,7 +297,7 @@ impl Of { pub(super) fn valid(&self) -> bool { let Of(of) = *self; let ol = of >> 3; - MIN_OL <= ol && ol <= MAX_OL + (MIN_OL..=MAX_OL).contains(&ol) } #[inline] diff --git a/src/naive/isoweek.rs b/src/naive/isoweek.rs index 31d44cb602..b3ccecdedd 100644 --- a/src/naive/isoweek.rs +++ b/src/naive/isoweek.rs @@ -135,7 +135,7 @@ impl fmt::Debug for IsoWeek { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let year = self.year(); let week = self.week(); - if 0 <= year && year <= 9999 { + if (0..=9999).contains(&year) { write!(f, "{:04}-W{:02}", year, week) } else { // ISO 8601 requires the explicit sign for out-of-range years diff --git a/src/naive/time/mod.rs b/src/naive/time/mod.rs index efb0ae61c0..a810c727b8 100644 --- a/src/naive/time/mod.rs +++ b/src/naive/time/mod.rs @@ -586,7 +586,7 @@ impl NaiveTime { secs += 1; } 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 { secs += 86_400; @@ -595,7 +595,7 @@ impl NaiveTime { secs -= 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) } diff --git a/src/offset/local/tz_info/rule.rs b/src/offset/local/tz_info/rule.rs index 7e9ed7e98d..69f6264e7a 100644 --- a/src/offset/local/tz_info/rule.rs +++ b/src/offset/local/tz_info/rule.rs @@ -365,13 +365,13 @@ fn parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error> { fn parse_offset(cursor: &mut Cursor) -> Result { 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")); } - if minute < 0 || minute > 59 { + if !(0..=59).contains(&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")); } @@ -382,13 +382,13 @@ fn parse_offset(cursor: &mut Cursor) -> Result { fn parse_rule_time(cursor: &mut Cursor) -> Result { 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")); } - if minute < 0 || minute > 59 { + if !(0..=59).contains(&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")); } @@ -402,10 +402,10 @@ fn parse_rule_time_extended(cursor: &mut Cursor) -> Result { if hour < -167 || hour > 167 { 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")); } - if second < 0 || second > 59 { + if !(0..=59).contains(&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 fn julian_1(julian_day_1: u16) -> Result { - 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")); } @@ -514,11 +514,11 @@ impl RuleDay { /// 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 { - if month < 1 || month > 12 { + if !(1..=12).contains(&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")); } diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index 6401fdec33..085b25c117 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -89,7 +89,7 @@ impl TimeZone { /// 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(super) fn from_tz_data(bytes: &[u8]) -> Result { + pub(crate) fn from_tz_data(bytes: &[u8]) -> Result { parser::parse(bytes) } @@ -104,7 +104,7 @@ impl TimeZone { } /// Construct the time zone associated to UTC - fn utc() -> Self { + pub(crate) fn utc() -> Self { Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::UTC], @@ -482,7 +482,7 @@ impl TimeZoneName { fn new(input: &[u8]) -> Result { let len = input.len(); - if len < 3 || len > 7 { + if !(3..=7).contains(&len) { return Err(Error::LocalTimeType( "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_1 = TimeZone::from_posix_tz(&tz)?; 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")?; diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 6cec6b751c..ab91dfde38 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -47,12 +47,19 @@ impl Default for Source { // to that in `naive_to_local` match env::var_os("TZ") { Some(ref s) if s.to_str().is_some() => Source::Environment, - Some(_) | None => Source::LocalTime { - mtime: fs::symlink_metadata("/etc/localtime") - .expect("localtime should exist") - .modified() - .unwrap(), - last_checked: SystemTime::now(), + Some(_) | None => match fs::symlink_metadata("/etc/localtime") { + Ok(data) => Source::LocalTime { + // we have to pick a sensible default when the mtime fails + // by picking SystemTime::now() we raise the probability of + // the cache being invalidated if/when the mtime starts working + 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, } +#[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 { + Some(TimeZone::utc()) +} + +#[cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "solaris")))] +fn fallback_timezone() -> Option { + 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 { fn default() -> Cache { + // default to UTC if no local timezone can be found 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(), } }