summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/arena.c56
-rw-r--r--src/arena.h43
-rw-r--r--src/creole-test.c273
-rw-r--r--src/creole.c111
-rw-r--r--src/creole.h15
-rw-r--r--src/die.c58
-rw-r--r--src/die.h31
-rw-r--r--src/main.c174
-rw-r--r--src/strutil.c58
-rw-r--r--src/strutil.h26
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