From 27cef5c80c8c88245591ff5d55d993ddb9602e9d Mon Sep 17 00:00:00 2001 From: mechanik20051988 <mechanik20051988@tarantool.org> Date: Wed, 3 Nov 2021 19:17:57 +0300 Subject: [PATCH] uri: implement ability to parse URI query paramters Previously, the part of the URI string that comes after the first '?' was saved as a single unit in the field `query` as a part of `struct uri`. Now an array of URI paramters has been added to the uri structure, and query is subjected to the additional parsing and URI query parameters are extracted from it. The separator symbol between URI query parameters is '&', the separator symbol between parameter and it's value is '='. For example URI = "/tmp/unix.sock?q1=v11&q1=v12&q2=v2" has two parameters: parameter 'q1' with two values 'v11' and 'v12' and parameter 'q2' with value 'v2'. Part of #5928 Closes #1175 --- src/lib/uri/uri.c | 136 ++++++++++++++++++++ src/lib/uri/uri.h | 25 +++- src/lua/uri.lua | 18 +++ test/app-tap/uri.test.lua | 52 +++++++- test/unit/CMakeLists.txt | 2 + test/unit/uri.c | 263 ++++++++++++++++++++++++++++++++++++++ test/unit/uri.result | 113 ++++++++++++++++ 7 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 test/unit/uri.c create mode 100644 test/unit/uri.result diff --git a/src/lib/uri/uri.c b/src/lib/uri/uri.c index e3f1cb51fe..1433147e61 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 9da2f4e635..a86efdad7a 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 396506d4da..76ee5ed3d5 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 03ff724de2..aa1efb7134 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 fbba64bd2c..cd67d682bb 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 0000000000..ee36b7e8b9 --- /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 0000000000..9eba461b78 --- /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 -- GitLab