| /* |
| * 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 <desrt@desrt.ca> |
| */ |
| |
| /* Prologue {{{1 */ |
| |
| #include "gtimezone.h" |
| |
| #include <string.h> |
| #include <stdlib.h> |
| #include <signal.h> |
| |
| #include "gmappedfile.h" |
| #include "gtestutils.h" |
| #include "gfileutils.h" |
| #include "gstrfuncs.h" |
| #include "ghash.h" |
| #include "gthread.h" |
| #include "gbuffer.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; |
| |
| GBuffer *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/*<string?, GTimeZone>*/ *time_zones; |
| |
| static guint |
| g_str_hash0 (gconstpointer data) |
| { |
| return data ? g_str_hash (data) : 0; |
| } |
| |
| static gboolean |
| g_str_equal0 (gconstpointer a, |
| gconstpointer b) |
| { |
| if (a == b) |
| return TRUE; |
| |
| if (!a || !b) |
| return FALSE; |
| |
| return g_str_equal (a, b); |
| } |
| |
| /** |
| * g_time_zone_unref: |
| * @tz: a #GTimeZone |
| * |
| * Decreases the reference count on @tz. |
| * |
| * Since: 2.26 |
| **/ |
| void |
| g_time_zone_unref (GTimeZone *tz) |
| { |
| g_assert (tz->ref_count > 0); |
| |
| if (g_atomic_int_dec_and_test (&tz->ref_count)) |
| { |
| G_LOCK(time_zones); |
| g_hash_table_remove (time_zones, tz->name); |
| G_UNLOCK(time_zones); |
| |
| if (tz->zoneinfo) |
| g_buffer_unref (tz->zoneinfo); |
| |
| g_free (tz->name); |
| |
| g_slice_free (GTimeZone, tz); |
| } |
| } |
| |
| /** |
| * 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 |
| */ |
| 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'; |
| } |
| |
| 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 GBuffer * |
| 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_buffer_new_take_data (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 |
| * <varname>TZ</varname> environment variable (including %NULL). |
| * |
| * Valid RFC3339 time offsets are <literal>"Z"</literal> (for UTC) or |
| * <literal>"±hh:mm"</literal>. ISO 8601 additionally specifies |
| * <literal>"±hhmm"</literal> and <literal>"±hh"</literal>. |
| * |
| * The <varname>TZ</varname> 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 |
| * <varname>TZ</varname> environment variable. This function itself is |
| * independent of the value of <varname>TZ</varname>, but if @identifier |
| * is %NULL then <filename>/etc/localtime</filename> will be consulted |
| * to discover the correct timezone. |
| * |
| * See <ulink |
| * url='http://tools.ietf.org/html/rfc3339#section-5.6'>RFC3339 |
| * §5.6</ulink> for a precise definition of valid RFC3339 time offsets |
| * (the <varname>time-offset</varname> expansion) and ISO 8601 for the |
| * full list of valid time offsets. See <ulink |
| * url='http://www.gnu.org/s/libc/manual/html_node/TZ-Variable.html'>The |
| * GNU C Library manual</ulink> for an explanation of the possible |
| * values of the <varname>TZ</varname> 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; |
| |
| G_LOCK (time_zones); |
| if (time_zones == NULL) |
| time_zones = g_hash_table_new (g_str_hash0, |
| g_str_equal0); |
| |
| tz = g_hash_table_lookup (time_zones, identifier); |
| 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; |
| |
| if (identifier != NULL) |
| { |
| const gchar *tzdir; |
| |
| tzdir = getenv ("TZDIR"); |
| if (tzdir == NULL) |
| tzdir = "/usr/share/zoneinfo"; |
| |
| filename = g_build_filename (tzdir, identifier, NULL); |
| } |
| else |
| filename = g_strdup ("/etc/localtime"); |
| |
| tz->zoneinfo = (GBuffer *) g_mapped_file_new (filename, FALSE, NULL); |
| g_free (filename); |
| } |
| |
| if (tz->zoneinfo != NULL) |
| { |
| const struct tzhead *header = tz->zoneinfo->data; |
| gsize size = tz->zoneinfo->size; |
| |
| /* we only bother to support version 2 */ |
| if (size < sizeof (struct tzhead) || memcmp (header, "TZif2", 5)) |
| { |
| g_buffer_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); |
| } |
| } |
| |
| 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. |
| * |
| * This is equivalent to calling g_time_zone_new() with the value of the |
| * <varname>TZ</varname> environment variable (including the possibility |
| * of %NULL). Changes made to <varname>TZ</varname> after the first |
| * call to this function may or may not be noticed by future calls. |
| * |
| * 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 13th 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 occured 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: */ |