/* * Input matching routines for CLI backend. * * -- * Copyright (C) 2016 Cumulus Networks, Inc. * * This file is part of GNU Zebra. * * GNU Zebra is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2, or (at your option) any * later version. * * GNU Zebra is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; see the file COPYING; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include "command_match.h" #include "memory.h" DEFINE_MTYPE_STATIC(LIB, CMD_MATCHSTACK, "Command Match Stack") #ifdef TRACE_MATCHER #define TM 1 #else #define TM 0 #endif #define trace_matcher(...) \ do { \ if (TM) \ fprintf(stderr, __VA_ARGS__); \ } while (0); /* matcher helper prototypes */ static int add_nexthops(struct list *, struct graph_node *, struct graph_node **, size_t); static enum matcher_rv command_match_r(struct graph_node *, vector, unsigned int, struct graph_node **, struct list **); static int score_precedence(enum cmd_token_type); static enum match_type min_match_level(enum cmd_token_type); static void del_arglist(struct list *); static struct cmd_token *disambiguate_tokens(struct cmd_token *, struct cmd_token *, char *); static struct list *disambiguate(struct list *, struct list *, vector, unsigned int); int compare_completions(const void *, const void *); /* token matcher prototypes */ static enum match_type match_token(struct cmd_token *, char *); static enum match_type match_ipv4(const char *); static enum match_type match_ipv4_prefix(const char *); static enum match_type match_ipv6_prefix(const char *, bool); static enum match_type match_range(struct cmd_token *, const char *); static enum match_type match_word(struct cmd_token *, const char *); static enum match_type match_variable(struct cmd_token *, const char *); static enum match_type match_mac(const char *, bool); enum matcher_rv command_match(struct graph *cmdgraph, vector vline, struct list **argv, const struct cmd_element **el) { struct graph_node *stack[CMD_ARGC_MAX]; enum matcher_rv status; *argv = NULL; // prepend a dummy token to match that pesky start node vector vvline = vector_init(vline->alloced + 1); vector_set_index(vvline, 0, (void *)XSTRDUP(MTYPE_TMP, "dummy")); memcpy(vvline->index + 1, vline->index, sizeof(void *) * vline->alloced); vvline->active = vline->active + 1; struct graph_node *start = vector_slot(cmdgraph->nodes, 0); status = command_match_r(start, vvline, 0, stack, argv); if (status == MATCHER_OK) { // successful match struct listnode *head = listhead(*argv); struct listnode *tail = listtail(*argv); assert(head); assert(tail); // delete dummy start node cmd_token_del((struct cmd_token *)head->data); list_delete_node(*argv, head); // get cmd_element out of list tail *el = listgetdata(tail); list_delete_node(*argv, tail); // now argv is an ordered list of cmd_token matching the user // input, with each cmd_token->arg holding the corresponding // input assert(*el); } else if (*argv) { del_arglist(*argv); *argv = NULL; } if (!*el) { trace_matcher("No match\n"); } else { trace_matcher("Matched command\n->string %s\n->desc %s\n", (*el)->string, (*el)->doc); } // free the leader token we alloc'd XFREE(MTYPE_TMP, vector_slot(vvline, 0)); // free vector vector_free(vvline); return status; } /** * Builds an argument list given a DFA and a matching input line. * * First the function determines if the node it is passed matches the first * token of input. If it does not, it returns NULL (MATCHER_NO_MATCH). If it * does match, then it saves the input token as the head of an argument list. * * The next step is to see if there is further input in the input line. If * there is not, the current node's children are searched to see if any of them * are leaves (type END_TKN). If this is the case, then the bottom of the * recursion stack has been reached, the leaf is pushed onto the argument list, * the current node is pushed, and the resulting argument list is * returned (MATCHER_OK). If it is not the case, NULL is returned, indicating * that there is no match for the input along this path (MATCHER_INCOMPLETE). * * If there is further input, then the function recurses on each of the current * node's children, passing them the input line minus the token that was just * matched. For each child, the return value of the recursive call is * inspected. If it is null, then there is no match for the input along the * subgraph headed by that child. If it is not null, then there is at least one * input match in that subgraph (more on this in a moment). * * If a recursive call on a child returns a non-null value, then it has matched * the input given it on the subgraph that starts with that child. However, due * to the flexibility of the grammar, it is sometimes the case that two or more * child graphs match the same input (two or more of the recursive calls have * non-NULL return values). This is not a valid state, since only one true * match is possible. In order to resolve this conflict, the function keeps a * reference to the child node that most specifically matches the input. This * is done by assigning each node type a precedence. If a child is found to * match the remaining input, then the precedence values of the current * best-matching child and this new match are compared. The node with higher * precedence is kept, and the other match is discarded. Due to the recursive * nature of this function, it is only necessary to compare the precedence of * immediate children, since all subsequent children will already have been * disambiguated in this way. * * In the event that two children are found to match with the same precedence, * then the input is ambiguous for the passed cmd_element and NULL is returned. * * @param[in] start the start node. * @param[in] vline the vectorized input line. * @param[in] n the index of the first input token. * @return A linked list of n elements. The first n-1 elements are pointers to * struct cmd_token and represent the sequence of tokens matched by the input. * The ->arg field of each token points to a copy of the input matched on it. * The final nth element is a pointer to struct cmd_element, which is the * command that was matched. * * If no match was found, the return value is NULL. */ static enum matcher_rv command_match_r(struct graph_node *start, vector vline, unsigned int n, struct graph_node **stack, struct list **currbest) { assert(n < vector_active(vline)); enum matcher_rv status = MATCHER_NO_MATCH; // get the minimum match level that can count as a full match struct cmd_token *copy, *token = start->data; enum match_type minmatch = min_match_level(token->type); /* check history/stack of tokens * this disallows matching the same one more than once if there is a * circle in the graph (used for keyword arguments) */ if (n == CMD_ARGC_MAX) return MATCHER_NO_MATCH; if (!token->allowrepeat) for (size_t s = 0; s < n; s++) if (stack[s] == start) return MATCHER_NO_MATCH; // get the current operating input token char *input_token = vector_slot(vline, n); #ifdef TRACE_MATCHER fprintf(stdout, "\"%-20s\" matches \"%-30s\" ? ", input_token, token->text); enum match_type mt = match_token(token, input_token); fprintf(stdout, "type: %d ", token->type); fprintf(stdout, "min: %d - ", minmatch); switch (mt) { case trivial_match: fprintf(stdout, "trivial_match "); break; case no_match: fprintf(stdout, "no_match "); break; case partly_match: fprintf(stdout, "partly_match "); break; case exact_match: fprintf(stdout, "exact_match "); break; } if (mt >= minmatch) fprintf(stdout, " MATCH"); fprintf(stdout, "\n"); #endif // if we don't match this node, die if (match_token(token, input_token) < minmatch) return MATCHER_NO_MATCH; stack[n] = start; // pointers for iterating linklist struct listnode *ln; struct graph_node *gn; // get all possible nexthops struct list *next = list_new(); add_nexthops(next, start, NULL, 0); // determine the best match for (ALL_LIST_ELEMENTS_RO(next, ln, gn)) { // if we've matched all input we're looking for END_TKN if (n + 1 == vector_active(vline)) { struct cmd_token *tok = gn->data; if (tok->type == END_TKN) { // if more than one END_TKN in the follow set if (*currbest) { status = MATCHER_AMBIGUOUS; break; } else { status = MATCHER_OK; } *currbest = list_new(); // node should have one child node with the // element struct graph_node *leaf = vector_slot(gn->to, 0); // last node in the list will hold the // cmd_element; this is important because // list_delete() expects that all nodes have // the same data type, so when deleting this // list the last node must be manually deleted struct cmd_element *el = leaf->data; listnode_add(*currbest, el); (*currbest)->del = (void (*)(void *)) & cmd_token_del; // do not break immediately; continue walking // through the follow set to ensure that there // is exactly one END_TKN } continue; } // else recurse on candidate child node struct list *result = NULL; enum matcher_rv rstat = command_match_r(gn, vline, n + 1, stack, &result); // save the best match if (result && *currbest) { // pick the best of two matches struct list *newbest = disambiguate(*currbest, result, vline, n + 1); // current best and result are ambiguous if (!newbest) status = MATCHER_AMBIGUOUS; // current best is still the best, but ambiguous else if (newbest == *currbest && status == MATCHER_AMBIGUOUS) status = MATCHER_AMBIGUOUS; // result is better, but also ambiguous else if (newbest == result && rstat == MATCHER_AMBIGUOUS) status = MATCHER_AMBIGUOUS; // one or the other is superior and not ambiguous else status = MATCHER_OK; // delete the unnecessary result struct list *todelete = ((newbest && newbest == result) ? *currbest : result); del_arglist(todelete); *currbest = newbest ? newbest : *currbest; } else if (result) { status = rstat; *currbest = result; } else if (!*currbest) { status = MAX(rstat, status); } } if (*currbest) { // copy token, set arg and prepend to currbest token = start->data; copy = cmd_token_dup(token); copy->arg = XSTRDUP(MTYPE_CMD_ARG, input_token); listnode_add_before(*currbest, (*currbest)->head, copy); } else if (n + 1 == vector_active(vline) && status == MATCHER_NO_MATCH) status = MATCHER_INCOMPLETE; // cleanup list_delete_and_null(&next); return status; } static void stack_del(void *val) { XFREE(MTYPE_CMD_MATCHSTACK, val); } enum matcher_rv command_complete(struct graph *graph, vector vline, struct list **completions) { // pointer to next input token to match char *input_token; struct list * current = list_new(), // current nodes to match input token against *next = list_new(); // possible next hops after current input // token current->del = next->del = stack_del; // pointers used for iterating lists struct graph_node **gstack, **newstack; struct listnode *node; // add all children of start node to list struct graph_node *start = vector_slot(graph->nodes, 0); add_nexthops(next, start, &start, 0); unsigned int idx; for (idx = 0; idx < vector_active(vline) && next->count > 0; idx++) { list_delete_and_null(¤t); current = next; next = list_new(); next->del = stack_del; input_token = vector_slot(vline, idx); int exact_match_exists = 0; for (ALL_LIST_ELEMENTS_RO(current, node, gstack)) if (!exact_match_exists) exact_match_exists = (match_token(gstack[0]->data, input_token) == exact_match); else break; for (ALL_LIST_ELEMENTS_RO(current, node, gstack)) { struct cmd_token *token = gstack[0]->data; if (token->attr == CMD_ATTR_HIDDEN || token->attr == CMD_ATTR_DEPRECATED) continue; enum match_type minmatch = min_match_level(token->type); trace_matcher("\"%s\" matches \"%s\" (%d) ? ", input_token, token->text, token->type); unsigned int last_token = (vector_active(vline) - 1 == idx); enum match_type matchtype = match_token(token, input_token); switch (matchtype) { // occurs when last token is whitespace case trivial_match: trace_matcher("trivial_match\n"); assert(last_token); newstack = XMALLOC(MTYPE_CMD_MATCHSTACK, sizeof(struct graph_node *)); /* we're not recursing here, just the first * element is OK */ newstack[0] = gstack[0]; listnode_add(next, newstack); break; case partly_match: trace_matcher("trivial_match\n"); if (exact_match_exists && !last_token) break; /* fallthru */ case exact_match: trace_matcher("exact_match\n"); if (last_token) { newstack = XMALLOC( MTYPE_CMD_MATCHSTACK, sizeof(struct graph_node *)); /* same as above, not recursing on this */ newstack[0] = gstack[0]; listnode_add(next, newstack); } else if (matchtype >= minmatch) add_nexthops(next, gstack[0], gstack, idx + 1); break; default: trace_matcher("no_match\n"); break; } } } /* Variable summary * ----------------------------------------------------------------- * token = last input token processed * idx = index in `command` of last token processed * current = set of all transitions from the previous input token * next = set of all nodes reachable from all nodes in `matched` */ enum matcher_rv mrv = idx == vector_active(vline) && next->count ? MATCHER_OK : MATCHER_NO_MATCH; *completions = NULL; if (!MATCHER_ERROR(mrv)) { // extract cmd_token into list *completions = list_new(); for (ALL_LIST_ELEMENTS_RO(next, node, gstack)) { listnode_add(*completions, gstack[0]->data); } } list_delete_and_null(¤t); list_delete_and_null(&next); return mrv; } /** * Adds all children that are reachable by one parser hop to the given list. * special tokens except END_TKN are treated as transparent. * * @param[in] list to add the nexthops to * @param[in] node to start calculating nexthops from * @param[in] stack listing previously visited nodes, if non-NULL. * @param[in] stackpos how many valid entries are in stack * @return the number of children added to the list * * NB: non-null "stack" means that new stacks will be added to "list" as * output, instead of direct node pointers! */ static int add_nexthops(struct list *list, struct graph_node *node, struct graph_node **stack, size_t stackpos) { int added = 0; struct graph_node *child; struct graph_node **nextstack; for (unsigned int i = 0; i < vector_active(node->to); i++) { child = vector_slot(node->to, i); size_t j; struct cmd_token *token = child->data; if (!token->allowrepeat && stack) { for (j = 0; j < stackpos; j++) if (child == stack[j]) break; if (j != stackpos) continue; } if (token->type >= SPECIAL_TKN && token->type != END_TKN) { added += add_nexthops(list, child, stack, stackpos); } else { if (stack) { nextstack = XMALLOC( MTYPE_CMD_MATCHSTACK, (stackpos + 1) * sizeof(struct graph_node *)); nextstack[0] = child; memcpy(nextstack + 1, stack, stackpos * sizeof(struct graph_node *)); listnode_add(list, nextstack); } else listnode_add(list, child); added++; } } return added; } /** * Determines the node types for which a partial match may count as a full * match. Enables command abbrevations. * * @param[in] type node type * @return minimum match level needed to for a token to fully match */ static enum match_type min_match_level(enum cmd_token_type type) { switch (type) { // anything matches a start node, for the sake of recursion case START_TKN: return no_match; // allowing words to partly match enables command abbreviation case WORD_TKN: return partly_match; default: return exact_match; } } /** * Assigns precedence scores to node types. * * @param[in] type node type to score * @return precedence score */ static int score_precedence(enum cmd_token_type type) { switch (type) { // some of these are mutually exclusive, so they share // the same precedence value case IPV4_TKN: case IPV4_PREFIX_TKN: case IPV6_TKN: case IPV6_PREFIX_TKN: case MAC_TKN: case MAC_PREFIX_TKN: case RANGE_TKN: return 2; case WORD_TKN: return 3; case VARIABLE_TKN: return 4; default: return 10; } } /** * Picks the better of two possible matches for a token. * * @param[in] first candidate node matching token * @param[in] second candidate node matching token * @param[in] token the token being matched * @return the best-matching node, or NULL if the two are entirely ambiguous */ static struct cmd_token *disambiguate_tokens(struct cmd_token *first, struct cmd_token *second, char *input_token) { // if the types are different, simply go off of type precedence if (first->type != second->type) { int firstprec = score_precedence(first->type); int secndprec = score_precedence(second->type); if (firstprec != secndprec) return firstprec < secndprec ? first : second; else return NULL; } // if they're the same, return the more exact match enum match_type fmtype = match_token(first, input_token); enum match_type smtype = match_token(second, input_token); if (fmtype != smtype) return fmtype > smtype ? first : second; return NULL; } /** * Picks the better of two possible matches for an input line. * * @param[in] first candidate list of cmd_token matching vline * @param[in] second candidate list of cmd_token matching vline * @param[in] vline the input line being matched * @param[in] n index into vline to start comparing at * @return the best-matching list, or NULL if the two are entirely ambiguous */ static struct list *disambiguate(struct list *first, struct list *second, vector vline, unsigned int n) { assert(first != NULL); assert(second != NULL); // doesn't make sense for these to be inequal length assert(first->count == second->count); assert(first->count == vector_active(vline) - n + 1); struct listnode *fnode = listhead_unchecked(first), *snode = listhead_unchecked(second); struct cmd_token *ftok = listgetdata(fnode), *stok = listgetdata(snode), *best = NULL; // compare each token, if one matches better use that one for (unsigned int i = n; i < vector_active(vline); i++) { char *token = vector_slot(vline, i); if ((best = disambiguate_tokens(ftok, stok, token))) return best == ftok ? first : second; fnode = listnextnode(fnode); snode = listnextnode(snode); ftok = listgetdata(fnode); stok = listgetdata(snode); } return NULL; } /* * Deletion function for arglist. * * Since list->del for arglists expects all listnode->data to hold cmd_token, * but arglists have cmd_element as the data for the tail, this function * manually deletes the tail before deleting the rest of the list as usual. * * The cmd_element at the end is *not* a copy. It is the one and only. * * @param list the arglist to delete */ static void del_arglist(struct list *list) { // manually delete last node struct listnode *tail = listtail(list); tail->data = NULL; list_delete_node(list, tail); // delete the rest of the list as usual list_delete_and_null(&list); } /*---------- token level matching functions ----------*/ static enum match_type match_token(struct cmd_token *token, char *input_token) { // nothing trivially matches everything if (!input_token) return trivial_match; switch (token->type) { case WORD_TKN: return match_word(token, input_token); case IPV4_TKN: return match_ipv4(input_token); case IPV4_PREFIX_TKN: return match_ipv4_prefix(input_token); case IPV6_TKN: return match_ipv6_prefix(input_token, false); case IPV6_PREFIX_TKN: return match_ipv6_prefix(input_token, true); case RANGE_TKN: return match_range(token, input_token); case VARIABLE_TKN: return match_variable(token, input_token); case MAC_TKN: return match_mac(input_token, false); case MAC_PREFIX_TKN: return match_mac(input_token, true); case END_TKN: default: return no_match; } } #define IPV4_ADDR_STR "0123456789." #define IPV4_PREFIX_STR "0123456789./" static enum match_type match_ipv4(const char *str) { const char *sp; int dots = 0, nums = 0; char buf[4]; for (;;) { memset(buf, 0, sizeof(buf)); sp = str; while (*str != '\0') { if (*str == '.') { if (dots >= 3) return no_match; if (*(str + 1) == '.') return no_match; if (*(str + 1) == '\0') return partly_match; dots++; break; } if (!isdigit((int)*str)) return no_match; str++; } if (str - sp > 3) return no_match; strncpy(buf, sp, str - sp); if (atoi(buf) > 255) return no_match; nums++; if (*str == '\0') break; str++; } if (nums < 4) return partly_match; return exact_match; } static enum match_type match_ipv4_prefix(const char *str) { const char *sp; int dots = 0; char buf[4]; for (;;) { memset(buf, 0, sizeof(buf)); sp = str; while (*str != '\0' && *str != '/') { if (*str == '.') { if (dots == 3) return no_match; if (*(str + 1) == '.' || *(str + 1) == '/') return no_match; if (*(str + 1) == '\0') return partly_match; dots++; break; } if (!isdigit((int)*str)) return no_match; str++; } if (str - sp > 3) return no_match; strncpy(buf, sp, str - sp); if (atoi(buf) > 255) return no_match; if (dots == 3) { if (*str == '/') { if (*(str + 1) == '\0') return partly_match; str++; break; } else if (*str == '\0') return partly_match; } if (*str == '\0') return partly_match; str++; } sp = str; while (*str != '\0') { if (!isdigit((int)*str)) return no_match; str++; } if (atoi(sp) > 32) return no_match; return exact_match; } #define IPV6_ADDR_STR "0123456789abcdefABCDEF:." #define IPV6_PREFIX_STR "0123456789abcdefABCDEF:./" #define STATE_START 1 #define STATE_COLON 2 #define STATE_DOUBLE 3 #define STATE_ADDR 4 #define STATE_DOT 5 #define STATE_SLASH 6 #define STATE_MASK 7 static enum match_type match_ipv6_prefix(const char *str, bool prefix) { int state = STATE_START; int colons = 0, nums = 0, double_colon = 0; int mask; const char *sp = NULL, *start = str; char *endptr = NULL; if (str == NULL) return partly_match; if (strspn(str, prefix ? IPV6_PREFIX_STR : IPV6_ADDR_STR) != strlen(str)) return no_match; while (*str != '\0' && state != STATE_MASK) { switch (state) { case STATE_START: if (*str == ':') { if (*(str + 1) != ':' && *(str + 1) != '\0') return no_match; colons--; state = STATE_COLON; } else { sp = str; state = STATE_ADDR; } continue; case STATE_COLON: colons++; if (*(str + 1) == '/') return no_match; else if (*(str + 1) == ':') state = STATE_DOUBLE; else { sp = str + 1; state = STATE_ADDR; } break; case STATE_DOUBLE: if (double_colon) return no_match; if (*(str + 1) == ':') return no_match; else { if (*(str + 1) != '\0' && *(str + 1) != '/') colons++; sp = str + 1; if (*(str + 1) == '/') state = STATE_SLASH; else state = STATE_ADDR; } double_colon++; nums += 1; break; case STATE_ADDR: if (*(str + 1) == ':' || *(str + 1) == '.' || *(str + 1) == '\0' || *(str + 1) == '/') { if (str - sp > 3) return no_match; for (; sp <= str; sp++) if (*sp == '/') return no_match; nums++; if (*(str + 1) == ':') state = STATE_COLON; else if (*(str + 1) == '.') { if (colons || double_colon) state = STATE_DOT; else return no_match; } else if (*(str + 1) == '/') state = STATE_SLASH; } break; case STATE_DOT: state = STATE_ADDR; break; case STATE_SLASH: if (*(str + 1) == '\0') return partly_match; state = STATE_MASK; break; default: break; } if (nums > 11) return no_match; if (colons > 7) return no_match; str++; } if (!prefix) { struct sockaddr_in6 sin6_dummy; int ret = inet_pton(AF_INET6, start, &sin6_dummy.sin6_addr); return ret == 1 ? exact_match : partly_match; } if (state < STATE_MASK) return partly_match; mask = strtol(str, &endptr, 10); if (*endptr != '\0') return no_match; if (mask < 0 || mask > 128) return no_match; return exact_match; } static enum match_type match_range(struct cmd_token *token, const char *str) { assert(token->type == RANGE_TKN); char *endptr = NULL; long long val; val = strtoll(str, &endptr, 10); if (*endptr != '\0') return no_match; if (val < token->min || val > token->max) return no_match; else return exact_match; } static enum match_type match_word(struct cmd_token *token, const char *word) { assert(token->type == WORD_TKN); // if the passed token is 0 length, partly match if (!strlen(word)) return partly_match; // if the passed token is strictly a prefix of the full word, partly // match if (strlen(word) < strlen(token->text)) return !strncmp(token->text, word, strlen(word)) ? partly_match : no_match; // if they are the same length and exactly equal, exact match else if (strlen(word) == strlen(token->text)) return !strncmp(token->text, word, strlen(word)) ? exact_match : no_match; return no_match; } static enum match_type match_variable(struct cmd_token *token, const char *word) { assert(token->type == VARIABLE_TKN); return exact_match; } #define MAC_CHARS "ABCDEFabcdef0123456789:" static enum match_type match_mac(const char *word, bool prefix) { /* 6 2-digit hex numbers separated by 5 colons */ size_t mac_explen = 6 * 2 + 5; /* '/' + 2-digit integer */ size_t mask_len = 1 + 2; unsigned int i; char *eptr; unsigned int maskval; /* length check */ if (strlen(word) > mac_explen + (prefix ? mask_len : 0)) return no_match; /* address check */ for (i = 0; i < mac_explen; i++) { if (word[i] == '\0' || !strchr(MAC_CHARS, word[i])) break; if (((i + 1) % 3 == 0) != (word[i] == ':')) return no_match; } /* incomplete address */ if (i < mac_explen && word[i] == '\0') return partly_match; else if (i < mac_explen) return no_match; /* mask check */ if (prefix && word[i] == '/') { if (word[++i] == '\0') return partly_match; maskval = strtoul(&word[i], &eptr, 10); if (*eptr != '\0' || maskval > 48) return no_match; } else if (prefix && word[i] == '\0') { return partly_match; } else if (prefix) { return no_match; } return exact_match; }