/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
 * gnome-timezone.c: Get and set the system timezone
 *
 * Copyright (C) 2004 William Jon McCann <mccann@jhu.edu>
 *
 * 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.
 *
 */

#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <ctype.h>
#include <signal.h>
#if defined(__linux__)
# include <stdint.h>
#endif
#include "getopt.h"

#include <glib.h>
#include <glib/gprintf.h>

#ifdef HAVE_DBUS
#define DBUS_API_SUBJECT_TO_CHANGE
#include <dbus/dbus.h>
#include <dbus/dbus-glib.h>
#endif

#ifndef ETC_LOCALTIME
#if defined(__osf__)
# define ETC_LOCALTIME       "/etc/zoneinfo/localtime"
#else
# define ETC_LOCALTIME       "/etc/localtime"
#endif
#endif
#ifndef ETC_TIMEZONE1
# define ETC_TIMEZONE1       "/etc/timezone"
#endif
#ifndef ETC_TIMEZONE2
# define ETC_TIMEZONE2       "/etc/TIMEZONE"
#endif
#ifndef ETC_SYSCONFIG_CLOCK
# define ETC_SYSCONFIG_CLOCK "/etc/sysconfig/clock"
#endif
#ifndef ETC_DEFAULT_INIT
# define ETC_DEFAULT_INIT    "/etc/default/init"
#endif

#ifndef ZONEINFO_DIR
#if defined(__linux__) || defined(__FreeBSD__)
# define ZONEINFO_DIR        "/usr/share/zoneinfo"
#elif defined(sun)
# define ZONEINFO_DIR        "/usr/share/lib/zoneinfo"
#elif defined(__osf__)
# define ZONEINFO_DIR        "/etc/zoneinfo"
#else
# define ZONEINFO_DIR        "/usr/share/zoneinfo"
#endif
#endif

#define COPY_CMD             "/bin/cp"

#ifndef O_BINARY
# define O_BINARY            0
#endif

#ifndef word
# define word                uintmax_t
#endif

#ifndef EXIT_SUCCESS
# define EXIT_SUCCESS        0
#endif
#if !EXIT_FAILURE
# undef EXIT_FAILURE /* Sony NEWS-OS 4.0C defines EXIT_FAILURE to 0.  */
# define EXIT_FAILURE        1
#endif
#define EXIT_TROUBLE         2

#define DEBUG(x)

static int cmp               (int fd1, int fd2);

static int opt_v;

typedef struct {
        char     *(*get_function) (const char *filename);
        int       (*set_function) (const char *filename,
                                   char       *name);
        const char *filename;
        void       *reserved1;
} ZoneMethodInfo;

#define ZONE_METHOD_ITEM(get, set, filename) { get, set, filename, NULL }

/*
 * The following two functions are only needed on systems
 * that have a glib version before 2.4
 */
#if GLIB_CHECK_VERSION(2,4,0)
#else
gchar *
g_file_read_link (const gchar *filename,
                  GError     **error)
{
  gchar *buffer;
  guint size;
  gint read_size;

  size = 256;
  buffer = g_malloc (size);

  while (TRUE)
    {
      read_size = readlink (filename, buffer, size);
      if (read_size < 0) {
        g_free (buffer);
        return NULL;
      }

      if (read_size < size)
        {
          buffer[read_size] = 0;
          return buffer;
        }

      size *= 2;
      buffer = g_realloc (buffer, size);
    }
}
#endif

#ifdef HAVE_DBUS
static gboolean
send_notification (const char *name)
{
        DBusConnection *bus;
        DBusError       error;        
        DBusMessage    *message;
        int             result;

#define GNOME_TZ_DBUS_PATH      "/org/gnome/timezone"
#define GNOME_TZ_DBUS_INTERFACE "org.gnome.timezone"
#define GNOME_TZ_DBUS_NAME      "changed"

        dbus_error_init (&error);
        bus = dbus_bus_get (DBUS_BUS_SYSTEM, &error);
        if (!bus) {
                fprintf (stderr, "Failed to connect to the D-BUS daemon: %s\n", error.message);
                dbus_error_free (&error);
                return FALSE;
        }

        message = dbus_message_new_signal (GNOME_TZ_DBUS_PATH,
                                           GNOME_TZ_DBUS_INTERFACE,
                                           GNOME_TZ_DBUS_NAME);
        if (!message) {
                fprintf (stderr, "Not enough memory for new dbus message!\n");
                return FALSE;
        }

        dbus_message_append_args (message,
                                  DBUS_TYPE_STRING, name,
                                  DBUS_TYPE_INVALID);

        result = dbus_connection_send (bus, message, NULL);
        if (!result)
                fprintf (stderr, "Could not send the timezone.changed signal\n");
        else 
                dbus_connection_flush (bus);
        
        dbus_message_unref (message);
        dbus_connection_disconnect (bus);

        return result;
}
#endif

static char *
get_zonefile_path_from_name (const char *name,
                             const char *zonedir)
{
        char *path;

        path = g_build_filename (zonedir, name, NULL);

        return path;
}

static int
could_be_valid_time_zone (const char *zone)
{
        char *path;

        if (!zone)
                return FALSE;

        if (!strcmp (zone, "local"))
                return FALSE;

        path = get_zonefile_path_from_name (zone, ZONEINFO_DIR);
        if (!g_file_test (path, G_FILE_TEST_EXISTS)) {
                g_free (path);
                return FALSE;
        }
        g_free (path);

        return TRUE;
}

static int
compare_node (const char *file1,
              const char *file2)
{
        struct stat sb1;
        struct stat sb2;

        g_return_val_if_fail (file1 != NULL, -1);
        g_return_val_if_fail (file2 != NULL, -1);

        if (stat (file1, &sb1) < 0)
                return -1;
        if (stat (file2, &sb2) < 0)
                return -1;

        if ((sb1.st_dev == sb2.st_dev) &&
            (sb1.st_ino == sb2.st_ino))
                return 0;               /* Files are the same */

        if (sb1.st_size > sb2.st_size)  /* File 1 is longer */
                return 1;
        if (sb1.st_size < sb2.st_size)  /* File 2 is longer */
                return 2;

        return 3;                       /* Files may differ */
}

static int
compare_files (const char *file1,
               const char *file2)
{
        int fd1, fd2;
        int res;

        fd1 = open (file1, O_RDONLY|O_BINARY);
        if (fd1 < 0)
                return FALSE;

        fd2 = open (file2, O_RDONLY|O_BINARY);
        if (fd2 < 0) {
                close (fd1);
                return FALSE;
        }

        DEBUG(fprintf (stderr, "** comparing: '%s' '%s'\n", file1, file2));
        res = cmp (fd1, fd2);
        close (fd1);
        close (fd2);

        return res;
}

static char *
search_dir (const char *file,
            const char *path)
{
	GDir       *dir;
	const char *d;
	char       *new  = NULL;
	char       *name = NULL;
	struct stat sb1;
        struct stat sb2;

        if (stat (file, &sb1) < 0)
                return NULL;

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

	while ( (d = g_dir_read_name (dir)) ) {
		new = g_build_filename (path, d, NULL);
                DEBUG(fprintf (stderr, "** checking '%s'\n", new));
		if (stat (new, &sb2) == -1) {
                        g_free (new);
			continue;
                }

		if (S_ISDIR (sb2.st_mode)) {
			name = search_dir (file, new);
                        if (name)
                                break;
		} else {
                        if ((sb1.st_dev == sb2.st_dev) &&
                            (sb1.st_ino == sb2.st_ino)) {
                                name = new;
                                break;
                        } else if (sb1.st_size != sb2.st_size) {
                                g_free (new);
                                continue;
                        } else if (compare_files (file, new) == EXIT_SUCCESS) {
                                DEBUG(fprintf (stderr, "Matched\n"));
                                name = new;
                                break;
                        }
		}
		g_free (new);
		new = NULL;
	}

	g_dir_close (dir);

	return name;
}

static char *
find_matching_zoneinfo_file (const char *needle,
                             const char *haystack)
{
        int   i;
        char *name = NULL;
        const char *search_order[] = { "America",
                                       "Europe",
                                       "Asia",
                                       "Australia",
                                       "Indian",
                                       "Pacific",
                                       "Atlantic",
                                       "Africa",
                                       "Antarctica",
                                       NULL };

        if (!g_file_test (haystack, G_FILE_TEST_IS_DIR) ||
            !g_file_test (haystack, G_FILE_TEST_EXISTS))
                return NULL;
        
        for (i = 0; search_order[i] != NULL; i++) {
                char *path = g_build_filename (haystack,
                                               search_order[i],
                                               NULL);
                name = search_dir (needle, path);
                g_free (path);
                
                if (name)
                        break;
        }

        return name;
}

static char *
get_name_from_zonefile (const char *filename,
                        const char *zonedir)
{
        char *name  = NULL;
        char *file1 = g_path_get_basename (filename);
        char *path1 = g_path_get_dirname (filename);
        
        /* zone files seem to only go 3 deep */
        if (compare_node (path1, zonedir) == 0) {
                name = g_strdup (file1);
        } else {
                char *file2 = g_path_get_basename (path1);
                char *path2 = g_path_get_dirname (path1);
                
                if (compare_node (path2, zonedir) == 0) {
                        name = g_strjoin ("/", file2, file1, NULL);
                } else {
                        char *file3 = g_path_get_basename (path2);
                        char *path3 = g_path_get_dirname (path2);
                        
                        if (compare_node (path3, zonedir) == 0) {
                                name = g_strjoin ("/", file3, file2, file1, NULL);
                        }
                        g_free (path3);
                        g_free (file3);
                }
                g_free (path2);
                g_free (file2);
        }
        g_free (path1);
        g_free (file1);
        
        return name;
}

static char *
get_name_by_etc_localtime_copy (const char *filename)
{
        char *name     = NULL;
        char *zonefile = NULL;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_EXISTS))
                return NULL;

        zonefile = find_matching_zoneinfo_file (filename, ZONEINFO_DIR);
        if (!zonefile)
                return NULL;

        name = get_name_from_zonefile (zonefile, ZONEINFO_DIR);
        return name;
}

static char *
get_name_by_etc_localtime_symlink (const char *filename)
{
        char *name     = NULL;
        char *zonefile = NULL;
        char *temp1;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_EXISTS))
                return NULL;

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

        temp1 = g_file_read_link (filename, NULL);
        if (!g_path_is_absolute (temp1)) {
                char *temp2;
                temp2 = g_path_get_dirname (filename);
                zonefile = g_build_filename (temp2, temp1, NULL);
                g_free (temp2);
        } else {
                zonefile = g_strdup (temp1);
        }
        g_free (temp1);

        if (!zonefile)
                return NULL;

        name = get_name_from_zonefile (zonefile, ZONEINFO_DIR);

        g_free (zonefile);

        return name;
}

static char *
get_name_by_etc_timezone1 (const char *filename)
{
        char  *name     = NULL;
        char  *contents = NULL;
        char  *line     = NULL;
        gsize  length;
        int    i;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR)
            || !g_file_test (filename, G_FILE_TEST_EXISTS))
                return NULL;

        if (!g_file_get_contents (filename,
                                  &contents,
                                  &length,
                                  NULL))
                return NULL;

        for (line = strtok (contents, "\n"); line; line = strtok (NULL, "\n")) {
                char *startpos = NULL;
                char *endpos   = NULL;

                for (i = 0; i < strlen (line); i++) {
                        if (line[i] == '#')
                                break;
                        if (isspace (line[i])) {
                                if (!startpos) {
                                        continue;
                                } else {
                                        endpos = line + i;
                                        break;
                                }
                        }
                        if (!startpos)
                                startpos = line + i;
                }
                if (startpos) {
                        if (!endpos)
                                name = g_strdup (startpos);
                        else
                                name = g_strndup (startpos, endpos - startpos);
                        break;
                }
        }

        g_free (contents);

        return name;
}

static char *
get_name_by_etc_timezone2 (const char *filename)
{
        char       *name     = NULL;
        char       *contents = NULL;
        const char *startpos;
        const char *endpos;
        gsize       length;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_EXISTS))
                return NULL;

        if (!g_file_get_contents (filename,
                                  &contents,
                                  &length,
                                  NULL))
                return NULL;

        startpos = strstr (contents, "TZ=");
        if (!startpos) {
                g_free (contents);
                return NULL;
        }
        startpos += 3;

        endpos = strchr (startpos, '\n');
        if (!endpos)
                name = g_strdup (startpos);
        else
                name = g_strndup (startpos, endpos - startpos);

        name = g_strstrip (name);

        g_free (contents);
        return name;
}

static char *
get_name_by_etc_sysconfig_clock (const char *filename)
{
        char       *name     = NULL;
        char       *contents = NULL;
        const char *startpos;
        const char *endpos;
        gsize       length;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_EXISTS))
                return NULL;

        if (!g_file_get_contents (filename,
                                  &contents,
                                  &length,
                                  NULL))
                return NULL;

        startpos = strstr (contents, "ZONE=\"");
        if (!startpos) {
                g_free (contents);
                return NULL;
        }
        startpos += 6;

        endpos = strchr (startpos, '"');
        if (!endpos) {
                g_free (contents);
                return NULL;
        }

        name = g_strstrip (g_strndup (startpos, endpos - startpos));
        g_free (contents);
        return name;   
}

static int
write_file_contents (const char *filename,
                     char       *contents)
{
        FILE *f;
        
        f = fopen (filename, "w");
        
        if (!f) {
                perror ("Couldn't open file for writing");
                return FALSE;
        }

	if (fprintf (f, "%s", contents) < strlen (contents)) {
                perror ("Error writing to file");
                fclose (f);
                return FALSE;
        }

	fclose (f);
	return TRUE;
}

static int
set_name_by_etc_localtime_symlink (const char *filename,
                                   char       *name)
{
        char *zonepath = NULL;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_IS_SYMLINK))
                return FALSE;

        if (opt_v)
                fprintf (stderr, "** Unlinking '%s'\n", filename);
        if (unlink (filename) < 0) {
                perror ("Could not unlink file");
                return FALSE;
        }

        zonepath = get_zonefile_path_from_name (name, ZONEINFO_DIR);
        if (!zonepath)
                return FALSE;

        if (opt_v)
                fprintf (stderr, "** Linking '%s' -> '%s'\n", filename, zonepath);
        if (symlink (zonepath, filename) < 0) {
                g_free (zonepath);
                perror ("Could not create symbolic link");
                return FALSE;
        }

        g_free (zonepath);
        return TRUE;
}

static int
set_name_by_etc_timezone1 (const char *filename,
                           char       *name)
{
        char  *contents     = NULL;
        char  *new_contents = NULL;
        char  *line         = NULL;
        gsize  length;
        int    i;
        int    res;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR)
            || !g_file_test (filename, G_FILE_TEST_EXISTS))
                return FALSE;

        if (!g_file_get_contents (filename,
                                  &contents,
                                  &length,
                                  NULL))
                return FALSE;

        for (line = strtok (contents, "\n"); line; line = strtok (NULL, "\n")) {
                char *startpos = NULL;
                char *endpos   = NULL;
                char *temp     = NULL;

                for (i = 0; i < strlen (line); i++) {
                        if (line[i] == '#')
                                break;
                        if (isspace (line[i])) {
                                if (!startpos) {
                                        continue;
                                } else {
                                        endpos = line + i;
                                        break;
                                }
                        }
                        if (!startpos)
                                startpos = line + i;
                }

                if (new_contents) {
                        temp = g_strdup (new_contents);
                        g_free (new_contents);
                }

                if (startpos) {
                        char *pre_string  = g_strndup (line, startpos - line);
                        char *post_string = NULL;

                        if (endpos)
                                post_string = g_strdup_printf ("%s\n", endpos);
                        else
                                post_string = g_strdup ("\n");

                        if (temp)
                                new_contents = g_strconcat (temp, pre_string, name, post_string, NULL);
                        else
                                new_contents = g_strconcat (pre_string, name, post_string, NULL);

                        g_free (post_string);
                        g_free (pre_string);
                } else {
                        if (temp)
                                new_contents = g_strconcat (temp, line, "\n", NULL);
                        else
                                new_contents = g_strdup_printf ("%s\n", line);
                }
                g_free (temp);
        }

        res = write_file_contents (filename, new_contents);

        g_free (new_contents);
        g_free (contents);

        return res;
}

static int
set_name_by_etc_timezone2 (const char *filename,
                           char       *name)
{
        char       *contents    = NULL;
        char       *pre_string  = NULL;
        char       *post_string = NULL;
        const char *startpos;
        const char *endpos;
        gsize       length;
        int         res;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_EXISTS))
                return FALSE;

        if (!g_file_get_contents (filename,
                                  &contents,
                                  &length,
                                  NULL))
                return FALSE;

        startpos = strstr (contents, "TZ=");
        if (!startpos) {
                g_free (contents);
                return FALSE;
        }
        startpos += 3;

        pre_string = g_strndup (contents, startpos - contents);

        endpos = strchr (startpos, '\n');
        if (endpos)
                post_string = g_strdup (endpos);
        
        g_free (contents);

        contents = g_strconcat (pre_string, name, post_string, NULL);

        res = write_file_contents (filename, contents);

        g_free (contents);

        return res;
}

static int
set_name_by_etc_sysconfig_clock (const char *filename,
                                 char       *name)
{
        char       *contents = NULL;
        char       *pre_string;
        char       *post_string;
        const char *startpos;
        const char *endpos;
        gsize       length;
        int         res;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR) ||
            !g_file_test (filename, G_FILE_TEST_EXISTS))
                return FALSE;

        if (!g_file_get_contents (filename,
                                  &contents,
                                  &length,
                                  NULL))
                return FALSE;

        startpos = strstr (contents, "ZONE=\"");
        if (!startpos) {
                g_free (contents);
                return FALSE;
        }
        startpos += 6;

        endpos = strchr (startpos, '"');
        if (!endpos) {
                g_free (contents);
                return FALSE;
        }

        pre_string = g_strndup (contents, startpos - contents);
        post_string = g_strdup (endpos);
        g_free (contents);

        contents = g_strconcat (pre_string, name, post_string, NULL);

        res = write_file_contents (filename, contents);

        g_free (contents);
        return res;
}

static int
copy_reg_file (const char *src_path,
               const char *dst_path)
{
        char *argv[4] = { COPY_CMD, NULL, NULL, NULL };
        char *output;
        char *error;
        int   exitcode;

        argv[1] = g_strdup (src_path);
        argv[2] = g_strdup (dst_path);

        if( g_spawn_sync ("/",
                          argv,
                          NULL,
                          0,
                          NULL,
                          NULL,
                          &output,
                          &error,
                          &exitcode,
                          NULL) !=TRUE ) {
                perror ("Couldn't copy file");
                g_free (argv[1]);
                g_free (argv[2]);

                return FALSE;
        }

        g_free (argv[1]);
        g_free (argv[2]);
        return TRUE;
}

static int
set_name_by_etc_localtime_copy (const char *filename,
                                char       *name)
{
        char *zonepath = NULL;

        if (g_file_test (filename, G_FILE_TEST_IS_DIR))
                return FALSE;

        if (opt_v)
                fprintf (stderr, "** Unlinking '%s'\n", filename);
        if (unlink (filename) < 0) {
                perror ("Could not unlink file");
                return FALSE;
        }

        zonepath = get_zonefile_path_from_name (name, ZONEINFO_DIR);
        if (!zonepath)
                return FALSE;

        if (opt_v)
                fprintf (stderr, "** Copying '%s' to '%s'\n", zonepath, filename);

        if (copy_reg_file (zonepath, filename) < 0) {
                g_free (zonepath);
                perror ("Could not copy file");
                return FALSE;
        }

        g_free (zonepath);
        return TRUE;
}

static char *
get_timezone_name (ZoneMethodInfo *methods)
{
        for (; methods->filename; methods++) {
                char *name = NULL;
                if (opt_v > 1)
                        fprintf (stderr, "** Trying to get zone from '%s'\n", methods->filename);
                name = (char *)methods->get_function (methods->filename);
                if (could_be_valid_time_zone (name))
                        return name;
                g_free (name);
        }
        
        return NULL;
}

static void
display_timezone_name (ZoneMethodInfo *methods)
{
        char *name = NULL;

        name = get_timezone_name (methods);
        if (name)
                printf ("%s\n", name);
}

static int
set_timezone_name (ZoneMethodInfo *methods,
                   char           *name)
{

        if (!could_be_valid_time_zone (name)) {
                fprintf (stderr, "Not a valid timezone: '%s'\n", name);
                return FALSE;
        }

        if (opt_v)
                fprintf (stderr, "** Setting system timezone to '%s'...\n", name);

        for (; methods->filename; methods++) {
                char *current_zone = NULL;
                int   res = TRUE;
                current_zone = (char *)methods->get_function (methods->filename);
                if (could_be_valid_time_zone (current_zone) && strcmp (current_zone, name) != 0) {
                        if (opt_v > 1)
                                fprintf (stderr, "** Trying to set zone in '%s'\n", methods->filename);
                        res = (int)methods->set_function (methods->filename, name);
                }
                g_free (current_zone);
                if (!res)
                        return FALSE;
        }

#ifdef HAVE_DBUS
        send_notification (name);
#endif

        return TRUE;
}

static void
usage (void)
{
        char *prog = g_get_prgname ();

        fprintf (stderr, "Usage: %s [-v]                         display timezone\n", prog);
        fprintf (stderr, "       %s [-v] {timezone}              set timezome\n\n", prog);
        fprintf (stderr, "       %s -V|--version|-h|--help       print info and exit\n\n", prog);
        exit (1);
}

static void
version (void)
{
        exit (0);
}

int
main (int argc, char **argv)
{
        int c;
        int option_index = 0;
        static const struct option long_options[] = {
                {"help", no_argument, 0, 'h'},
                {"version", no_argument, 0, 'V'},
                {"verbose", no_argument, 0, 'v'},
                {"dry-run", no_argument, 0, 'n'},
                {0, 0, 0, 0}
        };
        ZoneMethodInfo  methods[] = {
                ZONE_METHOD_ITEM (get_name_by_etc_localtime_symlink,
                                  set_name_by_etc_localtime_symlink,
                                  ETC_LOCALTIME),
                ZONE_METHOD_ITEM (get_name_by_etc_timezone1,
                                  set_name_by_etc_timezone1,
                                  ETC_TIMEZONE1),
                ZONE_METHOD_ITEM (get_name_by_etc_timezone2,
                                  set_name_by_etc_timezone2,
                                  ETC_TIMEZONE2),
                ZONE_METHOD_ITEM (get_name_by_etc_sysconfig_clock,
                                  set_name_by_etc_sysconfig_clock,
                                  ETC_SYSCONFIG_CLOCK),
                ZONE_METHOD_ITEM (get_name_by_etc_timezone2,
                                  set_name_by_etc_timezone2,
                                  ETC_DEFAULT_INIT),
                ZONE_METHOD_ITEM (get_name_by_etc_localtime_copy,
                                  set_name_by_etc_localtime_copy,
                                  ETC_LOCALTIME),
                ZONE_METHOD_ITEM (NULL, NULL, NULL)
        };

        while ((c = getopt_long (argc, argv, "hVv", long_options, &option_index)) != EOF)
                switch (c) {
                case 'v':
                        opt_v++;
                        break;
                case 'V':
                        version ();
                case '?':
                case 'h':
                default:
                        usage ();
                        break;
                };

	if (optind < argc) {
                if (!set_timezone_name (methods, argv[optind]))
                        exit (1);
        } else {
                display_timezone_name (methods);
        }

        return 0;
}


/*
 *  The following functions are based on code from GNU diffutils
 *
 *   Copyright (C) 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1998, 2001,
 *   2002 Free Software Foundation, Inc.
 */

/* Read NBYTES bytes from descriptor FD into BUF.
   NBYTES must not be SIZE_MAX.
   Return the number of characters successfully read.
   On error, return SIZE_MAX, setting errno.
   The number returned is always NBYTES unless end-of-file or error.  */

static size_t
block_read (int fd, char *buf, size_t nbytes)
{
        char       *bp      = buf;
        char const *buflim  = buf + nbytes;
        size_t      readlim = SSIZE_MAX;

        do {
                size_t  bytes_to_read = MIN (buflim - bp, readlim);
                ssize_t nread         = read (fd, bp, bytes_to_read);
                if (nread <= 0) {
                        if (nread == 0)
                                break;

                        /* Accommodate Tru64 5.1, which can't read more than INT_MAX
                           bytes at a time.  They call that a 64-bit OS?  */
                        if (errno == EINVAL && INT_MAX < bytes_to_read) {
                                readlim = INT_MAX;
                                continue;
                        }

                        /* This is needed for programs that have signal handlers on
                           older hosts without SA_RESTART.  It also accommodates
                           ancient AIX hosts that set errno to EINTR after uncaught
                           SIGCONT.  See <news:1r77ojINN85n@ftp.UU.NET>
                           (1993-04-22).  */
                        if (! SA_RESTART && errno == EINTR)
                                continue;

                        return SIZE_MAX;
                }
                bp += nread;
        }
        while (bp < buflim);

        return bp - buf;
}

/* Compare two blocks of memory P0 and P1 until they differ.
   If the blocks are not guaranteed to be different, put sentinels at the ends
   of the blocks before calling this function.

   Return the offset of the first byte that differs.  */

static size_t
block_compare (word const *p0, word const *p1)
{
        word const *l0, *l1;
        char const *c0, *c1;

        /* Find the rough position of the first difference by reading words,
           not bytes.  */

        for (l0 = p0, l1 = p1;  *l0 == *l1;  l0++, l1++)
                continue;

        /* Find the exact differing position (endianness independent).  */

        for (c0 = (char const *) l0, c1 = (char const *) l1;
             *c0 == *c1;
             c0++, c1++)
                continue;

        return c0 - (char const *) p0;
}

static int
cmp (int fd1,
     int fd2)
{
        off_t     byte_number = 1;
        uintmax_t remaining   = UINTMAX_MAX;	/* Remaining number of bytes to compare.  */
        size_t    read0, read1;	                /* Number of bytes read from each file. */
        size_t    first_diff;	                /* Offset (0...) in buffers of 1st diff. */
        size_t    smaller;                      /* The lesser of `read0' and `read1'. */
        word     *buffer0;
        word     *buffer1;
        char     *buf0;
        char     *buf1;
        int       ret = EXIT_SUCCESS;
        int       buf_size = 8 * 1024;
        int       words_per_buffer = (buf_size + 2 * sizeof (word) - 1) / sizeof (word);

        buffer0 = (word *) g_malloc (2 * sizeof (word) * words_per_buffer);
        buffer1 = buffer0 + words_per_buffer;
        buf0 = (char *) buffer0;
        buf1 = (char *) buffer1;

        do {
                size_t bytes_to_read = buf_size;

                if (remaining != UINTMAX_MAX) {
                        if (remaining < bytes_to_read)
                                bytes_to_read = remaining;
                        remaining -= bytes_to_read;
                }

                read0 = block_read (fd1, buf0, bytes_to_read);
                if (read0 == SIZE_MAX)
                        return EXIT_TROUBLE;
                read1 = block_read (fd2, buf1, bytes_to_read);
                if (read1 == SIZE_MAX)
                        return EXIT_TROUBLE;

                /* Insert sentinels for the block compare.  */

                buf0[read0] = ~buf1[read0];
                buf1[read1] = ~buf0[read1];

                /* If the line number should be written for differing files,
                   compare the blocks and count the number of newlines
                   simultaneously.  */
                first_diff = block_compare (buffer0, buffer1);

                byte_number += first_diff;
                smaller = MIN (read0, read1);

                if (first_diff < smaller)
                        return EXIT_FAILURE;

                if (read0 != read1)
                        return EXIT_FAILURE;
        }
        while (read0 == buf_size);

        g_free (buffer0);

        return ret;
}
