/* * Copyright © 2010 Codethink Limited * * 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 of the licence, 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, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * * Author: Ryan Lortie */ /* Prologue {{{1 */ #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" /** * 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. * * A time zone contains a number of intervals. Each interval has * an abbreviation to describe it, an offet 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. * * 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: * * #GDateTime is an opaque structure whose members cannot be accessed * directly. * * Since: 2.26 **/ /* 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); } 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; }; /* GTimeZone structure and lifecycle {{{1 */ struct _GTimeZone { gchar *name; GBytes *zoneinfo; const struct tzhead *header; const struct ttinfo *infos; const gint64_be *trans; const guint8 *indices; const gchar *abbrs; gint timecnt; gint ref_count; }; G_LOCK_DEFINE_STATIC (time_zones); static GHashTable/**/ *time_zones; /** * 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; } g_hash_table_remove (time_zones, tz->name); G_UNLOCK(time_zones); } if (tz->zoneinfo) g_bytes_unref (tz->zoneinfo); 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 'hh' 'hhmm' or 'hh:mm' where: * - hh is 00 to 23 * - mm is 00 to 59 */ static gboolean parse_time (const gchar *time_, gint32 *offset) { if (*time_ < '0' || '2' < *time_) return FALSE; *offset = 10 * 60 * 60 * (*time_++ - '0'); if (*time_ < '0' || '9' < *time_) return FALSE; *offset += 60 * 60 * (*time_++ - '0'); if (*offset > 23 * 60 * 60) return FALSE; if (*time_ == '\0') return TRUE; if (*time_ == ':') time_++; if (*time_ < '0' || '5' < *time_) return FALSE; *offset += 10 * 60 * (*time_++ - '0'); if (*time_ < '0' || '9' < *time_) return FALSE; *offset += 60 * (*time_++ - '0'); return *time_ == '\0'; } static gboolean parse_constant_offset (const gchar *name, gint32 *offset) { switch (*name++) { case 'Z': *offset = 0; return !*name; case '+': return parse_time (name, offset); case '-': if (parse_time (name, offset)) { *offset = -*offset; return TRUE; } default: return FALSE; } } static GBytes * zone_for_constant_offset (const gchar *name) { const gchar fake_zoneinfo_headers[] = "TZif" "2..." "...." "...." "...." "\0\0\0\0" "\0\0\0\0" "\0\0\0\0" "\0\0\0\0" "\0\0\0\0" "\0\0\0\0" "TZif" "2..." "...." "...." "...." "\0\0\0\0" "\0\0\0\0" "\0\0\0\0" "\0\0\0\0" "\0\0\0\1" "\0\0\0\7"; struct { struct tzhead headers[2]; struct ttinfo info; gchar abbr[8]; } *fake; gint32 offset; if (name == NULL || !parse_constant_offset (name, &offset)) return NULL; offset = GINT32_TO_BE (offset); fake = g_malloc (sizeof *fake); memcpy (fake, fake_zoneinfo_headers, sizeof fake_zoneinfo_headers); memcpy (&fake->info.tt_gmtoff, &offset, sizeof offset); fake->info.tt_isdst = FALSE; fake->info.tt_abbrind = 0; strcpy (fake->abbr, name); return g_bytes_new_take (fake, sizeof *fake); } /* Construction {{{1 */ /** * g_time_zone_new: * @identifier: (allow-none): a timezone identifier * * Creates a #GTimeZone corresponding to @identifier. * * @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). * * Valid RFC3339 time offsets are "Z" (for UTC) or * "±hh:mm". ISO 8601 additionally specifies * "±hhmm" and "±hh". * * The TZ environment variable typically corresponds * to the name of a file in the zoneinfo database, but there are many * other possibilities. Note that those other possibilities are not * currently implemented, but are planned. * * 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 timezone. * * See RFC3339 * §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 for an explanation of the possible * values of the TZ environment variable. * * 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 **/ GTimeZone * g_time_zone_new (const gchar *identifier) { GTimeZone *tz; GMappedFile *file; G_LOCK (time_zones); if (time_zones == NULL) time_zones = g_hash_table_new (g_str_hash, g_str_equal); if (identifier) tz = g_hash_table_lookup (time_zones, identifier); else tz = NULL; if (tz == NULL) { tz = g_slice_new0 (GTimeZone); tz->name = g_strdup (identifier); tz->ref_count = 0; tz->zoneinfo = zone_for_constant_offset (identifier); if (tz->zoneinfo == NULL) { gchar *filename; /* identifier can be a relative or absolute path name; if relative, it is interpreted starting from /usr/share/timezone while the POSIX standard says it should start with :, glibc allows both syntaxes, so we should too */ if (identifier != NULL) { const gchar *tzdir; tzdir = getenv ("TZDIR"); if (tzdir == NULL) tzdir = "/usr/share/zoneinfo"; if (*identifier == ':') identifier ++; if (g_path_is_absolute (identifier)) filename = g_strdup (identifier); else filename = g_build_filename (tzdir, identifier, NULL); } else filename = g_strdup ("/etc/localtime"); file = g_mapped_file_new (filename, FALSE, NULL); if (file != NULL) { tz->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_free (filename); } if (tz->zoneinfo != NULL) { gsize size; const struct tzhead *header = g_bytes_get_data (tz->zoneinfo, &size); /* we only bother to support version 2 */ if (size < sizeof (struct tzhead) || memcmp (header, "TZif2", 5)) { g_bytes_unref (tz->zoneinfo); tz->zoneinfo = NULL; } else { gint typecnt; /* we trust the file completely. */ tz->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)); typecnt = guint32_from_be (tz->header->tzh_typecnt); tz->timecnt = guint32_from_be (tz->header->tzh_timecnt); tz->trans = (gconstpointer) (tz->header + 1); tz->indices = (gconstpointer) (tz->trans + tz->timecnt); tz->infos = (gconstpointer) (tz->indices + tz->timecnt); tz->abbrs = (gconstpointer) (tz->infos + typecnt); } } if (identifier) g_hash_table_insert (time_zones, tz->name, tz); } g_atomic_int_inc (&tz->ref_count); G_UNLOCK (time_zones); 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) { return g_time_zone_new ("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) { return g_time_zone_new (getenv ("TZ")); } /* Internal helpers {{{1 */ inline static const struct ttinfo * interval_info (GTimeZone *tz, gint interval) { if (interval) return tz->infos + tz->indices[interval - 1]; return tz->infos; } inline static gint64 interval_start (GTimeZone *tz, gint interval) { if (interval) return gint64_from_be (tz->trans[interval - 1]); return G_MININT64; } inline static gint64 interval_end (GTimeZone *tz, gint interval) { if (interval < tz->timecnt) return gint64_from_be (tz->trans[interval]) - 1; return G_MAXINT64; } inline static gint32 interval_offset (GTimeZone *tz, gint interval) { return gint32_from_be (interval_info (tz, interval)->tt_gmtoff); } inline static gboolean interval_isdst (GTimeZone *tz, gint interval) { return interval_info (tz, interval)->tt_isdst; } inline static guint8 interval_abbrind (GTimeZone *tz, gint interval) { return interval_info (tz, interval)->tt_abbrind; } inline static gint64 interval_local_start (GTimeZone *tz, gint interval) { if (interval) return interval_start (tz, interval) + interval_offset (tz, interval); return G_MININT64; } inline static gint64 interval_local_end (GTimeZone *tz, gint interval) { if (interval < tz->timecnt) return interval_end (tz, interval) + interval_offset (tz, interval); return G_MAXINT64; } static gboolean interval_valid (GTimeZone *tz, gint interval) { return interval <= tz->timecnt; } /* g_time_zone_find_interval() {{{1 */ /** * g_time_zone_adjust_time: * @tz: a #GTimeZone * @type: the #GTimeType of @time_ * @time_: 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_) { gint i; if (tz->zoneinfo == NULL) return 0; /* find the interval containing *time UTC * TODO: this could be binary searched (or better) */ for (i = 0; i < tz->timecnt; 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 if (interval_isdst (tz, i) != type) /* 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 < tz->timecnt && *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 the 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 is 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_) { gint i; if (tz->zoneinfo == NULL) return 0; for (i = 0; i < tz->timecnt; 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 if (interval_isdst (tz, i) != type) { if (i && time_ <= interval_local_end (tz, i - 1)) i--; else if (i < tz->timecnt && 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, interval), NULL); if (tz->header == NULL) return "UTC"; return tz->abbrs + interval_abbrind (tz, 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, interval), 0); if (tz->header == NULL) return 0; return interval_offset (tz, 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->header == NULL) return FALSE; return interval_isdst (tz, interval); } /* Epilogue {{{1 */ /* vim:set foldmethod=marker: */