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);