diff --git a/include/time.h b/include/time.h new file mode 100644 index 0000000..f382739 --- /dev/null +++ b/include/time.h @@ -0,0 +1,255 @@ +#ifndef VLIBC_TIME_H +#define VLIBC_TIME_H + +/* + * vlibc — . + * + * Calendar time, clock ids, and interval timers. The ISO C functions + * (time, clock, timespec_get, difftime, asctime/ctime/mktime/gmtime/ + * localtime, strftime) and the POSIX clock_* / timer_* / sleep family are + * thin, level-gated wrappers over the Linux time syscalls. + * + * Level 1 (onlyposix): time, clock, timespec_get, difftime, + * clock_gettime/settime/getres/getcpuclockid, + * clock_nanosleep, nanosleep, sleep, and the + * broken-down-time/formatting declarations + * (mktime, gmtime/localtime and _r forms, asctime, + * ctime, strftime, tzset). + * Level 2 (muslmimic): POSIX interval timers (timer_create/delete/ + * settime/gettime/getoverrun) and the XSI + * conveniences (timegm, strftime_l, usleep, + * asctime_r, ctime_r, strptime, tzname/daylight/ + * timezone). + * + * The scalar time types (time_t, clock_t, clockid_t, pid_t, timer_t and + * useconds_t) come from , which this header includes; struct + * timespec and struct tm are defined here. timer_t is the + * opaque pointer type (kernel timer ids are returned through it); struct + * sigevent and struct itimerspec stay forward-declared here — their kernel + * ABI mirrors are private to the timer implementation (see src/time/ + * timer.c) and to consumers that define matching structs. + * + * The clock ids and TIMER_ABSTIME mirror the kernel ABI (x86_64): the + * CLOCK_* values are passed to SYS_clock_* and SYS_timer_* unchanged, and + * TIMER_ABSTIME is the timer_settime/clock_nanosleep flag bit. + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Base argument of timespec_get(); the only base defined by ISO C. */ +#define TIME_UTC 1 + +/* Ticks per second reported by clock() (vlibc always reports 1 MHz). */ +#define CLOCKS_PER_SEC 1000000 + +/* Clock ids (kernel ABI; SYS_clock_* argument). */ +#define CLOCK_REALTIME 0 /* wall clock since the Epoch */ +#define CLOCK_MONOTONIC 1 /* monotonic since boot */ +#define CLOCK_PROCESS_CPUTIME_ID 2 /* CPU time of this process */ +#define CLOCK_THREAD_CPUTIME_ID 3 /* CPU time of this thread */ + +/* Flag for absolute (vs relative) timer expiry. */ +#define TIMER_ABSTIME 1 + +/* + * Elapsed time, seconds + nanoseconds. Kernel ABI layout (x86_64): the + * wrappers pass this straight to the clock/nanosleep/timer syscalls. + */ +struct timespec +{ + time_t tv_sec; /* seconds */ + long tv_nsec; /* nanoseconds (0..999999999) */ +}; + +/* + * Broken-down civil time. tm_gmtoff and tm_zone are the XSI extensions + * (seconds east of UTC / timezone name) carried for glibc compatibility. + */ +struct tm +{ + int tm_sec; /* seconds after the minute (0..60) */ + int tm_min; /* minutes after the hour (0..59) */ + int tm_hour; /* hours since midnight (0..23) */ + int tm_mday; /* day of the month (1..31) */ + int tm_mon; /* months since January (0..11) */ + int tm_year; /* years since 1900 */ + int tm_wday; /* days since Sunday (0..6) */ + int tm_yday; /* days since January 1 (0..365) */ + int tm_isdst; /* daylight-saving flag */ + long tm_gmtoff; /* seconds east of UTC */ + const char *tm_zone; /* timezone abbreviation */ +}; + +/* Return the current wall-clock time in seconds; if t is not NULL store it + * there too, and return (time_t)-1 with errno set on failure. */ +time_t +time(time_t *t); + +/* Approximate processor time in CLOCKS_PER_SEC units since an arbitrary + * epoch, or (clock_t)-1 with errno set on failure. */ +clock_t +clock(void); + +/* Store the current time for base into ts and return base; return 0 if + * base is not TIME_UTC or the clock cannot be read. */ +int +timespec_get(struct timespec *ts, int base); + +/* Read the given clock into tp. Return 0 or -1 with errno set. */ +int +clock_gettime(clockid_t clockid, struct timespec *tp); + +/* Set the given clock from tp. Return 0 or -1 with errno set. */ +int +clock_settime(clockid_t clockid, const struct timespec *tp); + +/* Resolution of the given clock; store it in res when res is not NULL. + * Return 0 or -1 with errno set. */ +int +clock_getres(clockid_t clockid, struct timespec *res); + +/* Clock id measuring the CPU time of the given process (0 = calling + * process). Return 0, or -1 with errno set when the process is unknown. */ +int +clock_getcpuclockid(pid_t pid, clockid_t *clock_id); + +/* Sleep on the given clock. Without TIMER_ABSTIME request is relative and + * the sleep restarts from remain on signal interruption. */ +int +clock_nanosleep(clockid_t clockid, int flags, const struct timespec *request, + struct timespec *remain); + +/* Sleep request seconds; when interrupted and remain is not NULL the sleep + * restarts from the remaining time. Return 0 or -1 with errno set. */ +int +nanosleep(const struct timespec *request, struct timespec *remain); + +/* Sleep seconds (whole seconds; signals can shorten the sleep, which is + * then resumed). Return the unslept seconds, normally 0. */ +unsigned int +sleep(unsigned int seconds); + +/* Convert broken-down civil time back to calendar seconds. */ +time_t +mktime(struct tm *tm); + +/* Broken-down local time; localtime uses an internal static buffer. + * Return NULL on error. */ +struct tm * +localtime(const time_t *timer); + +/* Broken-down local time into buf. Return buf or NULL on error. */ +struct tm * +localtime_r(const time_t *timer, struct tm *buf); + +/* Broken-down UTC; gmtime uses an internal static buffer. */ +struct tm * +gmtime(const time_t *timer); + +/* Broken-down UTC into buf. Return buf or NULL on error. */ +struct tm * +gmtime_r(const time_t *timer, struct tm *buf); + +/* Fixed-format rendering of broken-down time ("Sun Sep 16 01:03:52 1973"). */ +char * +asctime(const struct tm *tm); + +/* asctime(localtime(timer)); fixed-format wall-clock rendering. */ +char * +ctime(const time_t *timer); + +/* Format broken-down time per format into s (at most maxsize bytes); + * return the bytes written (0 when the buffer was too small). */ +size_t +strftime(char *restrict s, size_t maxsize, const char *restrict format, + const struct tm *restrict timeptr); + +/* Seconds between two calendar times (b subtracted from a). */ +double +difftime(time_t a, time_t b); + +/* Establish the local timezone from TZ / the system default. */ +void +tzset(void); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): POSIX interval timers and XSI conveniences. */ + +/* Opaque kernel-ABI structures; consumers define layout-compatible structs + * (see src/time/timer.c). */ +struct sigevent; +struct itimerspec; + +/* Create a per-process interval timer on clockid; the timer id is stored + * through timerid. Return 0 or -1 with errno set. */ +int +timer_create(clockid_t clockid, const struct sigevent *restrict evp, timer_t *restrict timerid); + +/* Destroy the given timer. Return 0 or -1 with errno set. */ +int +timer_delete(timer_t timerid); + +/* Arm/disarm the timer; when old_value is not NULL the previous setting is + * stored there. Return 0 or -1 with errno set. */ +int +timer_settime(timer_t timerid, int flags, const struct itimerspec *restrict new_value, + struct itimerspec *restrict old_value); + +/* Remaining time until the timer expires (and its interval). Return 0 or + * -1 with errno set. */ +int +timer_gettime(timer_t timerid, struct itimerspec *curr_value); + +/* Number of timer expirations between the most recent signal delivery and + * the one before it; -1 with errno set when there was no pending delivery. */ +int +timer_getoverrun(timer_t timerid); + +/* mktime in UTC rather than local time. */ +time_t +timegm(struct tm *tm); + +/* strftime with an explicit locale object (locale_t is not yet defined; + * the locale argument is an opaque pointer). */ +size_t +strftime_l(char *restrict s, size_t maxsize, const char *restrict format, + const struct tm *restrict timeptr, void *loc); + +/* Microsecond sleep (whole microseconds; signals resume the sleep). + * Return 0 or the unslept microseconds with errno set. */ +useconds_t +usleep(useconds_t usec); + +/* asctime into a caller-provided buffer of at least 26 bytes. */ +char * +asctime_r(const struct tm *tm, char *buf); + +/* ctime into a caller-provided buffer of at least 26 bytes. */ +char * +ctime_r(const time_t *timer, char *buf); + +/* Parse format from s into broken-down time; return the first character + * not consumed, or NULL when the format did not match. */ +char * +strptime(const char *restrict s, const char *restrict format, struct tm *restrict tm); + +/* Timezone state maintained by tzset(). */ +extern char *tzname[2]; +extern int daylight; +extern long timezone; + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_TIME_H */ diff --git a/src/time/clock.c b/src/time/clock.c new file mode 100644 index 0000000..050f1ca --- /dev/null +++ b/src/time/clock.c @@ -0,0 +1,112 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * struct tms layout for SYS_times: four process-CPU clock-tick counters, + * in kernel order (x86_64: four longs; the kernel ABI names it __kernel_tms). + * It is private to this file; (a later todo) will own the + * public definition. + */ +struct tms +{ + clock_t tms_utime; /* user CPU time */ + clock_t tms_stime; /* system CPU time */ + clock_t tms_cutime; /* user CPU time of waited-for children */ + clock_t tms_cstime; /* system CPU time of waited-for children */ +}; + +/* + * clock: the calling process's CPU time in CLOCKS_PER_SEC units. + * + * SYS_times reports CPU time in USER_HZ ticks (100 per second on x86_64), + * so the tick counts are scaled by CLOCKS_PER_SEC / 100 == 10000 to the + * 1 MHz clock() unit. On failure SYS_times returns -1 and clock() reports + * (clock_t)-1 with errno set. + */ +clock_t +clock(void) +{ + struct tms t; + + if (syscall_ret(__syscall1(SYS_times, (long)&t)) < 0) + { + return (clock_t)-1; + } + return (clock_t)((t.tms_utime + t.tms_stime) * (CLOCKS_PER_SEC / 100)); +} + +/* + * clock_gettime/clock_settime/clock_getres: raw kernel clock reads and + * writes over SYS_clock_gettime(228)/SYS_clock_settime(227)/ + * SYS_clock_getres(229). The clock ids (CLOCK_REALTIME and friends) are + * kernel ABI values and pass through unchanged; the struct timespec + * arguments are the kernel's own layout. + */ +int +clock_gettime(clockid_t clockid, struct timespec *tp) +{ + return syscall_ret(__syscall2(SYS_clock_gettime, (long)clockid, (long)tp)); +} + +int +clock_settime(clockid_t clockid, const struct timespec *tp) +{ + return syscall_ret(__syscall2(SYS_clock_settime, (long)clockid, (long)tp)); +} + +int +clock_getres(clockid_t clockid, struct timespec *res) +{ + return syscall_ret(__syscall2(SYS_clock_getres, (long)clockid, (long)res)); +} + +/* + * clock_getcpuclockid: the clock id that measures a given process's CPU + * time. + * + * The calling process (pid 0 or our own pid) is always reported as + * CLOCK_PROCESS_CPUTIME_ID. Any other pid is encoded with the kernel's + * CPUCLOCK_PID scheme — (~pid << 3) | 2 — and validated by probing that + * clock with SYS_clock_getres, which the kernel answers with ESRCH when no + * such process exists. That probe failing is the only errno write this + * file performs on its own; every other path leaves errno to syscall_ret. + */ +int +clock_getcpuclockid(pid_t pid, clockid_t *clock_id) +{ + pid_t self = (pid_t)syscall_ret(__syscall0(SYS_getpid)); + clockid_t id; + + if (pid == 0) + { + pid = self; + } + if (pid == self) + { + *clock_id = CLOCK_PROCESS_CPUTIME_ID; + return 0; + } + + /* Linux CPUCLOCK_PID encoding of a process clock id. */ + id = (clockid_t)((~(unsigned)pid << 3) | 2u); + + /* Probe the encoded id; the kernel rejects unknown pids with ESRCH. */ + { + struct timespec res; + + if (syscall_ret(__syscall2(SYS_clock_getres, (long)id, (long)&res)) < 0) + { + errno = ESRCH; + return -1; + } + } + *clock_id = id; + return 0; +} diff --git a/src/time/ctime.c b/src/time/ctime.c new file mode 100644 index 0000000..7436b3b --- /dev/null +++ b/src/time/ctime.c @@ -0,0 +1,135 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * asctime/ctime: the fixed-format "Www Mmm dd hh:mm:ss yyyy\n" rendering + * (26 bytes including the terminating NUL). Both functions are thin layers + * over a single buffer writer: asctime renders a broken-down time directly, + * ctime renders localtime(t). Per POSIX the conversion fails (NULL) when + * the calendar year lies outside [1000, 9999], which is also what keeps the + * fixed 26-byte layout exact. + */ + +static const char *const wd_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +static const char *const mon_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +/* + * Render tm into buf (26 bytes: 24 + '\n' + '\0'). Returns buf, or NULL + * when the year is outside the representable range (the buffer is then + * left untouched, matching the POSIX contract). + */ +static char * +asctime_write(const struct tm *tm, char *buf) +{ + int year = tm->tm_year + 1900; + int wd = tm->tm_wday; + int mo = tm->tm_mon; + int mday = tm->tm_mday; + const char *s; + char *p = buf; + + if (year < 1000 || year > 9999) + { + return NULL; + } + if (wd < 0) + { + wd = 7 - (-wd) % 7; + if (wd == 7) + { + wd = 0; + } + } + else + { + wd %= 7; + } + if (mo < 0) + { + mo = 12 - (-mo) % 12; + if (mo == 12) + { + mo = 0; + } + } + else + { + mo %= 12; + } + + s = wd_abbr[wd]; + while (*s != '\0') + { + *p++ = *s++; + } + *p++ = ' '; + s = mon_abbr[mo]; + while (*s != '\0') + { + *p++ = *s++; + } + *p++ = ' '; + if (mday < 10) + { + *p++ = ' '; + } + *p++ = (char)('0' + (mday / 10) % 10); + *p++ = (char)('0' + mday % 10); + *p++ = ' '; + *p++ = (char)('0' + (tm->tm_hour / 10) % 10); + *p++ = (char)('0' + tm->tm_hour % 10); + *p++ = ':'; + *p++ = (char)('0' + (tm->tm_min / 10) % 10); + *p++ = (char)('0' + tm->tm_min % 10); + *p++ = ':'; + *p++ = (char)('0' + (tm->tm_sec / 10) % 10); + *p++ = (char)('0' + tm->tm_sec % 10); + *p++ = ' '; + *p++ = (char)('0' + (year / 1000) % 10); + *p++ = (char)('0' + (year / 100) % 10); + *p++ = (char)('0' + (year / 10) % 10); + *p++ = (char)('0' + year % 10); + *p++ = '\n'; + *p = '\0'; + return buf; +} + +char * +asctime(const struct tm *tm) // NOLINT(concurrency-mt-unsafe) +{ + static char asctime_buf[26]; + + return asctime_write(tm, asctime_buf); +} + +char * +ctime(const time_t *timer) +{ + return asctime(localtime(timer)); +} + +#if VLIBC_LEVEL_GE(2) + +char * +asctime_r(const struct tm *tm, char *buf) +{ + return asctime_write(tm, buf); +} + +char * +ctime_r(const time_t *timer, char *buf) +{ + struct tm tmv; + + if (localtime_r(timer, &tmv) == NULL) + { + return NULL; + } + return asctime_write(&tmv, buf); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/time/mktime.c b/src/time/mktime.c new file mode 100644 index 0000000..3e0f278 --- /dev/null +++ b/src/time/mktime.c @@ -0,0 +1,188 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include "time_impl.h" + +/* + * Civil <-> serial conversions (mktime/timegm/__fill_tm). + * + * days_from_civil/civil_from_days are Howard Hinnant's proleptic-Gregorian + * algorithms (the "chrono-Compatible Low-Level Date Algorithms" pair): the + * era-400 formulation keeps the year arithmetic exact for the full 32-bit + * int year range and the division by 146097 is compile-time constant. + * days_from_civil counts days since 1970-01-01 (negative before that); + * civil_from_days is its exact inverse and yields a 1-based month. + * + * floor_div/floor_mod give Euclidean division so that the day/time math + * stays correct for negative inputs (pre-1970 timestamps, out-of-range + * struct tm fields): floor_mod always returns a non-negative remainder. + */ + +static long long +floor_div(long long a, long long b) +{ + long long q = a / b; + long long r = a % b; + + if (r != 0 && ((r < 0) != (b < 0))) + { + q -= 1; + } + return q; +} + +static long long +floor_mod(long long a, long long b) +{ + long long r = a % b; + + if (r != 0 && ((r < 0) != (b < 0))) + { + r += b; + } + return r; +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters,bugprone-reserved-identifier) +long long +days_from_civil(int y, int m, int d) +{ + long long yy = y; + + yy -= (m <= 2); + { + long long era = (yy >= 0 ? yy : yy - 399) / 400; + unsigned int yoe = (unsigned int)(yy - era * 400); /* [0, 399] */ + unsigned int doy = (153u * (unsigned int)(m + (m > 2 ? -3 : 9)) + 2u) / 5u + + (unsigned int)d - 1u; /* [0, 365] */ + unsigned int doe = yoe * 365u + yoe / 4u - yoe / 100u + doy; /* [0, 146096] */ + + return era * 146097 + (long long)doe - 719468; + } +} + +void +civil_from_days(long long days, int *y, int *m, int *d) +{ + long long z = days + 719468; + long long era = (z >= 0 ? z : z - 146096) / 146097; + unsigned int doe = (unsigned int)(z - era * 146097); /* [0, 146096] */ + unsigned int yoe = (doe - doe / 1460u + doe / 36524u - doe / 146096u) / 365u; /* [0, 399] */ + unsigned int doy = doe - (365u * yoe + yoe / 4u - yoe / 100u); /* [0, 365] */ + unsigned int mp = (5u * doy + 2u) / 153u; /* [0, 11] */ + unsigned int dd = doy - (153u * mp + 2u) / 5u + 1u; /* [1, 31] */ + unsigned int mm = mp < 10u ? mp + 3u : mp - 9u; /* [1, 12] */ + + *y = (int)(yoe + era * 400) + (int)(mm <= 2u); + *m = (int)mm; + *d = (int)dd; +} + +/* + * __fill_tm: materialize a struct tm from a local civil time expressed as + * seconds (the pseudo-UTC frame every TZ computation uses), the offset in + * effect, the DST flag, and the zone abbreviation to store in tm_zone. + * The caller supplies the fully resolved values; this function only splits + * local_sec into the civil fields. + */ +void +__fill_tm(long long local_sec, long gmtoff, int isdst, const char *zone, struct tm *out) +{ + long long days = floor_div(local_sec, 86400); + long long tod = local_sec - days * 86400; /* [0, 86400) */ + int y; + int m; + int d; + + civil_from_days(days, &y, &m, &d); + out->tm_sec = (int)(tod % 60); + out->tm_min = (int)((tod / 60) % 60); + out->tm_hour = (int)(tod / 3600); + out->tm_mday = d; + out->tm_mon = m - 1; + out->tm_year = y - 1900; + out->tm_wday = (int)floor_mod(days + 4, 7); /* 1970-01-01 was a Thursday */ + out->tm_yday = (int)(days - days_from_civil(y, 1, 1)); + out->tm_isdst = isdst; + out->tm_gmtoff = gmtoff; + out->tm_zone = zone; +} +// NOLINTEND(bugprone-easily-swappable-parameters,bugprone-reserved-identifier) + +/* + * tm_civil_seconds: the struct tm fields reduced to one linear "local + * civil" second count. Out-of-range fields fold in naturally: the month is + * renormalized into the year with Euclidean division, the day is carried as + * an offset from the first of the month, and the time of day just adds on + * (hours/minutes/seconds may be anything and spill across day boundaries). + */ +static long long +tm_civil_seconds(const struct tm *tm) +{ + int y = tm->tm_year + 1900 + (int)floor_div((long long)tm->tm_mon, 12); + int m = (int)floor_mod((long long)tm->tm_mon, 12) + 1; + long long days = days_from_civil(y, m, 1) + (long long)tm->tm_mday - 1; + + return days * 86400 + (long long)tm->tm_hour * 3600 + (long long)tm->tm_min * 60 + + (long long)tm->tm_sec; +} + +/* + * mktime: interpret the broken-down fields as LOCAL civil time and return + * the corresponding UTC epoch. The DST regime is chosen from tm_isdst: + * positive forces daylight time, zero forces standard time, and negative + * (the "I don't know" value localtime fills) defers to the zone rules. + * POSIX requires the struct to be overwritten with the normalized fields, + * so __fill_tm runs on the resolved local time before returning. Overflow + * leaves the struct untouched and reports (time_t)-1 with errno EOVERFLOW. + */ +time_t +mktime(struct tm *tm) +{ + const struct tz_state *st; + long long local = tm_civil_seconds(tm); + long off; + int isdst = 0; + long long epoch; + + __tzset_lazy(); + st = __tz_state(); + off = tz_offset_for_local(st, local, tm->tm_isdst, &isdst); + if (__builtin_sub_overflow(local, (long long)off, &epoch)) + { + errno = EOVERFLOW; + return (time_t)-1; + } + __fill_tm(local, off, isdst, isdst ? st->dst_name : st->std_name, tm); + return (time_t)epoch; +} + +#if VLIBC_LEVEL_GE(2) + +/* + * timegm: mktime interpreted as UTC — the same normalization, with a zero + * offset and a fixed "UTC" zone, so no timezone state is consulted. + */ +time_t +timegm(struct tm *tm) +{ + long long local = tm_civil_seconds(tm); + long long epoch; + + if (__builtin_sub_overflow(local, 0, &epoch)) + { + errno = EOVERFLOW; + return (time_t)-1; + } + __fill_tm(local, 0, 0, "UTC", tm); + return (time_t)epoch; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/time/nanosleep.c b/src/time/nanosleep.c new file mode 100644 index 0000000..d37c475 --- /dev/null +++ b/src/time/nanosleep.c @@ -0,0 +1,99 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * nanosleep: high-resolution sleep for request. + * + * On EINTR the kernel writes the unslept remainder into rem (when rem is + * not NULL) and the sleep restarts from it, so a signal handler cannot + * shorten the requested interval by more than the time already slept — + * nanosleep only reports the interruption to the caller when rem is NULL. + * The loop therefore terminates on a completed sleep (0) or on any error + * the kernel cannot make progress from. + */ +int +nanosleep(const struct timespec *request, struct timespec *remain) +{ + for (;;) + { + long r = syscall_ret(__syscall2(SYS_nanosleep, (long)request, (long)remain)); + + if (r == 0) + { + return 0; + } + if (errno == EINTR && remain != NULL) + { + request = remain; + continue; + } + return (int)r; + } +} + +/* + * clock_nanosleep: nanosleep on an explicit clock. + * + * With TIMER_ABSTIME the request is an absolute expiry on clockid and the + * sleep cannot usefully restart, so an EINTR is reported to the caller; + * relative sleeps restart from the remainder like nanosleep. flags accepts + * exactly 0 or TIMER_ABSTIME (anything else is EINVAL before any syscall). + * Like nanosleep this reports failure as -1 with errno set rather than as + * a returned error number (POSIX permits either). + */ +int +clock_nanosleep(clockid_t clockid, int flags, const struct timespec *request, + struct timespec *remain) +{ + if (flags != 0 && flags != TIMER_ABSTIME) + { + errno = EINVAL; + return -1; + } + for (;;) + { + long r = syscall_ret(__syscall4(SYS_clock_nanosleep, (long)clockid, (long)flags, + (long)request, (long)remain)); + + if (r == 0) + { + return 0; + } + if (!(flags & TIMER_ABSTIME) && errno == EINTR && remain != NULL) + { + request = remain; + continue; + } + return (int)r; + } +} + +#if VLIBC_LEVEL_GE(2) + +/* + * usleep: microsecond sleep (level 2). nanosleep does the sleeping and + * restarts from the remainder on interruption, so the whole requested + * interval always elapses; a genuine error is reported as (useconds_t)-1. + */ +useconds_t +usleep(useconds_t usec) +{ + struct timespec ts; + + ts.tv_sec = usec / 1000000; + ts.tv_nsec = (long)(usec % 1000000) * 1000L; + if (nanosleep(&ts, &ts) != 0) + { + return (useconds_t)-1; + } + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/time/sleep.c b/src/time/sleep.c new file mode 100644 index 0000000..426152c --- /dev/null +++ b/src/time/sleep.c @@ -0,0 +1,26 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * sleep: whole-second sleep for seconds. + * + * nanosleep does the sleeping and restarts from the remainder on signal + * interruption (remain points back at the same struct), so a signal can + * only ever extend the wall time spent in sleep, never truncate it; the + * only non-zero return is a genuine error, reported with the (unconsumed) + * requested seconds, as POSIX requires. + */ +unsigned int +sleep(unsigned int seconds) +{ + struct timespec ts = {(time_t)seconds, 0}; + + if (nanosleep(&ts, &ts) != 0) + { + return (unsigned int)ts.tv_sec; + } + return 0; +} diff --git a/src/time/strftime.c b/src/time/strftime.c new file mode 100644 index 0000000..85de6c4 --- /dev/null +++ b/src/time/strftime.c @@ -0,0 +1,419 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "time_impl.h" + +/* + * strftime: C-locale formatting of broken-down time. + * + * Conversion coverage is the full POSIX set (%a %A %b %B %c %C %d %D %e %F + * %g %G %h %H %I %j %m %M %n %p %r %R %S %t %T %u %U %V %w %W %x %X %y %Y + * %z %Z %%). The composite conversions expand to the documented POSIX + * C-locale forms; %e is space-padded; the ISO year/week conversions %g/%G/ + * %V follow the weekday-of-4-January rule; %z renders tm_gmtoff as a signed + * HHMM. GNU-style modifier characters ('-' '_' '0' padding flags, '^' '#', + * width digits, 'E'/'O' alternatives) are accepted and ignored, so format + * strings written for glibc do not error out. + * + * The writer counts everything it would emit, so an oversized buffer is + * detected exactly (returns 0) without first needing the output to fit. + */ + +static const char *const wd_full[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"}; +static const char *const wd_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +static const char *const mon_full[12] = {"January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December"}; +static const char *const mon_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +static long long +floor_mod(long long a, long long b) +{ + long long r = a % b; + + if (r != 0 && ((r < 0) != (b < 0))) + { + r += b; + } + return r; +} + +struct out +{ + char *s; + size_t cap; + size_t n; +}; + +static void +out_ch(struct out *o, char c) +{ + if (o->n < o->cap) + { + o->s[o->n] = c; + } + o->n++; +} + +static void +out_mem(struct out *o, const char *p, size_t len) +{ + size_t i; + + for (i = 0; i < len; i++) + { + out_ch(o, p[i]); + } +} + +static void +out_str(struct out *o, const char *s) +{ + while (*s != '\0') + { + out_ch(o, *s); + s++; + } +} + +/* Emit the (non-negative) decimal v, left-padded with pad to width chars. */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +static void +out_num(struct out *o, long long v, int width, char pad) +{ + char tmp[24]; + int i = (int)sizeof tmp; + + do + { + tmp[--i] = (char)('0' + v % 10); + v /= 10; + } while (v > 0); + while ((int)sizeof tmp - i < width) + { + tmp[--i] = pad; + } + out_mem(o, tmp + i, (size_t)(sizeof tmp - i)); +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +/* Emit a year: optional '-' then at least four digits. */ +static void +out_year(struct out *o, int year) +{ + if (year < 0) + { + out_ch(o, '-'); + out_num(o, -(long long)year, 4, '0'); + } + else + { + out_num(o, year, 4, '0'); + } +} + +/* Weekday/month indexes, normalized so stray values cannot index out of + * range (negative inputs wrap into 0..6 / 0..11). */ +static int +wd_index(int w) +{ + w %= 7; + return w < 0 ? w + 7 : w; +} + +static int +mon_index(int m) +{ + m %= 12; + return m < 0 ? m + 12 : m; +} + +/* ISO-8601 year and week (week containing the year's first Thursday; the + * week whose Monday is closest to 4 January). Week 1 may belong to the + * adjacent calendar year. */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +static void +iso_week_year(const struct tm *tm, int *year_out, int *week_out) +{ + int y = tm->tm_year + 1900; + long long day = days_from_civil(y, 1, 1) + (long long)tm->tm_yday; + long long k = floor_mod(day + 3, 7); /* 0 = Monday */ + long long th = day + (k <= 3 ? 3 - k : -(k - 3)); + long long th_jan1; + int ty; + int tm_; + int td; + + civil_from_days(th, &ty, &tm_, &td); + (void)tm_; + (void)td; + th_jan1 = days_from_civil(ty, 1, 1); + *year_out = ty; + *week_out = (int)((th - th_jan1) / 7) + 1; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +static void +fmt_run(struct out *o, const struct tm *tm, const char *f); + +/* Format one conversion (after '%'). Composites expand recursively. */ +static void +fmt_one(struct out *o, const struct tm *tm, char c) +{ + int h; + int wd; + + switch (c) + { + case 'a': + out_str(o, wd_abbr[wd_index(tm->tm_wday)]); + break; + case 'A': + out_str(o, wd_full[wd_index(tm->tm_wday)]); + break; + case 'b': + case 'h': + out_str(o, mon_abbr[mon_index(tm->tm_mon)]); + break; + case 'B': + out_str(o, mon_full[mon_index(tm->tm_mon)]); + break; + case 'c': + fmt_run(o, tm, "%a %b %e %H:%M:%S %Y"); + break; + case 'C': + out_num(o, (long long)(tm->tm_year + 1900) / 100, 2, '0'); + break; + case 'd': + out_num(o, tm->tm_mday, 2, '0'); + break; + case 'D': + fmt_run(o, tm, "%m/%d/%y"); + break; + case 'e': + out_num(o, tm->tm_mday, 2, ' '); + break; + case 'F': + fmt_run(o, tm, "%Y-%m-%d"); + break; + case 'g': + case 'G': + case 'V': + { + int iy; + int iw; + + iso_week_year(tm, &iy, &iw); + if (c == 'V') + { + out_num(o, iw, 2, '0'); + } + else if (c == 'g') + { + int yy = iy % 100; + + if (yy < 0) + { + yy += 100; + } + out_num(o, yy, 2, '0'); + } + else + { + out_year(o, iy); + } + break; + } + case 'H': + out_num(o, tm->tm_hour, 2, '0'); + break; + case 'I': + h = tm->tm_hour % 12; + if (h == 0) + { + h = 12; + } + out_num(o, h, 2, '0'); + break; + case 'j': + out_num(o, (long long)tm->tm_yday + 1, 3, '0'); + break; + case 'm': + out_num(o, tm->tm_mon + 1, 2, '0'); + break; + case 'M': + out_num(o, tm->tm_min, 2, '0'); + break; + case 'n': + out_ch(o, '\n'); + break; + case 'p': + out_str(o, tm->tm_hour < 12 ? "AM" : "PM"); + break; + case 'r': + fmt_run(o, tm, "%I:%M:%S %p"); + break; + case 'R': + fmt_run(o, tm, "%H:%M"); + break; + case 'S': + out_num(o, tm->tm_sec, 2, '0'); + break; + case 't': + out_ch(o, '\t'); + break; + case 'T': + fmt_run(o, tm, "%H:%M:%S"); + break; + case 'u': + wd = tm->tm_wday; + out_num(o, wd == 0 ? 7 : wd, 1, '0'); + break; + case 'U': + /* Week of the year, Sunday as first day; days before the first + * Sunday are week 0. */ + out_num(o, (long long)(tm->tm_yday + 7 - wd_index(tm->tm_wday)) / 7, 2, '0'); + break; + case 'w': + out_num(o, tm->tm_wday, 1, '0'); + break; + case 'W': + /* Week of the year, Monday as first day. */ + wd = (wd_index(tm->tm_wday) + 6) % 7; + out_num(o, (long long)(tm->tm_yday + 7 - wd) / 7, 2, '0'); + break; + case 'x': + fmt_run(o, tm, "%m/%d/%y"); + break; + case 'X': + fmt_run(o, tm, "%H:%M:%S"); + break; + case 'y': + { + int yy = (tm->tm_year + 1900) % 100; + + if (yy < 0) + { + yy += 100; + } + out_num(o, yy, 2, '0'); + break; + } + case 'Y': + out_year(o, tm->tm_year + 1900); + break; + case 'z': + { + long off = tm->tm_gmtoff; + long mag; + + if (off < 0) + { + out_ch(o, '-'); + mag = -off; + } + else + { + out_ch(o, '+'); + mag = off; + } + out_num(o, mag / 3600, 2, '0'); + out_num(o, (mag % 3600) / 60, 2, '0'); + break; + } + case 'Z': + if (tm->tm_zone != NULL && tm->tm_zone[0] != '\0') + { + out_str(o, tm->tm_zone); + } + break; + case '%': + out_ch(o, '%'); + break; + default: + out_ch(o, c); + break; + } +} + +/* Scan a full format string; called by strftime and by the composites. */ +void +fmt_run(struct out *o, const struct tm *tm, const char *f) +{ + while (*f != '\0') + { + char c = *f; + + if (c != '%') + { + out_ch(o, c); + f++; + continue; + } + f++; + while (*f == '-' || *f == '_' || *f == '0' || *f == '^' || *f == '#') + { + f++; + } + while (*f >= '0' && *f <= '9') + { + f++; + } + if (*f == 'E' || *f == 'O') + { + f++; + } + if (*f == '\0') + { + out_ch(o, '%'); + break; + } + fmt_one(o, tm, *f); + f++; + } +} + +/* + * strftime: format tm per format into s. Returns the number of bytes placed + * (excluding the terminating NUL), or 0 when the full result (NUL included) + * does not fit in maxsize. + */ +size_t +strftime(char *restrict s, size_t maxsize, const char *restrict format, + const struct tm *restrict timeptr) +{ + struct out o; + + o.s = s; + o.cap = maxsize; + o.n = 0; + fmt_run(&o, timeptr, format); + if (o.n < maxsize) + { + s[o.n] = '\0'; + return o.n; + } + return 0; +} + +#if VLIBC_LEVEL_GE(2) + +/* + * strftime_l: strftime with an explicit locale. No locale support exists + * yet, so the locale argument is ignored and the C-locale output is + * produced (the fixed behavior of the whole file). + */ +size_t +strftime_l(char *restrict s, size_t maxsize, const char *restrict format, + const struct tm *restrict timeptr, void *loc) +{ + (void)loc; + return strftime(s, maxsize, format, timeptr); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/time/strptime.c b/src/time/strptime.c new file mode 100644 index 0000000..3dbc53f --- /dev/null +++ b/src/time/strptime.c @@ -0,0 +1,457 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * strptime: reverse of the strftime conversion set (POSIX, level 2). + * + * Accepts the same conversions strftime emits — the day/month names + * case-insensitively, the composite formats %c %D %F %r %R %T %x %X, the + * numeric fields %C %d %e %g %G %H %I %j %m %M %S %u %U %V %w %W %y %Y, + * %p (which converts a 12-hour %I into a 24-hour tm_hour), %z (into + * tm_gmtoff), %Z (consumed, no storage), %n/%t and literal whitespace + * (which both match any run of whitespace), and %%. GNU modifier + * characters before a conversion are accepted and ignored, mirroring + * strftime. Returns a pointer to the first input character not consumed; + * NULL when nothing was matched or a conversion fails mid-way. + */ + +static int +is_space(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +static int +is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int +is_alpha(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +static char +lower(char c) +{ + if (c >= 'A' && c <= 'Z') + { + return (char)(c - 'A' + 'a'); + } + return c; +} + +/* Case-insensitive whole-word match; the word must end at a non-letter. */ +static int +word_matches(const char *s, const char *word) +{ + size_t i = 0; + + while (word[i] != '\0') + { + if (lower(s[i]) != lower(word[i])) + { + return 0; + } + i++; + } + return !is_alpha(s[i]); +} + +static const char *const wd_full[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"}; +static const char *const wd_abbr[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +static const char *const mon_full[12] = {"January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December"}; +static const char *const mon_abbr[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +/* Match a month name; returns the advanced pointer or NULL. */ +static const char * +match_mon(const char *s, int *mon_out) +{ + int i; + + for (i = 0; i < 12; i++) + { + if (word_matches(s, mon_full[i])) + { + *mon_out = i; + return s + 7; + } + } + for (i = 0; i < 12; i++) + { + if (word_matches(s, mon_abbr[i])) + { + *mon_out = i; + return s + 3; + } + } + return NULL; +} + +/* Match a weekday name; returns the advanced pointer or NULL. */ +static const char * +match_wday(const char *s, int *wday_out) +{ + int i; + + for (i = 0; i < 7; i++) + { + if (word_matches(s, wd_full[i])) + { + *wday_out = i; + return s + 7; + } + } + for (i = 0; i < 7; i++) + { + if (word_matches(s, wd_abbr[i])) + { + *wday_out = i; + return s + 3; + } + } + return NULL; +} + +struct parse_ctx +{ + int am_pm; /* 0 = not seen, 1 = AM, 2 = PM */ +}; + +static const char * +parse_num(const char *s, int width, int *out) +{ + int v = 0; + int n = 0; + + while (is_space(*s)) + { + s++; + } + while (n < width && is_digit(*s)) + { + v = v * 10 + (*s - '0'); + s++; + n++; + } + if (n == 0) + { + return NULL; + } + *out = v; + return s; +} + +/* Advance past any run of whitespace in s. */ +static const char * +skip_ws(const char *s) +{ + while (is_space(*s)) + { + s++; + } + return s; +} + +static const char * +parse_one(const char *s, const char *f, struct tm *tm, struct parse_ctx *ctx); + +/* strptime_impl: match one conversion directive at f (f is the conv char). */ +static const char * +parse_conv(const char *s, char c, struct tm *tm, struct parse_ctx *ctx) +{ + const char *p; + int v; + int idx; + + switch (c) + { + case 'a': + case 'A': + return match_wday(s, &tm->tm_wday); + case 'b': + case 'B': + case 'h': + return match_mon(s, &tm->tm_mon); + case 'c': + return parse_one(s, "%a %b %e %H:%M:%S %Y", tm, ctx); + case 'C': + return parse_num(s, 2, &v); + case 'd': + case 'e': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_mday = v; + return p; + case 'D': + case 'x': + return parse_one(s, "%m/%d/%y", tm, ctx); + case 'F': + return parse_one(s, "%Y-%m-%d", tm, ctx); + case 'g': + case 'G': + case 'V': + return parse_num(s, 2, &v); + case 'H': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_hour = v; + return p; + case 'I': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + if (ctx->am_pm == 1) + { + tm->tm_hour = v == 12 ? 0 : v; + } + else if (ctx->am_pm == 2) + { + tm->tm_hour = v == 12 ? 12 : v + 12; + } + else + { + tm->tm_hour = v; + } + return p; + case 'j': + p = parse_num(s, 3, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_yday = v - 1; + return p; + case 'm': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_mon = v - 1; + return p; + case 'M': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_min = v; + return p; + case 'n': + case 't': + return skip_ws(s); + case 'p': + if (word_matches(s, "AM")) + { + ctx->am_pm = 1; + if (tm->tm_hour == 12) + { + tm->tm_hour = 0; + } + return s + 2; + } + if (word_matches(s, "PM")) + { + ctx->am_pm = 2; + if (tm->tm_hour >= 1 && tm->tm_hour <= 12) + { + tm->tm_hour += tm->tm_hour == 12 ? 0 : 12; + } + return s + 2; + } + return NULL; + case 'r': + return parse_one(s, "%I:%M:%S %p", tm, ctx); + case 'R': + return parse_one(s, "%H:%M", tm, ctx); + case 'S': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_sec = v; + return p; + case 'T': + case 'X': + return parse_one(s, "%H:%M:%S", tm, ctx); + case 'u': + p = parse_num(s, 1, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_wday = v == 7 ? 0 : v; + return p; + case 'U': + case 'W': + return parse_num(s, 2, &v); + case 'w': + p = parse_num(s, 1, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_wday = v; + return p; + case 'y': + p = parse_num(s, 2, &v); + if (p == NULL) + { + return NULL; + } + /* POSIX pivot: 69-99 -> 1969-1999, 00-68 -> 2000-2068. */ + tm->tm_year = v <= 68 ? v + 100 : v; + return p; + case 'Y': + p = parse_num(s, 9, &v); + if (p == NULL) + { + return NULL; + } + tm->tm_year = v - 1900; + return p; + case 'z': + idx = 0; + if (s[idx] == '+' || s[idx] == '-') + { + int neg = s[idx] == '-'; + int hh; + int mm; + + idx++; + p = parse_num(s + idx, 2, &hh); + if (p == NULL) + { + return NULL; + } + idx += (int)(p - (s + idx)); + if (s[idx] == ':') + { + idx++; + } + p = parse_num(s + idx, 2, &mm); + if (p == NULL) + { + return NULL; + } + idx += (int)(p - (s + idx)); + v = hh * 3600 + mm * 60; + tm->tm_gmtoff = neg ? -v : v; + return p; + } + return NULL; + case 'Z': + /* A timezone name: consume one alphabetic run (no storage). */ + while (is_alpha(*s)) + { + s++; + } + return s; + case '%': + if (*s == '%') + { + return s + 1; + } + return NULL; + default: + return NULL; + } +} + +/* Match s against the format starting at f; returns the input position. */ +static const char * +parse_one(const char *s, const char *f, struct tm *tm, struct parse_ctx *ctx) +{ + while (*f != '\0') + { + char c = *f; + + if (c == '%') + { + f++; + while (*f == '-' || *f == '_' || *f == '0' || *f == '^' || *f == '#') + { + f++; + } + while (is_digit(*f)) + { + f++; + } + if (*f == 'E' || *f == 'O') + { + f++; + } + if (*f == '\0') + { + return NULL; /* dangling '%' cannot match */ + } + s = parse_conv(s, *f, tm, ctx); + if (s == NULL) + { + return NULL; + } + f++; + } + else if (is_space(c)) + { + while (is_space(*f)) + { + f++; + } + s = skip_ws(s); + } + else + { + if (*s != c) + { + return NULL; + } + s++; + f++; + } + } + return s; +} + +char * +strptime(const char *restrict s, const char *restrict format, struct tm *restrict tm) +{ + const char *start = s; + struct parse_ctx ctx; + const char *end; + + ctx.am_pm = 0; + end = parse_one(s, format, tm, &ctx); + if (end == NULL) + { + return NULL; + } + if (end == start) + { + return NULL; /* nothing converted */ + } + return (char *)end; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/time/time.c b/src/time/time.c new file mode 100644 index 0000000..fbc9e98 --- /dev/null +++ b/src/time/time.c @@ -0,0 +1,41 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * time: the current wall-clock time in seconds since the Epoch, read via + * clock_gettime(CLOCK_REALTIME) (the dedicated SYS_time syscall is just a + * gettimeofday wrapper with no added precision). The value is stored + * through t when t is not NULL; time(NULL) is the common "just the value" + * call, so NULL must not be dereferenced. + */ +time_t +time(time_t *t) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) + { + return (time_t)-1; + } + if (t != NULL) + { + *t = ts.tv_sec; + } + return ts.tv_sec; +} + +/* + * difftime: the difference in seconds between two calendar times. + * + * Plain double subtraction needs no libm (the arithmetic is emitted + * inline); returning the exact time_t difference through double preserves + * it for any representable range of times. + */ +double +difftime(time_t a, time_t b) +{ + return (double)a - (double)b; +} diff --git a/src/time/time_impl.h b/src/time/time_impl.h new file mode 100644 index 0000000..75108db --- /dev/null +++ b/src/time/time_impl.h @@ -0,0 +1,72 @@ +#ifndef VLIBC_TIME_IMPL_H +#define VLIBC_TIME_IMPL_H + +/* + * vlibc — internal time/TZ implementation interface. + * + * Shared contract between the mktime/TZ engine (src/time/mktime.c, + * src/time/tzset.c) and, later, the formatting layer. Everything here is + * hidden library-internal API: the public surface stays in . + * + * A parsed POSIX TZ state describes one standard-time zone plus an + * optional daylight-saving regime. All offsets are seconds EAST of UTC + * (EST is -18000, EDT is -14400); the POSIX TZ offset text is seconds + * WEST and gets negated at parse time. The two transition rules delimit + * the DST interval in local civil seconds for a given year, exactly as a + * POSIX "std offset[dst[offset][,start[/time],end[/time]]]" string does. + */ + +#include "../internal/libc.h" + +#include + +/* + * One DST transition rule. kind selects the three POSIX spellings: + * 0: Mm.w.d — month 1-12, week 1-5 (5 = last), weekday 0-6 (Sunday 0); + * 1: Jn — Julian day 1-365, 29 February never counted; + * 2: n — zero-based day 0-365, leap day counted. + * time is the transition instant in seconds after local midnight (the + * default 02:00:00 = 7200). + */ +struct tz_rule +{ + int kind; + int month; + int week; + int wday; + long day; + long time; +}; + +/* Parsed POSIX TZ state (see the file comment for the offset sign). */ +struct tz_state +{ + char std_name[16]; + char dst_name[16]; + long std_off; + long dst_off; + int has_dst; + struct tz_rule start; + struct tz_rule end; +}; + +// NOLINTBEGIN(bugprone-reserved-identifier) +hidden void +__tzset_lazy(void); +hidden const struct tz_state * +__tz_state(void); +hidden int +tz_rule_contains(const struct tz_state *s, long long local_sec); +hidden long +tz_offset_for_local(const struct tz_state *s, long long local_sec, int isdst_hint, int *isdst_out); +hidden long +tz_offset_at_utc(const struct tz_state *s, long long utc_sec, int *isdst_out); +hidden void +__fill_tm(long long local_sec, long gmtoff, int isdst, const char *zone, struct tm *out); +hidden long long +days_from_civil(int y, int m, int d); +hidden void +civil_from_days(long long days, int *y, int *m, int *d); +// NOLINTEND(bugprone-reserved-identifier) + +#endif /* VLIBC_TIME_IMPL_H */ diff --git a/src/time/timer.c b/src/time/timer.c new file mode 100644 index 0000000..35e89ed --- /dev/null +++ b/src/time/timer.c @@ -0,0 +1,173 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * POSIX interval timers (level 2), thin wrappers over SYS_timer_create(222)/ + * SYS_timer_settime(223)/SYS_timer_gettime(224)/SYS_timer_getoverrun(225)/ + * SYS_timer_delete(226). + * + * The public only forward-declares struct sigevent and struct + * itimerspec (their canonical home is , which does not exist in + * vlibc yet), so the kernel ABI mirror of each is defined here, in this + * translation unit, and consumers that build their own struct sigevent / + * struct itimerspec must lay them out to match the kernel exactly: + * + * struct sigevent, x86_64 Linux (128 bytes, kernel uapi layout): + * offset 0: union sigval sigev_value (8 bytes) + * offset 8: int sigev_signo + * offset 12: int sigev_notify (0 SIGEV_SIGNAL, + * 1 SIGEV_NONE, + * 4 SIGEV_THREAD_ID) + * offset 16: union { int _tid; ... } (SIGEV_THREAD_ID target) + * rest: padding, zero. + * The kernel copy_from_user's only the leading part of this struct it + * knows about (64 bytes on current kernels, 128 on older ones); a full + * 128-byte buffer satisfies both, so the kernel mirror below is pinned + * to 128 bytes and always zero-initialized. + * + * struct itimerspec (32 bytes): struct timespec it_interval followed by + * struct timespec it_value, the kernel layout. + * + * timer_t is 's opaque pointer type; the kernel hands out + * plain int timer ids, so timer_create encodes the id into the timer_t + * value (cast to a pointer) and every timer_* wrapper decodes it back. + */ + +#if VLIBC_LEVEL_GE(2) + +/* SIGEV_SIGNAL, the notification kind the POSIX default timer_create(NULL) + * resolves to. */ +#define VLIBC_SIGEV_SIGNAL 0 + +/* SIGALRM, the POSIX default signal for timer_create(NULL). */ +#define VLIBC_SIGALRM 14 + +/* Kernel sigevent mirror: see the file comment for the layout. */ +struct sigevent +{ + union + { + void *sival_ptr; + int sival_int; + } sigev_value; /* 0: payload for the notification */ + + int sigev_signo; /* 8: signal to raise (SIGEV_SIGNAL) */ + int sigev_notify; /* 12: notification mechanism */ + + union + { + int sigev_tid; /* 16: target thread (SIGEV_THREAD_ID) */ + int sigev_pad[28]; /* padding to the 128-byte kernel struct */ + } sigev_un; +}; + +_Static_assert(sizeof(struct sigevent) == 128, "struct sigevent must be the 128-byte kernel ABI"); +_Static_assert(_Alignof(struct sigevent) == 8, "struct sigevent alignment"); + +/* Kernel itimerspec mirror: interval plus next expiry, two timespecs. */ +struct itimerspec +{ + struct timespec it_interval; + struct timespec it_value; +}; + +/* Decode the kernel timer id carried in an opaque timer_t. */ +static int +timer_id(timer_t t) +{ + return (int)(long)t; +} + +/* + * timer_create: create a per-process timer on clockid and store its id + * through timerid. + * + * A NULL evp selects the POSIX default (SIGEV_SIGNAL with SIGALRM), which + * the kernel would also apply to a NULL sigevent; for a real evp only the + * leading 24 bytes the kernel consumes are copied out of the caller's + * struct (value/signo/notify/tid all live there), with the rest of the + * 128-byte kernel image left zero. The kernel validates the notification + * kind and signal itself. + */ +int +timer_create(clockid_t clockid, const struct sigevent *restrict evp, timer_t *restrict timerid) +{ + struct sigevent kev; + int id; + long r; + + memset(&kev, 0, sizeof kev); + if (evp == NULL) + { + kev.sigev_notify = VLIBC_SIGEV_SIGNAL; + kev.sigev_signo = VLIBC_SIGALRM; + } + else + { + memcpy(&kev, evp, 24); + } + + r = syscall_ret(__syscall3(SYS_timer_create, (long)clockid, (long)&kev, (long)&id)); + if (r != 0) + { + return (int)r; + } + *timerid = (timer_t)(long)id; + return 0; +} + +/* + * timer_delete: destroy the timer, releasing its kernel resources and + * cancelling any pending expiry. A timer id is single-use: deleting an + * already-deleted id fails with EINVAL. + */ +int +timer_delete(timer_t timerid) +{ + return syscall_ret(__syscall1(SYS_timer_delete, (long)timer_id(timerid))); +} + +/* + * timer_settime: arm (flags == 0) or re-arm the timer. With TIMER_ABSTIME + * it_value is an absolute expiry on the timer's creation clock; otherwise + * it is relative to now. When old_value is not NULL the previous setting is + * stored there. SYS_timer_settime takes (timerid, flags, new, old). + */ +int +timer_settime(timer_t timerid, int flags, const struct itimerspec *restrict new_value, + struct itimerspec *restrict old_value) +{ + return syscall_ret(__syscall4(SYS_timer_settime, (long)timer_id(timerid), (long)flags, + (long)new_value, (long)old_value)); +} + +/* + * timer_gettime: the timer's current interval and the time remaining until + * its next expiry (0 once expired). Return 0 or -1 with errno set. + */ +int +timer_gettime(timer_t timerid, struct itimerspec *curr_value) +{ + return syscall_ret(__syscall2(SYS_timer_gettime, (long)timer_id(timerid), (long)curr_value)); +} + +/* + * timer_getoverrun: expirations of a periodic timer that were missed + * between the signal delivery just consumed and the one before it, capped + * at DELAYTIMER_MAX. Returns -1 with errno set when no timer signal is + * pending for this process. + */ +int +timer_getoverrun(timer_t timerid) +{ + return (int)syscall_ret(__syscall1(SYS_timer_getoverrun, (long)timer_id(timerid))); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/time/timespec_get.c b/src/time/timespec_get.c new file mode 100644 index 0000000..9421a14 --- /dev/null +++ b/src/time/timespec_get.c @@ -0,0 +1,24 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * timespec_get: ISO C interface over clock_gettime(CLOCK_REALTIME). + * TIME_UTC is the only supported base; any other base, or a clock failure, + * yields 0 (no errno), and success returns the base unchanged. + */ +int +timespec_get(struct timespec *ts, int base) +{ + if (base != TIME_UTC) + { + return 0; + } + if (clock_gettime(CLOCK_REALTIME, ts) != 0) + { + return 0; + } + return base; +} diff --git a/src/time/tzset.c b/src/time/tzset.c new file mode 100644 index 0000000..b3b5aec --- /dev/null +++ b/src/time/tzset.c @@ -0,0 +1,652 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include "time_impl.h" + +/* + * POSIX TZ engine (level-independent core of tzset/localtime). + * + * The one parsed state object mirrors the caller-visible libc timezone + * globals: tzset() re-reads the TZ environment variable on demand and + * (at level 2) republishes tzname/daylight/timezone from the result, while + * localtime/mktime consume the state lazily through __tzset_lazy. Parsing + * is deliberately lenient-fail: any syntax error, an empty TZ, or no TZ at + * all yields the UTC default (std "UTC", zero offset, no DST), never a + * crash and never a partial zone. + * + * POSIX TZ grammar (the subset vlibc accepts): + * std offset[dst[offset][,start[/time],end[/time]]] + * std/dst are 3+ alphabetic characters (or ); offset is + * [+-]hh[:mm[:ss]], the amount ADDED to local time to reach UTC, so the + * stored EAST-of-UTC std_off is its negation ("EST5" -> std_off -18000). + * An omitted dst offset defaults to standard plus one hour. Rules use the + * Mm.w.d, Jn and n spellings with an optional /time (default 02:00:00); + * when DST is present but no rules are given the US M3.2.0/M11.1.0 pair is + * assumed. + */ + +static struct tz_state tz_state; +static int tz_parsed; + +/* Euclidean helpers (same definitions as mktime.c, kept per-TU static). */ +static long long +floor_div(long long a, long long b) +{ + long long q = a / b; + long long r = a % b; + + if (r != 0 && ((r < 0) != (b < 0))) + { + q -= 1; + } + return q; +} + +static long long +floor_mod(long long a, long long b) +{ + long long r = a % b; + + if (r != 0 && ((r < 0) != (b < 0))) + { + r += b; + } + return r; +} + +static int +tz_isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int +tz_isalpha(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +static int +tz_is_leap(int y) +{ + return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; +} + +/* + * Parse a run of digits at *ip into *out; the run must be non-empty. + */ +static int +tz_parse_digits(const char *s, int *ip, long *out) +{ + int i = *ip; + long v = 0; + + if (!tz_isdigit(s[i])) + { + return 0; + } + while (tz_isdigit(s[i])) + { + v = v * 10 + (long)(s[i] - '0'); + i++; + } + *out = v; + *ip = i; + return 1; +} + +/* + * Parse an offset [+-]hh[:mm[:ss]] into signed seconds ("added to local + * time to reach UTC"; negation to the east-of-UTC convention happens in + * the caller). + */ +static int +tz_parse_offset(const char *s, int *ip, long *out) +{ + int i = *ip; + int neg = 0; + long h; + long m = 0; + long sec = 0; + + if (s[i] == '+' || s[i] == '-') + { + neg = (s[i] == '-'); + i++; + } + if (!tz_parse_digits(s, &i, &h)) + { + return 0; + } + if (s[i] == ':') + { + i++; + if (!tz_parse_digits(s, &i, &m)) + { + return 0; + } + if (s[i] == ':') + { + i++; + if (!tz_parse_digits(s, &i, &sec)) + { + return 0; + } + } + } + *out = (h * 3600 + m * 60 + sec) * (neg ? -1 : 1); + *ip = i; + return 1; +} + +/* Parse a rule transition time hh[:mm[:ss]] (no sign; seconds of day). */ +static int +tz_parse_rule_time(const char *s, int *ip, long *out) +{ + int i = *ip; + long h; + long m = 0; + long sec = 0; + + if (!tz_parse_digits(s, &i, &h) || h > 24) + { + return 0; + } + if (s[i] == ':') + { + i++; + if (!tz_parse_digits(s, &i, &m) || m > 59) + { + return 0; + } + if (s[i] == ':') + { + i++; + if (!tz_parse_digits(s, &i, &sec) || sec > 59) + { + return 0; + } + } + } + *out = h * 3600 + m * 60 + sec; + *ip = i; + return 1; +} + +/* + * Parse a zone name: either <...> (any bytes, stored truncated to the + * buffer) or 3+ alphabetic characters. + */ +static int +tz_parse_name(const char *s, int *ip, char *out, size_t cap) +{ + int i = *ip; + size_t n = 0; + + if (s[i] == '<') + { + i++; + while (s[i] != '\0' && s[i] != '>') + { + if (n + 1 < cap) + { + out[n++] = s[i]; + } + i++; + } + if (s[i] != '>') + { + return 0; + } + i++; + if (n == 0) + { + return 0; + } + } + else + { + while (tz_isalpha(s[i])) + { + if (n + 1 < cap) + { + out[n++] = s[i]; + } + i++; + } + if (n < 3) + { + return 0; + } + } + out[n] = '\0'; + *ip = i; + return 1; +} + +/* + * Parse one transition rule (Mm.w.d, Jn, or n), optionally followed by a + * /time suffix; an omitted time defaults to 02:00:00 (7200). Ranges are + * validated so a malformed rule degrades to the UTC default. + */ +static int +tz_parse_rule(const char *s, int *ip, struct tz_rule *r) +{ + int i = *ip; + long v; + + r->kind = 0; + r->month = 0; + r->week = 0; + r->wday = 0; + r->day = 0; + r->time = 7200; + if (s[i] == 'M') + { + i++; + if (!tz_parse_digits(s, &i, &v) || v < 1 || v > 12) + { + return 0; + } + r->month = (int)v; + if (s[i] != '.') + { + return 0; + } + i++; + if (!tz_parse_digits(s, &i, &v) || v < 1 || v > 5) + { + return 0; + } + r->week = (int)v; + if (s[i] != '.') + { + return 0; + } + i++; + if (!tz_parse_digits(s, &i, &v) || v > 6) + { + return 0; + } + r->wday = (int)v; + } + else if (s[i] == 'J') + { + i++; + if (!tz_parse_digits(s, &i, &v) || v < 1 || v > 365) + { + return 0; + } + r->kind = 1; + r->day = v; + } + else + { + if (!tz_parse_digits(s, &i, &v) || v < 0 || v > 365) + { + return 0; + } + r->kind = 2; + r->day = v; + } + if (s[i] == '/') + { + i++; + if (!tz_parse_rule_time(s, &i, &r->time)) + { + return 0; + } + } + *ip = i; + return 1; +} + +/* Parse the full TZ string into st; 1 on success, 0 to fall back to UTC. */ +static int +tz_parse(const char *s, struct tz_state *st) +{ + int i = 0; + struct tz_state tmp; + long west; + int have_dst; + + memset(&tmp, 0, sizeof tmp); + tmp.start.time = 7200; /* 02:00:00 */ + tmp.end.time = 7200; + if (s[0] == ':') + { + return 0; /* ":rest-of-line" spelling is not supported; UTC. */ + } + if (!tz_parse_name(s, &i, tmp.std_name, sizeof tmp.std_name)) + { + return 0; + } + if (!tz_parse_offset(s, &i, &west)) + { + return 0; + } + tmp.std_off = -west; /* east-of-UTC storage */ + + have_dst = 0; + if (s[i] != '\0' && s[i] != ',') + { + if (s[i] == '<' || tz_isalpha(s[i])) + { + if (!tz_parse_name(s, &i, tmp.dst_name, sizeof tmp.dst_name)) + { + return 0; + } + have_dst = 1; + if (s[i] != '\0' && s[i] != ',') + { + if (!tz_parse_offset(s, &i, &west)) + { + return 0; + } + tmp.dst_off = -west; + } + else + { + tmp.dst_off = tmp.std_off + 3600; + } + } + else + { + return 0; + } + } + tmp.has_dst = have_dst; + if (have_dst) + { + if (s[i] == ',') + { + i++; + if (!tz_parse_rule(s, &i, &tmp.start) || s[i] != ',') + { + return 0; + } + i++; + if (!tz_parse_rule(s, &i, &tmp.end)) + { + return 0; + } + } + else + { + /* Default transition rules: US second Sunday in March / first + * Sunday in November, both at 02:00:00 local. */ + tmp.start.kind = 0; + tmp.start.month = 3; + tmp.start.week = 2; + tmp.start.wday = 0; + tmp.end.kind = 0; + tmp.end.month = 11; + tmp.end.week = 1; + tmp.end.wday = 0; + } + } + if (s[i] != '\0') + { + return 0; + } + *st = tmp; + return 1; +} + +/* + * tz_rule_day: day count (days since 1970-01-01) of the calendar day on + * which rule r transitions in `year`, per the rule's spelling. + */ +static long long +tz_rule_day(int year, const struct tz_rule *r) +{ + if (r->kind == 0) + { + static const int mlen[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + if (r->week <= 4) + { + /* w-th weekday of the month. */ + int first_wday = (int)floor_mod(days_from_civil(year, r->month, 1) + 4, 7); + int dom = 1 + (int)floor_mod((long long)r->wday - first_wday, 7) + 7 * (r->week - 1); + + return days_from_civil(year, r->month, dom); + } + else + { + /* Last weekday of the month (week 5 = "last"). */ + int dim = mlen[r->month - 1]; + int last_wday; + + if (r->month == 2 && tz_is_leap(year)) + { + dim++; + } + last_wday = (int)floor_mod(days_from_civil(year, r->month, dim) + 4, 7); + return days_from_civil(year, r->month, + dim - (int)floor_mod((long long)last_wday - r->wday, 7)); + } + } + else + { + long long doy; + + if (r->kind == 1) + { + /* Julian day, never counting 29 February. */ + if (r->day <= 59) + { + doy = r->day - 1; + } + else + { + doy = r->day - (tz_is_leap(year) ? 0 : 1); + } + } + else + { + doy = r->day; /* zero-based day, leap day counted */ + } + return days_from_civil(year, 1, 1) + doy; + } +} + +/* + * tz_rule_contains: is DST in effect at the given LOCAL civil time + * (seconds, in the no-offset frame all TZ math uses)? + * + * The two transitions are computed for the civil year that contains + * local_sec. When the start rule comes before the end rule in the year the + * DST interval is [start, end); when it comes after (southern hemisphere, + * DST spanning the new year) the interval wraps, so local_sec before the + * year's end transition or at/after its start transition is daylight time. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters,bugprone-reserved-identifier) +int +tz_rule_contains(const struct tz_state *s, long long local_sec) +{ + int y; + int m; + int d; + long long start_day; + long long end_day; + long long start_sec; + long long end_sec; + + if (!s->has_dst) + { + return 0; + } + civil_from_days(floor_div(local_sec, 86400), &y, &m, &d); + (void)m; + (void)d; + start_day = tz_rule_day(y, &s->start); + end_day = tz_rule_day(y, &s->end); + start_sec = start_day * 86400 + s->start.time; + end_sec = end_day * 86400 + s->end.time; + if (start_day <= end_day) + { + return local_sec >= start_sec && local_sec < end_sec; + } + return local_sec >= start_sec || local_sec < end_sec; +} + +/* + * tz_offset_for_local: the offset (and DST flag) for a local civil time. + * A positive isdst_hint forces daylight time, a zero hint forces standard + * time, and a negative hint ("unknown", as mktime receives from callers + * that do not know) is resolved by the zone rules. + */ +long +tz_offset_for_local(const struct tz_state *s, long long local_sec, int isdst_hint, int *isdst_out) +{ + int isdst; + long off; + + if (s->has_dst && isdst_hint > 0) + { + isdst = 1; + } + else if (s->has_dst && isdst_hint < 0) + { + isdst = tz_rule_contains(s, local_sec); + } + else + { + isdst = 0; + } + off = isdst ? s->dst_off : s->std_off; + if (isdst_out != NULL) + { + *isdst_out = isdst; + } + return off; +} + +/* + * tz_offset_at_utc: the offset (and DST flag) in effect at the given UTC + * instant, found by the standard fixed-point iteration — assume standard + * time, convert to a local civil guess, consult the rules, and re-derive + * the offset once (DST transitions move the local time by at most one + * hour, so a second pass settles it). + */ +long +tz_offset_at_utc(const struct tz_state *s, long long utc_sec, int *isdst_out) +{ + long off = s->std_off; + int isdst = 0; + + if (s->has_dst) + { + isdst = tz_rule_contains(s, utc_sec + off); + off = isdst ? s->dst_off : s->std_off; + isdst = tz_rule_contains(s, utc_sec + off); + off = isdst ? s->dst_off : s->std_off; + } + if (isdst_out != NULL) + { + *isdst_out = isdst; + } + return off; +} +// NOLINTEND(bugprone-reserved-identifier) + +/* + * __tzset_lazy: (re)parse TZ into the static state, but only once per + * tzset() call. Callers may run before the environment is installed, so + * getenv returning nothing is simply the UTC default. + */ +// NOLINTBEGIN(bugprone-reserved-identifier) +void +__tzset_lazy(void) +{ + const char *tz; + + if (tz_parsed) + { + return; + } + tz = getenv("TZ"); + if (tz == NULL || tz[0] == '\0' || !tz_parse(tz, &tz_state)) + { + memset(&tz_state, 0, sizeof tz_state); + strcpy(tz_state.std_name, "UTC"); + } + tz_parsed = 1; +#if VLIBC_LEVEL_GE(2) + tzname[0] = tz_state.std_name; + tzname[1] = tz_state.dst_name; + daylight = tz_state.has_dst; + timezone = -tz_state.std_off; /* POSIX: seconds WEST of UTC */ +#endif +} + +const struct tz_state * +__tz_state(void) +{ + __tzset_lazy(); + return &tz_state; +} +// NOLINTEND(bugprone-reserved-identifier) + +/* + * tzset: re-read TZ on the next access. The parse itself is deferred to + * __tzset_lazy so that a tzset() with no following time query is cheap. + */ +void +tzset(void) +{ + tz_parsed = 0; + __tzset_lazy(); +} + +struct tm * +gmtime_r(const time_t *timer, struct tm *result) +{ + __fill_tm((long long)*timer, 0, 0, "UTC", result); + return result; +} + +struct tm * +localtime_r(const time_t *timer, struct tm *result) +{ + const struct tz_state *s; + long off; + int isdst; + + __tzset_lazy(); + s = __tz_state(); + off = tz_offset_at_utc(s, (long long)*timer, &isdst); + __fill_tm((long long)*timer + off, off, isdst, isdst ? s->dst_name : s->std_name, result); + return result; +} + +static struct tm tz_static_tm; + +struct tm * +gmtime(const time_t *timer) +{ + return gmtime_r(timer, &tz_static_tm); +} + +struct tm * +localtime(const time_t *timer) +{ + return localtime_r(timer, &tz_static_tm); +} + +#if VLIBC_LEVEL_GE(2) + +/* POSIX timezone globals, kept in sync by tzset()/__tzset_lazy(). */ +char *tzname[2]; +int daylight; +long timezone; + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_time.c b/tests/test_time.c new file mode 100644 index 0000000..6bf8410 --- /dev/null +++ b/tests/test_time.c @@ -0,0 +1,384 @@ +/* + * vlibc — time library test (todo 26, slice S3). + * + * Exercises the full time stack end to end against golden values verified + * against glibc (C locale, TZ=UTC): + * + * 1. strftime: every POSIX conversion on a fixed 2024-03-15 14:30:45 + * (Friday, yday 74) struct, plus %e space-padding and %z rendering of + * a negative offset. + * 2. asctime/ctime fixed-format "Www Mmm dd hh:mm:ss yyyy\n" rendering. + * 3. mktime/gmtime/timegm round-trips; localtime under TZ=UTC. + * 4. nanosleep and (level 2) usleep, strptime round-trip, and the POSIX + * interval-timer syscall wrappers. + * + * The -f mode exercises the failure shapes (buffer overflow, year outside + * [1000,9999], unmatchable strptime) and leaves via a raw SYS_exit_group so + * no host cleanup runs afterwards (vlibc's errno slot collides with the + * host TCB, so errno is never read here). + * + * Host-header-free: everything comes from vlibc's public headers via + * -Iinclude; diagnostics go through raw SYS_write; environ is defined in + * this TU (the host startup does not install the vlibc environment) and + * pointed at the envp main receives. + */ + +#include + +#include + +#include + +#define TEST_SYS_WRITE 1 +#define TEST_SYS_EXIT_GROUP 231 + +char **environ; +char **__environ; // NOLINT(bugprone-reserved-identifier) + +#if VLIBC_LEVEL_GE(2) +/* Level-2 timer ABI mirror (see src/time/timer.c). */ +struct itimerspec +{ + struct timespec it_interval; + struct timespec it_value; +}; +#endif + +static int failures; + +static long +raw3(long n, long a, long b, long c) +{ + unsigned long ret; + + __asm__ volatile("syscall" + : "=a"(ret) + : "a"(n), "D"(a), "S"(b), "d"(c) + : "rcx", "r11", "memory"); + return (long)ret; +} + +static long +raw1(long n, long a) +{ + unsigned long ret; + + __asm__ volatile("syscall" : "=a"(ret) : "a"(n), "D"(a) : "rcx", "r11", "memory"); + return (long)ret; +} + +static void +say(const char *s) +{ + size_t n = 0; + + while (s[n] != '\0') + { + n++; + } + raw3(TEST_SYS_WRITE, 1, (long)s, (long)n); +} + +static void +saynum(long v) +{ + char buf[24]; + size_t i = sizeof buf; + + if (v == 0) + { + say("0"); + return; + } + if (v < 0) + { + say("-"); + v = -v; + } + while (v > 0) + { + buf[--i] = (char)('0' + v % 10); + v /= 10; + } + raw3(TEST_SYS_WRITE, 1, (long)(buf + i), (long)(sizeof buf - i)); +} + +static void +check(int cond, const char *what) +{ + if (cond) + { + say("ok "); + } + else + { + say("FAIL "); + failures++; + } + say(what); + say("\n"); +} + +/* The canonical 2024-03-15 14:30:45 struct, normalized via mktime in UTC. */ +static void +make_tm(struct tm *t) +{ + memset(t, 0, sizeof *t); + t->tm_year = 2024 - 1900; + t->tm_mon = 2; + t->tm_mday = 15; + t->tm_hour = 14; + t->tm_min = 30; + t->tm_sec = 45; + t->tm_isdst = -1; +} + +static void +strftime_scenario(void) +{ + static const struct + { + const char *fmt; + const char *want; + } gold[] = { + {"%a", "Fri"}, + {"%A", "Friday"}, + {"%b", "Mar"}, + {"%B", "March"}, + {"%c", "Fri Mar 15 14:30:45 2024"}, + {"%C", "20"}, + {"%d", "15"}, + {"%D", "03/15/24"}, + {"%e", "15"}, + {"%F", "2024-03-15"}, + {"%g", "24"}, + {"%G", "2024"}, + {"%h", "Mar"}, + {"%H", "14"}, + {"%I", "02"}, + {"%j", "075"}, + {"%m", "03"}, + {"%M", "30"}, + {"%p", "PM"}, + {"%r", "02:30:45 PM"}, + {"%R", "14:30"}, + {"%S", "45"}, + {"%T", "14:30:45"}, + {"%u", "5"}, + {"%U", "10"}, + {"%V", "11"}, + {"%w", "5"}, + {"%W", "11"}, + {"%x", "03/15/24"}, + {"%X", "14:30:45"}, + {"%y", "24"}, + {"%Y", "2024"}, + {"%z", "+0000"}, + {"%Z", "UTC"}, + {"%%", "%"}, + {"%Y-%m-%d %H:%M:%S", "2024-03-15 14:30:45"}, + }; + struct tm t; + struct tm t5; + char buf[128]; + size_t i; + time_t e; + size_t want_len; + + make_tm(&t); + e = mktime(&t); + check(t.tm_wday == 5 && t.tm_yday == 74, "mktime normalized 2024-03-15 -> Fri/yday 74"); + check(t.tm_gmtoff == 0 && t.tm_isdst == 0, "mktime under TZ=UTC keeps gmtoff 0 isdst 0"); + + for (i = 0; i < sizeof gold / sizeof gold[0]; i++) + { + size_t n = strftime(buf, sizeof buf, gold[i].fmt, &t); + + want_len = 0; + while (gold[i].want[want_len] != '\0') + { + want_len++; + } + if (strcmp(buf, gold[i].want) != 0 || n != want_len) + { + check(0, gold[i].fmt); + } + else + { + check(1, gold[i].fmt); + } + } + + t5 = t; + t5.tm_mday = 5; + check(strftime(buf, sizeof buf, "%e", &t5) == 2 && strcmp(buf, " 5") == 0, + "%e space-pads single digits"); + + t.tm_gmtoff = -18000; + check(strftime(buf, sizeof buf, "%z", &t) == 5 && strcmp(buf, "-0500") == 0, + "%z renders negative gmtoff"); + + check(strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", &t) == 19 && + strcmp(buf, "2024-03-15 14:30:45") == 0, + "combined format golden"); + (void)e; +} + +static void +roundtrip_scenario(void) +{ + struct tm tmv; + struct timespec ts; + time_t e0 = 1700000000; + time_t t0 = 0; + struct tm gtm; + time_t back; + + /* ctime golden (UTC): same text as asctime of the fixed time. */ + back = 0; + (void)back; + tmv.tm_year = 124; + tmv.tm_mon = 2; + tmv.tm_mday = 15; + tmv.tm_hour = 14; + tmv.tm_min = 30; + tmv.tm_sec = 45; + tmv.tm_isdst = -1; + back = mktime(&tmv); + check(strcmp(asctime(&tmv), "Fri Mar 15 14:30:45 2024\n") == 0, "asctime golden"); + check(strcmp(ctime(&back), "Fri Mar 15 14:30:45 2024\n") == 0, "ctime golden"); + + /* mktime round-trips gmtime. */ + gtm = *gmtime(&e0); + back = mktime(>m); + check(back == e0, "mktime round-trips gmtime(1700000000)"); + + localtime_r(&t0, &tmv); + check(tmv.tm_gmtoff == 0 && tmv.tm_isdst == 0, "localtime(0) under TZ=UTC"); + + ts.tv_sec = 0; + ts.tv_nsec = 1000000L; + check(nanosleep(&ts, NULL) == 0, "nanosleep 1ms"); + +#if VLIBC_LEVEL_GE(2) + /* timegm round-trips gmtime without any timezone state. */ + gtm = *gmtime(&e0); + check(timegm(>m) == e0, "timegm round-trips gmtime(1700000000)"); + + /* strptime parses what strftime emitted for the fixed time. */ + { + char sbuf[64]; + char abuf[26]; + struct tm pt; + struct tm ft; + time_t fe; + + make_tm(&ft); + fe = mktime(&ft); + check(fe != (time_t)-1, "mktime on fixed time succeeds"); + strftime(sbuf, sizeof sbuf, "%Y-%m-%d %H:%M:%S", &ft); + check(strcmp(sbuf, "2024-03-15 14:30:45") == 0, "strftime source for round-trip"); + check(strptime(sbuf, "%Y-%m-%d %H:%M:%S", &pt) != NULL, "strptime parses golden"); + check(timegm(&pt) == fe, "strptime -> timegm round-trip equals mktime"); + + check(strcmp(asctime_r(&ft, abuf), "Fri Mar 15 14:30:45 2024\n") == 0, "asctime_r golden"); + check(strcmp(ctime_r(&fe, abuf), "Fri Mar 15 14:30:45 2024\n") == 0, "ctime_r golden"); + + check(usleep(1000) == 0, "usleep 1000us"); + } + + /* POSIX interval timers (S1 wrappers). */ + { + struct itimerspec itv; + struct itimerspec got; + timer_t tid = 0; + + check(timer_create(CLOCK_REALTIME, NULL, &tid) == 0, "timer_create"); + itv.it_interval.tv_sec = 0; + itv.it_interval.tv_nsec = 0; + itv.it_value.tv_sec = 0; + itv.it_value.tv_nsec = 10000000L; + check(timer_settime(tid, 0, &itv, NULL) == 0, "timer_settime 10ms"); + got.it_value.tv_sec = 0; + got.it_value.tv_nsec = 0; + check(timer_gettime(tid, &got) == 0 && + (got.it_value.tv_sec > 0 || got.it_value.tv_nsec > 0), + "timer_gettime reports pending expiry"); + check(timer_delete(tid) == 0, "timer_delete"); + } +#endif +} + +static void +failure_scenarios(void) +{ + struct tm t; + struct tm y10k; + char buf[4]; + + make_tm(&t); + mktime(&t); + + check(strftime(buf, sizeof buf, "%Y-%m-%d", &t) == 0, "-f strftime overflow returns 0"); + + memset(&y10k, 0, sizeof y10k); + y10k.tm_year = 10000 - 1900; + y10k.tm_mon = 0; + y10k.tm_mday = 1; + y10k.tm_hour = 0; + y10k.tm_min = 0; + y10k.tm_sec = 0; + y10k.tm_wday = 0; + check(asctime(&y10k) == NULL, "-f asctime year 10000 returns NULL"); + +#if VLIBC_LEVEL_GE(2) + { + struct tm pt; + + check(strptime("nope", "%Y", &pt) == NULL, "-f strptime mismatch returns NULL"); + } +#endif +} + +int +main(int argc, char **argv, char **envp) +{ + int fail_mode = argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f'; + + environ = envp; + __environ = envp; + + setenv("TZ", "UTC", 1); + tzset(); + + if (fail_mode) + { + failure_scenarios(); + if (failures != 0) + { + say("FAILURES: "); + saynum(failures); + say("\n"); + } + else + { + say("all time -f tests passed\n"); + } + raw1(TEST_SYS_EXIT_GROUP, failures != 0); + /* not reached */ + return failures != 0; + } + + strftime_scenario(); + roundtrip_scenario(); + + if (failures != 0) + { + say("FAILURES: "); + saynum(failures); + say("\n"); + return 1; + } + say("all time tests passed\n"); + return 0; +}