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