From 4871f5168d143444aa946a09a7e37b3746b75665 Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Sat, 6 Dec 2025 13:16:14 +0100 Subject: [PATCH] First version of the URL builder --- build.bat | 1 + build.sh | 1 + examples/004_build.c | 33 ++++++++ url.c | 176 ++++++++++++++++++++++++++++++++++++++++++- url.h | 36 +++++++++ 5 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 examples/004_build.c diff --git a/build.bat b/build.bat index 10aeebd..e5635ec 100644 --- a/build.bat +++ b/build.bat @@ -2,3 +2,4 @@ gcc -o 000_parse.exe examples/000_parse.c url.c -I. -Wall -Wextr gcc -o 001_serialize.exe examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb gcc -o 002_partial_parse.exe examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb gcc -o 003_references.exe examples/003_references.c url.c -I. -Wall -Wextra -ggdb +gcc -o 004_build.exe examples/004_build.c url.c -I. -Wall -Wextra -ggdb diff --git a/build.sh b/build.sh index f8b8099..682c149 100644 --- a/build.sh +++ b/build.sh @@ -2,3 +2,4 @@ gcc -o 000_parse.out examples/000_parse.c url.c -I. -Wall -Wextr gcc -o 001_serialize.out examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb gcc -o 002_partial_parse.out examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb gcc -o 003_references.out examples/003_references.c url.c -I. -Wall -Wextra -ggdb +gcc -o 004_build.out examples/004_build.c url.c -I. -Wall -Wextra -ggdb diff --git a/examples/004_build.c b/examples/004_build.c new file mode 100644 index 0000000..763fa91 --- /dev/null +++ b/examples/004_build.c @@ -0,0 +1,33 @@ +#include +#include + +int main(void) +{ + URL_Builder builder; + url_builder_init(&builder); + + url_builder_set_scheme(&builder, "http", -1); + + url_builder_set_username(&builder, "cozis", -1); + + url_builder_set_password(&builder, "my_secret", -1); + + url_builder_set_host_name(&builder, "www.ex@mple.com", -1); + + url_builder_set_path(&builder, "/../../index.html", -1); + + char url[1<<9]; + int len = url_builder_finalize(&builder, url, sizeof(url)); + if (len < 0) { + printf("Couldn't build URL\n"); + return -1; + } + if (len >= (int) sizeof(url)) { + printf("URL buffer is too small\n"); + return -1; + } + url[len] = '\0'; + + printf("Built URL: %s\n", url); + return 0; +} diff --git a/url.c b/url.c index 9731baa..aa12fb4 100644 --- a/url.c +++ b/url.c @@ -35,7 +35,7 @@ #define UINT16_MAX (URL_u16) ((1 << 16)-1) #define NULL ((void*) 0) -#define S(X) ((URL_String) { (X), sizeof(X)-1 }) +#define S(X) (URL_String) { (X), sizeof(X)-1 } #define EMPTY (URL_String) { NULL, 0 } #define SLICE(src, off, end) (URL_String) { src + off, end - off } @@ -49,6 +49,14 @@ static void memcpy_(char *dst, char *src, int len) } #endif +static int strlen_(char *s) +{ + int n = 0; + while (s[n] != '\0') + n++; + return n; +} + static URL_b32 streq(URL_String a, URL_String b) { if (a.len != b.len) @@ -784,6 +792,13 @@ static void append(Builder *b, URL_String s) b->len += s.len; } +static void appendc(Builder *b, char c) +{ + if (b->len < b->cap) + b->dst[b->len] = c; + b->len++; +} + static void append_port(Builder *b, URL_u16 port) { char buf[sizeof("65536")-1]; @@ -998,3 +1013,162 @@ int url_percent_decode(URL_String str, char *dst, int cap) } return wr; } + +static void append_percent_encoded(Builder *b, URL_String str, URL_b32 (*testfn)(char c)) +{ + for (int i = 0; i < str.len; i++) { + char c = str.ptr[i]; + if (testfn(c)) + appendc(b, str.ptr[i]); + else { + static const char table[] = "0123456789abcdef"; + appendc(b, '%'); + appendc(b, table[(URL_u8) c >> 4]); + appendc(b, table[(URL_u8) c & 0xF]); + } + } +} + +void url_builder_init(URL_Builder *ub) +{ + ub->scheme = EMPTY; + ub->username = EMPTY; + ub->password = EMPTY; + ub->host_type = URL_HOST_EMPTY; + ub->no_port = 1; + ub->port = 0; + ub->path = EMPTY; + ub->query = EMPTY; + ub->fragment = EMPTY; +} + +void url_builder_set_scheme(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->scheme = (URL_String) { str, len }; +} + +void url_builder_set_username(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->username = (URL_String) { str, len }; +} + +void url_builder_set_password(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->password = (URL_String) { str, len }; +} + +void url_builder_set_host_name(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->host_type = URL_HOST_NAME; + ub->host_name = (URL_String) { str, len }; +} + +void url_builder_set_host_ipv4(URL_Builder *ub, URL_IPv4 ipv4) +{ + ub->host_type = URL_HOST_IPV4; + ub->host_ipv4 = ipv4; +} + +void url_builder_set_host_ipv6(URL_Builder *ub, URL_IPv6 ipv6) +{ + ub->host_type = URL_HOST_IPV6; + ub->host_ipv6 = ipv6; +} + +void url_builder_set_port(URL_Builder *ub, URL_u16 port) +{ + ub->no_port = 0; + ub->port = port; +} + +void url_builder_set_path(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->path = (URL_String) { str, len }; +} + +void url_builder_set_query(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->query = (URL_String) { str, len }; +} + +void url_builder_set_fragment(URL_Builder *ub, char *str, int len) +{ + if (len < 0) len = str ? strlen_(str) : 0; + ub->fragment = (URL_String) { str, len }; +} + +int url_builder_finalize(URL_Builder *ub, char *dst, int cap) +{ + Builder b = { dst, cap, 0 }; + + // Validate and append the schema + { + if (ub->scheme.len == 0 || !is_scheme_first(ub->scheme.ptr[0])) + return -1; + for (int i = 1; i < ub->scheme.len; i++) + if (!is_scheme(ub->scheme.ptr[i])) + return -1; + append(&b, ub->scheme); + } + + URL_b32 authority = 0; + if (ub->username.len > 0 || ub->password.len > 0 || ub->host_type != URL_HOST_EMPTY || ub->no_port == 0) { + authority = 1; + appendc(&b, '/'); + appendc(&b, '/'); + if (ub->username.len > 0 || ub->password.len > 0) { + append_percent_encoded(&b, ub->username, is_userinfo); + appendc(&b, ':'); + append_percent_encoded(&b, ub->password, is_userinfo); + appendc(&b, '@'); + } + switch (ub->host_type) { + case URL_HOST_EMPTY: + // Do nothing + break; + case URL_HOST_NAME: + append_percent_encoded(&b, ub->host_name, is_reg_name); + break; + case URL_HOST_IPV4: + ASSERT(0); // TODO + break; + case URL_HOST_IPV6: + appendc(&b, '['); + ASSERT(0); + appendc(&b, ']'); + break; + } + if (ub->no_port == 0) { + appendc(&b, ':'); + append_port(&b, ub->port); + } + } + + if (ub->path.len > 0) { + + // If an authority is present, the path can't be relative + if (authority && ub->path.ptr[0] != '/') + return -1; + + PathComps comps = {0}; + if (resolve_dots_and_append_comps(&comps, ub->path) < 0) + return -1; + for (int i = 0; i < comps.count; i++) { + if (i > 0 || comps.first_slash) + append(&b, S("/")); + append_percent_encoded(&b, comps.stack[i], is_pchar); + } + if (comps.trailing_slash) + append(&b, S("/")); + } + + append_query(&b, ub->query); + append_fragment(&b, ub->fragment); + return b.len; +} diff --git a/url.h b/url.h index fb65154..29fd7f7 100644 --- a/url.h +++ b/url.h @@ -45,6 +45,7 @@ typedef enum { URL_HOST_NAME, } URL_HostType; +// This struct's fields are strictly read-only. typedef struct { // Flags used to parse this URL @@ -174,4 +175,39 @@ int url_percent_decode(URL_String str, char *dst, int cap); // though it's just a best-effor normalization for now. int url_serialize(URL url, URL *base, char *dst, int cap); +typedef struct { + + URL_String scheme; + + URL_String username; + URL_String password; + + URL_HostType host_type; + union { + URL_String host_name; + URL_IPv4 host_ipv4; + URL_IPv6 host_ipv6; + }; + + URL_b32 no_port; + URL_u16 port; + + URL_String path; + URL_String query; + URL_String fragment; +} URL_Builder; + +void url_builder_init(URL_Builder *ub); +void url_builder_set_scheme(URL_Builder *ub, char *str, int len); +void url_builder_set_username(URL_Builder *ub, char *str, int len); +void url_builder_set_password(URL_Builder *ub, char *str, int len); +void url_builder_set_host_name(URL_Builder *ub, char *str, int len); +void url_builder_set_host_ipv4(URL_Builder *ub, URL_IPv4 ipv4); +void url_builder_set_host_ipv6(URL_Builder *ub, URL_IPv6 ipv6); +void url_builder_set_port(URL_Builder *ub, URL_u16 port); +void url_builder_set_path(URL_Builder *ub, char *str, int len); +void url_builder_set_query(URL_Builder *ub, char *str, int len); +void url_builder_set_fragment(URL_Builder *ub, char *str, int len); +int url_builder_finalize(URL_Builder *ub, char *dst, int cap); + #endif // URL_INCLUDED