#include #include #include #include #include #include #include #include #include #include #if __MLIBC_POSIX_OPTION #include #include #endif #include #include #include #include #include #include #include #include #include #include // The DST rules to use if TZ has no rules and we can't load posixinfo. // POSIX does not specify the default DST rules, for historical reasons // America/New_York is a common default. #define TZ_DEFAULT_RULE_STRING ",M3.2.0,M11.1.0" const char __utc[] = "UTC"; // Variables defined by POSIX. int daylight; long timezone; char *tzname[2]; static FutexLock __time_lock; // Function taken from musl clock_t clock(void) { struct timespec ts; if(clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts)) return -1; if(ts.tv_sec > LONG_MAX / 1000000 || ts.tv_nsec / 1000 > LONG_MAX - 1000000 * ts.tv_sec) return -1; return ts.tv_sec * 1000000 + ts.tv_nsec / 1000; } double difftime(time_t a, time_t b) { return a - b; } time_t mktime(struct tm *tm) { return timegm(tm); } /* There is no other implemented value than TIME_UTC; all other values * are considered erroneous. */ // Function taken from musl int timespec_get(struct timespec *ts, int base) { if(base != TIME_UTC) return 0; int ret = clock_gettime(CLOCK_REALTIME, ts); return ret < 0 ? 0 : base; } char *asctime(const struct tm *ptr) { static char buf[26]; return asctime_r(ptr, buf); } char *ctime(const time_t *timer) { struct tm *tm = localtime(timer); if(!tm) { return nullptr; } return asctime(tm); } struct tm *gmtime(const time_t *unix_gmt) { static thread_local struct tm per_thread_tm; return gmtime_r(unix_gmt, &per_thread_tm); } struct tm *localtime(const time_t *unix_gmt) { tzset(); static thread_local struct tm per_thread_tm; return localtime_r(unix_gmt, &per_thread_tm); } size_t strftime(char *__restrict dest, size_t max_size, const char *__restrict format, const struct tm *__restrict tm) { auto c = format; auto p = dest; [[maybe_unused]] bool use_alternative_symbols = false; [[maybe_unused]] bool use_alternative_era_format = false; while(*c) { int chunk; auto space = (dest + max_size) - p; __ensure(space >= 0); if(*c != '%') { if(!space) return 0; *p = *c; c++; p++; continue; } if(*(c + 1) == 'O') { std::array valid{{'B', 'b', 'd', 'e', 'H', 'I', 'm', 'M', 'S', 'u', 'U', 'V', 'w', 'W', 'y'}}; auto next = *(c + 2); if(std::find(valid.begin(), valid.end(), next) != valid.end()) { use_alternative_symbols = true; c++; } else { *p = '%'; p++; c++; *p = 'O'; p++; c++; continue; } } else if(*(c + 1) == 'E') { std::array valid{{'c', 'C', 'x', 'X', 'y', 'Y'}}; auto next = *(c + 2); if(std::find(valid.begin(), valid.end(), next) != valid.end()) { use_alternative_era_format = true; c++; } else { *p = '%'; p++; c++; *p = 'E'; p++; c++; continue; } } switch(*++c) { case 'Y': { chunk = snprintf(p, space, "%d", 1900 + tm->tm_year); if(chunk >= space) return 0; p += chunk; c++; break; } case 'm': { chunk = snprintf(p, space, "%.2d", tm->tm_mon + 1); if(chunk >= space) return 0; p += chunk; c++; break; } case 'd': { chunk = snprintf(p, space, "%.2d", tm->tm_mday); if(chunk >= space) return 0; p += chunk; c++; break; } case 'z': { auto min = tm->tm_gmtoff / 60; auto diff = ((min / 60) * 100) + (min % 60); chunk = snprintf(p, space, "%c%04d", diff >= 0 ? '+' : '-', abs(diff)); if(chunk >= space) return 0; p += chunk; c++; break; } case 'Z': { chunk = snprintf(p, space, "%s", "UTC"); if(chunk >= space) return 0; p += chunk; c++; break; } case 'H': { chunk = snprintf(p, space, "%.2i", tm->tm_hour); if(chunk >= space) return 0; p += chunk; c++; break; } case 'M': { chunk = snprintf(p, space, "%.2i", tm->tm_min); if(chunk >= space) return 0; p += chunk; c++; break; } case 'S': { chunk = snprintf(p, space, "%.2d", tm->tm_sec); if(chunk >= space) return 0; p += chunk; c++; break; } case 'R': { chunk = snprintf(p, space, "%.2i:%.2i", tm->tm_hour, tm->tm_min); if(chunk >= space) return 0; p += chunk; c++; break; } case 'T': { chunk = snprintf(p, space, "%.2i:%.2i:%.2i", tm->tm_hour, tm->tm_min, tm->tm_sec); if(chunk >= space) return 0; p += chunk; c++; break; } case 'F': { chunk = snprintf(p, space, "%d-%.2d-%.2d", 1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday); if(chunk >= space) return 0; p += chunk; c++; break; } case 'D': { chunk = snprintf(p, space, "%.2d/%.2d/%.2d", tm->tm_mon + 1, tm->tm_mday, (tm->tm_year + 1900) % 100); if(chunk >= space) return 0; p += chunk; c++; break; } case 'a': { int day = tm->tm_wday; if(day < 0 || day > 6) __ensure(!"Day not in bounds."); chunk = snprintf(p, space, "%s", mlibc::nl_langinfo(ABDAY_1 + day)); if(chunk >= space) return 0; p += chunk; c++; break; } case 'b': case 'B': case 'h': { int mon = tm->tm_mon; if(mon < 0 || mon > 11) __ensure(!"Month not in bounds."); nl_item item = (*c == 'B') ? MON_1 : ABMON_1; chunk = snprintf(p, space, "%s", mlibc::nl_langinfo(item + mon)); if(chunk >= space) return 0; p += chunk; c++; break; } case 'c': { int day = tm->tm_wday; if(day < 0 || day > 6) __ensure(!"Day not in bounds."); int mon = tm->tm_mon; if(mon < 0 || mon > 11) __ensure(!"Month not in bounds."); chunk = snprintf(p, space, "%s %s %2d %.2i:%.2i:%.2d %d", mlibc::nl_langinfo(ABDAY_1 + day), mlibc::nl_langinfo(ABMON_1 + mon), tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, 1900 + tm->tm_year); if(chunk >= space) return 0; p += chunk; c++; break; } case 'e': { chunk = snprintf(p, space, "%2d", tm->tm_mday); if(chunk >= space) return 0; p += chunk; c++; break; } case 'l': { int hour = tm->tm_hour; if(!hour) hour = 12; if(hour > 12) hour -= 12; chunk = snprintf(p, space, "%2d", hour); if(chunk >= space) return 0; p += chunk; c++; break; } case 'k': { chunk = snprintf(p, space, "%2d", tm->tm_hour); if(chunk >= space) return 0; p += chunk; c++; break; } case 'I': { int hour = tm->tm_hour; if(!hour) hour = 12; if(hour > 12) hour -= 12; chunk = snprintf(p, space, "%.2d", hour); if(chunk >= space) return 0; p += chunk; c++; break; } case 'p': { chunk = snprintf(p, space, "%s", mlibc::nl_langinfo((tm->tm_hour < 12) ? AM_STR : PM_STR)); if(chunk >= space) return 0; p += chunk; c++; break; } case 'P': { char *str = mlibc::nl_langinfo((tm->tm_hour < 12) ? AM_STR : PM_STR); char *str_lower = reinterpret_cast(getAllocator().allocate(strlen(str) + 1)); for(size_t i = 0; str[i]; i++) str_lower[i] = tolower(str[i]); str_lower[strlen(str)] = '\0'; chunk = snprintf(p, space, "%s", str_lower); if(chunk >= space) return 0; p += chunk; c++; break; } case 'C': { chunk = snprintf(p, space, "%.2d", (1900 + tm->tm_year) / 100); if(chunk >= space) return 0; p += chunk; c++; break; } case 'y': { chunk = snprintf(p, space, "%.2d", (1900 + tm->tm_year) % 100); if(chunk >= space) return 0; p += chunk; c++; break; } case 'j': { chunk = snprintf(p, space, "%.3d", tm->tm_yday + 1); if(chunk >= space) return 0; p += chunk; c++; break; } case 'A': { chunk = snprintf(p, space, "%s", mlibc::nl_langinfo(DAY_1 + tm->tm_wday)); if(chunk >= space) return 0; p += chunk; c++; break; } case 'r': { int hour = tm->tm_hour; if(!hour) hour = 12; if(hour > 12) hour -= 12; chunk = snprintf(p, space, "%.2i:%.2i:%.2i %s", hour, tm->tm_min, tm->tm_sec, mlibc::nl_langinfo((tm->tm_hour < 12) ? AM_STR : PM_STR)); if(chunk >= space) return 0; p += chunk; c++; break; } case '%': { chunk = snprintf(p, space, "%%"); if(chunk >= space) return 0; p += chunk; c++; break; } case 'n': { chunk = snprintf(p, space, "\n"); if(chunk >= space) return 0; p += chunk; c++; break; } case 't': { chunk = snprintf(p, space, "\t"); if(chunk >= space) return 0; p += chunk; c++; break; } case 'x': { return strftime(dest, max_size, mlibc::nl_langinfo(D_FMT), tm); } case 'X': { return strftime(dest, max_size, mlibc::nl_langinfo(T_FMT), tm); } case '\0': { chunk = snprintf(p, space, "%%"); if(chunk >= space) return 0; p += chunk; break; } default: mlibc::panicLogger() << "mlibc: strftime unknown format type: " << c << frg::endlog; } } auto space = (dest + max_size) - p; if(!space) return 0; *p = '\0'; return (p - dest); } size_t wcsftime(wchar_t *__restrict, size_t, const wchar_t *__restrict, const struct tm *__restrict) { mlibc::infoLogger() << "mlibc: wcsftime is a stub" << frg::endlog; return 0; } namespace { // Given a pointer to a timezone string, extract a number and check if it's in // range; if it's not, return NULL. Otherwise, return a pointer to the first // character not part of the number. template const char *getnum(const char *str, T *nump, T min, T max) { if (str == nullptr || !isdigit(*str)) return nullptr; char c = *str; T num = 0; do { num = num * 10 + (c - '0'); if (num > max) return nullptr; c = *++str; } while (isdigit(c)); if (num < min) return nullptr; *nump = num; return str; } // Given a pointer into a timezone string, extract an offset, in // [+-]hh[:mm[:ss]] form. If any error occurs, return NULL. Otherwise, return a // pointer to the first character not part of the time. const char *getoffset(const char *str, long *offset) { bool negative = false; if (*str == '-') { negative = true; str++; } else if (*str == '+') { str++; } unsigned int num; // `24 * 7 - 1` allows for quasi-POSIX rules like "M10.4.6/26", which does // not conform to POSIX, but specifies the equivalent of "02:00 on the // first Sunday on or after 23 Oct". str = getnum(str, &num, 0, 24 * 7 - 1); if (str == nullptr) return nullptr; *offset = num * 60 * 60; if (*str == ':') { str++; str = getnum(str, &num, 0, 59); if (str == nullptr) return nullptr; *offset += num * 60; if (*str == ':') { str++; // Allows for leap seconds. str = getnum(str, &num, 0, 60); if (str == nullptr) return nullptr; *offset += num; } } if (negative) *offset *= -1; return str; } enum RuleType { TZFILE, // mlibc-internal rule type for TZ files JULIAN_DAY, // Jn = Julian day DAY_OF_YEAR, // n = day of year MONTH_NTH_DAY_OF_WEEK, // Mm.n.d = month, week, day of week }; struct Rule { RuleType type; uint16_t day; uint8_t week; uint8_t month; long time; }; // Given a pointer into a timezone string, extract a rule in the form // date[/time]. If a valid rule is not found, return NULL; otherwise, return a // pointer to the first character not part of the rule. const char *getrule(const char *str, Rule *rule) { if (*str == 'J') { // Julian day rule->type = JULIAN_DAY; str++; str = getnum(str, &rule->day, 1, 365); } else if (*str == 'M') { // Month, week, day rule->type = MONTH_NTH_DAY_OF_WEEK; str++; str = getnum(str, &rule->month, 1, 12); if (str == nullptr) return nullptr; if (*str++ != '.') return nullptr; str = getnum(str, &rule->week, 1, 5); if (str == nullptr) return nullptr; if (*str++ != '.') return nullptr; str = getnum(str, &rule->day, 0, 6); } else if (isdigit(*str)) { // Day of year rule->type = DAY_OF_YEAR; str = getnum(str, &rule->day, 0, 365); } else { return nullptr; } if (str == nullptr) return nullptr; if (*str == '/') { str++; str = getoffset(str, &rule->time); } else { // Fallback to 02:00:00. rule->time = 2 * 60 * 60; } return str; } struct[[gnu::packed]] ttinfo { int32_t tt_gmtoff; unsigned char tt_isdst; unsigned char tt_abbrind; }; // Let's just assume there's a maximum of two for now. ttinfo tt_infos[2]; Rule rules[2]; bool parse_tz(const char *tz, char *tz_name, char *tz_name_dst, size_t tz_name_max) { // POSIX defines :*characters* as a valid but implementation-defined format. // glibc ignores the initial colon and parses the rest as TZ. if (*tz == ':') tz++; // The timezone name may be wrapped in angle brackets, in which case we // parse them in quoted mode. bool quoted = false; if (*tz == '<') { quoted = true; tz++; } // Try parsing the timezone name. auto *tzn = tz; size_t tzn_len = 0; for (;; tz++) { tzn_len = tz - tzn; if (*tz == '\0') break; if (tzn_len > tz_name_max) return true; // Advance until the end of the timezone name. if (isalpha(*tz)) continue; if (quoted && (*tz == '+' || *tz == '-' || isdigit(*tz))) continue; // Check if the timezone name has a valid length. if (tzn_len < 3) return true; // Consume the terminating angle bracket. if (quoted && *tz == '>') { tz++; } else if (quoted) { mlibc::infoLogger() << "mlibc: TZ name has unclosed angle bracket" << frg::endlog; return true; } break; } long offset = 0; tz = getoffset(tz, &offset); if (tz == nullptr) return true; // If we're here, this MUST be of the POSIX timezone format. // Write the TZ name to the buffer passed to the function. memcpy(tz_name, tzn, tzn_len); tz_name[tzn_len] = '\0'; timezone = offset; tt_infos[0].tt_gmtoff = -offset; tt_infos[0].tt_isdst = false; tt_infos[0].tt_abbrind = 0; // If there's nothing left to parse, we should set tz_name_dst to tz_name. // This matches glibc behaviour. if (*tz == '\0') { memcpy(tz_name_dst, tzn, tzn_len); tz_name_dst[tzn_len] = '\0'; return false; } // From now on, we won't return an error but silently stop parsing. This // makes a parsing error on the rest of the TZ environment variable not // prevent setting the values we parsed before this point. This matches // glibc behaviour. // The timezone name may be wrapped in angle brackets, in which case we // parse them in quoted mode. quoted = false; if (*tz == '<') { quoted = true; tz++; } // Try parsing the alternate timezone (DST) name. auto *tzn_dst = tz; size_t tzn_len_dst = 0; for (;; tz++) { tzn_len_dst = tz - tzn_dst; if (*tz == '\0') break; if (tzn_len_dst > tz_name_max) return false; // Advance until the end of the timezone name. if (isalpha(*tz)) continue; if (quoted && (*tz == '+' || *tz == '-' || isdigit(*tz))) continue; // Check if the timezone name has a valid length. if (tzn_len_dst < 3) return false; // Consume the terminating angle bracket. if (quoted && *tz == '>') { tz++; } else if (quoted) { mlibc::infoLogger() << "mlibc: TZ name has unclosed angle bracket" << frg::endlog; return false; } break; } // Write the TZ name to the buffer passed to the function. memcpy(tz_name_dst, tzn_dst, tzn_len_dst); tz_name_dst[tzn_len_dst] = '\0'; // Fallback to 1 hour ahead of standard time. long offset_dst = offset - 60 * 60; if (*tz != '\0' && *tz != ',') { tz = getoffset(tz, &offset_dst); if (tz == nullptr) return false; } // TODO: Attempt to fallback to posixrules before falling back to this. if (*tz == '\0') tz = TZ_DEFAULT_RULE_STRING; if (*tz == ',') { tz++; tz = getrule(tz, &rules[0]); if (tz == nullptr) return false; if (*tz != ',') return false; tz++; tz = getrule(tz, &rules[1]); if (tz == nullptr) return false; if (*tz != '\0') return false; } else { return false; } tt_infos[1].tt_gmtoff = -offset_dst; tt_infos[1].tt_isdst = true; tt_infos[1].tt_abbrind = 0; daylight = 1; return false; } struct tzfile { uint8_t magic[4]; uint8_t version; uint8_t reserved[15]; uint32_t tzh_ttisgmtcnt; uint32_t tzh_ttisstdcnt; uint32_t tzh_leapcnt; uint32_t tzh_timecnt; uint32_t tzh_typecnt; uint32_t tzh_charcnt; }; frg::string parse_tzfile_path(const char *tz) { // POSIX defines :*characters* as a valid but implementation-defined format. // This was originally introduced as a way to support geographical // timezones in the format :Area/Location, but the colon was dropped in POSIX. if (*tz == ':') tz++; frg::string path {getAllocator()}; // TODO: generic path helpers in options/internal? if (*tz == '/') { path += tz; } else if (*tz == '.') { // FIXME: Figure out what we actually need to do in this case, consider // supporting relative paths or defaulting to UTC instead. mlibc::infoLogger() << "mlibc: relative path in TZ not supported, " "defaulting to /etc/localtime" << frg::endlog; path += "/etc/localtime"; } else { const char *tzdir = getenv("TZDIR"); if (tzdir == nullptr || *tzdir == '\0') { tzdir = "/usr/share/zoneinfo"; } else if (*tzdir != '/') { mlibc::infoLogger() << "mlibc: non-absolute path in TZDIR not " "supported, defaulting to /usr/share/zoneinfo" << frg::endlog; tzdir = "/usr/share/zoneinfo"; } path += tzdir; path += "/"; path += tz; } return path; } bool parse_tzfile(const char *tz) { frg::string path = parse_tzfile_path(tz); // Check if file exists, otherwise fallback to the default. if (!mlibc::sys_stat) { MLIBC_MISSING_SYSDEP(); __ensure(!"cannot proceed without sys_stat"); } struct stat info; if (mlibc::sys_stat(mlibc::fsfd_target::path, -1, path.data(), 0, &info)) return true; // FIXME: Make this fallible so the above check is not needed. file_window window {path.data()}; // TODO(geert): we can probably cache this somehow tzfile tzfile_time; memcpy(&tzfile_time, reinterpret_cast(window.get()), sizeof(tzfile)); tzfile_time.tzh_ttisgmtcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_ttisgmtcnt); tzfile_time.tzh_ttisstdcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_ttisstdcnt); tzfile_time.tzh_leapcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_leapcnt); tzfile_time.tzh_timecnt = mlibc::bit_util::byteswap(tzfile_time.tzh_timecnt); tzfile_time.tzh_typecnt = mlibc::bit_util::byteswap(tzfile_time.tzh_typecnt); tzfile_time.tzh_charcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_charcnt); if (tzfile_time.magic[0] != 'T' || tzfile_time.magic[1] != 'Z' || tzfile_time.magic[2] != 'i' || tzfile_time.magic[3] != 'f') { mlibc::infoLogger() << "mlibc: " << path << " is not a valid TZinfo file" << frg::endlog; return true; } if (tzfile_time.version != '\0' && tzfile_time.version != '2' && tzfile_time.version != '3') { mlibc::infoLogger() << "mlibc: " << path << " has an invalid TZinfo version" << frg::endlog; return true; } // There should be at least one entry in the ttinfo table. if (!tzfile_time.tzh_typecnt) return true; char *abbrevs = reinterpret_cast(window.get()) + sizeof(tzfile) + tzfile_time.tzh_timecnt * sizeof(int32_t) + tzfile_time.tzh_timecnt * sizeof(uint8_t) + tzfile_time.tzh_typecnt * sizeof(struct ttinfo); bool found_std = false; bool found_dst = false; // start from the last ttinfo entry, this matches the behaviour of glibc and musl for (int i = tzfile_time.tzh_typecnt; i > 0; i--) { ttinfo time_info; memcpy(&time_info, reinterpret_cast(window.get()) + sizeof(tzfile) + tzfile_time.tzh_timecnt * sizeof(int32_t) + tzfile_time.tzh_timecnt * sizeof(uint8_t) + i * sizeof(ttinfo), sizeof(ttinfo)); time_info.tt_gmtoff = mlibc::bit_util::byteswap(time_info.tt_gmtoff); if (!time_info.tt_isdst && !found_std) { tzname[0] = abbrevs + time_info.tt_abbrind; timezone = -time_info.tt_gmtoff; found_std = true; } if (time_info.tt_isdst && !found_dst) { tzname[1] = abbrevs + time_info.tt_abbrind; timezone = -time_info.tt_gmtoff; daylight = 1; found_dst = true; } if (found_std && found_dst) break; } rules[0].type = TZFILE; rules[1].type = TZFILE; return false; } // Assumes __time_lock is taken // TODO(geert): this function doesn't properly handle the case where // information might be missing from the tzinfo file void do_tzset(void) { const char *tz = getenv("TZ"); if (tz == nullptr) tz = "/etc/localtime"; if (*tz == '\0') tz = "UTC0"; size_t tz_name_max = TZNAME_MAX; #if __MLIBC_POSIX_OPTION if (long sc_tz_name_max = sysconf(_SC_TZNAME_MAX); sc_tz_name_max > TZNAME_MAX) tz_name_max = static_cast(sc_tz_name_max); #endif // 1 byte for null char *tz_name = (char *) malloc(tz_name_max + 1); char *tz_name_dst = (char *) malloc(tz_name_max + 1); memset(tz_name, 0, tz_name_max + 1); memset(tz_name_dst, 0, tz_name_max + 1); // Reset daylight in case the TZ environment variable changed. daylight = 0; if (!parse_tz(tz, tz_name, tz_name_dst, tz_name_max)) { tzname[0] = tz_name; tzname[1] = tz_name_dst; return; } // Try parsing as a geographic timezone. if (parse_tzfile(tz)) { // This should always succeed. __ensure(!parse_tz("UTC0", tz_name, tz_name_dst, tz_name_max)); tzname[0] = tz_name; tzname[1] = tz_name_dst; } } } // namespace void tzset(void) { frg::unique_lock lock(__time_lock); do_tzset(); } // POSIX extensions. int nanosleep(const struct timespec *req, struct timespec *) { if (req->tv_sec < 0 || req->tv_nsec > 999999999 || req->tv_nsec < 0) { errno = EINVAL; return -1; } if(!mlibc::sys_sleep) { MLIBC_MISSING_SYSDEP(); __ensure(!"Cannot continue without sys_sleep()"); } struct timespec tmp = *req; int e = mlibc::sys_sleep(&tmp.tv_sec, &tmp.tv_nsec); if (!e) { return 0; } else { errno = e; return -1; } } int clock_getres(clockid_t clockid, struct timespec *res) { MLIBC_CHECK_OR_ENOSYS(mlibc::sys_clock_getres, -1); if(int e = mlibc::sys_clock_getres(clockid, &res->tv_sec, &res->tv_nsec); e) { errno = e; return -1; } return 0; } int clock_gettime(clockid_t clock, struct timespec *time) { if(int e = mlibc::sys_clock_get(clock, &time->tv_sec, &time->tv_nsec); e) { errno = e; return -1; } return 0; } int clock_nanosleep(clockid_t clockid, int, const struct timespec *req, struct timespec *) { mlibc::infoLogger() << "clock_nanosleep is implemented as nanosleep!" << frg::endlog; __ensure(clockid == CLOCK_REALTIME || clockid == CLOCK_MONOTONIC); return nanosleep(req, nullptr); } int clock_settime(clockid_t clock, const struct timespec *time) { MLIBC_CHECK_OR_ENOSYS(mlibc::sys_clock_set, -1); if(int e = mlibc::sys_clock_set(clock, time->tv_sec, time->tv_nsec); e) { errno = e; return -1; } return 0; } time_t time(time_t *out) { time_t secs; long nanos; if(int e = mlibc::sys_clock_get(CLOCK_REALTIME, &secs, &nanos); e) { errno = e; return (time_t)-1; } if(out) *out = secs; return secs; } namespace { void civil_from_days(time_t days_since_epoch, int *year, unsigned int *month, unsigned int *day) { time_t time = days_since_epoch + 719468; int era = (time >= 0 ? time : time - 146096) / 146097; unsigned int doe = static_cast(time - era * 146097); unsigned int yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; int y = static_cast(yoe) + era * 400; unsigned int doy = doe - (365*yoe + yoe/4 - yoe/100); unsigned int mp = (5*doy + 2)/153; unsigned int d = doy - (153*mp+2)/5 + 1; unsigned int m = mp + (mp < 10 ? 3 : -9); *year = y + (m <= 2); *month = m; *day = d; } void weekday_from_days(time_t days_since_epoch, unsigned int *weekday) { *weekday = static_cast(days_since_epoch >= -4 ? (days_since_epoch+4) % 7 : (days_since_epoch+5) % 7 + 6); } void yearday_from_date(unsigned int year, unsigned int month, unsigned int day, unsigned int *yday) { unsigned int n1 = 275 * month / 9; unsigned int n2 = (month + 9) / 12; unsigned int n3 = (1 + (year - 4 * year / 4 + 2) / 3); *yday = n1 - (n2 * n3) + day - 30; } static bool is_leap_year(int year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } // Given a rule and a year, compute the time of the transition in seconds since the epoch. // TODO: Take into account the time of day when the transition occurs time_t time_from_rule(const Rule &rule, int year) { if (rule.type == JULIAN_DAY) { // Jn: Julian day, ignoring Feb 29 uint16_t day = rule.day - 1; if (is_leap_year(year) && day >= 60) day = rule.day; struct tm t = {}; t.tm_year = year - 1900; t.tm_yday = day; return mktime(&t); } else if (rule.type == DAY_OF_YEAR) { // n: zero-based day of year, including Feb 29 in leap years struct tm t = {}; t.tm_year = year - 1900; t.tm_yday = rule.day; return mktime(&t); } else if (rule.type == MONTH_NTH_DAY_OF_WEEK) { // Mm.n.d: Month, week, weekday (month 1-12, week 1-5, weekday 0=Sun) // Find the first day of the month struct tm t = {}; t.tm_year = year - 1900; t.tm_mon = rule.month - 1; t.tm_mday = 1; mktime(&t); int first_wday = t.tm_wday; int day = 1 + ((7 + rule.day - first_wday) % 7) + (rule.week - 1) * 7; // If week==5, but that day is past the end of the month, go back by 7 days t.tm_mday = day; mktime(&t); if (rule.week == 5 && t.tm_mon != rule.month - 1) day -= 7; t.tm_year = year - 1900; t.tm_mon = rule.month - 1; t.tm_mday = day; t.tm_hour = 0; t.tm_min = 0; t.tm_sec = 0; return mktime(&t); } else { __ensure(!"Invalid rule type"); __builtin_unreachable(); } } // Assumes TZ environment variable rules are used, not TZFILE. bool is_in_dst(time_t unix_gmt) { if (rules[0].type == TZFILE) __ensure(!"is_in_dst() called with invalid rules"); int year; unsigned int _month; unsigned int _day; civil_from_days(unix_gmt / (60 * 60 * 24), &year, &_month, &_day); // Get the start and end transition days of the year int start_time = time_from_rule(rules[0], year); int end_time = time_from_rule(rules[1], year); // Check if the unix_gmt falls within the DST period if (start_time <= end_time) { return unix_gmt >= start_time && unix_gmt < end_time; } else { // DST period wraps around the year end return unix_gmt >= start_time || unix_gmt < end_time; } } int unix_local_from_gmt_tzfile(time_t unix_gmt, time_t *offset, bool *dst, char **tm_zone) { const char *tz = getenv("TZ"); if (!tz || *tz == '\0') tz = "/etc/localtime"; frg::string path = parse_tzfile_path(tz); // Check if file exists if (!mlibc::sys_stat) { MLIBC_MISSING_SYSDEP(); __ensure(!"cannot proceed without sys_stat"); } struct stat info; if (mlibc::sys_stat(mlibc::fsfd_target::path, -1, path.data(), 0, &info)) return -1; // FIXME: Make this fallible so the above check is not needed. file_window window {path.data()}; // TODO(geert): we can probably cache this somehow tzfile tzfile_time; memcpy(&tzfile_time, reinterpret_cast(window.get()), sizeof(tzfile)); tzfile_time.tzh_ttisgmtcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_ttisgmtcnt); tzfile_time.tzh_ttisstdcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_ttisstdcnt); tzfile_time.tzh_leapcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_leapcnt); tzfile_time.tzh_timecnt = mlibc::bit_util::byteswap(tzfile_time.tzh_timecnt); tzfile_time.tzh_typecnt = mlibc::bit_util::byteswap(tzfile_time.tzh_typecnt); tzfile_time.tzh_charcnt = mlibc::bit_util::byteswap(tzfile_time.tzh_charcnt); if (tzfile_time.magic[0] != 'T' || tzfile_time.magic[1] != 'Z' || tzfile_time.magic[2] != 'i' || tzfile_time.magic[3] != 'f') { mlibc::infoLogger() << "mlibc: " << path << " is not a valid TZinfo file" << frg::endlog; return -1; } if (tzfile_time.version != '\0' && tzfile_time.version != '2' && tzfile_time.version != '3') { mlibc::infoLogger() << "mlibc: " << path << " has an invalid TZinfo version" << frg::endlog; return -1; } int index = -1; for (size_t i = 0; i < tzfile_time.tzh_timecnt; i++) { int32_t ttime; memcpy(&ttime, reinterpret_cast(window.get()) + sizeof(tzfile) + i * sizeof(int32_t), sizeof(int32_t)); ttime = mlibc::bit_util::byteswap(ttime); // If we are before the first transition, the format dicates that // the first ttinfo entry should be used (and not the ttinfo entry pointed // to by the first transition time). if (i && ttime > unix_gmt) { index = i - 1; break; } } // The format dictates that if no transition is applicable, // the first entry in the file is chosen. uint8_t ttinfo_index = 0; if (index >= 0) { memcpy(&ttinfo_index, reinterpret_cast(window.get()) + sizeof(tzfile) + tzfile_time.tzh_timecnt * sizeof(int32_t) + index * sizeof(uint8_t), sizeof(uint8_t)); } // There should be at least one entry in the ttinfo table. // TODO: If there is not, we might want to fall back to UTC, no DST (?). __ensure(tzfile_time.tzh_typecnt); ttinfo time_info; memcpy(&time_info, reinterpret_cast(window.get()) + sizeof(tzfile) + tzfile_time.tzh_timecnt * sizeof(int32_t) + tzfile_time.tzh_timecnt * sizeof(uint8_t) + ttinfo_index * sizeof(ttinfo), sizeof(ttinfo)); time_info.tt_gmtoff = mlibc::bit_util::byteswap(time_info.tt_gmtoff); char *abbrevs = reinterpret_cast(window.get()) + sizeof(tzfile) + tzfile_time.tzh_timecnt * sizeof(int32_t) + tzfile_time.tzh_timecnt * sizeof(uint8_t) + tzfile_time.tzh_typecnt * sizeof(struct ttinfo); *offset = time_info.tt_gmtoff; *dst = time_info.tt_isdst; *tm_zone = abbrevs + time_info.tt_abbrind; return 0; } // Looks up the local time rules for a given // UNIX GMT timestamp (seconds since 1970 GMT, ignoring leap seconds). // This function assumes the __time_lock has been taken int unix_local_from_gmt(time_t unix_gmt, time_t *offset, bool *dst, char **tm_zone) { do_tzset(); if (daylight && rules[0].type == TZFILE) return unix_local_from_gmt_tzfile(unix_gmt, offset, dst, tm_zone); if (daylight && is_in_dst(unix_gmt)) { *offset = tt_infos[1].tt_gmtoff; *dst = true; *tm_zone = tzname[1]; return 0; } *offset = -timezone; *dst = false; *tm_zone = tzname[0]; return 0; } } //anonymous namespace struct tm *gmtime_r(const time_t *unix_gmt, struct tm *res) { int year; unsigned int month; unsigned int day; unsigned int weekday; unsigned int yday; time_t unix_local = *unix_gmt; int days_since_epoch = unix_local / (60*60*24); civil_from_days(days_since_epoch, &year, &month, &day); weekday_from_days(days_since_epoch, &weekday); yearday_from_date(year, month, day, &yday); res->tm_sec = unix_local % 60; res->tm_min = (unix_local / 60) % 60; res->tm_hour = (unix_local / (60*60)) % 24; res->tm_mday = day; res->tm_mon = month - 1; res->tm_year = year - 1900; res->tm_wday = weekday; res->tm_yday = yday - 1; res->tm_isdst = -1; res->tm_zone = __utc; res->tm_gmtoff = 0; return res; } struct tm *localtime_r(const time_t *unix_gmt, struct tm *res) { int year; unsigned int month; unsigned int day; unsigned int weekday; unsigned int yday; time_t offset = 0; bool dst; char *tm_zone; frg::unique_lock lock(__time_lock); // TODO: Set errno if the conversion fails. if(unix_local_from_gmt(*unix_gmt, &offset, &dst, &tm_zone)) { __ensure(!"Error parsing /etc/localtime"); __builtin_unreachable(); } time_t unix_local = *unix_gmt + offset; int days_since_epoch = unix_local / (60*60*24); civil_from_days(days_since_epoch, &year, &month, &day); weekday_from_days(days_since_epoch, &weekday); yearday_from_date(year, month, day, &yday); res->tm_sec = unix_local % 60; res->tm_min = (unix_local / 60) % 60; res->tm_hour = (unix_local / (60*60)) % 24; res->tm_mday = day; res->tm_mon = month - 1; res->tm_year = year - 1900; res->tm_wday = weekday; res->tm_yday = yday - 1; res->tm_isdst = dst; res->tm_zone = tm_zone; res->tm_gmtoff = offset; return res; } // This implementation of asctime_r is taken from sortix char *asctime_r(const struct tm *tm, char *buf) { static char weekday_names[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static char month_names[12][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; sprintf(buf, "%.3s %.3s%3d %.2d:%.2d%.2d %d\n", weekday_names[tm->tm_wday], month_names[tm->tm_mon], tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, tm->tm_year + 1900); return buf; } char *ctime_r(const time_t *clock, char *buf) { return asctime_r(localtime(clock), buf); } time_t timelocal(struct tm *) { __ensure(!"Not implemented"); __builtin_unreachable(); } constexpr static int days_from_civil(int y, unsigned m, unsigned d) noexcept { y -= m <= 2; const int era = (y >= 0 ? y : y - 399) / 400; const unsigned yoe = static_cast(y - era * 400); // [0, 399] const unsigned doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1; // [0, 365] const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] return era * 146097 + static_cast(doe) - 719468; } time_t timegm(struct tm *tm) { time_t year = tm->tm_year + 1900; time_t month = tm->tm_mon + 1; time_t days = days_from_civil(year, month, tm->tm_mday); time_t secs = (days * 86400) + (tm->tm_hour * 60 * 60) + (tm->tm_min * 60) + tm->tm_sec; return secs; }