tzsetup.c revision 133425
1/*
2 * Copyright 1996 Massachusetts Institute of Technology
3 *
4 * Permission to use, copy, modify, and distribute this software and
5 * its documentation for any purpose and without fee is hereby
6 * granted, provided that both the above copyright notice and this
7 * permission notice appear in all copies, that both the above
8 * copyright notice and this permission notice appear in all
9 * supporting documentation, and that the name of M.I.T. not be used
10 * in advertising or publicity pertaining to distribution of the
11 * software without specific, written prior permission.  M.I.T. makes
12 * no representations about the suitability of this software for any
13 * purpose.  It is provided "as is" without express or implied
14 * warranty.
15 *
16 * THIS SOFTWARE IS PROVIDED BY M.I.T. ``AS IS''.  M.I.T. DISCLAIMS
17 * ALL EXPRESS OR IMPLIED WARRANTIES WITH REGARD TO THIS SOFTWARE,
18 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT
20 * SHALL M.I.T. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
26 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 * SUCH DAMAGE.
28 */
29
30/*
31 * Second attempt at a `tzmenu' program, using the separate description
32 * files provided in newer tzdata releases.
33 */
34
35#ifndef lint
36static const char rcsid[] =
37  "$FreeBSD: head/usr.sbin/tzsetup/tzsetup.c 133425 2004-08-10 05:34:23Z murray $";
38#endif /* not lint */
39
40#include <sys/types.h>
41#include <dialog.h>
42#include <err.h>
43#include <errno.h>
44#include <stdio.h>
45#include <stdlib.h>
46#include <string.h>
47#include <time.h>
48#include <unistd.h>
49
50#include <sys/fcntl.h>
51#include <sys/queue.h>
52#include <sys/stat.h>
53
54#include "paths.h"
55
56static int reallydoit = 1;
57
58static int continent_country_menu(dialogMenuItem *);
59static int set_zone_multi(dialogMenuItem *);
60static int set_zone_whole_country(dialogMenuItem *);
61static int set_zone_menu(dialogMenuItem *);
62
63struct continent {
64	dialogMenuItem *menu;
65	int nitems;
66	int ch;
67	int sc;
68};
69
70static struct continent africa, america, antarctica, arctic, asia, atlantic;
71static struct continent australia, europe, indian, pacific;
72
73static struct continent_names {
74	char *name;
75	struct continent *continent;
76} continent_names[] = {
77	{ "Africa", &africa }, { "America", &america },
78	{ "Antarctica", &antarctica }, { "Arctic", &arctic },
79	{ "Asia", &asia },
80	{ "Atlantic", &atlantic }, { "Australia", &australia },
81	{ "Europe", &europe }, { "Indian", &indian }, { "Pacific", &pacific }
82};
83
84static dialogMenuItem continents[] = {
85	{ "1", "Africa", 0, continent_country_menu, 0, &africa },
86	{ "2", "America -- North and South", 0, continent_country_menu, 0,
87		  &america },
88	{ "3", "Antarctica", 0, continent_country_menu, 0, &antarctica },
89	{ "4", "Arctic Ocean", 0, continent_country_menu, 0, &arctic },
90	{ "5", "Asia", 0, continent_country_menu, 0, &asia },
91	{ "6", "Atlantic Ocean", 0, continent_country_menu, 0, &atlantic },
92	{ "7", "Australia", 0, continent_country_menu, 0, &australia },
93	{ "8", "Europe", 0, continent_country_menu, 0, &europe },
94	{ "9", "Indian Ocean", 0, continent_country_menu, 0, &indian },
95	{ "0", "Pacific Ocean", 0, continent_country_menu, 0, &pacific }
96};
97#define NCONTINENTS (int)((sizeof continents)/(sizeof continents[0]))
98#define OCEANP(x) ((x) == 3 || (x) == 5 || (x) == 8 || (x) == 9)
99
100static int
101continent_country_menu(dialogMenuItem *continent)
102{
103	int rv;
104	struct continent *contp = continent->data;
105	char title[256];
106	int isocean = OCEANP(continent - continents);
107	int menulen;
108
109	/* Short cut -- if there's only one country, don't post a menu. */
110	if (contp->nitems == 1) {
111		return set_zone_menu(&contp->menu[0]);
112	}
113
114	/* It's amazing how much good grammar really matters... */
115	if (!isocean)
116		snprintf(title, sizeof title, "Countries in %s",
117			 continent->title);
118	else
119		snprintf(title, sizeof title, "Islands and groups in the %s",
120			 continent->title);
121
122	menulen = contp->nitems < 16 ? contp->nitems : 16;
123	rv = dialog_menu(title, (isocean ? "Select an island or group"
124                                 : "Select a country or region"), -1, -1, menulen,
125			 -contp->nitems, contp->menu, 0, &contp->ch,
126			 &contp->sc);
127	if (rv == 0)
128		return DITEM_LEAVE_MENU;
129	return DITEM_RECREATE;
130}
131
132static struct continent *
133find_continent(const char *name)
134{
135	int i;
136
137	for (i = 0; i < NCONTINENTS; i++) {
138		if (strcmp(name, continent_names[i].name) == 0)
139			return continent_names[i].continent;
140	}
141	return 0;
142}
143
144struct country {
145	char *name;
146	char *tlc;
147	int nzones;
148	char *filename;		/* use iff nzones < 0 */
149	struct continent *continent; /* use iff nzones < 0 */
150	TAILQ_HEAD(, zone) zones; /* use iff nzones > 0 */
151	dialogMenuItem *submenu; /* use iff nzones > 0 */
152};
153
154struct zone {
155	TAILQ_ENTRY(zone) link;
156	char *descr;
157	char *filename;
158	struct continent *continent;
159};
160
161/*
162 * This is the easiest organization... we use ISO 3166 country codes,
163 * of the two-letter variety, so we just size this array to suit.
164 * Beats worrying about dynamic allocation.
165 */
166#define NCOUNTRIES	(26*26)
167static struct country countries[NCOUNTRIES];
168#define CODE2INT(s) ((s[0] - 'A') * 26 + (s[1] - 'A'))
169
170/*
171 * Read the ISO 3166 country code database in _PATH_ISO3166
172 * (/usr/share/misc/iso3166).  On error, exit via err(3).
173 */
174static void
175read_iso3166_table(void)
176{
177	FILE *fp;
178	char *s, *t, *name;
179	size_t len;
180	int lineno;
181	struct country *cp;
182
183	fp = fopen(_PATH_ISO3166, "r");
184	if (!fp)
185		err(1, _PATH_ISO3166);
186	lineno = 0;
187
188	while ((s = fgetln(fp, &len)) != 0) {
189		lineno++;
190		if (s[len - 1] != '\n')
191			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
192		s[len - 1] = '\0';
193		if (s[0] == '#' || strspn(s, " \t") == len - 1)
194			continue;
195
196		/* Isolate the two-letter code. */
197		t = strsep(&s, "\t");
198		if (t == 0 || strlen(t) != 2)
199			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
200		if (t[0] < 'A' || t[0] > 'Z' || t[1] < 'A' || t[1] > 'Z')
201			errx(1, _PATH_ISO3166 ":%d: invalid code `%s'",
202			     lineno, t);
203
204		/* Now skip past the three-letter and numeric codes. */
205		name = strsep(&s, "\t"); /* 3-let */
206		if (name == 0 || strlen(name) != 3)
207			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
208		name = strsep(&s, "\t"); /* numeric */
209		if (name == 0 || strlen(name) != 3)
210			errx(1, _PATH_ISO3166 ":%d: invalid format", lineno);
211
212		name = s;
213
214		cp = &countries[CODE2INT(t)];
215		if (cp->name)
216			errx(1, _PATH_ISO3166
217			     ":%d: country code `%s' multiply defined: %s",
218			     lineno, t, cp->name);
219		cp->name = strdup(name);
220		if (cp->name == NULL)
221			errx(1, "malloc failed");
222		cp->tlc = strdup(t);
223		if (cp->tlc == NULL)
224			errx(1, "malloc failed");
225	}
226
227	fclose(fp);
228}
229
230static void
231add_zone_to_country(int lineno, const char *tlc, const char *descr,
232		    const char *file, struct continent *cont)
233{
234	struct zone *zp;
235	struct country *cp;
236
237	if (tlc[0] < 'A' || tlc[0] > 'Z' || tlc[1] < 'A' || tlc[1] > 'Z')
238		errx(1, _PATH_ZONETAB ":%d: country code `%s' invalid",
239		     lineno, tlc);
240
241	cp = &countries[CODE2INT(tlc)];
242	if (cp->name == 0)
243		errx(1, _PATH_ZONETAB ":%d: country code `%s' unknown",
244		     lineno, tlc);
245
246	if (descr) {
247		if (cp->nzones < 0)
248			errx(1, _PATH_ZONETAB
249			     ":%d: conflicting zone definition", lineno);
250
251		zp = malloc(sizeof *zp);
252		if (zp == 0)
253			errx(1, "malloc(%lu)", (unsigned long)sizeof *zp);
254
255		if (cp->nzones == 0)
256			TAILQ_INIT(&cp->zones);
257
258		zp->descr = strdup(descr);
259		if (zp->descr == NULL)
260			errx(1, "malloc failed");
261		zp->filename = strdup(file);
262		if (zp->filename == NULL)
263			errx(1, "malloc failed");
264		zp->continent = cont;
265		TAILQ_INSERT_TAIL(&cp->zones, zp, link);
266		cp->nzones++;
267	} else {
268		if (cp->nzones > 0)
269			errx(1, _PATH_ZONETAB
270			     ":%d: zone must have description", lineno);
271		if (cp->nzones < 0)
272			errx(1, _PATH_ZONETAB
273			     ":%d: zone multiply defined", lineno);
274		cp->nzones = -1;
275		cp->filename = strdup(file);
276		if (cp->filename == NULL)
277			errx(1, "malloc failed");
278		cp->continent = cont;
279	}
280}
281
282/*
283 * This comparison function intentionally sorts all of the null-named
284 * ``countries''---i.e., the codes that don't correspond to a real
285 * country---to the end.  Everything else is lexical by country name.
286 */
287static int
288compare_countries(const void *xa, const void *xb)
289{
290	const struct country *a = xa, *b = xb;
291
292	if (a->name == 0 && b->name == 0)
293		return 0;
294	if (a->name == 0 && b->name != 0)
295		return 1;
296	if (b->name == 0)
297		return -1;
298
299	return strcmp(a->name, b->name);
300}
301
302/*
303 * This must be done AFTER all zone descriptions are read, since it breaks
304 * CODE2INT().
305 */
306static void
307sort_countries(void)
308{
309	qsort(countries, NCOUNTRIES, sizeof countries[0], compare_countries);
310}
311
312static void
313read_zones(void)
314{
315	FILE *fp;
316	char *line;
317	size_t len;
318	int lineno;
319	char *tlc, *coord, *file, *descr, *p;
320	char contbuf[16];
321	struct continent *cont;
322
323	fp = fopen(_PATH_ZONETAB, "r");
324	if (!fp)
325		err(1, _PATH_ZONETAB);
326	lineno = 0;
327
328	while ((line = fgetln(fp, &len)) != 0) {
329		lineno++;
330		if (line[len - 1] != '\n')
331			errx(1, _PATH_ZONETAB ":%d: invalid format", lineno);
332		line[len - 1] = '\0';
333		if (line[0] == '#')
334			continue;
335
336		tlc = strsep(&line, "\t");
337		if (strlen(tlc) != 2)
338			errx(1, _PATH_ZONETAB ":%d: invalid country code `%s'",
339			     lineno, tlc);
340		coord = strsep(&line, "\t");
341		file = strsep(&line, "\t");
342		p = strchr(file, '/');
343		if (p == 0)
344			errx(1, _PATH_ZONETAB ":%d: invalid zone name `%s'",
345			     lineno, file);
346		contbuf[0] = '\0';
347		strncat(contbuf, file, p - file);
348		cont = find_continent(contbuf);
349		if (!cont)
350			errx(1, _PATH_ZONETAB ":%d: invalid region `%s'",
351			     lineno, contbuf);
352
353		descr = (line && *line) ? line : 0;
354
355		add_zone_to_country(lineno, tlc, descr, file, cont);
356	}
357	fclose(fp);
358}
359
360static void
361make_menus(void)
362{
363	struct country *cp;
364	struct zone *zp, *zp2;
365	struct continent *cont;
366	dialogMenuItem *dmi;
367	int i;
368
369	/*
370	 * First, count up all the countries in each continent/ocean.
371	 * Be careful to count those countries which have multiple zones
372	 * only once for each.  NB: some countries are in multiple
373	 * continents/oceans.
374	 */
375	for (cp = countries; cp->name; cp++) {
376		if (cp->nzones == 0)
377			continue;
378		if (cp->nzones < 0) {
379			cp->continent->nitems++;
380		} else {
381			TAILQ_FOREACH(zp, &cp->zones, link) {
382				cont = zp->continent;
383				for (zp2 = TAILQ_FIRST(&cp->zones);
384				     zp2->continent != cont;
385				     zp2 = TAILQ_NEXT(zp2, link))
386					;
387				if (zp2 == zp)
388					zp->continent->nitems++;
389			}
390		}
391	}
392
393	/*
394	 * Now allocate memory for the country menus.  We set
395	 * nitems back to zero so that we can use it for counting
396	 * again when we actually build the menus.
397	 */
398	for (i = 0; i < NCONTINENTS; i++) {
399		continent_names[i].continent->menu =
400			malloc(sizeof(dialogMenuItem) *
401			       continent_names[i].continent->nitems);
402		if (continent_names[i].continent->menu == 0)
403			errx(1, "malloc for continent menu");
404		continent_names[i].continent->nitems = 0;
405	}
406
407	/*
408	 * Now that memory is allocated, create the menu items for
409	 * each continent.  For multiple-zone countries, also create
410	 * the country's zone submenu.
411	 */
412	for (cp = countries; cp->name; cp++) {
413		if (cp->nzones == 0)
414			continue;
415		if (cp->nzones < 0) {
416			dmi = &cp->continent->menu[cp->continent->nitems];
417			memset(dmi, 0, sizeof *dmi);
418			asprintf(&dmi->prompt, "%d",
419				 ++cp->continent->nitems);
420			dmi->title = cp->name;
421			dmi->checked = 0;
422			dmi->fire = set_zone_whole_country;
423			dmi->selected = 0;
424			dmi->data = cp;
425		} else {
426			cp->submenu = malloc(cp->nzones * sizeof *dmi);
427			if (cp->submenu == 0)
428				errx(1, "malloc for submenu");
429			cp->nzones = 0;
430			TAILQ_FOREACH(zp, &cp->zones, link) {
431				cont = zp->continent;
432				dmi = &cp->submenu[cp->nzones];
433				memset(dmi, 0, sizeof *dmi);
434				asprintf(&dmi->prompt, "%d",
435					 ++cp->nzones);
436				dmi->title = zp->descr;
437				dmi->checked = 0;
438				dmi->fire = set_zone_multi;
439				dmi->selected = 0;
440				dmi->data = zp;
441
442				for (zp2 = TAILQ_FIRST(&cp->zones);
443				     zp2->continent != cont;
444				     zp2 = TAILQ_NEXT(zp2, link))
445					;
446				if (zp2 != zp)
447					continue;
448
449				dmi = &cont->menu[cont->nitems];
450				memset(dmi, 0, sizeof *dmi);
451				asprintf(&dmi->prompt, "%d", ++cont->nitems);
452				dmi->title = cp->name;
453				dmi->checked = 0;
454				dmi->fire = set_zone_menu;
455				dmi->selected = 0;
456				dmi->data = cp;
457			}
458		}
459	}
460}
461
462static int
463set_zone_menu(dialogMenuItem *dmi)
464{
465	int rv;
466	char buf[256];
467	struct country *cp = dmi->data;
468	int menulen;
469
470	snprintf(buf, sizeof buf, "%s Time Zones", cp->name);
471	menulen = cp->nzones < 16 ? cp->nzones : 16;
472	rv = dialog_menu(buf, "Select a zone which observes the same time as "
473			 "your locality.", -1, -1, menulen, -cp->nzones,
474			 cp->submenu, 0, 0, 0);
475	if (rv != 0)
476		return DITEM_RECREATE;
477	return DITEM_LEAVE_MENU;
478}
479
480static int
481install_zone_file(const char *filename)
482{
483	struct stat sb;
484	int fd1, fd2;
485	int copymode;
486	char *msg;
487	ssize_t len;
488	char buf[1024];
489
490	if (lstat(_PATH_LOCALTIME, &sb) < 0)
491		/* Nothing there yet... */
492		copymode = 1;
493	else if(S_ISLNK(sb.st_mode))
494		copymode = 0;
495	else
496		copymode = 1;
497
498#ifdef VERBOSE
499	if (copymode)
500		asprintf(&msg, "Copying %s to " _PATH_LOCALTIME, filename);
501	else
502		asprintf(&msg, "Creating symbolic link " _PATH_LOCALTIME
503			 " to %s", filename);
504
505	dialog_notify(msg);
506	free(msg);
507#endif
508
509	if (reallydoit) {
510		if (copymode) {
511			fd1 = open(filename, O_RDONLY, 0);
512			if (fd1 < 0) {
513				asprintf(&msg, "Could not open %s: %s",
514					 filename, strerror(errno));
515				dialog_mesgbox("Error", msg, 8, 72);
516				free(msg);
517				return DITEM_FAILURE | DITEM_RECREATE;
518			}
519
520			unlink(_PATH_LOCALTIME);
521			fd2 = open(_PATH_LOCALTIME,
522				   O_CREAT|O_EXCL|O_WRONLY,
523				   S_IRUSR|S_IRGRP|S_IROTH);
524			if (fd2 < 0) {
525				asprintf(&msg, "Could not open "
526					 _PATH_LOCALTIME ": %s",
527					 strerror(errno));
528				dialog_mesgbox("Error", msg, 8, 72);
529				free(msg);
530				return DITEM_FAILURE | DITEM_RECREATE;
531			}
532
533			while ((len = read(fd1, buf, sizeof buf)) > 0)
534				len = write(fd2, buf, len);
535
536			if (len == -1) {
537				asprintf(&msg, "Error copying %s to "
538					 _PATH_LOCALTIME ": %s",
539					 filename, strerror(errno));
540				dialog_mesgbox("Error", msg, 8, 72);
541				free(msg);
542				/* Better to leave none than a corrupt one. */
543				unlink(_PATH_LOCALTIME);
544				return DITEM_FAILURE | DITEM_RECREATE;
545			}
546			close(fd1);
547			close(fd2);
548		} else {
549			if (access(filename, R_OK) != 0) {
550				asprintf(&msg, "Cannot access %s: %s",
551					 filename, strerror(errno));
552				dialog_mesgbox("Error", msg, 8, 72);
553				free(msg);
554				return DITEM_FAILURE | DITEM_RECREATE;
555			}
556			unlink(_PATH_LOCALTIME);
557			if (symlink(filename, _PATH_LOCALTIME) < 0) {
558				asprintf(&msg, "Cannot create symbolic link "
559					 _PATH_LOCALTIME " to %s: %s",
560					 filename, strerror(errno));
561				dialog_mesgbox("Error", msg, 8, 72);
562				free(msg);
563				return DITEM_FAILURE | DITEM_RECREATE;
564			}
565		}
566	}
567
568#ifdef VERBOSE
569	if (copymode)
570		asprintf(&msg, "Copied timezone file from %s to "
571			 _PATH_LOCALTIME, filename);
572	else
573		asprintf(&msg, "Created symbolic link from " _PATH_LOCALTIME
574			 " to %s", filename);
575
576	dialog_mesgbox("Done", msg, 8, 72);
577	free(msg);
578#endif
579	return DITEM_LEAVE_MENU;
580}
581
582static int
583confirm_zone(const char *filename)
584{
585	char *msg;
586	struct tm *tm;
587	time_t t = time(0);
588	int rv;
589
590	setenv("TZ", filename, 1);
591	tzset();
592	tm = localtime(&t);
593
594	asprintf(&msg, "Does the abbreviation `%s' look reasonable?",
595		 tm->tm_zone);
596	rv = !dialog_yesno("Confirmation", msg, 5, 72);
597	free(msg);
598	return rv;
599}
600
601static int
602set_zone_multi(dialogMenuItem *dmi)
603{
604	char *fn;
605	struct zone *zp = dmi->data;
606	int rv;
607
608	if (!confirm_zone(zp->filename))
609		return DITEM_FAILURE | DITEM_RECREATE;
610
611	asprintf(&fn, "%s/%s", _PATH_ZONEINFO, zp->filename);
612	rv = install_zone_file(fn);
613	free(fn);
614	return rv;
615}
616
617static int
618set_zone_whole_country(dialogMenuItem *dmi)
619{
620	char *fn;
621	struct country *cp = dmi->data;
622	int rv;
623
624	if (!confirm_zone(cp->filename))
625		return DITEM_FAILURE | DITEM_RECREATE;
626
627	asprintf(&fn, "%s/%s", _PATH_ZONEINFO, cp->filename);
628	rv = install_zone_file(fn);
629	free(fn);
630	return rv;
631}
632
633static void
634usage()
635{
636	fprintf(stderr, "usage: tzsetup [-n]\n");
637	exit(1);
638}
639
640int
641main(int argc, char **argv)
642{
643	int c, fd;
644	int (*dialog_utc)(unsigned char *, unsigned char *, int, int);
645
646#if defined(__alpha__) || defined(__sparc64__)
647	dialog_utc = dialog_yesno;
648#else
649	dialog_utc = dialog_noyes;
650#endif
651
652	while ((c = getopt(argc, argv, "n")) != -1) {
653		switch(c) {
654		case 'n':
655			reallydoit = 0;
656			break;
657
658		default:
659			usage();
660		}
661	}
662
663	if (argc - optind > 1)
664		usage();
665
666	/* Override the user-supplied umask. */
667	(void)umask(S_IWGRP|S_IWOTH);
668
669	read_iso3166_table();
670	read_zones();
671	sort_countries();
672	make_menus();
673
674	init_dialog();
675	if (!dialog_utc("Select local or UTC (Greenwich Mean Time) clock",
676			"Is this machine's CMOS clock set to UTC?  If it is set to local time,\n"
677			"or you don't know, please choose NO here!", 7, 72)) {
678		if (reallydoit)
679			unlink(_PATH_WALL_CMOS_CLOCK);
680	} else {
681		if (reallydoit) {
682			fd = open(_PATH_WALL_CMOS_CLOCK,
683				  O_WRONLY|O_CREAT|O_TRUNC,
684				  S_IRUSR|S_IRGRP|S_IROTH);
685			if (fd < 0)
686				err(1, "create %s", _PATH_WALL_CMOS_CLOCK);
687			close(fd);
688		}
689	}
690	dialog_clear_norefresh();
691	if (optind == argc - 1) {
692		char *msg;
693		asprintf(&msg, "\nUse the default `%s' zone?", argv[optind]);
694		if (!dialog_yesno("Default timezone provided", msg, 7, 72)) {
695			install_zone_file(argv[optind]);
696			dialog_clear();
697			end_dialog();
698			return 0;
699		}
700		free(msg);
701		dialog_clear_norefresh();
702	}
703	dialog_menu("Time Zone Selector", "Select a region", -1, -1,
704		    NCONTINENTS, -NCONTINENTS, continents, 0, NULL, NULL);
705
706	dialog_clear();
707	end_dialog();
708	return 0;
709}
710
711