First commit

This commit is contained in:
2025-12-03 17:15:57 +01:00
commit 1c54c8ae28
6 changed files with 787 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
test
test.exe
+13
View File
@@ -0,0 +1,13 @@
.PHONY: all clean
ifeq ($(shell uname -s),Windows_NT)
EXT = .exe
else
EXT =
endif
all:
gcc -o test$(EXT) test.c url.c -Wall -Wextra
clean:
rm test test.exe
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
+63
View File
@@ -0,0 +1,63 @@
#include <stdio.h>
#include <string.h>
#include "url.h"
#define UNPACK(s) (s).len, (s).ptr
static char *host_type_to_str(URL_HostType type)
{
switch (type) {
case URL_HOST_EMPTY:
return "";
case URL_HOST_IPV4:
return "IPv4";
case URL_HOST_IPV6:
return "IPv6";
case URL_HOST_NAME:
return "name";
default:
}
return "???";
}
static void dump_parsed_url(URL *parsed)
{
printf(
"URL {\n"
" scheme = [%.*s]\n"
" username = [%.*s]\n"
" password = [%.*s]\n"
" host = [%.*s] (%s)\n"
" port = [%d]%s\n"
" path = [%.*s]\n"
" query = [%.*s]\n"
" fragment = [%.*s]\n"
"}\n",
UNPACK(parsed->scheme),
UNPACK(parsed->username),
UNPACK(parsed->password),
UNPACK(parsed->host_text),
host_type_to_str(parsed->host_type),
parsed->port,
parsed->no_port ? " (empty)" : "",
UNPACK(parsed->path),
UNPACK(parsed->query),
UNPACK(parsed->fragment));
}
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Missing URL\n");
return -1;
}
char *url = argv[1];
URL parsed;
int ret = url_parse(url, strlen(url), NULL, &parsed);
printf("ret=%d\n", ret);
if (ret == 0)
dump_parsed_url(&parsed);
return 0;
}
+581
View File
@@ -0,0 +1,581 @@
#include "url.h"
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <https://unlicense.org>
#ifdef NDEBUG
#define ASSERT(x) {}
#else
#define ASSERT(x) { if (!(x)) __builtin_trap(); }
#endif
#define UINT8_MAX (URL_u8) ((1 << 8)-1)
#define UINT16_MAX (URL_u16) ((1 << 16)-1)
#define EMPTY (URL_String) { (void*) 0, 0 }
#define SLICE(src, off, end) (URL_String) { src + off, end - off }
static URL_b32 is_alpha(char c)
{
return (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z');
}
static URL_b32 is_digit(char c)
{
return c >= '0' && c <= '9';
}
static URL_b32 is_hex_digit(char c)
{
return (c >= '0' && c <= '9')
|| (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F');
}
#if 0
static URL_b32 is_gen_delim(char c)
{
return c == ':'
|| c == '/'
|| c == '?'
|| c == '#'
|| c == '['
|| c == ']'
|| c == '@';
}
#endif
static URL_b32 is_sub_delim(char c)
{
return c == '!'
|| c == '$'
|| c == '&'
|| c == '\''
|| c == '('
|| c == ')'
|| c == '*'
|| c == '+'
|| c == ','
|| c == ';'
|| c == '=';
}
#if 0
static URL_b32 is_reserved(char c)
{
return is_gen_delim(c)
|| is_sub_delim(c);
}
#endif
static URL_b32 is_unreserved(char c)
{
return is_alpha(c)
|| is_digit(c)
|| c == '-'
|| c == '.'
|| c == '_'
|| c == '~';
}
static URL_b32 is_scheme_first(char c)
{
return is_alpha(c);
}
static URL_b32 is_scheme(char c)
{
return is_alpha(c)
|| is_digit(c)
|| c == '+'
|| c == '-'
|| c == '.';
}
static URL_b32 is_userinfo(char c)
{
return is_unreserved(c)
|| is_sub_delim(c);
}
static URL_b32 is_reg_name(char c)
{
return is_unreserved(c)
|| is_sub_delim(c);
}
static URL_b32 is_pchar(char c)
{
return is_unreserved(c)
|| is_sub_delim(c)
|| c == ':'
|| c == '@';
}
static URL_b32 is_query(char c)
{
return is_pchar(c)
|| c == '/'
|| c == '?';
}
static URL_b32 is_fragment(char c)
{
return is_pchar(c)
|| c == '/'
|| c == '?';
}
// Returns 1 if a percent encoded character is
// at offset "cur" and 0 otherwise. If a '%'
// character is found but the following characters
// aren't hex, -1 is returned.
static int is_percent_encoded(char *src, int len, int cur)
{
if (cur == len || src[cur] != '%')
return 0;
if (len - cur <= 2
|| !is_hex_digit(src[cur+1])
|| !is_hex_digit(src[cur+2]))
return -1;
return 1;
}
int url_parse_ipv4(char *src, int len, int *pcur, URL_IPv4 *out)
{
int cur = pcur ? *pcur : 0;
out->data = 0;
for (int i = 0; i < 4; i++) {
if (cur == len || !is_digit(src[cur]))
return -1;
URL_u8 byte = src[cur] - '0';
cur++;
while (cur < len && is_digit(src[cur])) {
int n = src[cur] - '0';
cur++;
if (byte > (UINT8_MAX - n) / 10)
return -1;
byte = byte * 10 + n;
}
if (i < 3) {
if (cur == len || src[cur] != '.')
return -1;
cur++;
}
out->data <<= 8;
out->data |= byte;
}
if (pcur) *pcur = cur;
return 0;
}
static int hex_digit_to_int(char c)
{
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return c - '0';
}
static int
parse_ipv6_comp(char *src, int len, int *pcur)
{
int cur = pcur ? *pcur : 0;
unsigned short buf;
if (cur == len || !is_hex_digit(src[cur]))
return -1;
buf = hex_digit_to_int(src[cur]);
cur++;
if (cur == len || !is_hex_digit(src[cur]))
goto success;
buf <<= 4;
buf |= hex_digit_to_int(src[cur]);
cur++;
if (cur == len || !is_hex_digit(src[cur]))
goto success;
buf <<= 4;
buf |= hex_digit_to_int(src[cur]);
cur++;
if (cur == len || !is_hex_digit(src[cur]))
goto success;
buf <<= 4;
buf |= hex_digit_to_int(src[cur]);
cur++;
success:
if (pcur) *pcur = cur;
return buf;
}
int url_parse_ipv6(char *src, int len, int *pcur, URL_IPv6 *out)
{
int cur = pcur ? *pcur : 0;
URL_u16 head[8];
URL_u16 tail[8];
int head_len = 0;
int tail_len = 0;
if (len - cur > 1
&& src[cur+0] == ':'
&& src[cur+1] == ':')
cur += 2;
else {
for (;;) {
int ret = parse_ipv6_comp(src, len, &cur);
if (ret < 0)
return ret;
head[head_len++] = (URL_u16) ret;
if (head_len == 8)
break;
if (cur == len || src[cur] != ':')
return -1;
cur++;
if (cur < len && src[cur] == ':') {
cur++;
break;
}
}
}
if (head_len < 8) {
while (cur < len && is_hex_digit(src[cur])) {
int ret = parse_ipv6_comp(src, len, &cur);
if (ret < 0)
return ret;
tail[tail_len++] = (URL_u16) ret;
if (head_len + tail_len == 8)
break;
if (cur == len || src[cur] != ':')
break;
cur++;
}
}
for (int i = 0; i < head_len; i++)
out->data[i] = head[i];
for (int i = 0; i < 8 - head_len - tail_len; i++)
out->data[head_len + i] = 0;
for (int i = 0; i < tail_len; i++)
out->data[8 - tail_len + i] = tail[i];
return 0;
}
int url_parse(char *src, int len, int *pcur, URL *out)
{
int cur = pcur ? *pcur : 0;
// Parse scheme characters until the end of the string,
// a character not allowed in the scheme, or ':'. If the
// found character wasn't ':', set the scheme to empty
// and rollback the cursor.
out->scheme = EMPTY;
if (cur < len && is_scheme_first(src[cur])) {
// Save the index of the first character of the scheme
// and advance
int off = cur;
cur++;
// Consume characters from the body of the scheme
while (cur < len && is_scheme(src[cur]))
cur++;
if (cur == len || src[cur] != ':') {
cur = 0; // Not a scheme after all
} else {
out->scheme = SLICE(src, off, cur);
cur++; // Consume the ':'
}
}
// If the following characters are // we expect an
// authority. An authority consists of some optional
// userinfo, a domain name, and a port.
//
// scheme://user:pass@domain.com:port...
//
// We won't know whether the first portion is user
// 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;
if (len - cur > 1 && src[cur] == '/' && src[cur+1] == '/') {
cur += 2; // Consume the //
authority = 1;
out->username = EMPTY;
out->password = EMPTY;
if (cur < len && (is_userinfo(src[cur]) || src[cur] == ':')) {
int user_off = cur;
cur++;
while (cur < len) {
if (is_userinfo(src[cur]))
cur++;
else {
int ret = is_percent_encoded(src, len, cur);
if (ret < 0)
return ret;
if (ret == 0)
break;
cur += 3;
}
}
if (cur < len && src[cur] == ':') {
cur++; // Consume the ':'
int pass_off = cur;
while (cur < len) {
if (is_userinfo(src[cur]))
cur++;
else {
int ret = is_percent_encoded(src, len, cur);
if (ret < 0)
return ret;
if (ret == 0)
break;
cur += 3;
}
}
if (cur == len || src[cur] != '@') {
cur = user_off; // Not userinfo after all
} else {
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->username = SLICE(src, user_off, cur);
out->password = EMPTY;
cur++; // Consume the '@'
} else {
cur = user_off; // Not userinfo
}
}
// The domain may be a registered name, an IPv4 address,
// or an IPv6 address.
// example.com
// 127.0.0.1
// [abcd:abcd::0001]
// Note that an IPv4 could technically be parsed as a
// registered name, so it's important that we first try
// the IPv4 rule, and then the registered name rule.
int host_off = cur;
if (cur < len && src[cur] == '[') {
cur++; // Consume the '['
// We started with an '[', so it's definitely an IPv6
out->host_type = URL_HOST_IPV6;
int ret = url_parse_ipv6(src, len, &cur, &out->host_ipv6);
if (ret < 0)
return ret;
if (cur == len || src[cur] != ']')
return -1; // Missing ']' after IPv6
cur++;
} else {
URL_b32 is_ipv4 = 0;
// First, try the IPv4 rule
if (is_digit(src[cur])) {
if (url_parse_ipv4(src, len, &cur, &out->host_ipv4) == 0) {
out->host_type = URL_HOST_IPV4;
is_ipv4 = 1;
}
}
if (!is_ipv4) {
if (cur < len && (is_reg_name(src[cur]) || is_percent_encoded(src, len, cur))) {
int ret = is_percent_encoded(src, len, cur);
ASSERT(ret >= 0);
cur += (ret == 1) ? 3 : 1;
out->host_type = URL_HOST_NAME;
while (cur < len) {
if (is_reg_name(src[cur]))
cur++;
else {
int ret = is_percent_encoded(src, len, cur);
if (ret < 0)
return ret;
if (ret == 0)
break;
cur += 3;
}
}
} else {
out->host_type = URL_HOST_EMPTY;
}
}
}
out->host_text = SLICE(src, host_off, cur);
// Now we parse the port
out->no_port = 1;
out->port = 0;
if (cur < len && src[cur] == ':') {
cur++; // Consume the ':'
out->no_port = 0;
while (cur < len && is_digit(src[cur])) {
int n = src[cur] - '0';
cur++;
if (out->port > (UINT16_MAX - n) / 10)
return -1; // Overflow
out->port = out->port * 10 + n;
}
}
}
// 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] != '/')) {
out->path = EMPTY;
} else {
int off = cur;
while (cur < len) {
if (is_pchar(src[cur]) || src[cur] == '/')
cur++;
else {
int ret = is_percent_encoded(src, len, cur);
if (ret < 0)
return ret;
if (ret == 0)
break;
cur += 3;
}
}
out->path = SLICE(src, off, cur);
}
// Consume the query
out->query = EMPTY;
if (cur < len && src[cur] == '?') {
cur++; // Consume '?'
int off = cur;
while (cur < len) {
if (is_query(src[cur]))
cur++;
else {
int ret = is_percent_encoded(src, len, cur);
if (ret < 0)
return ret;
if (ret == 0)
break;
cur += 3;
}
}
out->query = SLICE(src, off, cur);
}
// Consume the fragment
out->fragment = EMPTY;
if (cur < len && src[cur] == '#') {
cur++; // Consume '#'
int off = cur;
while (cur < len) {
if (is_fragment(src[cur]))
cur++;
else {
int ret = is_percent_encoded(src, len, cur);
if (ret < 0)
return ret;
if (ret == 0)
break;
cur += 3;
}
}
out->fragment = SLICE(src, off, cur);
}
// If a cursor pointer was provided, it is assumed
// the caller is parsing a string that contains an
// URL and may be followed by something else, therefore
// we allow partially parsing the source. If no cursor
// pointer was provided, we expect the source to be
// an exact URL string.
if (pcur) {
*pcur = cur;
} else {
if (cur != len)
return -1;
}
return 0;
}
+104
View File
@@ -0,0 +1,104 @@
#ifndef URL_INCLUDED
#define URL_INCLUDED
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <https://unlicense.org>
#define URL_STATIC_ASSERT _Static_assert
typedef unsigned char URL_u8;
typedef unsigned short URL_u16;
typedef unsigned int URL_u32;
typedef unsigned int URL_b32;
URL_STATIC_ASSERT(sizeof(URL_u8) == 1, "");
URL_STATIC_ASSERT(sizeof(URL_u16) == 2, "");
URL_STATIC_ASSERT(sizeof(URL_u32) == 4, "");
URL_STATIC_ASSERT(sizeof(URL_b32) == 4, "");
typedef struct {
char *ptr;
int len;
} URL_String;
typedef enum {
URL_HOST_EMPTY,
URL_HOST_IPV4,
URL_HOST_IPV6,
URL_HOST_NAME,
} URL_HostType;
typedef struct {
URL_u32 data;
} URL_IPv4;
typedef struct {
URL_u16 data[8];
} URL_IPv6;
URL_STATIC_ASSERT(sizeof(URL_IPv4) == 4, "");
URL_STATIC_ASSERT(sizeof(URL_IPv6) == 16, "");
typedef struct {
// May be empty
URL_String scheme;
// Both may be empty
URL_String username;
URL_String password;
// The raw host string is stored in host_text.
// If the host is an IPv4 or IPv6, its parsed
// value is also stored in host_ipv4 or host_ipv6
// (host byte order).
// Note that the host may be empty, in which
// case host_type=URL_HOST_EMPTY.
URL_HostType host_type;
URL_String host_text;
union {
URL_IPv4 host_ipv4;
URL_IPv6 host_ipv6;
};
// If no port was specified, no_port is set
// to 1 and port to 0.
URL_b32 no_port;
URL_u16 port;
URL_String path;
// May be empty
URL_String query;
// May be empty
URL_String fragment;
} URL;
int url_parse(char *src, int len, int *pcur, 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);
#endif // URL_INCLUDED