]>
Commit | Line | Data |
---|---|---|
9d0662e0 | 1 | /* |
1ab84bf3 | 2 | * Command format string parser for CLI backend. |
9d0662e0 | 3 | * |
1ab84bf3 | 4 | * -- |
9547b5d0 | 5 | * Copyright (C) 2016 Cumulus Networks, Inc. |
9d0662e0 | 6 | * |
1ab84bf3 QY |
7 | * This file is part of GNU Zebra. |
8 | * | |
9 | * GNU Zebra is free software; you can redistribute it and/or modify it | |
10 | * under the terms of the GNU General Public License as published by the | |
11 | * Free Software Foundation; either version 2, or (at your option) any | |
12 | * later version. | |
13 | * | |
14 | * GNU Zebra is distributed in the hope that it will be useful, but | |
15 | * WITHOUT ANY WARRANTY; without even the implied warranty of | |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
17 | * General Public License for more details. | |
18 | * | |
19 | * You should have received a copy of the GNU General Public License | |
20 | * along with GNU Zebra; see the file COPYING. If not, write to the Free | |
21 | * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA | |
22 | * 02111-1307, USA. | |
9d0662e0 QY |
23 | */ |
24 | ||
92055a92 | 25 | %{ |
1ab84bf3 QY |
26 | // compile with debugging facilities |
27 | #define YYDEBUG 1 | |
51fc9379 | 28 | %} |
782d9789 | 29 | |
e9484f70 | 30 | %define api.pure full |
0d37f9f3 | 31 | /* define api.prefix {cmd_yy} */ |
e9484f70 | 32 | |
51fc9379 QY |
33 | /* names for generated header and parser files */ |
34 | %defines "command_parse.h" | |
35 | %output "command_parse.c" | |
5a8bbed0 | 36 | |
afc3a6ce DL |
37 | /* note: code blocks are output in order, to both .c and .h: |
38 | * 1. %code requires | |
39 | * 2. %union + bison forward decls | |
40 | * 3. %code provides | |
41 | * command_lex.h needs to be included at 3.; it needs the union and YYSTYPE. | |
42 | * struct parser_ctx is needed for the bison forward decls. | |
43 | */ | |
eceb1066 | 44 | %code requires { |
460a7689 DS |
45 | #include "stdlib.h" |
46 | #include "string.h" | |
eceb1066 | 47 | #include "command.h" |
a9958674 | 48 | #include "log.h" |
7a6ded40 | 49 | #include "graph.h" |
51fc9379 | 50 | |
afc3a6ce DL |
51 | #define YYSTYPE CMD_YYSTYPE |
52 | struct parser_ctx; | |
2020b1c8 DL |
53 | |
54 | /* subgraph semantic value */ | |
55 | struct subgraph { | |
56 | struct graph_node *start, *end; | |
57 | }; | |
afc3a6ce DL |
58 | } |
59 | ||
60 | %union { | |
61 | long long number; | |
62 | char *string; | |
63 | struct graph_node *node; | |
2020b1c8 | 64 | struct subgraph subgraph; |
afc3a6ce DL |
65 | } |
66 | ||
67 | %code provides { | |
68 | #ifndef FLEX_SCANNER | |
69 | #include "command_lex.h" | |
70 | #endif | |
71 | ||
72 | extern void set_lexer_string (yyscan_t *scn, const char *string); | |
73 | extern void cleanup_lexer (yyscan_t *scn); | |
74 | ||
b07a15f8 | 75 | struct parser_ctx { |
e9484f70 DL |
76 | yyscan_t scanner; |
77 | ||
b07a15f8 DL |
78 | struct cmd_element *el; |
79 | ||
80 | struct graph *graph; | |
fd19e7a2 | 81 | struct graph_node *currnode; |
b07a15f8 DL |
82 | |
83 | /* pointers to copy of command docstring */ | |
84 | char *docstr_start, *docstr; | |
85 | }; | |
92055a92 QY |
86 | } |
87 | ||
51fc9379 | 88 | /* union types for lexed tokens */ |
5a5d576b QY |
89 | %token <string> WORD |
90 | %token <string> IPV4 | |
91 | %token <string> IPV4_PREFIX | |
92 | %token <string> IPV6 | |
93 | %token <string> IPV6_PREFIX | |
94 | %token <string> VARIABLE | |
95 | %token <string> RANGE | |
782d9789 | 96 | |
51fc9379 | 97 | /* union types for parsed rules */ |
782d9789 | 98 | %type <node> start |
782d9789 QY |
99 | %type <node> literal_token |
100 | %type <node> placeholder_token | |
9286efab | 101 | %type <node> simple_token |
9286efab QY |
102 | %type <subgraph> selector |
103 | %type <subgraph> selector_token | |
104 | %type <subgraph> selector_token_seq | |
105 | %type <subgraph> selector_seq_seq | |
782d9789 | 106 | |
51fc9379 | 107 | %code { |
e9484f70 | 108 | |
51fc9379 QY |
109 | /* bison declarations */ |
110 | void | |
e9484f70 | 111 | cmd_yyerror (struct parser_ctx *ctx, char const *msg); |
51fc9379 | 112 | |
51fc9379 QY |
113 | /* helper functions for parser */ |
114 | static char * | |
b07a15f8 | 115 | doc_next (struct parser_ctx *ctx); |
51fc9379 QY |
116 | |
117 | static struct graph_node * | |
1eb5e8dc | 118 | node_adjacent (struct graph_node *, struct graph_node *); |
51fc9379 QY |
119 | |
120 | static struct graph_node * | |
1eb5e8dc | 121 | add_edge_dedup (struct graph_node *, struct graph_node *); |
51fc9379 QY |
122 | |
123 | static int | |
d0bfb22c | 124 | cmp_token (struct cmd_token *, struct cmd_token *); |
1eb5e8dc QY |
125 | |
126 | static struct graph_node * | |
b07a15f8 | 127 | new_token_node (struct parser_ctx *, |
d0bfb22c | 128 | enum cmd_token_type type, |
b07a15f8 | 129 | char *text, |
ce882f81 | 130 | char *doc); |
51fc9379 QY |
131 | |
132 | static void | |
b07a15f8 DL |
133 | terminate_graph (struct parser_ctx *ctx, |
134 | struct graph_node *); | |
51fc9379 QY |
135 | |
136 | static void | |
b07a15f8 | 137 | cleanup (struct parser_ctx *ctx); |
e9484f70 DL |
138 | |
139 | #define scanner ctx->scanner | |
51fc9379 QY |
140 | } |
141 | ||
142 | /* yyparse parameters */ | |
e9484f70 DL |
143 | %lex-param {yyscan_t scanner} |
144 | %parse-param {struct parser_ctx *ctx} | |
51fc9379 QY |
145 | |
146 | /* called automatically before yyparse */ | |
147 | %initial-action { | |
148 | /* clear state pointers */ | |
fd19e7a2 | 149 | ctx->currnode = vector_slot (ctx->graph->nodes, 0); |
51fc9379 | 150 | |
51fc9379 | 151 | /* copy docstring and keep a pointer to the copy */ |
b07a15f8 | 152 | if (ctx->el->doc) |
a9958674 QY |
153 | { |
154 | // allocate a new buffer, making room for a flag | |
b07a15f8 DL |
155 | size_t length = (size_t) strlen (ctx->el->doc) + 2; |
156 | ctx->docstr = malloc (length); | |
157 | memcpy (ctx->docstr, ctx->el->doc, strlen (ctx->el->doc)); | |
a9958674 | 158 | // set the flag so doc_next knows when to print a warning |
b07a15f8 | 159 | ctx->docstr[length - 2] = 0x03; |
a9958674 | 160 | // null terminate |
b07a15f8 | 161 | ctx->docstr[length - 1] = 0x00; |
a9958674 | 162 | } |
b07a15f8 | 163 | ctx->docstr_start = ctx->docstr; |
51fc9379 | 164 | } |
92055a92 | 165 | |
92055a92 QY |
166 | %% |
167 | ||
88255c7c | 168 | start: |
fd19e7a2 | 169 | cmd_token_seq |
880e24a1 | 170 | { |
51fc9379 | 171 | // tack on the command element |
b07a15f8 | 172 | terminate_graph (ctx, ctx->currnode); |
880e24a1 | 173 | } |
fd19e7a2 | 174 | | cmd_token_seq placeholder_token '.' '.' '.' |
88255c7c | 175 | { |
fd19e7a2 DL |
176 | if ((ctx->currnode = add_edge_dedup (ctx->currnode, $2)) != $2) |
177 | graph_delete_node (ctx->graph, $2); | |
88255c7c | 178 | |
4d94b292 | 179 | ((struct cmd_token *)ctx->currnode->data)->allowrepeat = 1; |
7d5718c1 | 180 | |
1ab84bf3 | 181 | // adding a node as a child of itself accepts any number |
a9958674 | 182 | // of the same token, which is what we want for variadics |
b07a15f8 | 183 | add_edge_dedup (ctx->currnode, ctx->currnode); |
88255c7c | 184 | |
51fc9379 | 185 | // tack on the command element |
b07a15f8 | 186 | terminate_graph (ctx, ctx->currnode); |
88255c7c | 187 | } |
9286efab | 188 | ; |
92055a92 | 189 | |
9286efab | 190 | cmd_token_seq: |
0d4aa1b1 | 191 | /* empty */ |
9286efab QY |
192 | | cmd_token_seq cmd_token |
193 | ; | |
92055a92 | 194 | |
782d9789 | 195 | cmd_token: |
9286efab | 196 | simple_token |
5a5d576b | 197 | { |
b07a15f8 DL |
198 | if ((ctx->currnode = add_edge_dedup (ctx->currnode, $1)) != $1) |
199 | graph_delete_node (ctx->graph, $1); | |
5a5d576b | 200 | } |
663982cd | 201 | | selector |
5a5d576b | 202 | { |
2020b1c8 DL |
203 | graph_add_edge (ctx->currnode, $1.start); |
204 | ctx->currnode = $1.end; | |
478bdaeb | 205 | } |
9286efab QY |
206 | ; |
207 | ||
208 | simple_token: | |
209 | literal_token | |
210 | | placeholder_token | |
211 | ; | |
212 | ||
9286efab QY |
213 | literal_token: WORD |
214 | { | |
b07a15f8 | 215 | $$ = new_token_node (ctx, WORD_TKN, strdup($1), doc_next(ctx)); |
9286efab QY |
216 | free ($1); |
217 | } | |
782d9789 QY |
218 | ; |
219 | ||
220 | placeholder_token: | |
478bdaeb | 221 | IPV4 |
4b0abf24 | 222 | { |
b07a15f8 | 223 | $$ = new_token_node (ctx, IPV4_TKN, strdup($1), doc_next(ctx)); |
5a5d576b | 224 | free ($1); |
4b0abf24 | 225 | } |
478bdaeb | 226 | | IPV4_PREFIX |
340a2b4a | 227 | { |
b07a15f8 | 228 | $$ = new_token_node (ctx, IPV4_PREFIX_TKN, strdup($1), doc_next(ctx)); |
5a5d576b | 229 | free ($1); |
4b0abf24 | 230 | } |
478bdaeb | 231 | | IPV6 |
340a2b4a | 232 | { |
b07a15f8 | 233 | $$ = new_token_node (ctx, IPV6_TKN, strdup($1), doc_next(ctx)); |
5a5d576b | 234 | free ($1); |
4b0abf24 | 235 | } |
478bdaeb | 236 | | IPV6_PREFIX |
340a2b4a | 237 | { |
b07a15f8 | 238 | $$ = new_token_node (ctx, IPV6_PREFIX_TKN, strdup($1), doc_next(ctx)); |
5a5d576b | 239 | free ($1); |
4b0abf24 | 240 | } |
478bdaeb | 241 | | VARIABLE |
340a2b4a | 242 | { |
b07a15f8 | 243 | $$ = new_token_node (ctx, VARIABLE_TKN, strdup($1), doc_next(ctx)); |
5a5d576b | 244 | free ($1); |
4b0abf24 | 245 | } |
478bdaeb QY |
246 | | RANGE |
247 | { | |
b07a15f8 | 248 | $$ = new_token_node (ctx, RANGE_TKN, strdup($1), doc_next(ctx)); |
d0bfb22c | 249 | struct cmd_token *token = $$->data; |
478bdaeb QY |
250 | |
251 | // get the numbers out | |
b3899769 | 252 | yylval.string++; |
7a6ded40 | 253 | token->min = strtoll (yylval.string, &yylval.string, 10); |
ef955a80 | 254 | strsep (&yylval.string, "-"); |
7a6ded40 | 255 | token->max = strtoll (yylval.string, &yylval.string, 10); |
ef955a80 QY |
256 | |
257 | // validate range | |
e9484f70 | 258 | if (token->min > token->max) cmd_yyerror (ctx, "Invalid range."); |
5a5d576b QY |
259 | |
260 | free ($1); | |
478bdaeb | 261 | } |
782d9789 | 262 | |
4b0abf24 | 263 | /* <selector|set> productions */ |
9286efab | 264 | selector: '<' selector_seq_seq '>' |
2a23ca6e | 265 | { |
663982cd | 266 | $$ = $2; |
2a23ca6e | 267 | }; |
782d9789 | 268 | |
9286efab QY |
269 | selector_seq_seq: |
270 | selector_seq_seq '|' selector_token_seq | |
478bdaeb | 271 | { |
663982cd | 272 | $$ = $1; |
2020b1c8 DL |
273 | graph_add_edge ($$.start, $3.start); |
274 | graph_add_edge ($3.end, $$.end); | |
9286efab | 275 | } |
663982cd | 276 | | selector_token_seq |
9286efab | 277 | { |
663982cd DL |
278 | $$.start = new_token_node (ctx, FORK_TKN, NULL, NULL); |
279 | $$.end = new_token_node (ctx, JOIN_TKN, NULL, NULL); | |
280 | ((struct cmd_token *)$$.start->data)->forkjoin = $$.end; | |
281 | ((struct cmd_token *)$$.end->data)->forkjoin = $$.start; | |
282 | ||
2020b1c8 DL |
283 | graph_add_edge ($$.start, $1.start); |
284 | graph_add_edge ($1.end, $$.end); | |
478bdaeb | 285 | } |
9286efab | 286 | ; |
782d9789 | 287 | |
7d5718c1 DL |
288 | /* {keyword} productions */ |
289 | selector: '{' selector_seq_seq '}' | |
290 | { | |
663982cd DL |
291 | $$ = $2; |
292 | graph_add_edge ($$.end, $$.start); | |
293 | /* there is intentionally no start->end link, for two reasons: | |
294 | * 1) this allows "at least 1 of" semantics, which are otherwise impossible | |
295 | * 2) this would add a start->end->start loop in the graph that the current | |
296 | * loop-avoidal fails to handle | |
297 | * just use [{a|b}] if neccessary, that will work perfectly fine, and reason | |
298 | * #1 is good enough to keep it this way. */ | |
7d5718c1 DL |
299 | }; |
300 | ||
301 | ||
535ef556 | 302 | selector_token: |
9286efab | 303 | simple_token |
4b0abf24 | 304 | { |
2020b1c8 | 305 | $$.start = $$.end = $1; |
9286efab | 306 | } |
4d12266b | 307 | | selector |
9286efab | 308 | ; |
782d9789 | 309 | |
663982cd DL |
310 | selector_token_seq: |
311 | selector_token_seq selector_token | |
535ef556 | 312 | { |
2020b1c8 DL |
313 | graph_add_edge ($1.end, $2.start); |
314 | $$.start = $1.start; | |
315 | $$.end = $2.end; | |
535ef556 | 316 | } |
663982cd | 317 | | selector_token |
782d9789 QY |
318 | ; |
319 | ||
663982cd DL |
320 | /* [option] productions */ |
321 | selector: '[' selector_seq_seq ']' | |
9286efab | 322 | { |
663982cd DL |
323 | $$ = $2; |
324 | graph_add_edge ($$.start, $$.end); | |
9286efab | 325 | } |
782d9789 QY |
326 | ; |
327 | ||
92055a92 | 328 | %% |
4b0abf24 | 329 | |
e9484f70 DL |
330 | #undef scanner |
331 | ||
1eb5e8dc QY |
332 | void |
333 | command_parse_format (struct graph *graph, struct cmd_element *cmd) | |
51fc9379 | 334 | { |
b07a15f8 DL |
335 | struct parser_ctx ctx = { .graph = graph, .el = cmd }; |
336 | ||
1ab84bf3 | 337 | // set to 1 to enable parser traces |
51fc9379 QY |
338 | yydebug = 0; |
339 | ||
e9484f70 DL |
340 | set_lexer_string (&ctx.scanner, cmd->string); |
341 | ||
51fc9379 | 342 | // parse command into DFA |
e9484f70 DL |
343 | cmd_yyparse (&ctx); |
344 | ||
345 | /* cleanup lexer */ | |
346 | cleanup_lexer (&ctx.scanner); | |
07079d78 | 347 | |
7a6ded40 | 348 | // cleanup |
b07a15f8 | 349 | cleanup (&ctx); |
51fc9379 QY |
350 | } |
351 | ||
352 | /* parser helper functions */ | |
353 | ||
354 | void | |
b07a15f8 | 355 | yyerror (struct parser_ctx *ctx, char const *msg) |
51fc9379 | 356 | { |
1ab84bf3 | 357 | zlog_err ("%s: FATAL parse error: %s", __func__, msg); |
b07a15f8 | 358 | zlog_err ("while parsing this command definition: \n\t%s\n", ctx->el->string); |
a98d33ab | 359 | //exit(EXIT_FAILURE); |
92055a92 | 360 | } |
782d9789 | 361 | |
51fc9379 | 362 | static void |
b07a15f8 | 363 | cleanup (struct parser_ctx *ctx) |
782d9789 | 364 | { |
51fc9379 | 365 | /* free resources */ |
b07a15f8 | 366 | free (ctx->docstr_start); |
478bdaeb | 367 | |
4b0abf24 | 368 | /* clear state pointers */ |
b07a15f8 DL |
369 | ctx->currnode = NULL; |
370 | ctx->docstr_start = ctx->docstr = NULL; | |
51fc9379 | 371 | } |
4b0abf24 | 372 | |
51fc9379 | 373 | static void |
b07a15f8 | 374 | terminate_graph (struct parser_ctx *ctx, struct graph_node *finalnode) |
51fc9379 | 375 | { |
1eb5e8dc QY |
376 | // end of graph should look like this |
377 | // * -> finalnode -> END_TKN -> cmd_element | |
b07a15f8 | 378 | struct cmd_element *element = ctx->el; |
d8074cc0 | 379 | struct graph_node *end_token_node = |
b07a15f8 | 380 | new_token_node (ctx, |
d8074cc0 | 381 | END_TKN, |
460a7689 DS |
382 | strdup (CMD_CR_TEXT), |
383 | strdup ("")); | |
1eb5e8dc | 384 | struct graph_node *end_element_node = |
b5a1e9ef | 385 | graph_new_node (ctx->graph, element, NULL); |
7a6ded40 | 386 | |
1eb5e8dc | 387 | if (node_adjacent (finalnode, end_token_node)) |
e9484f70 | 388 | cmd_yyerror (ctx, "Duplicate command."); |
1eb5e8dc QY |
389 | |
390 | graph_add_edge (finalnode, end_token_node); | |
391 | graph_add_edge (end_token_node, end_element_node); | |
782d9789 | 392 | } |
f4b0c72e | 393 | |
51fc9379 | 394 | static char * |
b07a15f8 | 395 | doc_next (struct parser_ctx *ctx) |
51fc9379 | 396 | { |
b07a15f8 | 397 | const char *piece = ctx->docstr ? strsep (&ctx->docstr, "\n") : ""; |
a9958674 QY |
398 | if (*piece == 0x03) |
399 | { | |
b07a15f8 | 400 | zlog_debug ("Ran out of docstring while parsing '%s'", ctx->el->string); |
a9958674 QY |
401 | piece = ""; |
402 | } | |
403 | ||
460a7689 | 404 | return strdup (piece); |
51fc9379 QY |
405 | } |
406 | ||
407 | static struct graph_node * | |
b07a15f8 DL |
408 | new_token_node (struct parser_ctx *ctx, enum cmd_token_type type, |
409 | char *text, char *doc) | |
7a6ded40 | 410 | { |
b07a15f8 DL |
411 | struct cmd_token *token = new_cmd_token (type, ctx->el->attr, text, doc); |
412 | return graph_new_node (ctx->graph, token, (void (*)(void *)) &del_cmd_token); | |
7a6ded40 QY |
413 | } |
414 | ||
415 | /** | |
1eb5e8dc | 416 | * Determines if there is an out edge from the first node to the second |
7a6ded40 QY |
417 | */ |
418 | static struct graph_node * | |
1eb5e8dc | 419 | node_adjacent (struct graph_node *first, struct graph_node *second) |
f4b0c72e | 420 | { |
7a6ded40 | 421 | struct graph_node *adj; |
1eb5e8dc | 422 | for (unsigned int i = 0; i < vector_active (first->to); i++) |
1ab84bf3 | 423 | { |
1eb5e8dc | 424 | adj = vector_slot (first->to, i); |
d0bfb22c | 425 | struct cmd_token *ftok = adj->data, |
1eb5e8dc QY |
426 | *stok = second->data; |
427 | if (cmp_token (ftok, stok)) | |
7a6ded40 | 428 | return adj; |
1ab84bf3 | 429 | } |
f4b0c72e QY |
430 | return NULL; |
431 | } | |
432 | ||
1eb5e8dc QY |
433 | /** |
434 | * Creates an edge betwen two nodes, unless there is already an edge to an | |
435 | * equivalent node. | |
436 | * | |
437 | * The first node's out edges are searched to see if any of them point to a | |
438 | * node that is equivalent to the second node. If such a node exists, it is | |
439 | * returned. Otherwise an edge is created from the first node to the second. | |
440 | * | |
441 | * @param from start node for edge | |
442 | * @param to end node for edge | |
443 | * @return the node which the new edge points to | |
444 | */ | |
51fc9379 | 445 | static struct graph_node * |
1eb5e8dc | 446 | add_edge_dedup (struct graph_node *from, struct graph_node *to) |
f4b0c72e | 447 | { |
1eb5e8dc | 448 | struct graph_node *existing = node_adjacent (from, to); |
ce882f81 QY |
449 | if (existing) |
450 | { | |
451 | struct cmd_token *ex_tok = existing->data; | |
452 | struct cmd_token *to_tok = to->data; | |
453 | // NORMAL takes precedence over DEPRECATED takes precedence over HIDDEN | |
454 | ex_tok->attr = (ex_tok->attr < to_tok->attr) ? ex_tok->attr : to_tok->attr; | |
455 | return existing; | |
456 | } | |
457 | else | |
458 | return graph_add_edge (from, to); | |
f4b0c72e | 459 | } |
0aa2c2ff | 460 | |
1eb5e8dc QY |
461 | /** |
462 | * Compares two cmd_token's for equality, | |
463 | * | |
464 | * As such, this function is the working definition of token equality | |
465 | * for parsing purposes and determines overall graph structure. | |
466 | */ | |
51fc9379 | 467 | static int |
d0bfb22c | 468 | cmp_token (struct cmd_token *first, struct cmd_token *second) |
0aa2c2ff | 469 | { |
51fc9379 QY |
470 | // compare types |
471 | if (first->type != second->type) return 0; | |
472 | ||
473 | switch (first->type) { | |
1eb5e8dc QY |
474 | case WORD_TKN: |
475 | case VARIABLE_TKN: | |
e52c05cd QY |
476 | if (first->text && second->text) |
477 | { | |
478 | if (strcmp (first->text, second->text)) | |
97f13f08 | 479 | return 0; |
e52c05cd | 480 | } |
51fc9379 QY |
481 | else if (first->text != second->text) return 0; |
482 | break; | |
1eb5e8dc | 483 | case RANGE_TKN: |
51fc9379 QY |
484 | if (first->min != second->min || first->max != second->max) |
485 | return 0; | |
486 | break; | |
55589d30 QY |
487 | /* selectors and options should be equal if their subgraphs are equal, |
488 | * but the graph isomorphism problem is not known to be solvable in | |
489 | * polynomial time so we consider selectors and options inequal in all | |
490 | * cases; ultimately this forks the graph, but the matcher can handle | |
491 | * this regardless | |
51fc9379 | 492 | */ |
0bf5b1cb | 493 | case FORK_TKN: |
51fc9379 | 494 | return 0; |
1eb5e8dc | 495 | |
51fc9379 | 496 | /* end nodes are always considered equal, since each node may only |
1eb5e8dc | 497 | * have one END_TKN child at a time |
51fc9379 | 498 | */ |
1eb5e8dc QY |
499 | case START_TKN: |
500 | case END_TKN: | |
0bf5b1cb | 501 | case JOIN_TKN: |
51fc9379 QY |
502 | default: |
503 | break; | |
504 | } | |
505 | return 1; | |
0aa2c2ff | 506 | } |