socket-util.c

Socket helper utils
git clone git://git.finwo.net/lib/socket-util.c
Log | Files | Refs | LICENSE

commit 21f00bea954a015ede9cfffd78c0c6fb7337dc10
Author: Robin Bron <robin.bron@yourhosting.nl>
Date:   Mon, 16 Mar 2026 19:17:42 +0100

Project init

Diffstat:
A.clang-format | 334+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.dep.export | 1+
A.gitignore | 5+++++
ALICENSE.md | 39+++++++++++++++++++++++++++++++++++++++
AMakefile | 9+++++++++
Aconfig.mk | 1+
Asrc/socket-util.c | 520+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/socket-util.h | 22++++++++++++++++++++++
Atest/Makefile | 28++++++++++++++++++++++++++++
Atest/merge_fd_arrays.test.c | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/rxi/log.h | 11+++++++++++
Atest/sockaddr_equal.test.c | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/sockaddr_to_string.test.c | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/string_to_sockaddr.test.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/test.h | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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