commit 867af93a9afeb2e9d2a6f240483852b3086df9d1
Author: finwo <finwo@pm.me>
Date: Sat, 18 Jul 2026 21:35:43 +0200
Project init
Diffstat:
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