Files
nt-lite/code/kernel/ke_time.c
T

501 lines
18 KiB
C

// ke_time.c - kernel wall-clock time service
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <ntlite/status.h>
#include <ntlite/boot_info.h>
#include <ntlite/ddi/hal_api.h>
#include <kernel/ke_time.h>
#include <kernel/ke_debug.h>
#include <kernel/ke_settings.h>
#define KE_TIME_FALLBACK_YEAR 2024u
#define KE_TIME_FALLBACK_MONTH 1u
#define KE_TIME_FALLBACK_DAY 1u
#define KE_TZ_UNSPECIFIED SYS_TIME_UNSPECIFIED_TZ
#define KE_CE_STANDARD_OFFSET 60
#define KE_DST_OFFSET_DELTA 60
typedef enum KE_DST_RULE {
KeDstRuleNone = 0,
KeDstRuleAlways,
KeDstRuleCentralEurope,
} KE_DST_RULE;
typedef struct KE_TIME_ZONE_POLICY {
int16_t StandardOffsetMinutes; // minutes east of UTC.
KE_DST_RULE DstRule;
bool TimeZoneKnown;
} KE_TIME_ZONE_POLICY;
typedef enum KE_HARDWARE_CLOCK_MODE {
KeHardwareClockUtc = 0,
KeHardwareClockLocal,
} KE_HARDWARE_CLOCK_MODE;
static const HAL_API* gTimeHal;
static uint64_t gSourceRawEpochSeconds;
static int16_t gSourceTimeZoneMinutes;
static bool gSourceTimeZoneKnown;
static uint32_t gSourceNanosecond;
static uint64_t gSourceTickMicroseconds;
static uint64_t gBaseUtcEpochSeconds;
static uint64_t gBaseTickMicroseconds;
static uint16_t gSourceFlags;
static KE_TIME_ZONE_POLICY gTimeZonePolicy = {
KE_CE_STANDARD_OFFSET,
KeDstRuleCentralEurope,
true,
};
static KE_HARDWARE_CLOCK_MODE gHardwareClockMode = KeHardwareClockUtc;
static bool KiIsLeapYear(uint32_t year) {
return ((year % 4u) == 0u && (year % 100u) != 0u) || ((year % 400u) == 0u);
}
static uint32_t KiDaysInMonth(uint32_t year, uint32_t month) {
static const uint8_t days[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2u && KiIsLeapYear(year)) return 29u;
if (month < 1u || month > 12u) return 0u;
return days[month - 1u];
}
static bool KiValidCalendar(uint32_t year, uint32_t month, uint32_t day,
uint32_t hour, uint32_t minute, uint32_t second,
uint32_t nanosecond) {
if (year < 1970u || year > 9999u) return false;
if (month < 1u || month > 12u) return false;
uint32_t dim = KiDaysInMonth(year, month);
if (day < 1u || day > dim) return false;
if (hour > 23u || minute > 59u || second > 59u) return false;
if (nanosecond >= 1000000000u) return false;
return true;
}
static uint64_t KiDaysBeforeYear(uint32_t year) {
uint64_t days = 0;
for (uint32_t y = 1970u; y < year; y++)
days += KiIsLeapYear(y) ? 366u : 365u;
return days;
}
static uint64_t KiCalendarToEpochSeconds(uint32_t year, uint32_t month, uint32_t day,
uint32_t hour, uint32_t minute, uint32_t second) {
uint64_t days = KiDaysBeforeYear(year);
for (uint32_t m = 1u; m < month; m++)
days += KiDaysInMonth(year, m);
days += (uint64_t)(day - 1u);
return (((days * 24u) + hour) * 60u + minute) * 60u + second;
}
static void KiEpochSecondsToCalendar(uint64_t epochSeconds, SYS_SYSTEM_TIME* out) {
uint64_t days = epochSeconds / 86400u;
uint32_t sod = (uint32_t)(epochSeconds % 86400u);
uint32_t year = 1970u;
for (;;) {
uint32_t diy = KiIsLeapYear(year) ? 366u : 365u;
if (days < diy) break;
days -= diy;
year++;
}
uint32_t month = 1u;
for (;;) {
uint32_t dim = KiDaysInMonth(year, month);
if (days < dim) break;
days -= dim;
month++;
}
uint64_t totalDays = epochSeconds / 86400u;
out->Year = (uint16_t)year;
out->Month = (uint8_t)month;
out->Day = (uint8_t)(days + 1u);
out->Hour = (uint8_t)(sod / 3600u);
out->Minute = (uint8_t)((sod / 60u) % 60u);
out->Second = (uint8_t)(sod % 60u);
out->DayOfWeek = (uint8_t)((totalDays + 4u) % 7u); // 1970-01-01 was Thursday.
}
static uint32_t KiDayOfWeek(uint32_t year, uint32_t month, uint32_t day) {
uint64_t epoch = KiCalendarToEpochSeconds(year, month, day, 0, 0, 0);
return (uint32_t)(((epoch / 86400u) + 4u) % 7u);
}
static uint32_t KiLastSundayOfMonth(uint32_t year, uint32_t month) {
uint32_t last = KiDaysInMonth(year, month);
uint32_t dow = KiDayOfWeek(year, month, last); // Sunday == 0.
return last - dow;
}
static bool KiCentralEuropeDstActiveUtc(uint64_t utcEpochSeconds) {
SYS_SYSTEM_TIME utc;
memset(&utc, 0, sizeof(utc));
KiEpochSecondsToCalendar(utcEpochSeconds, &utc);
uint32_t startDay = KiLastSundayOfMonth(utc.Year, 3u);
uint32_t endDay = KiLastSundayOfMonth(utc.Year, 10u);
uint64_t startUtc = KiCalendarToEpochSeconds(utc.Year, 3u, startDay, 1u, 0u, 0u);
uint64_t endUtc = KiCalendarToEpochSeconds(utc.Year, 10u, endDay, 1u, 0u, 0u);
return utcEpochSeconds >= startUtc && utcEpochSeconds < endUtc;
}
static int16_t KiOffsetForUtcEpoch(uint64_t utcEpochSeconds, bool* daylight) {
int32_t offset = gTimeZonePolicy.StandardOffsetMinutes;
bool dst = false;
if (gTimeZonePolicy.DstRule == KeDstRuleAlways) {
dst = true;
} else if (gTimeZonePolicy.DstRule == KeDstRuleCentralEurope) {
dst = KiCentralEuropeDstActiveUtc(utcEpochSeconds);
}
if (dst) offset += KE_DST_OFFSET_DELTA;
if (daylight) *daylight = dst;
return (int16_t)offset;
}
static uint64_t KiLocalEpochToUtc(uint64_t localEpochSeconds, int16_t offsetMinutes) {
uint64_t offsetSeconds = (uint64_t)((offsetMinutes < 0) ? -offsetMinutes : offsetMinutes) * 60u;
if (offsetMinutes >= 0)
return (localEpochSeconds > offsetSeconds) ? (localEpochSeconds - offsetSeconds) : 0;
return localEpochSeconds + offsetSeconds;
}
static void KiRebaseUtcFromSource(void) {
if (gSourceTimeZoneKnown) {
gBaseUtcEpochSeconds = KiLocalEpochToUtc(gSourceRawEpochSeconds, gSourceTimeZoneMinutes);
} else if (gHardwareClockMode == KeHardwareClockLocal) {
bool daylight = false;
int16_t offset = KiOffsetForUtcEpoch(gSourceRawEpochSeconds, &daylight);
// The first pass treated the local clock as UTC only so we can choose
// the correct rule-year. Run one refinement against the candidate UTC
// instant to handle daylight-saving transitions cleanly.
uint64_t candidateUtc = KiLocalEpochToUtc(gSourceRawEpochSeconds, offset);
offset = KiOffsetForUtcEpoch(candidateUtc, &daylight);
gBaseUtcEpochSeconds = KiLocalEpochToUtc(gSourceRawEpochSeconds, offset);
} else {
gBaseUtcEpochSeconds = gSourceRawEpochSeconds;
}
gBaseTickMicroseconds = gSourceTickMicroseconds;
}
static int KiAsciiLower(int c) {
if (c >= 'A' && c <= 'Z') return c + ('a' - 'A');
return c;
}
static bool KiStringEqualsNoCase(const char* a, const char* b) {
if (!a || !b) return false;
while (*a && *b) {
if (KiAsciiLower((unsigned char)*a) != KiAsciiLower((unsigned char)*b))
return false;
a++;
b++;
}
return *a == '\0' && *b == '\0';
}
static bool KiParseInt32(const char* s, int32_t* out) {
if (!s || !*s || !out) return false;
int sign = 1;
if (*s == '-') {
sign = -1;
s++;
} else if (*s == '+') {
s++;
}
if (*s < '0' || *s > '9') return false;
int32_t value = 0;
while (*s >= '0' && *s <= '9') {
value = (value * 10) + (*s - '0');
s++;
}
if (*s != '\0') return false;
*out = value * sign;
return true;
}
static bool KiStartsWithNoCase(const char* s, const char* prefix) {
if (!s || !prefix) return false;
while (*prefix) {
if (KiAsciiLower((unsigned char)*s) != KiAsciiLower((unsigned char)*prefix))
return false;
s++;
prefix++;
}
return true;
}
static bool KiParseUtcOffset(const char* s, int16_t* out) {
if (!s || !out) return false;
if (KiStringEqualsNoCase(s, "utc") || KiStringEqualsNoCase(s, "gmt") ||
KiStringEqualsNoCase(s, "z") || KiStringEqualsNoCase(s, "utcz")) {
*out = 0;
return true;
}
if (KiStartsWithNoCase(s, "utc")) {
s += 3;
} else if (KiStartsWithNoCase(s, "gmt")) {
s += 3;
}
int sign = 1;
if (*s == '+') {
sign = 1;
s++;
} else if (*s == '-') {
sign = -1;
s++;
} else {
return false;
}
if (*s < '0' || *s > '9') return false;
int hours = 0;
int digits = 0;
while (*s >= '0' && *s <= '9' && digits < 2) {
hours = hours * 10 + (*s - '0');
s++;
digits++;
}
int minutes = 0;
if (*s == ':') {
s++;
if (s[0] < '0' || s[0] > '9' || s[1] < '0' || s[1] > '9') return false;
minutes = (s[0] - '0') * 10 + (s[1] - '0');
s += 2;
} else if (*s >= '0' && *s <= '9') {
// Compact form, e.g. UTC+0200. Exactly two minute digits must remain.
if (s[1] < '0' || s[1] > '9' || s[2] != '\0') return false;
minutes = (s[0] - '0') * 10 + (s[1] - '0');
s += 2;
}
if (*s != '\0') return false;
if (hours > 14 || minutes > 59) return false;
*out = (int16_t)(sign * (hours * 60 + minutes));
return true;
}
static void KiApplyDefaultTimeZonePolicy(void) {
gTimeZonePolicy.StandardOffsetMinutes = KE_CE_STANDARD_OFFSET;
gTimeZonePolicy.DstRule = KeDstRuleCentralEurope;
gTimeZonePolicy.TimeZoneKnown = true;
gHardwareClockMode = KeHardwareClockUtc;
}
void KeTimeInitialize(const BOOT_INFO* bootInfo, const HAL_API* hal) {
gTimeHal = hal;
KiApplyDefaultTimeZonePolicy();
gSourceTickMicroseconds = (hal && hal->Timer.QueryTicks) ? hal->Timer.QueryTicks() : 0;
const BOOT_TIME* bt = bootInfo ? &bootInfo->BootTime : 0;
if (bt && (bt->Flags & BOOT_TIME_FLAG_VALID) &&
KiValidCalendar(bt->Year, bt->Month, bt->Day, bt->Hour, bt->Minute,
bt->Second, bt->Nanosecond)) {
gSourceRawEpochSeconds = KiCalendarToEpochSeconds(bt->Year, bt->Month, bt->Day,
bt->Hour, bt->Minute, bt->Second);
gSourceTimeZoneKnown = (bt->Flags & BOOT_TIME_FLAG_TZ_KNOWN) &&
bt->TimeZoneMinutes != BOOT_TIME_UNSPECIFIED_TZ;
gSourceTimeZoneMinutes = gSourceTimeZoneKnown ? bt->TimeZoneMinutes : 0;
gSourceNanosecond = bt->Nanosecond;
gSourceFlags = SYS_TIME_FLAG_VALID | SYS_TIME_FLAG_UEFI;
} else {
HAL_RTC_TIME rtc;
memset(&rtc, 0, sizeof(rtc));
if (hal && hal->Rtc.QueryTime && hal->Rtc.QueryTime(&rtc) == STATUS_SUCCESS &&
(rtc.Flags & HAL_RTC_TIME_FLAG_VALID) &&
KiValidCalendar(rtc.Year, rtc.Month, rtc.Day, rtc.Hour, rtc.Minute,
rtc.Second, rtc.Nanosecond)) {
gSourceRawEpochSeconds = KiCalendarToEpochSeconds(rtc.Year, rtc.Month, rtc.Day,
rtc.Hour, rtc.Minute, rtc.Second);
gSourceTimeZoneKnown = false;
gSourceTimeZoneMinutes = 0;
gSourceNanosecond = rtc.Nanosecond;
gSourceFlags = SYS_TIME_FLAG_VALID | SYS_TIME_FLAG_RTC;
} else {
gSourceRawEpochSeconds = KiCalendarToEpochSeconds(KE_TIME_FALLBACK_YEAR,
KE_TIME_FALLBACK_MONTH,
KE_TIME_FALLBACK_DAY,
0, 0, 0);
gSourceTimeZoneKnown = false;
gSourceTimeZoneMinutes = 0;
gSourceNanosecond = 0;
gSourceFlags = SYS_TIME_FLAG_VALID | SYS_TIME_FLAG_FALLBACK;
}
}
KiRebaseUtcFromSource();
SYS_SYSTEM_TIME now;
if (KeQuerySystemTime(&now) == STATUS_SUCCESS) {
DbgPrint("time: initialized %04u-%02u-%02u %02u:%02u:%02u.%03u flags=0x%x tz=%d hw=%s epoch=%llu\n",
now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second,
now.Millisecond, now.Flags, now.TimeZoneMinutes,
gHardwareClockMode == KeHardwareClockLocal ? "local" : "utc",
now.EpochSeconds);
}
}
KSTATUS KeTimeReloadSettings(void) {
KE_TIME_ZONE_POLICY policy = gTimeZonePolicy;
char value[64];
bool fixedTimezone = false;
bool allowStandardOffsetSetting = false;
KSTATUS st = KeGetSettingString("time", "timezone", value, sizeof(value));
if (KSUCCESS(st)) {
if (KiStringEqualsNoCase(value, "central-europe") ||
KiStringEqualsNoCase(value, "europe/warsaw")) {
policy.StandardOffsetMinutes = KE_CE_STANDARD_OFFSET;
policy.DstRule = KeDstRuleCentralEurope;
policy.TimeZoneKnown = true;
} else if (KiStringEqualsNoCase(value, "cet")) {
policy.StandardOffsetMinutes = KE_CE_STANDARD_OFFSET;
policy.DstRule = KeDstRuleNone;
policy.TimeZoneKnown = true;
fixedTimezone = true;
} else if (KiStringEqualsNoCase(value, "cest")) {
policy.StandardOffsetMinutes = KE_CE_STANDARD_OFFSET + KE_DST_OFFSET_DELTA;
policy.DstRule = KeDstRuleNone;
policy.TimeZoneKnown = true;
fixedTimezone = true;
} else if (KiStringEqualsNoCase(value, "custom") ||
KiStringEqualsNoCase(value, "manual")) {
policy.DstRule = KeDstRuleNone;
policy.TimeZoneKnown = true;
fixedTimezone = true;
allowStandardOffsetSetting = true;
} else if (KiStringEqualsNoCase(value, "utc") ||
KiStringEqualsNoCase(value, "gmt") ||
KiStringEqualsNoCase(value, "z")) {
policy.StandardOffsetMinutes = 0;
policy.DstRule = KeDstRuleNone;
policy.TimeZoneKnown = true;
fixedTimezone = true;
} else {
int16_t parsedOffset = 0;
if (KiParseUtcOffset(value, &parsedOffset)) {
policy.StandardOffsetMinutes = parsedOffset;
policy.DstRule = KeDstRuleNone;
policy.TimeZoneKnown = true;
fixedTimezone = true;
}
}
}
st = KeGetSettingString("time", "standard_offset_minutes", value, sizeof(value));
if (KSUCCESS(st) && allowStandardOffsetSetting) {
int32_t minutes;
if (KiParseInt32(value, &minutes) && minutes >= -14 * 60 && minutes <= 14 * 60) {
policy.StandardOffsetMinutes = (int16_t)minutes;
policy.TimeZoneKnown = true;
fixedTimezone = true;
}
}
st = KeGetSettingString("time", "daylight_saving", value, sizeof(value));
if (KSUCCESS(st)) {
if (KiStringEqualsNoCase(value, "auto")) {
if (!fixedTimezone)
policy.DstRule = KeDstRuleCentralEurope;
} else if (KiStringEqualsNoCase(value, "central-europe")) {
policy.DstRule = KeDstRuleCentralEurope;
} else if (KiStringEqualsNoCase(value, "on") || KiStringEqualsNoCase(value, "yes") ||
KiStringEqualsNoCase(value, "true") || KiStringEqualsNoCase(value, "1")) {
policy.DstRule = KeDstRuleAlways;
} else if (KiStringEqualsNoCase(value, "off") || KiStringEqualsNoCase(value, "no") ||
KiStringEqualsNoCase(value, "false") || KiStringEqualsNoCase(value, "0")) {
policy.DstRule = KeDstRuleNone;
}
}
KE_HARDWARE_CLOCK_MODE hardwareClockMode = gHardwareClockMode;
st = KeGetSettingString("time", "hardware_clock", value, sizeof(value));
if (KSUCCESS(st)) {
if (KiStringEqualsNoCase(value, "utc") || KiStringEqualsNoCase(value, "z")) {
hardwareClockMode = KeHardwareClockUtc;
} else if (KiStringEqualsNoCase(value, "local") ||
KiStringEqualsNoCase(value, "localtime") ||
KiStringEqualsNoCase(value, "wall-clock")) {
hardwareClockMode = KeHardwareClockLocal;
}
}
gTimeZonePolicy = policy;
gHardwareClockMode = hardwareClockMode;
KiRebaseUtcFromSource();
SYS_SYSTEM_TIME now;
if (KeQuerySystemTime(&now) == STATUS_SUCCESS) {
DbgPrint("time: settings timezone=%d dst=%s hw=%s now=%04u-%02u-%02u %02u:%02u:%02u flags=0x%x epoch=%llu\n",
now.TimeZoneMinutes,
(now.Flags & SYS_TIME_FLAG_DAYLIGHT) ? "on" : "off",
gHardwareClockMode == KeHardwareClockLocal ? "local" : "utc",
now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second,
now.Flags, now.EpochSeconds);
}
return STATUS_SUCCESS;
}
__declspec(dllexport) KSTATUS KeQuerySystemTime(SYS_SYSTEM_TIME* outTime) {
if (!outTime) return STATUS_INVALID_PARAMETER;
uint64_t nowTick = (gTimeHal && gTimeHal->Timer.QueryTicks)
? gTimeHal->Timer.QueryTicks()
: gBaseTickMicroseconds;
uint64_t elapsedUs = nowTick - gBaseTickMicroseconds;
uint64_t utcEpochSeconds = gBaseUtcEpochSeconds + (elapsedUs / 1000000u);
uint64_t ns = (uint64_t)gSourceNanosecond + (elapsedUs % 1000000u) * 1000u;
if (ns >= 1000000000u) {
utcEpochSeconds += ns / 1000000000u;
ns %= 1000000000u;
}
bool daylight = false;
int16_t offset = KiOffsetForUtcEpoch(utcEpochSeconds, &daylight);
uint64_t offsetSeconds = (uint64_t)((offset < 0) ? -offset : offset) * 60u;
uint64_t localEpochSeconds;
if (offset >= 0) {
localEpochSeconds = utcEpochSeconds + offsetSeconds;
} else {
localEpochSeconds = (utcEpochSeconds > offsetSeconds) ? (utcEpochSeconds - offsetSeconds) : 0;
}
SYS_SYSTEM_TIME t;
memset(&t, 0, sizeof(t));
KiEpochSecondsToCalendar(localEpochSeconds, &t);
t.Nanosecond = (uint32_t)ns;
t.Millisecond = (uint16_t)(ns / 1000000u);
t.Flags = gSourceFlags;
if (gTimeZonePolicy.TimeZoneKnown) t.Flags |= SYS_TIME_FLAG_TZ_KNOWN;
if (daylight) t.Flags |= SYS_TIME_FLAG_DAYLIGHT;
t.TimeZoneMinutes = gTimeZonePolicy.TimeZoneKnown ? offset : KE_TZ_UNSPECIFIED;
t.EpochSeconds = utcEpochSeconds;
t.TickMicroseconds = nowTick;
*outTime = t;
return STATUS_SUCCESS;
}
__declspec(dllexport) uint64_t KeQueryUnixTimeSeconds(void) {
SYS_SYSTEM_TIME t;
if (KeQuerySystemTime(&t) != STATUS_SUCCESS)
return 0;
return t.EpochSeconds;
}