Handle null TM_ZONE in z case in strftime.
For correct %z output tzcode requires tm struct to be modified by
mktime call or be output of localtime. But as TM_ZONE is null, we
are comparing against +0000.
See https://mm.icann.org/pipermail/tz/2022-July/031674.html
Europe/Lisbon test is added to confirm that current implementation
deviates from libc specification and uses more than just tm_isdst
to find out a time zone's offset.
Bug: 239128167
Test: adb shell /data/nativetest64/bionic-unit-tests-static/bionic-unit-tests-static
Test: adb shell /data/nativetest/bionic-unit-tests-static/bionic-unit-tests-static
Change-Id: Ic27775c840467c4e9ef55bc730a313709372314b
diff --git a/libc/tzcode/strftime.c b/libc/tzcode/strftime.c
index 8c1b983..d04c5ba 100644
--- a/libc/tzcode/strftime.c
+++ b/libc/tzcode/strftime.c
@@ -190,6 +190,29 @@
return normal;
}
+// Android-added: fall back mechanism when TM_ZONE is not initialized.
+#ifdef TM_ZONE
+static const char* _safe_tm_zone(const struct tm* tm) {
+ const char* zone = tm->TM_ZONE;
+ if (!zone || !*zone) {
+ // "The value of tm_isdst shall be positive if Daylight Savings Time is
+ // in effect, 0 if Daylight Savings Time is not in effect, and negative
+ // if the information is not available."
+ if (tm->tm_isdst == 0) {
+ zone = tzname[0];
+ } else if (tm->tm_isdst > 0) {
+ zone = tzname[1];
+ }
+
+ // "Replaced by the timezone name or abbreviation, or by no bytes if no
+ // timezone information exists."
+ if (!zone || !*zone) zone = "";
+ }
+
+ return zone;
+}
+#endif
+
static char *
_fmt(const char *format, const struct tm *t, char *pt,
const char *ptlim, enum warn *warnp)
@@ -524,21 +547,7 @@
case 'Z':
#ifdef TM_ZONE
// BEGIN: Android-changed.
- {
- const char* zone = t->TM_ZONE;
- if (!zone || !*zone) {
- // "The value of tm_isdst shall be positive if Daylight Savings Time is
- // in effect, 0 if Daylight Savings Time is not in effect, and negative
- // if the information is not available."
- if (t->tm_isdst == 0) zone = tzname[0];
- else if (t->tm_isdst > 0) zone = tzname[1];
-
- // "Replaced by the timezone name or abbreviation, or by no bytes if no
- // timezone information exists."
- if (!zone || !*zone) zone = "";
- }
- pt = _add(zone, pt, ptlim, modifier);
- }
+ pt = _add(_safe_tm_zone(t), pt, ptlim, modifier);
// END: Android-changed.
#elif HAVE_TZNAME
if (t->tm_isdst >= 0)
@@ -599,7 +608,11 @@
negative = diff < 0;
if (diff == 0) {
#ifdef TM_ZONE
- negative = t->TM_ZONE[0] == '-';
+ // Android-changed: do not use TM_ZONE as it is as it may be null.
+ {
+ const char* zone = _safe_tm_zone(t);
+ negative = zone[0] == '-';
+ }
#else
negative = t->tm_isdst < 0;
# if HAVE_TZNAME
diff --git a/tests/time_test.cpp b/tests/time_test.cpp
index 898496d..40e5c6d 100644
--- a/tests/time_test.cpp
+++ b/tests/time_test.cpp
@@ -266,7 +266,7 @@
EXPECT_STREQ("-1", buf);
}
-TEST(time, strftime_null_tm_zone) {
+TEST(time, strftime_Z_null_tm_zone) {
// Netflix on Nexus Player wouldn't start (http://b/25170306).
struct tm t;
memset(&t, 0, sizeof(tm));
@@ -304,6 +304,86 @@
#endif
}
+// According to C language specification the only tm struct field needed to
+// find out replacement for %z and %Z in strftime is tm_isdst. Which is
+// wrong, as time zones change their standard offset and even DST savings.
+// tzcode deviates from C language specification and requires tm struct either
+// to be output of localtime-like functions or to be modified by mktime call
+// before passing to strftime. See tz mailing discussion for more details
+// https://mm.icann.org/pipermail/tz/2022-July/031674.html
+// But we are testing case when tm.tm_zone is null, which means that tm struct
+// is not coming from localtime and is neither modified by mktime. That's why
+// we are comparing against +0000, even though America/Los_Angeles never
+// observes it.
+TEST(time, strftime_z_null_tm_zone) {
+ char str[64];
+ struct tm tm = {.tm_year = 109, .tm_mon = 4, .tm_mday = 2, .tm_isdst = 0};
+
+ setenv("TZ", "America/Los_Angeles", 1);
+ tzset();
+
+ tm.tm_zone = NULL;
+
+ size_t result = strftime(str, sizeof(str), "%z", &tm);
+
+ EXPECT_EQ(5U, result);
+ EXPECT_STREQ("+0000", str);
+
+ tm.tm_isdst = 1;
+
+ result = strftime(str, sizeof(str), "%z", &tm);
+
+ EXPECT_EQ(5U, result);
+ EXPECT_STREQ("+0000", str);
+
+ setenv("TZ", "UTC", 1);
+ tzset();
+
+ tm.tm_isdst = 0;
+
+ result = strftime(str, sizeof(str), "%z", &tm);
+
+ EXPECT_EQ(5U, result);
+ EXPECT_STREQ("+0000", str);
+
+ tm.tm_isdst = 1;
+
+ result = strftime(str, sizeof(str), "%z", &tm);
+
+ EXPECT_EQ(5U, result);
+ EXPECT_STREQ("+0000", str);
+}
+
+TEST(time, strftime_z_Europe_Lisbon) {
+ char str[64];
+ // During 1992-1996 Europe/Lisbon standard offset was 1 hour.
+ // tm_isdst is not set as it will be overridden by mktime call anyway.
+ struct tm tm = {.tm_year = 1996 - 1900, .tm_mon = 2, .tm_mday = 13};
+
+ setenv("TZ", "Europe/Lisbon", 1);
+ tzset();
+
+ // tzcode's strftime implementation for %z relies on prior mktime call.
+ // At the moment of writing %z value is taken from tm_gmtoff. So without
+ // mktime call %z is replaced with +0000.
+ // See https://mm.icann.org/pipermail/tz/2022-July/031674.html
+ mktime(&tm);
+
+ size_t result = strftime(str, sizeof(str), "%z", &tm);
+
+ EXPECT_EQ(5U, result);
+ EXPECT_STREQ("+0100", str);
+
+ // Now standard offset is 0.
+ tm = {.tm_year = 2022 - 1900, .tm_mon = 2, .tm_mday = 13};
+
+ mktime(&tm);
+ result = strftime(str, sizeof(str), "%z", &tm);
+
+ EXPECT_EQ(5U, result);
+ EXPECT_STREQ("+0000", str);
+}
+
TEST(time, strftime_l) {
locale_t cloc = newlocale(LC_ALL, "C.UTF-8", nullptr);
locale_t old_locale = uselocale(cloc);