commit 21f00bea954a015ede9cfffd78c0c6fb7337dc10
Author: Robin Bron <robin.bron@yourhosting.nl>
Date: Mon, 16 Mar 2026 19:17:42 +0100
Project init
Diffstat:
15 files changed, 1532 insertions(+), 0 deletions(-)
diff --git a/.clang-format b/.clang-format
@@ -0,0 +1,334 @@
+---
+Language: Cpp
+AccessModifierOffset: -1
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: true
+AlignConsecutiveBitFields:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveDeclarations:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: true
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveMacros:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveShortCaseStatements:
+ Enabled: true
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCaseArrows: false
+ AlignCaseColons: false
+AlignConsecutiveTableGenBreakingDAGArgColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenCondOperatorColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenDefinitionColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignEscapedNewlines: Left
+AlignOperands: Align
+AlignTrailingComments:
+ Kind: Always
+ OverEmptyLines: 0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseExpressionOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: true
+AllowShortNamespacesOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AttributeMacros:
+ - __capability
+ - absl_nonnull
+ - absl_nullable
+ - absl_nullability_unknown
+BinPackArguments: true
+BinPackLongBracedList: true
+BinPackParameters: BinPack
+BitFieldColonSpacing: Both
+BracedInitializerIndentWidth: -1
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: Never
+ AfterEnum: false
+ AfterExternBlock: false
+ AfterFunction: true
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakAfterReturnType: None
+BreakArrays: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTemplateCloser: false
+BreakBeforeTernaryOperators: true
+BreakBinaryOperations: Never
+BreakConstructorInitializers: BeforeColon
+BreakFunctionDefinitionParameters: false
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+BreakTemplateDeclarations: Yes
+ColumnLimit: 120
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat: false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+EnumTrailingComma: Leave
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IfMacros:
+ - KJ_IF_MAYBE
+IncludeBlocks: Regroup
+IncludeCategories:
+ - Regex: '^<ext/.*\.h>'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^<.*\.h>'
+ Priority: 1
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^<.*'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '.*'
+ Priority: 3
+ SortPriority: 0
+ CaseSensitive: false
+IncludeIsMainRegex: '([-_](test|unittest))?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: true
+IndentCaseLabels: true
+IndentExportBlock: true
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: true
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+InsertBraces: false
+InsertNewlineAtEOF: true
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+ Binary: 0
+ BinaryMinDigits: 0
+ Decimal: 0
+ DecimalMinDigits: 0
+ Hex: 0
+ HexMinDigits: 0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLines:
+ AtEndOfFile: false
+ AtStartOfBlock: false
+ AtStartOfFile: false
+KeepFormFeed: false
+LambdaBodyIndentation: Signature
+LineEnding: DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MainIncludeChar: Quote
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Never
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+OneLineFormatOffRegex: ''
+PackConstructorInitializers: NextLine
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakBeforeMemberAccess: 150
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Right
+PPIndentWidth: -1
+QualifierAlignment: Leave
+RawStringFormats:
+ - Language: Cpp
+ Delimiters:
+ - cc
+ - CC
+ - cpp
+ - Cpp
+ - CPP
+ - 'c++'
+ - 'C++'
+ CanonicalDelimiter: ''
+ BasedOnStyle: google
+ - Language: TextProto
+ Delimiters:
+ - pb
+ - PB
+ - proto
+ - PROTO
+ EnclosingFunctions:
+ - EqualsProto
+ - EquivToProto
+ - PARSE_PARTIAL_TEXT_PROTO
+ - PARSE_TEST_PROTO
+ - PARSE_TEXT_PROTO
+ - ParseTextOrDie
+ - ParseTextProtoOrDie
+ - ParseTestProto
+ - ParsePartialTestProto
+ CanonicalDelimiter: pb
+ BasedOnStyle: google
+ReferenceAlignment: Pointer
+ReflowComments: Always
+RemoveBracesLLVM: false
+RemoveEmptyLinesInUnwrappedLines: false
+RemoveParentheses: Leave
+RemoveSemicolon: false
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes:
+ Enabled: true
+ IgnoreCase: false
+SortJavaStaticImport: Before
+SortUsingDeclarations: LexicographicNumeric
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterOperatorKeyword: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: ControlStatements
+SpaceBeforeParensOptions:
+ AfterControlStatements: true
+ AfterForeachMacros: true
+ AfterFunctionDefinitionName: false
+ AfterFunctionDeclarationName: false
+ AfterIfMacros: true
+ AfterNot: false
+ AfterOverloadedOperator: false
+ AfterPlacementOperator: true
+ AfterRequiresInClause: false
+ AfterRequiresInExpression: false
+ BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 2
+SpacesInAngles: Never
+SpacesInContainerLiterals: true
+SpacesInLineCommentPrefix:
+ Minimum: 1
+ Maximum: -1
+SpacesInParens: Never
+SpacesInParensOptions:
+ ExceptDoubleParentheses: false
+ InCStyleCasts: false
+ InConditionalStatements: false
+ InEmptyParentheses: false
+ Other: false
+SpacesInSquareBrackets: false
+Standard: Auto
+StatementAttributeLikeMacros:
+ - Q_EMIT
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TableGenBreakInsideDAGArg: DontBreak
+TabWidth: 8
+UseTab: Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+ - BOOST_PP_STRINGIZE
+ - CF_SWIFT_NAME
+ - NS_SWIFT_NAME
+ - PP_STRINGIZE
+ - STRINGIZE
+WrapNamespaceBodyWithEmptyLines: Leave
+...
+
diff --git a/.dep.export b/.dep.export
@@ -0,0 +1 @@
+src/socket-util.h
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,5 @@
+*.o
+/test/sockaddr_equal
+/test/merge_fd_arrays
+/test/sockaddr_to_string
+/test/string_to_sockaddr
diff --git a/LICENSE.md b/LICENSE.md
@@ -0,0 +1,39 @@
+Copyright (c) 2026 finwo
+
+<!-- paragraph -->
+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:
+
+<!-- list:start -->
+ 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.
+<!-- list:end -->
+
+<!-- paragraph -->
+Any violation of these conditions terminates the permissions granted herein.
+
+<!-- paragraph -->
+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/Makefile b/Makefile
@@ -0,0 +1,9 @@
+FIND=$(shell which gfind find | head -1)
+
+.PHONY: test
+test:
+ $(MAKE) -C test run
+
+.PHONY: format
+format:
+ $(FIND) src/ -type f \( -name '*.c' -o -name '*.h' \) -exec clang-format -i {} +
diff --git a/config.mk b/config.mk
@@ -0,0 +1 @@
+SRC+={{module.dirname}}/src/socket-util.c
diff --git a/src/socket-util.c b/src/socket-util.c
@@ -0,0 +1,520 @@
+#include "socket-util.h"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "rxi/log.h"
+
+int set_socket_nonblocking(int fd, int nonblock) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ if (flags < 0) return -1;
+ if (nonblock)
+ flags |= O_NONBLOCK;
+ else
+ flags &= ~O_NONBLOCK;
+ return fcntl(fd, F_SETFL, flags) == 0 ? 0 : -1;
+}
+
+int *tcp_listen(const char *addr, const char *default_host, const char *default_port) {
+ char host[256] = "";
+ char port[32] = "";
+
+ if (default_host && default_host[0]) {
+ snprintf(host, sizeof(host), "%s", default_host);
+ }
+ if (default_port && default_port[0]) {
+ snprintf(port, sizeof(port), "%s", default_port);
+ }
+
+ if (!addr || !addr[0]) {
+ if (!host[0] || !port[0]) {
+ log_error("tcp_listen: empty address and no defaults");
+ return NULL;
+ }
+ } else if (addr[0] == '[') {
+ const char *close_bracket = strchr(addr, ']');
+ if (!close_bracket) {
+ log_error("tcp_listen: invalid IPv6 format: missing ']'");
+ return NULL;
+ }
+ size_t hlen = (size_t)(close_bracket - addr - 1);
+ if (hlen > 0) {
+ if (hlen >= sizeof(host)) hlen = sizeof(host) - 1;
+ memcpy(host, addr + 1, hlen);
+ host[hlen] = '\0';
+ }
+ const char *colon = close_bracket + 1;
+ if (*colon == ':') {
+ snprintf(port, sizeof(port), "%s", colon + 1);
+ } else if (*colon != '\0') {
+ log_error("tcp_listen: invalid IPv6 format: expected ':' after ']'");
+ return NULL;
+ }
+ } else {
+ int leading_colon = (addr[0] == ':');
+ const char *p = leading_colon ? addr + 1 : addr;
+ int is_port_only = 1;
+ for (const char *q = p; *q; q++) {
+ if (*q < '0' || *q > '9') {
+ is_port_only = 0;
+ break;
+ }
+ }
+
+ const char *colon = strrchr(addr, ':');
+ if (leading_colon && is_port_only) {
+ snprintf(port, sizeof(port), "%s", p);
+ } else if (is_port_only) {
+ if (default_host && default_host[0]) {
+ snprintf(host, sizeof(host), "%s", default_host);
+ }
+ snprintf(port, sizeof(port), "%s", p);
+ } else if (colon) {
+ size_t hlen = (size_t)(colon - addr);
+ if (hlen > 0) {
+ if (hlen >= sizeof(host)) hlen = sizeof(host) - 1;
+ memcpy(host, addr, hlen);
+ host[hlen] = '\0';
+ }
+ snprintf(port, sizeof(port), "%s", colon + 1);
+ } else {
+ snprintf(host, sizeof(host), "%s", addr);
+ }
+ }
+
+ if (!port[0]) {
+ log_error("tcp_listen: no port specified");
+ return NULL;
+ }
+
+ struct addrinfo hints, *res = NULL;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+ if (getaddrinfo(host[0] ? host : NULL, port, &hints, &res) != 0 || !res) {
+ log_error("tcp_listen: getaddrinfo failed for %s:%s", host, port);
+ return NULL;
+ }
+
+ int *fds = malloc(sizeof(int) * 3);
+ if (!fds) {
+ freeaddrinfo(res);
+ return NULL;
+ }
+ fds[0] = 0;
+
+ int listen_all = (host[0] == '\0');
+ struct addrinfo *p;
+
+ for (p = res; p; p = p->ai_next) {
+ if (p->ai_family == AF_INET) {
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) continue;
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ if (bind(fd, p->ai_addr, p->ai_addrlen) == 0 && listen(fd, 8) == 0) {
+ set_socket_nonblocking(fd, 1);
+ fds[++fds[0]] = fd;
+ } else {
+ close(fd);
+ }
+ } else if (p->ai_family == AF_INET6 && listen_all) {
+ int fd = socket(AF_INET6, SOCK_STREAM, 0);
+ if (fd < 0) continue;
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
+ if (bind(fd, p->ai_addr, p->ai_addrlen) == 0 && listen(fd, 8) == 0) {
+ set_socket_nonblocking(fd, 1);
+ fds[++fds[0]] = fd;
+ } else {
+ close(fd);
+ }
+ }
+ }
+
+ freeaddrinfo(res);
+
+ if (fds[0] == 0) {
+ log_error("tcp_listen: failed to bind to %s:%s", host, port);
+ free(fds);
+ return NULL;
+ }
+
+ return fds;
+}
+
+int *udp_recv(const char *addr, const char *default_host, const char *default_port) {
+ char host[256] = "";
+ char port[32] = "";
+
+ if (default_host && default_host[0]) {
+ snprintf(host, sizeof(host), "%s", default_host);
+ }
+ if (default_port && default_port[0]) {
+ snprintf(port, sizeof(port), "%s", default_port);
+ }
+
+ if (!addr || !addr[0]) {
+ if (!host[0] || !port[0]) {
+ log_error("udp_recv: empty address and no defaults");
+ return NULL;
+ }
+ } else if (addr[0] == '[') {
+ const char *close_bracket = strchr(addr, ']');
+ if (!close_bracket) {
+ log_error("udp_recv: invalid IPv6 format: missing ']'");
+ return NULL;
+ }
+ size_t hlen = (size_t)(close_bracket - addr - 1);
+ if (hlen > 0) {
+ if (hlen >= sizeof(host)) hlen = sizeof(host) - 1;
+ memcpy(host, addr + 1, hlen);
+ host[hlen] = '\0';
+ }
+ const char *colon = close_bracket + 1;
+ if (*colon == ':') {
+ snprintf(port, sizeof(port), "%s", colon + 1);
+ } else if (*colon != '\0') {
+ log_error("udp_recv: invalid IPv6 format: expected ':' after ']'");
+ return NULL;
+ }
+ } else {
+ int leading_colon = (addr[0] == ':');
+ const char *p = leading_colon ? addr + 1 : addr;
+ int is_port_only = 1;
+ for (const char *q = p; *q; q++) {
+ if (*q < '0' || *q > '9') {
+ is_port_only = 0;
+ break;
+ }
+ }
+
+ const char *colon = strrchr(addr, ':');
+ if (leading_colon && is_port_only) {
+ snprintf(port, sizeof(port), "%s", p);
+ } else if (is_port_only) {
+ if (default_host && default_host[0]) {
+ snprintf(host, sizeof(host), "%s", default_host);
+ }
+ snprintf(port, sizeof(port), "%s", p);
+ } else if (colon) {
+ size_t hlen = (size_t)(colon - addr);
+ if (hlen > 0) {
+ if (hlen >= sizeof(host)) hlen = sizeof(host) - 1;
+ memcpy(host, addr, hlen);
+ host[hlen] = '\0';
+ }
+ snprintf(port, sizeof(port), "%s", colon + 1);
+ } else {
+ snprintf(host, sizeof(host), "%s", addr);
+ }
+ }
+
+ if (!port[0]) {
+ log_error("udp_recv: no port specified");
+ return NULL;
+ }
+
+ struct addrinfo hints, *res = NULL;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ hints.ai_flags = AI_PASSIVE;
+ if (getaddrinfo(host[0] ? host : NULL, port, &hints, &res) != 0 || !res) {
+ log_error("udp_recv: getaddrinfo failed for %s:%s", host, port);
+ return NULL;
+ }
+
+ int *fds = malloc(sizeof(int) * 3);
+ if (!fds) {
+ freeaddrinfo(res);
+ return NULL;
+ }
+ fds[0] = 0;
+
+ int listen_all = (host[0] == '\0');
+ struct addrinfo *p;
+
+ for (p = res; p; p = p->ai_next) {
+ if (p->ai_family == AF_INET) {
+ int fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) continue;
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ if (bind(fd, p->ai_addr, p->ai_addrlen) == 0) {
+ set_socket_nonblocking(fd, 1);
+ fds[++fds[0]] = fd;
+ } else {
+ close(fd);
+ }
+ } else if (p->ai_family == AF_INET6 && listen_all) {
+ int fd = socket(AF_INET6, SOCK_DGRAM, 0);
+ if (fd < 0) continue;
+ int opt = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+ setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
+ if (bind(fd, p->ai_addr, p->ai_addrlen) == 0) {
+ set_socket_nonblocking(fd, 1);
+ fds[++fds[0]] = fd;
+ } else {
+ close(fd);
+ }
+ }
+ }
+
+ freeaddrinfo(res);
+
+ if (fds[0] == 0) {
+ log_error("udp_recv: failed to bind to %s:%s", host, port);
+ free(fds);
+ return NULL;
+ }
+
+ return fds;
+}
+
+int *unix_listen(const char *path, int sock_type, const char *owner) {
+ if (!path || !path[0]) {
+ log_error("unix_listen: empty path");
+ return NULL;
+ }
+
+ char *path_copy = strdup(path);
+ if (!path_copy) {
+ return NULL;
+ }
+
+ char *dir = strdup(path);
+ if (!dir) {
+ free(path_copy);
+ return NULL;
+ }
+
+ char *last_slash = strrchr(dir, '/');
+ if (last_slash && last_slash != dir) {
+ *last_slash = '\0';
+ if (strlen(dir) > 0) {
+ mkdir(dir, 0755);
+ }
+ } else if (!last_slash) {
+ dir[0] = '.';
+ dir[1] = '\0';
+ }
+ free(dir);
+
+ unlink(path_copy);
+
+ int *fds = malloc(sizeof(int) * 2);
+ if (!fds) {
+ free(path_copy);
+ return NULL;
+ }
+ fds[0] = 0;
+
+ int fd = socket(AF_UNIX, sock_type, 0);
+ if (fd < 0) {
+ log_error("unix_listen: socket failed: %s", strerror(errno));
+ free(path_copy);
+ free(fds);
+ return NULL;
+ }
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, path_copy, sizeof(addr.sun_path) - 1);
+
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ log_error("unix_listen: bind failed: %s", strerror(errno));
+ close(fd);
+ free(path_copy);
+ free(fds);
+ return NULL;
+ }
+
+ if (owner && owner[0]) {
+ uid_t uid = -1;
+ gid_t gid = -1;
+ char *owner_copy = strdup(owner);
+ if (owner_copy) {
+ char *colon = strchr(owner_copy, ':');
+ if (colon) {
+ *colon = '\0';
+ colon++;
+ if (colon[0]) {
+ struct passwd *pw = getpwnam(owner_copy);
+ if (pw) {
+ uid = pw->pw_uid;
+ struct group *gr = getgrnam(colon);
+ if (gr) {
+ gid = gr->gr_gid;
+ }
+ }
+ }
+ } else {
+ struct passwd *pw = getpwnam(owner_copy);
+ if (pw) {
+ uid = pw->pw_uid;
+ gid = pw->pw_gid;
+ }
+ }
+ free(owner_copy);
+ }
+ if (uid != (uid_t)-1 || gid != (gid_t)-1) {
+ if (fchown(fd, uid, gid) < 0) {
+ log_error("unix_listen: fchown failed: %s", strerror(errno));
+ close(fd);
+ unlink(path_copy);
+ free(path_copy);
+ free(fds);
+ return NULL;
+ }
+ }
+ }
+
+ if (sock_type == SOCK_STREAM) {
+ if (listen(fd, 8) < 0) {
+ log_error("unix_listen: listen failed: %s", strerror(errno));
+ close(fd);
+ unlink(path_copy);
+ free(path_copy);
+ free(fds);
+ return NULL;
+ }
+ }
+
+ set_socket_nonblocking(fd, 1);
+
+ fds[++fds[0]] = fd;
+
+ free(path_copy);
+ return fds;
+}
+
+int *merge_fd_arrays(int **arrays, int count) {
+ if (!arrays || count <= 0) {
+ return NULL;
+ }
+
+ int total_count = 0;
+ for (int i = 0; i < count; i++) {
+ if (arrays[i] && arrays[i][0] > 0) {
+ total_count += arrays[i][0];
+ }
+ }
+
+ if (total_count == 0) {
+ for (int i = 0; i < count; i++) {
+ free(arrays[i]);
+ }
+ return NULL;
+ }
+
+ int *merged = malloc(sizeof(int) * (total_count + 1));
+ if (!merged) {
+ for (int i = 0; i < count; i++) {
+ free(arrays[i]);
+ }
+ return NULL;
+ }
+
+ merged[0] = 0;
+ int idx = 1;
+
+ for (int i = 0; i < count; i++) {
+ if (arrays[i] && arrays[i][0] > 0) {
+ for (int j = 1; j <= arrays[i][0]; j++) {
+ merged[idx++] = arrays[i][j];
+ }
+ merged[0] += arrays[i][0];
+ }
+ free(arrays[i]);
+ }
+
+ return merged;
+}
+
+void sockaddr_to_string(const struct sockaddr *addr, char *buf, size_t buf_size) {
+ if (!addr || !buf || buf_size == 0) return;
+
+ if (addr->sa_family == AF_INET) {
+ struct sockaddr_in *sin = (struct sockaddr_in *)addr;
+ inet_ntop(AF_INET, &sin->sin_addr, buf, buf_size);
+ size_t len = strlen(buf);
+ if (buf_size - len > 6) {
+ snprintf(buf + len, buf_size - len, ":%d", ntohs(sin->sin_port));
+ }
+ } else if (addr->sa_family == AF_INET6) {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)addr;
+ buf[0] = '[';
+ inet_ntop(AF_INET6, &sin6->sin6_addr, buf + 1, (socklen_t)(buf_size - 1));
+ size_t len = strlen(buf);
+ if (buf_size - len > 6) {
+ snprintf(buf + len, buf_size - len, "]:%d", ntohs(sin6->sin6_port));
+ }
+ } else {
+ buf[0] = '\0';
+ }
+}
+
+int string_to_sockaddr(const char *str, struct sockaddr_storage *addr) {
+ if (!str || !addr) return -1;
+ memset(addr, 0, sizeof(*addr));
+
+ const char *port_str = strrchr(str, ':');
+ if (!port_str) return -1;
+
+ char host[256];
+ size_t host_len = (size_t)(port_str - str);
+ if (host_len >= sizeof(host)) return -1;
+ memcpy(host, str, host_len);
+ host[host_len] = '\0';
+
+ int port = atoi(port_str + 1);
+ if (port <= 0 || port > 65535) return -1;
+
+ if (host[0] == '[' && host[host_len - 1] == ']') {
+ host[host_len - 1] = '\0';
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)addr;
+ sin6->sin6_family = AF_INET6;
+ sin6->sin6_port = htons((uint16_t)port);
+ if (inet_pton(AF_INET6, host + 1, &sin6->sin6_addr) != 1) return -1;
+ } else {
+ struct sockaddr_in *sin = (struct sockaddr_in *)addr;
+ sin->sin_family = AF_INET;
+ sin->sin_port = htons((uint16_t)port);
+ if (inet_pton(AF_INET, host, &sin->sin_addr) != 1) return -1;
+ }
+
+ return 0;
+}
+
+int sockaddr_equal(const struct sockaddr *a, const struct sockaddr *b) {
+ if (!a || !b || a->sa_family != b->sa_family) return 0;
+
+ if (a->sa_family == AF_INET) {
+ struct sockaddr_in *sin_a = (struct sockaddr_in *)a;
+ struct sockaddr_in *sin_b = (struct sockaddr_in *)b;
+ return sin_a->sin_addr.s_addr == sin_b->sin_addr.s_addr && sin_a->sin_port == sin_b->sin_port;
+ } else if (a->sa_family == AF_INET6) {
+ struct sockaddr_in6 *sin6_a = (struct sockaddr_in6 *)a;
+ struct sockaddr_in6 *sin6_b = (struct sockaddr_in6 *)b;
+ return memcmp(&sin6_a->sin6_addr, &sin6_b->sin6_addr, sizeof(sin6_a->sin6_addr)) == 0 &&
+ sin6_a->sin6_port == sin6_b->sin6_port;
+ }
+ return 0;
+}
diff --git a/src/socket-util.h b/src/socket-util.h
@@ -0,0 +1,22 @@
+#ifndef SOCKET_UTIL_H
+#define SOCKET_UTIL_H
+
+#include <sys/socket.h>
+
+int set_socket_nonblocking(int fd, int nonblock);
+
+int *tcp_listen(const char *addr, const char *default_host, const char *default_port);
+
+int *udp_recv(const char *addr, const char *default_host, const char *default_port);
+
+int *unix_listen(const char *path, int sock_type, const char *owner);
+
+int *merge_fd_arrays(int **arrays, int count);
+
+void sockaddr_to_string(const struct sockaddr *addr, char *buf, size_t buf_size);
+
+int string_to_sockaddr(const char *str, struct sockaddr_storage *addr);
+
+int sockaddr_equal(const struct sockaddr *a, const struct sockaddr *b);
+
+#endif
diff --git a/test/Makefile b/test/Makefile
@@ -0,0 +1,28 @@
+CC = gcc
+CFLAGS = -Wall -Wextra -I../src -I. -Itest
+SRC = ../src/socket-util.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/merge_fd_arrays.test.c b/test/merge_fd_arrays.test.c
@@ -0,0 +1,65 @@
+#include <stdlib.h>
+#include "socket-util.h"
+#include "test.h"
+
+void test_merge_fd_arrays_single() {
+ int *arr1 = malloc(sizeof(int) * 3);
+ arr1[0] = 2;
+ arr1[1] = 5;
+ arr1[2] = 10;
+
+ int *arrays[] = { arr1 };
+ int *result = merge_fd_arrays(arrays, 1);
+
+ ASSERT("single array merge - count", result[0] == 2);
+ ASSERT("single array merge - first", result[1] == 5);
+ ASSERT("single array merge - second", result[2] == 10);
+
+ free(result);
+}
+
+void test_merge_fd_arrays_multiple() {
+ int *arr1 = malloc(sizeof(int) * 3);
+ arr1[0] = 2;
+ arr1[1] = 1;
+ arr1[2] = 2;
+
+ int *arr2 = malloc(sizeof(int) * 3);
+ arr2[0] = 2;
+ arr2[1] = 3;
+ arr2[2] = 4;
+
+ int *arrays[] = { arr1, arr2 };
+ int *result = merge_fd_arrays(arrays, 2);
+
+ ASSERT("multiple array merge - count", result[0] == 4);
+ ASSERT("multiple array merge - first", result[1] == 1);
+ ASSERT("multiple array merge - second", result[2] == 2);
+ ASSERT("multiple array merge - third", result[3] == 3);
+ ASSERT("multiple array merge - fourth", result[4] == 4);
+
+ free(result);
+}
+
+void test_merge_fd_arrays_empty() {
+ int *arr1 = malloc(sizeof(int) * 1);
+ arr1[0] = 0;
+
+ int *arrays[] = { arr1 };
+ int *result = merge_fd_arrays(arrays, 1);
+
+ ASSERT("empty array merge", result == NULL);
+}
+
+void test_merge_fd_arrays_null() {
+ int *result = merge_fd_arrays(NULL, 0);
+ ASSERT("null arrays", result == NULL);
+}
+
+int main() {
+ RUN(test_merge_fd_arrays_single);
+ RUN(test_merge_fd_arrays_multiple);
+ RUN(test_merge_fd_arrays_empty);
+ RUN(test_merge_fd_arrays_null);
+ return TEST_REPORT();
+}
diff --git a/test/rxi/log.h b/test/rxi/log.h
@@ -0,0 +1,11 @@
+#ifndef RXI_LOG_H
+#define RXI_LOG_H
+
+#include <stdio.h>
+
+#define log_error(fmt, ...) fprintf(stderr, "ERROR: " fmt "\n", ##__VA_ARGS__)
+#define log_warn(fmt, ...) fprintf(stderr, "WARN: " fmt "\n", ##__VA_ARGS__)
+#define log_info(fmt, ...) fprintf(stderr, "INFO: " fmt "\n", ##__VA_ARGS__)
+#define log_debug(fmt, ...) fprintf(stderr, "DEBUG: " fmt "\n", ##__VA_ARGS__)
+
+#endif
diff --git a/test/sockaddr_equal.test.c b/test/sockaddr_equal.test.c
@@ -0,0 +1,120 @@
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <string.h>
+#include "socket-util.h"
+#include "test.h"
+
+void test_sockaddr_equal_ipv4_same() {
+ struct sockaddr_in a, b;
+ memset(&a, 0, sizeof(a));
+ memset(&b, 0, sizeof(b));
+
+ a.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.1", &a.sin_addr);
+ a.sin_port = htons(8080);
+
+ b.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.1", &b.sin_addr);
+ b.sin_port = htons(8080);
+
+ ASSERT("ipv4 same", sockaddr_equal((struct sockaddr *)&a, (struct sockaddr *)&b) == 1);
+}
+
+void test_sockaddr_equal_ipv4_different_ip() {
+ struct sockaddr_in a, b;
+ memset(&a, 0, sizeof(a));
+ memset(&b, 0, sizeof(b));
+
+ a.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.1", &a.sin_addr);
+ a.sin_port = htons(8080);
+
+ b.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.2", &b.sin_addr);
+ b.sin_port = htons(8080);
+
+ ASSERT("ipv4 different ip", sockaddr_equal((struct sockaddr *)&a, (struct sockaddr *)&b) == 0);
+}
+
+void test_sockaddr_equal_ipv4_different_port() {
+ struct sockaddr_in a, b;
+ memset(&a, 0, sizeof(a));
+ memset(&b, 0, sizeof(b));
+
+ a.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.1", &a.sin_addr);
+ a.sin_port = htons(8080);
+
+ b.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.1", &b.sin_addr);
+ b.sin_port = htons(8081);
+
+ ASSERT("ipv4 different port", sockaddr_equal((struct sockaddr *)&a, (struct sockaddr *)&b) == 0);
+}
+
+void test_sockaddr_equal_ipv6_same() {
+ struct sockaddr_in6 a, b;
+ memset(&a, 0, sizeof(a));
+ memset(&b, 0, sizeof(b));
+
+ a.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, "::1", &a.sin6_addr);
+ a.sin6_port = htons(3000);
+
+ b.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, "::1", &b.sin6_addr);
+ b.sin6_port = htons(3000);
+
+ ASSERT("ipv6 same", sockaddr_equal((struct sockaddr *)&a, (struct sockaddr *)&b) == 1);
+}
+
+void test_sockaddr_equal_ipv6_different_ip() {
+ struct sockaddr_in6 a, b;
+ memset(&a, 0, sizeof(a));
+ memset(&b, 0, sizeof(b));
+
+ a.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, "::1", &a.sin6_addr);
+ a.sin6_port = htons(3000);
+
+ b.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, "::2", &b.sin6_addr);
+ b.sin6_port = htons(3000);
+
+ ASSERT("ipv6 different ip", sockaddr_equal((struct sockaddr *)&a, (struct sockaddr *)&b) == 0);
+}
+
+void test_sockaddr_equal_different_family() {
+ struct sockaddr_in a;
+ struct sockaddr_in6 b;
+
+ memset(&a, 0, sizeof(a));
+ memset(&b, 0, sizeof(b));
+
+ a.sin_family = AF_INET;
+ b.sin6_family = AF_INET6;
+
+ ASSERT("different family", sockaddr_equal((struct sockaddr *)&a, (struct sockaddr *)&b) == 0);
+}
+
+void test_sockaddr_equal_null() {
+ struct sockaddr_in a;
+ memset(&a, 0, sizeof(a));
+ a.sin_family = AF_INET;
+
+ ASSERT("null a", sockaddr_equal(NULL, (struct sockaddr *)&a) == 0);
+ ASSERT("null b", sockaddr_equal((struct sockaddr *)&a, NULL) == 0);
+ ASSERT("both null", sockaddr_equal(NULL, NULL) == 0);
+}
+
+int main() {
+ RUN(test_sockaddr_equal_ipv4_same);
+ RUN(test_sockaddr_equal_ipv4_different_ip);
+ RUN(test_sockaddr_equal_ipv4_different_port);
+ RUN(test_sockaddr_equal_ipv6_same);
+ RUN(test_sockaddr_equal_ipv6_different_ip);
+ RUN(test_sockaddr_equal_different_family);
+ RUN(test_sockaddr_equal_null);
+ return TEST_REPORT();
+}
diff --git a/test/sockaddr_to_string.test.c b/test/sockaddr_to_string.test.c
@@ -0,0 +1,59 @@
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <string.h>
+#include "socket-util.h"
+#include "test.h"
+
+void test_sockaddr_to_string_ipv4() {
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
+ addr.sin_port = htons(8080);
+
+ char buf[INET6_ADDRSTRLEN + 8];
+ sockaddr_to_string((struct sockaddr *)&addr, buf, sizeof(buf));
+
+ ASSERT_STRING_EQUALS("192.168.1.1:8080", buf);
+}
+
+void test_sockaddr_to_string_ipv6() {
+ struct sockaddr_in6 addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, "::1", &addr.sin6_addr);
+ addr.sin6_port = htons(3000);
+
+ char buf[INET6_ADDRSTRLEN + 8];
+ sockaddr_to_string((struct sockaddr *)&addr, buf, sizeof(buf));
+
+ ASSERT_STRING_EQUALS("[::1]:3000", buf);
+}
+
+void test_sockaddr_to_string_ipv6_full() {
+ struct sockaddr_in6 addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, "2001:db8::1", &addr.sin6_addr);
+ addr.sin6_port = htons(443);
+
+ char buf[INET6_ADDRSTRLEN + 8];
+ sockaddr_to_string((struct sockaddr *)&addr, buf, sizeof(buf));
+
+ ASSERT_STRING_EQUALS("[2001:db8::1]:443", buf);
+}
+
+void test_sockaddr_to_string_null() {
+ char buf[64] = "initial";
+ sockaddr_to_string(NULL, buf, sizeof(buf));
+ ASSERT("null addr doesn't crash", 1);
+}
+
+int main() {
+ RUN(test_sockaddr_to_string_ipv4);
+ RUN(test_sockaddr_to_string_ipv6);
+ RUN(test_sockaddr_to_string_ipv6_full);
+ RUN(test_sockaddr_to_string_null);
+ return TEST_REPORT();
+}
diff --git a/test/string_to_sockaddr.test.c b/test/string_to_sockaddr.test.c
@@ -0,0 +1,76 @@
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <string.h>
+#include "socket-util.h"
+#include "test.h"
+
+void test_string_to_sockaddr_ipv4() {
+ struct sockaddr_storage addr;
+ int result = string_to_sockaddr("192.168.1.1:8080", &addr);
+
+ ASSERT("ipv4 parse success", result == 0);
+ ASSERT("ipv4 family", addr.ss_family == AF_INET);
+
+ struct sockaddr_in *sin = (struct sockaddr_in *)&addr;
+ char buf[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET, &sin->sin_addr, buf, sizeof(buf));
+ ASSERT_STRING_EQUALS("192.168.1.1", buf);
+ ASSERT("ipv4 port", ntohs(sin->sin_port) == 8080);
+}
+
+void test_string_to_sockaddr_ipv6() {
+ struct sockaddr_storage addr;
+ int result = string_to_sockaddr("[::1]:3000", &addr);
+
+ ASSERT("ipv6 parse success", result == 0);
+ ASSERT("ipv6 family", addr.ss_family == AF_INET6);
+
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&addr;
+ char buf[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET6, &sin6->sin6_addr, buf, sizeof(buf));
+ ASSERT_STRING_EQUALS("::1", buf);
+ ASSERT("ipv6 port", ntohs(sin6->sin6_port) == 3000);
+}
+
+void test_string_to_sockaddr_ipv6_full() {
+ struct sockaddr_storage addr;
+ int result = string_to_sockaddr("[2001:db8::1]:443", &addr);
+
+ ASSERT("ipv6 full parse success", result == 0);
+ ASSERT("ipv6 full family", addr.ss_family == AF_INET6);
+
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&addr;
+ char buf[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET6, &sin6->sin6_addr, buf, sizeof(buf));
+ ASSERT_STRING_EQUALS("2001:db8::1", buf);
+ ASSERT("ipv6 full port", ntohs(sin6->sin6_port) == 443);
+}
+
+void test_string_to_sockaddr_invalid_no_port() {
+ struct sockaddr_storage addr;
+ int result = string_to_sockaddr("192.168.1.1", &addr);
+ ASSERT("no port fails", result == -1);
+}
+
+void test_string_to_sockaddr_invalid_port() {
+ struct sockaddr_storage addr;
+ int result = string_to_sockaddr("192.168.1.1:99999", &addr);
+ ASSERT("invalid port fails", result == -1);
+}
+
+void test_string_to_sockaddr_null() {
+ struct sockaddr_storage addr;
+ int result = string_to_sockaddr(NULL, &addr);
+ ASSERT("null string fails", result == -1);
+}
+
+int main() {
+ RUN(test_string_to_sockaddr_ipv4);
+ RUN(test_string_to_sockaddr_ipv6);
+ RUN(test_string_to_sockaddr_ipv6_full);
+ RUN(test_string_to_sockaddr_invalid_no_port);
+ RUN(test_string_to_sockaddr_invalid_port);
+ RUN(test_string_to_sockaddr_null);
+ return TEST_REPORT();
+}
diff --git a/test/test.h b/test/test.h
@@ -0,0 +1,242 @@
+/// 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>
+
+///
+/// Helper function for string comparison to avoid NULL warnings
+///
+static inline int _string_equals(const char *a, const char *b) {
+ if (a == NULL && b == NULL) return 1;
+ if (a == NULL || b == NULL) return 0;
+ return strcmp(a, b) == 0;
+}
+
+///
+/// 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), _string_equals((expected),(actual)))
+///>
+/// </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