/* $OpenBSD: cmd-parse.y,v 1.52 2024/11/18 08:29:35 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ %{ #include #include #include #include #include #include #include #include #include "tmux.h" static int yylex(void); static int yyparse(void); static int printflike(1,2) yyerror(const char *, ...); static char *yylex_token(int); static char *yylex_format(void); struct cmd_parse_scope { int flag; TAILQ_ENTRY (cmd_parse_scope) entry; }; enum cmd_parse_argument_type { CMD_PARSE_STRING, CMD_PARSE_COMMANDS, CMD_PARSE_PARSED_COMMANDS }; struct cmd_parse_argument { enum cmd_parse_argument_type type; char *string; struct cmd_parse_commands *commands; struct cmd_list *cmdlist; TAILQ_ENTRY(cmd_parse_argument) entry; }; TAILQ_HEAD(cmd_parse_arguments, cmd_parse_argument); struct cmd_parse_command { u_int line; struct cmd_parse_arguments arguments; TAILQ_ENTRY(cmd_parse_command) entry; }; TAILQ_HEAD(cmd_parse_commands, cmd_parse_command); struct cmd_parse_state { FILE *f; const char *buf; size_t len; size_t off; int condition; int eol; int eof; struct cmd_parse_input *input; u_int escapes; char *error; struct cmd_parse_commands *commands; struct cmd_parse_scope *scope; TAILQ_HEAD(, cmd_parse_scope) stack; }; static struct cmd_parse_state parse_state; static char *cmd_parse_get_error(const char *, u_int, const char *); static void cmd_parse_free_command(struct cmd_parse_command *); static struct cmd_parse_commands *cmd_parse_new_commands(void); static void cmd_parse_free_commands(struct cmd_parse_commands *); static void cmd_parse_build_commands(struct cmd_parse_commands *, struct cmd_parse_input *, struct cmd_parse_result *); static void cmd_parse_print_commands(struct cmd_parse_input *, struct cmd_list *); %} %union { char *token; struct cmd_parse_arguments *arguments; struct cmd_parse_argument *argument; int flag; struct { int flag; struct cmd_parse_commands *commands; } elif; struct cmd_parse_commands *commands; struct cmd_parse_command *command; } %token ERROR %token HIDDEN %token IF %token ELSE %token ELIF %token ENDIF %token FORMAT TOKEN EQUALS %type expanded format %type arguments %type argument %type if_open if_elif %type elif elif1 %type argument_statements statements statement %type commands condition condition1 %type command %% lines : /* empty */ | statements { struct cmd_parse_state *ps = &parse_state; ps->commands = $1; } statements : statement '\n' { $$ = $1; } | statements statement '\n' { $$ = $1; TAILQ_CONCAT($$, $2, entry); free($2); } statement : /* empty */ { $$ = xmalloc (sizeof *$$); TAILQ_INIT($$); } | hidden_assignment { $$ = xmalloc (sizeof *$$); TAILQ_INIT($$); } | condition { struct cmd_parse_state *ps = &parse_state; if (ps->scope == NULL || ps->scope->flag) $$ = $1; else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($1); } } | commands { struct cmd_parse_state *ps = &parse_state; if (ps->scope == NULL || ps->scope->flag) $$ = $1; else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($1); } } format : FORMAT { $$ = $1; } | TOKEN { $$ = $1; } expanded : format { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_input *pi = ps->input; struct format_tree *ft; struct client *c = pi->c; struct cmd_find_state *fsp; struct cmd_find_state fs; int flags = FORMAT_NOJOBS; if (cmd_find_valid_state(&pi->fs)) fsp = &pi->fs; else { cmd_find_from_client(&fs, c, 0); fsp = &fs; } ft = format_create(NULL, pi->item, FORMAT_NONE, flags); format_defaults(ft, c, fsp->s, fsp->wl, fsp->wp); $$ = format_expand(ft, $1); format_free(ft); free($1); } optional_assignment : /* empty */ | assignment assignment : EQUALS { struct cmd_parse_state *ps = &parse_state; int flags = ps->input->flags; int flag = 1; struct cmd_parse_scope *scope; if (ps->scope != NULL) { flag = ps->scope->flag; TAILQ_FOREACH(scope, &ps->stack, entry) flag = flag && scope->flag; } if ((~flags & CMD_PARSE_PARSEONLY) && flag) environ_put(global_environ, $1, 0); free($1); } hidden_assignment : HIDDEN EQUALS { struct cmd_parse_state *ps = &parse_state; int flags = ps->input->flags; int flag = 1; struct cmd_parse_scope *scope; if (ps->scope != NULL) { flag = ps->scope->flag; TAILQ_FOREACH(scope, &ps->stack, entry) flag = flag && scope->flag; } if ((~flags & CMD_PARSE_PARSEONLY) && flag) environ_put(global_environ, $2, ENVIRON_HIDDEN); free($2); } if_open : IF expanded { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_scope *scope; scope = xmalloc(sizeof *scope); $$ = scope->flag = format_true($2); free($2); if (ps->scope != NULL) TAILQ_INSERT_HEAD(&ps->stack, ps->scope, entry); ps->scope = scope; } if_else : ELSE { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_scope *scope; scope = xmalloc(sizeof *scope); scope->flag = !ps->scope->flag; free(ps->scope); ps->scope = scope; } if_elif : ELIF expanded { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_scope *scope; scope = xmalloc(sizeof *scope); $$ = scope->flag = format_true($2); free($2); free(ps->scope); ps->scope = scope; } if_close : ENDIF { struct cmd_parse_state *ps = &parse_state; free(ps->scope); ps->scope = TAILQ_FIRST(&ps->stack); if (ps->scope != NULL) TAILQ_REMOVE(&ps->stack, ps->scope, entry); } condition : if_open '\n' statements if_close { if ($1) $$ = $3; else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($3); } } | if_open '\n' statements if_else '\n' statements if_close { if ($1) { $$ = $3; cmd_parse_free_commands($6); } else { $$ = $6; cmd_parse_free_commands($3); } } | if_open '\n' statements elif if_close { if ($1) { $$ = $3; cmd_parse_free_commands($4.commands); } else if ($4.flag) { $$ = $4.commands; cmd_parse_free_commands($3); } else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($3); cmd_parse_free_commands($4.commands); } } | if_open '\n' statements elif if_else '\n' statements if_close { if ($1) { $$ = $3; cmd_parse_free_commands($4.commands); cmd_parse_free_commands($7); } else if ($4.flag) { $$ = $4.commands; cmd_parse_free_commands($3); cmd_parse_free_commands($7); } else { $$ = $7; cmd_parse_free_commands($3); cmd_parse_free_commands($4.commands); } } elif : if_elif '\n' statements { if ($1) { $$.flag = 1; $$.commands = $3; } else { $$.flag = 0; $$.commands = cmd_parse_new_commands(); cmd_parse_free_commands($3); } } | if_elif '\n' statements elif { if ($1) { $$.flag = 1; $$.commands = $3; cmd_parse_free_commands($4.commands); } else if ($4.flag) { $$.flag = 1; $$.commands = $4.commands; cmd_parse_free_commands($3); } else { $$.flag = 0; $$.commands = cmd_parse_new_commands(); cmd_parse_free_commands($3); cmd_parse_free_commands($4.commands); } } commands : command { struct cmd_parse_state *ps = &parse_state; $$ = cmd_parse_new_commands(); if (!TAILQ_EMPTY(&$1->arguments) && (ps->scope == NULL || ps->scope->flag)) TAILQ_INSERT_TAIL($$, $1, entry); else cmd_parse_free_command($1); } | commands ';' { $$ = $1; } | commands ';' condition1 { $$ = $1; TAILQ_CONCAT($$, $3, entry); free($3); } | commands ';' command { struct cmd_parse_state *ps = &parse_state; if (!TAILQ_EMPTY(&$3->arguments) && (ps->scope == NULL || ps->scope->flag)) { $$ = $1; TAILQ_INSERT_TAIL($$, $3, entry); } else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($1); cmd_parse_free_command($3); } } | condition1 { $$ = $1; } command : assignment { struct cmd_parse_state *ps = &parse_state; $$ = xcalloc(1, sizeof *$$); $$->line = ps->input->line; TAILQ_INIT(&$$->arguments); } | optional_assignment TOKEN { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_argument *arg; $$ = xcalloc(1, sizeof *$$); $$->line = ps->input->line; TAILQ_INIT(&$$->arguments); arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; arg->string = $2; TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); } | optional_assignment TOKEN arguments { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_argument *arg; $$ = xcalloc(1, sizeof *$$); $$->line = ps->input->line; TAILQ_INIT(&$$->arguments); TAILQ_CONCAT(&$$->arguments, $3, entry); free($3); arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; arg->string = $2; TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); } condition1 : if_open commands if_close { if ($1) $$ = $2; else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($2); } } | if_open commands if_else commands if_close { if ($1) { $$ = $2; cmd_parse_free_commands($4); } else { $$ = $4; cmd_parse_free_commands($2); } } | if_open commands elif1 if_close { if ($1) { $$ = $2; cmd_parse_free_commands($3.commands); } else if ($3.flag) { $$ = $3.commands; cmd_parse_free_commands($2); } else { $$ = cmd_parse_new_commands(); cmd_parse_free_commands($2); cmd_parse_free_commands($3.commands); } } | if_open commands elif1 if_else commands if_close { if ($1) { $$ = $2; cmd_parse_free_commands($3.commands); cmd_parse_free_commands($5); } else if ($3.flag) { $$ = $3.commands; cmd_parse_free_commands($2); cmd_parse_free_commands($5); } else { $$ = $5; cmd_parse_free_commands($2); cmd_parse_free_commands($3.commands); } } elif1 : if_elif commands { if ($1) { $$.flag = 1; $$.commands = $2; } else { $$.flag = 0; $$.commands = cmd_parse_new_commands(); cmd_parse_free_commands($2); } } | if_elif commands elif1 { if ($1) { $$.flag = 1; $$.commands = $2; cmd_parse_free_commands($3.commands); } else if ($3.flag) { $$.flag = 1; $$.commands = $3.commands; cmd_parse_free_commands($2); } else { $$.flag = 0; $$.commands = cmd_parse_new_commands(); cmd_parse_free_commands($2); cmd_parse_free_commands($3.commands); } } arguments : argument { $$ = xcalloc(1, sizeof *$$); TAILQ_INIT($$); TAILQ_INSERT_HEAD($$, $1, entry); } | argument arguments { TAILQ_INSERT_HEAD($2, $1, entry); $$ = $2; } argument : TOKEN { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_STRING; $$->string = $1; } | EQUALS { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_STRING; $$->string = $1; } | '{' argument_statements { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_COMMANDS; $$->commands = $2; } argument_statements : statement '}' { $$ = $1; } | statements statement '}' { $$ = $1; TAILQ_CONCAT($$, $2, entry); free($2); } %% static char * cmd_parse_get_error(const char *file, u_int line, const char *error) { char *s; if (file == NULL) s = xstrdup(error); else xasprintf(&s, "%s:%u: %s", file, line, error); return (s); } static void cmd_parse_print_commands(struct cmd_parse_input *pi, struct cmd_list *cmdlist) { char *s; if (pi->item == NULL || (~pi->flags & CMD_PARSE_VERBOSE)) return; s = cmd_list_print(cmdlist, 0); if (pi->file != NULL) cmdq_print(pi->item, "%s:%u: %s", pi->file, pi->line, s); else cmdq_print(pi->item, "%u: %s", pi->line, s); free(s); } static void cmd_parse_free_argument(struct cmd_parse_argument *arg) { switch (arg->type) { case CMD_PARSE_STRING: free(arg->string); break; case CMD_PARSE_COMMANDS: cmd_parse_free_commands(arg->commands); break; case CMD_PARSE_PARSED_COMMANDS: cmd_list_free(arg->cmdlist); break; } free(arg); } static void cmd_parse_free_arguments(struct cmd_parse_arguments *args) { struct cmd_parse_argument *arg, *arg1; TAILQ_FOREACH_SAFE(arg, args, entry, arg1) { TAILQ_REMOVE(args, arg, entry); cmd_parse_free_argument(arg); } } static void cmd_parse_free_command(struct cmd_parse_command *cmd) { cmd_parse_free_arguments(&cmd->arguments); free(cmd); } static struct cmd_parse_commands * cmd_parse_new_commands(void) { struct cmd_parse_commands *cmds; cmds = xmalloc(sizeof *cmds); TAILQ_INIT(cmds); return (cmds); } static void cmd_parse_free_commands(struct cmd_parse_commands *cmds) { struct cmd_parse_command *cmd, *cmd1; TAILQ_FOREACH_SAFE(cmd, cmds, entry, cmd1) { TAILQ_REMOVE(cmds, cmd, entry); cmd_parse_free_command(cmd); } free(cmds); } static struct cmd_parse_commands * cmd_parse_run_parser(char **cause) { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_scope *scope, *scope1; int retval; ps->commands = NULL; TAILQ_INIT(&ps->stack); retval = yyparse(); TAILQ_FOREACH_SAFE(scope, &ps->stack, entry, scope1) { TAILQ_REMOVE(&ps->stack, scope, entry); free(scope); } if (retval != 0) { *cause = ps->error; return (NULL); } if (ps->commands == NULL) return (cmd_parse_new_commands()); return (ps->commands); } static struct cmd_parse_commands * cmd_parse_do_file(FILE *f, struct cmd_parse_input *pi, char **cause) { struct cmd_parse_state *ps = &parse_state; memset(ps, 0, sizeof *ps); ps->input = pi; ps->f = f; return (cmd_parse_run_parser(cause)); } static struct cmd_parse_commands * cmd_parse_do_buffer(const char *buf, size_t len, struct cmd_parse_input *pi, char **cause) { struct cmd_parse_state *ps = &parse_state; memset(ps, 0, sizeof *ps); ps->input = pi; ps->buf = buf; ps->len = len; return (cmd_parse_run_parser(cause)); } static void cmd_parse_log_commands(struct cmd_parse_commands *cmds, const char *prefix) { struct cmd_parse_command *cmd; struct cmd_parse_argument *arg; u_int i, j; char *s; i = 0; TAILQ_FOREACH(cmd, cmds, entry) { j = 0; TAILQ_FOREACH(arg, &cmd->arguments, entry) { switch (arg->type) { case CMD_PARSE_STRING: log_debug("%s %u:%u: %s", prefix, i, j, arg->string); break; case CMD_PARSE_COMMANDS: xasprintf(&s, "%s %u:%u", prefix, i, j); cmd_parse_log_commands(arg->commands, s); free(s); break; case CMD_PARSE_PARSED_COMMANDS: s = cmd_list_print(arg->cmdlist, 0); log_debug("%s %u:%u: %s", prefix, i, j, s); free(s); break; } j++; } i++; } } static int cmd_parse_expand_alias(struct cmd_parse_command *cmd, struct cmd_parse_input *pi, struct cmd_parse_result *pr) { struct cmd_parse_argument *arg, *arg1, *first; struct cmd_parse_commands *cmds; struct cmd_parse_command *last; char *alias, *name, *cause; if (pi->flags & CMD_PARSE_NOALIAS) return (0); memset(pr, 0, sizeof *pr); first = TAILQ_FIRST(&cmd->arguments); if (first == NULL || first->type != CMD_PARSE_STRING) { pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); return (1); } name = first->string; alias = cmd_get_alias(name); if (alias == NULL) return (0); log_debug("%s: %u alias %s = %s", __func__, pi->line, name, alias); cmds = cmd_parse_do_buffer(alias, strlen(alias), pi, &cause); free(alias); if (cmds == NULL) { pr->status = CMD_PARSE_ERROR; pr->error = cause; return (1); } last = TAILQ_LAST(cmds, cmd_parse_commands); if (last == NULL) { pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); return (1); } TAILQ_REMOVE(&cmd->arguments, first, entry); cmd_parse_free_argument(first); TAILQ_FOREACH_SAFE(arg, &cmd->arguments, entry, arg1) { TAILQ_REMOVE(&cmd->arguments, arg, entry); TAILQ_INSERT_TAIL(&last->arguments, arg, entry); } cmd_parse_log_commands(cmds, __func__); pi->flags |= CMD_PARSE_NOALIAS; cmd_parse_build_commands(cmds, pi, pr); pi->flags &= ~CMD_PARSE_NOALIAS; return (1); } static void cmd_parse_build_command(struct cmd_parse_command *cmd, struct cmd_parse_input *pi, struct cmd_parse_result *pr) { struct cmd_parse_argument *arg; struct cmd *add; char *cause; struct args_value *values = NULL; u_int count = 0, idx; memset(pr, 0, sizeof *pr); if (cmd_parse_expand_alias(cmd, pi, pr)) return; TAILQ_FOREACH(arg, &cmd->arguments, entry) { values = xrecallocarray(values, count, count + 1, sizeof *values); switch (arg->type) { case CMD_PARSE_STRING: values[count].type = ARGS_STRING; values[count].string = xstrdup(arg->string); break; case CMD_PARSE_COMMANDS: cmd_parse_build_commands(arg->commands, pi, pr); if (pr->status != CMD_PARSE_SUCCESS) goto out; values[count].type = ARGS_COMMANDS; values[count].cmdlist = pr->cmdlist; break; case CMD_PARSE_PARSED_COMMANDS: values[count].type = ARGS_COMMANDS; values[count].cmdlist = arg->cmdlist; values[count].cmdlist->references++; break; } count++; } add = cmd_parse(values, count, pi->file, pi->line, &cause); if (add == NULL) { pr->status = CMD_PARSE_ERROR; pr->error = cmd_parse_get_error(pi->file, pi->line, cause); free(cause); goto out; } pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); cmd_list_append(pr->cmdlist, add); out: for (idx = 0; idx < count; idx++) args_free_value(&values[idx]); free(values); } static void cmd_parse_build_commands(struct cmd_parse_commands *cmds, struct cmd_parse_input *pi, struct cmd_parse_result *pr) { struct cmd_parse_command *cmd; u_int line = UINT_MAX; struct cmd_list *current = NULL, *result; char *s; memset(pr, 0, sizeof *pr); /* Check for an empty list. */ if (TAILQ_EMPTY(cmds)) { pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); return; } cmd_parse_log_commands(cmds, __func__); /* * Parse each command into a command list. Create a new command list * for each line (unless the flag is set) so they get a new group (so * the queue knows which ones to remove if a command fails when * executed). */ result = cmd_list_new(); TAILQ_FOREACH(cmd, cmds, entry) { if (((~pi->flags & CMD_PARSE_ONEGROUP) && cmd->line != line)) { if (current != NULL) { cmd_parse_print_commands(pi, current); cmd_list_move(result, current); cmd_list_free(current); } current = cmd_list_new(); } if (current == NULL) current = cmd_list_new(); line = pi->line = cmd->line; cmd_parse_build_command(cmd, pi, pr); if (pr->status != CMD_PARSE_SUCCESS) { cmd_list_free(result); cmd_list_free(current); return; } cmd_list_append_all(current, pr->cmdlist); cmd_list_free(pr->cmdlist); } if (current != NULL) { cmd_parse_print_commands(pi, current); cmd_list_move(result, current); cmd_list_free(current); } s = cmd_list_print(result, 0); log_debug("%s: %s", __func__, s); free(s); pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = result; } struct cmd_parse_result * cmd_parse_from_file(FILE *f, struct cmd_parse_input *pi) { static struct cmd_parse_result pr; struct cmd_parse_input input; struct cmd_parse_commands *cmds; char *cause; if (pi == NULL) { memset(&input, 0, sizeof input); pi = &input; } memset(&pr, 0, sizeof pr); cmds = cmd_parse_do_file(f, pi, &cause); if (cmds == NULL) { pr.status = CMD_PARSE_ERROR; pr.error = cause; return (&pr); } cmd_parse_build_commands(cmds, pi, &pr); cmd_parse_free_commands(cmds); return (&pr); } struct cmd_parse_result * cmd_parse_from_string(const char *s, struct cmd_parse_input *pi) { struct cmd_parse_input input; if (pi == NULL) { memset(&input, 0, sizeof input); pi = &input; } /* * When parsing a string, put commands in one group even if there are * multiple lines. This means { a \n b } is identical to "a ; b" when * given as an argument to another command. */ pi->flags |= CMD_PARSE_ONEGROUP; return (cmd_parse_from_buffer(s, strlen(s), pi)); } enum cmd_parse_status cmd_parse_and_insert(const char *s, struct cmd_parse_input *pi, struct cmdq_item *after, struct cmdq_state *state, char **error) { struct cmd_parse_result *pr; struct cmdq_item *item; pr = cmd_parse_from_string(s, pi); switch (pr->status) { case CMD_PARSE_ERROR: if (error != NULL) *error = pr->error; else free(pr->error); break; case CMD_PARSE_SUCCESS: item = cmdq_get_command(pr->cmdlist, state); cmdq_insert_after(after, item); cmd_list_free(pr->cmdlist); break; } return (pr->status); } enum cmd_parse_status cmd_parse_and_append(const char *s, struct cmd_parse_input *pi, struct client *c, struct cmdq_state *state, char **error) { struct cmd_parse_result *pr; struct cmdq_item *item; pr = cmd_parse_from_string(s, pi); switch (pr->status) { case CMD_PARSE_ERROR: if (error != NULL) *error = pr->error; else free(pr->error); break; case CMD_PARSE_SUCCESS: item = cmdq_get_command(pr->cmdlist, state); cmdq_append(c, item); cmd_list_free(pr->cmdlist); break; } return (pr->status); } struct cmd_parse_result * cmd_parse_from_buffer(const void *buf, size_t len, struct cmd_parse_input *pi) { static struct cmd_parse_result pr; struct cmd_parse_input input; struct cmd_parse_commands *cmds; char *cause; if (pi == NULL) { memset(&input, 0, sizeof input); pi = &input; } memset(&pr, 0, sizeof pr); if (len == 0) { pr.status = CMD_PARSE_SUCCESS; pr.cmdlist = cmd_list_new(); return (&pr); } cmds = cmd_parse_do_buffer(buf, len, pi, &cause); if (cmds == NULL) { pr.status = CMD_PARSE_ERROR; pr.error = cause; return (&pr); } cmd_parse_build_commands(cmds, pi, &pr); cmd_parse_free_commands(cmds); return (&pr); } struct cmd_parse_result * cmd_parse_from_arguments(struct args_value *values, u_int count, struct cmd_parse_input *pi) { static struct cmd_parse_result pr; struct cmd_parse_input input; struct cmd_parse_commands *cmds; struct cmd_parse_command *cmd; struct cmd_parse_argument *arg; u_int i; char *copy; size_t size; int end; /* * The commands are already split up into arguments, so just separate * into a set of commands by ';'. */ if (pi == NULL) { memset(&input, 0, sizeof input); pi = &input; } memset(&pr, 0, sizeof pr); cmds = cmd_parse_new_commands(); cmd = xcalloc(1, sizeof *cmd); cmd->line = pi->line; TAILQ_INIT(&cmd->arguments); for (i = 0; i < count; i++) { end = 0; if (values[i].type == ARGS_STRING) { copy = xstrdup(values[i].string); size = strlen(copy); if (size != 0 && copy[size - 1] == ';') { copy[--size] = '\0'; if (size > 0 && copy[size - 1] == '\\') copy[size - 1] = ';'; else end = 1; } if (!end || size != 0) { arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; arg->string = copy; TAILQ_INSERT_TAIL(&cmd->arguments, arg, entry); } else free(copy); } else if (values[i].type == ARGS_COMMANDS) { arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_PARSED_COMMANDS; arg->cmdlist = values[i].cmdlist; arg->cmdlist->references++; TAILQ_INSERT_TAIL(&cmd->arguments, arg, entry); } else fatalx("unknown argument type"); if (end) { TAILQ_INSERT_TAIL(cmds, cmd, entry); cmd = xcalloc(1, sizeof *cmd); cmd->line = pi->line; TAILQ_INIT(&cmd->arguments); } } if (!TAILQ_EMPTY(&cmd->arguments)) TAILQ_INSERT_TAIL(cmds, cmd, entry); else free(cmd); cmd_parse_build_commands(cmds, pi, &pr); cmd_parse_free_commands(cmds); return (&pr); } static int printflike(1, 2) yyerror(const char *fmt, ...) { struct cmd_parse_state *ps = &parse_state; struct cmd_parse_input *pi = ps->input; va_list ap; char *error; if (ps->error != NULL) return (0); va_start(ap, fmt); xvasprintf(&error, fmt, ap); va_end(ap); ps->error = cmd_parse_get_error(pi->file, pi->line, error); free(error); return (0); } static int yylex_is_var(char ch, int first) { if (ch == '=') return (0); if (first && isdigit((u_char)ch)) return (0); return (isalnum((u_char)ch) || ch == '_'); } static void yylex_append(char **buf, size_t *len, const char *add, size_t addlen) { if (addlen > SIZE_MAX - 1 || *len > SIZE_MAX - 1 - addlen) fatalx("buffer is too big"); *buf = xrealloc(*buf, (*len) + 1 + addlen); memcpy((*buf) + *len, add, addlen); (*len) += addlen; } static void yylex_append1(char **buf, size_t *len, char add) { yylex_append(buf, len, &add, 1); } static int yylex_getc1(void) { struct cmd_parse_state *ps = &parse_state; int ch; if (ps->f != NULL) ch = getc(ps->f); else { if (ps->off == ps->len) ch = EOF; else ch = ps->buf[ps->off++]; } return (ch); } static void yylex_ungetc(int ch) { struct cmd_parse_state *ps = &parse_state; if (ps->f != NULL) ungetc(ch, ps->f); else if (ps->off > 0 && ch != EOF) ps->off--; } static int yylex_getc(void) { struct cmd_parse_state *ps = &parse_state; int ch; if (ps->escapes != 0) { ps->escapes--; return ('\\'); } for (;;) { ch = yylex_getc1(); if (ch == '\\') { ps->escapes++; continue; } if (ch == '\n' && (ps->escapes % 2) == 1) { ps->input->line++; ps->escapes--; continue; } if (ps->escapes != 0) { yylex_ungetc(ch); ps->escapes--; return ('\\'); } return (ch); } } static char * yylex_get_word(int ch) { char *buf; size_t len; len = 0; buf = xmalloc(1); do yylex_append1(&buf, &len, ch); while ((ch = yylex_getc()) != EOF && strchr(" \t\n", ch) == NULL); yylex_ungetc(ch); buf[len] = '\0'; log_debug("%s: %s", __func__, buf); return (buf); } static int yylex(void) { struct cmd_parse_state *ps = &parse_state; char *token, *cp; int ch, next, condition; if (ps->eol) ps->input->line++; ps->eol = 0; condition = ps->condition; ps->condition = 0; for (;;) { ch = yylex_getc(); if (ch == EOF) { /* * Ensure every file or string is terminated by a * newline. This keeps the parser simpler and avoids * having to add a newline to each string. */ if (ps->eof) break; ps->eof = 1; return ('\n'); } if (ch == ' ' || ch == '\t') { /* * Ignore whitespace. */ continue; } if (ch == '\r') { /* * Treat \r\n as \n. */ ch = yylex_getc(); if (ch != '\n') { yylex_ungetc(ch); ch = '\r'; } } if (ch == '\n') { /* * End of line. Update the line number. */ ps->eol = 1; return ('\n'); } if (ch == ';' || ch == '{' || ch == '}') { /* * A semicolon or { or } is itself. */ return (ch); } if (ch == '#') { /* * #{ after a condition opens a format; anything else * is a comment, ignore up to the end of the line. */ next = yylex_getc(); if (condition && next == '{') { yylval.token = yylex_format(); if (yylval.token == NULL) return (ERROR); return (FORMAT); } while (next != '\n' && next != EOF) next = yylex_getc(); if (next == '\n') { ps->input->line++; return ('\n'); } continue; } if (ch == '%') { /* * % is a condition unless it is all % or all numbers, * then it is a token. */ yylval.token = yylex_get_word('%'); for (cp = yylval.token; *cp != '\0'; cp++) { if (*cp != '%' && !isdigit((u_char)*cp)) break; } if (*cp == '\0') return (TOKEN); ps->condition = 1; if (strcmp(yylval.token, "%hidden") == 0) { free(yylval.token); return (HIDDEN); } if (strcmp(yylval.token, "%if") == 0) { free(yylval.token); return (IF); } if (strcmp(yylval.token, "%else") == 0) { free(yylval.token); return (ELSE); } if (strcmp(yylval.token, "%elif") == 0) { free(yylval.token); return (ELIF); } if (strcmp(yylval.token, "%endif") == 0) { free(yylval.token); return (ENDIF); } free(yylval.token); return (ERROR); } /* * Otherwise this is a token. */ token = yylex_token(ch); if (token == NULL) return (ERROR); yylval.token = token; if (strchr(token, '=') != NULL && yylex_is_var(*token, 1)) { for (cp = token + 1; *cp != '='; cp++) { if (!yylex_is_var(*cp, 0)) break; } if (*cp == '=') return (EQUALS); } return (TOKEN); } return (0); } static char * yylex_format(void) { char *buf; size_t len; int ch, brackets = 1; len = 0; buf = xmalloc(1); yylex_append(&buf, &len, "#{", 2); for (;;) { if ((ch = yylex_getc()) == EOF || ch == '\n') goto error; if (ch == '#') { if ((ch = yylex_getc()) == EOF || ch == '\n') goto error; if (ch == '{') brackets++; yylex_append1(&buf, &len, '#'); } else if (ch == '}') { if (brackets != 0 && --brackets == 0) { yylex_append1(&buf, &len, ch); break; } } yylex_append1(&buf, &len, ch); } if (brackets != 0) goto error; buf[len] = '\0'; log_debug("%s: %s", __func__, buf); return (buf); error: free(buf); return (NULL); } static int yylex_token_escape(char **buf, size_t *len) { int ch, type, o2, o3, mlen; u_int size, i, tmp; char s[9], m[MB_LEN_MAX]; ch = yylex_getc(); if (ch >= '4' && ch <= '7') { yyerror("invalid octal escape"); return (0); } if (ch >= '0' && ch <= '3') { o2 = yylex_getc(); if (o2 >= '0' && o2 <= '7') { o3 = yylex_getc(); if (o3 >= '0' && o3 <= '7') { ch = 64 * (ch - '0') + 8 * (o2 - '0') + (o3 - '0'); yylex_append1(buf, len, ch); return (1); } } yyerror("invalid octal escape"); return (0); } switch (ch) { case EOF: return (0); case 'a': ch = '\a'; break; case 'b': ch = '\b'; break; case 'e': ch = '\033'; break; case 'f': ch = '\f'; break; case 's': ch = ' '; break; case 'v': ch = '\v'; break; case 'r': ch = '\r'; break; case 'n': ch = '\n'; break; case 't': ch = '\t'; break; case 'u': type = 'u'; size = 4; goto unicode; case 'U': type = 'U'; size = 8; goto unicode; } yylex_append1(buf, len, ch); return (1); unicode: for (i = 0; i < size; i++) { ch = yylex_getc(); if (ch == EOF || ch == '\n') return (0); if (!isxdigit((u_char)ch)) { yyerror("invalid \\%c argument", type); return (0); } s[i] = ch; } s[i] = '\0'; if ((size == 4 && sscanf(s, "%4x", &tmp) != 1) || (size == 8 && sscanf(s, "%8x", &tmp) != 1)) { yyerror("invalid \\%c argument", type); return (0); } mlen = wctomb(m, tmp); if (mlen <= 0 || mlen > (int)sizeof m) { yyerror("invalid \\%c argument", type); return (0); } yylex_append(buf, len, m, mlen); return (1); } static int yylex_token_variable(char **buf, size_t *len) { struct environ_entry *envent; int ch, brackets = 0; char name[1024]; size_t namelen = 0; const char *value; ch = yylex_getc(); if (ch == EOF) return (0); if (ch == '{') brackets = 1; else { if (!yylex_is_var(ch, 1)) { yylex_append1(buf, len, '$'); yylex_ungetc(ch); return (1); } name[namelen++] = ch; } for (;;) { ch = yylex_getc(); if (brackets && ch == '}') break; if (ch == EOF || !yylex_is_var(ch, 0)) { if (!brackets) { yylex_ungetc(ch); break; } yyerror("invalid environment variable"); return (0); } if (namelen == (sizeof name) - 2) { yyerror("environment variable is too long"); return (0); } name[namelen++] = ch; } name[namelen] = '\0'; envent = environ_find(global_environ, name); if (envent != NULL && envent->value != NULL) { value = envent->value; log_debug("%s: %s -> %s", __func__, name, value); yylex_append(buf, len, value, strlen(value)); } return (1); } static int yylex_token_tilde(char **buf, size_t *len) { struct environ_entry *envent; int ch; char name[1024]; size_t namelen = 0; struct passwd *pw; const char *home = NULL; for (;;) { ch = yylex_getc(); if (ch == EOF || strchr("/ \t\n\"'", ch) != NULL) { yylex_ungetc(ch); break; } if (namelen == (sizeof name) - 2) { yyerror("user name is too long"); return (0); } name[namelen++] = ch; } name[namelen] = '\0'; if (*name == '\0') { envent = environ_find(global_environ, "HOME"); if (envent != NULL && *envent->value != '\0') home = envent->value; else if ((pw = getpwuid(getuid())) != NULL) home = pw->pw_dir; } else { if ((pw = getpwnam(name)) != NULL) home = pw->pw_dir; } if (home == NULL) return (0); log_debug("%s: ~%s -> %s", __func__, name, home); yylex_append(buf, len, home, strlen(home)); return (1); } static char * yylex_token(int ch) { char *buf; size_t len; enum { START, NONE, DOUBLE_QUOTES, SINGLE_QUOTES } state = NONE, last = START; len = 0; buf = xmalloc(1); for (;;) { /* EOF or \n are always the end of the token. */ if (ch == EOF) { log_debug("%s: end at EOF", __func__); break; } if (state == NONE && ch == '\r') { ch = yylex_getc(); if (ch != '\n') { yylex_ungetc(ch); ch = '\r'; } } if (state == NONE && ch == '\n') { log_debug("%s: end at EOL", __func__); break; } /* Whitespace or ; or } ends a token unless inside quotes. */ if (state == NONE && (ch == ' ' || ch == '\t')) { log_debug("%s: end at WS", __func__); break; } if (state == NONE && (ch == ';' || ch == '}')) { log_debug("%s: end at %c", __func__, ch); break; } /* * Spaces and comments inside quotes after \n are removed but * the \n is left. */ if (ch == '\n' && state != NONE) { yylex_append1(&buf, &len, '\n'); while ((ch = yylex_getc()) == ' ' || ch == '\t') /* nothing */; if (ch != '#') continue; ch = yylex_getc(); if (strchr(",#{}:", ch) != NULL) { yylex_ungetc(ch); ch = '#'; } else { while ((ch = yylex_getc()) != '\n' && ch != EOF) /* nothing */; } continue; } /* \ ~ and $ are expanded except in single quotes. */ if (ch == '\\' && state != SINGLE_QUOTES) { if (!yylex_token_escape(&buf, &len)) goto error; goto skip; } if (ch == '~' && last != state && state != SINGLE_QUOTES) { if (!yylex_token_tilde(&buf, &len)) goto error; goto skip; } if (ch == '$' && state != SINGLE_QUOTES) { if (!yylex_token_variable(&buf, &len)) goto error; goto skip; } if (ch == '}' && state == NONE) goto error; /* unmatched (matched ones were handled) */ /* ' and " starts or end quotes (and is consumed). */ if (ch == '\'') { if (state == NONE) { state = SINGLE_QUOTES; goto next; } if (state == SINGLE_QUOTES) { state = NONE; goto next; } } if (ch == '"') { if (state == NONE) { state = DOUBLE_QUOTES; goto next; } if (state == DOUBLE_QUOTES) { state = NONE; goto next; } } /* Otherwise add the character to the buffer. */ yylex_append1(&buf, &len, ch); skip: last = state; next: ch = yylex_getc(); } yylex_ungetc(ch); buf[len] = '\0'; log_debug("%s: %s", __func__, buf); return (buf); error: free(buf); return (NULL); }