buf.c

Simple byte buffers
git clone git://git.finwo.net/lib/buf.c
Log | Files | Refs | README | LICENSE

commit 867af93a9afeb2e9d2a6f240483852b3086df9d1
Author: finwo <finwo@pm.me>
Date:   Sat, 18 Jul 2026 21:35:43 +0200

Project init

Diffstat:
A.dep.export | 1+
A.editorconfig | 13+++++++++++++
A.gitignore | 4++++
AMakefile | 18++++++++++++++++++
Aexport.mk | 1+
Asrc/buf.c | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/buf.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/.gitignore | 1+
Atest/0000-basics.c | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Atest/assert.h | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 517 insertions(+), 0 deletions(-)

diff --git a/.dep.export b/.dep.export @@ -0,0 +1 @@ +include/finwo/buf.h src/buf.h diff --git a/.editorconfig b/.editorconfig @@ -0,0 +1,13 @@ +# 2d0e4a3f-ac19-430c-bf1b-46c68651ce21 +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[Makefile*] +indent_style = tab diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +/.cache/ +*.o +/lib/ +/compile_commands.json diff --git a/Makefile b/Makefile @@ -0,0 +1,18 @@ +TESTS:= +TESTS+=$(patsubst %.c,%,$(wildcard test/*.c)) + +.PHONY: default +default: tests + +$(TESTS): ${TESTS:=.c} + $(CC) $@.c src/buf.c -I test -I src -o $@ + +.PHONY: tests +tests: $(TESTS) ${TESTS:=.c} + for test in $(TESTS); do \ + $$test ; \ + done + +.PHONY: clean +clean: + rm -f $(TESTS) diff --git a/export.mk b/export.mk @@ -0,0 +1 @@ +SRC+={{module.dirname}}/src/buf.c diff --git a/src/buf.c b/src/buf.c @@ -0,0 +1,136 @@ +#include <string.h> +#include <stdlib.h> + +#include "buf.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#ifndef MAX +#define MAX(a,b) ((a)>(b)?(a):(b)) +#endif + +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif + +#ifndef NULL +#define NULL ((void*)0) +#endif + +#define ARRAY_SIZE(x) ((sizeof x) / (sizeof *x)) + +const char *error_messages[] = { + "Unknown error", + "Missing subject", + "Missing source", + "Negative length", +}; + +#define BUF_ERRNO_UNKNOWN -1 +#define BUF_ERRNO_OK 0 +#define BUF_ERRNO_MISSING_SUBJECT 1 +#define BUF_ERRNO_MISSING_SOURCE 2 +#define BUF_ERRNO_NEGATIVE_LENGTH 3 + +static void *(*_realloc)(void* ptr,size_t size) = realloc; +static void (*_free)(void* ptr) = free; + +void buf_set_allocator(void *(*realloc)(void*,size_t), void (*free)(void*)) { + _realloc = realloc; + _free = free; +} + +const char *buf_error(int errno) { + + // No error + if (!errno) return NULL; + + // Known error + if ((errno > 0) && (errno < ARRAY_SIZE(error_messages))) { + return error_messages[errno]; + } + + // Unknown error + return error_messages[0]; +} + +int buf_append(struct buf *subject, const char *source, const size_t length) { + if (!subject) return BUF_ERRNO_MISSING_SUBJECT; + if (!source) return BUF_ERRNO_MISSING_SOURCE; + if (length < 0) return BUF_ERRNO_NEGATIVE_LENGTH; + + // Initial alloc if blank buffer + if (!subject->alloc) { + size_t l = MAX(32, length); + subject->alloc = _realloc(NULL, l); + subject->dat = subject->alloc; + subject->cap = l; + subject->len = 0; + goto lbl_buf_append_copy; // Simply skips cap checks + } + + // Eject consumed data if we need space + if ( + (subject->dat != subject->alloc) && + ((subject->len + length) > subject->cap) + ) { + size_t diff = subject->dat - subject->alloc; + memmove(subject->alloc, subject->dat, subject->len); + subject->cap += diff; + subject->dat = subject->alloc; + } + + // Grow buffer if still not enough space + if ((subject->len + length) > subject->cap) { + subject->cap = MAX(32, subject->cap * 2); + subject->alloc = _realloc(subject->alloc, subject->cap); + subject->dat = subject->alloc; + } + + // Add the data to the end +lbl_buf_append_copy: + memcpy(subject->dat + subject->len, source, length); + subject->len += length; + + return BUF_ERRNO_OK; +} + +int buf_append_byte(struct buf *subject, const char value) { + return buf_append(subject, &value, sizeof(char)); +} + +size_t buf_consume(struct buf *subject, size_t length) { + if (!subject) return 0; + if (length <= 0) return 0; + + size_t n = MIN(length, subject->len); + subject->dat += n; + subject->len -= n; + subject->cap -= n; + + return n; +} + +/** + * Clears out all data from the buffer + */ +int buf_clear(struct buf *subject) { + if (!subject) return BUF_ERRNO_MISSING_SUBJECT; + + if (subject->alloc) { + _free(subject->alloc); + subject->alloc = NULL; + } + + subject->dat = NULL; + subject->len = 0; + subject->cap = 0; + + return BUF_ERRNO_OK; +} + +#ifdef __cplusplus +} // extern "C" { +#endif // __cplusplus diff --git a/src/buf.h b/src/buf.h @@ -0,0 +1,52 @@ +#ifndef __FINWO_BUF_H__ +#define __FINWO_BUF_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stddef.h> + +struct buf { + char *alloc; + char *dat; + size_t len; + size_t cap; +}; + +/** + * Append <length> bytes to the data in the buffer + * Copies the data, does not mutate the source + * + * Returns an error code, or 0 on success + */ +int buf_append(struct buf *subject, const char *source, size_t length); + +/** + * Append single byte to the data in the buffer + * + * Returns an error code, or 0 on success + */ +int buf_append_byte(struct buf *subject, const char value); + +/** + * Remove <length> bytes from the front of the buffer + */ +size_t buf_consume(struct buf *subject, size_t length); + +/** + * Clears out all data from the buffer + */ +int buf_clear(struct buf *subject); + +/** + * Sets custom allocator instead of libc. This function, if needed, should be + * called only once at startup and prior to calling buf_* functions + */ +void buf_set_allocator(void *(*realloc)(void*,size_t), void (*free)(void*)); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __FINWO_BUF_H__ diff --git a/test/.gitignore b/test/.gitignore @@ -0,0 +1 @@ +/0000-basics diff --git a/test/0000-basics.c b/test/0000-basics.c @@ -0,0 +1,49 @@ +#include <stdlib.h> +#include <string.h> + +#include "assert.h" +#include "buf.h" + +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif + +void test_basic() { + struct buf *buf = malloc(sizeof(struct buf)); + buf_clear(buf); + + ASSERT("Allocation of buffer", buf != NULL); + ASSERT("Cleared buf has null alloc", buf->alloc == NULL); + ASSERT("Cleared buf has null data", buf->dat == NULL); + ASSERT("Cleared buf has 0 length", buf->len == 0); + ASSERT("Cleared buf has 0 capacity", buf->cap == 0); + + buf_append_byte(buf, 'H'); + + ASSERT("Appended buffer has alloc", buf->alloc != NULL); + ASSERT("Appended buffer has data", buf->dat != NULL); + ASSERT("Appended buffer has matching alloc and data", buf->dat == buf->alloc); + ASSERT("Appended buffer has correct length", buf->len == 1); + ASSERT("Appended buffer has correct capacity", buf->cap == 32); + + buf_append(buf, "ello World", 10); + + ASSERT("Appended buffer has matching alloc and data", buf->dat == buf->alloc); + ASSERT("Appended buffer has correct length", buf->len == 11); + ASSERT("Appended buffer has correct capacity", buf->cap == 32); + ASSERT("Appended buffer has correct data", strncmp(buf->dat, "Hello World", MIN(buf->len, 11)) == 0); + + buf_consume(buf, 6); + + ASSERT("Consumed buffer has matching alloc and data", buf->dat - buf->alloc == 6); + ASSERT("Consumed buffer has correct length", buf->len == 5); + ASSERT("Consumed buffer has correct capacity", buf->cap == 26); + ASSERT("Consumed buffer has correct data", strncmp(buf->dat, "World", MIN(buf->len, 5)) == 0); +} + +int main(int argc, const char *argv[]) { + buf_set_allocator(realloc, free); + + RUN(test_basic); + return TEST_REPORT(); +} diff --git a/test/assert.h b/test/assert.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