diff --git a/components/libc/compilers/common/ctime.c b/components/libc/compilers/common/ctime.c index 77ef7fab3467..32fd11468db7 100644 --- a/components/libc/compilers/common/ctime.c +++ b/components/libc/compilers/common/ctime.c @@ -167,6 +167,8 @@ struct tm *gmtime_r(const time_t *timep, struct tm *r) { int i; int work; + time_t days; + time_t seconds_of_day; if(timep == RT_NULL || r == RT_NULL) { @@ -176,20 +178,44 @@ struct tm *gmtime_r(const time_t *timep, struct tm *r) rt_memset(r, RT_NULL, sizeof(struct tm)); - work = *timep % (24*60*60); + /* + * C division truncates toward zero, so a negative timestamp yields a + * negative day count and a negative remainder. Normalise to floor division + * first, otherwise tm_sec / tm_min / tm_hour come out negative (for + * example time_t -1 used to produce 1970-01-01 00:00:-1). + */ + days = *timep / (24 * 60 * 60); + seconds_of_day = *timep % (24 * 60 * 60); + if (seconds_of_day < 0) + { + seconds_of_day += 24 * 60 * 60; + days -= 1; + } + + work = (int)seconds_of_day; r->tm_sec = work % 60; work /= 60; r->tm_min = work % 60; r->tm_hour = work / 60; - work = (int)(*timep / (24*60*60)); - r->tm_wday = (4 + work) % 7; - for (i = 1970;; ++i) + + work = (int)days; + /* 1970-01-01 was a Thursday; keep the result positive for days < 0 too */ + r->tm_wday = (int)(((4 + days % 7) % 7 + 7) % 7); + + /* + * Walk to the year. The day count can be negative (timestamps before the + * epoch), so the loop has to be able to go backwards as well. + */ + i = 1970; + while (work < 0) { - int k = __isleap(i) ? 366 : 365; - if (work >= k) - work -= k; - else - break; + --i; + work += __isleap(i) ? 366 : 365; + } + while (work >= (__isleap(i) ? 366 : 365)) + { + work -= __isleap(i) ? 366 : 365; + ++i; } r->tm_year = i - 1900; r->tm_yday = work; diff --git a/components/libc/compilers/common/utest/time_tc.c b/components/libc/compilers/common/utest/time_tc.c index 7ad227d7a47c..a7d36e5ca829 100644 --- a/components/libc/compilers/common/utest/time_tc.c +++ b/components/libc/compilers/common/utest/time_tc.c @@ -269,6 +269,74 @@ static void timegm_combined_negative_test(void) uassert_int_equal(time_value.tm_sec, 59); } +/* + * gmtime_r() against timestamps before the epoch. + * + * Every field is checked, not only the date: the defective implementation returned + * a non-NULL struct tm holding impossible values (negative tm_sec, zero tm_mday, + * negative tm_wday / tm_yday), so tm_wday and tm_yday are part of the contract + * that has to be pinned down here. + */ +struct gmtime_r_case +{ + const char *name; + time_t timestamp; + int year; /* tm_year, i.e. years since 1900 */ + int mon; /* tm_mon, 0 based */ + int mday; + int hour; + int min; + int sec; + int wday; /* 0 = Sunday */ + int yday; /* 0 based */ +}; + +static const struct gmtime_r_case gmtime_r_cases[] = +{ + /* 1970-01-01 00:00:00, Thursday */ + { "epoch zero", (time_t)0, 70, 0, 1, 0, 0, 0, 4, 0 }, + /* 1969-12-31 23:59:59, Wednesday */ + { "t = -1", (time_t)-1, 69, 11, 31, 23, 59, 59, 3, 364 }, + /* 1969-12-31 00:00:00, Wednesday */ + { "t = -86400", (time_t)-86400, 69, 11, 31, 0, 0, 0, 3, 364 }, + /* 1969-12-30 23:59:59, Tuesday */ + { "t = -86401", (time_t)-86401, 69, 11, 30, 23, 59, 59, 2, 363 }, + /* 1968-02-29 00:00:00, Thursday, 672 days before the epoch (1968 is a leap year) */ + { "1968 leap day", (time_t)-58060800, 68, 1, 29, 0, 0, 0, 4, 59 }, +}; + +/* Check every field gmtime_r() fills in, for timestamps before the epoch. */ +static void gmtime_r_before_epoch_test(void) +{ + size_t i; + + for (i = 0; i < sizeof(gmtime_r_cases) / sizeof(gmtime_r_cases[0]); i++) + { + const struct gmtime_r_case *c = &gmtime_r_cases[i]; + struct tm r; + time_t timestamp = c->timestamp; + + rt_memset(&r, 0, sizeof(r)); + + uassert_not_null(gmtime_r(×tamp, &r)); + + LOG_I("%s: %04d-%02d-%02d %02d:%02d:%02d wday=%d yday=%d", + c->name, + r.tm_year + 1900, r.tm_mon + 1, r.tm_mday, + r.tm_hour, r.tm_min, r.tm_sec, + r.tm_wday, r.tm_yday); + + uassert_int_equal(r.tm_year, c->year); + uassert_int_equal(r.tm_mon, c->mon); + uassert_int_equal(r.tm_mday, c->mday); + uassert_int_equal(r.tm_hour, c->hour); + uassert_int_equal(r.tm_min, c->min); + uassert_int_equal(r.tm_sec, c->sec); + uassert_int_equal(r.tm_wday, c->wday); + uassert_int_equal(r.tm_yday, c->yday); + } +} + /* Initialize the time conversion tests. */ static rt_err_t utest_tc_init(void) { @@ -301,6 +369,7 @@ static void timegm_all_test(void) static void testcase(void) { UTEST_UNIT_RUN(timegm_all_test); + UTEST_UNIT_RUN(gmtime_r_before_epoch_test); } UTEST_TC_EXPORT(testcase, "components.libc.time", utest_tc_init, utest_tc_cleanup, 10);