Imported Upstream version 2.72.3
[platform/upstream/glib.git] / glib / gtimezone.c
index 8e0621e..a37dbe2 100644 (file)
@@ -157,7 +157,7 @@ typedef struct
  */
 typedef struct
 {
-  gint         start_year;
+  guint        start_year;
   gint32       std_offset;
   gint32       dlt_offset;
   TimeZoneDate dlt_start;
@@ -195,6 +195,8 @@ struct _GTimeZone
 
 G_LOCK_DEFINE_STATIC (time_zones);
 static GHashTable/*<string?, GTimeZone>*/ *time_zones;
+G_LOCK_DEFINE_STATIC (tz_default);
+static GTimeZone *tz_default = NULL;
 G_LOCK_DEFINE_STATIC (tz_local);
 static GTimeZone *tz_local = NULL;
 
@@ -238,7 +240,8 @@ again:
               goto again;
             }
 
-          g_hash_table_remove (time_zones, tz->name);
+          if (time_zones != NULL)
+            g_hash_table_remove (time_zones, tz->name);
           G_UNLOCK(time_zones);
         }
 
@@ -437,19 +440,178 @@ zone_for_constant_offset (GTimeZone *gtz, const gchar *name)
 }
 
 #ifdef G_OS_UNIX
+
+#if defined(__sun) && defined(__SVR4)
+/*
+ * only used by Illumos distros or Solaris < 11: parse the /etc/default/init
+ * text file looking for TZ= followed by the timezone, possibly quoted
+ *
+ */
+static gchar *
+zone_identifier_illumos (void)
+{
+  gchar *resolved_identifier = NULL;
+  gchar *contents = NULL;
+  const gchar *line_start = NULL;
+  gsize tz_len = 0;
+
+  if (!g_file_get_contents ("/etc/default/init", &contents, NULL, NULL) )
+    return NULL;
+
+  /* is TZ= the first/only line in the file? */
+  if (strncmp (contents, "TZ=", 3) == 0)
+    {
+      /* found TZ= on the first line, skip over the TZ= */
+      line_start = contents + 3;
+    }
+  else 
+    {
+      /* find a newline followed by TZ= */
+      line_start = strstr (contents, "\nTZ=");
+      if (line_start != NULL)
+        line_start = line_start + 4; /* skip past the \nTZ= */
+    }
+
+  /* 
+   * line_start is NULL if we didn't find TZ= at the start of any line,
+   * otherwise it points to what is after the '=' (possibly '\0')
+   */
+  if (line_start == NULL || *line_start == '\0')
+    return NULL;
+
+  /* skip past a possible opening " or ' */
+  if (*line_start == '"' || *line_start == '\'')
+    line_start++;
+
+  /*
+   * loop over the next few characters, building up the length of
+   * the timezone identifier, ending with end of string, newline or
+   * a " or ' character
+   */
+  while (*(line_start + tz_len) != '\0' &&
+         *(line_start + tz_len) != '\n' &&
+         *(line_start + tz_len) != '"'  &&
+         *(line_start + tz_len) != '\'')
+    tz_len++; 
+
+  if (tz_len > 0)
+    {
+      /* found it */
+      resolved_identifier = g_strndup (line_start, tz_len);
+      g_strchomp (resolved_identifier);
+      g_free (contents);
+      return g_steal_pointer (&resolved_identifier);
+    }
+  else
+    return NULL;
+}
+#endif /* defined(__sun) && defined(__SRVR) */
+
+/*
+ * returns the path to the top of the Olson zoneinfo timezone hierarchy.
+ */
+static const gchar *
+zone_info_base_dir (void)
+{
+  if (g_file_test ("/usr/share/zoneinfo", G_FILE_TEST_IS_DIR))
+    return "/usr/share/zoneinfo";     /* Most distros */
+  else if (g_file_test ("/usr/share/lib/zoneinfo", G_FILE_TEST_IS_DIR))
+    return "/usr/share/lib/zoneinfo"; /* Illumos distros */
+
+  /* need a better fallback case */
+  return "/usr/share/zoneinfo";
+}
+
+static gchar *
+zone_identifier_unix (void)
+{
+  gchar *resolved_identifier = NULL;
+  gsize prefix_len = 0;
+  gchar *canonical_path = NULL;
+  GError *read_link_err = NULL;
+  const gchar *tzdir;
+
+  /* Resolve the actual timezone pointed to by /etc/localtime. */
+  resolved_identifier = g_file_read_link ("/etc/localtime", &read_link_err);
+  if (resolved_identifier == NULL)
+    {
+      gboolean not_a_symlink = g_error_matches (read_link_err,
+                                                G_FILE_ERROR,
+                                                G_FILE_ERROR_INVAL);
+      g_clear_error (&read_link_err);
+
+      /* if /etc/localtime is not a symlink, try:
+       *  - /var/db/zoneinfo : 'tzsetup' program on FreeBSD and
+       *    DragonflyBSD stores the timezone chosen by the user there.
+       *  - /etc/timezone : Gentoo, OpenRC, and others store
+       *    the user choice there.
+       *  - call zone_identifier_illumos iff __sun and __SVR4 are defined,
+       *    as a last-ditch effort to parse the TZ= setting from within
+       *    /etc/default/init
+       */
+      if (not_a_symlink && (g_file_get_contents ("/var/db/zoneinfo",
+                                                 &resolved_identifier,
+                                                 NULL, NULL) ||
+                            g_file_get_contents ("/etc/timezone",
+                                                 &resolved_identifier,
+                                                 NULL, NULL)
+#if defined(__sun) && defined(__SVR4)
+                                                             ||
+                            (resolved_identifier = zone_identifier_illumos ())
+#endif
+                                                             ))
+        g_strchomp (resolved_identifier);
+      else
+        {
+          /* Error */
+          g_assert (resolved_identifier == NULL);
+          goto out;
+        }
+    }
+  else
+    {
+      /* Resolve relative path */
+      canonical_path = g_canonicalize_filename (resolved_identifier, "/etc");
+      g_free (resolved_identifier);
+      resolved_identifier = g_steal_pointer (&canonical_path);
+    }
+
+  tzdir = g_getenv ("TZDIR");
+  if (tzdir == NULL)
+    tzdir = zone_info_base_dir ();
+
+  /* Strip the prefix and slashes if possible. */
+  if (g_str_has_prefix (resolved_identifier, tzdir))
+    {
+      prefix_len = strlen (tzdir);
+      while (*(resolved_identifier + prefix_len) == '/')
+        prefix_len++;
+    }
+
+  if (prefix_len > 0)
+    memmove (resolved_identifier, resolved_identifier + prefix_len,
+             strlen (resolved_identifier) - prefix_len + 1  /* nul terminator */);
+
+  g_assert (resolved_identifier != NULL);
+
+out:
+  g_free (canonical_path);
+
+  return resolved_identifier;
+}
+
 static GBytes*
-zone_info_unix (const gchar  *identifier,
-                gchar       **out_identifier)
+zone_info_unix (const gchar *identifier,
+                const gchar *resolved_identifier)
 {
-  gchar *filename;
+  gchar *filename = NULL;
   GMappedFile *file = NULL;
   GBytes *zoneinfo = NULL;
-  gchar *resolved_identifier = NULL;
   const gchar *tzdir;
 
   tzdir = g_getenv ("TZDIR");
   if (tzdir == NULL)
-    tzdir = "/usr/share/zoneinfo";
+    tzdir = zone_info_base_dir ();
 
   /* identifier can be a relative or absolute path name;
      if relative, it is interpreted starting from /usr/share/zoneinfo
@@ -457,8 +619,6 @@ zone_info_unix (const gchar  *identifier,
      glibc allows both syntaxes, so we should too */
   if (identifier != NULL)
     {
-      resolved_identifier = g_strdup (identifier);
-
       if (*identifier == ':')
         identifier ++;
 
@@ -469,61 +629,10 @@ zone_info_unix (const gchar  *identifier,
     }
   else
     {
-      gsize prefix_len = 0;
-      gchar *canonical_path = NULL;
-      GError *read_link_err = NULL;
-
-      filename = g_strdup ("/etc/localtime");
-
-      /* Resolve the actual timezone pointed to by /etc/localtime. */
-      resolved_identifier = g_file_read_link (filename, &read_link_err);
       if (resolved_identifier == NULL)
-        {
-          gboolean not_a_symlink = g_error_matches (read_link_err,
-                                                    G_FILE_ERROR,
-                                                    G_FILE_ERROR_INVAL);
-          g_clear_error (&read_link_err);
-
-          /* Fallback to the content of /var/db/zoneinfo or /etc/timezone
-           * if /etc/localtime is not a symlink. /var/db/zoneinfo is
-           * where 'tzsetup' program on FreeBSD and DragonflyBSD stores
-           * the timezone chosen by the user. /etc/timezone is where user
-           * choice is expressed on Gentoo OpenRC and others. */
-          if (not_a_symlink && (g_file_get_contents ("/var/db/zoneinfo",
-                                                     &resolved_identifier,
-                                                     NULL, NULL) ||
-                                g_file_get_contents ("/etc/timezone",
-                                                     &resolved_identifier,
-                                                     NULL, NULL)))
-            g_strchomp (resolved_identifier);
-          else
-            {
-              /* Error */
-              g_assert (resolved_identifier == NULL);
-              goto out;
-            }
-        }
-      else
-        {
-          /* Resolve relative path */
-          canonical_path = g_canonicalize_filename (resolved_identifier, "/etc");
-          g_free (resolved_identifier);
-          resolved_identifier = g_steal_pointer (&canonical_path);
-        }
-
-      /* Strip the prefix and slashes if possible. */
-      if (g_str_has_prefix (resolved_identifier, tzdir))
-        {
-          prefix_len = strlen (tzdir);
-          while (*(resolved_identifier + prefix_len) == '/')
-            prefix_len++;
-        }
+        goto out;
 
-      if (prefix_len > 0)
-        memmove (resolved_identifier, resolved_identifier + prefix_len,
-                 strlen (resolved_identifier) - prefix_len + 1  /* nul terminator */);
-
-      g_free (canonical_path);
+      filename = g_strdup ("/etc/localtime");
     }
 
   file = g_mapped_file_new (filename, FALSE, NULL);
@@ -539,10 +648,6 @@ zone_info_unix (const gchar  *identifier,
   g_assert (resolved_identifier != NULL);
 
 out:
-  if (out_identifier != NULL)
-    *out_identifier = g_steal_pointer (&resolved_identifier);
-
-  g_free (resolved_identifier);
   g_free (filename);
 
   return zoneinfo;
@@ -814,20 +919,19 @@ register_tzi_to_tzi (RegTZI *reg, TIME_ZONE_INFORMATION *tzi)
 
 static guint
 rules_from_windows_time_zone (const gchar   *identifier,
-                              gchar        **out_identifier,
-                              TimeZoneRule **rules,
-                              gboolean       copy_identifier)
+                              const gchar   *resolved_identifier,
+                              TimeZoneRule **rules)
 {
   HKEY key;
   gchar *subkey = NULL;
   gchar *subkey_dynamic = NULL;
-  gchar *key_name = NULL;
+  const gchar *key_name;
   const gchar *reg_key =
     "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\";
   TIME_ZONE_INFORMATION tzi;
   DWORD size;
   guint rules_num = 0;
-  RegTZI regtzi, regtzi_prev;
+  RegTZI regtzi = { 0 }, regtzi_prev;
   WCHAR winsyspath[MAX_PATH];
   gunichar2 *subkey_w, *subkey_dynamic_w;
 
@@ -836,19 +940,15 @@ rules_from_windows_time_zone (const gchar   *identifier,
   if (GetSystemDirectoryW (winsyspath, MAX_PATH) == 0)
     return 0;
 
-  g_assert (copy_identifier == FALSE || out_identifier != NULL);
   g_assert (rules != NULL);
 
-  if (copy_identifier)
-    *out_identifier = NULL;
-
   *rules = NULL;
   key_name = NULL;
 
   if (!identifier)
-    key_name = windows_default_tzname ();
+    key_name = resolved_identifier;
   else
-    key_name = g_strdup (identifier);
+    key_name = identifier;
 
   if (!key_name)
     return 0;
@@ -897,8 +997,7 @@ rules_from_windows_time_zone (const gchar   *identifier,
   if (RegOpenKeyExW (HKEY_LOCAL_MACHINE, subkey_dynamic_w, 0,
                      KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
     {
-      DWORD first, last;
-      int year, i;
+      DWORD i, first, last, year;
       wchar_t s[12];
 
       size = sizeof first;
@@ -991,16 +1090,9 @@ utf16_conv_failed:
       else
         (*rules)[rules_num - 1].start_year = (*rules)[rules_num - 2].start_year + 1;
 
-      if (copy_identifier)
-        *out_identifier = g_steal_pointer (&key_name);
-      else
-        g_free (key_name);
-
       return rules_num;
     }
 
-  g_free (key_name);
-
   return 0;
 }
 
@@ -1035,12 +1127,16 @@ find_relative_date (TimeZoneDate *buffer)
       g_date_set_dmy (&date, 1, buffer->mon, buffer->year);
       first_wday = g_date_get_weekday (&date);
 
-      if (first_wday > wday)
+      if ((guint) first_wday > wday)
         ++(buffer->week);
       /* week is 1 <= w <= 5, we need 0-based */
       days = 7 * (buffer->week - 1) + wday - first_wday;
 
-      while (days > days_in_month)
+      /* "days" is a 0-based offset from the 1st of the month.
+       * Adding days == days_in_month would bring us into the next month,
+       * hence the ">=" instead of just ">".
+       */
+      while (days >= days_in_month)
         days -= 7;
 
       g_date_add_days (&date, days);
@@ -1452,6 +1548,8 @@ set_tz_name (gchar **pos, gchar *buffer, guint size)
   gchar *name_pos = *pos;
   guint len;
 
+  g_assert (size != 0);
+
   if (quoted)
     {
       name_pos++;
@@ -1473,7 +1571,7 @@ set_tz_name (gchar **pos, gchar *buffer, guint size)
 
   memset (buffer, 0, size);
   /* name_pos isn't 0-terminated, so we have to limit the length expressly */
-  len = *pos - name_pos > size - 1 ? size - 1 : *pos - name_pos;
+  len = (guint) (*pos - name_pos) > size - 1 ? size - 1 : (guint) (*pos - name_pos);
   strncpy (buffer, name_pos, len);
   *pos += quoted;
   return TRUE;
@@ -1501,16 +1599,13 @@ parse_identifier_boundaries (gchar **pos, TimeZoneRule *tzr)
  */
 static guint
 rules_from_identifier (const gchar   *identifier,
-                       gchar        **out_identifier,
                        TimeZoneRule **rules)
 {
   gchar *pos;
   TimeZoneRule tzr;
 
-  g_assert (out_identifier != NULL);
   g_assert (rules != NULL);
 
-  *out_identifier = NULL;
   *rules = NULL;
 
   if (!identifier)
@@ -1525,7 +1620,6 @@ rules_from_identifier (const gchar   *identifier,
 
   if (*pos == 0)
     {
-      *out_identifier = g_strdup (identifier);
       return create_ruleset_from_rule (rules, &tzr);
     }
 
@@ -1540,20 +1634,13 @@ rules_from_identifier (const gchar   *identifier,
 #ifdef G_OS_WIN32
     /* Windows allows us to use the US DST boundaries if they're not given */
     {
-      int i;
-      guint rules_num = 0;
+      guint i, rules_num = 0;
 
       /* Use US rules, Windows' default is Pacific Standard Time */
       if ((rules_num = rules_from_windows_time_zone ("Pacific Standard Time",
                                                      NULL,
-                                                     rules,
-                                                     FALSE)))
+                                                     rules)))
         {
-          /* We don't want to hardcode our identifier here as
-           * "Pacific Standard Time", use what was passed in
-           */
-          *out_identifier = g_strdup (identifier);
-
           for (i = 0; i < rules_num - 1; i++)
             {
               (*rules)[i].std_offset = - tzr.std_offset;
@@ -1574,7 +1661,6 @@ rules_from_identifier (const gchar   *identifier,
   if (!parse_identifier_boundaries (&pos, &tzr))
     return 0;
 
-  *out_identifier = g_strdup (identifier);
   return create_ruleset_from_rule (rules, &tzr);
 }
 
@@ -1585,17 +1671,13 @@ parse_footertz (const gchar *footer, size_t footerlen)
   gchar *tzstring = g_strndup (footer + 1, footerlen - 2);
   GTimeZone *footertz = NULL;
 
-  /* FIXME: it might make sense to modify rules_from_identifier to
-     allow NULL to be passed instead of &ident, saving the strdup/free
-     pair.  The allocation for tzstring could also be avoided by
+  /* FIXME: The allocation for tzstring could be avoided by
      passing a gsize identifier_len argument to rules_from_identifier
      and changing the code in that function to stop assuming that
      identifier is nul-terminated.  */
-  gchar *ident;
   TimeZoneRule *rules;
-  guint rules_num = rules_from_identifier (tzstring, &ident, &rules);
+  guint rules_num = rules_from_identifier (tzstring, &rules);
 
-  g_free (ident);
   g_free (tzstring);
   if (rules_num > 1)
     {
@@ -1613,7 +1695,39 @@ parse_footertz (const gchar *footer, size_t footerlen)
  * g_time_zone_new:
  * @identifier: (nullable): a timezone identifier
  *
- * Creates a #GTimeZone corresponding to @identifier.
+ * A version of g_time_zone_new_identifier() which returns the UTC time zone
+ * if @identifier could not be parsed or loaded.
+ *
+ * If you need to check whether @identifier was loaded successfully, use
+ * g_time_zone_new_identifier().
+ *
+ * Returns: (transfer full) (not nullable): the requested timezone
+ * Deprecated: 2.68: Use g_time_zone_new_identifier() instead, as it provides
+ *     error reporting. Change your code to handle a potentially %NULL return
+ *     value.
+ *
+ * Since: 2.26
+ **/
+GTimeZone *
+g_time_zone_new (const gchar *identifier)
+{
+  GTimeZone *tz = g_time_zone_new_identifier (identifier);
+
+  /* Always fall back to UTC. */
+  if (tz == NULL)
+    tz = g_time_zone_new_utc ();
+
+  g_assert (tz != NULL);
+
+  return g_steal_pointer (&tz);
+}
+
+/**
+ * g_time_zone_new_identifier:
+ * @identifier: (nullable): a timezone identifier
+ *
+ * Creates a #GTimeZone corresponding to @identifier. If @identifier cannot be
+ * parsed or loaded, %NULL is returned.
  *
  * @identifier can either be an RFC3339/ISO 8601 time offset or
  * something that would pass as a valid value for the `TZ` environment
@@ -1678,24 +1792,24 @@ parse_footertz (const gchar *footer, size_t footerlen)
  * You should release the return value by calling g_time_zone_unref()
  * when you are done with it.
  *
- * Returns: the requested timezone
- *
- * Since: 2.26
- **/
+ * Returns: (transfer full) (nullable): the requested timezone, or %NULL on
+ *     failure
+ * Since: 2.68
+ */
 GTimeZone *
-g_time_zone_new (const gchar *identifier)
+g_time_zone_new_identifier (const gchar *identifier)
 {
   GTimeZone *tz = NULL;
   TimeZoneRule *rules;
   gint rules_num;
   gchar *resolved_identifier = NULL;
 
-  G_LOCK (time_zones);
-  if (time_zones == NULL)
-    time_zones = g_hash_table_new (g_str_hash, g_str_equal);
-
   if (identifier)
     {
+      G_LOCK (time_zones);
+      if (time_zones == NULL)
+        time_zones = g_hash_table_new (g_str_hash, g_str_equal);
+
       tz = g_hash_table_lookup (time_zones, identifier);
       if (tz)
         {
@@ -1703,6 +1817,36 @@ g_time_zone_new (const gchar *identifier)
           G_UNLOCK (time_zones);
           return tz;
         }
+      else
+        resolved_identifier = g_strdup (identifier);
+    }
+  else
+    {
+      G_LOCK (tz_default);
+#ifdef G_OS_UNIX
+      resolved_identifier = zone_identifier_unix ();
+#elif defined (G_OS_WIN32)
+      resolved_identifier = windows_default_tzname ();
+#endif
+      if (tz_default)
+        {
+          /* Flush default if changed. If the identifier couldn’t be resolved,
+           * we’re going to fall back to UTC eventually, so don’t clear out the
+           * cache if it’s already UTC. */
+          if (!(resolved_identifier == NULL && g_str_equal (tz_default->name, "UTC")) &&
+              g_strcmp0 (tz_default->name, resolved_identifier) != 0)
+            {
+              g_clear_pointer (&tz_default, g_time_zone_unref);
+            }
+          else
+            {
+              tz = g_time_zone_ref (tz_default);
+              G_UNLOCK (tz_default);
+
+              g_free (resolved_identifier);
+              return tz;
+            }
+        }
     }
 
   tz = g_slice_new0 (GTimeZone);
@@ -1711,7 +1855,7 @@ g_time_zone_new (const gchar *identifier)
   zone_for_constant_offset (tz, identifier);
 
   if (tz->t_info == NULL &&
-      (rules_num = rules_from_identifier (identifier, &resolved_identifier, &rules)))
+      (rules_num = rules_from_identifier (identifier, &rules)))
     {
       init_zone_from_rules (tz, rules, rules_num, g_steal_pointer (&resolved_identifier));
       g_free (rules);
@@ -1720,7 +1864,7 @@ g_time_zone_new (const gchar *identifier)
   if (tz->t_info == NULL)
     {
 #ifdef G_OS_UNIX
-      GBytes *zoneinfo = zone_info_unix (identifier, &resolved_identifier);
+      GBytes *zoneinfo = zone_info_unix (identifier, resolved_identifier);
       if (zoneinfo != NULL)
         {
           init_zone_from_iana_info (tz, zoneinfo, g_steal_pointer (&resolved_identifier));
@@ -1728,9 +1872,8 @@ g_time_zone_new (const gchar *identifier)
         }
 #elif defined (G_OS_WIN32)
       if ((rules_num = rules_from_windows_time_zone (identifier,
-                                                     &resolved_identifier,
-                                                     &rules,
-                                                     TRUE)))
+                                                     resolved_identifier,
+                                                     &rules)))
         {
           init_zone_from_rules (tz, rules, rules_num, g_steal_pointer (&resolved_identifier));
           g_free (rules);
@@ -1757,7 +1900,7 @@ g_time_zone_new (const gchar *identifier)
                   rules[0].start_year = MIN_TZYEAR;
                   rules[1].start_year = MAX_TZYEAR;
 
-                  init_zone_from_rules (tz, rules, 2, windows_default_tzname ());
+                  init_zone_from_rules (tz, rules, 2, g_steal_pointer (&resolved_identifier));
                 }
 
               g_free (rules);
@@ -1768,20 +1911,37 @@ g_time_zone_new (const gchar *identifier)
 
   g_free (resolved_identifier);
 
-  /* Always fall back to UTC. */
+  /* Failed to load the timezone. */
   if (tz->t_info == NULL)
-    zone_for_constant_offset (tz, "UTC");
+    {
+      g_slice_free (GTimeZone, tz);
+
+      if (identifier)
+        G_UNLOCK (time_zones);
+      else
+        G_UNLOCK (tz_default);
+
+      return NULL;
+    }
 
   g_assert (tz->name != NULL);
   g_assert (tz->t_info != NULL);
 
-  if (tz->t_info != NULL)
+  if (identifier)
+    g_hash_table_insert (time_zones, tz->name, tz);
+  else if (tz->name)
     {
-      if (identifier)
-        g_hash_table_insert (time_zones, tz->name, tz);
+      /* Caching reference */
+      g_atomic_int_inc (&tz->ref_count);
+      tz_default = tz;
     }
+
   g_atomic_int_inc (&tz->ref_count);
-  G_UNLOCK (time_zones);
+
+  if (identifier)
+    G_UNLOCK (time_zones);
+  else
+    G_UNLOCK (tz_default);
 
   return tz;
 }
@@ -1809,7 +1969,8 @@ g_time_zone_new_utc (void)
 
   if (g_once_init_enter (&initialised))
     {
-      utc = g_time_zone_new ("UTC");
+      utc = g_time_zone_new_identifier ("UTC");
+      g_assert (utc != NULL);
       g_once_init_leave (&initialised, TRUE);
     }
 
@@ -1846,7 +2007,9 @@ g_time_zone_new_local (void)
     g_clear_pointer (&tz_local, g_time_zone_unref);
 
   if (tz_local == NULL)
-    tz_local = g_time_zone_new (tzenv);
+    tz_local = g_time_zone_new_identifier (tzenv);
+  if (tz_local == NULL)
+    tz_local = g_time_zone_new_utc ();
 
   tz = g_time_zone_ref (tz_local);
 
@@ -1865,7 +2028,13 @@ g_time_zone_new_local (void)
  * This is equivalent to calling g_time_zone_new() with a string in the form
  * `[+|-]hh[:mm[:ss]]`.
  *
- * Returns: (transfer full): a timezone at the given offset from UTC
+ * It is possible for this function to fail if @seconds is too big (greater than
+ * 24 hours), in which case this function will return the UTC timezone for
+ * backwards compatibility. To detect failures like this, use
+ * g_time_zone_new_identifier() directly.
+ *
+ * Returns: (transfer full): a timezone at the given offset from UTC, or UTC on
+ *   failure
  * Since: 2.58
  */
 GTimeZone *
@@ -1877,16 +2046,22 @@ g_time_zone_new_offset (gint32 seconds)
   /* Seemingly, we should be using @seconds directly to set the
    * #TransitionInfo.gmt_offset to avoid all this string building and parsing.
    * However, we always need to set the #GTimeZone.name to a constructed
-   * string anyway, so we might as well reuse its code. */
+   * string anyway, so we might as well reuse its code.
+   * g_time_zone_new_identifier() should never fail in this situation. */
   identifier = g_strdup_printf ("%c%02u:%02u:%02u",
                                 (seconds >= 0) ? '+' : '-',
                                 (ABS (seconds) / 60) / 60,
                                 (ABS (seconds) / 60) % 60,
                                 ABS (seconds) % 60);
-  tz = g_time_zone_new (identifier);
-  g_free (identifier);
+  tz = g_time_zone_new_identifier (identifier);
+
+  if (tz == NULL)
+    tz = g_time_zone_new_utc ();
+  else
+    g_assert (g_time_zone_get_offset (tz, 0) == seconds);
 
-  g_assert (g_time_zone_get_offset (tz, 0) == seconds);
+  g_assert (tz != NULL);
+  g_free (identifier);
 
   return tz;
 }