diff --git a/src/header/time/mod.rs b/src/header/time/mod.rs index bb512797f8bcbc5dee593844f707c6d6bfb71c68..c22af065086b74ecdfa69d4b2fa4a06ff3452580 100644 --- a/src/header/time/mod.rs +++ b/src/header/time/mod.rs @@ -1,7 +1,9 @@ //! time implementation for Redox, following http://pubs.opengroup.org/onlinepubs/7908799/xsh/time.h.html +use core::convert::{TryFrom, TryInto}; + use crate::{ - header::errno::EIO, + header::errno::{EIO, EOVERFLOW}, platform::{self, types::*, Pal, Sys}, }; @@ -188,81 +190,92 @@ fn leap_year(year: c_int) -> bool { #[no_mangle] pub unsafe extern "C" fn gmtime_r(clock: *const time_t, result: *mut tm) -> *mut tm { - let clock = *clock; - - let mut day = (clock / (60 * 60 * 24)) as c_int; - if clock < 0 && clock % (60 * 60 * 24) != 0 { - // -1 because for negative values round upwards - // -0.3 == 0, but we want -1 - day -= 1; - } - - (*result).tm_sec = (clock % 60) as c_int; - (*result).tm_min = ((clock / 60) % 60) as c_int; - (*result).tm_hour = ((clock / (60 * 60)) % 24) as c_int; - - while (*result).tm_sec < 0 { - (*result).tm_sec += 60; - (*result).tm_min -= 1; - } - while (*result).tm_min < 0 { - (*result).tm_min += 60; - (*result).tm_hour -= 1; - } - while (*result).tm_hour < 0 { - (*result).tm_hour += 24; - } - - // Jan 1th was a thursday, 4th of a zero-indexed week. - (*result).tm_wday = (day + 4) % 7; - if (*result).tm_wday < 0 { - (*result).tm_wday += 7; - } - - let mut year = 1970; - if day < 0 { - while day < 0 { - let days_in_year = if leap_year(year) { 366 } else { 365 }; - - day += days_in_year; - year -= 1; + /* For the details of the algorithm used here, see + * http://howardhinnant.github.io/date_algorithms.html#civil_from_days + * Note that we need 0-based months here, though. + * Overall, this implementation should generate correct results as + * long as the tm_year value will fit in a c_int. */ + const SECS_PER_DAY: time_t = 24*60*60; + const DAYS_PER_ERA: time_t = 146097; + + let unix_secs = *clock; + + /* Day number here is possibly negative, remainder will always be + * nonnegative when using Euclidean division */ + let unix_days: time_t = unix_secs.div_euclid(SECS_PER_DAY); + + /* In range [0, 86399]. Needs a u32 since this is larger (at least + * theoretically) than the guaranteed range of c_int */ + let secs_of_day: u32 = unix_secs.rem_euclid(SECS_PER_DAY).try_into().unwrap(); + + /* Shift origin from 1970-01-01 to 0000-03-01 and find out where we + * are in terms of 400-year eras since then */ + let days_since_origin = unix_days + 719468; + let era = days_since_origin.div_euclid(DAYS_PER_ERA); + let day_of_era = days_since_origin.rem_euclid(DAYS_PER_ERA); + let year_of_era = (day_of_era - day_of_era/1460 + day_of_era/36524 - day_of_era/146096) / 365; + + /* "transformed" here refers to dates in a calendar where years + * start on March 1 */ + let year_transformed = year_of_era + 400*era; // retain large range, don't convert to c_int yet + let day_of_year_transformed: c_int = (day_of_era - (365*year_of_era + year_of_era/4 - year_of_era/100)).try_into().unwrap(); + let month_transformed: c_int = (5*day_of_year_transformed + 2)/153; + + // Convert back to calendar with year starting on January 1 + let month: c_int = (month_transformed + 2) % 12; // adapted to 0-based months + let year: time_t = if month < 2 {year_transformed + 1} else {year_transformed}; + + /* Subtract 1900 *before* converting down to c_int in order to + * maximize the range of input timestamps that will succeed */ + match c_int::try_from(year - 1900) { + Ok(year_less_1900) => { + let mday: c_int = (day_of_year_transformed - (153*month_transformed+2)/5 + 1).try_into().unwrap(); + + /* 1970-01-01 was a Thursday. Again, Euclidean division is + * used to ensure a nonnegative remainder (range [0, 6]). */ + let wday: c_int = ((unix_days + 4).rem_euclid(7)).try_into().unwrap(); + + /* Yes, duplicated code for now (to work on non-c_int-values + * so that we are not constrained by the subtraction of + * 1900) */ + let is_leap_year: bool = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + + /* For dates that are March 1 or later, we can use day-of- + * year in the transformed calendar. For January and + * February, that value is sensitive to whether the previous + * year is a leap year. Therefore, we use the already + * computed date for those two months. */ + let yday: c_int = match month { + 0 => mday - 1, // January + 1 => 31 + mday - 1, // February + _ => day_of_year_transformed + if is_leap_year {60} else {59}, + }; + + let hour: c_int = (secs_of_day / (60*60)).try_into().unwrap(); + let min: c_int = ((secs_of_day / 60) % 60).try_into().unwrap(); + let sec: c_int = (secs_of_day % 60).try_into().unwrap(); + + *result = tm { + tm_sec: sec, + tm_min: min, + tm_hour: hour, + tm_mday: mday, + tm_mon: month, + tm_year: year_less_1900, + tm_wday: wday, + tm_yday: yday, + tm_isdst: 0, + tm_gmtoff: 0, + tm_zone: UTC, + }; + + result } - (*result).tm_year = year - 1900; - (*result).tm_yday = day + 1; - } else { - loop { - let days_in_year = if leap_year(year) { 366 } else { 365 }; - - if day < days_in_year { - break; - } - - day -= days_in_year; - year += 1; - } - (*result).tm_year = year - 1900; - (*result).tm_yday = day; - } - - let leap = if leap_year(year) { 1 } else { 0 }; - (*result).tm_mon = 0; - loop { - let days_in_month = MONTH_DAYS[leap][(*result).tm_mon as usize]; - - if day < days_in_month { - break; + Err(_) => { + platform::errno = EOVERFLOW; + core::ptr::null_mut() } - - day -= days_in_month; - (*result).tm_mon += 1; } - (*result).tm_mday = 1 + day as c_int; - - (*result).tm_isdst = 0; - (*result).tm_gmtoff = 0; - (*result).tm_zone = UTC; - - result } #[no_mangle]