commit e8b86f0d1557b265a856c14dee2ec90610469da5
parent bc4c4716d153dfc31e3ace78cb37a31efa87dd60
Author: finwo <finwo@pm.me>
Date: Tue, 3 Mar 2026 12:48:14 +0100
Lib rewrite
Diffstat:
11 files changed, 955 insertions(+), 128 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
@@ -0,0 +1,22 @@
+name: Test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install GCC
+ run: sudo apt-get update && sudo apt-get install -y gcc
+
+ - name: Run tests
+ run: |
+ cd test
+ make run
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+/test/unix-socket
+/test/empty-host
+/test/basic
+*.o
+.DS_Store
+._.DS_Store
diff --git a/LICENSE.md b/LICENSE.md
@@ -0,0 +1,34 @@
+Copyright (c) 2026 finwo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to use, copy,
+modify, and distribute the Software, subject to the following conditions:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions, and the following disclaimer.
+
+ 2. Redistributions in binary form, or any public offering of the Software
+ (including hosted or managed services), must reproduce the above copyright
+ notice, this list of conditions, and the following disclaimer in the
+ documentation and/or other materials provided.
+
+ 3. Any redistribution or public offering of the Software must clearly attribute
+ the Software to the original copyright holder, reference this License, and
+ include a link to the official project repository or website.
+
+ 4. The Software may not be renamed, rebranded, or marketed in a manner that
+ implies it is an independent or proprietary product. Derivative works must
+ clearly state that they are based on the Software.
+
+ 5. Modifications to copies of the Software must carry prominent notices stating
+ that changes were made, the nature of the modifications, and the date of the
+ modifications.
+
+Any violation of these conditions terminates the permissions granted herein.
+
+THIS 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 COPYRIGHT
+HOLDER OR CONTRIBUTORS 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.
diff --git a/README.md b/README.md
@@ -0,0 +1,99 @@
+# url-parser
+
+A C library for parsing URLs, with support for Unix socket paths and empty hosts.
+
+## Installation
+
+This library can be installed using the [dep](https://github.com/finwo/dep) package manager:
+
+```bash
+dep add finwo/url-parser
+```
+
+## Usage
+
+```c
+#include "url-parser.h"
+
+struct parsed_url *purl = parse_url("http://www.example.com/path?query=value#fragment");
+
+if (purl != NULL) {
+ printf("Scheme: %s\n", purl->scheme);
+ printf("Host: %s\n", purl->host);
+ printf("Port: %s\n", purl->port);
+ printf("Path: %s\n", purl->path);
+ printf("Query: %s\n", purl->query);
+ printf("Fragment: %s\n", purl->fragment);
+ printf("Username: %s\n", purl->username);
+ printf("Password: %s\n", purl->password);
+
+ parsed_url_free(purl);
+}
+```
+
+## Features
+
+### Standard URL Parsing
+
+```c
+parse_url("http://example.com/path");
+// scheme: "http", host: "example.com", path: "/path"
+
+parse_url("https://example.com:8080/path");
+// scheme: "https", host: "example.com", port: "8080", path: "/path"
+
+parse_url("ftp://user:password@ftp.example.com/file");
+// scheme: "ftp", host: "ftp.example.com", username: "user", password: "password", path: "/file"
+
+parse_url("http://[::1]:8080/path");
+// scheme: "http", host: "::1", port: "8080", path: "/path"
+```
+
+### Query and Fragment Only
+
+```c
+parse_url("http://example.com?foo=bar");
+// scheme: "http", host: "example.com", query: "foo=bar"
+
+parse_url("http://example.com#section");
+// scheme: "http", host: "example.com", fragment: "section"
+```
+
+### Empty Host
+
+```c
+parse_url("tcp://:6379");
+// scheme: "tcp", host: NULL, port: "6379"
+```
+
+### Unix Socket URLs
+
+The library supports Unix socket paths with various formats:
+
+```c
+// Standard unix socket
+parse_url("unix:///var/run/redis.sock");
+// scheme: "unix", path: "/var/run/redis.sock", host: NULL
+
+// Unix socket with leading slash
+parse_url("unix:/path/to/socket");
+// scheme: "unix", path: "/path/to/socket"
+
+// Unix socket without slashes
+parse_url("unix:redis.sock");
+// scheme: "unix", path: "redis.sock"
+
+// Unix socket with credentials
+parse_url("unix://user:pass@/path/to/socket");
+// scheme: "unix", username: "user", password: "pass", path: "/path/to/socket"
+
+// Redis/Postgres style unix socket
+parse_url("redis:///var/run/redis.sock");
+// scheme: "redis", path: "/var/run/redis.sock"
+```
+
+Note: For path-based schemes (`unix:`, `file:`, `cunix:`), the entire portion after `://` is treated as the path, and no host or port is parsed.
+
+## License
+
+Copyright (c) 2026 finwo. See LICENSE.md for details.
diff --git a/src/url-parser.c b/src/url-parser.c
@@ -1,8 +1,38 @@
/*_
- * Copyright 2010-2011 Scyphus Solutions Co. Ltd. All rights reserved.
+ * Copyright (c) 2026 finwo
*
- * Authors:
- * Hirochika Asai
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to use, copy,
+ * modify, and distribute the Software, subject to the following conditions:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions, and the following disclaimer.
+ *
+ * 2. Redistributions in binary form, or any public offering of the Software
+ * (including hosted or managed services), must reproduce the above copyright
+ * notice, this list of conditions, and the following disclaimer in the
+ * documentation and/or other materials provided.
+ *
+ * 3. Any redistribution or public offering of the Software must clearly attribute
+ * the Software to the original copyright holder, reference this License, and
+ * include a link to the official project repository or website.
+ *
+ * 4. The Software may not be renamed, rebranded, or marketed in a manner that
+ * implies it is an independent or proprietary product. Derivative works must
+ * clearly state that they are based on the Software.
+ *
+ * 5. Modifications to copies of the Software must carry prominent notices stating
+ * that changes were made, the nature of the modifications, and the date of the
+ * modifications.
+ *
+ * Any violation of these conditions terminates the permissions granted herein.
+ *
+ * THIS 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 COPYRIGHT
+ * HOLDER OR CONTRIBUTORS 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.
*/
#include "url-parser.h"
@@ -18,6 +48,27 @@
static __inline__ int _is_scheme_char(int);
/*
+ * Check if scheme is a path-based scheme (unix socket, file path, etc.)
+ */
+static __inline__ int
+_is_path_scheme(const char *scheme)
+{
+ if ( NULL == scheme ) {
+ return 0;
+ }
+ if ( 0 == strncmp(scheme, "unix", 4) && scheme[4] == '\0' ) {
+ return 1;
+ }
+ if ( 0 == strncmp(scheme, "file", 4) && scheme[4] == '\0' ) {
+ return 1;
+ }
+ if ( 0 == strncmp(scheme, "cunix", 5) && scheme[5] == '\0' ) {
+ return 1;
+ }
+ return 0;
+}
+
+/*
* Check whether the character is permitted in scheme string
*/
static __inline__ int
@@ -39,6 +90,7 @@ parse_url(const char *url)
int i;
int userpass_flag;
int bracket_flag;
+ int is_path;
/* Allocate the parsed url storage */
purl = malloc(sizeof(struct parsed_url));
@@ -64,7 +116,6 @@ parse_url(const char *url)
/* Read scheme */
tmpstr = strchr(curstr, ':');
if ( NULL == tmpstr ) {
- /* Not found the character */
parsed_url_free(purl);
return NULL;
}
@@ -73,7 +124,6 @@ parse_url(const char *url)
/* Check restrictions */
for ( i = 0; i < len; i++ ) {
if ( !_is_scheme_char(curstr[i]) ) {
- /* Invalid format */
parsed_url_free(purl);
return NULL;
}
@@ -90,194 +140,226 @@ parse_url(const char *url)
for ( i = 0; i < len; i++ ) {
purl->scheme[i] = tolower(purl->scheme[i]);
}
+
+ /* Check if this is a path-based scheme */
+ is_path = _is_path_scheme(purl->scheme);
+
/* Skip ':' */
tmpstr++;
curstr = tmpstr;
/*
- * //<user>:<password>@<host>:<port>/<url-path>
- * Any ":", "@" and "/" must be encoded.
+ * Normalize: ensure we have // after scheme
+ * If missing, treat everything as path
*/
- /* Eat "//" */
- for ( i = 0; i < 2; i++ ) {
- if ( '/' != *curstr ) {
- parsed_url_free(purl);
- return NULL;
+ if ( '/' != curstr[0] || '/' != curstr[1] ) {
+ /* No // - entire rest is path */
+ tmpstr = curstr;
+ while ( '\0' != *tmpstr ) {
+ tmpstr++;
}
- curstr++;
+ len = tmpstr - curstr;
+ if ( len > 0 ) {
+ purl->path = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->path ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->path, curstr, len);
+ purl->path[len] = '\0';
+ }
+ return purl;
}
- /* Check if the user (and password) are specified. */
- userpass_flag = 0;
+ /* Skip the "//" */
+ curstr += 2;
+
+ /*
+ * Detect and consume username:password, consume @
+ */
tmpstr = curstr;
- while ( '\0' != *tmpstr ) {
- if ( '@' == *tmpstr ) {
- /* Username and password are specified */
- userpass_flag = 1;
- break;
- } else if ( '/' == *tmpstr ) {
- /* End of <host>:<port> specification */
- userpass_flag = 0;
- break;
- }
+ while ( '\0' != *tmpstr && '@' != *tmpstr ) {
tmpstr++;
}
- /* User and password specification */
- tmpstr = curstr;
- if ( userpass_flag ) {
+ if ( '@' == *tmpstr ) {
+ /* Has userinfo */
+ /* First check if there's a password (look for : before @) */
+ const char *colon = curstr;
+ int has_password = 0;
+ while (colon < tmpstr) {
+ if (':' == *colon) {
+ has_password = 1;
+ break;
+ }
+ colon++;
+ }
+
/* Read username */
- while ( '\0' != *tmpstr && ':' != *tmpstr && '@' != *tmpstr ) {
- tmpstr++;
+ const char *username_start = curstr;
+ if (has_password) {
+ len = colon - curstr;
+ } else {
+ len = tmpstr - curstr;
}
- len = tmpstr - curstr;
- purl->username = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->username ) {
- parsed_url_free(purl);
- return NULL;
+ if ( len > 0 ) {
+ purl->username = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->username ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->username, username_start, len);
+ purl->username[len] = '\0';
}
- (void)strncpy(purl->username, curstr, len);
- purl->username[len] = '\0';
- /* Proceed current pointer */
- curstr = tmpstr;
- if ( ':' == *curstr ) {
- /* Skip ':' */
- curstr++;
+
+ /* Skip to password or @ */
+ if (has_password) {
+ curstr = colon + 1;
/* Read password */
tmpstr = curstr;
while ( '\0' != *tmpstr && '@' != *tmpstr ) {
tmpstr++;
}
len = tmpstr - curstr;
- purl->password = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->password ) {
- parsed_url_free(purl);
- return NULL;
+ if ( len > 0 ) {
+ purl->password = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->password ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->password, curstr, len);
+ purl->password[len] = '\0';
}
- (void)strncpy(purl->password, curstr, len);
- purl->password[len] = '\0';
curstr = tmpstr;
+ } else {
+ /* No password - advance past username to @ */
+ curstr = username_start + len;
}
- /* Skip '@' */
- if ( '@' != *curstr ) {
- parsed_url_free(purl);
- return NULL;
+ /* Skip @ */
+ while ( '@' == *curstr ) {
+ curstr++;
}
- curstr++;
}
- if ( '[' == *curstr ) {
- bracket_flag = 1;
- } else {
- bracket_flag = 0;
- }
- /* Proceed on by delimiters with reading host */
- tmpstr = curstr;
- while ( '\0' != *tmpstr ) {
- if ( bracket_flag && ']' == *tmpstr ) {
- /* End of IPv6 address. */
- tmpstr++;
- break;
- } else if ( !bracket_flag && (':' == *tmpstr || '/' == *tmpstr) ) {
- /* Port number is specified. */
- break;
+ /*
+ * If NOT a path scheme, detect and consume host:port
+ */
+ if ( !is_path ) {
+ if ( '[' == *curstr ) {
+ bracket_flag = 1;
+ curstr++;
+ } else {
+ bracket_flag = 0;
}
- tmpstr++;
- }
- len = tmpstr - curstr;
- purl->host = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->host || len <= 0 ) {
- parsed_url_free(purl);
- return NULL;
- }
- (void)strncpy(purl->host, curstr, len);
- purl->host[len] = '\0';
- curstr = tmpstr;
- /* Is port number specified? */
- if ( ':' == *curstr ) {
- curstr++;
- /* Read port number */
+ /* Read host */
tmpstr = curstr;
- while ( '\0' != *tmpstr && '/' != *tmpstr ) {
+ while ( '\0' != *tmpstr ) {
+ if ( bracket_flag && ']' == *tmpstr ) {
+ break;
+ } else if ( !bracket_flag && (':' == *tmpstr || '/' == *tmpstr || '?' == *tmpstr || '#' == *tmpstr) ) {
+ break;
+ }
tmpstr++;
}
len = tmpstr - curstr;
- purl->port = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->port ) {
- parsed_url_free(purl);
- return NULL;
+
+ if ( len > 0 ) {
+ purl->host = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->host ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->host, curstr, len);
+ purl->host[len] = '\0';
}
- (void)strncpy(purl->port, curstr, len);
- purl->port[len] = '\0';
curstr = tmpstr;
+
+ /* Skip ']' if IPv6 */
+ if ( ']' == *curstr ) {
+ curstr++;
+ }
+
+ /* Read port */
+ if ( ':' == *curstr ) {
+ curstr++;
+ tmpstr = curstr;
+ while ( '\0' != *tmpstr && '/' != *tmpstr && '?' != *tmpstr && '#' != *tmpstr ) {
+ tmpstr++;
+ }
+ len = tmpstr - curstr;
+ if ( len > 0 ) {
+ purl->port = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->port ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->port, curstr, len);
+ purl->port[len] = '\0';
+ }
+ curstr = tmpstr;
+ }
}
- /* End of the string */
+ /* End of string? */
if ( '\0' == *curstr ) {
return purl;
}
- /* Skip '/' */
- if ( '/' != *curstr ) {
- parsed_url_free(purl);
- return NULL;
- }
- curstr++;
-
/* Parse path */
tmpstr = curstr;
- while ( '\0' != *tmpstr && '#' != *tmpstr && '?' != *tmpstr ) {
+ while ( '\0' != *tmpstr && '?' != *tmpstr && '#' != *tmpstr ) {
tmpstr++;
}
len = tmpstr - curstr;
- purl->path = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->path ) {
- parsed_url_free(purl);
- return NULL;
+ if ( len > 0 ) {
+ purl->path = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->path ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->path, curstr, len);
+ purl->path[len] = '\0';
}
- (void)strncpy(purl->path, curstr, len);
- purl->path[len] = '\0';
curstr = tmpstr;
- /* Is query specified? */
+ /* Parse query */
if ( '?' == *curstr ) {
- /* Skip '?' */
curstr++;
- /* Read query */
tmpstr = curstr;
while ( '\0' != *tmpstr && '#' != *tmpstr ) {
tmpstr++;
}
len = tmpstr - curstr;
- purl->query = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->query ) {
- parsed_url_free(purl);
- return NULL;
+ if ( len > 0 ) {
+ purl->query = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->query ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->query, curstr, len);
+ purl->query[len] = '\0';
}
- (void)strncpy(purl->query, curstr, len);
- purl->query[len] = '\0';
curstr = tmpstr;
}
- /* Is fragment specified? */
+ /* Parse fragment */
if ( '#' == *curstr ) {
- /* Skip '#' */
curstr++;
- /* Read fragment */
tmpstr = curstr;
while ( '\0' != *tmpstr ) {
tmpstr++;
}
len = tmpstr - curstr;
- purl->fragment = malloc(sizeof(char) * (len + 1));
- if ( NULL == purl->fragment ) {
- parsed_url_free(purl);
- return NULL;
+ if ( len > 0 ) {
+ purl->fragment = malloc(sizeof(char) * (len + 1));
+ if ( NULL == purl->fragment ) {
+ parsed_url_free(purl);
+ return NULL;
+ }
+ (void)strncpy(purl->fragment, curstr, len);
+ purl->fragment[len] = '\0';
}
- (void)strncpy(purl->fragment, curstr, len);
- purl->fragment[len] = '\0';
- curstr = tmpstr;
}
return purl;
@@ -286,7 +368,9 @@ parse_url(const char *url)
/*
* Free memory of parsed url
*/
-void parsed_url_free(struct parsed_url *purl) {
+void
+parsed_url_free(struct parsed_url *purl)
+{
if ( NULL != purl ) {
if ( NULL != purl->scheme ) {
free(purl->scheme);
diff --git a/src/url-parser.h b/src/url-parser.h
@@ -1,8 +1,38 @@
/*_
- * Copyright 2010 Scyphus Solutions Co. Ltd. All rights reserved.
+ * Copyright (c) 2026 finwo
*
- * Authors:
- * Hirochika Asai
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to use, copy,
+ * modify, and distribute the Software, subject to the following conditions:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions, and the following disclaimer.
+ *
+ * 2. Redistributions in binary form, or any public offering of the Software
+ * (including hosted or managed services), must reproduce the above copyright
+ * notice, this list of conditions, and the following disclaimer in the
+ * documentation and/or other materials provided.
+ *
+ * 3. Any redistribution or public offering of the Software must clearly attribute
+ * the Software to the original copyright holder, reference this License, and
+ * include a link to the official project repository or website.
+ *
+ * 4. The Software may not be renamed, rebranded, or marketed in a manner that
+ * implies it is an independent or proprietary product. Derivative works must
+ * clearly state that they are based on the Software.
+ *
+ * 5. Modifications to copies of the Software must carry prominent notices stating
+ * that changes were made, the nature of the modifications, and the date of the
+ * modifications.
+ *
+ * Any violation of these conditions terminates the permissions granted herein.
+ *
+ * THIS 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 COPYRIGHT
+ * HOLDER OR CONTRIBUTORS 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.
*/
#ifndef _URL_PARSER_H
diff --git a/test/Makefile b/test/Makefile
@@ -0,0 +1,28 @@
+CC = gcc
+CFLAGS = -Wall -Wextra -I../src -Itest
+SRC = ../src/url-parser.c
+
+TESTS = $(wildcard *.test.c)
+BINS = $(TESTS:.test.c=)
+
+all: $(BINS)
+
+%: %.test.c $(SRC)
+ $(CC) $(CFLAGS) -o $@ $< $(SRC)
+
+run: all
+ @failed=0; \
+ for test in $(BINS); do \
+ echo "Running $$test..."; \
+ ./$$test || failed=1; \
+ done; \
+ if [ $$failed -eq 1 ]; then \
+ echo "Some tests failed!"; \
+ exit 1; \
+ fi
+ @echo "All tests passed!"
+
+clean:
+ rm -f $(BINS)
+
+.PHONY: all run clean
diff --git a/test/basic.test.c b/test/basic.test.c
@@ -0,0 +1,103 @@
+#include "url-parser.h"
+#include "test.h"
+
+void test_http_url() {
+ struct parsed_url *purl = parse_url("http://www.example.com/path/to/resource?query=value#fragment");
+ ASSERT("http scheme", purl != NULL);
+ ASSERT_STRING_EQUALS("http", purl->scheme);
+ ASSERT_STRING_EQUALS("www.example.com", purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/path/to/resource", purl->path);
+ ASSERT_STRING_EQUALS("query=value", purl->query);
+ ASSERT_STRING_EQUALS("fragment", purl->fragment);
+ ASSERT_STRING_EQUALS(NULL, purl->username);
+ ASSERT_STRING_EQUALS(NULL, purl->password);
+ parsed_url_free(purl);
+}
+
+void test_https_with_port() {
+ struct parsed_url *purl = parse_url("https://www.example.com:8080/path");
+ ASSERT("https with port", purl != NULL);
+ ASSERT_STRING_EQUALS("https", purl->scheme);
+ ASSERT_STRING_EQUALS("www.example.com", purl->host);
+ ASSERT_STRING_EQUALS("8080", purl->port);
+ ASSERT_STRING_EQUALS("/path", purl->path);
+ parsed_url_free(purl);
+}
+
+void test_ftp_with_credentials() {
+ struct parsed_url *purl = parse_url("ftp://user:password@ftp.example.com/file.txt");
+ ASSERT("ftp with credentials", purl != NULL);
+ ASSERT_STRING_EQUALS("ftp", purl->scheme);
+ ASSERT_STRING_EQUALS("ftp.example.com", purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/file.txt", purl->path);
+ ASSERT_STRING_EQUALS("user", purl->username);
+ ASSERT_STRING_EQUALS("password", purl->password);
+ parsed_url_free(purl);
+}
+
+void test_http_ipv6() {
+ struct parsed_url *purl = parse_url("http://[::1]:8080/path");
+ ASSERT("http IPv6", purl != NULL);
+ ASSERT_STRING_EQUALS("http", purl->scheme);
+ ASSERT_STRING_EQUALS("::1", purl->host);
+ ASSERT_STRING_EQUALS("8080", purl->port);
+ ASSERT_STRING_EQUALS("/path", purl->path);
+ parsed_url_free(purl);
+}
+
+void test_query_only() {
+ struct parsed_url *purl = parse_url("http://example.com?foo=bar");
+ ASSERT("query only", purl != NULL);
+ ASSERT_STRING_EQUALS("http", purl->scheme);
+ ASSERT_STRING_EQUALS("example.com", purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS(NULL, purl->path);
+ ASSERT_STRING_EQUALS("foo=bar", purl->query);
+ parsed_url_free(purl);
+}
+
+void test_fragment_only() {
+ struct parsed_url *purl = parse_url("http://example.com#section");
+ ASSERT("fragment only", purl != NULL);
+ ASSERT_STRING_EQUALS("http", purl->scheme);
+ ASSERT_STRING_EQUALS("example.com", purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS(NULL, purl->path);
+ ASSERT_STRING_EQUALS(NULL, purl->query);
+ ASSERT_STRING_EQUALS("section", purl->fragment);
+ parsed_url_free(purl);
+}
+
+void test_no_path() {
+ struct parsed_url *purl = parse_url("http://www.example.com");
+ ASSERT("no path", purl != NULL);
+ ASSERT_STRING_EQUALS("http", purl->scheme);
+ ASSERT_STRING_EQUALS("www.example.com", purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS(NULL, purl->path);
+ parsed_url_free(purl);
+}
+
+void test_username_only() {
+ struct parsed_url *purl = parse_url("ftp://user@ftp.example.com/file");
+ ASSERT("username only", purl != NULL);
+ ASSERT_STRING_EQUALS("ftp", purl->scheme);
+ ASSERT_STRING_EQUALS("ftp.example.com", purl->host);
+ ASSERT_STRING_EQUALS("user", purl->username);
+ ASSERT_STRING_EQUALS(NULL, purl->password);
+ parsed_url_free(purl);
+}
+
+int main() {
+ RUN(test_http_url);
+ RUN(test_https_with_port);
+ RUN(test_ftp_with_credentials);
+ RUN(test_http_ipv6);
+ RUN(test_query_only);
+ RUN(test_fragment_only);
+ RUN(test_no_path);
+ RUN(test_username_only);
+ return TEST_REPORT();
+}
diff --git a/test/empty-host.test.c b/test/empty-host.test.c
@@ -0,0 +1,57 @@
+#include "url-parser.h"
+#include "test.h"
+
+void test_tcp_empty_host_with_port() {
+ struct parsed_url *purl = parse_url("tcp://:6379");
+ ASSERT("tcp empty host with port", purl != NULL);
+ ASSERT_STRING_EQUALS("tcp", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_STRING_EQUALS("6379", purl->port);
+ ASSERT_STRING_EQUALS(NULL, purl->path);
+ parsed_url_free(purl);
+}
+
+void test_redis_empty_host_with_port() {
+ struct parsed_url *purl = parse_url("redis://:6379");
+ ASSERT("redis empty host with port", purl != NULL);
+ ASSERT_STRING_EQUALS("redis", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_STRING_EQUALS("6379", purl->port);
+ parsed_url_free(purl);
+}
+
+void test_http_empty_host_with_port() {
+ struct parsed_url *purl = parse_url("http://:8080");
+ ASSERT("http empty host with port", purl != NULL);
+ ASSERT_STRING_EQUALS("http", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_STRING_EQUALS("8080", purl->port);
+ parsed_url_free(purl);
+}
+
+void test_empty_host_no_port() {
+ struct parsed_url *purl = parse_url("tcp://:");
+ ASSERT("tcp empty host no port", purl != NULL);
+ ASSERT_STRING_EQUALS("tcp", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ parsed_url_free(purl);
+}
+
+void test_empty_host_only_slashes() {
+ struct parsed_url *purl = parse_url("tcp://");
+ ASSERT("tcp only slashes", purl != NULL);
+ ASSERT_STRING_EQUALS("tcp", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_STRING_EQUALS(NULL, purl->port);
+ parsed_url_free(purl);
+}
+
+int main() {
+ RUN(test_tcp_empty_host_with_port);
+ RUN(test_redis_empty_host_with_port);
+ RUN(test_http_empty_host_with_port);
+ RUN(test_empty_host_no_port);
+ RUN(test_empty_host_only_slashes);
+ return TEST_REPORT();
+}
diff --git a/test/test.h b/test/test.h
@@ -0,0 +1,236 @@
+/// assert.h
+/// ========
+///
+/// Single-file unit-testing library for C
+///
+/// Features
+/// --------
+///
+/// - Single header file, no other library dependencies
+/// - Simple ANSI C. The library should work with virtually every C(++) compiler on
+/// virtually any playform
+/// - Reporting of assertion failures, including the expression and location of the
+/// failure
+/// - Stops test on first failed assertion
+/// - ANSI color output for maximum visibility
+/// - Easily embeddable in applications for runtime tests or separate testing
+/// applications
+///
+/// Todo
+/// ----
+///
+/// - Disable assertions on definition, to allow production build without source modifications
+///
+/// Example Usage
+/// -------------
+///
+/// ```C
+/// #include "finwo/assert.h"
+/// #include "mylib.h"
+///
+/// void test_sheep() {
+/// ASSERT("Sheep are cool", are_sheep_cool());
+/// ASSERT_EQUALS(4, sheep.legs);
+/// }
+///
+/// void test_cheese() {
+/// ASSERT("Cheese is tangy", cheese.tanginess > 0);
+/// ASSERT_STRING_EQUALS("Wensleydale", cheese.name);
+/// }
+///
+/// int main() {
+/// RUN(test_sheep);
+/// RUN(test_cheese);
+/// return TEST_REPORT();
+/// }
+/// ```
+///
+/// To run the tests, compile the tests as a binary and run it.
+
+#ifndef __TINYTEST_INCLUDED_H__
+#define __TINYTEST_INCLUDED_H__
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+///
+/// API
+/// ---
+///
+
+///
+/// ### Macros
+///
+
+
+/// <details>
+/// <summary>ASSERT(msg, expression)</summary>
+///
+/// Perform an assertion
+///<C
+#define ASSERT(msg, expression) if (!tap_assert(__FILE__, __LINE__, (msg), (#expression), (expression) ? 1 : 0)) return
+///>
+/// </details>
+
+
+/// <details>
+/// <summary>ASSERT_EQUALS(expected, actual)</summary>
+///
+/// Perform an equal assertion
+///<C
+/* Convenient assertion methods */
+/* TODO: Generate readable error messages for assert_equals or assert_str_equals */
+#define ASSERT_EQUALS(expected, actual) ASSERT((#actual), (expected) == (actual))
+///>
+/// </details>
+
+/// <details>
+/// <summary>ASSERT_STRING_EQUALS(expected, actual)</summary>
+///
+/// Perform an equal string assertion
+///<
+#define ASSERT_STRING_EQUALS(expected, actual) ASSERT((#actual), ( \
+ ((expected) == NULL && (actual) == NULL) || \
+ ((expected) != NULL && (actual) != NULL && strcmp((expected),(actual)) == 0) \
+))
+///>
+/// </details>
+
+/// <details>
+/// <summary>RUN(fn)</summary>
+///
+/// Run a test suite/function containing assertions
+///<C
+#define RUN(test_function) tap_execute((#test_function), (test_function))
+///>
+/// </details>
+
+/// <details>
+/// <summary>TEST_REPORT()</summary>
+///
+/// Report on the tests that have been run
+///<C
+#define TEST_REPORT() tap_report()
+///>
+/// </details>
+
+///
+/// Extras
+/// ------
+///
+/// ### Disable color
+///
+/// If you want to disable color during the assertions, because you want to
+/// interpret the output for example (it is "tap" format after all), you can
+/// define `NO_COLOR` during compilation to disable color output.
+///
+/// ```sh
+/// cc -D NO_COLOR source.c -o test
+/// ```
+///
+/// ### Silent assertions
+///
+/// You can also fully disable output for assertions by defining the
+/// `ASSERT_SILENT` macro. This will fully disable the printf performed after
+/// the assertion is performed.
+///
+/// ```sh
+/// cc -D ASSERT_SILENT source.c -o test
+/// ```
+///
+/// ### Silent reporting
+///
+/// If you do not want the report to be displayed at the end, you can define the
+/// `REPORT_SILENT` macro. This will disable the printf during reporting and
+/// only keep the return code.
+///
+/// ```sh
+/// cc -D REPORT_SILENT source.c -o test
+/// ```
+///
+
+#ifdef NO_COLOR
+#define TAP_COLOR_CODE ""
+#define TAP_COLOR_RED ""
+#define TAP_COLOR_GREEN ""
+#define TAP_COLOR_RESET ""
+#else
+#define TAP_COLOR_CODE "\x1B"
+#define TAP_COLOR_RED "[1;31m"
+#define TAP_COLOR_GREEN "[1;32m"
+#define TAP_COLOR_RESET "[0m"
+#endif
+
+int tap_asserts = 0;
+int tap_passes = 0;
+int tap_fails = 0;
+const char *tap_current_name = NULL;
+
+void tap_execute(const char* name, void (*test_function)()) {
+ tap_current_name = name;
+ printf("# %s\n", name);
+ test_function();
+}
+
+int tap_assert(const char* file, int line, const char* msg, const char* expression, int pass) {
+ tap_asserts++;
+
+ if (pass) {
+ tap_passes++;
+#ifndef ASSERT_SILENT
+ printf("%s%sok%s%s %d - %s\n",
+ TAP_COLOR_CODE, TAP_COLOR_GREEN,
+ TAP_COLOR_CODE, TAP_COLOR_RESET,
+ tap_asserts,
+ msg
+ );
+#endif
+ } else {
+ tap_fails++;
+#ifndef ASSERT_SILENT
+ printf(
+ "%s%snot ok%s%s %d - %s\n"
+ " On %s:%d, in test %s()\n"
+ " %s\n"
+ ,
+ TAP_COLOR_CODE, TAP_COLOR_RED,
+ TAP_COLOR_CODE, TAP_COLOR_RESET,
+ tap_asserts, msg,
+ file, line, tap_current_name,
+ expression
+ );
+ }
+#endif
+ return pass;
+}
+
+int tap_report(void) {
+#ifndef REPORT_SILENT
+ printf(
+ "1..%d\n"
+ "# tests %d\n"
+ "# pass %d\n"
+ "# fail %d\n",
+ tap_asserts,
+ tap_asserts,
+ tap_passes,
+ tap_fails
+ );
+#endif
+ return tap_fails ? 2 : 0;
+}
+
+#endif // __TINYTEST_INCLUDED_H__
+
+///
+/// Credits
+/// -------
+///
+/// This library was heavily based on the [tinytest][tinytest] library by
+/// [Joe Walnes][joewalnes]. A license reference to his library could not be
+/// found, which is why this reference is in this file. Should I be contacted
+/// about licensing issues, I'll investigate further.
+///
+/// [joewalnes]: https://github.com/joewalnes
+/// [tinytest]: https://github.com/joewalnes/tinytest
diff --git a/test/unix-socket.test.c b/test/unix-socket.test.c
@@ -0,0 +1,128 @@
+#include "url-parser.h"
+#include "test.h"
+
+void test_unix_triple_slash() {
+ struct parsed_url *purl = parse_url("unix:///path/to/socket");
+ ASSERT("unix triple slash", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/path/to/socket", purl->path);
+ ASSERT_EQUALS(NULL, purl->username);
+ ASSERT_EQUALS(NULL, purl->password);
+ parsed_url_free(purl);
+}
+
+void test_unix_triple_slash_with_credentials() {
+ struct parsed_url *purl = parse_url("unix://user:pass@/path/to/socket");
+ ASSERT("unix with credentials", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/path/to/socket", purl->path);
+ ASSERT_STRING_EQUALS("user", purl->username);
+ ASSERT_STRING_EQUALS("pass", purl->password);
+ parsed_url_free(purl);
+}
+
+void test_unix_single_slash() {
+ struct parsed_url *purl = parse_url("unix:/path/to/socket");
+ ASSERT("unix single slash", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/path/to/socket", purl->path);
+ ASSERT_EQUALS(NULL, purl->username);
+ ASSERT_EQUALS(NULL, purl->password);
+ parsed_url_free(purl);
+}
+
+void test_unix_no_slash() {
+ struct parsed_url *purl = parse_url("unix:filename");
+ ASSERT("unix no slash", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("filename", purl->path);
+ ASSERT_EQUALS(NULL, purl->username);
+ ASSERT_EQUALS(NULL, purl->password);
+ parsed_url_free(purl);
+}
+
+void test_unix_double_slash_no_host() {
+ struct parsed_url *purl = parse_url("unix://path/to/socket");
+ ASSERT("unix double slash no host", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("path/to/socket", purl->path);
+ ASSERT_EQUALS(NULL, purl->username);
+ ASSERT_EQUALS(NULL, purl->password);
+ parsed_url_free(purl);
+}
+
+void test_unix_double_slash_with_credentials_no_leading_slash() {
+ struct parsed_url *purl = parse_url("unix://user:pass@path/to/socket");
+ ASSERT("unix double slash with credentials no leading slash", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("path/to/socket", purl->path);
+ ASSERT_STRING_EQUALS("user", purl->username);
+ ASSERT_STRING_EQUALS("pass", purl->password);
+ parsed_url_free(purl);
+}
+
+void test_redis_unix_socket() {
+ struct parsed_url *purl = parse_url("redis:///var/run/redis.sock");
+ ASSERT("redis unix socket", purl != NULL);
+ ASSERT_STRING_EQUALS("redis", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/var/run/redis.sock", purl->path);
+ parsed_url_free(purl);
+}
+
+void test_postgres_unix_socket() {
+ struct parsed_url *purl = parse_url("postgres:///var/run/postgres.sock");
+ ASSERT("postgres unix socket", purl != NULL);
+ ASSERT_STRING_EQUALS("postgres", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/var/run/postgres.sock", purl->path);
+ parsed_url_free(purl);
+}
+
+void test_unix_socket_with_relative_path() {
+ struct parsed_url *purl = parse_url("unix:redis.sock");
+ ASSERT("unix relative path", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_STRING_EQUALS("redis.sock", purl->path);
+ parsed_url_free(purl);
+}
+
+void test_unix_socket_credentials_with_port() {
+ struct parsed_url *purl = parse_url("unix://user:pass@/path/to/sock:8080");
+ ASSERT("unix with credentials and port", purl != NULL);
+ ASSERT_STRING_EQUALS("unix", purl->scheme);
+ ASSERT_EQUALS(NULL, purl->host);
+ ASSERT_EQUALS(NULL, purl->port);
+ ASSERT_STRING_EQUALS("/path/to/sock:8080", purl->path);
+ ASSERT_STRING_EQUALS("user", purl->username);
+ ASSERT_STRING_EQUALS("pass", purl->password);
+ parsed_url_free(purl);
+}
+
+int main() {
+ RUN(test_unix_triple_slash);
+ RUN(test_unix_triple_slash_with_credentials);
+ RUN(test_unix_single_slash);
+ RUN(test_unix_no_slash);
+ RUN(test_unix_double_slash_no_host);
+ RUN(test_unix_double_slash_with_credentials_no_leading_slash);
+ RUN(test_redis_unix_socket);
+ RUN(test_postgres_unix_socket);
+ RUN(test_unix_socket_with_relative_path);
+ RUN(test_unix_socket_credentials_with_port);
+ return TEST_REPORT();
+}