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