url-parser.c

URL parsing library
git clone git://git.finwo.net/lib/url-parser.c
Log | Files | Refs | README | LICENSE

commit e8b86f0d1557b265a856c14dee2ec90610469da5
parent bc4c4716d153dfc31e3ace78cb37a31efa87dd60
Author: finwo <finwo@pm.me>
Date:   Tue,  3 Mar 2026 12:48:14 +0100

Lib rewrite

Diffstat:
A.github/workflows/test.yml | 22++++++++++++++++++++++
A.gitignore | 6++++++
ALICENSE.md | 34++++++++++++++++++++++++++++++++++
AREADME.md | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/url-parser.c | 334+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/url-parser.h | 36+++++++++++++++++++++++++++++++++---
Atest/Makefile | 28++++++++++++++++++++++++++++
Atest/basic.test.c | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/empty-host.test.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/test.h | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/unix-socket.test.c | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); +}