tzsetup.c revision 60833
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 60833 2000-05-23 20:41:01Z jake $";
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(, struct zone) zones; /* use iff nzones > 0 */
150	dialogMenuItem *submenu; /* use iff nzones > 0 */
151};
152
153struct zone {
154	TAILQ_ENTRY(struct 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		if (cp->name == NULL)
220			errx(1, "malloc failed");
221		cp->tlc = strdup(t);
222		if (cp->tlc == NULL)
223			errx(1, "malloc failed");
224	}
225
226	fclose(fp);
227}
228
229static void
230add_zone_to_country(int lineno, const char *tlc, const char *descr,
231		    const char *file, struct continent *cont)
232{
233	struct zone *zp;
234	struct country *cp;
235
236	if (tlc[0] < 'A' || tlc[0] > 'Z' || tlc[1] < 'A' || tlc[1] > 'Z')
237		errx(1, _PATH_ZONETAB ":%d: country code `%s' invalid",
238		     lineno, tlc);
239
240	cp = &countries[CODE2INT(tlc)];
241	if (cp->name == 0)
242		errx(1, _PATH_ZONETAB ":%d: country code `%s' unknown",
243		     lineno, tlc);
244
245	if (descr) {
246		if (cp->nzones < 0)
247			errx(1, _PATH_ZONETAB
248			     ":%d: conflicting zone definition", lineno);
249
250		zp = malloc(sizeof *zp);
251		if (zp == 0)
252			errx(1, "malloc(%lu)", (unsigned long)sizeof *zp);
253
254		if (cp->nzones == 0)
255			TAILQ_INIT(&cp->zones);
256
257		zp->descr = strdup(descr);
258		if (zp->descr == NULL)
259			errx(1, "malloc failed");
260		zp->filename = strdup(file);
261		if (zp->filename == NULL)
262			errx(1, "malloc failed");
263		zp->continent = cont;
264		TAILQ_INSERT_TAIL(&cp->zones, zp, link);
265		cp->nzones++;
266	} else {
267		if (cp->nzones > 0)
268			errx(1, _PATH_ZONETAB
269			     ":%d: zone must have description", lineno);
270		if (cp->nzones < 0)
271			errx(1, _PATH_ZONETAB
272			     ":%d: zone multiply defined", lineno);
273		cp->nzones = -1;
274		cp->filename = strdup(file);
275		if (cp->filename == NULL)
276			errx(1, "malloc failed");
277		cp->continent = cont;
278	}
279}
280
281/*
282 * This comparison function intentionally sorts all of the null-named
283 * ``countries''---i.e., the codes that don't correspond to a real
284 * country---to the end.  Everything else is lexical by country name.
285 */
286static int
287compare_countries(const void *xa, const void *xb)
288{
289	const struct country *a = xa, *b = xb;
290
291	if (a->name == 0 && b->name == 0)
292		return 0;
293	if (a->name == 0 && b->name != 0)
294		return 1;
295	if (b->name == 0)
296		return -1;
297
298	return strcmp(a->name, b->name);
299}
300
301/*
302 * This must be done AFTER all zone descriptions are read, since it breaks
303 * CODE2INT().
304 */
305static void
306sort_countries(void)
307{
308	qsort(countries, NCOUNTRIES, sizeof countries[0], compare_countries);
309}
310
311static void
312read_zones(void)
313{
314	FILE *fp;
315	char *line;
316	size_t len;
317	int lineno;
318	char *tlc, *coord, *file, *descr, *p;
319	char contbuf[16];
320	struct continent *cont;
321
322	fp = fopen(_PATH_ZONETAB, "r");
323	if (!fp)
324		err(1, _PATH_ZONETAB);
325	lineno = 0;
326
327	while ((line = fgetln(fp, &len)) != 0) {
328		lineno++;
329		if (line[len - 1] != '\n')
330			errx(1, _PATH_ZONETAB ":%d: invalid format", lineno);
331		line[len - 1] = '\0';
332		if (line[0] == '#')
333			continue;
334
335		tlc = strsep(&line, "\t");
336		if (strlen(tlc) != 2)
337			errx(1, _PATH_ZONETAB ":%d: invalid country code `%s'",
338			     lineno, tlc);
339		coord = strsep(&line, "\t");
340		file = strsep(&line, "\t");
341		p = strchr(file, '/');
342		if (p == 0)
343			errx(1, _PATH_ZONETAB ":%d: invalid zone name `%s'",
344			     lineno, file);
345		contbuf[0] = '\0';
346		strncat(contbuf, file, p - file);
347		cont = find_continent(contbuf);
348		if (!cont)
349			errx(1, _PATH_ZONETAB ":%d: invalid region `%s'",
350			     lineno, contbuf);
351
352		descr = (line && *line) ? line : 0;
353
354		add_zone_to_country(lineno, tlc, descr, file, cont);
355	}
356	fclose(fp);
357}
358
359static void
360make_menus(void)
361{
362	struct country *cp;
363	struct zone *zp, *zp2;
364	struct continent *cont;
365	dialogMenuItem *dmi;
366	int i;
367
368	/*
369	 * First, count up all the countries in each continent/ocean.
370	 * Be careful to count those countries which have multiple zones
371	 * only once for each.  NB: some countries are in multiple
372	 * continents/oceans.
373	 */
374	for (cp = countries; cp->name; cp++) {
375		if (cp->nzones == 0)
376			continue;
377		if (cp->nzones < 0) {
378			cp->continent->nitems++;
379		} else {
380			for (zp = cp->zones.tqh_first; zp;
381			     zp = zp->link.tqe_next) {
382				cont = zp->continent;
383				for (zp2 = cp->zones.tqh_first;
384				     zp2->continent != cont;
385				     zp2 = zp2->link.tqe_next)
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			for (zp = cp->zones.tqh_first; zp;
431			     zp = zp->link.tqe_next) {
432				cont = zp->continent;
433				dmi = &cp->submenu[cp->nzones];
434				memset(dmi, 0, sizeof *dmi);
435				asprintf(&dmi->prompt, "%d",
436					 ++cp->nzones);
437				dmi->title = zp->descr;
438				dmi->checked = 0;
439				dmi->fire = set_zone_multi;
440				dmi->selected = 0;
441				dmi->data = zp;
442
443				for (zp2 = cp->zones.tqh_first;
444				     zp2->continent != cont;
445				     zp2 = zp2->link.tqe_next)
446					;
447				if (zp2 != zp)
448					continue;
449
450				dmi = &cont->menu[cont->nitems];
451				memset(dmi, 0, sizeof *dmi);
452				asprintf(&dmi->prompt, "%d", ++cont->nitems);
453				dmi->title = cp->name;
454				dmi->checked = 0;
455				dmi->fire = set_zone_menu;
456				dmi->selected = 0;
457				dmi->data = cp;
458			}
459		}
460	}
461}
462
463static int
464set_zone_menu(dialogMenuItem *dmi)
465{
466	int rv;
467	char buf[256];
468	struct country *cp = dmi->data;
469	int menulen;
470
471	snprintf(buf, sizeof buf, "%s Time Zones", cp->name);
472	menulen = cp->nzones < 16 ? cp->nzones : 16;
473	rv = dialog_menu(buf, "Select a zone which observes the same time as "
474			 "your locality.", -1, -1, menulen, -cp->nzones,
475			 cp->submenu, 0, 0, 0);
476	if (rv != 0)
477		return DITEM_RECREATE;
478	return DITEM_LEAVE_MENU;
479}
480
481static int
482install_zone_file(const char *filename)
483{
484	struct stat sb;
485	int fd1, fd2;
486	int copymode;
487	char *msg;
488	ssize_t len;
489	char buf[1024];
490
491	if (lstat(_PATH_LOCALTIME, &sb) < 0)
492		/* Nothing there yet... */
493		copymode = 1;
494	else if(S_ISLNK(sb.st_mode))
495		copymode = 0;
496	else
497		copymode = 1;
498
499#ifdef VERBOSE
500	if (copymode)
501		asprintf(&msg, "Copying %s to " _PATH_LOCALTIME, filename);
502	else
503		asprintf(&msg, "Creating symbolic link " _PATH_LOCALTIME
504			 " to %s", filename);
505
506	dialog_notify(msg);
507	free(msg);
508#endif
509
510	if (reallydoit) {
511		if (copymode) {
512			fd1 = open(filename, O_RDONLY, 0);
513			if (fd1 < 0) {
514				asprintf(&msg, "Could not open %s: %s",
515					 filename, strerror(errno));
516				dialog_mesgbox("Error", msg, 8, 72);
517				free(msg);
518				return DITEM_FAILURE | DITEM_RECREATE;
519			}
520
521			unlink(_PATH_LOCALTIME);
522			fd2 = open(_PATH_LOCALTIME,
523				   O_CREAT|O_EXCL|O_WRONLY,
524				   S_IRUSR|S_IRGRP|S_IROTH);
525			if (fd2 < 0) {
526				asprintf(&msg, "Could not open "
527					 _PATH_LOCALTIME ": %s",
528					 strerror(errno));
529				dialog_mesgbox("Error", msg, 8, 72);
530				free(msg);
531				return DITEM_FAILURE | DITEM_RECREATE;
532			}
533
534			while ((len = read(fd1, buf, sizeof buf)) > 0)
535				len = write(fd2, buf, len);
536
537			if (len == -1) {
538				asprintf(&msg, "Error copying %s to "
539					 _PATH_LOCALTIME ": %s",
540					 filename, strerror(errno));
541				dialog_mesgbox("Error", msg, 8, 72);
542				free(msg);
543				/* Better to leave none than a corrupt one. */
544				unlink(_PATH_LOCALTIME);
545				return DITEM_FAILURE | DITEM_RECREATE;
546			}
547			close(fd1);
548			close(fd2);
549		} else {
550			if (access(filename, R_OK) != 0) {
551				asprintf(&msg, "Cannot access %s: %s",
552					 filename, strerror(errno));
553				dialog_mesgbox("Error", msg, 8, 72);
554				free(msg);
555				return DITEM_FAILURE | DITEM_RECREATE;
556			}
557			unlink(_PATH_LOCALTIME);
558			if (symlink(filename, _PATH_LOCALTIME) < 0) {
559				asprintf(&msg, "Cannot create symbolic link "
560					 _PATH_LOCALTIME " to %s: %s",
561					 filename, strerror(errno));
562				dialog_mesgbox("Error", msg, 8, 72);
563				free(msg);
564				return DITEM_FAILURE | DITEM_RECREATE;
565			}
566		}
567	}
568
569#ifdef VERBOSE
570	if (copymode)
571		asprintf(&msg, "Copied timezone file from %s to "
572			 _PATH_LOCALTIME, filename);
573	else
574		asprintf(&msg, "Created symbolic link from " _PATH_LOCALTIME
575			 " to %s", filename);
576
577	dialog_mesgbox("Done", msg, 8, 72);
578	free(msg);
579#endif
580	return DITEM_LEAVE_MENU;
581}
582
583static int
584confirm_zone(const char *filename)
585{
586	char *msg;
587	struct tm *tm;
588	time_t t = time(0);
589	int rv;
590
591	setenv("TZ", filename, 1);
592	tzset();
593	tm = localtime(&t);
594
595	asprintf(&msg, "Does the abbreviation `%s' look reasonable?",
596		 tm->tm_zone);
597	rv = !dialog_yesno("Confirmation", msg, 4, 72);
598	free(msg);
599	return rv;
600}
601
602static int
603set_zone_multi(dialogMenuItem *dmi)
604{
605	char *fn;
606	struct zone *zp = dmi->data;
607	int rv;
608
609	if (!confirm_zone(zp->filename))
610		return DITEM_FAILURE | DITEM_RECREATE;
611
612	asprintf(&fn, "%s/%s", _PATH_ZONEINFO, zp->filename);
613	rv = install_zone_file(fn);
614	free(fn);
615	return rv;
616}
617
618static int
619set_zone_whole_country(dialogMenuItem *dmi)
620{
621	char *fn;
622	struct country *cp = dmi->data;
623	int rv;
624
625	if (!confirm_zone(cp->filename))
626		return DITEM_FAILURE | DITEM_RECREATE;
627
628	asprintf(&fn, "%s/%s", _PATH_ZONEINFO, cp->filename);
629	rv = install_zone_file(fn);
630	free(fn);
631	return rv;
632}
633
634static void
635usage()
636{
637	fprintf(stderr, "usage: tzsetup [-n]\n");
638	exit(1);
639}
640
641int
642main(int argc, char **argv)
643{
644	int c, fd;
645
646	while ((c = getopt(argc, argv, "n")) != -1) {
647		switch(c) {
648		case 'n':
649			reallydoit = 0;
650			break;
651
652		default:
653			usage();
654		}
655	}
656
657	if (argc - optind > 1)
658		usage();
659
660	/* Override the user-supplied umask. */
661	(void)umask(S_IWGRP|S_IWOTH);
662
663	read_iso3166_table();
664	read_zones();
665	sort_countries();
666	make_menus();
667
668	init_dialog();
669	if (!dialog_yesno("Select local or UTC (Greenwich Mean Time) clock",
670			  "Is this machine's CMOS clock set to UTC?  If it is set to local time,\n"
671			  "or you don't know, please choose NO here!", 7, 72)) {
672		if (reallydoit)
673			unlink(_PATH_WALL_CMOS_CLOCK);
674	} else {
675		if (reallydoit) {
676			fd = open(_PATH_WALL_CMOS_CLOCK,
677				  O_WRONLY|O_CREAT|O_TRUNC,
678				  S_IRUSR|S_IRGRP|S_IROTH);
679			if (fd < 0)
680				err(1, "create %s", _PATH_WALL_CMOS_CLOCK);
681			close(fd);
682		}
683	}
684	dialog_clear_norefresh();
685	if (optind == argc - 1) {
686		char *msg;
687		asprintf(&msg, "\nUse the default `%s' zone?", argv[optind]);
688		if (!dialog_yesno("Default timezone provided", msg, 7, 72)) {
689			install_zone_file(argv[optind]);
690			dialog_clear();
691			end_dialog();
692			return 0;
693		}
694		free(msg);
695		dialog_clear_norefresh();
696	}
697	dialog_menu("Time Zone Selector", "Select a region", -1, -1,
698		    NCONTINENTS, -NCONTINENTS, continents, 0, NULL, NULL);
699
700	dialog_clear();
701	end_dialog();
702	return 0;
703}
704
705