/* * Copyright © 2010 Codethink Limited * * SPDX-License-Identifier: LGPL-2.1-or-later * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, see . * * Author: Ryan Lortie */ /* Prologue {{{1 */ #include "config.h" #include "gtimezone.h" #include #include #include #include "gmappedfile.h" #include "gtestutils.h" #include "gfileutils.h" #include "gstrfuncs.h" #include "ghash.h" #include "gthread.h" #include "gbytes.h" #include "gslice.h" #include "gdatetime.h" #include "gdate.h" #include "genviron.h" #ifdef G_OS_UNIX #include "gstdio.h" #endif #ifdef G_OS_WIN32 #define STRICT #include #include #endif /** * SECTION:timezone * @title: GTimeZone * @short_description: a structure representing a time zone * @see_also: #GDateTime * * #GTimeZone is a structure that represents a time zone, at no * particular point in time. It is refcounted and immutable. * * Each time zone has an identifier (for example, ‘Europe/London’) which is * platform dependent. See g_time_zone_new() for information on the identifier * formats. The identifier of a time zone can be retrieved using * g_time_zone_get_identifier(). * * A time zone contains a number of intervals. Each interval has * an abbreviation to describe it (for example, ‘PDT’), an offset to UTC and a * flag indicating if the daylight savings time is in effect during that * interval. A time zone always has at least one interval — interval 0. Note * that interval abbreviations are not the same as time zone identifiers * (apart from ‘UTC’), and cannot be passed to g_time_zone_new(). * * Every UTC time is contained within exactly one interval, but a given * local time may be contained within zero, one or two intervals (due to * incontinuities associated with daylight savings time). * * An interval may refer to a specific period of time (eg: the duration * of daylight savings time during 2010) or it may refer to many periods * of time that share the same properties (eg: all periods of daylight * savings time). It is also possible (usually for political reasons) * that some properties (like the abbreviation) change between intervals * without other properties changing. * * #GTimeZone is available since GLib 2.26. */ /** * GTimeZone: * * #GTimeZone is an opaque structure whose members cannot be accessed * directly. * * Since: 2.26 **/ /* IANA zoneinfo file format {{{1 */ /* unaligned */ typedef struct { gchar bytes[8]; } gint64_be; typedef struct { gchar bytes[4]; } gint32_be; typedef struct { gchar bytes[4]; } guint32_be; static inline gint64 gint64_from_be (const gint64_be be) { gint64 tmp; memcpy (&tmp, &be, sizeof tmp); return GINT64_FROM_BE (tmp); } static inline gint32 gint32_from_be (const gint32_be be) { gint32 tmp; memcpy (&tmp, &be, sizeof tmp); return GINT32_FROM_BE (tmp); } static inline guint32 guint32_from_be (const guint32_be be) { guint32 tmp; memcpy (&tmp, &be, sizeof tmp); return GUINT32_FROM_BE (tmp); } /* The layout of an IANA timezone file header */ struct tzhead { gchar tzh_magic[4]; gchar tzh_version; guchar tzh_reserved[15]; guint32_be tzh_ttisgmtcnt; guint32_be tzh_ttisstdcnt; guint32_be tzh_leapcnt; guint32_be tzh_timecnt; guint32_be tzh_typecnt; guint32_be tzh_charcnt; }; struct ttinfo { gint32_be tt_gmtoff; guint8 tt_isdst; guint8 tt_abbrind; }; /* A Transition Date structure for TZ Rules, an intermediate structure for parsing MSWindows and Environment-variable time zones. It Generalizes MSWindows's SYSTEMTIME struct. */ typedef struct { gint year; gint mon; gint mday; gint wday; gint week; gint32 offset; /* hour*3600 + min*60 + sec; can be negative. */ } TimeZoneDate; /* POSIX Timezone abbreviations are typically 3 or 4 characters, but Microsoft uses 32-character names. We'll use one larger to ensure we have room for the terminating \0. */ #define NAME_SIZE 33 /* A MSWindows-style time zone transition rule. Generalizes the MSWindows TIME_ZONE_INFORMATION struct. Also used to compose time zones from tzset-style identifiers. */ typedef struct { guint start_year; gint32 std_offset; gint32 dlt_offset; TimeZoneDate dlt_start; TimeZoneDate dlt_end; gchar std_name[NAME_SIZE]; gchar dlt_name[NAME_SIZE]; } TimeZoneRule; /* GTimeZone's internal representation of a Daylight Savings (Summer) time interval. */ typedef struct { gint32 gmt_offset; gboolean is_dst; gchar *abbrev; } TransitionInfo; /* GTimeZone's representation of a transition time to or from Daylight Savings (Summer) time and Standard time for the zone. */ typedef struct { gint64 time; gint info_index; } Transition; /* GTimeZone structure */ struct _GTimeZone { gchar *name; GArray *t_info; /* Array of TransitionInfo */ GArray *transitions; /* Array of Transition */ gint ref_count; }; G_LOCK_DEFINE_STATIC (time_zones); static GHashTable/**/ *time_zones; G_LOCK_DEFINE_STATIC (tz_default); static GTimeZone *tz_default = NULL; G_LOCK_DEFINE_STATIC (tz_local); static GTimeZone *tz_local = NULL; #define MIN_TZYEAR 1916 /* Daylight Savings started in WWI */ #define MAX_TZYEAR 2999 /* And it's not likely ever to go away, but there's no point in getting carried away. */ #ifdef G_OS_UNIX static GTimeZone *parse_footertz (const gchar *, size_t); #endif /** * g_time_zone_unref: * @tz: a #GTimeZone * * Decreases the reference count on @tz. * * Since: 2.26 **/ void g_time_zone_unref (GTimeZone *tz) { int ref_count; again: ref_count = g_atomic_int_get (&tz->ref_count); g_assert (ref_count > 0); if (ref_count == 1) { if (tz->name != NULL) { G_LOCK(time_zones); /* someone else might have grabbed a ref in the meantime */ if G_UNLIKELY (g_atomic_int_get (&tz->ref_count) != 1) { G_UNLOCK(time_zones); goto again; } if (time_zones != NULL) g_hash_table_remove (time_zones, tz->name); G_UNLOCK(time_zones); } if (tz->t_info != NULL) { guint idx; for (idx = 0; idx < tz->t_info->len; idx++) { TransitionInfo *info = &g_array_index (tz->t_info, TransitionInfo, idx); g_free (info->abbrev); } g_array_free (tz->t_info, TRUE); } if (tz->transitions != NULL) g_array_free (tz->transitions, TRUE); g_free (tz->name); g_slice_free (GTimeZone, tz); } else if G_UNLIKELY (!g_atomic_int_compare_and_exchange (&tz->ref_count, ref_count, ref_count - 1)) goto again; } /** * g_time_zone_ref: * @tz: a #GTimeZone * * Increases the reference count on @tz. * * Returns: a new reference to @tz. * * Since: 2.26 **/ GTimeZone * g_time_zone_ref (GTimeZone *tz) { g_assert (tz->ref_count > 0); g_atomic_int_inc (&tz->ref_count); return tz; } /* fake zoneinfo creation (for RFC3339/ISO 8601 timezones) {{{1 */ /* * parses strings of the form h or hh[[:]mm[[[:]ss]]] where: * - h[h] is 0 to 24 * - mm is 00 to 59 * - ss is 00 to 59 * If RFC8536, TIME_ is a transition time sans sign, * so colons are required before mm and ss, and hh can be up to 167. * See Internet RFC 8536 section 3.3.1: * https://tools.ietf.org/html/rfc8536#section-3.3.1 * and POSIX Base Definitions 8.3 TZ rule time: * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03 */ static gboolean parse_time (const gchar *time_, gint32 *offset, gboolean rfc8536) { if (*time_ < '0' || '9' < *time_) return FALSE; *offset = 60 * 60 * (*time_++ - '0'); if (*time_ == '\0') return TRUE; if (*time_ != ':') { if (*time_ < '0' || '9' < *time_) return FALSE; *offset *= 10; *offset += 60 * 60 * (*time_++ - '0'); if (rfc8536) { /* Internet RFC 8536 section 3.3.1 and POSIX 8.3 TZ together say that a transition time must be of the form [+-]hh[:mm[:ss]] where the hours part can range from -167 to 167. */ if ('0' <= *time_ && *time_ <= '9') { *offset *= 10; *offset += 60 * 60 * (*time_++ - '0'); } if (*offset > 167 * 60 * 60) return FALSE; } else if (*offset > 24 * 60 * 60) return FALSE; if (*time_ == '\0') return TRUE; } if (*time_ == ':') time_++; else if (rfc8536) return FALSE; if (*time_ < '0' || '5' < *time_) return FALSE; *offset += 10 * 60 * (*time_++ - '0'); if (*time_ < '0' || '9' < *time_) return FALSE; *offset += 60 * (*time_++ - '0'); if (*time_ == '\0') return TRUE; if (*time_ == ':') time_++; else if (rfc8536) return FALSE; if (*time_ < '0' || '5' < *time_) return FALSE; *offset += 10 * (*time_++ - '0'); if (*time_ < '0' || '9' < *time_) return FALSE; *offset += *time_++ - '0'; return *time_ == '\0'; } static gboolean parse_constant_offset (const gchar *name, gint32 *offset, gboolean rfc8536) { /* Internet RFC 8536 section 3.3.1 and POSIX 8.3 TZ together say that a transition time must be numeric. */ if (!rfc8536 && g_strcmp0 (name, "UTC") == 0) { *offset = 0; return TRUE; } if (*name >= '0' && '9' >= *name) return parse_time (name, offset, rfc8536); switch (*name++) { case 'Z': *offset = 0; /* Internet RFC 8536 section 3.3.1 requires a numeric zone. */ return !rfc8536 && !*name; case '+': return parse_time (name, offset, rfc8536); case '-': if (parse_time (name, offset, rfc8536)) { *offset = -*offset; return TRUE; } else return FALSE; default: return FALSE; } } static void zone_for_constant_offset (GTimeZone *gtz, const gchar *name) { gint32 offset; TransitionInfo info; if (name == NULL || !parse_constant_offset (name, &offset, FALSE)) return; info.gmt_offset = offset; info.is_dst = FALSE; info.abbrev = g_strdup (name); gtz->name = g_strdup (name); gtz->t_info = g_array_sized_new (FALSE, TRUE, sizeof (TransitionInfo), 1); g_array_append_val (gtz->t_info, info); /* Constant offset, no transitions */ gtz->transitions = NULL; } #if defined(G_OS_UNIX) && 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) */ #ifdef G_OS_UNIX /* * 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; gboolean not_a_symlink_to_zoneinfo = FALSE; struct stat file_status; /* Resolve the actual timezone pointed to by /etc/localtime. */ resolved_identifier = g_file_read_link ("/etc/localtime", &read_link_err); if (resolved_identifier != NULL) { if (!g_path_is_absolute (resolved_identifier)) { gchar *absolute_resolved_identifier = g_build_filename ("/etc", resolved_identifier, NULL); g_free (resolved_identifier); resolved_identifier = g_steal_pointer (&absolute_resolved_identifier); } if (g_lstat (resolved_identifier, &file_status) == 0) { if ((file_status.st_mode & S_IFMT) != S_IFREG) { /* Some systems (e.g. toolbox containers) make /etc/localtime be a symlink * to a symlink. * * Rather than try to cope with that, just ignore /etc/localtime and use * the fallback code to read timezone from /etc/timezone */ g_clear_pointer (&resolved_identifier, g_free); not_a_symlink_to_zoneinfo = TRUE; } } else { g_clear_pointer (&resolved_identifier, g_free); } } else { not_a_symlink_to_zoneinfo = g_error_matches (read_link_err, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_clear_error (&read_link_err); } if (resolved_identifier == NULL) { /* 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_to_zoneinfo && (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, const gchar *resolved_identifier) { gchar *filename = NULL; GMappedFile *file = NULL; GBytes *zoneinfo = NULL; const gchar *tzdir; tzdir = g_getenv ("TZDIR"); if (tzdir == NULL) tzdir = zone_info_base_dir (); /* identifier can be a relative or absolute path name; if relative, it is interpreted starting from /usr/share/zoneinfo while the POSIX standard says it should start with :, glibc allows both syntaxes, so we should too */ if (identifier != NULL) { if (*identifier == ':') identifier ++; if (g_path_is_absolute (identifier)) filename = g_strdup (identifier); else filename = g_build_filename (tzdir, identifier, NULL); } else { if (resolved_identifier == NULL) goto out; filename = g_strdup ("/etc/localtime"); } file = g_mapped_file_new (filename, FALSE, NULL); if (file != NULL) { zoneinfo = g_bytes_new_with_free_func (g_mapped_file_get_contents (file), g_mapped_file_get_length (file), (GDestroyNotify)g_mapped_file_unref, g_mapped_file_ref (file)); g_mapped_file_unref (file); } g_assert (resolved_identifier != NULL); out: g_free (filename); return zoneinfo; } static void init_zone_from_iana_info (GTimeZone *gtz, GBytes *zoneinfo, gchar *identifier /* (transfer full) */) { gsize size; guint index; guint32 time_count, type_count; guint8 *tz_transitions, *tz_type_index, *tz_ttinfo; guint8 *tz_abbrs; gsize timesize = sizeof (gint32); gconstpointer header_data = g_bytes_get_data (zoneinfo, &size); const gchar *data = header_data; const struct tzhead *header = header_data; GTimeZone *footertz = NULL; guint extra_time_count = 0, extra_type_count = 0; gint64 last_explicit_transition_time = 0; g_return_if_fail (size >= sizeof (struct tzhead) && memcmp (header, "TZif", 4) == 0); /* FIXME: Handle invalid TZif files better (Issue#1088). */ if (header->tzh_version >= '2') { /* Skip ahead to the newer 64-bit data if it's available. */ header = (const struct tzhead *) (((const gchar *) (header + 1)) + guint32_from_be(header->tzh_ttisgmtcnt) + guint32_from_be(header->tzh_ttisstdcnt) + 8 * guint32_from_be(header->tzh_leapcnt) + 5 * guint32_from_be(header->tzh_timecnt) + 6 * guint32_from_be(header->tzh_typecnt) + guint32_from_be(header->tzh_charcnt)); timesize = sizeof (gint64); } time_count = guint32_from_be(header->tzh_timecnt); type_count = guint32_from_be(header->tzh_typecnt); if (header->tzh_version >= '2') { const gchar *footer = (((const gchar *) (header + 1)) + guint32_from_be(header->tzh_ttisgmtcnt) + guint32_from_be(header->tzh_ttisstdcnt) + 12 * guint32_from_be(header->tzh_leapcnt) + 9 * time_count + 6 * type_count + guint32_from_be(header->tzh_charcnt)); const gchar *footerlast; size_t footerlen; g_return_if_fail (footer <= data + size - 2 && footer[0] == '\n'); footerlast = memchr (footer + 1, '\n', data + size - (footer + 1)); g_return_if_fail (footerlast); footerlen = footerlast + 1 - footer; if (footerlen != 2) { footertz = parse_footertz (footer, footerlen); g_return_if_fail (footertz); extra_type_count = footertz->t_info->len; extra_time_count = footertz->transitions->len; } } tz_transitions = ((guint8 *) (header) + sizeof (*header)); tz_type_index = tz_transitions + timesize * time_count; tz_ttinfo = tz_type_index + time_count; tz_abbrs = tz_ttinfo + sizeof (struct ttinfo) * type_count; gtz->name = g_steal_pointer (&identifier); gtz->t_info = g_array_sized_new (FALSE, TRUE, sizeof (TransitionInfo), type_count + extra_type_count); gtz->transitions = g_array_sized_new (FALSE, TRUE, sizeof (Transition), time_count + extra_time_count); for (index = 0; index < type_count; index++) { TransitionInfo t_info; struct ttinfo info = ((struct ttinfo*)tz_ttinfo)[index]; t_info.gmt_offset = gint32_from_be (info.tt_gmtoff); t_info.is_dst = info.tt_isdst ? TRUE : FALSE; t_info.abbrev = g_strdup ((gchar *) &tz_abbrs[info.tt_abbrind]); g_array_append_val (gtz->t_info, t_info); } for (index = 0; index < time_count; index++) { Transition trans; if (header->tzh_version >= '2') trans.time = gint64_from_be (((gint64_be*)tz_transitions)[index]); else trans.time = gint32_from_be (((gint32_be*)tz_transitions)[index]); last_explicit_transition_time = trans.time; trans.info_index = tz_type_index[index]; g_assert (trans.info_index >= 0); g_assert ((guint) trans.info_index < gtz->t_info->len); g_array_append_val (gtz->transitions, trans); } if (footertz) { /* Append footer time types. Don't bother to coalesce duplicates with existing time types. */ for (index = 0; index < extra_type_count; index++) { TransitionInfo t_info; TransitionInfo *footer_t_info = &g_array_index (footertz->t_info, TransitionInfo, index); t_info.gmt_offset = footer_t_info->gmt_offset; t_info.is_dst = footer_t_info->is_dst; t_info.abbrev = g_steal_pointer (&footer_t_info->abbrev); g_array_append_val (gtz->t_info, t_info); } /* Append footer transitions that follow the last explicit transition. */ for (index = 0; index < extra_time_count; index++) { Transition *footer_transition = &g_array_index (footertz->transitions, Transition, index); if (time_count <= 0 || last_explicit_transition_time < footer_transition->time) { Transition trans; trans.time = footer_transition->time; trans.info_index = type_count + footer_transition->info_index; g_array_append_val (gtz->transitions, trans); } } g_time_zone_unref (footertz); } } #elif defined (G_OS_WIN32) static void copy_windows_systemtime (SYSTEMTIME *s_time, TimeZoneDate *tzdate) { tzdate->offset = s_time->wHour * 3600 + s_time->wMinute * 60 + s_time->wSecond; tzdate->mon = s_time->wMonth; tzdate->year = s_time->wYear; tzdate->wday = s_time->wDayOfWeek ? s_time->wDayOfWeek : 7; if (s_time->wYear) { tzdate->mday = s_time->wDay; tzdate->wday = 0; } else tzdate->week = s_time->wDay; } /* UTC = local time + bias while local time = UTC + offset */ static gboolean rule_from_windows_time_zone_info (TimeZoneRule *rule, TIME_ZONE_INFORMATION *tzi) { gchar *std_name, *dlt_name; std_name = g_utf16_to_utf8 ((gunichar2 *)tzi->StandardName, -1, NULL, NULL, NULL); if (std_name == NULL) return FALSE; dlt_name = g_utf16_to_utf8 ((gunichar2 *)tzi->DaylightName, -1, NULL, NULL, NULL); if (dlt_name == NULL) { g_free (std_name); return FALSE; } /* Set offset */ if (tzi->StandardDate.wMonth) { rule->std_offset = -(tzi->Bias + tzi->StandardBias) * 60; rule->dlt_offset = -(tzi->Bias + tzi->DaylightBias) * 60; copy_windows_systemtime (&(tzi->DaylightDate), &(rule->dlt_start)); copy_windows_systemtime (&(tzi->StandardDate), &(rule->dlt_end)); } else { rule->std_offset = -tzi->Bias * 60; rule->dlt_start.mon = 0; } strncpy (rule->std_name, std_name, NAME_SIZE - 1); strncpy (rule->dlt_name, dlt_name, NAME_SIZE - 1); g_free (std_name); g_free (dlt_name); return TRUE; } static gchar* windows_default_tzname (void) { const gunichar2 *subkey = L"SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation"; HKEY key; gchar *key_name = NULL; gunichar2 *key_name_w = NULL; if (RegOpenKeyExW (HKEY_LOCAL_MACHINE, subkey, 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) { DWORD size = 0; if (RegQueryValueExW (key, L"TimeZoneKeyName", NULL, NULL, NULL, &size) == ERROR_SUCCESS) { key_name_w = g_malloc ((gint)size); if (key_name_w == NULL || RegQueryValueExW (key, L"TimeZoneKeyName", NULL, NULL, (LPBYTE)key_name_w, &size) != ERROR_SUCCESS) { g_free (key_name_w); key_name = NULL; } else key_name = g_utf16_to_utf8 (key_name_w, -1, NULL, NULL, NULL); } RegCloseKey (key); } return key_name; } typedef struct { LONG Bias; LONG StandardBias; LONG DaylightBias; SYSTEMTIME StandardDate; SYSTEMTIME DaylightDate; } RegTZI; static void system_time_copy (SYSTEMTIME *orig, SYSTEMTIME *target) { g_return_if_fail (orig != NULL); g_return_if_fail (target != NULL); target->wYear = orig->wYear; target->wMonth = orig->wMonth; target->wDayOfWeek = orig->wDayOfWeek; target->wDay = orig->wDay; target->wHour = orig->wHour; target->wMinute = orig->wMinute; target->wSecond = orig->wSecond; target->wMilliseconds = orig->wMilliseconds; } static void register_tzi_to_tzi (RegTZI *reg, TIME_ZONE_INFORMATION *tzi) { g_return_if_fail (reg != NULL); g_return_if_fail (tzi != NULL); tzi->Bias = reg->Bias; system_time_copy (&(reg->StandardDate), &(tzi->StandardDate)); tzi->StandardBias = reg->StandardBias; system_time_copy (&(reg->DaylightDate), &(tzi->DaylightDate)); tzi->DaylightBias = reg->DaylightBias; } static guint rules_from_windows_time_zone (const gchar *identifier, const gchar *resolved_identifier, TimeZoneRule **rules) { HKEY key; gchar *subkey = NULL; gchar *subkey_dynamic = 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 = { 0 }, regtzi_prev; WCHAR winsyspath[MAX_PATH]; gunichar2 *subkey_w, *subkey_dynamic_w; subkey_dynamic_w = NULL; if (GetSystemDirectoryW (winsyspath, MAX_PATH) == 0) return 0; g_assert (rules != NULL); *rules = NULL; key_name = NULL; if (!identifier) key_name = resolved_identifier; else key_name = identifier; if (!key_name) return 0; subkey = g_strconcat (reg_key, key_name, NULL); subkey_w = g_utf8_to_utf16 (subkey, -1, NULL, NULL, NULL); if (subkey_w == NULL) goto utf16_conv_failed; subkey_dynamic = g_strconcat (subkey, "\\Dynamic DST", NULL); subkey_dynamic_w = g_utf8_to_utf16 (subkey_dynamic, -1, NULL, NULL, NULL); if (subkey_dynamic_w == NULL) goto utf16_conv_failed; if (RegOpenKeyExW (HKEY_LOCAL_MACHINE, subkey_w, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) goto utf16_conv_failed; size = sizeof tzi.StandardName; /* use RegLoadMUIStringW() to query MUI_Std from the registry if possible, otherwise fallback to querying Std */ if (RegLoadMUIStringW (key, L"MUI_Std", tzi.StandardName, size, &size, 0, winsyspath) != ERROR_SUCCESS) { size = sizeof tzi.StandardName; if (RegQueryValueExW (key, L"Std", NULL, NULL, (LPBYTE)&(tzi.StandardName), &size) != ERROR_SUCCESS) goto registry_failed; } size = sizeof tzi.DaylightName; /* use RegLoadMUIStringW() to query MUI_Dlt from the registry if possible, otherwise fallback to querying Dlt */ if (RegLoadMUIStringW (key, L"MUI_Dlt", tzi.DaylightName, size, &size, 0, winsyspath) != ERROR_SUCCESS) { size = sizeof tzi.DaylightName; if (RegQueryValueExW (key, L"Dlt", NULL, NULL, (LPBYTE)&(tzi.DaylightName), &size) != ERROR_SUCCESS) goto registry_failed; } RegCloseKey (key); if (RegOpenKeyExW (HKEY_LOCAL_MACHINE, subkey_dynamic_w, 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) { DWORD i, first, last, year; wchar_t s[12]; size = sizeof first; if (RegQueryValueExW (key, L"FirstEntry", NULL, NULL, (LPBYTE) &first, &size) != ERROR_SUCCESS) goto registry_failed; size = sizeof last; if (RegQueryValueExW (key, L"LastEntry", NULL, NULL, (LPBYTE) &last, &size) != ERROR_SUCCESS) goto registry_failed; rules_num = last - first + 2; *rules = g_new0 (TimeZoneRule, rules_num); for (year = first, i = 0; *rules != NULL && year <= last; year++) { gboolean failed = FALSE; swprintf_s (s, 11, L"%d", year); if (!failed) { size = sizeof regtzi; if (RegQueryValueExW (key, s, NULL, NULL, (LPBYTE) ®tzi, &size) != ERROR_SUCCESS) failed = TRUE; } if (failed) { g_free (*rules); *rules = NULL; break; } if (year > first && memcmp (®tzi_prev, ®tzi, sizeof regtzi) == 0) continue; else memcpy (®tzi_prev, ®tzi, sizeof regtzi); register_tzi_to_tzi (®tzi, &tzi); if (!rule_from_windows_time_zone_info (&(*rules)[i], &tzi)) { g_free (*rules); *rules = NULL; break; } (*rules)[i++].start_year = year; } rules_num = i + 1; registry_failed: RegCloseKey (key); } else if (RegOpenKeyExW (HKEY_LOCAL_MACHINE, subkey_w, 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) { size = sizeof regtzi; if (RegQueryValueExW (key, L"TZI", NULL, NULL, (LPBYTE) ®tzi, &size) == ERROR_SUCCESS) { rules_num = 2; *rules = g_new0 (TimeZoneRule, 2); register_tzi_to_tzi (®tzi, &tzi); if (!rule_from_windows_time_zone_info (&(*rules)[0], &tzi)) { g_free (*rules); *rules = NULL; } } RegCloseKey (key); } utf16_conv_failed: g_free (subkey_dynamic_w); g_free (subkey_dynamic); g_free (subkey_w); g_free (subkey); if (*rules) { (*rules)[0].start_year = MIN_TZYEAR; if ((*rules)[rules_num - 2].start_year < MAX_TZYEAR) (*rules)[rules_num - 1].start_year = MAX_TZYEAR; else (*rules)[rules_num - 1].start_year = (*rules)[rules_num - 2].start_year + 1; return rules_num; } return 0; } #endif static void find_relative_date (TimeZoneDate *buffer) { guint wday; GDate date; g_date_clear (&date, 1); wday = buffer->wday; /* Get last day if last is needed, first day otherwise */ if (buffer->mon == 13 || buffer->mon == 14) /* Julian Date */ { g_date_set_dmy (&date, 1, 1, buffer->year); if (wday >= 59 && buffer->mon == 13 && g_date_is_leap_year (buffer->year)) g_date_add_days (&date, wday); else g_date_add_days (&date, wday - 1); buffer->mon = (int) g_date_get_month (&date); buffer->mday = (int) g_date_get_day (&date); buffer->wday = 0; } else /* M.W.D */ { guint days; guint days_in_month = g_date_get_days_in_month (buffer->mon, buffer->year); GDateWeekday first_wday; g_date_set_dmy (&date, 1, buffer->mon, buffer->year); first_wday = g_date_get_weekday (&date); if ((guint) first_wday > wday) ++(buffer->week); /* week is 1 <= w <= 5, we need 0-based */ days = 7 * (buffer->week - 1) + wday - first_wday; /* "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); buffer->mday = g_date_get_day (&date); } } /* Offset is previous offset of local time. Returns 0 if month is 0 */ static gint64 boundary_for_year (TimeZoneDate *boundary, gint year, gint32 offset) { TimeZoneDate buffer; GDate date; const guint64 unix_epoch_start = 719163L; const guint64 seconds_per_day = 86400L; if (!boundary->mon) return 0; buffer = *boundary; if (boundary->year == 0) { buffer.year = year; if (buffer.wday) find_relative_date (&buffer); } g_assert (buffer.year == year); g_date_clear (&date, 1); g_date_set_dmy (&date, buffer.mday, buffer.mon, buffer.year); return ((g_date_get_julian (&date) - unix_epoch_start) * seconds_per_day + buffer.offset - offset); } static void fill_transition_info_from_rule (TransitionInfo *info, TimeZoneRule *rule, gboolean is_dst) { gint offset = is_dst ? rule->dlt_offset : rule->std_offset; gchar *name = is_dst ? rule->dlt_name : rule->std_name; info->gmt_offset = offset; info->is_dst = is_dst; if (name) info->abbrev = g_strdup (name); else info->abbrev = g_strdup_printf ("%+03d%02d", (int) offset / 3600, (int) abs (offset / 60) % 60); } static void init_zone_from_rules (GTimeZone *gtz, TimeZoneRule *rules, guint rules_num, gchar *identifier /* (transfer full) */) { guint type_count = 0, trans_count = 0, info_index = 0; guint ri; /* rule index */ gboolean skip_first_std_trans = TRUE; gint32 last_offset; type_count = 0; trans_count = 0; /* Last rule only contains max year */ for (ri = 0; ri < rules_num - 1; ri++) { if (rules[ri].dlt_start.mon || rules[ri].dlt_end.mon) { guint rulespan = (rules[ri + 1].start_year - rules[ri].start_year); guint transitions = rules[ri].dlt_start.mon > 0 ? 1 : 0; transitions += rules[ri].dlt_end.mon > 0 ? 1 : 0; type_count += rules[ri].dlt_start.mon > 0 ? 2 : 1; trans_count += transitions * rulespan; } else type_count++; } gtz->name = g_steal_pointer (&identifier); gtz->t_info = g_array_sized_new (FALSE, TRUE, sizeof (TransitionInfo), type_count); gtz->transitions = g_array_sized_new (FALSE, TRUE, sizeof (Transition), trans_count); last_offset = rules[0].std_offset; for (ri = 0; ri < rules_num - 1; ri++) { if ((rules[ri].std_offset || rules[ri].dlt_offset) && rules[ri].dlt_start.mon == 0 && rules[ri].dlt_end.mon == 0) { TransitionInfo std_info; /* Standard */ fill_transition_info_from_rule (&std_info, &(rules[ri]), FALSE); g_array_append_val (gtz->t_info, std_info); if (ri > 0 && ((rules[ri - 1].dlt_start.mon > 12 && rules[ri - 1].dlt_start.wday > rules[ri - 1].dlt_end.wday) || rules[ri - 1].dlt_start.mon > rules[ri - 1].dlt_end.mon)) { /* The previous rule was a southern hemisphere rule that starts the year with DST, so we need to add a transition to return to standard time */ guint year = rules[ri].start_year; gint64 std_time = boundary_for_year (&rules[ri].dlt_end, year, last_offset); Transition std_trans = {std_time, info_index}; g_array_append_val (gtz->transitions, std_trans); } last_offset = rules[ri].std_offset; ++info_index; skip_first_std_trans = TRUE; } else { const guint start_year = rules[ri].start_year; const guint end_year = rules[ri + 1].start_year; gboolean dlt_first; guint year; TransitionInfo std_info, dlt_info; if (rules[ri].dlt_start.mon > 12) dlt_first = rules[ri].dlt_start.wday > rules[ri].dlt_end.wday; else dlt_first = rules[ri].dlt_start.mon > rules[ri].dlt_end.mon; /* Standard rules are always even, because before the first transition is always standard time, and 0 is even. */ fill_transition_info_from_rule (&std_info, &(rules[ri]), FALSE); fill_transition_info_from_rule (&dlt_info, &(rules[ri]), TRUE); g_array_append_val (gtz->t_info, std_info); g_array_append_val (gtz->t_info, dlt_info); /* Transition dates. We hope that a year which ends daylight time in a southern-hemisphere country (i.e., one that begins the year in daylight time) will include a rule which has only a dlt_end. */ for (year = start_year; year < end_year; year++) { gint32 dlt_offset = (dlt_first ? last_offset : rules[ri].dlt_offset); gint32 std_offset = (dlt_first ? rules[ri].std_offset : last_offset); /* NB: boundary_for_year returns 0 if mon == 0 */ gint64 std_time = boundary_for_year (&rules[ri].dlt_end, year, dlt_offset); gint64 dlt_time = boundary_for_year (&rules[ri].dlt_start, year, std_offset); Transition std_trans = {std_time, info_index}; Transition dlt_trans = {dlt_time, info_index + 1}; last_offset = (dlt_first ? rules[ri].dlt_offset : rules[ri].std_offset); if (dlt_first) { if (skip_first_std_trans) skip_first_std_trans = FALSE; else if (std_time) g_array_append_val (gtz->transitions, std_trans); if (dlt_time) g_array_append_val (gtz->transitions, dlt_trans); } else { if (dlt_time) g_array_append_val (gtz->transitions, dlt_trans); if (std_time) g_array_append_val (gtz->transitions, std_trans); } } info_index += 2; } } if (ri > 0 && ((rules[ri - 1].dlt_start.mon > 12 && rules[ri - 1].dlt_start.wday > rules[ri - 1].dlt_end.wday) || rules[ri - 1].dlt_start.mon > rules[ri - 1].dlt_end.mon)) { /* The previous rule was a southern hemisphere rule that starts the year with DST, so we need to add a transition to return to standard time */ TransitionInfo info; guint year = rules[ri].start_year; Transition trans; fill_transition_info_from_rule (&info, &(rules[ri - 1]), FALSE); g_array_append_val (gtz->t_info, info); trans.time = boundary_for_year (&rules[ri - 1].dlt_end, year, last_offset); trans.info_index = info_index; g_array_append_val (gtz->transitions, trans); } } /* * parses date[/time] for parsing TZ environment variable * * date is either Mm.w.d, Jn or N * - m is 1 to 12 * - w is 1 to 5 * - d is 0 to 6 * - n is 1 to 365 * - N is 0 to 365 * * time is either h or hh[[:]mm[[[:]ss]]] * - h[h] is 0 to 24 * - mm is 00 to 59 * - ss is 00 to 59 */ static gboolean parse_mwd_boundary (gchar **pos, TimeZoneDate *boundary) { gint month, week, day; if (**pos == '\0' || **pos < '0' || '9' < **pos) return FALSE; month = *(*pos)++ - '0'; if ((month == 1 && **pos >= '0' && '2' >= **pos) || (month == 0 && **pos >= '0' && '9' >= **pos)) { month *= 10; month += *(*pos)++ - '0'; } if (*(*pos)++ != '.' || month == 0) return FALSE; if (**pos == '\0' || **pos < '1' || '5' < **pos) return FALSE; week = *(*pos)++ - '0'; if (*(*pos)++ != '.') return FALSE; if (**pos == '\0' || **pos < '0' || '6' < **pos) return FALSE; day = *(*pos)++ - '0'; if (!day) day += 7; boundary->year = 0; boundary->mon = month; boundary->week = week; boundary->wday = day; return TRUE; } /* * This parses two slightly different ways of specifying * the Julian day: * * - ignore_leap == TRUE * * Jn This specifies the Julian day with n between 1 and 365. Leap days * are not counted. In this format, February 29 can't be represented; * February 28 is day 59, and March 1 is always day 60. * * - ignore_leap == FALSE * * n This specifies the zero-based Julian day with n between 0 and 365. * February 29 is counted in leap years. */ static gboolean parse_julian_boundary (gchar** pos, TimeZoneDate *boundary, gboolean ignore_leap) { gint day = 0; GDate date; while (**pos >= '0' && '9' >= **pos) { day *= 10; day += *(*pos)++ - '0'; } if (ignore_leap) { if (day < 1 || 365 < day) return FALSE; if (day >= 59) day++; } else { if (day < 0 || 365 < day) return FALSE; /* GDate wants day in range 1->366 */ day++; } g_date_clear (&date, 1); g_date_set_julian (&date, day); boundary->year = 0; boundary->mon = (int) g_date_get_month (&date); boundary->mday = (int) g_date_get_day (&date); boundary->wday = 0; return TRUE; } static gboolean parse_tz_boundary (const gchar *identifier, TimeZoneDate *boundary) { gchar *pos; pos = (gchar*)identifier; /* Month-week-weekday */ if (*pos == 'M') { ++pos; if (!parse_mwd_boundary (&pos, boundary)) return FALSE; } /* Julian date which ignores Feb 29 in leap years */ else if (*pos == 'J') { ++pos; if (!parse_julian_boundary (&pos, boundary, TRUE)) return FALSE ; } /* Julian date which counts Feb 29 in leap years */ else if (*pos >= '0' && '9' >= *pos) { if (!parse_julian_boundary (&pos, boundary, FALSE)) return FALSE; } else return FALSE; /* Time */ if (*pos == '/') return parse_constant_offset (pos + 1, &boundary->offset, TRUE); else { boundary->offset = 2 * 60 * 60; return *pos == '\0'; } } static guint create_ruleset_from_rule (TimeZoneRule **rules, TimeZoneRule *rule) { *rules = g_new0 (TimeZoneRule, 2); (*rules)[0].start_year = MIN_TZYEAR; (*rules)[1].start_year = MAX_TZYEAR; (*rules)[0].std_offset = -rule->std_offset; (*rules)[0].dlt_offset = -rule->dlt_offset; (*rules)[0].dlt_start = rule->dlt_start; (*rules)[0].dlt_end = rule->dlt_end; strcpy ((*rules)[0].std_name, rule->std_name); strcpy ((*rules)[0].dlt_name, rule->dlt_name); return 2; } static gboolean parse_offset (gchar **pos, gint32 *target) { gchar *buffer; gchar *target_pos = *pos; gboolean ret; while (**pos == '+' || **pos == '-' || **pos == ':' || (**pos >= '0' && '9' >= **pos)) ++(*pos); buffer = g_strndup (target_pos, *pos - target_pos); ret = parse_constant_offset (buffer, target, FALSE); g_free (buffer); return ret; } static gboolean parse_identifier_boundary (gchar **pos, TimeZoneDate *target) { gchar *buffer; gchar *target_pos = *pos; gboolean ret; while (**pos != ',' && **pos != '\0') ++(*pos); buffer = g_strndup (target_pos, *pos - target_pos); ret = parse_tz_boundary (buffer, target); g_free (buffer); return ret; } static gboolean set_tz_name (gchar **pos, gchar *buffer, guint size) { gboolean quoted = **pos == '<'; gchar *name_pos = *pos; guint len; g_assert (size != 0); if (quoted) { name_pos++; do ++(*pos); while (g_ascii_isalnum (**pos) || **pos == '-' || **pos == '+'); if (**pos != '>') return FALSE; } else while (g_ascii_isalpha (**pos)) ++(*pos); /* Name should be three or more characters */ /* FIXME: Should return FALSE if the name is too long. This should simplify code later in this function. */ if (*pos - name_pos < 3) return FALSE; memset (buffer, 0, size); /* name_pos isn't 0-terminated, so we have to limit the length expressly */ len = (guint) (*pos - name_pos) > size - 1 ? size - 1 : (guint) (*pos - name_pos); strncpy (buffer, name_pos, len); *pos += quoted; return TRUE; } static gboolean parse_identifier_boundaries (gchar **pos, TimeZoneRule *tzr) { if (*(*pos)++ != ',') return FALSE; /* Start date */ if (!parse_identifier_boundary (pos, &(tzr->dlt_start)) || *(*pos)++ != ',') return FALSE; /* End date */ if (!parse_identifier_boundary (pos, &(tzr->dlt_end))) return FALSE; return TRUE; } /* * Creates an array of TimeZoneRule from a TZ environment variable * type of identifier. Should free rules afterwards */ static guint rules_from_identifier (const gchar *identifier, TimeZoneRule **rules) { gchar *pos; TimeZoneRule tzr; g_assert (rules != NULL); *rules = NULL; if (!identifier) return 0; pos = (gchar*)identifier; memset (&tzr, 0, sizeof (tzr)); /* Standard offset */ if (!(set_tz_name (&pos, tzr.std_name, NAME_SIZE)) || !parse_offset (&pos, &(tzr.std_offset))) return 0; if (*pos == 0) { return create_ruleset_from_rule (rules, &tzr); } /* Format 2 */ if (!(set_tz_name (&pos, tzr.dlt_name, NAME_SIZE))) return 0; parse_offset (&pos, &(tzr.dlt_offset)); if (tzr.dlt_offset == 0) /* No daylight offset given, assume it's 1 hour earlier that standard */ tzr.dlt_offset = tzr.std_offset - 3600; if (*pos == '\0') #ifdef G_OS_WIN32 /* Windows allows us to use the US DST boundaries if they're not given */ { 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))) { for (i = 0; i < rules_num - 1; i++) { (*rules)[i].std_offset = - tzr.std_offset; (*rules)[i].dlt_offset = - tzr.dlt_offset; strcpy ((*rules)[i].std_name, tzr.std_name); strcpy ((*rules)[i].dlt_name, tzr.dlt_name); } return rules_num; } else return 0; } #else return 0; #endif /* Start and end required (format 2) */ if (!parse_identifier_boundaries (&pos, &tzr)) return 0; return create_ruleset_from_rule (rules, &tzr); } #ifdef G_OS_UNIX static GTimeZone * parse_footertz (const gchar *footer, size_t footerlen) { gchar *tzstring = g_strndup (footer + 1, footerlen - 2); GTimeZone *footertz = NULL; /* 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. */ TimeZoneRule *rules; guint rules_num = rules_from_identifier (tzstring, &rules); g_free (tzstring); if (rules_num > 1) { footertz = g_slice_new0 (GTimeZone); init_zone_from_rules (footertz, rules, rules_num, NULL); footertz->ref_count++; } g_free (rules); return footertz; } #endif /* Construction {{{1 */ /** * g_time_zone_new: * @identifier: (nullable): a timezone 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 * variable (including %NULL). * * In Windows, @identifier can also be the unlocalized name of a time * zone for standard time, for example "Pacific Standard Time". * * Valid RFC3339 time offsets are `"Z"` (for UTC) or * `"±hh:mm"`. ISO 8601 additionally specifies * `"±hhmm"` and `"±hh"`. Offsets are * time values to be added to Coordinated Universal Time (UTC) to get * the local time. * * In UNIX, the `TZ` environment variable typically corresponds * to the name of a file in the zoneinfo database, an absolute path to a file * somewhere else, or a string in * "std offset [dst [offset],start[/time],end[/time]]" (POSIX) format. * There are no spaces in the specification. The name of standard * and daylight savings time zone must be three or more alphabetic * characters. Offsets are time values to be added to local time to * get Coordinated Universal Time (UTC) and should be * `"[±]hh[[:]mm[:ss]]"`. Dates are either * `"Jn"` (Julian day with n between 1 and 365, leap * years not counted), `"n"` (zero-based Julian day * with n between 0 and 365) or `"Mm.w.d"` (day d * (0 <= d <= 6) of week w (1 <= w <= 5) of month m (1 <= m <= 12), day * 0 is a Sunday). Times are in local wall clock time, the default is * 02:00:00. * * In Windows, the "tzn[+|–]hh[:mm[:ss]][dzn]" format is used, but also * accepts POSIX format. The Windows format uses US rules for all time * zones; daylight savings time is 60 minutes behind the standard time * with date and time of change taken from Pacific Standard Time. * Offsets are time values to be added to the local time to get * Coordinated Universal Time (UTC). * * g_time_zone_new_local() calls this function with the value of the * `TZ` environment variable. This function itself is independent of * the value of `TZ`, but if @identifier is %NULL then `/etc/localtime` * will be consulted to discover the correct time zone on UNIX and the * registry will be consulted or GetTimeZoneInformation() will be used * to get the local time zone on Windows. * * If intervals are not available, only time zone rules from `TZ` * environment variable or other means, then they will be computed * from year 1900 to 2037. If the maximum year for the rules is * available and it is greater than 2037, then it will followed * instead. * * See * [RFC3339 §5.6](http://tools.ietf.org/html/rfc3339#section-5.6) * for a precise definition of valid RFC3339 time offsets * (the `time-offset` expansion) and ISO 8601 for the * full list of valid time offsets. See * [The GNU C Library manual](http://www.gnu.org/s/libc/manual/html_node/TZ-Variable.html) * for an explanation of the possible * values of the `TZ` environment variable. See * [Microsoft Time Zone Index Values](http://msdn.microsoft.com/en-us/library/ms912391%28v=winembedded.11%29.aspx) * for the list of time zones on Windows. * * You should release the return value by calling g_time_zone_unref() * when you are done with it. * * Returns: (transfer full) (nullable): the requested timezone, or %NULL on * failure * Since: 2.68 */ GTimeZone * g_time_zone_new_identifier (const gchar *identifier) { GTimeZone *tz = NULL; TimeZoneRule *rules; gint rules_num; gchar *resolved_identifier = NULL; 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) { g_atomic_int_inc (&tz->ref_count); 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); tz->ref_count = 0; zone_for_constant_offset (tz, identifier); if (tz->t_info == NULL && (rules_num = rules_from_identifier (identifier, &rules))) { init_zone_from_rules (tz, rules, rules_num, g_steal_pointer (&resolved_identifier)); g_free (rules); } if (tz->t_info == NULL) { #ifdef G_OS_UNIX GBytes *zoneinfo = zone_info_unix (identifier, resolved_identifier); if (zoneinfo != NULL) { init_zone_from_iana_info (tz, zoneinfo, g_steal_pointer (&resolved_identifier)); g_bytes_unref (zoneinfo); } #elif defined (G_OS_WIN32) if ((rules_num = rules_from_windows_time_zone (identifier, resolved_identifier, &rules))) { init_zone_from_rules (tz, rules, rules_num, g_steal_pointer (&resolved_identifier)); g_free (rules); } #endif } #if defined (G_OS_WIN32) if (tz->t_info == NULL) { if (identifier == NULL) { TIME_ZONE_INFORMATION tzi; if (GetTimeZoneInformation (&tzi) != TIME_ZONE_ID_INVALID) { rules = g_new0 (TimeZoneRule, 2); if (rule_from_windows_time_zone_info (&rules[0], &tzi)) { memset (rules[0].std_name, 0, NAME_SIZE); memset (rules[0].dlt_name, 0, NAME_SIZE); rules[0].start_year = MIN_TZYEAR; rules[1].start_year = MAX_TZYEAR; init_zone_from_rules (tz, rules, 2, g_steal_pointer (&resolved_identifier)); } g_free (rules); } } } #endif g_free (resolved_identifier); /* Failed to load the timezone. */ if (tz->t_info == NULL) { 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 (identifier) g_hash_table_insert (time_zones, tz->name, tz); else if (tz->name) { /* Caching reference */ g_atomic_int_inc (&tz->ref_count); tz_default = tz; } g_atomic_int_inc (&tz->ref_count); if (identifier) G_UNLOCK (time_zones); else G_UNLOCK (tz_default); return tz; } /** * g_time_zone_new_utc: * * Creates a #GTimeZone corresponding to UTC. * * This is equivalent to calling g_time_zone_new() with a value like * "Z", "UTC", "+00", etc. * * You should release the return value by calling g_time_zone_unref() * when you are done with it. * * Returns: the universal timezone * * Since: 2.26 **/ GTimeZone * g_time_zone_new_utc (void) { static GTimeZone *utc = NULL; static gsize initialised; if (g_once_init_enter (&initialised)) { utc = g_time_zone_new_identifier ("UTC"); g_assert (utc != NULL); g_once_init_leave (&initialised, TRUE); } return g_time_zone_ref (utc); } /** * g_time_zone_new_local: * * Creates a #GTimeZone corresponding to local time. The local time * zone may change between invocations to this function; for example, * if the system administrator changes it. * * This is equivalent to calling g_time_zone_new() with the value of * the `TZ` environment variable (including the possibility of %NULL). * * You should release the return value by calling g_time_zone_unref() * when you are done with it. * * Returns: the local timezone * * Since: 2.26 **/ GTimeZone * g_time_zone_new_local (void) { const gchar *tzenv = g_getenv ("TZ"); GTimeZone *tz; G_LOCK (tz_local); /* Is time zone changed and must be flushed? */ if (tz_local && g_strcmp0 (g_time_zone_get_identifier (tz_local), tzenv)) g_clear_pointer (&tz_local, g_time_zone_unref); if (tz_local == NULL) 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); G_UNLOCK (tz_local); return tz; } /** * g_time_zone_new_offset: * @seconds: offset to UTC, in seconds * * Creates a #GTimeZone corresponding to the given constant offset from UTC, * in seconds. * * This is equivalent to calling g_time_zone_new() with a string in the form * `[+|-]hh[:mm[:ss]]`. * * 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 * g_time_zone_new_offset (gint32 seconds) { GTimeZone *tz = NULL; gchar *identifier = NULL; /* 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. * 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 (identifier); if (tz == NULL) tz = g_time_zone_new_utc (); else g_assert (g_time_zone_get_offset (tz, 0) == seconds); g_assert (tz != NULL); g_free (identifier); return tz; } #define TRANSITION(n) g_array_index (tz->transitions, Transition, n) #define TRANSITION_INFO(n) g_array_index (tz->t_info, TransitionInfo, n) /* Internal helpers {{{1 */ /* NB: Interval 0 is before the first transition, so there's no * transition structure to point to which TransitionInfo to * use. Rule-based zones are set up so that TI 0 is always standard * time (which is what's in effect before Daylight time got started * in the early 20th century), but IANA tzfiles don't follow that * convention. The tzfile documentation says to use the first * standard-time (i.e., non-DST) tinfo, so that's what we do. */ inline static const TransitionInfo* interval_info (GTimeZone *tz, guint interval) { guint index; g_return_val_if_fail (tz->t_info != NULL, NULL); if (interval && tz->transitions && interval <= tz->transitions->len) index = (TRANSITION(interval - 1)).info_index; else { for (index = 0; index < tz->t_info->len; index++) { TransitionInfo *tzinfo = &(TRANSITION_INFO(index)); if (!tzinfo->is_dst) return tzinfo; } index = 0; } return &(TRANSITION_INFO(index)); } inline static gint64 interval_start (GTimeZone *tz, guint interval) { if (!interval || tz->transitions == NULL || tz->transitions->len == 0) return G_MININT64; if (interval > tz->transitions->len) interval = tz->transitions->len; return (TRANSITION(interval - 1)).time; } inline static gint64 interval_end (GTimeZone *tz, guint interval) { if (tz->transitions && interval < tz->transitions->len) { gint64 lim = (TRANSITION(interval)).time; return lim - (lim != G_MININT64); } return G_MAXINT64; } inline static gint32 interval_offset (GTimeZone *tz, guint interval) { g_return_val_if_fail (tz->t_info != NULL, 0); return interval_info (tz, interval)->gmt_offset; } inline static gboolean interval_isdst (GTimeZone *tz, guint interval) { g_return_val_if_fail (tz->t_info != NULL, 0); return interval_info (tz, interval)->is_dst; } inline static gchar* interval_abbrev (GTimeZone *tz, guint interval) { g_return_val_if_fail (tz->t_info != NULL, 0); return interval_info (tz, interval)->abbrev; } inline static gint64 interval_local_start (GTimeZone *tz, guint interval) { if (interval) return interval_start (tz, interval) + interval_offset (tz, interval); return G_MININT64; } inline static gint64 interval_local_end (GTimeZone *tz, guint interval) { if (tz->transitions && interval < tz->transitions->len) return interval_end (tz, interval) + interval_offset (tz, interval); return G_MAXINT64; } static gboolean interval_valid (GTimeZone *tz, guint interval) { if ( tz->transitions == NULL) return interval == 0; return interval <= tz->transitions->len; } /* g_time_zone_find_interval() {{{1 */ /** * g_time_zone_adjust_time: * @tz: a #GTimeZone * @type: the #GTimeType of @time_ * @time_: (inout): a pointer to a number of seconds since January 1, 1970 * * Finds an interval within @tz that corresponds to the given @time_, * possibly adjusting @time_ if required to fit into an interval. * The meaning of @time_ depends on @type. * * This function is similar to g_time_zone_find_interval(), with the * difference that it always succeeds (by making the adjustments * described below). * * In any of the cases where g_time_zone_find_interval() succeeds then * this function returns the same value, without modifying @time_. * * This function may, however, modify @time_ in order to deal with * non-existent times. If the non-existent local @time_ of 02:30 were * requested on March 14th 2010 in Toronto then this function would * adjust @time_ to be 03:00 and return the interval containing the * adjusted time. * * Returns: the interval containing @time_, never -1 * * Since: 2.26 **/ gint g_time_zone_adjust_time (GTimeZone *tz, GTimeType type, gint64 *time_) { guint i, intervals; gboolean interval_is_dst; if (tz->transitions == NULL) return 0; intervals = tz->transitions->len; /* find the interval containing *time UTC * TODO: this could be binary searched (or better) */ for (i = 0; i <= intervals; i++) if (*time_ <= interval_end (tz, i)) break; g_assert (interval_start (tz, i) <= *time_ && *time_ <= interval_end (tz, i)); if (type != G_TIME_TYPE_UNIVERSAL) { if (*time_ < interval_local_start (tz, i)) /* if time came before the start of this interval... */ { i--; /* if it's not in the previous interval... */ if (*time_ > interval_local_end (tz, i)) { /* it doesn't exist. fast-forward it. */ i++; *time_ = interval_local_start (tz, i); } } else if (*time_ > interval_local_end (tz, i)) /* if time came after the end of this interval... */ { i++; /* if it's not in the next interval... */ if (*time_ < interval_local_start (tz, i)) /* it doesn't exist. fast-forward it. */ *time_ = interval_local_start (tz, i); } else { interval_is_dst = interval_isdst (tz, i); if ((interval_is_dst && type != G_TIME_TYPE_DAYLIGHT) || (!interval_is_dst && type == G_TIME_TYPE_DAYLIGHT)) { /* it's in this interval, but dst flag doesn't match. * check neighbours for a better fit. */ if (i && *time_ <= interval_local_end (tz, i - 1)) i--; else if (i < intervals && *time_ >= interval_local_start (tz, i + 1)) i++; } } } return i; } /** * g_time_zone_find_interval: * @tz: a #GTimeZone * @type: the #GTimeType of @time_ * @time_: a number of seconds since January 1, 1970 * * Finds an interval within @tz that corresponds to the given @time_. * The meaning of @time_ depends on @type. * * If @type is %G_TIME_TYPE_UNIVERSAL then this function will always * succeed (since universal time is monotonic and continuous). * * Otherwise @time_ is treated as local time. The distinction between * %G_TIME_TYPE_STANDARD and %G_TIME_TYPE_DAYLIGHT is ignored except in * the case that the given @time_ is ambiguous. In Toronto, for example, * 01:30 on November 7th 2010 occurred twice (once inside of daylight * savings time and the next, an hour later, outside of daylight savings * time). In this case, the different value of @type would result in a * different interval being returned. * * It is still possible for this function to fail. In Toronto, for * example, 02:00 on March 14th 2010 does not exist (due to the leap * forward to begin daylight savings time). -1 is returned in that * case. * * Returns: the interval containing @time_, or -1 in case of failure * * Since: 2.26 */ gint g_time_zone_find_interval (GTimeZone *tz, GTimeType type, gint64 time_) { guint i, intervals; gboolean interval_is_dst; if (tz->transitions == NULL) return 0; intervals = tz->transitions->len; for (i = 0; i <= intervals; i++) if (time_ <= interval_end (tz, i)) break; if (type == G_TIME_TYPE_UNIVERSAL) return i; if (time_ < interval_local_start (tz, i)) { if (time_ > interval_local_end (tz, --i)) return -1; } else if (time_ > interval_local_end (tz, i)) { if (time_ < interval_local_start (tz, ++i)) return -1; } else { interval_is_dst = interval_isdst (tz, i); if ((interval_is_dst && type != G_TIME_TYPE_DAYLIGHT) || (!interval_is_dst && type == G_TIME_TYPE_DAYLIGHT)) { if (i && time_ <= interval_local_end (tz, i - 1)) i--; else if (i < intervals && time_ >= interval_local_start (tz, i + 1)) i++; } } return i; } /* Public API accessors {{{1 */ /** * g_time_zone_get_abbreviation: * @tz: a #GTimeZone * @interval: an interval within the timezone * * Determines the time zone abbreviation to be used during a particular * @interval of time in the time zone @tz. * * For example, in Toronto this is currently "EST" during the winter * months and "EDT" during the summer months when daylight savings time * is in effect. * * Returns: the time zone abbreviation, which belongs to @tz * * Since: 2.26 **/ const gchar * g_time_zone_get_abbreviation (GTimeZone *tz, gint interval) { g_return_val_if_fail (interval_valid (tz, (guint)interval), NULL); return interval_abbrev (tz, (guint)interval); } /** * g_time_zone_get_offset: * @tz: a #GTimeZone * @interval: an interval within the timezone * * Determines the offset to UTC in effect during a particular @interval * of time in the time zone @tz. * * The offset is the number of seconds that you add to UTC time to * arrive at local time for @tz (ie: negative numbers for time zones * west of GMT, positive numbers for east). * * Returns: the number of seconds that should be added to UTC to get the * local time in @tz * * Since: 2.26 **/ gint32 g_time_zone_get_offset (GTimeZone *tz, gint interval) { g_return_val_if_fail (interval_valid (tz, (guint)interval), 0); return interval_offset (tz, (guint)interval); } /** * g_time_zone_is_dst: * @tz: a #GTimeZone * @interval: an interval within the timezone * * Determines if daylight savings time is in effect during a particular * @interval of time in the time zone @tz. * * Returns: %TRUE if daylight savings time is in effect * * Since: 2.26 **/ gboolean g_time_zone_is_dst (GTimeZone *tz, gint interval) { g_return_val_if_fail (interval_valid (tz, interval), FALSE); if (tz->transitions == NULL) return FALSE; return interval_isdst (tz, (guint)interval); } /** * g_time_zone_get_identifier: * @tz: a #GTimeZone * * Get the identifier of this #GTimeZone, as passed to g_time_zone_new(). * If the identifier passed at construction time was not recognised, `UTC` will * be returned. If it was %NULL, the identifier of the local timezone at * construction time will be returned. * * The identifier will be returned in the same format as provided at * construction time: if provided as a time offset, that will be returned by * this function. * * Returns: identifier for this timezone * Since: 2.58 */ const gchar * g_time_zone_get_identifier (GTimeZone *tz) { g_return_val_if_fail (tz != NULL, NULL); return tz->name; } /* Epilogue {{{1 */ /* vim:set foldmethod=marker: */