From f5b0f3a1420e5a7ac787bc18fda45b6233cb4169 Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Sat, 6 Dec 2025 00:40:05 +0100 Subject: [PATCH] Add url_resolve_reference to translare relative references to absolute URLs --- tests/test.c | 10 +- url.c | 309 +++++++++++++++++++++++++++++++++++++++++++++++---- url.h | 11 +- 3 files changed, 306 insertions(+), 24 deletions(-) diff --git a/tests/test.c b/tests/test.c index 847ce61..f30c580 100644 --- a/tests/test.c +++ b/tests/test.c @@ -310,6 +310,7 @@ static int run_test(cJSON *json) } int ret; + char tmp[1<<10]; char buf[1<<10]; URL parsed; if (cJSON_IsNull(json_base)) { @@ -335,6 +336,13 @@ static int run_test(cJSON *json) } assert(ret == 0); + ret = url_remove_white_space(src.ptr, src.len, tmp, (int) sizeof(tmp)); + if (ret > (int) sizeof(tmp)) { + assert(0); // TODO + } + src.ptr = tmp; + src.len = ret; + ret = url_resolve_reference(src.ptr, src.len, NULL, &parsed_base, false, buf, (int) sizeof(buf)); if (ret >= (int) sizeof(buf)) { test_printf("\n"); @@ -440,7 +448,7 @@ static char *load_file(char *path) int main(void) { - char *path = "data/urltestdata.json"; + char *path = "wpt/urltestdata.json"; char *buf = load_file(path); if (buf == NULL) { fprintf(stderr, "Error: Couldn't open %s\n", path); diff --git a/url.c b/url.c index a255cbb..1307440 100644 --- a/url.c +++ b/url.c @@ -1,3 +1,4 @@ +#include #include "url.h" // This is free and unencumbered software released into the public domain. @@ -44,8 +45,8 @@ static URL_b32 streq(URL_String a, URL_String b) return 0; for (int i = 0; i < a.len; i++) if (a.ptr[i] != b.ptr[i]) - return false; - return true; + return 0; + return 1; } static URL_b32 is_alpha(char c) @@ -320,7 +321,7 @@ int url_parse_ipv6(char *src, int len, int *pcur, URL_IPv6 *out) return 0; } -int url_parse(char *src, int len, int *pcur, bool strict, URL *out) +static int parse(char *src, int len, int *pcur, URL_b32 strict, URL *out) { int cur = pcur ? *pcur : 0; @@ -358,11 +359,12 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) // info or the domain until we hit a @ character or // a character not allowed in the user info, se we // need to backtrack in a similar way to the scheme - URL_b32 authority = 0; + out->no_authority = 1; + out->no_userinfo = 1; int authority_off = cur; if (len - cur > 1 && src[cur] == '/' && src[cur+1] == '/') { cur += 2; // Consume the // - authority = 1; + out->no_authority = 0; out->username = EMPTY; out->password = EMPTY; @@ -404,12 +406,14 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) if (cur == len || src[cur] != '@') { cur = user_off; // Not userinfo after all } else { + out->no_userinfo = 0; out->username = SLICE(src, user_off, pass_off-1); out->password = SLICE(src, pass_off, cur); cur++; // Consume the '@' } } else if (cur < len && src[cur] == '@') { + out->no_userinfo = 0; out->username = SLICE(src, user_off, cur); out->password = EMPTY; cur++; // Consume the '@' @@ -446,7 +450,7 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) URL_b32 is_ipv4 = 0; // First, try the IPv4 rule - if (is_digit(src[cur])) { + if (cur < len && is_digit(src[cur])) { if (url_parse_ipv4(src, len, &cur, &out->host_ipv4) == 0) { out->host_type = URL_HOST_IPV4; is_ipv4 = 1; @@ -474,10 +478,13 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) cur += 3; } } + ASSERT(cur > 0); // Registered names are not allowed to end with a dot, // so unconsume the last character if it is one - if (cur > 0 && src[cur] == '.') + // + // TODO: What if the dot is percent-encoded? + if (src[cur-1] == '.') cur--; } else { @@ -512,6 +519,21 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) out->port = out->port * 10 + n; } } + + // The WHATWG standard considers the sequence "//" + // part of the path and not the authority if userinfo, + // host, and port are missing + if (!strict) { + if ((streq(out->scheme, S("http")) || streq(out->scheme, S("https"))) + && out->username.len == 0 + && out->password.len == 0 + && out->host_type == URL_HOST_EMPTY + && out->no_port) { + out->no_authority = 1; + cur = authority_off; + } + } + } else { out->username = EMPTY; out->password = EMPTY; @@ -521,27 +543,13 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) out->no_port = 1; } - // The WHATWG standard considers the sequence "//" - // part of the path and not the authority if userinfo, - // host, and port are missing - if (!strict) { - if (authority - && out->username.len == 0 - && out->password.len == 0 - && out->host_type == URL_HOST_EMPTY - && out->no_port) { - authority = false; - cur = authority_off; - } - } - // Now we parse the path. This is most difficult // component as it changes based on what comes // before it. // // If an URL contains an authority component, it // must be absolute or empty. - if (authority && (cur == len || src[cur] != '/')) { + if (!out->no_authority && (cur == len || src[cur] != '/')) { out->path = EMPTY; } else { @@ -636,3 +644,260 @@ int url_parse(char *src, int len, int *pcur, bool strict, URL *out) } return 0; } + +int url_parse(char *src, int len, int *pcur, URL_b32 strict, URL *out) +{ + int ret = parse(src, len, pcur, strict, out); + if (ret < 0) + return ret; + + if (out->scheme.len == 0) + return -1; + return 0; +} + +#define PATH_COMPONENT_LIMIT 32 + +typedef struct { + int count; + URL_String stack[PATH_COMPONENT_LIMIT]; +} PathComps; + +static int +resolve_dots_and_append_comps(PathComps *comps, URL_String src) +{ + URL_b32 is_absolute = 0; + if (src.len > 0 && src.ptr[0] == '/') { + is_absolute = 1; + src.ptr++; + src.len--; + if (src.len == 0) + return 0; + } + + int i = 0; + for (;;) { + + int off = i; + while (i < src.len && src.ptr[i] != '/') + i++; + int len = i - off; + + if (len == 0) + return -1; // Empty component + + URL_String comp = { src.ptr + off, len }; + if (comp.len == 2 && comp.ptr[0] == '.' && comp.ptr[1] == '.') { + if (comps->count == 0) { + // For absolute paths, ".." at root is ignored (stays at root) + // For relative paths, ".." with no components references parent, which is invalid + if (!is_absolute) + return -1; + // Otherwise, ignore the ".." (absolute path, already at root) + } else { + comps->count--; + } + } else if (comp.len != 1 || comp.ptr[0] != '.') { + if (comps->count == PATH_COMPONENT_LIMIT) + return -1; // To many components + comps->stack[comps->count++] = comp; + } + + if (i == src.len) + break; + + ASSERT(src.ptr[i] == '/'); + i++; + + if (i == src.len) + break; + } + + return 0; +} + +typedef struct { + char *dst; + int cap; + int len; +} Builder; + +static void append(Builder *b, URL_String s) +{ + int unused = b->cap - b->len; + if (unused > 0) { + int copy = s.len; + if (copy > unused) + copy = unused; + memcpy(b->dst + b->len, s.ptr, copy); + } + b->len += s.len; +} + +static void append_port(Builder *b, URL_u16 port) +{ + char buf[sizeof("65536")-1]; + URL_u16 magn = 10000; + for (int i = 0; i < 5; i++) { + buf[i] = '0' + (port / magn); + port %= magn; + magn /= 10; + } + + // Remove leading zeros + char *ptr = buf; + while (ptr < buf + sizeof(buf)-1 && *ptr == '0') + ptr++; + + append(b, (URL_String) { ptr, 5 - (ptr - buf) }); +} + +static void append_authority(Builder *b, URL url) +{ + if (!url.no_authority) { + append(b, S("//")); + if (!url.no_userinfo) { + append(b, url.username); + append(b, S(":")); + append(b, url.password); + append(b, S("@")); + } + append(b, url.host_text); + if (!url.no_port) { + append(b, S(":")); + append_port(b, url.port); + } + } +} + +int url_resolve_reference(char *src, int len, int *pcur, + URL *base, URL_b32 strict, char *dst, int cap) +{ + if (base != NULL && base->scheme.len == 0) + return -1; // Base is not an absolute URL + + URL ref; + int ret = parse(src, len, pcur, strict, &ref); + if (ret < 0) + return ret; + + Builder b = { dst, cap, 0 }; + if (ref.scheme.len > 0) { + append(&b, ref.scheme); + append(&b, S(":")); + append_authority(&b, ref); + append(&b, ref.path); + if (ref.query.len > 0) { + append(&b, S("?")); + append(&b, ref.query); + } + } else { + + if (base == NULL) { + // No base URL provided, which means the + // source is required to be an absolute URL + return -1; + } + + ASSERT(base->scheme.len > 0); + append(&b, base->scheme); + append(&b, S(":")); + if (!ref.no_authority) { + + append_authority(&b, ref); + + PathComps comps = {0}; + if (resolve_dots_and_append_comps(&comps, ref.path) < 0) + return -1; + for (int i = 0; i < comps.count; i++) { + append(&b, S("/")); + append(&b, comps.stack[i]); + } + + if (ref.query.len > 0) { + append(&b, S("?")); + append(&b, ref.query); + } + + } else { + + append_authority(&b, *base); + + if (ref.path.len == 0) { + append(&b, base->path); + if (ref.query.len > 0) { + append(&b, S("?")); + append(&b, ref.query); + } else if (base->query.len > 0) { + append(&b, S("?")); + append(&b, base->query); + } + } else { + ASSERT(ref.path.len > 0); + PathComps comps = {0}; + if (ref.path.ptr[0] == '/') { + if (resolve_dots_and_append_comps(&comps, ref.path) < 0) + return -1; + } else { + if (!base->no_authority && base->path.len == 0) { + if (resolve_dots_and_append_comps(&comps, ref.path) < 0) + return -1; + } else { + if (resolve_dots_and_append_comps(&comps, base->path) < 0) + return -1; + if (comps.count > 0) { + comps.count--; + } + if (resolve_dots_and_append_comps(&comps, ref.path) < 0) + return -1; + } + } + for (int i = 0; i < comps.count; i++) { + append(&b, S("/")); + append(&b, comps.stack[i]); + } + if (ref.query.len > 0) { + append(&b, S("?")); + append(&b, ref.query); + } + } + } + } + + if (ref.fragment.len > 0) { + append(&b, S("#")); + append(&b, ref.fragment); + } + + return b.len; +} + +static URL_b32 is_white_space(char c) +{ + return c == ' ' + || c == '\n' + || c == '\t' + || c == '\r'; +} + +int url_remove_white_space(char *src, int len, char *dst, int cap) +{ + while (len > 0 && is_white_space(src[0])) { + src++; + len--; + } + + while (len > 0 && is_white_space(src[len-1])) + len--; + + int copied = 0; + for (int i = 0; i < len; i++) { + if (!is_white_space(src[i]) || src[i] == ' ') { + if (copied < cap) + dst[copied] = src[i]; + copied++; + } + } + + return copied; +} diff --git a/url.h b/url.h index a775f5a..acaa2be 100644 --- a/url.h +++ b/url.h @@ -62,6 +62,9 @@ URL_STATIC_ASSERT(sizeof(URL_IPv6) == 16, ""); typedef struct { + URL_b32 no_authority; + URL_b32 no_userinfo; + // May be empty URL_String scheme; @@ -97,8 +100,14 @@ typedef struct { } URL; -int url_parse(char *src, int len, int *pcur, bool strict, URL *out); int url_parse_ipv4(char *src, int len, int *pcur, URL_IPv4 *out); int url_parse_ipv6(char *src, int len, int *pcur, URL_IPv6 *out); +int url_parse(char *src, int len, int *pcur, URL_b32 strict, URL *out); + +int url_resolve_reference(char *src, int len, int *pcur, + URL *base, URL_b32 strict, char *dst, int cap); + +int url_remove_white_space(char *src, int len, char *dst, int cap); + #endif // URL_INCLUDED