Add url_resolve_reference to translare relative references to absolute URLs

This commit is contained in:
2025-12-06 01:25:22 +01:00
parent 1d53c3d8ac
commit f5b0f3a142
3 changed files with 306 additions and 24 deletions
+9 -1
View File
@@ -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);
+287 -22
View File
@@ -1,3 +1,4 @@
#include <string.h>
#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;
}
+10 -1
View File
@@ -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