diff --git a/libc/config/linux/api.td b/libc/config/linux/api.td --- a/libc/config/linux/api.td +++ b/libc/config/linux/api.td @@ -20,6 +20,28 @@ }]; } +def StructTm: TypeDecl<"struct tm"> { + let Decl = [{ + struct tm { + int tm_sec; // seconds after the minute + int tm_min; // minutes after the hour + int tm_hour; // hours since midnight + int tm_mday; // day of the month + int tm_mon; // months since January + int tm_year; // years since 1900 + int tm_wday; // days since Sunday + int tm_yday; // days since January + int tm_isdst; // Daylight Saving Time flag + }; + }]; +} + +def TimeT: TypeDecl<"time_t"> { + let Decl = [{ + typedef long time_t; + }]; +} + def OffT : TypeDecl<"off_t"> { let Decl = [{ #define __need_off_t @@ -177,6 +199,17 @@ def StdlibAPI : PublicAPI<"stdlib.h"> { } +def TimeAPI : PublicAPI<"time.h"> { + let TypeDeclarations = [ + StructTm, + TimeT, + ]; + + let Functions = [ + "mktime", + ]; +} + def ErrnoAPI : PublicAPI<"errno.h"> { let Macros = [ ErrnoMacro, diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt --- a/libc/config/linux/x86_64/entrypoints.txt +++ b/libc/config/linux/x86_64/entrypoints.txt @@ -67,6 +67,9 @@ libc.src.threads.thrd_create libc.src.threads.thrd_join + # time.h entrypoints + libc.src.time.mktime + # unistd.h entrypoints libc.src.unistd.write ) diff --git a/libc/config/linux/x86_64/headers.txt b/libc/config/linux/x86_64/headers.txt --- a/libc/config/linux/x86_64/headers.txt +++ b/libc/config/linux/x86_64/headers.txt @@ -9,5 +9,6 @@ libc.include.sys_mman libc.include.sys_syscall libc.include.threads + libc.include.time libc.include.unistd ) diff --git a/libc/include/CMakeLists.txt b/libc/include/CMakeLists.txt --- a/libc/include/CMakeLists.txt +++ b/libc/include/CMakeLists.txt @@ -50,6 +50,14 @@ .llvm_libc_common_h ) +add_gen_header( + time + DEF_FILE time.h.def + GEN_HDR time.h + DEPENDS + .llvm_libc_common_h +) + add_gen_header( threads DEF_FILE threads.h.def diff --git a/libc/include/time.h.def b/libc/include/time.h.def new file mode 100644 --- /dev/null +++ b/libc/include/time.h.def @@ -0,0 +1,16 @@ +//===-- C standard library header time.h ----------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_TIME_H +#define LLVM_LIBC_TIME_H + +#include <__llvm-libc-common.h> + +%%public_api() + +#endif // LLVM_LIBC_TIME_H diff --git a/libc/spec/spec.td b/libc/spec/spec.td --- a/libc/spec/spec.td +++ b/libc/spec/spec.td @@ -79,6 +79,8 @@ def SigHandlerT : NamedType<"__sighandler_t">; +def TimeTType : NamedType<"time_t">; + //added because __assert_fail needs it. def UnsignedType : NamedType<"unsigned">; diff --git a/libc/spec/stdc.td b/libc/spec/stdc.td --- a/libc/spec/stdc.td +++ b/libc/spec/stdc.td @@ -3,6 +3,8 @@ NamedType FILE = NamedType<"FILE">; PtrType FILEPtr = PtrType; RestrictedPtrType FILERestrictedPtr = RestrictedPtrType; + NamedType StructTmType = NamedType<"struct tm">; + PtrType StructTmPtr = PtrType; HeaderSpec Assert = HeaderSpec< "assert.h", @@ -463,6 +465,23 @@ ] >; + HeaderSpec Time = HeaderSpec< + "time.h", + [], // Macros + [ // Types + StructTmType, + TimeTType, + ], + [], // Enumerations + [ + FunctionSpec< + "mktime", + RetValSpec, + [ArgSpec] + >, + ] + >; + let Headers = [ Assert, CType, @@ -473,5 +492,6 @@ StdLib, Signal, Threads, + Time, ]; } diff --git a/libc/src/CMakeLists.txt b/libc/src/CMakeLists.txt --- a/libc/src/CMakeLists.txt +++ b/libc/src/CMakeLists.txt @@ -9,6 +9,7 @@ # TODO: Add this target conditional to the target OS. add_subdirectory(sys) add_subdirectory(threads) +add_subdirectory(time) add_subdirectory(unistd) add_subdirectory(__support) diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libc/src/time/CMakeLists.txt @@ -0,0 +1,11 @@ +add_entrypoint_object( + mktime + SRCS + mktime.cpp + HDRS + mktime.h + DEPENDS + libc.include.errno + libc.include.time + libc.src.errno.__errno_location +) diff --git a/libc/src/time/mktime.h b/libc/src/time/mktime.h new file mode 100644 --- /dev/null +++ b/libc/src/time/mktime.h @@ -0,0 +1,23 @@ +//===-- Implementation header of mktime -------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_TIME_MKTIME_H +#define LLVM_LIBC_SRC_TIME_MKTIME_H + +#include "src/time/mktime.h" +#include + +namespace __llvm_libc { + +time_t mktime(struct tm *t1); + +} // namespace __llvm_libc + +#endif // LLVM_LIBC_SRC_TIME_MKTIME_H + +#include "include/time.h" diff --git a/libc/src/time/mktime.cpp b/libc/src/time/mktime.cpp new file mode 100644 --- /dev/null +++ b/libc/src/time/mktime.cpp @@ -0,0 +1,125 @@ +//===-- Implementation of mktime function ---------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "include/errno.h" + +#include "src/__support/common.h" +#include "src/errno/llvmlibc_errno.h" +#include "src/time/mktime.h" + +namespace __llvm_libc { + +constexpr int SecondsPerMin = 60; +constexpr int MinutesPerHour = 60; +constexpr int HoursPerDay = 24; +constexpr int DaysPerWeek = 7; +constexpr int MonthsPerYear = 12; +constexpr int DaysPerNonLeapYear = 365; +constexpr int TimeYearBase = 1900; +constexpr int EpochYear = 1970; +constexpr int EpochWeekDay = 4; +// The latest time that can be represented in this form is 03:14:07 UTC on +// Tuesday, 19 January 2038 (corresponding to 2,147,483,647 seconds since the +// start of the epoch). This means that systems using a 32-bit time_t type are +// susceptible to the Year 2038 problem. +constexpr int EndOf32BitEpochYear = 2038; + +constexpr int NonLeapYearDaysInMonth[] = {31 /* Jan */, 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31}; + +constexpr bool isLeapYear(const time_t year) { + return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)); +} + +// POSIX.1-2017 requires this. +static inline time_t outOfRange() { + llvmlibc_errno = EOVERFLOW; + return (time_t)(-1); +} + +time_t LLVM_LIBC_ENTRYPOINT(mktime)(struct tm *t1) { + // Unlike most C Library functions, mktime doesn't just die on bad input. + // TODO(rtenneti); Handle leap seconds. + if (t1->tm_sec < 0 || t1->tm_sec > (SecondsPerMin - 1)) + return outOfRange(); + if (t1->tm_min < 0 || t1->tm_min > (MinutesPerHour - 1)) + return outOfRange(); + if (t1->tm_hour < 0 || t1->tm_hour > (HoursPerDay - 1)) + return outOfRange(); + time_t tmYearFromBase = t1->tm_year + TimeYearBase; + + if (tmYearFromBase < EpochYear) + return outOfRange(); + + // 32-bit end-of-the-world is 03:14:07 UTC on 19 January 2038. + // + // TODO(rtenneti): Investigate if we can "do the overflow check in a more + // general fashion? For example, we can find a maxYears that can fit in + // TIME_T_MAX (I am making up TIME_T_MAX). After that, assuming valid inputs + // for other fields, we can progressively check if the result will fit in + // time_t." + if (sizeof(time_t) == 4 && tmYearFromBase >= EndOf32BitEpochYear) { + if (tmYearFromBase > EndOf32BitEpochYear) + return outOfRange(); + if (t1->tm_mon > 0) + return outOfRange(); + if (t1->tm_mday > 19) + return outOfRange(); + if (t1->tm_hour > 3) + return outOfRange(); + if (t1->tm_min > 14) + return outOfRange(); + if (t1->tm_sec > 7) + return outOfRange(); + } + + // Years are ints. A 32-bit year will fit into a 64-bit time_t. + // A 64-bit year will not. + static_assert(sizeof(int) == 4, + "ILP64 is unimplemented. This implementation requires " + "32-bit integers."); + + if (t1->tm_mon < 0 || t1->tm_mon > (MonthsPerYear - 1)) + return outOfRange(); + bool tmYearIsLeap = isLeapYear(tmYearFromBase); + time_t daysInMonth = NonLeapYearDaysInMonth[t1->tm_mon]; + // Add one day if it is a leap year and the month is February. + if (tmYearIsLeap && t1->tm_mon == 1) + ++daysInMonth; + if (t1->tm_mday < 1 || t1->tm_mday > daysInMonth) + return outOfRange(); + + time_t totalDays = t1->tm_mday - 1; + for (int i = 0; i < t1->tm_mon; ++i) + totalDays += NonLeapYearDaysInMonth[i]; + // Add one day if it is a leap year and the month is after February. + if (tmYearIsLeap && t1->tm_mon > 1) + totalDays++; + t1->tm_yday = totalDays; + totalDays += (tmYearFromBase - EpochYear) * DaysPerNonLeapYear; + + // Add an extra day for each leap year, starting with 1972 + for (time_t year = EpochYear + 2; year < tmYearFromBase;) { + if (isLeapYear(year)) { + totalDays += 1; + year += 4; + } else { + year++; + } + } + + t1->tm_wday = (EpochWeekDay + totalDays) % DaysPerWeek; + if (t1->tm_wday < 0) + t1->tm_wday += DaysPerWeek; + // TODO(rtenneti): Need to handle timezone and update of tm_isdst. + return t1->tm_sec + t1->tm_min * SecondsPerMin + + t1->tm_hour * MinutesPerHour * SecondsPerMin + + totalDays * HoursPerDay * MinutesPerHour * SecondsPerMin; +} + +} // namespace __llvm_libc diff --git a/libc/test/src/CMakeLists.txt b/libc/test/src/CMakeLists.txt --- a/libc/test/src/CMakeLists.txt +++ b/libc/test/src/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(string) add_subdirectory(sys) add_subdirectory(threads) +add_subdirectory(time) add_subdirectory(unistd) set(public_test ${CMAKE_CURRENT_BINARY_DIR}/public_integration_test.cpp) diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libc/test/src/time/CMakeLists.txt @@ -0,0 +1,11 @@ +add_libc_testsuite(libc_time_unittests) + +add_libc_unittest( + mktime + SUITE + libc_time_unittests + SRCS + mktime_test.cpp + DEPENDS + libc.src.time.mktime +) diff --git a/libc/test/src/time/mktime_test.cpp b/libc/test/src/time/mktime_test.cpp new file mode 100644 --- /dev/null +++ b/libc/test/src/time/mktime_test.cpp @@ -0,0 +1,135 @@ +//===-- Unittests for mktime ----------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/time/mktime.h" +#include "test/ErrnoSetterMatcher.h" +#include "utils/UnitTest/Test.h" + +#include +#include + +using __llvm_libc::testing::ErrnoSetterMatcher::Fails; + +// A helper function to initialize tm data structure. +static void initialize_tm_data(struct tm *tm_data, time_t year, time_t month, + time_t mday, time_t hour, time_t min, + time_t sec) { + memset(tm_data, 0, sizeof(struct tm)); + tm_data->tm_sec = sec; // seconds after the minute + tm_data->tm_min = min; // minutes after the hour + tm_data->tm_hour = hour; // hours since midnight + tm_data->tm_mday = mday; // day of the month + tm_data->tm_mon = month; // months since January + tm_data->tm_year = year - 1900; // years since 1900 +} + +static time_t mktime_with_tm_data(struct tm *tm_data, time_t year, time_t month, + time_t mday, time_t hour, time_t min, + time_t sec) { + initialize_tm_data(tm_data, year, month, mday, hour, min, sec); + return __llvm_libc::mktime(tm_data); +} + +static time_t mktime(time_t year, time_t month, time_t mday, time_t hour, + time_t min, time_t sec) { + struct tm tm_data; + return mktime_with_tm_data(&tm_data, year, month, mday, hour, min, sec); +} + +TEST(MkTime, FailureSetsErrno) { + struct tm temp; + initialize_tm_data(&temp, 0, 0, 0, 0, 0, -1); + EXPECT_THAT(__llvm_libc::mktime(&temp), Fails(EOVERFLOW)); +} + +TEST(MkTime, MktimeTestsInvalidSeconds) { + EXPECT_EQ(mktime(0, 0, 0, 0, 0, -1), (time_t)-1); + EXPECT_EQ(mktime(0, 0, 0, 0, 0, 60), (time_t)-1); +} + +TEST(MkTime, MktimeTestsInvalidMinutes) { + EXPECT_EQ(mktime(0, 0, 0, 0, -1, 0), (time_t)-1); + EXPECT_EQ(mktime(0, 0, 1, 0, 60, 0), (time_t)-1); +} + +TEST(MkTime, MktimeTestsInvalidHours) { + EXPECT_EQ(mktime(0, 0, 0, -1, 0, 0), (time_t)-1); + EXPECT_EQ(mktime(0, 0, 0, 24, 0, 0), (time_t)-1); +} + +TEST(MkTime, MktimeTestsInvalidYear) { + EXPECT_EQ(mktime(1969, 0, 0, 0, 0, 0), (time_t)-1); +} + +TEST(MkTime, MktimeTestsInvalidEndOf32BitEpochYear) { + if (sizeof(time_t) != 4) + return; + EXPECT_EQ(mktime(2038, 0, 19, 3, 14, 8), (time_t)-1); // 2038-01-19 03:14:08 + EXPECT_EQ(mktime(2038, 0, 19, 3, 15, 7), (time_t)-1); // 2038-01-19 03:15:07 + EXPECT_EQ(mktime(2038, 0, 19, 4, 14, 7), (time_t)-1); // 2038-01-19 04:14:07 + EXPECT_EQ(mktime(2038, 0, 20, 3, 14, 7), (time_t)-1); // 2038-01-20 03:14:07 + EXPECT_EQ(mktime(2038, 1, 19, 3, 14, 7), (time_t)-1); // 2038-02-19 03:14:07 + EXPECT_EQ(mktime(2039, 0, 19, 3, 14, 7), (time_t)-1); // 2038-01-19 03:14:07 +} + +TEST(MkTime, MktimeTestsInvalidMonths) { + EXPECT_EQ(mktime(1970, -1, 0, 0, 0, 0), (time_t)-1); // Before Jan of 1970 + EXPECT_EQ(mktime(1970, 12, 0, 0, 0, 0), (time_t)-1); // After Dec of 1970 +} + +TEST(MkTime, MktimeTestsInvalidDays) { + EXPECT_EQ(mktime(1970, 0, -1, 0, 0, 0), (time_t)-1); // -1 day of Jan, 1970 + EXPECT_EQ(mktime(1970, 0, 32, 0, 0, 0), (time_t)-1); // 32 day of Jan, 1970 + EXPECT_EQ(mktime(1970, 1, 29, 0, 0, 0), (time_t)-1); // 29 day of Feb, 1970 + EXPECT_EQ(mktime(1972, 1, 30, 0, 0, 0), (time_t)-1); // 30 day of Feb, 1972 + EXPECT_EQ(mktime(1970, 3, 31, 0, 0, 0), (time_t)-1); // 31 day of Apr, 1970 +} + +TEST(MkTime, MktimeTestsStartEpochYear) { + // Thu Jan 1 00:00:00 1970 + struct tm tm_data; + EXPECT_EQ(mktime_with_tm_data(&tm_data, 1970, 0, 1, 0, 0, 0), (time_t)0); + EXPECT_EQ(4, tm_data.tm_wday); + EXPECT_EQ(0, tm_data.tm_yday); +} + +TEST(MkTime, MktimeTestsEpochYearRandomTime) { + // Thu Jan 1 12:50:50 1970 + struct tm tm_data; + EXPECT_EQ(mktime_with_tm_data(&tm_data, 1970, 0, 1, 12, 50, 50), + (time_t)46250); + EXPECT_EQ(4, tm_data.tm_wday); + EXPECT_EQ(0, tm_data.tm_yday); +} + +TEST(MkTime, MktimeTestsEndOf32BitEpochYear) { + struct tm tm_data; + // Test for maximum value of a signed 32-bit integer. + // Test implementation can encode time for Tue 19 January 2038 03:14:07 UTC. + EXPECT_EQ(mktime_with_tm_data(&tm_data, 2038, 0, 19, 3, 14, 7), + (time_t)0x7FFFFFFF); + EXPECT_EQ(2, tm_data.tm_wday); + EXPECT_EQ(18, tm_data.tm_yday); +} + +TEST(MkTime, MktimeTests64BitYear) { + if (sizeof(time_t) == 4) + return; + // Mon Jan 1 12:50:50 2170 + struct tm tm_data; + EXPECT_EQ(mktime_with_tm_data(&tm_data, 2170, 0, 1, 12, 50, 50), + (time_t)6311479850); + EXPECT_EQ(1, tm_data.tm_wday); + EXPECT_EQ(0, tm_data.tm_yday); + + // Test for Tue Jan 1 12:50:50 in 2,147,483,647th year. + EXPECT_EQ(mktime_with_tm_data(&tm_data, (time_t)2147483647, 0, 1, 12, 50, 50), + (time_t)67767976202043050); + EXPECT_EQ(2, tm_data.tm_wday); + EXPECT_EQ(0, tm_data.tm_yday); +}