diff --git a/.gitignore b/.gitignore index 69ab2bd3..34f8c464 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ test/vagrant/site.mk test/cicd/site.mk doc/html +.idea/ diff --git a/lib/src/Makefile.in b/lib/src/Makefile.in index a69f2c2b..407900b5 100644 --- a/lib/src/Makefile.in +++ b/lib/src/Makefile.in @@ -83,7 +83,8 @@ SRC = clixon_sig.c clixon_uid.c clixon_log.c clixon_err.c clixon_event.c \ clixon_xpath.c clixon_xpath_ctx.c clixon_xpath_eval.c clixon_xpath_function.c \ clixon_xpath_optimize.c clixon_xpath_yang.c \ clixon_datastore.c clixon_datastore_write.c clixon_datastore_read.c \ - clixon_netconf_lib.c clixon_stream.c clixon_nacm.c clixon_client.c clixon_netns.c + clixon_netconf_lib.c clixon_stream.c clixon_nacm.c clixon_client.c clixon_netns.c \ + clixon_dispatcher.c YACCOBJS = lex.clixon_xml_parse.o clixon_xml_parse.tab.o \ lex.clixon_yang_parse.o clixon_yang_parse.tab.o \ diff --git a/lib/src/clixon_dispatcher.c b/lib/src/clixon_dispatcher.c new file mode 100644 index 00000000..c26f5421 --- /dev/null +++ b/lib/src/clixon_dispatcher.c @@ -0,0 +1,400 @@ +/* + * Copyright 2021 Rubicon Communications LLC (Netgate) + */ + +/* + * we start with a series of dispatcher_definitions, which are a + * path and handler. + * + * we break the path up into elements and build a tree out of them + * example: + * + * we start with two paths /a/b/c and /a/d with handler_c() and + * handler_d() as their handlers respectively. + * + * this produces a tree like this: + * + * [/] root_handler() + * [a] NULL + * [b] NULL + * [c] handler_c() + * [d] handler_d() + * + * NULL means that there is no handler defined - if the terminal + * element of the path has a NULL handler then you look for the + * closest ancestor that does. + * + * for example, if I lookup /a/b I get back a pointer to root_handler() + * if i lookup /a/d, I get handler_d(). + * + * if a element has a key (/a/b=c) then the list element is + * marked with an = sign and without the key + * so /a/b=c creates multiple entries: + * + * [/] + * [a] + * [b=] + * [b] + * + * NOTE 1: there is not a mechanism to free the created structures since + * it is intended that this tree is created only at startup. if use case + * changes, this function is trivial. + * + * NOTE 2: there is no attempt to optimize list searching here, sorry. I + * do not think that the known use cases will get big enough to make the + * tree get too large. I do not recommend that you encode every possible + * path, just top level key handlers. + * + * there are 2 functions to the API: + * clixon_register_handler(): build the dispatcher table + * clixon_call_handlers(): query the dispatcher table + */ + +/* + * Important for writing handlers: a handler must return a complete + * valid response. It must operate in isolation, it must not expect + * any ordering in the calls and [under review] it should not call + * another handler directly or indirectly. Responses must me bound + * to a yang model and properly sorted and indexed. + */ + +#include +#include +#include +#include + +#include "clixon_dispatcher.h" + +/* ===== utility routines ==== */ + +#define PATH_CHUNKS 32 + +/** + * spilt a path into elements + * + * given an api-path, break it up into chunks separated by '/' + * characters. it is expected that api-paths are URI encoded, so no need + * to deal with unescaped special characters like ', ", and / + * + * @param path [input] path string + * @param plist [output] pointer to split path array + * @param plist_len [output] pointer to storage space for path array length + */ + +static void split_path(char *path, char ***plist, size_t *plist_len) +{ + size_t allocated = PATH_CHUNKS; + + /* don't modify the original copy */ + char *work = strdup(path); + + char **list = malloc(allocated * sizeof(char *)); + memset(list, 0, allocated * sizeof(char *)); + + size_t len = 0; + + char *ptr = work; + if (*ptr == '/') { + char *new_element = strdup("/"); + list[len++] = new_element; + ptr++; + } + + ptr = strtok(ptr, "/"); + + while (ptr != NULL) { + if (len > allocated) { + /* we've run out of space, allocate a bigger list */ + allocated += PATH_CHUNKS; + list = realloc(list, allocated * sizeof(char *)); + } + + char *new_element = strdup(ptr); + list[len++] = new_element; + + ptr = strtok(NULL, "/"); + } + + *plist = list; + *plist_len = len; + + free(work); +} + +/** + * free a split path structure + * + * @param list [input] pointer to split path array + * @param len [input] length of split path array + */ + +static void split_path_free(char **list, size_t len) +{ + size_t i; + + for (i = 0; i < len; i++) { + free(list[i]); + } + free(list); +} + +/** + * find a peer of this node by name + * search through the list pointed at by peer + * + * @param node [input] pointer to a node in the peer list + * @param node_name [input] name of node we're looking for + * @return pointer to found node or NULL + */ + +static dispatcher_entry_t *find_peer(dispatcher_entry_t *node, char *node_name) +{ + if ((node == NULL) || (node_name == NULL)) { + /* protect against idiot users */ + return NULL; + } + + dispatcher_entry_t *i = node->peer_head; + + while (i != NULL) { + if (strcmp(node_name, i->node_name) == 0) { + break; + } + i = i->peer; + } + + return i; +} + +/** + * add a node as the last node in peer list + * + * @param node [input] pointer to an element of the peer list + * @param name [input] name of new node + * @return pointer to added/existing node + */ + +static dispatcher_entry_t *add_peer_node(dispatcher_entry_t *node, char *name) +{ + dispatcher_entry_t *new_node = malloc(sizeof(dispatcher_entry_t)); + memset(new_node, 0, sizeof(dispatcher_entry_t)); + + if (node == NULL) { + /* this is a new node */ + + new_node->node_name = strdup(name); + new_node->peer = NULL; + new_node->children = NULL; + new_node->peer_head = new_node; + + return new_node; + } else { + /* possibly adding to the list */ + + /* search for existing, or get tail end of list */ + dispatcher_entry_t *eptr = node->peer_head; + while (eptr->peer != NULL) { + if (strcmp(eptr->node_name, name) == 0) { + return eptr; + } + eptr = eptr->peer; + } + + // if eptr->node_name == name, we done + if (strcmp(eptr->node_name, name) == 0) { + return eptr; + } + + new_node->node_name = strdup(name); + new_node->peer = NULL; + new_node->children = NULL; + new_node->peer_head = node->peer_head; + + eptr->peer = new_node; + + return new_node; + } +} + +/** + * add a node as a child of this node + * + * this is different from add_peer_node() in that it returns a + * pointer to the head_peer of the children list where the node was + * added. + * + * @param node [input] pointer to parent node of children list + * @param name [input] name of child node + * @return pointer to head of children list + */ + +static dispatcher_entry_t *add_child_node(dispatcher_entry_t *node, char *name) +{ + dispatcher_entry_t *child_ptr = add_peer_node(node->children, name); + node->children = child_ptr->peer_head; + + return child_ptr; +} + +/** + * + * @param root + * @param path + * @return + */ + +static dispatcher_entry_t *get_entry(dispatcher_entry_t *root, char *path) +{ + char **split_path_list = NULL; + size_t split_path_len = 0; + dispatcher_entry_t *ptr = root; + dispatcher_entry_t *best = root; + + /* cut the path up into individual elements */ + split_path(path, &split_path_list, &split_path_len); + + /* some elements may have keys defined, strip them off */ + for (int i = 0; i < split_path_len; i++) { + char *kptr = strchr(split_path_list[i], '='); + + if ((kptr != NULL) && (*kptr == '=')) { + *(kptr + 1) = 0; + } + } + + /* search down the tree */ + for (int i = 0; i < split_path_len; i++) { + + char *query = split_path_list[i]; + ptr = find_peer(ptr, query); + + if (ptr == NULL) { + /* we ran out of matches, use last found handler */ + return best; + } + if (ptr->handler != NULL) { + /* if handler is defined, save it */ + best = ptr; + } + + /* skip to next element */ + ptr = ptr->children; + } + + return best; +} + +/** + * given a pointer to an entry, call the handler and all + * descendant and peer handlers. + * + * @param entry + * @param path + * @return + */ + +static int call_handler_helper(dispatcher_entry_t *entry, char *path, void *user_args) +{ + if (entry->children != NULL) { + call_handler_helper(entry->children, path, user_args); + } + if (entry->peer != NULL) { + call_handler_helper(entry->peer, path, user_args); + } + if (entry->handler != NULL) { + (entry->handler) (path, user_args); + } + + return 1; +} + +/* + * ===== PUBLIC API FUNCTIONS ===== + */ + +/** + * register a dispatcher handler + * + * called from initialization code to build a dispatcher tree + * + * @param root pointer to pointer to dispatch tree + * @param x handler to registration data + * @return + */ + +dispatcher_entry_t *clixon_register_handler(dispatcher_entry_t **root, dispatcher_definition *x) +{ + char **split_path_list = NULL; + size_t split_path_len = 0; + + if (*x->path != '/') { + printf("%s: part '%s' must start at root\n", __func__, x->path); + return NULL; + } + + /* + * get the path from the dispatcher_definition, break it + * up to create the elements of the dispatcher table + */ + split_path(x->path, &split_path_list, &split_path_len); + + /* + * the first element is always a peer to the top level + */ + dispatcher_entry_t *ptr = *root; + + ptr = add_peer_node(ptr, split_path_list[0]); + if (*root == NULL) { + *root = ptr; + } + + for (size_t i = 1; i < split_path_len; i++) { + ptr = add_child_node(ptr, split_path_list[i]); + } + + /* when we get here, ptr points at last entry added */ + if (x->handler != NULL) { + /* + * we're adding/changing a handler + * you could make this an error optionally + */ + if (ptr->handler != NULL) { + printf("%s: warning: replacing existing handler: (%s) %p -> %p\n", __func__, + ptr->node_name, ptr->handler, x->handler); + } + ptr->handler = x->handler; + } + + /* clean up */ + split_path_free(split_path_list, split_path_len); + + return NULL; +} + +/** + * call the handler and all its descendant handlers + * + * NOTE: There is no guarantee of the order in which handlers + * are called! Any handler must assume that it is called in + * isolation, even if this duplicates work. The right to + * reorder calls by this code is reserved. + * + * @param root + * @param path + * @return + */ + +int clixon_call_handlers(dispatcher_entry_t *root, char *path, void *user_args) +{ + int ret = 0; + dispatcher_entry_t *best = get_entry(root, path); + + if (best->children != NULL) { + call_handler_helper(best->children, path, user_args); + } + if (best->handler != NULL) { + ret = (*best->handler) (path, user_args); + } + + return ret; +} diff --git a/lib/src/clixon_dispatcher.h b/lib/src/clixon_dispatcher.h new file mode 100644 index 00000000..e3be32f9 --- /dev/null +++ b/lib/src/clixon_dispatcher.h @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Rubicon Communications LLC (Netgate) + */ + +#ifndef DISPATCH_DISPATCHER_H +#define DISPATCH_DISPATCHER_H + +/* + * prototype for a function to handle the path + * minimally needs the path it's working on, but probably + * we want to hand down cached data somehow + */ + +typedef int (*handler_function)(char *path, void *args); + +/* + * this structure is used to map a handler to a path + */ +typedef struct { + char *path; + handler_function handler; +} dispatcher_definition; + +/* + * the dispatcher_entry_t is the structure created from + * the registered dispatcher_definitions + */ + +struct _dispatcher_entry; +typedef struct _dispatcher_entry dispatcher_entry_t; + +struct _dispatcher_entry { + /* + * the name of this node, NOT the complete path + */ + char *node_name; + + /* + * peer points at peer to the right of this one + * if NULL then this is the rightmost and last on list + */ + dispatcher_entry_t *peer; + + /* + * peer_head points at leftmost peer at this level + * if NULL, then this is the leftmost and first on the list + */ + dispatcher_entry_t *peer_head; + + /* + * points at peer_head of children list + * if NULL, then no children + */ + dispatcher_entry_t *children; + + /* + * pointer to handler function for this node + */ + handler_function handler; +}; + +dispatcher_entry_t *clixon_register_handler (dispatcher_entry_t **root, dispatcher_definition *x); +int clixon_call_handlers(dispatcher_entry_t *root, char *path, void *user_args); + +#endif /* DISPATCH_DISPATCHER_H */