1 /* $OpenBSD: parse.y,v 1.10 2024/11/11 15:19:31 florian Exp $ */ 2 3 /* 4 * Copyright (c) 2018 Florian Obser <florian@openbsd.org> 5 * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org> 6 * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org> 7 * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org> 8 * Copyright (c) 2001 Markus Friedl. All rights reserved. 9 * Copyright (c) 2001 Daniel Hartmeier. All rights reserved. 10 * Copyright (c) 2001 Theo de Raadt. All rights reserved. 11 * 12 * Permission to use, copy, modify, and distribute this software for any 13 * purpose with or without fee is hereby granted, provided that the above 14 * copyright notice and this permission notice appear in all copies. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 17 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 19 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 20 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 21 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 22 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 23 */ 24 25 %{ 26 #include <sys/types.h> 27 #include <sys/queue.h> 28 #include <sys/socket.h> 29 #include <sys/stat.h> 30 31 #include <net/if.h> 32 33 #include <netinet/in.h> 34 #include <netinet/if_ether.h> 35 36 #include <arpa/inet.h> 37 38 #include <ctype.h> 39 #include <err.h> 40 #include <errno.h> 41 #include <event.h> 42 #include <imsg.h> 43 #include <limits.h> 44 #include <stdarg.h> 45 #include <stdio.h> 46 #include <string.h> 47 #include <syslog.h> 48 #include <unistd.h> 49 #include <vis.h> 50 51 #include "log.h" 52 #include "dhcpleased.h" 53 #include "frontend.h" 54 55 TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); 56 static struct file { 57 TAILQ_ENTRY(file) entry; 58 FILE *stream; 59 char *name; 60 size_t ungetpos; 61 size_t ungetsize; 62 u_char *ungetbuf; 63 int eof_reached; 64 int lineno; 65 int errors; 66 } *file, *topfile; 67 struct file *pushfile(const char *, int); 68 int popfile(void); 69 int check_file_secrecy(int, const char *); 70 int yyparse(void); 71 int yylex(void); 72 int yyerror(const char *, ...) 73 __attribute__((__format__ (printf, 1, 2))) 74 __attribute__((__nonnull__ (1))); 75 int kw_cmp(const void *, const void *); 76 int lookup(char *); 77 int igetc(void); 78 int lgetc(int); 79 void lungetc(int); 80 int findeol(void); 81 82 TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead); 83 struct sym { 84 TAILQ_ENTRY(sym) entry; 85 int used; 86 int persist; 87 char *nam; 88 char *val; 89 }; 90 91 int symset(const char *, const char *, int); 92 char *symget(const char *); 93 94 static struct dhcpleased_conf *conf; 95 static int errors; 96 97 static struct iface_conf *iface_conf; 98 99 struct iface_conf *conf_get_iface(char *); 100 101 typedef struct { 102 union { 103 int64_t number; 104 char *string; 105 } v; 106 int lineno; 107 } YYSTYPE; 108 109 %} 110 111 %token DHCP_IFACE ERROR SEND VENDOR CLASS ID CLIENT IGNORE DNS ROUTES HOST NAME 112 %token NO PREFER IPV6 113 114 %token <v.string> STRING 115 %token <v.number> NUMBER 116 %type <v.string> string 117 118 %% 119 120 grammar : /* empty */ 121 | grammar '\n' 122 | grammar varset '\n' 123 | grammar dhcp_iface '\n' 124 | grammar error '\n' { file->errors++; } 125 ; 126 127 string : string STRING { 128 if (asprintf(&$$, "%s %s", $1, $2) == -1) { 129 free($1); 130 free($2); 131 yyerror("string: asprintf"); 132 YYERROR; 133 } 134 free($1); 135 free($2); 136 } 137 | STRING 138 ; 139 140 varset : STRING '=' string { 141 char *s = $1; 142 if (log_getverbose() == 1) 143 printf("%s = \"%s\"\n", $1, $3); 144 while (*s++) { 145 if (isspace((unsigned char)*s)) { 146 yyerror("macro name cannot contain " 147 "whitespace"); 148 free($1); 149 free($3); 150 YYERROR; 151 } 152 } 153 if (symset($1, $3, 0) == -1) 154 fatal("cannot store variable"); 155 free($1); 156 free($3); 157 } 158 ; 159 160 optnl : '\n' optnl /* zero or more newlines */ 161 | /*empty*/ 162 ; 163 164 nl : '\n' optnl /* one or more newlines */ 165 ; 166 167 dhcp_iface : DHCP_IFACE STRING { 168 iface_conf = conf_get_iface($2); 169 } '{' iface_block '}' { 170 iface_conf = NULL; 171 } 172 ; 173 174 iface_block : optnl ifaceopts_l 175 ; 176 177 ifaceopts_l : ifaceopts_l ifaceoptsl nl 178 | ifaceoptsl optnl 179 ; 180 181 ifaceoptsl : SEND VENDOR CLASS ID STRING { 182 ssize_t len; 183 char buf[256]; 184 185 if (iface_conf->vc_id != NULL) { 186 yyerror("vendor class id already set"); 187 YYERROR; 188 } 189 190 len = strnunvis(buf, $5, sizeof(buf)); 191 free($5); 192 193 if (len == -1) { 194 yyerror("invalid vendor class id"); 195 YYERROR; 196 } 197 if ((size_t)len >= sizeof(buf)) { 198 yyerror("vendor class id too long"); 199 YYERROR; 200 } 201 202 iface_conf->vc_id_len = 2 + strlen(buf); 203 iface_conf->vc_id = malloc(iface_conf->vc_id_len); 204 if (iface_conf->vc_id == NULL) { 205 yyerror("malloc"); 206 YYERROR; 207 } 208 iface_conf->vc_id[0] = DHO_DHCP_CLASS_IDENTIFIER; 209 iface_conf->vc_id[1] = iface_conf->vc_id_len - 2; 210 memcpy(&iface_conf->vc_id[2], buf, 211 iface_conf->vc_id_len - 2); 212 } 213 | SEND CLIENT ID STRING { 214 size_t i; 215 ssize_t len; 216 int not_hex = 0, val; 217 char buf[256], *hex, *p, excess; 218 219 if (iface_conf->c_id != NULL) { 220 yyerror("client-id already set"); 221 YYERROR; 222 } 223 224 /* parse as hex string including the type byte */ 225 if ((hex = strdup($4)) == NULL) { 226 free($4); 227 yyerror("malloc"); 228 YYERROR; 229 } 230 for (i = 0; (p = strsep(&hex, ":")) != NULL && i < 231 sizeof(buf); ) { 232 if (sscanf(p, "%x%c", &val, &excess) != 1 || 233 val < 0 || val > 0xff) { 234 not_hex = 1; 235 break; 236 } 237 buf[i++] = (val & 0xff); 238 } 239 if (p != NULL && i == sizeof(buf)) 240 not_hex = 1; 241 free(hex); 242 243 if (not_hex) { 244 len = strnunvis(buf, $4, sizeof(buf)); 245 free($4); 246 247 if (len == -1) { 248 yyerror("invalid client-id"); 249 YYERROR; 250 } 251 if ((size_t)len >= sizeof(buf)) { 252 yyerror("client-id too long"); 253 YYERROR; 254 } 255 iface_conf->c_id_len = 2 + len; 256 iface_conf->c_id = malloc(iface_conf->c_id_len); 257 if (iface_conf->c_id == NULL) { 258 yyerror("malloc"); 259 YYERROR; 260 } 261 memcpy(&iface_conf->c_id[2], buf, 262 iface_conf->c_id_len - 2); 263 } else { 264 free($4); 265 iface_conf->c_id_len = 2 + i; 266 iface_conf->c_id = malloc(iface_conf->c_id_len); 267 if (iface_conf->c_id == NULL) { 268 yyerror("malloc"); 269 YYERROR; 270 } 271 memcpy(&iface_conf->c_id[2], buf, 272 iface_conf->c_id_len - 2); 273 } 274 iface_conf->c_id[0] = DHO_DHCP_CLIENT_IDENTIFIER; 275 iface_conf->c_id[1] = iface_conf->c_id_len - 2; 276 } 277 | SEND HOST NAME STRING { 278 if (iface_conf->h_name != NULL) { 279 free($4); 280 yyerror("host name already set"); 281 YYERROR; 282 } 283 if (strlen($4) > 255) { 284 free($4); 285 yyerror("host name too long"); 286 YYERROR; 287 } 288 iface_conf->h_name = $4; 289 } 290 | SEND NO HOST NAME { 291 if (iface_conf->h_name != NULL) { 292 yyerror("host name already set"); 293 YYERROR; 294 } 295 296 if ((iface_conf->h_name = strdup("")) == NULL) { 297 yyerror("malloc"); 298 YYERROR; 299 } 300 } 301 | IGNORE ROUTES { 302 iface_conf->ignore |= IGN_ROUTES; 303 } 304 | IGNORE DNS { 305 iface_conf->ignore |= IGN_DNS; 306 } 307 | IGNORE STRING { 308 int res; 309 310 if (iface_conf->ignore_servers_len >= MAX_SERVERS) { 311 yyerror("too many servers to ignore"); 312 free($2); 313 YYERROR; 314 } 315 res = inet_pton(AF_INET, $2, 316 &iface_conf->ignore_servers[ 317 iface_conf->ignore_servers_len++]); 318 319 if (res != 1) { 320 yyerror("Invalid server IP %s", $2); 321 free($2); 322 YYERROR; 323 } 324 free($2); 325 } 326 | PREFER IPV6 { 327 iface_conf->prefer_ipv6 = 1; 328 } 329 ; 330 %% 331 332 struct keywords { 333 const char *k_name; 334 int k_val; 335 }; 336 337 int 338 yyerror(const char *fmt, ...) 339 { 340 va_list ap; 341 char *msg; 342 343 file->errors++; 344 va_start(ap, fmt); 345 if (vasprintf(&msg, fmt, ap) == -1) 346 fatalx("yyerror vasprintf"); 347 va_end(ap); 348 logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg); 349 free(msg); 350 return (0); 351 } 352 353 int 354 kw_cmp(const void *k, const void *e) 355 { 356 return (strcmp(k, ((const struct keywords *)e)->k_name)); 357 } 358 359 int 360 lookup(char *s) 361 { 362 /* This has to be sorted always. */ 363 static const struct keywords keywords[] = { 364 {"class", CLASS}, 365 {"client", CLIENT}, 366 {"dns", DNS}, 367 {"host", HOST}, 368 {"id", ID}, 369 {"ignore", IGNORE}, 370 {"interface", DHCP_IFACE}, 371 {"ipv6", IPV6}, 372 {"name", NAME}, 373 {"no", NO}, 374 {"prefer", PREFER}, 375 {"routes", ROUTES}, 376 {"send", SEND}, 377 {"vendor", VENDOR}, 378 }; 379 const struct keywords *p; 380 381 p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]), 382 sizeof(keywords[0]), kw_cmp); 383 384 if (p) 385 return (p->k_val); 386 else 387 return (STRING); 388 } 389 390 #define START_EXPAND 1 391 #define DONE_EXPAND 2 392 393 static int expanding; 394 395 int 396 igetc(void) 397 { 398 int c; 399 400 while (1) { 401 if (file->ungetpos > 0) 402 c = file->ungetbuf[--file->ungetpos]; 403 else 404 c = getc(file->stream); 405 406 if (c == START_EXPAND) 407 expanding = 1; 408 else if (c == DONE_EXPAND) 409 expanding = 0; 410 else 411 break; 412 } 413 return (c); 414 } 415 416 int 417 lgetc(int quotec) 418 { 419 int c, next; 420 421 if (quotec) { 422 if ((c = igetc()) == EOF) { 423 yyerror("reached end of file while parsing " 424 "quoted string"); 425 if (file == topfile || popfile() == EOF) 426 return (EOF); 427 return (quotec); 428 } 429 return (c); 430 } 431 432 while ((c = igetc()) == '\\') { 433 next = igetc(); 434 if (next != '\n') { 435 c = next; 436 break; 437 } 438 yylval.lineno = file->lineno; 439 file->lineno++; 440 } 441 442 if (c == EOF) { 443 /* 444 * Fake EOL when hit EOF for the first time. This gets line 445 * count right if last line in included file is syntactically 446 * invalid and has no newline. 447 */ 448 if (file->eof_reached == 0) { 449 file->eof_reached = 1; 450 return ('\n'); 451 } 452 while (c == EOF) { 453 if (file == topfile || popfile() == EOF) 454 return (EOF); 455 c = igetc(); 456 } 457 } 458 return (c); 459 } 460 461 void 462 lungetc(int c) 463 { 464 if (c == EOF) 465 return; 466 467 if (file->ungetpos >= file->ungetsize) { 468 void *p = reallocarray(file->ungetbuf, file->ungetsize, 2); 469 if (p == NULL) 470 err(1, "lungetc"); 471 file->ungetbuf = p; 472 file->ungetsize *= 2; 473 } 474 file->ungetbuf[file->ungetpos++] = c; 475 } 476 477 int 478 findeol(void) 479 { 480 int c; 481 482 /* Skip to either EOF or the first real EOL. */ 483 while (1) { 484 c = lgetc(0); 485 if (c == '\n') { 486 file->lineno++; 487 break; 488 } 489 if (c == EOF) 490 break; 491 } 492 return (ERROR); 493 } 494 495 int 496 yylex(void) 497 { 498 char buf[8096]; 499 char *p, *val; 500 int quotec, next, c; 501 int token; 502 503 top: 504 p = buf; 505 while ((c = lgetc(0)) == ' ' || c == '\t') 506 ; /* nothing */ 507 508 yylval.lineno = file->lineno; 509 if (c == '#') 510 while ((c = lgetc(0)) != '\n' && c != EOF) 511 ; /* nothing */ 512 if (c == '$' && !expanding) { 513 while (1) { 514 if ((c = lgetc(0)) == EOF) 515 return (0); 516 517 if (p + 1 >= buf + sizeof(buf) - 1) { 518 yyerror("string too long"); 519 return (findeol()); 520 } 521 if (isalnum(c) || c == '_') { 522 *p++ = c; 523 continue; 524 } 525 *p = '\0'; 526 lungetc(c); 527 break; 528 } 529 val = symget(buf); 530 if (val == NULL) { 531 yyerror("macro '%s' not defined", buf); 532 return (findeol()); 533 } 534 p = val + strlen(val) - 1; 535 lungetc(DONE_EXPAND); 536 while (p >= val) { 537 lungetc((unsigned char)*p); 538 p--; 539 } 540 lungetc(START_EXPAND); 541 goto top; 542 } 543 544 switch (c) { 545 case '\'': 546 case '"': 547 quotec = c; 548 while (1) { 549 if ((c = lgetc(quotec)) == EOF) 550 return (0); 551 if (c == '\n') { 552 file->lineno++; 553 continue; 554 } else if (c == '\\') { 555 if ((next = lgetc(quotec)) == EOF) 556 return (0); 557 if (next == quotec || next == ' ' || 558 next == '\t') 559 c = next; 560 else if (next == '\n') { 561 file->lineno++; 562 continue; 563 } else 564 lungetc(next); 565 } else if (c == quotec) { 566 *p = '\0'; 567 break; 568 } else if (c == '\0') { 569 yyerror("syntax error"); 570 return (findeol()); 571 } 572 if (p + 1 >= buf + sizeof(buf) - 1) { 573 yyerror("string too long"); 574 return (findeol()); 575 } 576 *p++ = c; 577 } 578 yylval.v.string = strdup(buf); 579 if (yylval.v.string == NULL) 580 err(1, "yylex: strdup"); 581 return (STRING); 582 } 583 584 #define allowed_to_end_number(x) \ 585 (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') 586 587 if (c == '-' || isdigit(c)) { 588 do { 589 *p++ = c; 590 if ((size_t)(p-buf) >= sizeof(buf)) { 591 yyerror("string too long"); 592 return (findeol()); 593 } 594 } while ((c = lgetc(0)) != EOF && isdigit(c)); 595 lungetc(c); 596 if (p == buf + 1 && buf[0] == '-') 597 goto nodigits; 598 if (c == EOF || allowed_to_end_number(c)) { 599 const char *errstr = NULL; 600 601 *p = '\0'; 602 yylval.v.number = strtonum(buf, LLONG_MIN, 603 LLONG_MAX, &errstr); 604 if (errstr) { 605 yyerror("\"%s\" invalid number: %s", 606 buf, errstr); 607 return (findeol()); 608 } 609 return (NUMBER); 610 } else { 611 nodigits: 612 while (p > buf + 1) 613 lungetc((unsigned char)*--p); 614 c = (unsigned char)*--p; 615 if (c == '-') 616 return (c); 617 } 618 } 619 620 #define allowed_in_string(x) \ 621 (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \ 622 x != '{' && x != '}' && \ 623 x != '!' && x != '=' && x != '#' && \ 624 x != ',')) 625 626 if (isalnum(c) || c == ':' || c == '_') { 627 do { 628 *p++ = c; 629 if ((size_t)(p-buf) >= sizeof(buf)) { 630 yyerror("string too long"); 631 return (findeol()); 632 } 633 } while ((c = lgetc(0)) != EOF && (allowed_in_string(c))); 634 lungetc(c); 635 *p = '\0'; 636 if ((token = lookup(buf)) == STRING) 637 if ((yylval.v.string = strdup(buf)) == NULL) 638 err(1, "yylex: strdup"); 639 return (token); 640 } 641 if (c == '\n') { 642 yylval.lineno = file->lineno; 643 file->lineno++; 644 } 645 if (c == EOF) 646 return (0); 647 return (c); 648 } 649 650 int 651 check_file_secrecy(int fd, const char *fname) 652 { 653 struct stat st; 654 655 if (fstat(fd, &st)) { 656 log_warn("cannot stat %s", fname); 657 return (-1); 658 } 659 if (st.st_uid != 0 && st.st_uid != getuid()) { 660 log_warnx("%s: owner not root or current user", fname); 661 return (-1); 662 } 663 if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) { 664 log_warnx("%s: group writable or world read/writable", fname); 665 return (-1); 666 } 667 return (0); 668 } 669 670 struct file * 671 pushfile(const char *name, int secret) 672 { 673 struct file *nfile; 674 675 if ((nfile = calloc(1, sizeof(struct file))) == NULL) { 676 log_warn("calloc"); 677 return (NULL); 678 } 679 if ((nfile->name = strdup(name)) == NULL) { 680 log_warn("strdup"); 681 free(nfile); 682 return (NULL); 683 } 684 if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { 685 free(nfile->name); 686 free(nfile); 687 return (NULL); 688 } else if (secret && 689 check_file_secrecy(fileno(nfile->stream), nfile->name)) { 690 fclose(nfile->stream); 691 free(nfile->name); 692 free(nfile); 693 return (NULL); 694 } 695 nfile->lineno = TAILQ_EMPTY(&files) ? 1 : 0; 696 nfile->ungetsize = 16; 697 nfile->ungetbuf = malloc(nfile->ungetsize); 698 if (nfile->ungetbuf == NULL) { 699 log_warn("malloc"); 700 fclose(nfile->stream); 701 free(nfile->name); 702 free(nfile); 703 return (NULL); 704 } 705 TAILQ_INSERT_TAIL(&files, nfile, entry); 706 return (nfile); 707 } 708 709 int 710 popfile(void) 711 { 712 struct file *prev; 713 714 if ((prev = TAILQ_PREV(file, files, entry)) != NULL) 715 prev->errors += file->errors; 716 717 TAILQ_REMOVE(&files, file, entry); 718 fclose(file->stream); 719 free(file->name); 720 free(file->ungetbuf); 721 free(file); 722 file = prev; 723 return (file ? 0 : EOF); 724 } 725 726 struct dhcpleased_conf * 727 parse_config(const char *filename) 728 { 729 extern const char default_conffile[]; 730 struct sym *sym, *next; 731 732 conf = config_new_empty(); 733 734 file = pushfile(filename, 0); 735 if (file == NULL) { 736 /* no default config file is fine */ 737 if (errno == ENOENT && filename == default_conffile) 738 return (conf); 739 log_warn("%s", filename); 740 free(conf); 741 return (NULL); 742 } 743 topfile = file; 744 745 yyparse(); 746 errors = file->errors; 747 popfile(); 748 749 /* Free macros and check which have not been used. */ 750 TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) { 751 if ((log_getverbose() == 2) && !sym->used) 752 fprintf(stderr, "warning: macro '%s' not used\n", 753 sym->nam); 754 if (!sym->persist) { 755 free(sym->nam); 756 free(sym->val); 757 TAILQ_REMOVE(&symhead, sym, entry); 758 free(sym); 759 } 760 } 761 762 if (errors) { 763 config_clear(conf); 764 return (NULL); 765 } 766 767 return (conf); 768 } 769 770 int 771 symset(const char *nam, const char *val, int persist) 772 { 773 struct sym *sym; 774 775 TAILQ_FOREACH(sym, &symhead, entry) { 776 if (strcmp(nam, sym->nam) == 0) 777 break; 778 } 779 780 if (sym != NULL) { 781 if (sym->persist == 1) 782 return (0); 783 else { 784 free(sym->nam); 785 free(sym->val); 786 TAILQ_REMOVE(&symhead, sym, entry); 787 free(sym); 788 } 789 } 790 if ((sym = calloc(1, sizeof(*sym))) == NULL) 791 return (-1); 792 793 sym->nam = strdup(nam); 794 if (sym->nam == NULL) { 795 free(sym); 796 return (-1); 797 } 798 sym->val = strdup(val); 799 if (sym->val == NULL) { 800 free(sym->nam); 801 free(sym); 802 return (-1); 803 } 804 sym->used = 0; 805 sym->persist = persist; 806 TAILQ_INSERT_TAIL(&symhead, sym, entry); 807 return (0); 808 } 809 810 int 811 cmdline_symset(char *s) 812 { 813 char *sym, *val; 814 int ret; 815 816 if ((val = strrchr(s, '=')) == NULL) 817 return (-1); 818 sym = strndup(s, val - s); 819 if (sym == NULL) 820 errx(1, "%s: strndup", __func__); 821 ret = symset(sym, val + 1, 1); 822 free(sym); 823 824 return (ret); 825 } 826 827 char * 828 symget(const char *nam) 829 { 830 struct sym *sym; 831 832 TAILQ_FOREACH(sym, &symhead, entry) { 833 if (strcmp(nam, sym->nam) == 0) { 834 sym->used = 1; 835 return (sym->val); 836 } 837 } 838 return (NULL); 839 } 840 841 struct iface_conf * 842 conf_get_iface(char *name) 843 { 844 struct iface_conf *iface; 845 size_t n; 846 847 SIMPLEQ_FOREACH(iface, &conf->iface_list, entry) { 848 if (strcmp(name, iface->name) == 0) 849 return (iface); 850 } 851 852 iface = calloc(1, sizeof(*iface)); 853 if (iface == NULL) 854 errx(1, "%s: calloc", __func__); 855 n = strlcpy(iface->name, name, sizeof(iface->name)); 856 if (n >= sizeof(iface->name)) 857 errx(1, "%s: name too long", __func__); 858 859 860 SIMPLEQ_INSERT_TAIL(&conf->iface_list, iface, entry); 861 862 return (iface); 863 } 864