diff options
author | Linnnus <[email protected]> | 2024-02-01 22:59:38 +0100 |
---|---|---|
committer | Linnnus <[email protected]> | 2024-02-04 09:58:06 +0100 |
commit | d38f82f6462af4e5aad6a2c776f5c00ce5b13c87 (patch) | |
tree | 01a222792dfb10473ae4370b4fc90f3a48e1a499 /src |
feat: initial commit
Here is a small overview of the state of the project at this first
commit.
I have basic Git Repo -> HTML working, and a plan for how setting up an
actual server would work (mainly, NGINX + a git hook to rebuild).
The main thing I'm working on right now is parsing WikiCreole, though I
am starting to wonder if this is the right langauge. WikiCreole is
pretty irregular and has a lot of edge cases (e.g. around emphasis).
Diffstat (limited to 'src')
-rw-r--r-- | src/arena.c | 56 | ||||
-rw-r--r-- | src/arena.h | 43 | ||||
-rw-r--r-- | src/creole-test.c | 273 | ||||
-rw-r--r-- | src/creole.c | 111 | ||||
-rw-r--r-- | src/creole.h | 15 | ||||
-rw-r--r-- | src/die.c | 58 | ||||
-rw-r--r-- | src/die.h | 31 | ||||
-rw-r--r-- | src/main.c | 174 | ||||
-rw-r--r-- | src/strutil.c | 58 | ||||
-rw-r--r-- | src/strutil.h | 26 |
10 files changed, 845 insertions, 0 deletions
diff --git a/src/arena.c b/src/arena.c new file mode 100644 index 0000000..3782b79 --- /dev/null +++ b/src/arena.c @@ -0,0 +1,56 @@ +#include "arena.h" + +#include <assert.h> // assert +#include <stdint.h> // uintptr_t +#include <stdio.h> // fprintf +#include <stdlib.h> // abort, malloc +#include <stdnoreturn.h> // noreturn +#include <string.h> // memset + +static noreturn void arena_panic(const char *reason) { + fprintf(stderr, "Memory allocation failed: %s", reason); + abort(); +} + +struct arena arena_create(size_t capacity) { + struct arena arena = { + .root = malloc(capacity), + .capacity = capacity, + .used = 0, + }; + if (arena.root == NULL) { + arena_panic("cannot allocate system memory"); + } + return arena; +} + +void *arena_alloc(struct arena *arena, size_t size, size_t alignment, unsigned flags) { + // Alignment must be a power of two. + // See: https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 + assert(alignment != 0 && (alignment & (alignment - 1)) == 0); + size_t padding = -(uintptr_t)(arena->root + arena->used) & (alignment - 1); + + // If no more memory is available, we fall back to our error strategy. + assert(arena->capacity >= arena->used); + if (arena->used + padding + size > arena->capacity) { + if (flags & ARENA_NO_PANIC) { + return NULL; + } else { + arena_panic("out of preallocated memory"); + } + } + + // Reserve memory from arena. + void *ptr = arena->root + arena->used + padding; + arena->used += padding + size; + + if (~flags & ARENA_NO_ZERO) { + memset(ptr, 0, size); + } + + return ptr; +} + +void arena_destroy(struct arena *arena) { + free(arena->root); +} diff --git a/src/arena.h b/src/arena.h new file mode 100644 index 0000000..cd4aa17 --- /dev/null +++ b/src/arena.h @@ -0,0 +1,43 @@ +#ifndef ARENA_H +#define ARENA_H + +// +// This module defines a simple, fixed-size arena allocator. +// + +#include <stddef.h> // size_t + +struct arena { + void *root; + size_t capacity; + size_t used; +}; + +// Initialize an arena with the given `capacity`. +// Panics on failure to allocate. +struct arena arena_create(size_t capacity); + +// These flags control the behavior of `arena_alloc`. +#define ARENA_NO_ZERO 1 +#define ARENA_NO_PANIC 2 + +// Allocate `size` bytes in `arena`. +// The resulting memory is zeroed unless ARENA_NO_ZERO is passed. +// Unless ARENA_NO_PANIC is specified, the resulting pointer is always valid. +void *arena_alloc(struct arena *arena, size_t size, size_t alignment, unsigned flags); + +// Free the memory associated with the arena using the underlying allocator. +void arena_destroy(struct arena *arena); + +// +// The `new` macro makes the basic allocation case simple. It uses a bit of +// preprocessor magic to simulate default argument values. +// + +#define new(...) newx(__VA_ARGS__,new4,new3,new2)(__VA_ARGS__) +#define newx(a1, a2, a3, a4, a5, ...) a5 +#define new2(arena, typ) (typ *)arena_alloc(arena, sizeof(typ), _Alignof(typ), 0) +#define new3(arena, typ, count) (typ *)arena_alloc(arena, count * sizeof(typ), _Alignof(typ), 0) +#define new4(arena, typ, count, flags) (typ *)arena_alloc(arena, count * sizeof(typ), _Alignof(typ), flags) + +#endif diff --git a/src/creole-test.c b/src/creole-test.c new file mode 100644 index 0000000..18d24a2 --- /dev/null +++ b/src/creole-test.c @@ -0,0 +1,273 @@ +#include "creole.h" + +#include <assert.h> +#include <dirent.h> +#include <stdio.h> +#include <glob.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <errno.h> +//#include <ftw.h> + +#define RED "\x1b[31m" +#define GREEN "\x1b[32m" +#define YELLOW "\x1b[33m" +#define CYAN "\x1b[96m" +#define BOLD "\x1b[39;1m" +#define DIM "\x1b[39;2m" +#define CLEAR "\x1b[0m" + +#define LENGTH(a) (sizeof(a)/sizeof((a)[0])) + +#define CHUNK_SIZE 5 + +int read_file(const char *file_path, char **out_buffer, size_t *out_length) { + assert(out_buffer != NULL && *out_buffer == NULL); + assert(file_path != NULL); + + FILE *fp = fopen(file_path, "r"); + if (fp == NULL) { + return -1; + } + + char *buffer = NULL; + size_t allocated = 0; + size_t used = 0; + while (true) { + // Grow buffer, if needed. + if (used + CHUNK_SIZE > allocated) { + // Grow exponentially to guarantee O(log(n)) performance. + allocated = (allocated == 0) ? CHUNK_SIZE : allocated * 2; + + // Overflow check. Some ANSI C compilers may optimize this away, though. + if (allocated <= used) { + free(buffer); + fclose(fp); + errno = EOVERFLOW; + return -1; + } + + char *temp = realloc(buffer, allocated); + if (temp == NULL) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + buffer = temp; + } + + size_t nread = fread(buffer + used, 1, CHUNK_SIZE, fp); + if (nread == 0) { + // End-of-file or errnor has occured. + // FIXME: Should we be checking (nread < CHUNK_SIZE)? + // https://stackoverflow.com/a/39322170 + break; + } + used += nread; + } + + if (ferror(fp)) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + + // Reallocate to optimal size. + char *temp = realloc(buffer, used + 1); + if (temp == NULL) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + buffer = temp; + + // Null-terminate the buffer. Note that buffers may still contain \0, + // so strlen(buffer) == length may not be true. + buffer[used] = '\0'; + + // Return buffer. + *out_buffer = buffer; + if (out_length != NULL) { + *out_length = used; + } + fclose(fp); + return 0; +} + +// https://stackoverflow.com/a/779960 +char *replace(const char *orig, char *rep, char *with) { + assert(orig != NULL); + assert(rep != NULL); + + char *tmp; // varies + + int len_rep = strlen(rep); // length of rep (the string to remove) + if (len_rep == 0) { + errno = EINVAL; // empty rep causes infinite loop during count + return NULL; + } + + int len_with; // length of with (the string to replace rep with) + if (with == NULL) + with = ""; + len_with = strlen(with); + + // count the number of replacements needed + const char *ins; // the next insert point + int count; // number of replacements + ins = orig; + for (count = 0; (tmp = strstr(ins, rep)) != NULL; ++count) { + ins = tmp + len_rep; + } + + char *result; // the return string + tmp = result = malloc(strlen(orig) + (len_with - len_rep) * count + 1); + if (!result) { + return NULL; + } + + // first time through the loop, all the variable are set correctly + // from here on, + // tmp points to the end of the result string + // ins points to the next occurrence of rep in orig + // orig points to the remainder of orig after "end of rep" + while (count--) { + ins = strstr(orig, rep); + int len_front = ins - orig; + tmp = strncpy(tmp, orig, len_front) + len_front; + tmp = strcpy(tmp, with) + len_with; + orig += len_front + len_rep; // move to next "end of rep" + } + strcpy(tmp, orig); + return result; +} + +int print_escaped(FILE *fp, const char *string, size_t length) { + static struct { + char from; + const char *to; + } replacements[] = { + { '\t', "\\t" }, + { '\n', "\\n" }, + { '"', "\\\"" }, + }; + + if (fputc('"', fp) == EOF) { + return -1; + } + + for (size_t i = 0; i < length; ++i) { + for (size_t j = 0; j < LENGTH(replacements); ++j) { + if (string[i] == replacements[j].from) { + if (fprintf(fp, "%s", replacements[j].to) < 0) { + return -1; + } + goto next_char; + } + } + if (fputc(string[i], fp) == EOF) { + return -1; + } +next_char: + ; + } + + if (fputc('"', fp) == EOF) { + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) { + if (argc != 1) { + fprintf(stderr, "Usage: %s\n", argv[0]); + fprintf(stderr, "Takes no arguments, must be invoked in parent of test dir.\n"); + return EXIT_FAILURE; + } + + glob_t glob_result; + if (glob("./test/*.input.txt", GLOB_ERR, NULL, &glob_result) < 0) { + perror("Glob failed"); + return EXIT_FAILURE; + } + + unsigned ok = 0; + unsigned failures = 0; + unsigned errors = 0; + + for (int i = 0; i < glob_result.gl_matchc; ++i) { + char *input_name = glob_result.gl_pathv[i]; + + int input_name_len = strlen(input_name); + int prefix_len = strlen("./test/"); + int sufix_len = strlen(".input.txt"); + printf("Running: " BOLD "%.*s" CLEAR "... ", input_name_len - prefix_len - sufix_len, input_name + prefix_len); + + char *input_buffer = NULL; + size_t input_length; + if (read_file(input_name, &input_buffer, &input_length) < 0) { + printf(RED "internal error!" CLEAR "\n " CYAN "error:" CLEAR " reading %s: %s\n", input_name, strerror(errno)); + errors += 1; + goto fail_input_buffer; + } + + // TODO: replace() is a bit overkill. Just strcpy to buffer of size (strlen(input_name) - strlen(".input.txt") + strlen(".output.txt")). + char *output_name = replace(input_name, ".input.txt", ".output.txt"); + if (output_name == NULL) { + printf(RED "internal error!" CLEAR "\n " CYAN "error:" CLEAR " generating output name: %s\n", strerror(errno)); + errors += 1; + goto fail_output_name; + } + char *output_buffer = NULL; + size_t output_length; + if (read_file(output_name, &output_buffer, &output_length) < 0) { + printf(RED "internal error!" CLEAR "\n " CYAN "error:" CLEAR " reading %s: %s\n", output_name, strerror(errno)); + errors += 1; + goto fail_output_buffer; + } + + // Do actual render. + static char buffer[1024]; + FILE *fp = fmemopen(buffer, sizeof(buffer), "wb"); + render_creole(fp, input_buffer, input_length); + long buffer_length = ftell(fp); + fclose(fp); + + bool success = strcmp(output_buffer, buffer) == 0; + if (success) { + ok += 1; + printf(GREEN "ok" CLEAR "\n"); + } else { + failures += 1; + printf(RED "unexpected output!" CLEAR); + printf(CYAN "\n input: " CLEAR); + print_escaped(stdout, input_buffer, input_length); + printf(CYAN "\n want: " CLEAR); + print_escaped(stdout, output_buffer, output_length); + printf(CYAN"\n got: " CLEAR); + print_escaped(stdout, buffer, buffer_length); // TODO: rendered + putchar('\n'); + } + + free(output_buffer); +fail_output_buffer: + free(output_name); +fail_output_name: + free(input_buffer); +fail_input_buffer: + ; + } + + printf("Summary: " YELLOW "%u" CLEAR " errors, " RED "%u" CLEAR " failures and " GREEN "%u" CLEAR " successes\n", errors, failures, ok); + + globfree(&glob_result); + return (failures == 0 && errors == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/creole.c b/src/creole.c new file mode 100644 index 0000000..f69c543 --- /dev/null +++ b/src/creole.c @@ -0,0 +1,111 @@ +#include "creole.h" + +#include <assert.h> +#include <ctype.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> + +#define LENGTH(x) (sizeof(x)/sizeof((x)[0])) + +#define DEBUG(...) (fprintf(stderr, __VA_ARGS__), fflush(stderr)) + +void process(const char *begin, const char *end, bool new_block, FILE *out); +int do_headers(const char *begin, const char *end, bool new_block, FILE *out); + +// A parser takes a (sub)string and returns the number of characters consumed, if any. +// +// The parameter `new_block` determines whether `begin` points to the beginning of a new block. +// The sign of the return value determines whether a new block should begin, after the consumed text. +typedef int (* parser_t)(const char *begin, const char *end, bool new_block, FILE *out); + +static parser_t parsers[] = { do_headers }; + +int do_headers(const char *begin, const char *end, bool new_block, FILE *out) { + if (!new_block) { // Headers are block-level elements. + return 0; + } + + if (*begin != '=') { + return 0; + } + + unsigned level = 0; + while (*begin == '=') { + level += 1; + begin += 1; + } + DEBUG("level %d\n", level); + + while (isspace(*begin)) { + begin += 1; + } + + const char *stop = end; + while (stop + 1 != end && stop[1] != '\n') { + stop += 1; + } + while (*stop == '=') { + stop -= 1; + } + + fprintf(out, "<h%u>", level); + process(begin, stop, false, out); + fprintf(out, "</h%u>", level); + + return -(stop - begin); +} + +void process(const char *begin, const char *end, bool new_block, FILE *out) { + const char *p = begin; + while (p < end) { + // Eat all newlines if we're starting a block. + if (new_block) { + while (*p == '\n') { + p += 1; + if (p == end) { + return; + } + } + } + + // Greedily try all parsers. + int affected; + for (unsigned i = 0; i < LENGTH(parsers); ++i) { + DEBUG("%p\n", parsers[i]); + affected = parsers[i](p, end, new_block, out); + if (affected) { + break; + } + } + if (affected) { + p += abs(affected); + } else { + fputc(*p, out); + p += 1; + } + + if (p + 1 == end) { + // Don't print single newline at end. + if (*p == '\n') { + return; + } + } else { + // Determine whether we've reached a new block. + if (p[0] == '\n' && p[1] == '\n') { + // Double newline characters separate blocks; + // if we've found them, we're starting a new block + new_block = true; + } else { + // ...otherwise the parser gets to decide. + new_block = affected < 0; + } + } + } +} + +void render_creole(FILE *out, const char *source, size_t source_length) +{ + process(source, source + source_length, true, out); +} diff --git a/src/creole.h b/src/creole.h new file mode 100644 index 0000000..ac3e706 --- /dev/null +++ b/src/creole.h @@ -0,0 +1,15 @@ +#ifndef CREOLE_H +#define CREOLE_H + +// Defines a module for rendering Wiki Creole [1] to a file. This functionality +// of this module is based on the formal grammar [2] of Wiki Creole. +// +// [1]: http://www.wikicreole.org/wiki/Home +// [2]: http://www.wikicreole.org/wiki/EBNFGrammarForWikiCreole1.0 + +#include <stddef.h> // size_t +#include <stdio.h> // FILE + +void render_creole(FILE *out, const char *source, size_t length); + +#endif diff --git a/src/die.c b/src/die.c new file mode 100644 index 0000000..529eb9b --- /dev/null +++ b/src/die.c @@ -0,0 +1,58 @@ +#include "die.h" + +#include <assert.h> // assert +#include <stdio.h> // fprintf, vfprintf, fputc, stderr +#include <stdlib.h> // exit, EXIT_FAILURE +#include <stdarg.h> // va_* +#include <git2.h> // git_* +#include <string.h> // strerror +#include <errno.h> // errno + +void die(const char *msg, ...) +{ + va_list ap; + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + + fputc('\n', stderr); + +#ifndef NDEBUG + git_libgit2_shutdown(); +#endif + exit(EXIT_FAILURE); +} + +// Die but include the last git error. +void noreturn die_git(const char *msg, ...) +{ + va_list ap; + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + + const git_error *e = git_error_last(); + assert(e != NULL && "die_git called without error"); + fprintf(stderr, ": %s\n", e->message); + +#ifndef NDEBUG + git_libgit2_shutdown(); +#endif + exit(EXIT_FAILURE); +} + +// Die but include errno information. +void noreturn die_errno(const char *msg, ...) +{ + va_list ap; + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + + fprintf(stderr, ": %s\n", strerror(errno)); + +#ifndef NDEBUG + git_libgit2_shutdown(); +#endif + exit(EXIT_FAILURE); +} diff --git a/src/die.h b/src/die.h new file mode 100644 index 0000000..891d10f --- /dev/null +++ b/src/die.h @@ -0,0 +1,31 @@ +#ifndef DIE_H +#define DIE_H + +// +// This module defines various utilities for ending program execution +// abnormally. +// + +#include <stdnoreturn.h> // noreturn + +#ifdef __GNUC__ +#define _DIE_PRINTF_ATTR __attribute__((format(printf, 1, 2))) +#else +#define _DIE_PRINTF_ATTR +#endif + +// Exit the program, displaying no extra information. +_DIE_PRINTF_ATTR +noreturn void die(const char *msg, ...); + +// Exit the program, displaying the last libgit error. +// It is an error to invoke this if there has been no libgit error. +_DIE_PRINTF_ATTR +noreturn void die_git(const char *msg, ...); + +// Exit the program, displaying errno message. +// It is NOT an error to invoke this if errno is 0, just pretty weird. +_DIE_PRINTF_ATTR +noreturn void die_errno(const char *msg, ...); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..a6b438f --- /dev/null +++ b/src/main.c @@ -0,0 +1,174 @@ +#include "arena.h" +#include "die.h" +#include "strutil.h" +#include "creole.h" + +// #include <assert.h> +#include <errno.h> // errno, EEXIST +#include <git2.h> // git_* +#include <stdbool.h> // false +#include <stdio.h> +#include <stdlib.h> // EXIT_SUCCESS +#include <sys/stat.h> // mkdir +#include <sys/types.h> // mode_t + +void xmkdir(const char *path, mode_t mode, bool exist_ok) { + if (mkdir(path, mode) < 0) { + if (exist_ok && errno == EEXIST) { + return; + } else { + die_errno("failed to mkdir %s", path); + } + } +} + +void process_other_file(const char *path, const char *source, size_t source_len) { + FILE *out = fopen(path, "w"); + if (out == NULL) { + die_errno("failed to open %s for writing", path); + } + if (fwrite(source, 1, source_len, out) < source_len) { + die_errno("failed to write content to %s\n", path); + } + fclose(out); +} + +void process_markup_file(const char *path, const char *source, size_t source_len) { + FILE *out = fopen(path, "w"); + if (out == NULL) { + die_errno("failed to open %s for writing", path); + } + int status = render_creole(out, source, source_len); + if (status != 0) { + fprintf(stderr, "warning: failed to parse: %s (status %d)\n", path, status); + } + fclose(out); +} + +void process_dir(const char *path) { + xmkdir(path, 0755, false); +} + +void list_tree(struct arena *a, struct git_repository *repo, struct git_tree *tree, const char *prefix) { + // Grab a snapshot of the arena. + // All memory allocated within the arena in this subcalltree will be freed. + // This is effectively the same as allocating a new arena for each call to list_tree. + struct arena snapshot = *a; + + size_t tree_count = git_tree_entrycount(tree); + for (size_t i = 0; i < tree_count; ++i) { + // Read the entry. + const struct git_tree_entry *entry; + if ((entry = git_tree_entry_byindex(tree, i)) == NULL) { + die("read tree item"); + } + + // Construct path to entry. + const char *entry_out_path = joinpath(a, prefix, git_tree_entry_name(entry)); + printf("Generating: %s\n", entry_out_path); + + // entry->obj fail on submodules. just ignore them. + struct git_object *obj; + if (git_tree_entry_to_object(&obj, repo, entry) == 0) { + git_object_t type = git_object_type(obj); + switch (type) { + case GIT_OBJECT_BLOB: { + struct git_blob *blob = (struct git_blob *)obj; + const char *source = git_blob_rawcontent(blob); + if (source == NULL) { + die_git("get source for blob %s", git_oid_tostr_s(git_object_id(obj))); + } + size_t source_len = git_blob_rawsize(blob); + if (endswith(entry_out_path, ".md") && !git_blob_is_binary(blob)) { + process_markup_file(entry_out_path, source, source_len); + } else { + process_other_file(entry_out_path, source, source_len); + } + git_object_free(obj); + } break; + case GIT_OBJECT_TREE: { + process_dir(entry_out_path); + list_tree(a, repo, (struct git_tree *)obj, entry_out_path); + git_object_free(obj); + } break; + default: { + // Ignore whatever weird thing this is. + git_object_free(obj); + } break; + } + } + } + + // Restore snapshot. + *a = snapshot; +} + +int main(int argc, char *argv[]) +{ + if (argc != 3) { + die("Usage: %s git-path out-path", argv[0]); + } + char *git_path = argv[1]; + char *out_path = argv[2]; + + // Initialize libgit. Note that calling git_libgit2_shutdown is not + // necessary, as per this snippet from the documentation: + // + // > Usually you don’t need to call the shutdown function as the operating + // > system will take care of reclaiming resources, but if your + // > application uses libgit2 in some areas which are not usually active, + // > you can use + // + // That's good news! + if (git_libgit2_init() < 0) { + die_git("initialize libgit"); + } + + // Do not search outside the git repository. GIT_CONFIG_LEVEL_APP is the highest level currently. + // for (int i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) { + // if (git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "") < 0) { + // die_git("set search path"); + // } + // } + + // Don't require the repository to be owned by the current user. + git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); + + struct git_repository *repo; + if (git_repository_open_ext(&repo, git_path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { + die_git("open repository"); + } + + // Find HEAD. + struct git_object *head; + const struct git_oid *head_id; + if (git_revparse_single(&head, repo, "HEAD") < 0) { + die_git("parse HEAD"); + } + head_id = git_object_id(head); + git_object_free(head); + + // Get a handle to the tree at head. + struct git_commit *commit; + if (git_commit_lookup(&commit, repo, head_id) < 0) { + die_git("look up head commit"); + } + struct git_tree *tree; + if (git_commit_tree(&tree, commit) < 0) { + die_git("get tree for commit %s", git_oid_tostr_s(git_commit_id(commit))); + } + + // Create the initial output directory. + xmkdir(out_path, 0755, true); + + struct arena a = arena_create(1024); + list_tree(&a, repo, tree, out_path); +#ifndef NDEBUG + arena_destroy(&a); +#endif + +#ifndef NDEBUG + git_libgit2_shutdown(); +#endif + return EXIT_SUCCESS; +} diff --git a/src/strutil.c b/src/strutil.c new file mode 100644 index 0000000..9192989 --- /dev/null +++ b/src/strutil.c @@ -0,0 +1,58 @@ +#include "strutil.h" + +#include "arena.h" // struct arena, new +#include <assert.h> // assert +#include <stdarg.h> // va_* +#include <stdbool.h> // bool, false +#include <stdio.h> // vsnprintf +#include <string.h> // strlen, strncmp + +int aprintf(struct arena *a, char **out, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vaprintf(a, out, fmt, ap); + va_end(ap); + return ret; +} + +int vaprintf(struct arena *a, char **out, const char *fmt, va_list args) { + // Calculate size. + va_list tmp; + va_copy(tmp, args); + int size = vsnprintf(NULL, 0, fmt, args); + va_end(tmp); + + // If e.g. the format string was broken, we cannot continue. + if (size < 0) { + return -1; + } + + // Arena allocation cannot fail. + *out = new(a, char, size + 1); + + int t = vsnprintf(*out, size + 1, fmt, args); + assert(t == size); + + return size; +} + +char *joinpath(struct arena *a, const char *path_a, const char *path_b) { + char *out; + int ret = aprintf(a, &out, "%s/%s", path_a, path_b); + assert(ret > 0 && "should be infallible"); + return out; +} + +bool endswith(const char *haystack, const char *needle) { + assert(haystack != NULL); + assert(needle != NULL); + + size_t haystack_len = strlen(haystack); + size_t needle_len = strlen(needle); + + if (needle_len > haystack_len) { + return false; + } + + return strncmp(haystack + (haystack_len - needle_len), needle, needle_len) == 0; +} diff --git a/src/strutil.h b/src/strutil.h new file mode 100644 index 0000000..03f8294 --- /dev/null +++ b/src/strutil.h @@ -0,0 +1,26 @@ +#ifndef STRUTIL_H +#define STRUTIL_H + +// +// Defines various utilities for working with strings. +// + +#include "arena.h" // struct arena +#include <stdbool.h> // bool +#include <stdarg.h> // va_list + +// Like asprintf except the allocation is made inside the given arena. +// Panics on allocation failure. +int aprintf(struct arena *a, char **out, const char *fmt, ...); + +// Same as aprintf, except takes a varargs list. +int vaprintf(struct arena *a, char **out, const char *fmt, va_list args); + +// Join the two paths with a directory separator. +// Result is allocated in arena. +char *joinpath(struct arena *a, const char *path_a, const char *path_b); + +// Returns boolean indicating if `haystack` ends with `needle`. +bool endswith(const char *haystack, const char *needle); + +#endif |