/* System timezone handling
 *
 * Copyright (C) 2008 Novell, Inc.
 *
 * Authors: Vincent Untz <vuntz@gnome.org>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 * 
 * Some code is based on previous code in clock-location.c and on code from
 * tz.c (shipped with version <= 2.22.0). Those files were under the same
 * license, with those authors and copyrights:
 * 
 * clock-location.c:
 * ================
 * No header, but most of the work was done (AFAIK) by
 * Federico Mena Quintero <federico@novell.com>
 * Matthias Clasen <mclasen@redhat.com>
 *
 * tz.c:
 * ====
 * Copyright (C) 2000-2001 Ximian, Inc.
 * Copyright (C) 2004 Sun Microsystems, Inc.
 *
 * Authors: Hans Petter Jansson <hpj@ximian.com>
 *	    additional functions by Erwann Chenede <erwann.chenede@sun.com>
 *	    reworked by Vincent Untz <vuntz@gnome.org>
 * 
 * Largely based on Michael Fulbright's work on Anaconda.
 */

/* FIXME: it'd be nice to filter out the timezones that we might get when
 * parsing config files that are not in zone.tab. Note that it's also wrong
 * in some cases: eg, in tzdata2008b, Asia/Calcutta got renamed to
 * Asia/Kolkata and the old name is not in zone.tab. */

#include <string.h>
#include <unistd.h>

#include <glib.h>
#include <glib/gstdio.h>
#include <gio/gio.h>

#include "system-timezone.h"

/* Files that we look at and that should be monitored */
#define CHECK_NB 5
#define ETC_TIMEZONE        "/etc/timezone"
#define ETC_TIMEZONE_MAJ    "/etc/TIMEZONE"
#define ETC_RC_CONF         "/etc/rc.conf"
#define ETC_SYSCONFIG_CLOCK "/etc/sysconfig/clock"
#define ETC_CONF_D_CLOCK    "/etc/conf.d/clock"
#define ETC_LOCALTIME       "/etc/localtime"

/* The first 4 characters in a timezone file, from tzfile.h */
#define TZ_MAGIC "TZif"

static char *files_to_check[CHECK_NB] = {
        ETC_TIMEZONE,
        ETC_TIMEZONE_MAJ,
        ETC_SYSCONFIG_CLOCK,
        ETC_CONF_D_CLOCK,
        ETC_LOCALTIME
};

static GObject *systz_singleton = NULL;

G_DEFINE_TYPE (SystemTimezone, system_timezone, G_TYPE_OBJECT)

typedef struct {
        char *tz;
        char *env_tz;
        GFileMonitor *monitors[CHECK_NB];
} SystemTimezonePrivate;

enum {
	CHANGED,
	LAST_SIGNAL
};

static guint system_timezone_signals[LAST_SIGNAL] = { 0 };

static GObject *system_timezone_constructor (GType                  type,
                                             guint                  n_construct_properties,
                                             GObjectConstructParam *construct_properties);
static void system_timezone_finalize (GObject *obj);

static void system_timezone_monitor_changed (GFileMonitor *handle,
                                             GFile *file,
                                             GFile *other_file,
                                             GFileMonitorEvent event,
                                             gpointer user_data);
static char *system_timezone_find (void);

#define PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SYSTEM_TIMEZONE_TYPE, SystemTimezonePrivate))

SystemTimezone *
system_timezone_new (void)
{
        return g_object_new (SYSTEM_TIMEZONE_TYPE, NULL);
}

const char *
system_timezone_get (SystemTimezone *systz)
{
        SystemTimezonePrivate *priv;

        g_return_val_if_fail (IS_SYSTEM_TIMEZONE (systz), NULL);

        priv = PRIVATE (systz);
        return priv->tz;
}

const char *
system_timezone_get_env (SystemTimezone *systz)
{
        SystemTimezonePrivate *priv;

        g_return_val_if_fail (IS_SYSTEM_TIMEZONE (systz), NULL);

        priv = PRIVATE (systz);
        return priv->env_tz;
}

static void
system_timezone_class_init (SystemTimezoneClass *class)
{
        GObjectClass *g_obj_class = G_OBJECT_CLASS (class);

        g_obj_class->constructor = system_timezone_constructor;
        g_obj_class->finalize = system_timezone_finalize;

        system_timezone_signals[CHANGED] =
		g_signal_new ("changed",
			      G_OBJECT_CLASS_TYPE (g_obj_class),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (SystemTimezoneClass, changed),
			      NULL, NULL,
			      g_cclosure_marshal_VOID__STRING,
			      G_TYPE_NONE, 1, G_TYPE_STRING);

        g_type_class_add_private (class, sizeof (SystemTimezonePrivate));
}

static void
system_timezone_init (SystemTimezone *systz)
{
        int i;
        SystemTimezonePrivate *priv = PRIVATE (systz);

        priv->tz = NULL;
        priv->env_tz = NULL;
        for (i = 0; i < CHECK_NB; i++) 
                priv->monitors[i] = NULL;
}

static GObject *
system_timezone_constructor (GType                  type,
                             guint                  n_construct_properties,
                             GObjectConstructParam *construct_properties)
{
        GObject *obj;
        SystemTimezonePrivate *priv;
        int i;

        /* This is a singleton, we don't need to have it per-applet */
        if (systz_singleton)
                return g_object_ref (systz_singleton);

        obj = G_OBJECT_CLASS (system_timezone_parent_class)->constructor (
                                                type,
                                                n_construct_properties,
                                                construct_properties);

        priv = PRIVATE (obj);

        priv->tz = system_timezone_find ();

        priv->env_tz = g_strdup (g_getenv ("TZ"));

        for (i = 0; i < CHECK_NB; i++) {
                GFile     *file;
                GFile     *parent;
                GFileType  parent_type;

                file = g_file_new_for_path (files_to_check[i]);

                parent = g_file_get_parent (file);
                parent_type = g_file_query_file_type (parent, G_FILE_QUERY_INFO_NONE, NULL);
                g_object_unref (parent);

                /* We don't try to monitor the file if the parent directory
                 * doesn't exist: this means we're on a system where this file
                 * is not useful to determine the system timezone.
                 * Since gio does not monitor file in non-existing directories
                 * in a clever way (as of gio 2.22, it just polls every other
                 * seconds to see if the directory now exists), this avoids
                 * unnecessary wakeups. */
                if (parent_type == G_FILE_TYPE_DIRECTORY)
                        priv->monitors[i] = g_file_monitor_file (file,
                                                                 G_FILE_MONITOR_NONE,
                                                                 NULL, NULL);
                g_object_unref (file);

                if (priv->monitors[i])
                        g_signal_connect (G_OBJECT (priv->monitors[i]),
                                          "changed", 
                                          G_CALLBACK (system_timezone_monitor_changed),
                                          obj);
        }

        systz_singleton = obj;

        return systz_singleton;
}

static void
system_timezone_finalize (GObject *obj)
{
        int i;
        SystemTimezonePrivate *priv = PRIVATE (obj);

        if (priv->tz) {
                g_free (priv->tz);
                priv->tz = NULL;
        }

        if (priv->env_tz) {
                g_free (priv->env_tz);
                priv->env_tz = NULL;
        }

        for (i = 0; i < CHECK_NB; i++) {
                if (priv->monitors[i])
                        g_object_unref (priv->monitors[i]);
                priv->monitors[i] = NULL;
        }

        G_OBJECT_CLASS (system_timezone_parent_class)->finalize (obj);

        g_assert (obj == systz_singleton);

        systz_singleton = NULL;
}

static void
system_timezone_monitor_changed (GFileMonitor *handle,
                                 GFile *file,
                                 GFile *other_file,
                                 GFileMonitorEvent event,
                                 gpointer user_data)
{
        SystemTimezonePrivate *priv = PRIVATE (user_data);
        char *new_tz;

        if (event != G_FILE_MONITOR_EVENT_CHANGED &&
            event != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT &&
            event != G_FILE_MONITOR_EVENT_DELETED &&
            event != G_FILE_MONITOR_EVENT_CREATED)
                return;

        new_tz = system_timezone_find ();

        g_assert (priv->tz != NULL && new_tz != NULL);

        if (strcmp (priv->tz, new_tz) != 0) {
                g_free (priv->tz);
                priv->tz = new_tz;

                g_signal_emit (G_OBJECT (user_data),
                               system_timezone_signals[CHANGED],
                               0, priv->tz);
        } else
                g_free (new_tz);
}


/*
 * Code to deal with the system timezone on all distros.
 * There's no dependency on the SystemTimezone GObject here.
 *
 * Here's what we know:
 *
 *  + /etc/localtime contains the binary data of the timezone.
 *    It can be a symlink to the actual data file, a hard link to the data
 *    file, or just a copy. So we can determine the timezone with this
 *    (reading the symlink, comparing inodes, or comparing content).
 *
 *  + However, most distributions also have the timezone setting
 *    configured somewhere else. This might be better to read it from there.
 *
 *    Debian/Ubuntu/Gentoo (new): content of /etc/timezone
 *    Fedora/Mandriva: the ZONE key in /etc/sysconfig/clock
 *    openSUSE: the TIMEZONE key in /etc/sysconfig/clock
 *    Solaris/OpenSolaris: the TZ key in /etc/TIMEZONE
 *    Arch Linux: the TIMEZONE key in /etc/rc.conf
 *    Gentoo (old): the ZONE key in /etc/conf.d/clock
 *
 *    FIXME: reading the system-tools-backends, it seems there's this too:
 *           Solaris: the TZ key in /etc/default/init
 *                    /etc/TIMEZONE seems to be a link to /etc/default/init
 *
 * First, some functions to handle those system config files.
 *
 */

/* This works for Debian and derivatives (including Ubuntu), and new Gentoo */
static char *
system_timezone_read_etc_timezone (void)
{
        FILE    *etc_timezone;
        GString *reading;
        int      c;

        etc_timezone = g_fopen (ETC_TIMEZONE, "r");
        if (!etc_timezone)
                return NULL;

        reading = g_string_new ("");

        c = fgetc (etc_timezone);
        /* only get the first line, we'll validate the value later */
        while (c != EOF && !g_ascii_isspace (c)) {
                reading = g_string_append_c (reading, c);
                c = fgetc (etc_timezone);
        }

        fclose (etc_timezone);

        if (reading->str && reading->str[0] != '\0')
                return g_string_free (reading, FALSE);
        else
                g_string_free (reading, TRUE);

        return NULL;
}

static gboolean
system_timezone_write_etc_timezone (const char  *tz,
                                    GError     **error)
{
        char     *content;
        GError   *our_error;
        gboolean  retval;

        if (!g_file_test (ETC_TIMEZONE, G_FILE_TEST_IS_REGULAR))
                return TRUE;

        content = g_strdup_printf ("%s\n", tz);

        our_error = NULL;
        retval = g_file_set_contents (ETC_TIMEZONE, content, -1, &our_error);
        g_free (content);

        if (!retval) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_GENERAL,
                             ETC_TIMEZONE" cannot be overwritten: %s",
                             our_error->message);
                g_error_free (our_error);
        }

        return retval;
}


/* Read a file that looks like a key-file (but there's no need for groups)
 * and get the last value for a specific key */
static char *
system_timezone_read_key_file (const char *filename,
                               const char *key)
{
        GIOChannel *channel;
        char       *key_eq;
        char       *line;
        char       *retval;

        if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR))
                return NULL;

        channel = g_io_channel_new_file (filename, "r", NULL);
        if (!channel)
                return NULL;

        key_eq = g_strdup_printf ("%s=", key);
        retval = NULL;

        while (g_io_channel_read_line (channel, &line, NULL,
                                       NULL, NULL) == G_IO_STATUS_NORMAL) {
                if (g_str_has_prefix (line, key_eq)) {
                        char *value;
                        int   len;

                        value = line + strlen (key_eq);
                        g_strstrip (value);

                        len = strlen (value);

                        if (value[0] == '\"') {
                                if (value[len - 1] == '\"') {
                                        if (retval)
                                                g_free (retval);

                                        retval = g_strndup (value + 1,
                                                            len - 2);
                                }
                        } else {
                                if (retval)
                                        g_free (retval);

                                retval = g_strdup (line + strlen (key_eq));
                        }

                        g_strstrip (retval);
                }

                g_free (line);
        }

        g_free (key_eq);
        g_io_channel_unref (channel);

        return retval;
}

static gboolean
system_timezone_write_key_file (const char  *filename,
                                const char  *key,
                                const char  *value,
                                GError     **error)
{
        GError    *our_error;
        char      *content;
        gsize      len;
        char      *key_eq;
        char     **lines;
        gboolean   replaced;
        gboolean   retval;
        int        n;
        
        if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR))
                return TRUE;

        our_error = NULL;

        if (!g_file_get_contents (filename, &content, &len, &our_error)) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_GENERAL,
                             "%s cannot be read: %s",
                             filename, our_error->message);
                g_error_free (our_error);
                return FALSE;
        }

        lines = g_strsplit (content, "\n", 0);
        g_free (content);

        key_eq = g_strdup_printf ("%s=", key);
        replaced = FALSE;

        for (n = 0; lines[n] != NULL; n++) {
                if (g_str_has_prefix (lines[n], key_eq)) {
                        char     *old_value;
                        gboolean  use_quotes;

                        old_value = lines[n] + strlen (key_eq);
                        g_strstrip (old_value);
                        use_quotes = old_value[0] == '\"';

                        g_free (lines[n]);

                        if (use_quotes)
                                lines[n] = g_strdup_printf ("%s\"%s\"",
                                                            key_eq, value);
                        else
                                lines[n] = g_strdup_printf ("%s%s",
                                                            key_eq, value);

                        replaced = TRUE;
                }
        }

        g_free (key_eq);

        if (!replaced) {
                g_strfreev (lines);
                return TRUE;
        }

        content = g_strjoinv ("\n", lines);
        g_strfreev (lines);

        retval = g_file_set_contents (filename, content, -1, &our_error);
        g_free (content);

        if (!retval) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_GENERAL,
                             "%s cannot be overwritten: %s",
                             filename, our_error->message);
                g_error_free (our_error);
        }

        return retval;
}

/* This works for Solaris/OpenSolaris */
static char *
system_timezone_read_etc_TIMEZONE (void)
{
        return system_timezone_read_key_file (ETC_TIMEZONE_MAJ,
                                              "TZ");
}

static gboolean
system_timezone_write_etc_TIMEZONE (const char  *tz,
                                    GError     **error)
{
        return system_timezone_write_key_file (ETC_TIMEZONE_MAJ,
                                               "TZ", tz, error);
}

/* This works for Fedora and Mandriva */
static char *
system_timezone_read_etc_sysconfig_clock (void)
{
        return system_timezone_read_key_file (ETC_SYSCONFIG_CLOCK,
                                              "ZONE");
}

static gboolean
system_timezone_write_etc_sysconfig_clock (const char  *tz,
                                           GError     **error)
{
        return system_timezone_write_key_file (ETC_SYSCONFIG_CLOCK,
                                               "ZONE", tz, error);
}

/* This works for openSUSE */
static char *
system_timezone_read_etc_sysconfig_clock_alt (void)
{
        return system_timezone_read_key_file (ETC_SYSCONFIG_CLOCK,
                                              "TIMEZONE");
}

static gboolean
system_timezone_write_etc_sysconfig_clock_alt (const char  *tz,
                                               GError     **error)
{
        return system_timezone_write_key_file (ETC_SYSCONFIG_CLOCK,
                                               "TIMEZONE", tz, error);
}

/* This works for old Gentoo */
static char *
system_timezone_read_etc_conf_d_clock (void)
{
        return system_timezone_read_key_file (ETC_CONF_D_CLOCK,
                                              "TIMEZONE");
}

static gboolean
system_timezone_write_etc_conf_d_clock (const char  *tz,
                                        GError     **error)
{
        return system_timezone_write_key_file (ETC_CONF_D_CLOCK,
                                               "TIMEZONE", tz, error);
}

/* This works for Arch Linux */
static char *
system_timezone_read_etc_rc_conf (void)
{
        return system_timezone_read_key_file (ETC_RC_CONF,
                                              "TIMEZONE");
}

static gboolean
system_timezone_write_etc_rc_conf (const char  *tz,
                                   GError     **error)
{
        return system_timezone_write_key_file (ETC_RC_CONF,
                                               "TIMEZONE", tz, error);
}

/*
 *
 * First, getting the timezone.
 *
 */

static char *
system_timezone_strip_path_if_valid (const char *filename)
{
        int skip;

        if (!filename || !g_str_has_prefix (filename, SYSTEM_ZONEINFODIR"/"))
                return NULL;

        /* Timezone data files also live under posix/ and right/ for some
         * reason.
         * FIXME: make sure accepting those files is valid. I think "posix" is
         * okay, not sure about "right" */
        if (g_str_has_prefix (filename, SYSTEM_ZONEINFODIR"/posix/"))
                skip = strlen (SYSTEM_ZONEINFODIR"/posix/");
        else if (g_str_has_prefix (filename, SYSTEM_ZONEINFODIR"/right/"))
                skip = strlen (SYSTEM_ZONEINFODIR"/right/");
        else
                skip = strlen (SYSTEM_ZONEINFODIR"/");

        return g_strdup (filename + skip);
}

/* Read the soft symlink from /etc/localtime */
static char *
system_timezone_read_etc_localtime_softlink (void)
{
        char *file;
        char *tz;

        if (!g_file_test (ETC_LOCALTIME, G_FILE_TEST_IS_SYMLINK))
                return NULL;

        file = g_file_read_link (ETC_LOCALTIME, NULL);
        tz = system_timezone_strip_path_if_valid (file);
        g_free (file);

        return tz;
}

typedef gboolean (*CompareFiles) (struct stat *a_stat,
                                  struct stat *b_stat,
                                  const char  *a_content,
                                  gsize        a_content_len,
                                  const char  *b_filename);

static char *
recursive_compare (struct stat  *localtime_stat,
                   const char   *localtime_content,
                   gsize         localtime_content_len,
                   char         *file,
                   CompareFiles  compare_func)
{
        struct stat file_stat;

        if (g_stat (file, &file_stat) != 0)
                return NULL;

        if (S_ISREG (file_stat.st_mode)) {
                if (compare_func (localtime_stat,
                                  &file_stat,
                                  localtime_content,
                                  localtime_content_len,
                                  file))
                        return system_timezone_strip_path_if_valid (file);
                else
                        return NULL;
        } else if (S_ISDIR (file_stat.st_mode)) {
                GDir       *dir = NULL;
                char       *ret = NULL;
                const char *subfile = NULL;
                char       *subpath = NULL;

                dir = g_dir_open (file, 0, NULL);
                if (dir == NULL)
                        return NULL;

                while ((subfile = g_dir_read_name (dir)) != NULL) {
                        subpath = g_build_filename (file, subfile, NULL);

                        ret = recursive_compare (localtime_stat,
                                                 localtime_content,
                                                 localtime_content_len,
                                                 subpath,
                                                 compare_func);

                        g_free (subpath);

                        if (ret != NULL)
                                break;
                }

                g_dir_close (dir);

                return ret;
        }

        return NULL;
}


static gboolean
files_are_identical_inode (struct stat *a_stat,
                           struct stat *b_stat,
                           const char  *a_content,
                           gsize        a_content_len,
                           const char  *b_filename)
{
        return (a_stat->st_ino == b_stat->st_ino);
}


/* Determine if /etc/localtime is a hard link to some file, by looking at
 * the inodes */
static char *
system_timezone_read_etc_localtime_hardlink (void)
{
        struct stat stat_localtime;

        if (g_stat (ETC_LOCALTIME, &stat_localtime) != 0)
                return NULL;

        if (!S_ISREG (stat_localtime.st_mode))
                return NULL;

        return recursive_compare (&stat_localtime,
                                  NULL,
                                  0,
                                  SYSTEM_ZONEINFODIR,
                                  files_are_identical_inode);
}

static gboolean
files_are_identical_content (struct stat *a_stat,
                             struct stat *b_stat,
                             const char  *a_content,
                             gsize        a_content_len,
                             const char  *b_filename)
{
        char  *b_content = NULL;
        gsize  b_content_len = -1;
        int    cmp;

        if (a_stat->st_size != b_stat->st_size)
                return FALSE;

        if (!g_file_get_contents (b_filename,
                                  &b_content, &b_content_len, NULL))
                return FALSE;

        if (a_content_len != b_content_len) {
                g_free (b_content);
                return FALSE;
        }

        cmp = memcmp (a_content, b_content, a_content_len);
        g_free (b_content);

        return (cmp == 0);
}

/* Determine if /etc/localtime is a copy of a timezone file */
static char *
system_timezone_read_etc_localtime_content (void)
{
        struct stat  stat_localtime;
        char        *localtime_content = NULL;
        gsize        localtime_content_len = -1;
        char        *retval;

        if (g_stat (ETC_LOCALTIME, &stat_localtime) != 0)
                return NULL;

        if (!S_ISREG (stat_localtime.st_mode))
                return NULL;

        if (!g_file_get_contents (ETC_LOCALTIME,
                                  &localtime_content,
                                  &localtime_content_len,
                                  NULL))
                return NULL;

        retval = recursive_compare (&stat_localtime,
                                   localtime_content,
                                   localtime_content_len,
                                   SYSTEM_ZONEINFODIR,
                                   files_are_identical_content);

        g_free (localtime_content);

        return retval;
}

typedef char * (*GetSystemTimezone) (void);
/* The order of the functions here define the priority of the methods used
 * to find the timezone. First method has higher priority. */
static GetSystemTimezone get_system_timezone_methods[] = {
        /* cheap and "more correct" than data from a config file */
        system_timezone_read_etc_localtime_softlink,
        /* reading various config files */
        system_timezone_read_etc_timezone,
        system_timezone_read_etc_sysconfig_clock,
        system_timezone_read_etc_sysconfig_clock_alt,
        system_timezone_read_etc_TIMEZONE,
        system_timezone_read_etc_rc_conf,
        /* reading deprecated config files */
        system_timezone_read_etc_conf_d_clock,
        /* reading /etc/timezone directly. Expensive since we have to stat
         * many files */
        system_timezone_read_etc_localtime_hardlink,
        system_timezone_read_etc_localtime_content,
        NULL
};

static gboolean
system_timezone_is_valid (const char *tz)
{
        const char *c;

        if (!tz)
                return FALSE;

        for (c = tz; *c != '\0'; c++) {
                if (!(g_ascii_isalnum (*c) ||
                      *c == '/' || *c == '-' || *c == '_'))
                        return FALSE;
        }

        return TRUE;
}

static char *
system_timezone_find (void)
{
        char *tz;
        int   i;

        for (i = 0; get_system_timezone_methods[i] != NULL; i++) {
                tz = get_system_timezone_methods[i] ();

                if (system_timezone_is_valid (tz))
                        return tz;

                g_free (tz);
        }

        return g_strdup ("UTC");
}

/*
 *
 * Now, setting the timezone.
 *
 */

static gboolean
system_timezone_is_zone_file_valid (const char  *zone_file,
                                    GError     **error)
{
        GError     *our_error;
        GIOChannel *channel;
        GIOStatus   status;
        char        buffer[strlen (TZ_MAGIC)];
        gsize       read;

        /* First, check the zone_file is properly rooted */
        if (!g_str_has_prefix (zone_file, SYSTEM_ZONEINFODIR"/")) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_INVALID_TIMEZONE_FILE,
                             "Timezone file needs to be under "SYSTEM_ZONEINFODIR);
                return FALSE;
        }

        /* Second, check it's a regular file that exists */
        if (!g_file_test (zone_file, G_FILE_TEST_IS_REGULAR)) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_INVALID_TIMEZONE_FILE,
                             "No such timezone file %s", zone_file);
                return FALSE;
        }

        /* Third, check that it's a tzfile (see tzfile(5)). The file has a 4
         * bytes header which is TZ_MAGIC.
         *
         * TODO: is there glibc API for this? */
        our_error = NULL;
        channel = g_io_channel_new_file (zone_file, "r", &our_error);
        if (!our_error)
                status = g_io_channel_read_chars (channel,
                                                  buffer, strlen (TZ_MAGIC),
                                                  &read, &our_error);
        if (channel)
                g_io_channel_unref (channel);

        if (our_error) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_INVALID_TIMEZONE_FILE,
                             "Timezone file %s cannot be read: %s",
                             zone_file, our_error->message);
                g_error_free (our_error);
                return FALSE;
        }

        if (read != strlen (TZ_MAGIC) || strncmp (buffer, TZ_MAGIC, strlen (TZ_MAGIC)) != 0) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_INVALID_TIMEZONE_FILE,
                             "%s is not a timezone file",
                             zone_file);
                return FALSE;
        }

        return TRUE;
}

static gboolean
system_timezone_set_etc_timezone (const char  *zone_file,
                                  GError     **error)
{
        GError *our_error;
        char   *content;
        gsize   len;

        if (!system_timezone_is_zone_file_valid (zone_file, error))
                return FALSE;

        /* If /etc/localtime is a symlink, write a symlink */
        if (g_file_test (ETC_LOCALTIME, G_FILE_TEST_IS_SYMLINK)) {
                if (g_unlink (ETC_LOCALTIME) == 0 &&
                    symlink (zone_file, ETC_LOCALTIME) == 0)
                        return TRUE;

                /* If we couldn't symlink the file, we'll just fallback on
                 * copying it */
        }

        /* Else copy the file to /etc/localtime. We explicitly avoid doing
         * hard links since they break with different partitions */
        our_error = NULL;
        if (!g_file_get_contents (zone_file, &content, &len, &our_error)) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_GENERAL,
                             "Timezone file %s cannot be read: %s",
                             zone_file, our_error->message);
                g_error_free (our_error);
                return FALSE;
        }

        if (!g_file_set_contents (ETC_LOCALTIME, content, len, &our_error)) {
                g_set_error (error, SYSTEM_TIMEZONE_ERROR,
                             SYSTEM_TIMEZONE_ERROR_GENERAL,
                             ETC_LOCALTIME" cannot be overwritten: %s",
                             our_error->message);
                g_error_free (our_error);
                g_free (content);
                return FALSE;
        }

        g_free (content);

        return TRUE;
}

typedef gboolean (*SetSystemTimezone) (const char  *tz,
                                       GError     **error);
/* The order here does not matter too much: we'll try to change all files
 * that already have a timezone configured. It matters in case of error,
 * since the process will be stopped and the last methods won't be called.
 * So we use the same order as in get_system_timezone_methods */
static SetSystemTimezone set_system_timezone_methods[] = {
        /* writing various config files if they exist and have the
         * setting already present */
        system_timezone_write_etc_timezone,
        system_timezone_write_etc_sysconfig_clock,
        system_timezone_write_etc_sysconfig_clock_alt,
        system_timezone_write_etc_TIMEZONE,
        system_timezone_write_etc_rc_conf,
        /* writing deprecated config files if they exist and have the
         * setting already present */
        system_timezone_write_etc_conf_d_clock,
        NULL
};

static gboolean
system_timezone_update_config (const char  *tz,
                               GError     **error)
{
        int i;

        for (i = 0; set_system_timezone_methods[i] != NULL; i++) {
                if (!set_system_timezone_methods[i] (tz, error))
                        return FALSE;
                /* FIXME: maybe continue to change all config files if
                 * possible? */
        }

        return TRUE;
}

gboolean
system_timezone_set_from_file (const char  *zone_file,
                               GError     **error)
{
        const char *tz;

        g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

        tz = zone_file + strlen (SYSTEM_ZONEINFODIR"/");

        /* FIXME: is it right to return FALSE even when /etc/localtime was
         * changed but not the config files? */
        return (system_timezone_set_etc_timezone (zone_file, error) &&
                system_timezone_update_config (tz, error));
}

gboolean
system_timezone_set (const char  *tz,
                     GError     **error)
{
        char     *zone_file;
        gboolean  retval;

        g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

        zone_file = g_build_filename (SYSTEM_ZONEINFODIR, tz, NULL);

        /* FIXME: is it right to return FALSE even when /etc/localtime was
         * changed but not the config files? */
        retval = system_timezone_set_etc_timezone (zone_file, error) &&
                 system_timezone_update_config (tz, error);

        g_free (zone_file);

        return retval;
}

GQuark
system_timezone_error_quark (void)
{
        static GQuark ret = 0;

        if (ret == 0) {
                ret = g_quark_from_static_string ("system-timezone-error");
        }

        return ret;
}
