From e28baa77644bf15a799ae21396da32d550100d37 Mon Sep 17 00:00:00 2001 From: Aleksandr Cupacenko Date: Fri, 28 Aug 2026 09:40:39 +0300 Subject: [PATCH] Use libc for UTC timestamp formatting --- cli/zget.c | 58 ++++++++++++-------------------------------- tests/integration.py | 13 +++++++--- 2 files changed, 26 insertions(+), 45 deletions(-) diff --git a/cli/zget.c b/cli/zget.c index 20ca65d..60f40fc 100644 --- a/cli/zget.c +++ b/cli/zget.c @@ -6,6 +6,7 @@ #include #include #include +#include struct file_output { FILE *file; @@ -21,11 +22,6 @@ struct list_output { int header_written; }; -struct utc_time { - int64_t year; - unsigned int month, day, hour, minute; -}; - static int write_stream(struct file_output *out, const void *data, size_t size) { errno = 0; @@ -94,45 +90,25 @@ static int write_list_header(struct list_output *out) return write_stream(&out->stream, header, sizeof(header) - 1); } -/* Convert days since 1970-01-01 using the proleptic Gregorian calendar. */ -static void civil_from_days(int64_t days, struct utc_time *time) -{ - int64_t z, era, year_of_era, day_of_year, month_prime; - - z = days + 719468; - era = (z >= 0 ? z : z - 146096) / 146097; - day_of_year = z - era * 146097; - year_of_era = (day_of_year - day_of_year / 1460 + - day_of_year / 36524 - day_of_year / 146096) / 365; - time->year = year_of_era + era * 400; - day_of_year -= 365 * year_of_era + year_of_era / 4 - year_of_era / 100; - month_prime = (5 * day_of_year + 2) / 153; - time->day = (unsigned int)(day_of_year - - (153 * month_prime + 2) / 5 + 1); - time->month = (unsigned int)(month_prime + - (month_prime < 10 ? 3 : -9)); - time->year += time->month <= 2; -} - -static int utc_time_from_epoch(int64_t seconds, struct utc_time *time) +static int format_utc_mtime(int64_t mtime, char *buffer, size_t size) { - int64_t days = seconds / 86400; - int64_t remainder = seconds % 86400; + time_t timestamp = (time_t)mtime; + struct tm time; - if (remainder < 0) { - --days; - remainder += 86400; - } - civil_from_days(days, time); - time->hour = (unsigned int)(remainder / 3600); - time->minute = (unsigned int)((remainder % 3600) / 60); - return time->year >= 0 && time->year <= 9999; +#ifdef _WIN32 + if (gmtime_s(&time, ×tamp) != 0) + return 0; +#else + if (gmtime_r(×tamp, &time) == NULL) + return 0; +#endif + return strftime(buffer, size, "%m-%d-%Y %H:%M", &time) != 0; } static int list_member(void *opaque, const zget_member_info *member) { struct list_output *out = opaque; - struct utc_time time; + char datetime[32]; char prefix[64]; int length; @@ -147,12 +123,10 @@ static int list_member(void *opaque, const zget_member_info *member) if (write_list_header(out)) return 1; - if (utc_time_from_epoch(member->mtime, &time)) + if (format_utc_mtime(member->mtime, datetime, sizeof(datetime))) length = snprintf(prefix, sizeof(prefix), - "%9" PRIu64 " %02u-%02u-%04" PRId64 " %02u:%02u ", - member->uncompressed_size, - time.month, time.day, time.year, - time.hour, time.minute); + "%9" PRIu64 " %s ", + member->uncompressed_size, datetime); else length = snprintf(prefix, sizeof(prefix), "%9" PRIu64 " ---------- ----- ", diff --git a/tests/integration.py b/tests/integration.py index 900105c..4b7c971 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -560,11 +560,16 @@ def legacy_listing(base): assert extracted.stdout == b"hello" run_server(bytes(legacy_name), "normal", legacy_listing) + # A non-UTC process timezone ensures localtime() could not accidentally + # satisfy the UTC listing assertions below. + timestamp_environment = dict(os.environ, TZ="UTC-2") + def semantic_metadata(base): """Prefer valid Unicode Path and NTFS mtime over all fallbacks.""" url = base + "/archive.zip" listed = subprocess.run([binary, "-l", url], check=True, - stdout=subprocess.PIPE).stdout + stdout=subprocess.PIPE, + env=timestamp_environment).stdout assert b"01-01-2030 00:00 preferred.txt" in listed extracted = subprocess.run([binary, url, "preferred.txt"], check=True, stdout=subprocess.PIPE).stdout @@ -574,7 +579,8 @@ def semantic_metadata(base): def extended_timestamp(base): """Use Extended Timestamp when NTFS mtime is absent.""" listed = subprocess.run([binary, "-l", base + "/archive.zip"], - check=True, stdout=subprocess.PIPE).stdout + check=True, stdout=subprocess.PIPE, + env=timestamp_environment).stdout assert b"09-09-2001 01:46 preferred.txt" in listed run_server(semantic_archive(include_ntfs=False), "normal", extended_timestamp) @@ -582,7 +588,8 @@ def extended_timestamp(base): def negative_timestamp(base): """Preserve signed Extended Timestamp values before the Unix epoch.""" listed = subprocess.run([binary, "-l", base + "/archive.zip"], - check=True, stdout=subprocess.PIPE).stdout + check=True, stdout=subprocess.PIPE, + env=timestamp_environment).stdout assert b"12-31-1969 23:59 preferred.txt" in listed run_server(semantic_archive(include_ntfs=False, extended_value=-1), "normal", negative_timestamp)