commit db3e63fac4ba6cc029c260b89820a6ab969e3aec
Author: Robin Bron <robin.bron@yourhosting.nl>
Date: Thu, 12 Mar 2026 23:37:06 +0100
Project init
Diffstat:
13 files changed, 1550 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/.github/FUNDING.yml b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# f4d2ed80-57b6-46e6-b245-5049428a931d
+github: finwo
+liberapay: finwo
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+*.o
+/test/basic
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
@@ -0,0 +1,5 @@
+# Contributor Code of Conduct
+
+This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters.
+
+For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage.
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/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+=__DIRNAME/src/resp.c
+\ No newline at end of file
diff --git a/package.ini b/package.ini
@@ -0,0 +1,7 @@
+[export]
+config.mk=config.mk
+include/finwo/resp.h=src/resp.h
+
+[package]
+deps=lib
+name=finwo/resp
+\ No newline at end of file
diff --git a/src/resp.c b/src/resp.c
@@ -0,0 +1,683 @@
+#include "resp.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MAX_BULK_LEN (256 * 1024)
+#define LINE_BUF 4096
+
+static void resp_free_internal(resp_object *o);
+
+int resp_read_buf(const char *buf, size_t len, resp_object **out_obj) {
+ if (!out_obj) return -1;
+
+ const char *start = buf;
+ const char *p = buf;
+ size_t remaining = len;
+
+ // We need at least 1 byte
+ if (len <= 0) return -1;
+
+ // Ensure we have memory to place data in
+ resp_object *output = *out_obj;
+ if (!output) {
+ *out_obj = output = calloc(1, sizeof(resp_object));
+ if (!output) return -1;
+ }
+
+ // Skip empty lines (only \r\n)
+ if (p[0] == '\r' || p[0] == '\n') {
+ while (remaining > 0 && (p[0] == '\r' || p[0] == '\n')) {
+ p++;
+ remaining--;
+ }
+ if (remaining == 0) return 0; // only whitespace, need more data
+ }
+
+ // Consume first character for object type detection
+ int type_c = p[0];
+ remaining--;
+ p++;
+
+ // And act accordingly
+ switch ((char)type_c) {
+ case '+':
+ output->type = output->type ? output->type : RESPT_SIMPLE;
+ if (output->type != RESPT_SIMPLE) {
+ return -2; // Mismatching types
+ }
+ // Read until \r\n, don't include \r\n in string
+ {
+ size_t i = 0;
+ char line[LINE_BUF];
+ int found_crlf = 0;
+ while (i + 1 < LINE_BUF && remaining > 0) {
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ found_crlf = 1;
+ break;
+ }
+ line[i++] = p[0];
+ p++;
+ remaining--;
+ }
+ if (!found_crlf) {
+ return -1; // Incomplete, need more data
+ }
+ line[i] = '\0';
+ if (output->u.s) free(output->u.s);
+ output->u.s = strdup(line);
+ }
+ break;
+
+ case '-':
+ output->type = output->type ? output->type : RESPT_ERROR;
+ if (output->type != RESPT_ERROR) {
+ return -2; // Mismatching types
+ }
+ // Read until \r\n, don't include \r\n in string
+ {
+ size_t i = 0;
+ char line[LINE_BUF];
+ int found_crlf = 0;
+ while (i + 1 < LINE_BUF && remaining > 0) {
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ found_crlf = 1;
+ break;
+ }
+ line[i++] = p[0];
+ p++;
+ remaining--;
+ }
+ if (!found_crlf) {
+ return -1; // Incomplete, need more data
+ }
+ line[i] = '\0';
+ if (output->u.s) free(output->u.s);
+ output->u.s = strdup(line);
+ }
+ break;
+
+ case ':':
+ output->type = output->type ? output->type : RESPT_INT;
+ if (output->type != RESPT_INT) {
+ return -2; // Mismatching types
+ }
+ // Read until \r\n, don't include \r\n in string
+ // value = strtoll(line);
+ {
+ size_t i = 0;
+ char line[LINE_BUF];
+ int found_crlf = 0;
+ while (i + 1 < LINE_BUF && remaining > 0) {
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ found_crlf = 1;
+ break;
+ }
+ line[i++] = p[0];
+ p++;
+ remaining--;
+ }
+ if (!found_crlf) {
+ return -1; // Incomplete, need more data
+ }
+ line[i] = '\0';
+ output->u.i = strtoll(line, NULL, 10);
+ }
+ break;
+
+ case '$':
+ output->type = output->type ? output->type : RESPT_BULK;
+ if (output->type != RESPT_BULK) {
+ return -2; // Mismatching types
+ }
+ // Read until \r\n, don't include \r\n in string
+ // data_length = strtoll(line);
+ {
+ size_t i = 0;
+ char line[LINE_BUF];
+ int found_crlf = 0;
+ while (i + 1 < LINE_BUF && remaining > 0) {
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ found_crlf = 1;
+ break;
+ }
+ line[i++] = p[0];
+ p++;
+ remaining--;
+ }
+ if (!found_crlf) {
+ return -1; // Incomplete, need more data
+ }
+ line[i] = '\0';
+ long data_length = strtol(line, NULL, 10);
+
+ if (data_length < 0) {
+ output->u.s = NULL;
+ } else if (data_length == 0) {
+ // Null bulk string or empty string - need \r\n
+
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ } else {
+ return -1; // Incomplete, need more data
+ }
+ if (output->u.s) free(output->u.s);
+ output->u.s = strdup("");
+ } else {
+ // Read data_length bytes
+ if ((size_t)data_length > remaining) {
+ return -1; // not enough data
+ }
+ if (output->u.s) free(output->u.s);
+ output->u.s = malloc((size_t)data_length + 1);
+ if (!output->u.s) return -1;
+ memcpy(output->u.s, p, (size_t)data_length);
+ output->u.s[data_length] = '\0';
+ p += data_length;
+ remaining -= data_length;
+ // Skip \r\n
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ } else {
+ free(output->u.s);
+ output->u.s = NULL;
+ return -1; // Incomplete, need more data
+ }
+ }
+ }
+ break;
+
+ case '*':
+ output->type = output->type ? output->type : RESPT_ARRAY;
+ if (output->type != RESPT_ARRAY) {
+ return -2; // Mismatching types
+ }
+ // Read until \r\n, don't include \r\n in string
+ // items = strtoll(line);
+ {
+ size_t i = 0;
+ char line[LINE_BUF];
+ int found_crlf = 0;
+ while (i + 1 < LINE_BUF && remaining > 0) {
+ if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') {
+ p += 2;
+ remaining -= 2;
+ found_crlf = 1;
+ break;
+ }
+ line[i++] = p[0];
+ p++;
+ remaining--;
+ }
+ if (!found_crlf) {
+ return -1; // Incomplete, need more data
+ }
+ line[i] = '\0';
+ long items = strtol(line, NULL, 10);
+
+ if (items < 0 || items > 65536) {
+ return -1;
+ }
+
+ // Initialize array if needed
+ if (!output->u.arr.elem) {
+ output->u.arr.n = 0;
+ output->u.arr.elem = NULL;
+ }
+
+ for (size_t j = 0; j < (size_t)items; j++) {
+ if (remaining == 0) {
+ return -1;
+ }
+ resp_object *element = NULL;
+ int element_consumed = resp_read_buf(p, remaining, &element);
+ if (element_consumed <= 0) {
+ return -1;
+ }
+ if (resp_array_append_obj(output, element) != 0) {
+ resp_free(element);
+ return -1;
+ }
+ p += element_consumed;
+ remaining -= element_consumed;
+ }
+ }
+ break;
+
+ default:
+ return -1;
+ }
+
+ return (int)(p - start);
+}
+
+static int resp_read_byte(int fd) {
+ unsigned char c;
+ ssize_t n = read(fd, &c, 1);
+ if (n != 1) {
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return -2;
+ return -1;
+ }
+ return (int)c;
+}
+
+static int resp_read_line(int fd, char *buf, size_t buf_size) {
+ size_t i = 0;
+ int prev = -1;
+ while (i + 1 < buf_size) {
+ int b = resp_read_byte(fd);
+ if (b < 0) return -1;
+ if (prev == '\r' && b == '\n') {
+ buf[i - 1] = '\0';
+ return 0;
+ }
+ prev = b;
+ buf[i++] = (char)b;
+ }
+ return -1;
+}
+
+resp_object *resp_read(int fd) {
+ int type_c = resp_read_byte(fd);
+ if (type_c < 0) return NULL;
+ if (type_c == -2) return NULL;
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return NULL;
+ char line[LINE_BUF];
+ switch ((char)type_c) {
+ case '+':
+ o->type = RESPT_SIMPLE;
+ if (resp_read_line(fd, line, sizeof(line)) != 0) {
+ free(o);
+ return NULL;
+ }
+ o->u.s = strdup(line);
+ break;
+ case '-':
+ o->type = RESPT_ERROR;
+ if (resp_read_line(fd, line, sizeof(line)) != 0) {
+ free(o);
+ return NULL;
+ }
+ o->u.s = strdup(line);
+ break;
+ case ':':
+ {
+ if (resp_read_line(fd, line, sizeof(line)) != 0) {
+ free(o);
+ return NULL;
+ }
+ o->type = RESPT_INT;
+ o->u.i = (long long)strtoll(line, NULL, 10);
+ break;
+ }
+ case '$':
+ {
+ if (resp_read_line(fd, line, sizeof(line)) != 0) {
+ free(o);
+ return NULL;
+ }
+ long len = strtol(line, NULL, 10);
+ if (len < 0 || len > (long)MAX_BULK_LEN) {
+ free(o);
+ return NULL;
+ }
+ o->type = RESPT_BULK;
+ if (len == 0) {
+ o->u.s = strdup("");
+ if (resp_read_line(fd, line, sizeof(line)) != 0) {
+ free(o->u.s);
+ free(o);
+ return NULL;
+ }
+ } else {
+ o->u.s = malloc((size_t)len + 1);
+ if (!o->u.s) {
+ free(o);
+ return NULL;
+ }
+ if (read(fd, o->u.s, (size_t)len) != (ssize_t)len) {
+ free(o->u.s);
+ free(o);
+ return NULL;
+ }
+ o->u.s[len] = '\0';
+ if (resp_read_byte(fd) != '\r' || resp_read_byte(fd) != '\n') {
+ free(o->u.s);
+ free(o);
+ return NULL;
+ }
+ }
+ break;
+ }
+ case '*':
+ {
+ if (resp_read_line(fd, line, sizeof(line)) != 0) {
+ free(o);
+ return NULL;
+ }
+ long n = strtol(line, NULL, 10);
+ if (n < 0 || n > 65536) {
+ free(o);
+ return NULL;
+ }
+ o->type = RESPT_ARRAY;
+ o->u.arr.n = (size_t)n;
+ o->u.arr.elem = n ? calloc((size_t)n, sizeof(resp_object)) : NULL;
+ if (n && !o->u.arr.elem) {
+ free(o);
+ return NULL;
+ }
+ for (size_t i = 0; i < (size_t)n; i++) {
+ resp_object *sub = resp_read(fd);
+ if (!sub) {
+ for (size_t j = 0; j < i; j++) resp_free_internal(&o->u.arr.elem[j]);
+ free(o->u.arr.elem);
+ free(o);
+ return NULL;
+ }
+ o->u.arr.elem[i] = *sub;
+ free(sub);
+ }
+ break;
+ }
+ default:
+ free(o);
+ return NULL;
+ }
+ return o;
+}
+
+static void resp_free_internal(resp_object *o) {
+ if (!o) return;
+ if (o->type == RESPT_SIMPLE || o->type == RESPT_ERROR || o->type == RESPT_BULK) {
+ free(o->u.s);
+ } else if (o->type == RESPT_ARRAY) {
+ for (size_t i = 0; i < o->u.arr.n; i++) resp_free_internal(&o->u.arr.elem[i]);
+ free(o->u.arr.elem);
+ }
+}
+
+void resp_free(resp_object *o) {
+ resp_free_internal(o);
+ free(o);
+}
+
+resp_object *resp_deep_copy(const resp_object *o) {
+ if (!o) return NULL;
+ resp_object *c = (resp_object *)calloc(1, sizeof(resp_object));
+ if (!c) return NULL;
+ c->type = o->type;
+ if (o->type == RESPT_SIMPLE || o->type == RESPT_ERROR || o->type == RESPT_BULK) {
+ c->u.s = o->u.s ? strdup(o->u.s) : NULL;
+ if (o->u.s && !c->u.s) {
+ free(c);
+ return NULL;
+ }
+ return c;
+ }
+ if (o->type == RESPT_INT) {
+ c->u.i = o->u.i;
+ return c;
+ }
+ if (o->type == RESPT_ARRAY) {
+ c->u.arr.n = o->u.arr.n;
+ c->u.arr.elem = o->u.arr.n ? (resp_object *)calloc(o->u.arr.n, sizeof(resp_object)) : NULL;
+ if (o->u.arr.n && !c->u.arr.elem) {
+ free(c);
+ return NULL;
+ }
+ for (size_t i = 0; i < o->u.arr.n; i++) {
+ resp_object *sub = resp_deep_copy(&o->u.arr.elem[i]);
+ if (!sub) {
+ for (size_t j = 0; j < i; j++) resp_free_internal(&c->u.arr.elem[j]);
+ free(c->u.arr.elem);
+ free(c);
+ return NULL;
+ }
+ c->u.arr.elem[i] = *sub;
+ free(sub);
+ }
+ return c;
+ }
+ free(c);
+ return NULL;
+}
+
+resp_object *resp_map_get(const resp_object *o, const char *key) {
+ if (!o || !key || o->type != RESPT_ARRAY) return NULL;
+ size_t n = o->u.arr.n;
+ if (n & 1) return NULL;
+ for (size_t i = 0; i < n; i += 2) {
+ const resp_object *k = &o->u.arr.elem[i];
+ const char *s = (k->type == RESPT_BULK || k->type == RESPT_SIMPLE) ? k->u.s : NULL;
+ if (s && strcmp(s, key) == 0 && i + 1 < n) return (resp_object *)&o->u.arr.elem[i + 1];
+ }
+ return NULL;
+}
+
+const char *resp_map_get_string(const resp_object *o, const char *key) {
+ resp_object *val = resp_map_get(o, key);
+ if (!val) return NULL;
+ if (val->type == RESPT_BULK || val->type == RESPT_SIMPLE) return val->u.s;
+ return NULL;
+}
+
+void resp_map_set(resp_object *o, const char *key, resp_object *value) {
+ if (!o || !key || o->type != RESPT_ARRAY) return;
+ for (size_t i = 0; i + 1 < o->u.arr.n; i += 2) {
+ const resp_object *k = &o->u.arr.elem[i];
+ const char *s = (k->type == RESPT_BULK || k->type == RESPT_SIMPLE) ? k->u.s : NULL;
+ if (s && strcmp(s, key) == 0 && i + 1 < o->u.arr.n) {
+ resp_free(&o->u.arr.elem[i + 1]);
+ o->u.arr.elem[i + 1] = *value;
+ free(value);
+ return;
+ }
+ }
+ resp_array_append_bulk(o, key);
+ resp_array_append_obj(o, value);
+}
+
+static int resp_append_object(char **buf, size_t *cap, size_t *len, const resp_object *o) {
+ if (!o) return -1;
+ size_t need = *len + 256;
+ if (o->type == RESPT_BULK || o->type == RESPT_SIMPLE || o->type == RESPT_ERROR) {
+ size_t slen = o->u.s ? strlen(o->u.s) : 0;
+ need = *len + 32 + slen + 2;
+ } else if (o->type == RESPT_ARRAY) {
+ need = *len + 32;
+ for (size_t i = 0; i < o->u.arr.n; i++) need += 64;
+ }
+ if (need > *cap) {
+ size_t newcap = need + 4096;
+ char *n = realloc(*buf, newcap);
+ if (!n) return -1;
+ *buf = n;
+ *cap = newcap;
+ }
+ switch (o->type) {
+ case RESPT_SIMPLE:
+ {
+ const char *s = o->u.s ? o->u.s : "";
+ *len += (size_t)snprintf(*buf + *len, *cap - *len, "+%s\r\n", s);
+ break;
+ }
+ case RESPT_ERROR:
+ {
+ const char *s = o->u.s ? o->u.s : "";
+ *len += (size_t)snprintf(*buf + *len, *cap - *len, "-%s\r\n", s);
+ break;
+ }
+ case RESPT_INT:
+ *len += (size_t)snprintf(*buf + *len, *cap - *len, ":%lld\r\n", (long long)o->u.i);
+ break;
+ case RESPT_BULK:
+ {
+ const char *s = o->u.s ? o->u.s : "";
+ size_t slen = strlen(s);
+ *len += (size_t)snprintf(*buf + *len, *cap - *len, "$%zu\r\n%s\r\n", slen, s);
+ break;
+ }
+ case RESPT_ARRAY:
+ {
+ size_t n = o->u.arr.n;
+ *len += (size_t)snprintf(*buf + *len, *cap - *len, "*%zu\r\n", n);
+ for (size_t i = 0; i < n; i++) {
+ if (resp_append_object(buf, cap, len, &o->u.arr.elem[i]) != 0) return -1;
+ }
+ break;
+ }
+ default:
+ return -1;
+ }
+ return 0;
+}
+
+int resp_encode_array(int argc, const resp_object *const *argv, char **out_buf, size_t *out_len) {
+ size_t cap = 64;
+ size_t len = 0;
+ char *buf = malloc(cap);
+ if (!buf) return -1;
+ len += (size_t)snprintf(buf + len, cap - len, "*%d\r\n", argc);
+ if (len >= cap) {
+ free(buf);
+ return -1;
+ }
+ for (int i = 0; i < argc; i++) {
+ if (resp_append_object(&buf, &cap, &len, argv[i]) != 0) {
+ free(buf);
+ return -1;
+ }
+ }
+ *out_buf = buf;
+ *out_len = len;
+ return 0;
+}
+
+int resp_serialize(const resp_object *o, char **out_buf, size_t *out_len) {
+ size_t cap = 64;
+ size_t len = 0;
+ char *buf = malloc(cap);
+ if (!buf) return -1;
+ if (resp_append_object(&buf, &cap, &len, o) != 0) {
+ free(buf);
+ return -1;
+ }
+ *out_buf = buf;
+ *out_len = len;
+ return 0;
+}
+
+resp_object *resp_array_init(void) {
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return NULL;
+ o->type = RESPT_ARRAY;
+ o->u.arr.n = 0;
+ o->u.arr.elem = NULL;
+ return o;
+}
+
+resp_object *resp_simple_init(const char *value) {
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return NULL;
+ o->type = RESPT_SIMPLE;
+ o->u.s = value ? strdup(value) : NULL;
+ return o;
+}
+
+int resp_array_append_obj(resp_object *destination, resp_object *value) {
+ if (!destination || destination->type != RESPT_ARRAY || !value) return -1;
+ size_t n = destination->u.arr.n;
+ resp_object *new_elem = realloc(destination->u.arr.elem, (n + 1) * sizeof(resp_object));
+ if (!new_elem) return -1;
+ destination->u.arr.elem = new_elem;
+ destination->u.arr.elem[n] = *value;
+ destination->u.arr.n++;
+ free(value);
+ return 0;
+}
+
+resp_object *resp_error_init(const char *value) {
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return NULL;
+ o->type = RESPT_ERROR;
+ o->u.s = strdup(value ? value : "");
+ if (!o->u.s) {
+ free(o);
+ return NULL;
+ }
+ return o;
+}
+
+int resp_array_append_simple(resp_object *destination, const char *str) {
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return -1;
+ o->type = RESPT_SIMPLE;
+ o->u.s = strdup(str ? str : "");
+ if (!o->u.s) {
+ free(o);
+ return -1;
+ }
+ if (resp_array_append_obj(destination, o) != 0) {
+ free(o->u.s);
+ free(o);
+ return -1;
+ }
+ return 0;
+}
+
+int resp_array_append_error(resp_object *destination, const char *str) {
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return -1;
+ o->type = RESPT_ERROR;
+ o->u.s = strdup(str ? str : "");
+ if (!o->u.s) {
+ free(o);
+ return -1;
+ }
+ if (resp_array_append_obj(destination, o) != 0) {
+ free(o->u.s);
+ free(o);
+ return -1;
+ }
+ return 0;
+}
+
+int resp_array_append_bulk(resp_object *destination, const char *str) {
+ resp_object *o = calloc(1, sizeof(resp_object));
+ if (!o) return -1;
+ o->type = RESPT_BULK;
+ o->u.s = strdup(str ? str : "");
+ if (!o->u.s) {
+ free(o);
+ return -1;
+ }
+ if (resp_array_append_obj(destination, o) != 0) {
+ free(o->u.s);
+ free(o);
+ return -1;
+ }
+ return 0;
+}
+
+int resp_array_append_int(resp_object *destination, long long i) {
+ resp_object *o = malloc(sizeof(resp_object));
+ if (!o) return -1;
+ o->type = RESPT_INT;
+ o->u.i = i;
+ return resp_array_append_obj(destination, o);
+}
diff --git a/src/resp.h b/src/resp.h
@@ -0,0 +1,76 @@
+#ifndef RESP_H
+#define RESP_H
+
+#include <stddef.h>
+
+#define RESPT_SIMPLE 0
+#define RESPT_ERROR 1
+#define RESPT_BULK 2
+#define RESPT_INT 3
+#define RESPT_ARRAY 4
+
+typedef struct resp_object resp_object;
+struct resp_object {
+ int type;
+ union {
+ char *s;
+ long long i;
+ struct {
+ resp_object *elem;
+ size_t n;
+ } arr;
+ } u;
+};
+
+void resp_free(resp_object *o);
+/* Takes ownership: frees the object and all nested data */
+
+resp_object *resp_deep_copy(const resp_object *o);
+/* Returns new object: caller owns the result, must call resp_free() */
+
+resp_object *resp_map_get(const resp_object *o, const char *key);
+/* Returns pointer into o: caller must NOT call resp_free() on result */
+
+const char *resp_map_get_string(const resp_object *o, const char *key);
+/* Returns pointer into o: caller must NOT free the result */
+
+void resp_map_set(resp_object *map, const char *key, resp_object *value);
+/* Takes ownership of value */
+
+resp_object *resp_read(int fd);
+/* Returns new object: caller owns the result, must call resp_free() */
+
+int resp_read_buf(const char *buf, size_t len, resp_object **out_obj);
+/* Returns 0=no data yet, <0=incomplete (need more data), >0=bytes consumed from buffer */
+/* If out_obj is NULL, returns -1 */
+/* If *out_obj is NULL, creates new object */
+/* If *out_obj exists, appends to it (for arrays) */
+
+int resp_encode_array(int argc, const resp_object *const *argv, char **out_buf, size_t *out_len);
+/* Returns allocated string in out_buf: caller must free() the string */
+
+int resp_serialize(const resp_object *o, char **out_buf, size_t *out_len);
+/* Returns allocated string in out_buf: caller must free() the string */
+
+resp_object *resp_array_init(void);
+/* Returns new array object: caller owns the result, must call resp_free() */
+
+resp_object *resp_simple_init(const char *value);
+/* Returns new simple string object: caller owns the result, must call
+ * resp_free() */
+
+resp_object *resp_error_init(const char *value);
+/* Returns new error object: caller owns the result, must call resp_free() */
+
+int resp_array_append_obj(resp_object *destination, resp_object *value);
+/* Takes ownership of value */
+
+int resp_array_append_simple(resp_object *destination, const char *str);
+/* Copies str: caller may free str after return */
+
+int resp_array_append_bulk(resp_object *destination, const char *str);
+/* Copies str: caller may free str after return */
+
+int resp_array_append_int(resp_object *destination, long long i);
+
+#endif
diff --git a/test/Makefile b/test/Makefile
@@ -0,0 +1,28 @@
+CC = gcc
+CFLAGS = -Wall -Wextra -I../ -Itest
+SRC = ../src/resp.c
+
+TESTS = $(wildcard *.c)
+BINS = $(TESTS:.c=)
+
+all: $(BINS)
+
+%: %.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
+\ No newline at end of file
diff --git a/test/basic.c b/test/basic.c
@@ -0,0 +1,132 @@
+/*
+ * RESP encoding and decoding tests using finwo/assert.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "../src/resp.h"
+#include "test.h"
+
+/* Write encoded buf to pipe, read back one RESP value. Caller resp_frees result. */
+static resp_object *round_trip(const char *buf, size_t len) {
+ int fd[2];
+ if (pipe(fd) != 0) return NULL;
+ ssize_t n = write(fd[1], buf, len);
+ close(fd[1]);
+ if (n != (ssize_t)len) {
+ close(fd[0]);
+ return NULL;
+ }
+ resp_object *o = resp_read(fd[0]);
+ close(fd[0]);
+ return o;
+}
+
+void test_resp_encode_decode_bulk(void) {
+ resp_object bulk = {.type = RESPT_BULK, .u = {.s = strdup("hello")}};
+ const resp_object *av[] = {&bulk};
+ char *buf = NULL;
+ size_t len = 0;
+ ASSERT_EQUALS(0, resp_encode_array(1, av, &buf, &len));
+ ASSERT("encoded", buf != NULL && len > 0);
+ resp_object *dec = round_trip(buf, len);
+ ASSERT("decode non-NULL", dec != NULL);
+ ASSERT_EQUALS(RESPT_ARRAY, dec->type);
+ ASSERT_EQUALS(1, (int)dec->u.arr.n);
+ ASSERT_EQUALS(RESPT_BULK, dec->u.arr.elem[0].type);
+ ASSERT_STRING_EQUALS("hello", dec->u.arr.elem[0].u.s);
+ resp_free(dec);
+ free(buf);
+ free(bulk.u.s);
+}
+
+void test_resp_encode_decode_int(void) {
+ resp_object iobj = {.type = RESPT_INT, .u = {.i = -42}};
+ const resp_object *av[] = {&iobj};
+ char *buf = NULL;
+ size_t len = 0;
+ ASSERT_EQUALS(0, resp_encode_array(1, av, &buf, &len));
+ resp_object *dec = round_trip(buf, len);
+ ASSERT("decode non-NULL", dec != NULL);
+ ASSERT_EQUALS(RESPT_ARRAY, dec->type);
+ ASSERT_EQUALS(1, (int)dec->u.arr.n);
+ ASSERT_EQUALS(RESPT_INT, dec->u.arr.elem[0].type);
+ ASSERT_EQUALS(-42, (int)dec->u.arr.elem[0].u.i);
+ resp_free(dec);
+ free(buf);
+}
+
+void test_resp_encode_decode_array_of_strings(void) {
+ resp_object k = {.type = RESPT_BULK, .u = {.s = strdup("key")}};
+ resp_object v = {.type = RESPT_BULK, .u = {.s = strdup("value")}};
+ const resp_object *av[] = {&k, &v};
+ char *buf = NULL;
+ size_t len = 0;
+ ASSERT_EQUALS(0, resp_encode_array(2, av, &buf, &len));
+ resp_object *dec = round_trip(buf, len);
+ ASSERT("decode non-NULL", dec != NULL);
+ ASSERT_EQUALS(RESPT_ARRAY, dec->type);
+ ASSERT_EQUALS(2, (int)dec->u.arr.n);
+ ASSERT_EQUALS(RESPT_BULK, dec->u.arr.elem[0].type);
+ ASSERT_EQUALS(RESPT_BULK, dec->u.arr.elem[1].type);
+ ASSERT_STRING_EQUALS("key", dec->u.arr.elem[0].u.s);
+ ASSERT_STRING_EQUALS("value", dec->u.arr.elem[1].u.s);
+ resp_free(dec);
+ free(buf);
+ free(k.u.s);
+ free(v.u.s);
+}
+
+void test_resp_map_get_string(void) {
+ resp_object k1 = {.type = RESPT_BULK, .u = {.s = strdup("a")}};
+ resp_object v1 = {.type = RESPT_BULK, .u = {.s = strdup("1")}};
+ resp_object k2 = {.type = RESPT_BULK, .u = {.s = strdup("b")}};
+ resp_object v2 = {.type = RESPT_BULK, .u = {.s = strdup("2")}};
+ resp_object *map = calloc(1, sizeof(resp_object));
+ ASSERT("map alloc", map != NULL);
+ map->type = RESPT_ARRAY;
+ map->u.arr.n = 4;
+ map->u.arr.elem = calloc(4, sizeof(resp_object));
+ map->u.arr.elem[0] = k1;
+ map->u.arr.elem[1] = v1;
+ map->u.arr.elem[2] = k2;
+ map->u.arr.elem[3] = v2;
+ ASSERT_STRING_EQUALS("1", resp_map_get_string(map, "a"));
+ ASSERT_STRING_EQUALS("2", resp_map_get_string(map, "b"));
+ ASSERT("missing key returns NULL", resp_map_get_string(map, "c") == NULL);
+ resp_free(map);
+}
+
+void test_resp_map_get_missing_key(void) {
+ resp_object *map = resp_array_init();
+ ASSERT("map alloc", map != NULL);
+ ASSERT("missing key NULL", resp_map_get_string(map, "x") == NULL);
+ ASSERT("resp_map_get NULL", resp_map_get(map, "x") == NULL);
+ resp_free(map);
+}
+
+void test_resp_read_invalid_returns_null(void) {
+ int fd[2];
+ ASSERT_EQUALS(0, pipe(fd));
+ if (write(fd[1], "X\r\n", 3) != 3) {
+ close(fd[0]);
+ close(fd[1]);
+ return;
+ }
+ close(fd[1]);
+ resp_object *o = resp_read(fd[0]);
+ close(fd[0]);
+ ASSERT("invalid type byte returns NULL", o == NULL);
+}
+
+int main(void) {
+ RUN(test_resp_encode_decode_bulk);
+ RUN(test_resp_encode_decode_int);
+ RUN(test_resp_encode_decode_array_of_strings);
+ RUN(test_resp_map_get_string);
+ RUN(test_resp_map_get_missing_key);
+ RUN(test_resp_read_invalid_returns_null);
+ return TEST_REPORT();
+}
diff --git a/test/test.h b/test/test.h
@@ -0,0 +1,233 @@
+/// 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
+///<C
+#define ASSERT_STRING_EQUALS(expected, actual) ASSERT((#actual), 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