diff --git a/.gitignore b/.gitignore index de971ab..c08a326 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,2 @@ -test -test.exe -reference.out -reference.exe -parse.out -parse.exe -decode_field.out -decode_field.exe +*.out +*.exe diff --git a/README.md b/README.md index 740e246..c5ef7f7 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,4 @@ It features * The ability to switch between RFC 3986 and WHATWG with a flag * Relative reference parsing and resolution * URL normalization +* Doesn't rely on null-terminated strings diff --git a/build.bat b/build.bat index 4a821d0..10aeebd 100644 --- a/build.bat +++ b/build.bat @@ -1,3 +1,4 @@ -gcc url.c examples/reference.c -o reference.exe -Wall -Wextra -ggdb -gcc url.c examples/parse.c -o parse.exe -Wall -Wextra -ggdb -gcc url.c examples/decode_field.c -o decode_field.exe -Wall -Wextra -ggdb +gcc -o 000_parse.exe examples/000_parse.c url.c -I. -Wall -Wextra -ggdb +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 diff --git a/build.sh b/build.sh index 7f7ac67..f8b8099 100644 --- a/build.sh +++ b/build.sh @@ -1,3 +1,4 @@ -gcc url.c examples/reference.c -o reference.out -Wall -Wextra -ggdb -gcc url.c examples/parse.c -o parse.out -Wall -Wextra -ggdb -gcc url.c examples/decode_field.c -o decode_field.out -Wall -Wextra -ggdb +gcc -o 000_parse.out examples/000_parse.c url.c -I. -Wall -Wextra -ggdb +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 diff --git a/examples/000_parse.c b/examples/000_parse.c new file mode 100644 index 0000000..25fe855 --- /dev/null +++ b/examples/000_parse.c @@ -0,0 +1,44 @@ +#include +#include +#include +#include + +int main(void) +{ + char url[] = "http://example.com"; + + // This is how you parse an URL + URL parsed_url; + int ret = url_parse(url, strlen(url), NULL, &parsed_url, 0); + if (ret < 0) { + printf("Invalid URL!\n"); + return -1; + } + + // Now the URL's component are stored in the URL + // structure. Note that the fields of URL are slices + // into the source string. + + // Let's print the URL's domain. Note that any + // field except for the scheme may be percent-encoded, + // so if you want the raw representation of a + // field you should decode it: + char domain[1<<9]; + int domain_len = url_percent_decode(parsed_url.host_text, domain, sizeof(domain)); + + // url_percent_decode may fail if the percent-encoding + // is invalid or the buffer is too small. The url_parse + // function validates the percent-encoding, so we only + // need to worry about the buffer size. + assert(domain_len > -1); + if (domain_len >= (int) sizeof(domain)) { + printf("Domain buffer is too small\n"); + return -1; + } + + // Note that url.c never adds null characters to outputs + domain[domain_len] = '\0'; + + printf("The domain is: %s\n", domain); + return 0; +} diff --git a/examples/001_serialize.c b/examples/001_serialize.c new file mode 100644 index 0000000..7bf0745 --- /dev/null +++ b/examples/001_serialize.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include + +int main(void) +{ + // We can translate parsed URLs back into strings + // using the url_serialize function + + char url[] = "http://example.com"; + + URL parsed_url; + int ret = url_parse(url, strlen(url), NULL, &parsed_url, 0); + if (ret < 0) { + printf("Invalid URL!\n"); + return -1; + } + + char buf[1<<9]; + ret = url_serialize(&parsed_url, NULL, buf, sizeof(buf)); + + // On error, url_serialize returns -1. Errors can + // only occur when relative references are involved, + // which isn't our case. + assert(ret > -1); + + // We still need to worry about the buffer's capacity + // though. + if (ret >= (int) sizeof(buf)) { + printf("Serialization buffer is too small\n"); + return -1; + } + + // url.c never adds null terminators + buf[ret] = '\0'; + + printf("Serialized URL: %s\n", buf); + return 0; +} diff --git a/examples/002_partial_parse.c b/examples/002_partial_parse.c new file mode 100644 index 0000000..5ddfb2f --- /dev/null +++ b/examples/002_partial_parse.c @@ -0,0 +1,47 @@ +#include +#include + +int main(void) +{ + // We can also parse URLs when they are part of a larger + // string. This is useful when we know the starting offset + // of an URL but don't know the end. + // + // For instance, say we have this string: + char str[] = " http://websiteA.com http://websiteB.com/index.html "; + int len = sizeof(str)-1; + + // The "pcur" argument of url_parse allows it to tell us + // how long the URL is. + + int cur = 0; + for (;;) { + + // Skip whitespace preceding the URL + while (cur < len && str[cur] == ' ') + cur++; + + if (cur == len) + break; // No more string + + int url_off = cur; + + URL url; + int ret = url_parse(str, len, &cur, &url, 0); + if (ret < 0) { + printf("Invalid URL\n"); + return -1; + } + + int url_len = cur - url_off; + + // Thanks to the pcur argument of url_parse we can infer + // when the URL ends. We can get the original text of the + // URL my slicing the source string using the starting and + // end offsets, or use something like url_serialize to + // translate the URL structure into a string. + printf("Found an URL: %.*s\n", url_len, str + url_off); + } + + return 0; +} diff --git a/examples/003_references.c b/examples/003_references.c new file mode 100644 index 0000000..9896855 --- /dev/null +++ b/examples/003_references.c @@ -0,0 +1,60 @@ +#include +#include +#include + +int main(void) +{ + // url.c also allows us to parse relative references to URLs. + // These are strings that aren't technically URLs but may be + // evaluated to one in reference to a base URL. + // + // Here's an example: + char base_url[] = "http://example.com/files/document.txt"; + char relative_reference[] = "../images/cat.png"; + + // The url_serialize function allows us to translate the + // reference into an URL. But first, we need to parse both + // the base URL and reference. + + URL parsed_base_url; + int ret = url_parse(base_url, strlen(base_url), NULL, &parsed_base_url, 0); + if (ret < 0) { + printf("Invalid base URL\n"); + return -1; + } + + // Note that url_parse will reject relative references by + // default. We need to pass the URL_FLAG_ALLOWREF flag. + URL parsed_relative_reference; + ret = url_parse(relative_reference, strlen(relative_reference), + NULL, &parsed_relative_reference, URL_FLAG_ALLOWREF); + if (ret < 0) { + printf("Invalid relative reference\n"); + return -1; + } + + // Now we can resolve the reference by serializing it with + // the base URL + char buf[1<<9]; + ret = url_serialize(&parsed_relative_reference, &parsed_base_url, buf, sizeof(buf)); + + // Since url_serialize was called with a non-NULL base URL + // argument, it may fail. We need to check for a negative + // return value. + if (ret < 0) { + printf("Reference resolution failed\n"); + return -1; + } + + // Check that the buffer's capacity was enough + if (ret >= (int) sizeof(buf)) { + printf("Serialization buffer is too small\n"); + return -1; + } + + // All good. Now we can print the result + buf[ret] = '\0'; + printf("Resolved reference: %s\n", buf); + + return 0; +} diff --git a/examples/decode_field.c b/examples/decode_field.c deleted file mode 100644 index 2066aed..0000000 --- a/examples/decode_field.c +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include -#include "../url.h" - -int main(void) -{ - char src[] = "http://web%73ite.com"; - - URL parsed; - int ret = url_parse(src, strlen(src), NULL, 1, &parsed); - if (ret < 0) { - printf("Couldn't parse base URL\n"); - return -1; - } - - printf("%.*s\n", - parsed.host_text.len, - parsed.host_text.ptr); - - char buf[100]; - ret = url_decode_field(parsed.host_text, buf, (int) sizeof(buf)-1); - if (ret < 0 || ret >= (int) sizeof(buf)) { - printf("Couldn't decode field\n"); - return -1; - } - - printf("%.*s\n", ret, buf); - return 0; -} diff --git a/examples/parse.c b/examples/parse.c deleted file mode 100644 index c1eb8d7..0000000 --- a/examples/parse.c +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include -#include "../url.h" - -int main(void) -{ - char src[] = "http://website.com/users/000?filter=no#title"; - - URL parsed; - int ret = url_parse(src, strlen(src), NULL, 1, &parsed); - if (ret < 0) { - printf("Couldn't parse base URL\n"); - return -1; - } - - // http - printf("scheme=%.*s\n", - parsed.scheme.len, - parsed.scheme.ptr); - - // (empty) - if (parsed.username.len == 0) - printf("username=(empty)\n"); - else - printf("username=%.*s\n", - parsed.username.len, - parsed.username.ptr); - - // (password) - if (parsed.password.len == 0) - printf("password=(empty)\n"); - else - printf("password=%.*s\n", - parsed.password.len, - parsed.password.ptr); - - if (parsed.host_type == URL_HOST_EMPTY) - printf("host=(empty)\n"); - else - printf("host=%.*s\n", - parsed.host_text.len, - parsed.host_text.ptr); - - if (parsed.no_port) - printf("port=(empty)\n"); - else - printf("port=%d\n", parsed.port); - - printf("path=%.*s\n", - parsed.path.len, - parsed.path.ptr); - - printf("query=%.*s\n", - parsed.query.len, - parsed.query.ptr); - - printf("fragment=%.*s\n", - parsed.fragment.len, - parsed.fragment.ptr); - - return 0; -} diff --git a/examples/reference.c b/examples/reference.c deleted file mode 100644 index 4b4dadb..0000000 --- a/examples/reference.c +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include "../url.h" - -int main(void) -{ - char base[] = "http://website.com/users/000"; - char ref[] = "../images/cat.png"; - - URL parsed_base; - int ret = url_parse(base, strlen(base), NULL, 1, &parsed_base); - if (ret < 0) { - printf("Couldn't parse base URL\n"); - return -1; - } - - char dst[1<<10]; - ret = url_resolve_reference(ref, strlen(ref), NULL, &parsed_base, 1, dst, (int) sizeof(dst)); - if (ret < 0) { - printf("Couldn't resolve reference\n"); - return -1; - } - - // http://website.com/images/cat.png - printf("%.*s\n", ret, dst); - return 0; -} diff --git a/tests/Makefile b/tests/Makefile index b500d66..7b068ab 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -3,11 +3,11 @@ ifeq ($(shell uname -s),Windows_NT) EXT = .exe else - EXT = + EXT = .out endif all: gcc -o test$(EXT) test.c ../url.c cJSON.c -Wall -Wextra -ggdb clean: - rm test test.exe + rm test.out test.exe diff --git a/tests/wpt/test b/tests/wpt/test new file mode 100644 index 0000000..c37ebe1 Binary files /dev/null and b/tests/wpt/test differ diff --git a/url.c b/url.c index 2d58bf6..fff8868 100644 --- a/url.c +++ b/url.c @@ -1,4 +1,3 @@ -#include #include "url.h" // This is free and unencumbered software released into the public domain. @@ -35,10 +34,21 @@ #define UINT8_MAX (URL_u8) ((1 << 8)-1) #define UINT16_MAX (URL_u16) ((1 << 16)-1) +#define NULL ((void*) 0) #define S(X) ((URL_String) { (X), sizeof(X)-1 }) -#define EMPTY (URL_String) { (void*) 0, 0 } +#define EMPTY (URL_String) { NULL, 0 } #define SLICE(src, off, end) (URL_String) { src + off, end - off } +#if defined(__GNUC__) || defined(__clang__) +#define memcpy_ __builtin_memcpy +#else +static void memcpy_(char *dst, char *src, int len) +{ + for (int i = 0; i < len; i++) + dst[i] = src[i]; +} +#endif + static URL_b32 streq(URL_String a, URL_String b) { if (a.len != b.len) @@ -734,7 +744,7 @@ static void append(Builder *b, URL_String s) int copy = s.len; if (copy > unused) copy = unused; - memcpy(b->dst + b->len, s.ptr, copy); + memcpy_(b->dst + b->len, s.ptr, copy); } b->len += s.len; } @@ -806,7 +816,7 @@ int url_serialize(URL *url, URL *base, char *dst, int cap) Builder b = { dst, cap, 0 }; if (url->scheme.len > 0) { append_scheme(&b, url->scheme); - append_authority(&b, url); + append_authority(&b, *url); append(&b, url->path); append_query(&b, url->query); } else { @@ -818,7 +828,7 @@ int url_serialize(URL *url, URL *base, char *dst, int cap) ASSERT(base->scheme.len > 0); append_scheme(&b, base->scheme); if (!url->no_authority) { - append_authority(&b, url); + append_authority(&b, *url); PathComps comps = {0}; if (resolve_dots_and_append_comps(&comps, url->path) < 0) return -1; diff --git a/url.h b/url.h index c930241..0ff1b56 100644 --- a/url.h +++ b/url.h @@ -25,23 +25,19 @@ // // For more information, please refer to -#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 struct { URL_u32 data; } URL_IPv4; +typedef struct { URL_u16 data[8]; } URL_IPv6; + typedef enum { URL_HOST_EMPTY, URL_HOST_IPV4, @@ -49,17 +45,6 @@ typedef enum { 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 { // These flags are used to differentiate between @@ -79,6 +64,7 @@ typedef struct { URL_String scheme; // Both may be empty + // May be percent-encoded URL_String username; URL_String password; @@ -88,6 +74,7 @@ typedef struct { // (host byte order). // Note that the host may be empty, in which // case host_type=URL_HOST_EMPTY. + // If host_text is set, it may be percent-encoded. URL_HostType host_type; URL_String host_text; union { @@ -100,12 +87,15 @@ typedef struct { URL_b32 no_port; URL_u16 port; + // May be percent-encoded URL_String path; // May be empty + // May be percent-encoded URL_String query; // May be empty + // May be percent-encoded URL_String fragment; } URL;