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