dhclient - Fix old bug that could cause lease mangling.
[dragonfly.git] / sbin / dhclient / clparse.c
CommitLineData
44967127 1/* $OpenBSD: src/sbin/dhclient/clparse.c,v 1.35 2009/05/27 15:04:34 stevesk Exp $ */
846204b6
HT
2
3/* Parser for dhclient config and lease files... */
4
5/*
6 * Copyright (c) 1997 The Internet Software Consortium.
7 * All rights reserved.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions
11 * are met:
12 *
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 * 3. Neither the name of The Internet Software Consortium nor the names
19 * of its contributors may be used to endorse or promote products derived
20 * from this software without specific prior written permission.
21 *
22 * THIS SOFTWARE IS PROVIDED BY THE INTERNET SOFTWARE CONSORTIUM AND
23 * CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
24 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
25 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 * DISCLAIMED. IN NO EVENT SHALL THE INTERNET SOFTWARE CONSORTIUM OR
27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
30 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
31 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
33 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
34 * SUCH DAMAGE.
35 *
36 * This software has been written for the Internet Software Consortium
37 * by Ted Lemon <mellon@fugue.com> in cooperation with Vixie
38 * Enterprises. To learn more about the Internet Software Consortium,
39 * see ``http://www.vix.com/isc''. To learn more about Vixie
40 * Enterprises, see ``http://www.vix.com''.
41 */
42
43#include "dhcpd.h"
44#include "dhctoken.h"
45
46/*
47 * client-conf-file :== client-declarations EOF
48 * client-declarations :== <nil>
49 * | client-declaration
50 * | client-declarations client-declaration
51 */
52int
53read_client_conf(void)
54{
55 FILE *cfile;
56 char *val;
57 int token;
58
59 new_parse(path_dhclient_conf);
60
61 /* Set some defaults... */
62 config->link_timeout = 10;
63 config->timeout = 60;
64 config->select_interval = 0;
65 config->reboot_timeout = 10;
66 config->retry_interval = 300;
67 config->backoff_cutoff = 15;
68 config->initial_interval = 3;
69 config->bootp_policy = ACCEPT;
70 config->script_name = _PATH_DHCLIENT_SCRIPT;
71 config->requested_options
72 [config->requested_option_count++] = DHO_SUBNET_MASK;
73 config->requested_options
74 [config->requested_option_count++] = DHO_BROADCAST_ADDRESS;
75 config->requested_options
76 [config->requested_option_count++] = DHO_TIME_OFFSET;
77 config->requested_options
78 [config->requested_option_count++] = DHO_ROUTERS;
79 config->requested_options
80 [config->requested_option_count++] = DHO_DOMAIN_NAME;
81 config->requested_options
82 [config->requested_option_count++] = DHO_DOMAIN_NAME_SERVERS;
83 config->requested_options
84 [config->requested_option_count++] = DHO_HOST_NAME;
85
86 if ((cfile = fopen(path_dhclient_conf, "r")) != NULL) {
87 do {
88 token = peek_token(&val, cfile);
89 if (token == EOF)
90 break;
91 parse_client_statement(cfile);
92 } while (1);
93 token = next_token(&val, cfile); /* Clear the peek buffer */
94 fclose(cfile);
95 }
96
97 return (!warnings_occurred);
98}
99
100/*
101 * lease-file :== client-lease-statements EOF
102 * client-lease-statements :== <nil>
103 * | client-lease-statements LEASE client-lease-statement
104 */
105void
106read_client_leases(void)
107{
108 FILE *cfile;
109 char *val;
110 int token;
111
112 new_parse(path_dhclient_db);
113
114 /* Open the lease file. If we can't open it, just return -
115 we can safely trust the server to remember our state. */
116 if ((cfile = fopen(path_dhclient_db, "r")) == NULL)
117 return;
118 do {
119 token = next_token(&val, cfile);
120 if (token == EOF)
121 break;
122 if (token != TOK_LEASE) {
123 warning("Corrupt lease file - possible data loss!");
124 skip_to_semi(cfile);
125 break;
126 } else
127 parse_client_lease_statement(cfile, 0);
128
129 } while (1);
130 fclose(cfile);
131}
132
133/*
134 * client-declaration :==
135 * TOK_SEND option-decl |
136 * TOK_DEFAULT option-decl |
137 * TOK_SUPERSEDE option-decl |
138 * TOK_APPEND option-decl |
139 * TOK_PREPEND option-decl |
140 * TOK_MEDIA string-list |
141 * hardware-declaration |
142 * TOK_REQUEST option-list |
143 * TOK_REQUIRE option-list |
144 * TOK_TIMEOUT number |
145 * TOK_RETRY number |
146 * TOK_SELECT_TIMEOUT number |
147 * TOK_REBOOT number |
148 * TOK_BACKOFF_CUTOFF number |
149 * TOK_INITIAL_INTERVAL number |
150 * TOK_SCRIPT string |
151 * interface-declaration |
152 * TOK_LEASE client-lease-statement |
153 * TOK_ALIAS client-lease-statement |
154 * TOK_REJECT reject-statement
155 */
156void
157parse_client_statement(FILE *cfile)
158{
159 char *val;
160 int token, code;
161
162 switch (next_token(&val, cfile)) {
163 case TOK_SEND:
164 parse_option_decl(cfile, &config->send_options[0]);
165 return;
166 case TOK_DEFAULT:
167 code = parse_option_decl(cfile, &config->defaults[0]);
168 if (code != -1)
169 config->default_actions[code] = ACTION_DEFAULT;
170 return;
171 case TOK_SUPERSEDE:
172 code = parse_option_decl(cfile, &config->defaults[0]);
173 if (code != -1)
174 config->default_actions[code] = ACTION_SUPERSEDE;
175 return;
176 case TOK_APPEND:
177 code = parse_option_decl(cfile, &config->defaults[0]);
178 if (code != -1)
179 config->default_actions[code] = ACTION_APPEND;
180 return;
181 case TOK_PREPEND:
182 code = parse_option_decl(cfile, &config->defaults[0]);
183 if (code != -1)
184 config->default_actions[code] = ACTION_PREPEND;
185 return;
186 case TOK_MEDIA:
187 parse_string_list(cfile, &config->media, 1);
188 return;
189 case TOK_HARDWARE:
190 parse_hardware_param(cfile, &ifi->hw_address);
191 return;
192 case TOK_REQUEST:
193 config->requested_option_count =
194 parse_option_list(cfile, config->requested_options);
195 return;
196 case TOK_REQUIRE:
197 memset(config->required_options, 0,
198 sizeof(config->required_options));
199 parse_option_list(cfile, config->required_options);
200 return;
201 case TOK_LINK_TIMEOUT:
202 parse_lease_time(cfile, &config->link_timeout);
203 return;
204 case TOK_TIMEOUT:
205 parse_lease_time(cfile, &config->timeout);
206 return;
207 case TOK_RETRY:
208 parse_lease_time(cfile, &config->retry_interval);
209 return;
210 case TOK_SELECT_TIMEOUT:
211 parse_lease_time(cfile, &config->select_interval);
212 return;
213 case TOK_REBOOT:
214 parse_lease_time(cfile, &config->reboot_timeout);
215 return;
216 case TOK_BACKOFF_CUTOFF:
217 parse_lease_time(cfile, &config->backoff_cutoff);
218 return;
219 case TOK_INITIAL_INTERVAL:
220 parse_lease_time(cfile, &config->initial_interval);
221 return;
222 case TOK_SCRIPT:
223 config->script_name = parse_string(cfile);
224 return;
225 case TOK_INTERFACE:
226 parse_interface_declaration(cfile);
227 return;
228 case TOK_LEASE:
229 parse_client_lease_statement(cfile, 1);
230 return;
231 case TOK_ALIAS:
232 parse_client_lease_statement(cfile, 2);
233 return;
234 case TOK_REJECT:
235 parse_reject_statement(cfile);
236 return;
237 default:
238 parse_warn("expecting a statement.");
239 skip_to_semi(cfile);
240 break;
241 }
242 token = next_token(&val, cfile);
243 if (token != ';') {
244 parse_warn("semicolon expected.");
245 skip_to_semi(cfile);
246 }
247}
248
249int
250parse_X(FILE *cfile, u_int8_t *buf, int max)
251{
252 int token;
253 char *val;
254 int len;
255
256 token = peek_token(&val, cfile);
257 if (token == TOK_NUMBER_OR_NAME || token == TOK_NUMBER) {
258 len = 0;
259 do {
260 token = next_token(&val, cfile);
261 if (token != TOK_NUMBER && token != TOK_NUMBER_OR_NAME) {
262 parse_warn("expecting hexadecimal constant.");
263 skip_to_semi(cfile);
264 return (0);
265 }
266 convert_num(&buf[len], val, 16, 8);
267 if (len++ > max) {
268 parse_warn("hexadecimal constant too long.");
269 skip_to_semi(cfile);
270 return (0);
271 }
272 token = peek_token(&val, cfile);
273 if (token == ':')
274 token = next_token(&val, cfile);
275 } while (token == ':');
276 val = (char *)buf;
277 } else if (token == TOK_STRING) {
278 token = next_token(&val, cfile);
279 len = strlen(val);
280 if (len + 1 > max) {
281 parse_warn("string constant too long.");
282 skip_to_semi(cfile);
283 return (0);
284 }
285 memcpy(buf, val, len + 1);
286 } else {
287 parse_warn("expecting string or hexadecimal data");
288 skip_to_semi(cfile);
289 return (0);
290 }
291 return (len);
292}
293
294/*
295 * option-list :== option_name |
296 * option_list COMMA option_name
297 */
298int
299parse_option_list(FILE *cfile, u_int8_t *list)
300{
301 int ix, i;
302 int token;
303 char *val;
304
305 ix = 0;
306 do {
307 token = next_token(&val, cfile);
308 if (!is_identifier(token)) {
309 parse_warn("expected option name.");
310 skip_to_semi(cfile);
311 return (0);
312 }
313 for (i = 0; i < 256; i++)
314 if (!strcasecmp(dhcp_options[i].name, val))
315 break;
316
317 if (i == 256) {
318 parse_warn("%s: unexpected option name.", val);
319 skip_to_semi(cfile);
320 return (0);
321 }
322 list[ix++] = i;
323 if (ix == 256) {
324 parse_warn("%s: too many options.", val);
325 skip_to_semi(cfile);
326 return (0);
327 }
328 token = next_token(&val, cfile);
329 } while (token == ',');
330 if (token != ';') {
331 parse_warn("expecting semicolon.");
332 skip_to_semi(cfile);
333 return (0);
334 }
335 return (ix);
336}
337
338/*
339 * interface-declaration :==
340 * INTERFACE string LBRACE client-declarations RBRACE
341 */
342void
343parse_interface_declaration(FILE *cfile)
344{
345 char *val;
346 int token;
347
348 token = next_token(&val, cfile);
349 if (token != TOK_STRING) {
350 parse_warn("expecting interface name (in quotes).");
351 skip_to_semi(cfile);
352 return;
353 }
354
355 if (strcmp(ifi->name, val) != 0) {
356 skip_to_semi(cfile);
357 return;
358 }
359
360 token = next_token(&val, cfile);
361 if (token != '{') {
362 parse_warn("expecting left brace.");
363 skip_to_semi(cfile);
364 return;
365 }
366
367 do {
368 token = peek_token(&val, cfile);
369 if (token == EOF) {
370 parse_warn("unterminated interface declaration.");
371 return;
372 }
373 if (token == '}')
374 break;
375 parse_client_statement(cfile);
376 } while (1);
377 token = next_token(&val, cfile);
378}
379
380/*
381 * client-lease-statement :==
382 * RBRACE client-lease-declarations LBRACE
383 *
384 * client-lease-declarations :==
385 * <nil> |
386 * client-lease-declaration |
387 * client-lease-declarations client-lease-declaration
388 */
389void
390parse_client_lease_statement(FILE *cfile, int is_static)
391{
392 struct client_lease *lease, *lp, *pl;
393 int token;
394 char *val;
395
396 token = next_token(&val, cfile);
397 if (token != '{') {
398 parse_warn("expecting left brace.");
399 skip_to_semi(cfile);
400 return;
401 }
402
403 lease = malloc(sizeof(struct client_lease));
404 if (!lease)
405 error("no memory for lease.");
406 memset(lease, 0, sizeof(*lease));
407 lease->is_static = is_static;
408
409 do {
410 token = peek_token(&val, cfile);
411 if (token == EOF) {
412 parse_warn("unterminated lease declaration.");
413 return;
414 }
415 if (token == '}')
416 break;
417 parse_client_lease_declaration(cfile, lease);
418 } while (1);
419 token = next_token(&val, cfile);
420
421 /* If the lease declaration didn't include an interface
422 * declaration that we recognized, it's of no use to us.
423 */
424 if (!ifi) {
425 free_client_lease(lease);
426 return;
427 }
428
429 /* If this is an alias lease, it doesn't need to be sorted in. */
430 if (is_static == 2) {
431 client->alias = lease;
432 return;
433 }
434
435 /*
436 * The new lease may supersede a lease that's not the active
437 * lease but is still on the lease list, so scan the lease list
438 * looking for a lease with the same address, and if we find it,
439 * toss it.
440 */
441 pl = NULL;
442 for (lp = client->leases; lp; lp = lp->next) {
443 if (lp->address.len == lease->address.len &&
444 !memcmp(lp->address.iabuf, lease->address.iabuf,
445 lease->address.len)) {
446 if (pl)
447 pl->next = lp->next;
448 else
449 client->leases = lp->next;
450 free_client_lease(lp);
451 break;
44967127
AHJ
452 } else
453 pl = lp;
846204b6
HT
454 }
455
456 /*
457 * If this is a preloaded lease, just put it on the list of
458 * recorded leases - don't make it the active lease.
459 */
460 if (is_static) {
461 lease->next = client->leases;
462 client->leases = lease;
463 return;
464 }
465
466 /*
467 * The last lease in the lease file on a particular interface is
468 * the active lease for that interface. Of course, we don't
469 * know what the last lease in the file is until we've parsed
470 * the whole file, so at this point, we assume that the lease we
471 * just parsed is the active lease for its interface. If
472 * there's already an active lease for the interface, and this
473 * lease is for the same ip address, then we just toss the old
474 * active lease and replace it with this one. If this lease is
475 * for a different address, then if the old active lease has
476 * expired, we dump it; if not, we put it on the list of leases
477 * for this interface which are still valid but no longer
478 * active.
479 */
480 if (client->active) {
481 if (client->active->expiry < cur_time)
482 free_client_lease(client->active);
483 else if (client->active->address.len ==
484 lease->address.len &&
485 !memcmp(client->active->address.iabuf,
486 lease->address.iabuf, lease->address.len))
487 free_client_lease(client->active);
488 else {
489 client->active->next = client->leases;
490 client->leases = client->active;
491 }
492 }
493 client->active = lease;
494
495 /* Phew. */
496}
497
498/*
499 * client-lease-declaration :==
500 * BOOTP |
501 * INTERFACE string |
502 * FIXED_ADDR ip_address |
503 * FILENAME string |
504 * SERVER_NAME string |
505 * OPTION option-decl |
506 * RENEW time-decl |
507 * REBIND time-decl |
508 * EXPIRE time-decl
509 */
510void
511parse_client_lease_declaration(FILE *cfile, struct client_lease *lease)
512{
513 char *val;
514 int token;
515
516 switch (next_token(&val, cfile)) {
517 case TOK_BOOTP:
518 lease->is_bootp = 1;
519 break;
520 case TOK_INTERFACE:
521 token = next_token(&val, cfile);
522 if (token != TOK_STRING) {
523 parse_warn("expecting interface name (in quotes).");
524 skip_to_semi(cfile);
525 break;
526 }
527 if (strcmp(ifi->name, val) != 0) {
528 parse_warn("wrong interface name. Expecting '%s'.",
529 ifi->name);
530 skip_to_semi(cfile);
531 break;
532 }
533 break;
534 case TOK_FIXED_ADDR:
535 if (!parse_ip_addr(cfile, &lease->address))
536 return;
537 break;
538 case TOK_MEDIUM:
539 parse_string_list(cfile, &lease->medium, 0);
540 return;
541 case TOK_FILENAME:
542 lease->filename = parse_string(cfile);
543 return;
544 case TOK_SERVER_NAME:
545 lease->server_name = parse_string(cfile);
546 return;
547 case TOK_RENEW:
548 lease->renewal = parse_date(cfile);
549 return;
550 case TOK_REBIND:
551 lease->rebind = parse_date(cfile);
552 return;
553 case TOK_EXPIRE:
554 lease->expiry = parse_date(cfile);
555 return;
556 case TOK_OPTION:
557 parse_option_decl(cfile, lease->options);
558 return;
559 default:
560 parse_warn("expecting lease declaration.");
561 skip_to_semi(cfile);
562 break;
563 }
564 token = next_token(&val, cfile);
565 if (token != ';') {
566 parse_warn("expecting semicolon.");
567 skip_to_semi(cfile);
568 }
569}
570
571int
572parse_option_decl(FILE *cfile, struct option_data *options)
573{
574 char *val;
575 int token;
576 u_int8_t buf[4];
577 u_int8_t hunkbuf[1024];
578 int hunkix = 0;
579 char *fmt;
580 struct iaddr ip_addr;
581 u_int8_t *dp;
582 int len, code;
583 int nul_term = 0;
584
585 token = next_token(&val, cfile);
586 if (!is_identifier(token)) {
587 parse_warn("expecting identifier after option keyword.");
588 if (token != ';')
589 skip_to_semi(cfile);
590 return (-1);
591 }
592
593 /* Look up the actual option info. */
594 fmt = NULL;
595 for (code = 0; code < 256; code++)
596 if (strcmp(dhcp_options[code].name, val) == 0)
597 break;
598
599 if (code > 255) {
600 parse_warn("no option named %s", val);
601 skip_to_semi(cfile);
602 return (-1);
603 }
604
605 /* Parse the option data... */
606 do {
607 for (fmt = dhcp_options[code].format; *fmt; fmt++) {
608 if (*fmt == 'A')
609 break;
610 switch (*fmt) {
611 case 'X':
612 len = parse_X(cfile, &hunkbuf[hunkix],
613 sizeof(hunkbuf) - hunkix);
614 hunkix += len;
615 break;
616 case 't': /* Text string... */
617 token = next_token(&val, cfile);
618 if (token != TOK_STRING) {
619 parse_warn("expecting string.");
620 skip_to_semi(cfile);
621 return (-1);
622 }
623 len = strlen(val);
624 if (hunkix + len + 1 > sizeof(hunkbuf)) {
625 parse_warn("option data buffer %s",
626 "overflow");
627 skip_to_semi(cfile);
628 return (-1);
629 }
630 memcpy(&hunkbuf[hunkix], val, len + 1);
631 nul_term = 1;
632 hunkix += len;
633 break;
634 case 'I': /* IP address. */
635 if (!parse_ip_addr(cfile, &ip_addr))
636 return (-1);
637 len = ip_addr.len;
638 dp = ip_addr.iabuf;
639alloc:
640 if (hunkix + len > sizeof(hunkbuf)) {
641 parse_warn("option data buffer "
642 "overflow");
643 skip_to_semi(cfile);
644 return (-1);
645 }
646 memcpy(&hunkbuf[hunkix], dp, len);
647 hunkix += len;
648 break;
649 case 'L': /* Unsigned 32-bit integer... */
650 case 'l': /* Signed 32-bit integer... */
651 token = next_token(&val, cfile);
652 if (token != TOK_NUMBER) {
653need_number:
654 parse_warn("expecting number.");
655 if (token != ';')
656 skip_to_semi(cfile);
657 return (-1);
658 }
659 convert_num(buf, val, 0, 32);
660 len = 4;
661 dp = buf;
662 goto alloc;
663 case 's': /* Signed 16-bit integer. */
664 case 'S': /* Unsigned 16-bit integer. */
665 token = next_token(&val, cfile);
666 if (token != TOK_NUMBER)
667 goto need_number;
668 convert_num(buf, val, 0, 16);
669 len = 2;
670 dp = buf;
671 goto alloc;
672 case 'b': /* Signed 8-bit integer. */
673 case 'B': /* Unsigned 8-bit integer. */
674 token = next_token(&val, cfile);
675 if (token != TOK_NUMBER)
676 goto need_number;
677 convert_num(buf, val, 0, 8);
678 len = 1;
679 dp = buf;
680 goto alloc;
681 case 'f': /* Boolean flag. */
682 token = next_token(&val, cfile);
683 if (!is_identifier(token)) {
684 parse_warn("expecting identifier.");
685bad_flag:
686 if (token != ';')
687 skip_to_semi(cfile);
688 return (-1);
689 }
690 if (!strcasecmp(val, "true") ||
691 !strcasecmp(val, "on"))
692 buf[0] = 1;
693 else if (!strcasecmp(val, "false") ||
694 !strcasecmp(val, "off"))
695 buf[0] = 0;
696 else {
697 parse_warn("expecting boolean.");
698 goto bad_flag;
699 }
700 len = 1;
701 dp = buf;
702 goto alloc;
703 default:
704 warning("Bad format %c in parse_option_param.",
705 *fmt);
706 skip_to_semi(cfile);
707 return (-1);
708 }
709 }
710 token = next_token(&val, cfile);
711 } while (*fmt == 'A' && token == ',');
712
713 if (token != ';') {
714 parse_warn("semicolon expected.");
715 skip_to_semi(cfile);
716 return (-1);
717 }
718
719 options[code].data = malloc(hunkix + nul_term);
720 if (!options[code].data)
721 error("out of memory allocating option data.");
722 memcpy(options[code].data, hunkbuf, hunkix + nul_term);
723 options[code].len = hunkix;
724 return (code);
725}
726
727void
728parse_string_list(FILE *cfile, struct string_list **lp, int multiple)
729{
730 int token;
731 char *val;
732 struct string_list *cur, *tmp;
733
734 /* Find the last medium in the media list. */
735 if (*lp)
736 for (cur = *lp; cur->next; cur = cur->next)
737 ; /* nothing */
738 else
739 cur = NULL;
740
741 do {
742 token = next_token(&val, cfile);
743 if (token != TOK_STRING) {
744 parse_warn("Expecting media options.");
745 skip_to_semi(cfile);
746 return;
747 }
748
749 tmp = malloc(sizeof(struct string_list) + strlen(val));
750 if (tmp == NULL)
751 error("no memory for string list entry.");
752 strlcpy(tmp->string, val, strlen(val) + 1);
753 tmp->next = NULL;
754
755 /* Store this medium at the end of the media list. */
756 if (cur)
757 cur->next = tmp;
758 else
759 *lp = tmp;
760 cur = tmp;
761
762 token = next_token(&val, cfile);
763 } while (multiple && token == ',');
764
765 if (token != ';') {
766 parse_warn("expecting semicolon.");
767 skip_to_semi(cfile);
768 }
769}
770
771void
772parse_reject_statement(FILE *cfile)
773{
774 struct iaddrlist *list;
775 struct iaddr addr;
776 char *val;
777 int token;
778
779 do {
780 if (!parse_ip_addr(cfile, &addr)) {
781 parse_warn("expecting IP address.");
782 skip_to_semi(cfile);
783 return;
784 }
785
786 list = malloc(sizeof(struct iaddrlist));
787 if (!list)
788 error("no memory for reject list!");
789
790 list->addr = addr;
791 list->next = config->reject_list;
792 config->reject_list = list;
793
794 token = next_token(&val, cfile);
795 } while (token == ',');
796
797 if (token != ';') {
798 parse_warn("expecting semicolon.");
799 skip_to_semi(cfile);
800 }
801}