resp.c

Basic RESP handling library in C
git clone git://git.finwo.net/lib/resp.c
Log | Files | Refs | LICENSE

commit db3e63fac4ba6cc029c260b89820a6ab969e3aec
Author: Robin Bron <robin.bron@yourhosting.nl>
Date:   Thu, 12 Mar 2026 23:37:06 +0100

Project init

Diffstat:
A.clang-format | 334+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.github/FUNDING.yml | 3+++
A.gitignore | 2++
ACODE_OF_CONDUCT.md | 5+++++
ALICENSE.md | 34++++++++++++++++++++++++++++++++++
AMakefile | 9+++++++++
Aconfig.mk | 2++
Apackage.ini | 8++++++++
Asrc/resp.c | 683+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/resp.h | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/Makefile | 29+++++++++++++++++++++++++++++
Atest/basic.c | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/test.h | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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