diff --git a/src/shared/import-util.c b/src/shared/import-util.c index 2a30e40686..bb1e5136a7 100644 --- a/src/shared/import-util.c +++ b/src/shared/import-util.c @@ -14,53 +14,114 @@ #include "string-table.h" #include "string-util.h" -int import_url_last_component(const char *url, char **ret) { - const char *e, *p; - char *s; +static const char *skip_protocol_and_hostname(const char *url) { + const char *d; + size_t n; - e = strchrnul(url, '?'); + /* A very very lenient implementation of RFC3986 Section 3.2 */ - while (e > url && e[-1] == '/') + /* Find colon separating protocol and hostname */ + d = strchr(url, ':'); + if (!d || url == d) + return NULL; + d++; + + /* Skip slashes after colon */ + d += strspn(d, "/"); + + /* Skip everything till next slash or end */ + n = strcspn(d, "/?#"); + if (n == 0) + return NULL; + + return d + n; +} + +int import_url_last_component( + const char *url, + char **ret) { + + const char *e, *p, *h; + + /* This extracts the last path component of the specified URI, i.e. the last non-empty substrings + * between two "/" characters. This ignores "Query" and "Fragment" suffixes (as per RFC3986). */ + + h = skip_protocol_and_hostname(url); + if (!h) + return -EINVAL; + + e = h + strcspn(h, "?#"); /* Cut off "Query" and "Fragment" */ + + while (e > h && e[-1] == '/') /* Eat trailing slashes */ e--; p = e; - while (p > url && p[-1] != '/') + while (p > h && p[-1] != '/') /* Find component before that */ p--; - if (e <= p) - return -EINVAL; + if (e <= p) /* Empty component? */ + return -EADDRNOTAVAIL; - s = strndup(p, e - p); - if (!s) - return -ENOMEM; + if (ret) { + char *s; + + s = strndup(p, e - p); + if (!s) + return -ENOMEM; + + *ret = s; + } - *ret = s; return 0; } -int import_url_change_last_component(const char *url, const char *suffix, char **ret) { - const char *e; +int import_url_change_suffix( + const char *url, + size_t n_drop_components, + const char *suffix, + char **ret) { + + const char *e, *h; char *s; assert(url); assert(ret); - e = strchrnul(url, '?'); + /* This drops the specified number of path components of the specified URI, i.e. the specified number + * of non-empty substring between two "/" characters from the end of the string, and then append the + * specified suffix instead. Before doing all this it chops off the "Query" and "Fragment" suffixes + * (they are *not* readded to the final URL). Note that n_drop_components may be 0 (in which case the + * component are simply added to the end). The suffix may be specified as NULL or empty string in + * which case nothing is appended, only the specified number of components chopped off. Note that the + * function may be called with n_drop_components == 0 and suffix == NULL, in which case the "Query" + * and "Fragment" is chopped off, and ensured the URL ends in a single "/", and that's it. */ - while (e > url && e[-1] == '/') - e--; - - while (e > url && e[-1] != '/') - e--; - - if (e <= url) + h = skip_protocol_and_hostname(url); + if (!h) return -EINVAL; - s = new(char, (e - url) + strlen(suffix) + 1); + e = h + strcspn(h, "?#"); /* Cut off "Query" and "Fragment" */ + + while (e > h && e[-1] == '/') /* Eat trailing slashes */ + e--; + + /* Drop the specified number of components from the end. Note that this is pretty lenient: if there + * are less component we silently drop those and then append the suffix to the top. */ + while (n_drop_components > 0) { + while (e > h && e[-1] != '/') /* Eat last word (we don't mind if empty) */ + e--; + + while (e > h && e[-1] == '/') /* Eat slashes before the last word */ + e--; + + n_drop_components--; + } + + s = new(char, (e - url) + 1 + strlen_ptr(suffix) + 1); if (!s) return -ENOMEM; - strcpy(mempcpy(s, url, e - url), suffix); + strcpy(stpcpy(mempcpy(s, url, e - url), "/"), strempty(suffix)); *ret = s; return 0; } diff --git a/src/shared/import-util.h b/src/shared/import-util.h index c7ec3b4eab..3b2425b916 100644 --- a/src/shared/import-util.h +++ b/src/shared/import-util.h @@ -14,7 +14,16 @@ typedef enum ImportVerify { } ImportVerify; int import_url_last_component(const char *url, char **ret); -int import_url_change_last_component(const char *url, const char *suffix, char **ret); + +int import_url_change_suffix(const char *url, size_t n_drop_components, const char *suffix, char **ret); + +static inline int import_url_change_last_component(const char *url, const char *suffix, char **ret) { + return import_url_change_suffix(url, 1, suffix, ret); +} + +static inline int import_url_append_component(const char *url, const char *suffix, char **ret) { + return import_url_change_suffix(url, 0, suffix, ret); +} const char* import_verify_to_string(ImportVerify v) _const_; ImportVerify import_verify_from_string(const char *s) _pure_; diff --git a/src/test/meson.build b/src/test/meson.build index 07625ad38c..e9204b150d 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -250,6 +250,8 @@ tests += [ [['src/test/test-sysctl-util.c']], + [['src/test/test-import-util.c']], + [['src/test/test-user-record.c']], [['src/test/test-user-util.c']], diff --git a/src/test/test-import-util.c b/src/test/test-import-util.c new file mode 100644 index 0000000000..92ff0f5654 --- /dev/null +++ b/src/test/test-import-util.c @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "import-util.h" +#include "log.h" +#include "string-util.h" +#include "tests.h" + +static void test_import_url_last_component_one(const char *input, const char *output, int ret) { + _cleanup_free_ char *s = NULL; + + assert_se(import_url_last_component(input, &s) == ret); + assert_se(streq_ptr(output, s)); +} + +static void test_import_url_last_component(void) { + test_import_url_last_component_one("https://foobar/waldo/quux", "quux", 0); + test_import_url_last_component_one("https://foobar/waldo/quux/", "quux", 0); + test_import_url_last_component_one("https://foobar/waldo/", "waldo", 0); + test_import_url_last_component_one("https://foobar/", NULL, -EADDRNOTAVAIL); + test_import_url_last_component_one("https://foobar", NULL, -EADDRNOTAVAIL); + test_import_url_last_component_one("https://foobar/waldo/quux?foo=bar", "quux", 0); + test_import_url_last_component_one("https://foobar/waldo/quux/?foo=bar", "quux", 0); + test_import_url_last_component_one("https://foobar/waldo/quux/?foo=bar#piep", "quux", 0); + test_import_url_last_component_one("https://foobar/waldo/quux/#piep", "quux", 0); + test_import_url_last_component_one("https://foobar/waldo/quux#piep", "quux", 0); + test_import_url_last_component_one("https://", NULL, -EINVAL); + test_import_url_last_component_one("", NULL, -EINVAL); + test_import_url_last_component_one(":", NULL, -EINVAL); + test_import_url_last_component_one(":/", NULL, -EINVAL); + test_import_url_last_component_one("x:/", NULL, -EINVAL); + test_import_url_last_component_one("x:y", NULL, -EADDRNOTAVAIL); + test_import_url_last_component_one("x:y/z", "z", 0); +} + +static void test_import_url_change_suffix_one(const char *input, size_t n, const char *suffix, const char *output, int ret) { + _cleanup_free_ char *s = NULL; + + assert_se(import_url_change_suffix(input, n, suffix, &s) == ret); + assert_se(streq_ptr(output, s)); +} + +static void test_import_url_change_suffix(void) { + test_import_url_change_suffix_one("https://foobar/waldo/quux", 1, "wuff", "https://foobar/waldo/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux/", 1, "wuff", "https://foobar/waldo/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux///?mief", 1, "wuff", "https://foobar/waldo/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux///?mief#opopo", 1, "wuff", "https://foobar/waldo/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux/quff", 2, "wuff", "https://foobar/waldo/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux/quff/", 2, "wuff", "https://foobar/waldo/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux/quff", 0, "wuff", "https://foobar/waldo/quux/quff/wuff", 0); + test_import_url_change_suffix_one("https://foobar/waldo/quux/quff?aa?bb##4", 0, "wuff", "https://foobar/waldo/quux/quff/wuff", 0); + test_import_url_change_suffix_one("https://", 0, "wuff", NULL, -EINVAL); + test_import_url_change_suffix_one("", 0, "wuff", NULL, -EINVAL); + test_import_url_change_suffix_one(":", 0, "wuff", NULL, -EINVAL); + test_import_url_change_suffix_one(":/", 0, "wuff", NULL, -EINVAL); + test_import_url_change_suffix_one("x:/", 0, "wuff", NULL, -EINVAL); + test_import_url_change_suffix_one("x:y", 0, "wuff", "x:y/wuff", 0); + test_import_url_change_suffix_one("x:y/z", 0, "wuff", "x:y/z/wuff", 0); + test_import_url_change_suffix_one("x:y/z/", 0, "wuff", "x:y/z/wuff", 0); + test_import_url_change_suffix_one("x:y/z/", 1, "wuff", "x:y/wuff", 0); + test_import_url_change_suffix_one("x:y/z/", 2, "wuff", "x:y/wuff", 0); +} + +int main(int argc, char *argv[]) { + + test_setup_logging(LOG_INFO); + + test_import_url_last_component(); + test_import_url_change_suffix(); + + return 0; +}