diff --git a/src/lib/uri/uri.c b/src/lib/uri/uri.c index e3f1cb51fe0912b8a8291b95f59c8ed590d90dd9..1433147e618e117d81490ff7df882b021da13ed0 100644 --- a/src/lib/uri/uri.c +++ b/src/lib/uri/uri.c @@ -9,9 +9,126 @@ #define XSTRNDUP(s, n) (s != NULL ? xstrndup(s, n) : NULL) +struct uri_param { + /** Name of URI parameter. */ + char *name; + /** Count of values for this parameter. */ + int value_count; + /** Array of values for this parameter. */ + char **values; +}; + +/** + * Find URI parameter by its @a name in @a uri structure. + */ +static struct uri_param * +uri_find_param(const struct uri *uri, const char *name) +{ + for (int i = 0; i < uri->param_count; i++) { + if (strcmp(uri->params[i].name, name) == 0) + return &uri->params[i]; + } + return NULL; +} + +/** + * Add new @a value to URI @a param. + */ +static void +uri_param_add_value(struct uri_param *param, const char *value) +{ + size_t size = (param->value_count + 1) * sizeof(char *); + param->values = xrealloc(param->values, size); + param->values[param->value_count++] = xstrdup(value); +} + +/** + * Destroy URI @a param and free all associated resources. + */ +static void +uri_param_destroy(struct uri_param *param) +{ + for (int i = 0; i < param->value_count; i++) + free(param->values[i]); + free(param->values); + free(param->name); + TRASH(param); +} + +/** + * Create new URI @a param, with given @a name. + */ +static void +uri_param_create(struct uri_param *param, const char *name) +{ + param->name = xstrdup(name); + param->values = NULL; + param->value_count = 0; +} + +/** + * Appends @a value to @a uri parameter with given @a name, + * creating one if it doesn't exist. + */ +static void +uri_add_param(struct uri *uri, const char *name, const char *value) +{ + struct uri_param *param = uri_find_param(uri, name); + if (param == NULL) { + size_t size = (uri->param_count + 1) * + sizeof(struct uri_param); + uri->params = xrealloc(uri->params, size); + param = &uri->params[uri->param_count++]; + uri_param_create(param, name); + } + if (value != NULL) + uri_param_add_value(param, value); +} + +/** + * Destroy all @a uri parameters and free all resources associated + * with them. + */ +static void +uri_destroy_params(struct uri *uri) +{ + for (int i = 0; i < uri->param_count; i++) + uri_param_destroy(&uri->params[i]); + free(uri->params); +} + +/** + * Create parameters for @a uri from @a query string. Expected @a + * query format is a string which contains parameters separated by '&'. + * For example: "backlog=10&transport=tls". Also @a query can contain + * several values for one parameter separated by '&'. For example: + * "backlog=10&backlog=30". + */ +static void +uri_create_params(struct uri *uri, const char *query) +{ + char *copy = xstrdup(query); + char *saveptr, *optstr = strtok_r(copy, "&", &saveptr); + while (optstr != NULL) { + char *value = NULL, *name = optstr; + char *delim = strchr(optstr, '='); + if (delim != NULL) { + *delim = '\0'; + value = delim + 1; + } + optstr = strtok_r(NULL, "&", &saveptr); + /* Ignore params with empty name */ + if (*name == 0) + continue; + uri_add_param(uri, name, value); + } + free(copy); +} + void uri_destroy(struct uri *uri) { + uri_destroy_params(uri); free(uri->scheme); free(uri->login); free(uri->password); @@ -41,6 +158,8 @@ uri_create(struct uri *uri, const char *str) uri->query = XSTRNDUP(uri_raw.query, uri_raw.query_len); uri->fragment = XSTRNDUP(uri_raw.fragment, uri_raw.fragment_len); uri->host_hint = uri_raw.host_hint; + if (uri->query != NULL) + uri_create_params(uri, uri->query); return 0; } @@ -75,3 +194,20 @@ uri_format(char *str, int len, const struct uri *uri, bool write_password) } return total; } + +const char * +uri_param(const struct uri *uri, const char *name, int idx) +{ + struct uri_param *param = uri_find_param(uri, name); + assert(idx >= 0); + if (param == NULL || idx >= param->value_count) + return NULL; + return param->values[idx]; +} + +int +uri_param_count(const struct uri *uri, const char *name) +{ + struct uri_param *param = uri_find_param(uri, name); + return (param != NULL ? param->value_count : 0); +} diff --git a/src/lib/uri/uri.h b/src/lib/uri/uri.h index 9da2f4e6351972f58087275b818483fc066e7200..a86efdad7adee7f25ba10ab160f5e9e9ec4aa58d 100644 --- a/src/lib/uri/uri.h +++ b/src/lib/uri/uri.h @@ -13,6 +13,8 @@ extern "C" { #endif /* defined(__cplusplus) */ +struct uri_param; + struct uri { char *scheme; char *login; @@ -23,6 +25,10 @@ struct uri { char *query; char *fragment; int host_hint; + /** Count of URI parameters */ + int param_count; + /** Different URI parameters */ + struct uri_param *params; }; #define URI_HOST_UNIX "unix/" @@ -37,7 +43,9 @@ struct uri { * URI components in appropriate fields of @a uri. @a uri * can be safely destroyed in case this function fails. * If @str == NULL function fill uri structure with zeros - * and return 0. This function doesn't set diag. + * and return 0. Expected format of @a src string: "uri?query", + * where query contains parameters separated by '&'. This + * function doesn't set diag. */ int uri_create(struct uri *uri, const char *str); @@ -53,6 +61,21 @@ uri_destroy(struct uri *uri); int uri_format(char *str, int len, const struct uri *uri, bool write_password); +/** + * Return @a uri parameter value by given @a idx. If parameter with @a name + * does not exist or @a idx is greater than or equal to URI parameter value + * count, return NULL. + */ +const char * +uri_param(const struct uri *uri, const char *name, int idx); + +/** + * Return count of values for @a uri parameter with given @a name. + * If parameter with such @a name does not exist return 0. + */ +int +uri_param_count(const struct uri *uri, const char *name); + #if defined(__cplusplus) } /* extern "C" */ #endif /* defined(__cplusplus) */ diff --git a/src/lua/uri.lua b/src/lua/uri.lua index 396506d4dae2e609c01cd73ade22124e4d023ae4..76ee5ed3d54187fdafa90a6db7bb3374ef5fba1a 100644 --- a/src/lua/uri.lua +++ b/src/lua/uri.lua @@ -4,6 +4,12 @@ local ffi = require('ffi') local buffer = require('buffer') ffi.cdef[[ +struct uri_param { + const char *name; + int value_count; + const char **values; +}; + /** * We define all strings inside the `struct uri` as const, despite * the fact that they are not constant in the C structure. This is @@ -21,6 +27,8 @@ struct uri { const char *query; const char *fragment; int host_hint; + int param_count; + struct uri_param *params; }; int @@ -56,6 +64,16 @@ local function parse(str) result[k] = ffi.string(uribuf[k]) end end + result.params = {} + for param_idx = 0, uribuf.param_count - 1 do + local param = uribuf.params[param_idx] + local name = ffi.string(param.name) + result.params[name] = {} + for val_idx = 0, param.value_count - 1 do + result.params[name][val_idx + 1] = + ffi.string(param.values[val_idx]) + end + end if uribuf.host_hint == 1 then result.ipv4 = result.host elseif uribuf.host_hint == 2 then diff --git a/test/app-tap/uri.test.lua b/test/app-tap/uri.test.lua index 03ff724de24916db8ad9275f257b3625222d371b..aa1efb7134f59b3e291f7fc96838e86edc276da4 100755 --- a/test/app-tap/uri.test.lua +++ b/test/app-tap/uri.test.lua @@ -99,8 +99,58 @@ local function test_format(test) test:is(uri.format(u, true), "user:password@localhost", "password kept") end +local function test_parse_uri_query_params(test) + -- Tests for uri.parse() Lua bindings (URI with query parameters). + -- Parser itself is tested by test/unit/uri unit test. + test:plan(33) + + local u + + u = uri.parse("/tmp/unix.sock?q1=v1") + test:is(u.host, 'unix/', 'host') + test:is(u.service, '/tmp/unix.sock', 'service') + test:is(u.unix, '/tmp/unix.sock', 'unix') + test:is(type(u.params["q1"]), "table", "name") + test:is(#u.params["q1"], 1, "value count") + test:is(u.params["q1"][1], "v1", "param value") + + u = uri.parse("/tmp/unix.sock?q1=v1&q1=v2") + test:is(u.host, 'unix/', 'host') + test:is(u.service, '/tmp/unix.sock', 'service') + test:is(u.unix, '/tmp/unix.sock', 'unix') + test:is(type(u.params["q1"]), "table", "name") + test:is(#u.params["q1"], 2, "value count") + test:is(u.params["q1"][1], "v1", "param value") + test:is(u.params["q1"][2], "v2", "param value") + + u = uri.parse("/tmp/unix.sock?q1=v11&q1=v12&q2=v21&q2=v22") + test:is(u.host, 'unix/', 'host') + test:is(u.service, '/tmp/unix.sock', 'service') + test:is(u.unix, '/tmp/unix.sock', 'unix') + test:is(type(u.params["q1"]), "table", "name") + test:is(#u.params["q1"], 2, "value count") + test:is(u.params["q1"][1], "v11", "param value") + test:is(u.params["q1"][2], "v12", "param value") + test:is(type(u.params["q2"]), "table", "name") + test:is(#u.params["q2"], 2, "value count") + test:is(u.params["q2"][1], "v21", "param value") + test:is(u.params["q2"][2], "v22", "param value") + + u = uri.parse("/tmp/unix.sock?q1=v1&q1=&q2") + test:is(u.host, 'unix/', 'host') + test:is(u.service, '/tmp/unix.sock', 'service') + test:is(u.unix, '/tmp/unix.sock', 'unix') + test:is(type(u.params["q1"]), "table", "name") + test:is(#u.params["q1"], 2, "value count") + test:is(u.params["q1"][1], "v1", "param value") + test:is(u.params["q1"][2], "", "param value") + test:is(type(u.params["q2"]), "table", "name") + test:is(#u.params["q2"], 0, "value count") +end + tap.test("uri", function(test) - test:plan(2) + test:plan(3) test:test("parse", test_parse) + test:test("parse URI query params", test_parse_uri_query_params) test:test("format", test_format) end) diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index fbba64bd2c056172661e48eea5aa405a0233c6d1..cd67d682bb3773c4e984e40369d041ea3ec43fa1 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -26,6 +26,8 @@ add_executable(stailq.test stailq.c) target_link_libraries(stailq.test unit) add_executable(uri_parser.test uri_parser.c unit.c) target_link_libraries(uri_parser.test uri unit) +add_executable(uri.test uri.c unit.c) +target_link_libraries(uri.test uri unit) add_executable(queue.test queue.c) add_executable(mhash.test mhash.c) target_link_libraries(mhash.test unit) diff --git a/test/unit/uri.c b/test/unit/uri.c new file mode 100644 index 0000000000000000000000000000000000000000..ee36b7e8b9b8ae8f053a8c3b2ac9edda248750e3 --- /dev/null +++ b/test/unit/uri.c @@ -0,0 +1,263 @@ +#include "unit.h" +#include "uri/uri.h" +#include "lua/utils.h" +#include "trivia/util.h" +#include "diag.h" +#include "memory.h" +#include "fiber.h" +#include "tt_static.h" + +#include <stdio.h> + +#define URI_PARAM_MAX 10 +#define URI_PARAM_VALUE_MAX 10 + +struct uri_param_expected { + /** URI parameter name */ + const char *name; + /** Count of URI parameter values */ + int value_count; + /** Expected URI parameter values */ + const char *values[URI_PARAM_VALUE_MAX]; +}; + +struct uri_expected { + /** String URI passed for parse and validation */ + const char *string; + /** Count of URI parameters */ + int param_count; + /** Array of expected URI parameters */ + struct uri_param_expected params[URI_PARAM_MAX]; +}; + +static int +uri_param_expected_check(const struct uri_param_expected *param, + const struct uri *uri) +{ + plan(1 + param->value_count); + int value_count = uri_param_count(uri, param->name); + is(param->value_count, value_count, "value count"); + for (int idx = 0; idx < MIN(value_count, param->value_count); idx++) { + const char *value = uri_param(uri, param->name, idx); + is(strcmp(value, param->values[idx]), 0, "param value"); + } + return check_plan(); +} + +static int +uri_expected_check(const struct uri_expected *uri_ex, const struct uri *uri) +{ + plan(1 + uri_ex->param_count); + is(uri_ex->param_count, uri->param_count, "param count"); + for (int i = 0; i < MIN(uri_ex->param_count, uri->param_count); i++) + uri_param_expected_check(&uri_ex->params[i], uri); + return check_plan(); +} + +static int +test_string_uri_with_query_params_parse(void) +{ + const struct uri_expected uris[] = { + /* One string URI without parameters. */ + [0] = { + .string = "/unix.sock", + .param_count = 0, + .params = {}, + }, + /* One string URI without parameters with additional '?'. */ + [1] = { + .string = "/unix.sock?", + .param_count = 0, + .params = {}, + }, + /* One string URI with one parameter and one parameter value. */ + [2] = { + .string = "/unix.sock?q1=v1", + .param_count = 1, + .params = { + [0] = { + .name = "q1", + .value_count = 1, + .values = { "v1" }, + }, + }, + }, + /* + * Same as previous but with extra '&' at the end + * of the string. + */ + [3] = { + .string = "/unix.sock?q1=v1&", + .param_count = 1, + .params = { + [0] = { + .name = "q1", + .value_count = 1, + .values = { "v1" }, + }, + }, + }, + /* + * Same as previos but with two extra '&' at the end + * of the string. + */ + [4] = { + .string = "/unix.sock?q1=v1&&", + .param_count = 1, + .params = { + [0] = { + .name = "q1", + .value_count = 1, + .values = { "v1" }, + }, + }, + }, + /* + * One string URI with one parameter and two parameter values, + * separated by "&". + */ + [5] = { + .string = "/unix.sock?q1=v1&q1=v2", + .param_count = 1, + .params = { + [0] = { + .name = "q1", + .value_count = 2, + .values = { "v1", "v2" }, + }, + }, + }, + /* + * Same as previous but with extra '&' between parameters. + */ + [6] = { + .string = "/unix.sock?q1=v1&&q1=v2", + .param_count = 1, + .params = { + [0] = { + .name = "q1", + .value_count = 2, + .values = { "v1", "v2" }, + }, + }, + }, + /* + * On string uri with several parameters without values. + */ + [7] = { + .string = "/unix.sock?q1&q2", + .param_count = 2, + .params = { + [0] = { + .name = "q1", + .value_count = 0, + .values = {}, + }, + [1] = { + .name = "q2", + .value_count = 0, + .values = {}, + }, + } + }, + /* + * One string URI with several parameters. + */ + [8] = { + .string = "/unix.sock?q1=v11&q1=v12&q2=v21&q2=v22", + .param_count = 2, + .params = { + [0] = { + .name = "q1", + .value_count = 2, + .values = { "v11", "v12" }, + }, + [1] = { + .name = "q2", + .value_count = 2, + .values = { "v21", "v22" }, + }, + }, + }, + /* + * One string URI with several parameters, at the same time, + * some of them have empty value or don't have values at all. + */ + [9] = { + .string = "/unix.sock?q1=v1&q1=&q2&q3=", + .param_count = 3, + .params = { + [0] = { + .name = "q1", + .value_count = 2, + .values = { "v1", "" }, + }, + [1] = { + .name = "q2", + .value_count = 0, + .values = {}, + }, + [2] = { + .name = "q3", + .value_count = 1, + .values = { "" }, + }, + }, + }, + /* + * Single URI with query, that contains extra '=' between + * parameter and it's value. (All extra '=' is interpreted + * as a part of value). + */ + [10] = { + .string = "/unix.sock?q1===v1&q2===v2", + .param_count = 2, + .params = { + [0] = { + .name = "q1", + .value_count = 1, + .values = { "==v1" }, + }, + [1] = { + .name = "q2", + .value_count = 1, + .values = { "==v2" }, + }, + }, + }, + /* + * Single URI with strange query, that contains combination + * of delimiters. + */ + [11] = { + .string = "/unix.sock?&=&=", + .param_count = 0, + .params = {}, + }, + /* + * Same as previous, but another sequence of delimiters. + */ + [12] = { + .string = "/unix.sock?=&=&", + .param_count = 0, + .params = {}, + } + }; + plan(2 * lengthof(uris)); + struct uri u; + for (unsigned i = 0; i < lengthof(uris); i++) { + int rc = uri_create(&u, uris[i].string); + is(rc, 0, "%s: parse", uris[i].string); + uri_expected_check(&uris[i], &u); + uri_destroy(&u); + } + return check_plan(); +} + +int +main(void) +{ + plan(1); + test_string_uri_with_query_params_parse(); + return check_plan(); +} diff --git a/test/unit/uri.result b/test/unit/uri.result new file mode 100644 index 0000000000000000000000000000000000000000..9eba461b783fc7eb430aaebe27467d2c3b8c2b83 --- /dev/null +++ b/test/unit/uri.result @@ -0,0 +1,113 @@ +1..1 + 1..26 + ok 1 - /unix.sock: parse + 1..1 + ok 1 - param count + ok 2 - subtests + ok 3 - /unix.sock?: parse + 1..1 + ok 1 - param count + ok 4 - subtests + ok 5 - /unix.sock?q1=v1: parse + 1..2 + ok 1 - param count + 1..2 + ok 1 - value count + ok 2 - param value + ok 2 - subtests + ok 6 - subtests + ok 7 - /unix.sock?q1=v1&: parse + 1..2 + ok 1 - param count + 1..2 + ok 1 - value count + ok 2 - param value + ok 2 - subtests + ok 8 - subtests + ok 9 - /unix.sock?q1=v1&&: parse + 1..2 + ok 1 - param count + 1..2 + ok 1 - value count + ok 2 - param value + ok 2 - subtests + ok 10 - subtests + ok 11 - /unix.sock?q1=v1&q1=v2: parse + 1..2 + ok 1 - param count + 1..3 + ok 1 - value count + ok 2 - param value + ok 3 - param value + ok 2 - subtests + ok 12 - subtests + ok 13 - /unix.sock?q1=v1&&q1=v2: parse + 1..2 + ok 1 - param count + 1..3 + ok 1 - value count + ok 2 - param value + ok 3 - param value + ok 2 - subtests + ok 14 - subtests + ok 15 - /unix.sock?q1&q2: parse + 1..3 + ok 1 - param count + 1..1 + ok 1 - value count + ok 2 - subtests + 1..1 + ok 1 - value count + ok 3 - subtests + ok 16 - subtests + ok 17 - /unix.sock?q1=v11&q1=v12&q2=v21&q2=v22: parse + 1..3 + ok 1 - param count + 1..3 + ok 1 - value count + ok 2 - param value + ok 3 - param value + ok 2 - subtests + 1..3 + ok 1 - value count + ok 2 - param value + ok 3 - param value + ok 3 - subtests + ok 18 - subtests + ok 19 - /unix.sock?q1=v1&q1=&q2&q3=: parse + 1..4 + ok 1 - param count + 1..3 + ok 1 - value count + ok 2 - param value + ok 3 - param value + ok 2 - subtests + 1..1 + ok 1 - value count + ok 3 - subtests + 1..2 + ok 1 - value count + ok 2 - param value + ok 4 - subtests + ok 20 - subtests + ok 21 - /unix.sock?q1===v1&q2===v2: parse + 1..3 + ok 1 - param count + 1..2 + ok 1 - value count + ok 2 - param value + ok 2 - subtests + 1..2 + ok 1 - value count + ok 2 - param value + ok 3 - subtests + ok 22 - subtests + ok 23 - /unix.sock?&=&=: parse + 1..1 + ok 1 - param count + ok 24 - subtests + ok 25 - /unix.sock?=&=&: parse + 1..1 + ok 1 - param count + ok 26 - subtests +ok 1 - subtests