From d102ba8676b6980672b630e77e87a9af36bf6b3a Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Fri, 22 Jul 2022 20:19:12 -0400 Subject: [PATCH] Initial revision --- .cvsignore | 1 + .idea/Telodendria.iml | 8 + .idea/modules.xml | 8 + .idea/workspace.xml | 52 ++++ Telodendria.css | 62 +++++ Telodendria.html | 541 +++++++++++++++++++++++++++++++++++++++ contrib/httpd.conf | 9 + contrib/telodendria.conf | 13 + make.sh | 57 +++++ src/Array.c | 174 +++++++++++++ src/Base64.c | 212 +++++++++++++++ src/Config.c | 452 ++++++++++++++++++++++++++++++++ src/HashMap.c | 300 ++++++++++++++++++++++ src/Log.c | 310 ++++++++++++++++++++++ src/Telodendria.c | 157 ++++++++++++ src/include/Array.h | 32 +++ src/include/Base64.h | 25 ++ src/include/Config.h | 37 +++ src/include/HashMap.h | 85 ++++++ src/include/Http.h | 96 +++++++ src/include/Json.h | 69 +++++ src/include/Log.h | 63 +++++ 22 files changed, 2763 insertions(+) create mode 100644 .cvsignore create mode 100644 .idea/Telodendria.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/workspace.xml create mode 100644 Telodendria.css create mode 100644 Telodendria.html create mode 100644 contrib/httpd.conf create mode 100644 contrib/telodendria.conf create mode 100644 make.sh create mode 100644 src/Array.c create mode 100644 src/Base64.c create mode 100644 src/Config.c create mode 100644 src/HashMap.c create mode 100644 src/Log.c create mode 100644 src/Telodendria.c create mode 100644 src/include/Array.h create mode 100644 src/include/Base64.h create mode 100644 src/include/Config.h create mode 100644 src/include/HashMap.h create mode 100644 src/include/Http.h create mode 100644 src/include/Json.h create mode 100644 src/include/Log.h diff --git a/.cvsignore b/.cvsignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.cvsignore @@ -0,0 +1 @@ +build diff --git a/.idea/Telodendria.iml b/.idea/Telodendria.iml new file mode 100644 index 0000000..bc2cd87 --- /dev/null +++ b/.idea/Telodendria.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..857f442 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..997c9ec --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + { + "keyToString": { + "RunOnceActivity.OpenProjectViewOnStart": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.cidr.known.project.marker": "true", + "WebServerToolWindowFactoryState": "false", + "cf.first.check.clang-format": "false", + "cidr.known.project.marker": "true" + } +} + + + + + 1658323016759 + + + + + + \ No newline at end of file diff --git a/Telodendria.css b/Telodendria.css new file mode 100644 index 0000000..0519a73 --- /dev/null +++ b/Telodendria.css @@ -0,0 +1,62 @@ +body { + margin: auto; + max-width: 8.5in; + padding: 0.25in; +} +.code { + background-color: #eee; + border-radius: 5px; + display: block; + padding: 0.5em 1em 1.5em 1em; + + font-family: monospace; + white-space: pre; + overflow-x: scroll; +} +kbd { + background-color: #eee; + border-radius: 3px; + border: 1px solid #b4b4b4; + box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset; + color: #333; + display: inline-block; + font-size: .85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} +h1, h4, h5, h6 { + border-bottom: 1px dashed gray; +} +h4, h5, h6 { + width: fit-content; +} +a { + color: #0969da; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +table { + width: 100%; + border-collapse: collapse; +} +td, th { + border: 1px solid #eee; + text-align: left; + padding: 8px; +} +tr:nth-child(even) { + background-color: #eee; +} +@media (prefers-color-scheme: dark) { + body { + background-color: #212121; + color: white; + } + .code, tr:nth-child(even) { + background-color: #333333; + } +} \ No newline at end of file diff --git a/Telodendria.html b/Telodendria.html new file mode 100644 index 0000000..61bd78f --- /dev/null +++ b/Telodendria.html @@ -0,0 +1,541 @@ + + + + + + + + + + + + + + + + +Telodendria | A Matrix Homeserver written in ANSI C. + + +

Telodendria

+

+Telodendria: The terminal branches of an axon. +

+

+Note: Telodendria is under heavy development. +Please see the Project Status. +

+

+Telodendria is a Matrix homeserver implementation written from +scratch in ANSI C. It is designed to be lightweight and simple, yet +functional. Telodendria differentiates itself from other Matrix +homeserver implementations because it: +

+

+Telodendria is on Matrix! Check out the official Matrix rooms: +

+ + + + + + + + + + + + + + + + + + + + + +
RoomDescription
+#telodendria-releases:bancino.net + +Get notified of new releases. +
+#telodendria-general:bancino.net + +General discussion and support for Telodendria. +
+#telodendria-issues:bancino.net + +Report issues with Telodendria. +
+#telodendria-patches:bancino.net + +Submit code patches to the Telodendria project. +
+

Table of Contents

+ +

Getting Started

+

Install Telodendria

+

+If your operating system has an official package or port of +Telodendria, you should prefer to use that. If your operating +system's package or port is too out of date for your tastes, please +contact the package's maintainers to notify them, or offer to update +the package yourself. +

+

+If your operating system does not have an official package, see below +for instructions on building from source and use them to create one. +

+

OpenBSD

+

+Telodendria is available in the ports tree and as a binary +package. You can install it with the following command: +

+
+$ pkg_add telodendria +
+

Building From Source

+

+Telodendria is designed to be light enough that it can be built +from source on just about any operating system. It only has the +following requirements, all of which should be already available to +you on a sufficiently complete operating system: +

+ +
+$ ./make.sh +
+

+If everything went well, that will produce +telodendria.cgi, which you can then place under your web +root and configure your web server to execute. You'll need to make sure +/.well-known/matrix and /_matrix and all the +paths under them actually execute telodendria.cgi. See the +provided OpenBSD httpd.conf for reference. Even if you +aren't using OpenBSD's httpd(8), you should find its +configuration syntax simple enough to adequately demonstrate the proper +configuration. +

+

Configure Telodendria

+

+Once you get Telodendria built and hooked into your web server, +you will have to write a configuration file for it. The configuration +file is just JSON, and it should be called +Telodendria.json. +

+

Project Status

+

+Telodendria is a very ambitious project. There's a lot that needs +to happen yet before it is even remotely usable. At the moment, there's +nothing that even remotely resembles a Matrix homeserver here; we're still +getting off the ground and building a foundation. +

+

+Just because there's nothing here yet doesn't mean you should go away +though! We desparately need help, so you are more than welcome to help +out if you want things to go quicker. Please see the +Contributing section for details on how you +can get involved. +

+

Phase 1: Getting Off The Ground

+ +

Phase 2: Building A Foundation

+ +

Phase 3: Welcome To Matrix

+ +

Phase 4: A Real Homeserver

+ +

Documentation Status

+

+This documentation needs just a little work. Here's the things +on my list for that: +

+ +

Rationale

+

+This section explains +

+

+I want a lightweight Matrix homeserver designed for OpenBSD. I want a +homeserver that can be developed in vi(1) and compiled +with a C compiler. I want it to function entirely on a base OpenBSD +install without having to install any extra packages whatsoever. I've +found that the existing homeserver implementations are way +over-engineered and written in such a way that many programs and +libraries have to be pulled in to use them. I also want to learn how +Matrix works, and I want to understand the code I'm running on my +server. +

+

+So I wrote Telodendria. +

+

+Telodendria is written entirely in portable ANSI C. It depends on no +third-party C libraries other than the standard C library. The only +thing you need to run it is a web server that supports executing CGI +programs, and a directory that data can be written to. Everything +Telodendria needs to run itself is compiled into a single static +binary, and the source code can be built anywhere, right out of the +box. +

+

+Telodendria doesn't use a database like all the other homeservers. +Instead, it operates more like email: it uses a flat-file data +structure similar to maildir to store data. The advantage of this is +that it saves server maintainers from also having to maintain a +database. It greatly simplifies the process of getting a Matrix +homeserver up and running, and it makes it highly portable. It also is +extremely easy to back up and restore with base tools; just +tar(1) up the directory, and you're good to go. +

+

+Telodendria is developed and tested on OpenBSD, but you'll find that it +should run under any web server that supports CGI. I chose to write +Telodendria as a CGI program because anyone running an existing Matrix +server is likely running a web server acting as a reverse proxy in +front of it anyway, so why not just hook the homeserver directly into +the web server? That's one less daemon to run, which means memory and +CPU savings. CGI also allows Telodendria to remain single-threaded. +Each request that comes in is handled as its own process, and +operations are entirely isolated. +

+

Project Goals

+

+The goals of this project are as follows: +

+ +

Getting Support

+

+Telodendria is designed to be fairly straightforward, but that +doesn't mean there won't be hiccups along the way. If you are struggling +to get Telodendria up and running, you're more than welcome to +reach out for support. Just join the +#telodendria-general:bancino.net Matrix channel. Before +you do though, make sure you're running the latest version of +Telodendria and you've thoroughly read through all the +relevant documentation. +

+

Contributing

+

+Telodendria is an open source project. As such, it welcomes +contributions. There are many ways you can contribute, and any way you +can is greatly appreciated. +

+

Reporting Issues

+

+If—after you've reached out to +#telodendria-general:bancino.net—it has been +determined that there is a problem with Telodendria, it should +be reported to #telodendria-issues:bancino.net. There it +can be discussed further. The issues channel serves as the official +issue tracker of Telodendria; although issues may be copied +into a TODO file in the CVS repository just so they +don't get lost. +

+

Developing

+

+The primary language used to write Telodendria code is ANSI C. +Yes, that's the original C standard from 1989. The reason this standard +is chosen, and the reason that it will not be changed, is because the +original C is the most portable. Other languages you'll find in the +Telodendria repository are shell scripts and HTML. If you have +any experience at all with any of these languages, your contributions +are valuable. Please follow the guidelines in this section to ensure +the contribution workflow goes as smoothly as possible. +

+

Getting The Code

+

+There are multiple ways to get the source code for Telodendria. +You can download an official release tarball from +here if you would like, +but the preferred way is to check out the source code from CVS. This +makes generating patches a lot easier. If you do not have CVS, consult +your operating system's package repository to install it. CVS was the +chosen version control system for this project primarily because it is +built into OpenBSD. +

+
+$ export CVSROOT=anoncvs@bancino.net:/cvs +$ cvs checkout Telodendria +$ cd Telodendria +
+

+You should now have the latest Telodendria source code. Follow +the Code Style as you make your changes. +

+

Code Style

+

+Telodendria's code style is very unique. In general, these are +the conventions used by the code base. +

+ +

+This guide may be subject to change. The source code is the absolute +source of truth, so as long as you make your code look like the +code surrounding it, you should be fine. +

+

Submitting Patches

+

+Submitting patches is fairly easy to do if you've got the CVS sources +checked out. Once you have made your changes, just run +cvs diff: +

+
+$ cvs diff -uNp > your-changes.patch +
+

+Then, send the resulting patches to +#telodendria-patches:bancino.net, where they will be +promptly reviewed by the community. +

+

License

+

+All of the code and documentation for Telodendria is licensed +under the following terms and conditions: +

+
+Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE 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 AUTHORS OR COPYRIGHT HOLDERS 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. +
+

Change Log

+

+At this time, Telodendria does not have any tagged releases because it +is not yet functional as a Matrix homeserver. Please check out the Project Status to see where things are +currently at. +

+ + diff --git a/contrib/httpd.conf b/contrib/httpd.conf new file mode 100644 index 0000000..bc1297f --- /dev/null +++ b/contrib/httpd.conf @@ -0,0 +1,9 @@ +# +# httpd.conf: An OpenBSD httpd(8) configuration file for running +# Telodendria. Note that this is a development configuration that +# should be adapted using the httpd.conf(5) man page for production +# use. +# +server "matrix" { + listen on localhost port http +} diff --git a/contrib/telodendria.conf b/contrib/telodendria.conf new file mode 100644 index 0000000..8d32a41 --- /dev/null +++ b/contrib/telodendria.conf @@ -0,0 +1,13 @@ +# Telodendria configuration file + +log "/var/log/telodendria.log" { + level "message"; + timestampFormat "none"; + color "true"; +}; + +threads "4"; + +data-dir "/var/telodendria"; + +federation "true"; \ No newline at end of file diff --git a/make.sh b/make.sh new file mode 100644 index 0000000..dc24dd2 --- /dev/null +++ b/make.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env sh + +TELODENDRIA_VERSION="0.0.1" + +HEADERS="-D_POSIX_C_SOURCE=199506L -DTELODENDRIA_VERSION=\"$TELODENDRIA_VERSION\"" +INCLUDES="-Isrc/include" + +CC="${CC:-cc}" +CFLAGS="-Wall -Werror -pedantic -std=c89 -O3 $HEADERS $INCLUDES" +LDFLAGS="-static -flto -fdata-sections -ffunction-sections -s -Wl,-static -Wl,-gc-sections" +PROG="telodendria" + +mod_time() { + if [ -n "$1" ] && [ -f "$1" ]; then + case "$(uname)" in + Linux) + stat -c %Y "$1" + ;; + *BSD) + stat -f %m "$1" + ;; + *) + echo "0" + ;; + esac + else + echo "0" + fi +} + +mkdir -p build + +do_rebuild=0 +objs="" +for src in $(find src -name '*.c'); do + obj=$(echo "$src" | sed -e 's/^src/build/' -e 's/\.c$/\.o/') + objs="$objs $obj" + + if [ $(mod_time "$src") -gt $(mod_time "$obj") ]; then + echo "CC $obj" + obj_dir=$(dirname "$obj") + mkdir -p "$obj_dir" + if ! $CC $CFLAGS -c -o "$obj" "$src"; then + exit 1 + fi + do_rebuild=1 + fi +done + +if [ $do_rebuild -eq 1 ] || [ ! -f "build/$PROG" ]; then + echo "LD build/$PROG" + $CC $LDFLAGS -o "build/$PROG" $objs +else + echo "Up to date." +fi + +ls -lh "build/$PROG" diff --git a/src/Array.c b/src/Array.c new file mode 100644 index 0000000..25e8db6 --- /dev/null +++ b/src/Array.c @@ -0,0 +1,174 @@ +#include + +#ifndef ARRAY_BLOCK +#define ARRAY_BLOCK 16 +#endif + +#include +#include + +struct Array { + void **entries; /* An array of void pointers, to store any data */ + size_t allocated; /* Elements allocated on the heap */ + size_t size; /* Elements actually filled */ +}; + +int +ArrayAdd(Array *array, void *value) +{ + if (!array) + { + return 0; + } + + return ArrayInsert(array, value, array->size); +} + +Array * +ArrayCreate(void) +{ + Array *array = malloc(sizeof(Array)); + + if (!array) + { + return NULL; + } + + array->size = 0; + array->allocated = ARRAY_BLOCK; + array->entries = malloc(sizeof(void *) * ARRAY_BLOCK); + + if (!array->entries) + { + free(array); + return NULL; + } + + return array; +} + +void * +ArrayDelete(Array *array, size_t index) +{ + size_t i; + void *element; + + if (!array) + { + return NULL; + } + + element = array->entries[index]; + + for (i = index; i < array->size - 1; i++) + { + array->entries[i] = array->entries[i + 1]; + } + + array->size--; + + return element; +} + +void +ArrayFree(Array *array) +{ + if (array) + { + free(array->entries); + free(array); + } +} + +void * +ArrayGet(Array *array, size_t index) +{ + if (!array) + { + return NULL; + } + + if (index >= array->size) + { + return NULL; + } + + return array->entries[index]; +} + + +extern int +ArrayInsert(Array *array, void *value, size_t index) +{ + size_t i; + + if (!array || !value || index > array->size) + { + return 0; + } + + if (array->size >= array->allocated) + { + void **tmp; + size_t newSize = array->allocated + ARRAY_BLOCK; + + tmp = array->entries; + + array->entries = realloc(array->entries, + sizeof(void *) * newSize); + + if (!array->entries) + { + array->entries = tmp; + return 0; + } + + array->allocated = newSize; + } + + for (i = array->size; i > index; i--) + { + array->entries[i] = array->entries[i - 1]; + } + + array->size++; + + array->entries[index] = value; + + return 1; +} + +size_t +ArraySize(Array *array) +{ + if (!array) + { + return 0; + } + + return array->size; +} + +int +ArrayTrim(Array *array) +{ + void **tmp; + + if (!array) + { + return 0; + } + + tmp = array->entries; + + array->entries = realloc(array->entries, + sizeof(void *) * array->size); + + if (!array->entries) + { + array->entries = tmp; + return 0; + } + + return 1; +} diff --git a/src/Base64.c b/src/Base64.c new file mode 100644 index 0000000..12b60aa --- /dev/null +++ b/src/Base64.c @@ -0,0 +1,212 @@ +#include + +#include + +static const char Base64EncodeMap[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static const int Base64DecodeMap[] = { + 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51 +}; + +size_t +Base64EncodedSize(size_t inputSize) +{ + size_t size = inputSize; + + if (inputSize % 3) + { + size += 3 - (inputSize % 3); + } + + size /= 3; + size *= 4; + + return size; +} + +size_t +Base64DecodedSize(const char *base64, size_t len) +{ + size_t ret; + size_t i; + + if (!base64) + { + return 0; + } + + ret = len / 4 * 3; + + for (i = len; i > 0; i--) { + if (base64[i] == '=') { + ret--; + } else { + break; + } + } + + return ret; +} + +char * +Base64Encode(const char *input, size_t len) +{ + char *out; + size_t outLen; + size_t i, j, v; + + if (!input || !len) + { + return NULL; + } + + outLen = Base64EncodedSize(len); + out = malloc(outLen + 1); + if (!out) + { + return NULL; + } + out[outLen] = '\0'; + + for (i = 0, j = 0; i < len; i += 3, j += 4) + { + v = input[i]; + v = i + 1 < len ? v << 8 | input[i + 1] : v << 8; + v = i + 2 < len ? v << 8 | input[i + 2] : v << 8; + + out[j] = Base64EncodeMap[(v >> 18) & 0x3F]; + out[j + 1] = Base64EncodeMap[(v >> 12) & 0x3F]; + + if (i + 1 < len) + { + out[j + 2] = Base64EncodeMap[(v >> 6) & 0x3F]; + } else { + out[j + 2] = '='; + } + if (i + 2 < len) { + out[j + 3] = Base64EncodeMap[v & 0x3F]; + } else { + out[j + 3] = '='; + } + } + + return out; +} + +static int +Base64IsValidChar(char c) +{ + return (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == '+') || + (c == '/') || + (c == '='); +} + +char * +Base64Decode(const char *input, size_t len) +{ + size_t i, j; + int v; + size_t outLen; + char *out; + + if (!input) + { + return NULL; + } + + outLen = Base64DecodedSize(input, len); + if (len % 4) + { + /* Invalid length; must have incorrect padding */ + return NULL; + } + + /* Scan for invalid characters. */ + for (i = 0; i < len; i++) + { + if (!Base64IsValidChar(input[i])) + { + return NULL; + } + } + + out = malloc(outLen + 1); + if (!out) + { + return NULL; + } + + out[outLen] = '\0'; + + for (i = 0, j = 0; i < len; i += 4, j += 3) { + v = Base64DecodeMap[input[i] - 43]; + v = (v << 6) | Base64DecodeMap[input[i + 1] - 43]; + v = input[i + 2] == '=' ? v << 6 : (v << 6) | Base64DecodeMap[input[i + 2] - 43]; + v = input[i + 3] == '=' ? v << 6 : (v << 6) | Base64DecodeMap[input[i + 3] - 43]; + + out[j] = (v >> 16) & 0xFF; + if (input[i + 2] != '=') + out[j + 1] = (v >> 8) & 0xFF; + if (input[i + 3] != '=') + out[j + 2] = v & 0xFF; + } + + return out; +} + +extern void +Base64Unpad(char *base64, size_t length) +{ + if (!base64) + { + return; + } + + while (base64[length - 1] == '=') + { + length--; + } + + base64[length] = '\0'; +} + +extern int +Base64Pad(char **base64Ptr, size_t length) +{ + char *tmp; + size_t newSize; + size_t i; + + if (length % 4 == 0) + { + return length; /* Success: no padding needed */ + } + + newSize = length + (4 - (length % 4)); + + tmp = realloc(*base64Ptr, newSize + 100);; + if (!tmp) + { + return 0; /* Memory error */ + } + *base64Ptr = tmp; + + for (i = length; i < newSize; i++) + { + (*base64Ptr)[i] = '='; + } + + (*base64Ptr)[newSize] = '\0'; + + return newSize; +} + diff --git a/src/Config.c b/src/Config.c new file mode 100644 index 0000000..57b06b2 --- /dev/null +++ b/src/Config.c @@ -0,0 +1,452 @@ +#include + +#include +#include +#include + +#ifndef CONFIG_BUFFER_BLOCK +#define CONFIG_BUFFER_BLOCK 32 +#endif + +struct ConfigDirective { + Array *values; + HashMap *children; +}; + +struct ConfigParseResult { + unsigned int ok : 1; + union { + size_t lineNumber; + HashMap *confMap; + } data; +}; + +typedef enum ConfigToken { + TOKEN_UNKNOWN, + TOKEN_NAME, + TOKEN_MACRO_ASSIGNMENT, + TOKEN_VALUE, + TOKEN_SEMICOLON, + TOKEN_BLOCK_OPEN, + TOKEN_BLOCK_CLOSE, + TOKEN_MACRO, + TOKEN_EOF +} ConfigToken; + +typedef struct ConfigParserState { + FILE *stream; + unsigned int line; + + char *token; + size_t tokenSize; + size_t tokenLen; + ConfigToken tokenType; + + HashMap *macroMap; + +} ConfigParserState; + +unsigned int +ConfigParseResultOk(ConfigParseResult *result) +{ + return result ? result->ok : 0; +} + +size_t +ConfigParseResultLineNumber(ConfigParseResult *result) +{ + return result && !result->ok ? result->data.lineNumber : 0; +} + +HashMap * +ConfigParseResultGet(ConfigParseResult *result) +{ + return result && result->ok ? result->data.confMap : NULL; +} + +void +ConfigParseResultFree(ConfigParseResult *result) +{ + /* + * Note that if the parse was valid, the hash map + * needs to be freed separately. + */ + free(result); +} + +Array * +ConfigValuesGet(ConfigDirective *directive) +{ + return directive ? directive->values : NULL; +} + +HashMap * +ConfigChildrenGet(ConfigDirective *directive) +{ + return directive ? directive->children : NULL; +} + +/* + * Takes a void pointer because it is only used with + * HashMapIterate(), which requires a pointer to a function + * that takes a void pointer. + */ +static void +ConfigDirectiveFree(void *ptr) +{ + ConfigDirective *directive = ptr; + size_t i; + + if (!directive) + { + return; + } + + for (i = 0; i < ArraySize(directive->values); i++) + { + free(ArrayGet(directive->values, i)); + } + + ArrayFree(directive->values); + ConfigFree(directive->children); + + free(directive); +} + +void +ConfigFree(HashMap *conf) +{ + HashMapIterate(conf, ConfigDirectiveFree); + HashMapFree(conf); +} + +static ConfigParserState * +ConfigParserStateCreate(FILE * stream) +{ + ConfigParserState *state = malloc(sizeof(ConfigParserState)); + if (!state) + { + return NULL; + } + + state->macroMap = HashMapCreate(); + + if (!state->macroMap) + { + free(state); + return NULL; + } + + state->stream = stream; + state->line = 1; + state->token = NULL; + state->tokenSize = 0; + state->tokenLen = 0; + state->tokenType = TOKEN_UNKNOWN; + + return state; +} + +static void +ConfigParserStateFree(ConfigParserState *state) +{ + if (!state) + { + return; + } + + free(state->token); + + HashMapIterate(state->macroMap, free); + HashMapFree(state->macroMap); + + free(state); +} + +static int +ConfigIsNameChar(int c) +{ + return isdigit(c) || isalpha(c) || (c == '-' || c == '_'); +} + +static char +ConfigConsumeWhitespace(ConfigParserState *state) +{ + int c; + while (isspace(c = fgetc(state->stream))) + { + if (c == '\n') + { + state->line++; + } + } + return c; +} + +static void +ConfigConsumeLine(ConfigParserState *state) +{ + while (fgetc(state->stream) != '\n'); + state->line++; +} + +static void +ConfigTokenSeek(ConfigParserState *state) +{ + int c; + + /* If we already hit EOF, don't do anything */ + if (state->tokenType == TOKEN_EOF) { + return; + } + while ((c = ConfigConsumeWhitespace(state)) == '#') { + ConfigConsumeLine(state); + } + + /* + * After all whitespace and comments are consumed, identify the + * token by looking at the next character + */ + + if (feof(state->stream)) { + state->tokenType = TOKEN_EOF; + return; + } + if (ConfigIsNameChar(c)) { + state->tokenLen = 0; + + /* Read the key/macro into state->token */ + if (!state->token) { + state->tokenSize = CONFIG_BUFFER_BLOCK; + state->token = malloc(CONFIG_BUFFER_BLOCK); + } + state->token[state->tokenLen] = c; + state->tokenLen++; + + while (ConfigIsNameChar((c = fgetc(state->stream)))) { + state->token[state->tokenLen] = c; + state->tokenLen++; + + if (state->tokenLen >= state->tokenSize) { + state->tokenSize += CONFIG_BUFFER_BLOCK; + state->token = realloc(state->token, + state->tokenSize); + } + } + + state->token[state->tokenLen] = '\0'; + state->tokenLen++; + + if (!isspace(c)) { + state->tokenType = TOKEN_UNKNOWN; + } else { + state->tokenType = TOKEN_NAME; + + if (c == '\n') { + state->line++; + } + } + + } else { + switch (c) { + case '=': + state->tokenType = TOKEN_MACRO_ASSIGNMENT; + break; + case '"': + state->tokenLen = 0; + state->tokenType = TOKEN_VALUE; + + /* read the value into state->curtok */ + while ((c = fgetc(state->stream)) != '"') { + if (c == '\n') { + state->line++; + } + /* + * End of the stream reached without finding + * a closing quote + */ + if (feof(state->stream)) { + state->tokenType = TOKEN_EOF; + break; + } + state->token[state->tokenLen] = c; + state->tokenLen++; + + if (state->tokenLen >= state->tokenSize) { + state->tokenSize += CONFIG_BUFFER_BLOCK; + state->token = realloc(state->token, + state->tokenSize); + } + } + state->token[state->tokenLen] = '\0'; + state->tokenLen++; + break; + case ';': + state->tokenType = TOKEN_SEMICOLON; + break; + case '{': + state->tokenType = TOKEN_BLOCK_OPEN; + break; + case '}': + state->tokenType = TOKEN_BLOCK_CLOSE; + break; + case '$': + state->tokenLen = 0; + /* read the macro name into state->curtok */ + while (ConfigIsNameChar(c = fgetc(state->stream))) { + state->token[state->tokenLen] = c; + state->tokenLen++; + + if (state->tokenLen >= state->tokenSize) { + state->tokenSize += CONFIG_BUFFER_BLOCK; + state->token = realloc(state->token, + state->tokenSize); + } + } + state->token[state->tokenLen] = '\0'; + state->tokenLen++; + state->tokenType = TOKEN_MACRO; + + ungetc(c, state->stream); + break; + default: + state->tokenType = TOKEN_UNKNOWN; + break; + } + } + + /* Resize curtok to only use the bytes it needs */ + if (state->tokenLen) { + state->tokenSize = state->tokenLen; + state->token = realloc(state->token, state->tokenSize); + } +} + +static int +ConfigExpect(ConfigParserState *state, ConfigToken tokenType) +{ + return state->tokenType == tokenType; +} + + +static HashMap * +ConfigParseBlock(ConfigParserState *state, int level) +{ + HashMap *block = HashMapCreate(); + + ConfigTokenSeek(state); + + while (ConfigExpect(state, TOKEN_NAME)) { + char *name = malloc(state->tokenLen + 1); + strcpy(name, state->token); + + ConfigTokenSeek(state); + if (ConfigExpect(state, TOKEN_VALUE) || ConfigExpect(state, TOKEN_MACRO)) { + ConfigDirective *directive; + + directive = malloc(sizeof(ConfigDirective)); + directive->children = NULL; + directive->values = ArrayCreate(); + + while (ConfigExpect(state, TOKEN_VALUE) || + ConfigExpect(state, TOKEN_MACRO)) { + + char *dval; + char *dvalCpy; + + if (ConfigExpect(state, TOKEN_VALUE)) { + dval = state->token; + } else if (ConfigExpect(state, TOKEN_MACRO)) { + dval = HashMapGet(state->macroMap, state->token); + if (!dval) { + goto error; + } + } else { + dval = NULL; /* Should never happen */ + } + + /* dval is a pointer which is overwritten with the next token. */ + dvalCpy = malloc(strlen(dval) + 1); + strcpy(dvalCpy, dval); + + ArrayAdd(directive->values, dvalCpy); + ConfigTokenSeek(state); + } + + if (ConfigExpect(state, TOKEN_BLOCK_OPEN)) { + /* token_seek(state); */ + directive->children = ConfigParseBlock(state, level + 1); + if (!directive->children) { + goto error; + } + } + + /* + * Append this directive to the current block, + * overwriting a directive at this level with the same name. + * + * Note that if a value already exists with this name, it is + * returned by HashMapSet() and then immediately passed to + * ConfigDirectiveFree(). If the value does not exist, then + * NULL is sent to ConfigDirectiveFree(), making it a no-op. + */ + ConfigDirectiveFree(HashMapSet(block, name, directive)); + + } else if (ConfigExpect(state, TOKEN_MACRO_ASSIGNMENT)) { + ConfigTokenSeek(state); + if (ConfigExpect(state, TOKEN_VALUE)) { + char * valueCopy = malloc(strlen(state->token) + 1); + strcpy(valueCopy, state->token); + free(HashMapSet(state->macroMap, name, state->token)); + ConfigTokenSeek(state); + } else { + goto error; + } + } else { + goto error; + } + + if (!ConfigExpect(state, TOKEN_SEMICOLON)) { + goto error; + } + ConfigTokenSeek(state); + } + + if (ConfigExpect(state, level ? TOKEN_BLOCK_CLOSE : TOKEN_EOF)) { + ConfigTokenSeek(state); + return block; + } else { + goto error; + } + + error: + /* Only free the very top level, because this will recurse */ + if (!level) { + ConfigFree(block); + } + return NULL; +} + +ConfigParseResult * +ConfigParse(FILE * stream) +{ + ConfigParseResult *result; + HashMap *conf; + ConfigParserState *state; + + result = malloc(sizeof(ConfigParseResult)); + state = ConfigParserStateCreate(stream); + conf = ConfigParseBlock(state, 0); + + if (!conf) { + result->ok = 0; + result->data.lineNumber = state->line; + } else { + result->ok = 1; + result->data.confMap = conf; + } + + ConfigParserStateFree(state); + return result; +} + diff --git a/src/HashMap.c b/src/HashMap.c new file mode 100644 index 0000000..351c4a2 --- /dev/null +++ b/src/HashMap.c @@ -0,0 +1,300 @@ +#include + +#include +#include +#include + +typedef struct HashMapBucket { + uint32_t hash; + void *value; +} HashMapBucket; + +struct HashMap { + size_t count; + size_t capacity; + HashMapBucket **entries; + + float maxLoad; +}; + +static uint32_t +HashMapHashKey(const char *key) +{ + uint32_t hash = 2166136261u; + size_t i = 0; + + while (key[i]) + { + hash ^= (uint8_t) key[i]; + hash *= 16777619; + + i++; + } + + return hash; +} + + +static int +HashMapGrow(HashMap *map) +{ + size_t oldCapacity; + size_t i; + HashMapBucket **newEntries; + + if (!map) + { + return 0; + } + + oldCapacity = map->capacity; + map->capacity *= 2; + + newEntries = calloc(map->capacity, sizeof(HashMapBucket *)); + if (!newEntries) + { + return 0; + } + + for (i = 0; i < oldCapacity; i++) + { + /* If there is a value here, and it isn't a tombstone */ + if (map->entries[i] && map->entries[i]->hash) + { + /* Copy it to the new entries array */ + size_t index = map->entries[i]->hash % map->capacity; + + for (;;) + { + if (newEntries[index]) + { + if (!newEntries[index]->hash) + { + free(newEntries[index]); + newEntries[index] = map->entries[i]; + break; + } + } + else + { + newEntries[index] = map->entries[i]; + break; + } + + index = (index + 1) % map->capacity; + } + } + else + { + /* Either NULL or a tombstone */ + free(map->entries[i]); + } + } + + free(map->entries); + map->entries = newEntries; + return 1; +} + +HashMap * +HashMapCreate(void) +{ + HashMap *map = malloc(sizeof(HashMap)); + if (!map) + { + return NULL; + } + + map->maxLoad = 0.75; + map->count = 0; + map->capacity = 16; + + map->entries = calloc(map->capacity, sizeof(HashMapBucket *)); + if (!map->entries) + { + free(map); + return NULL; + } + + return map; +} + +void * +HashMapDelete(HashMap *map, const char *key) +{ + uint32_t hash; + size_t index; + + if (!map || !key) + { + return NULL; + } + + hash = HashMapHashKey(key); + index = hash % map->capacity; + + for (;;) + { + HashMapBucket *bucket = map->entries[index]; + + if (!bucket) + { + break; + } + + if (bucket->hash == hash) + { + bucket->hash = 0; + return bucket->value; + } + + index = (index + 1) % map->capacity; + } + + return NULL; +} + +void +HashMapFree(HashMap *map) +{ + if (map) + { + size_t i; + + for (i = 0; i < map->capacity; i++) + { + if (map->entries[i]) + { + free(map->entries[i]); + } + } + } + + free(map); +} + +void * +HashMapGet(HashMap *map, const char *key) +{ + uint32_t hash; + size_t index; + + if (!map || !key) + { + return NULL; + } + + hash = HashMapHashKey(key); + index = hash % map->capacity; + + for (;;) + { + HashMapBucket *bucket = map->entries[index]; + + if (!bucket) + { + break; + } + + if (bucket->hash == hash) + { + return bucket->value; + } + + index = (index + 1) % map->capacity; + } + + return NULL; +} + +void +HashMapIterate(HashMap *map, void (*iteratorFunc)(void *)) +{ + size_t i; + + if (!map) + { + return; + } + + for (i = 0; i < map->capacity; i++) + { + HashMapBucket *bucket = map->entries[i]; + + if (bucket) + { + iteratorFunc(bucket->value); + } + } +} + +void +HashMapMaxLoadSet(HashMap *map, float load) +{ + if (!map) + { + return; + } + + map->maxLoad = load; +} + + +void * +HashMapSet(HashMap *map, const char *key, void *value) +{ + uint32_t hash; + size_t index; + + if (!map || !key || !value) + { + return NULL; + } + + if (map->count + 1 > map->capacity * map->maxLoad) + { + HashMapGrow(map); + } + + hash = HashMapHashKey(key); + index = hash % map->capacity; + + for (;;) + { + HashMapBucket *bucket = map->entries[index]; + + if (!bucket) + { + bucket = malloc(sizeof(HashMapBucket)); + if (!bucket) + { + break; + } + + bucket->hash = hash; + bucket->value = value; + map->entries[index] = bucket; + map->count++; + break; + } + + if (!bucket->hash) + { + bucket->hash = hash; + bucket->value = value; + break; + } + + if (bucket->hash == hash) + { + void *oldValue = bucket->value; + bucket->value = value; + return oldValue; + } + + index = (index + 1) % map->capacity; + } + + return NULL; +} + diff --git a/src/Log.c b/src/Log.c new file mode 100644 index 0000000..c33d29a --- /dev/null +++ b/src/Log.c @@ -0,0 +1,310 @@ +#include + +#include +#include +#include +#include +#include + +#define LOG_TSBUFFER 64 + +struct LogConfig { + LogLevel level; + size_t indent; + FILE *out; + int flags; + char *tsFmt; +}; + +void +Log(LogConfig *config, LogLevel level, const char *msg, ...) +{ + int i; + int doColor; + char indicator; + va_list argp; + + /* + * Only proceed if we have a config and its log level is set to a + * value that permits us to log. This is as close as we can get + * to a no-op function if we aren't logging anything, without doing + * some crazy macro magic. + */ + if (!config || level > config->level) + { + return; + } + + /* Misconfiguration */ + if (!config->out) + { + return; + } + + for (i = 0; i < config->indent; i++) + { + fputc(' ', config->out); + } + + doColor = LogConfigFlagGet(config, LOG_FLAG_COLOR) + && isatty(fileno(config->out)); + + if (doColor) + { + char *ansi; + switch (level) + { + case LOG_ERROR: + /* Bold Red */ + ansi = "\033[1;31m"; + break; + case LOG_WARNING: + /* Bold Yellow */ + ansi = "\033[1;33m"; + break; + case LOG_TASK: + /* Bold Magenta */ + ansi = "\033[1;35m"; + break; + case LOG_MESSAGE: + /* Bold Green */ + ansi = "\033[1;32m"; + break; + case LOG_DEBUG: + /* Bold Blue */ + ansi = "\033[1;34m"; + break; + default: + ansi = ""; + break; + } + + fputs(ansi, config->out); + } + + fputc('[', config->out); + + if (config->tsFmt) + { + time_t timer = time(NULL); + struct tm *timeInfo = localtime(&timer); + char tsBuffer[LOG_TSBUFFER]; + + int tsLength = strftime(tsBuffer, LOG_TSBUFFER, config->tsFmt, + timeInfo); + + if (tsLength) + { + fputs(tsBuffer, config->out); + if (!isspace(tsBuffer[tsLength - 1])) + { + fputc(' ', config->out); + } + } + } + + switch (level) + { + case LOG_ERROR: + indicator = 'x'; + break; + case LOG_WARNING: + indicator = '!'; + break; + case LOG_TASK: + indicator = '~'; + break; + case LOG_MESSAGE: + indicator = '>'; + break; + case LOG_DEBUG: + indicator = '*'; + break; + default: + indicator = ' '; + break; + } + + fprintf(config->out, "%c]", indicator); + + if (doColor) + { + /* ANSI Reset */ + fputs("\033[0m", config->out); + } + + fputc(' ', config->out); + + va_start(argp, msg); + vfprintf(config->out, msg, argp); + fputc('\n', config->out); + va_end(argp); + + /* If we are debugging, there might be something that's + * going to segfault the program coming up, so flush the + * output stream immediately. + */ + if (config->level == LOG_DEBUG) + { + fflush(config->out); + } +} + +LogConfig * +LogConfigCreate(void) +{ + LogConfig *config; + + config = calloc(1, sizeof(LogConfig)); + + if (!config) + { + return NULL; + } + + LogConfigLevelSet(config, LOG_MESSAGE); + LogConfigIndentSet(config, 0); + LogConfigOutputSet(config, NULL); /* Will set to stdout */ + LogConfigFlagSet(config, LOG_FLAG_COLOR); + LogConfigTimeStampFormatSet(config, "%y-%m-%d %H:%M:%S"); + + return config; +} + +void +LogConfigFlagClear(LogConfig *config, int flags) +{ + if (!config) + { + return; + } + + config->flags &= ~flags; +} + +int +LogConfigFlagGet(LogConfig *config, int flags) +{ + if (!config) + { + return 0; + } + + return config->flags & flags; +} + +void +LogConfigFlagSet(LogConfig *config, int flags) +{ + if (!config) + { + return; + } + + config->flags |= flags; +} + +void +LogConfigFree(LogConfig *config) +{ + free(config); +} + +void +LogConfigIndent(LogConfig *config) +{ + if (config) + { + config->indent += 2; + } +} + +size_t +LogConfigIndentGet(LogConfig *config) +{ + if (!config) + { + return 0; + } + + return config->indent; +} + +void +LogConfigIndentSet(LogConfig *config, size_t indent) +{ + if (!config) + { + return; + } + + config->indent = indent; +} + +LogLevel +LogConfigLevelGet(LogConfig *config) +{ + if (!config) + { + return -1; + } + + return config->level; +} + +void +LogConfigLevelSet(LogConfig *config, LogLevel level) +{ + if (!config) + { + return; + } + + switch (level) + { + case LOG_ERROR: + case LOG_WARNING: + case LOG_MESSAGE: + case LOG_DEBUG: + config->level = level; + default: + break; + } +} + +void +LogConfigOutputSet(LogConfig *config, FILE *out) +{ + if (!config) + { + return; + } + + if (out) + { + config->out = out; + } + else + { + config->out = stdout; + } + +} + +void +LogConfigTimeStampFormatSet(LogConfig *config, char *tsFmt) +{ + if (config) + { + config->tsFmt = tsFmt; + } +} + +void +LogConfigUnindent(LogConfig *config) +{ + if (config && config->indent >= 2) + { + config->indent -= 2; + } +} diff --git a/src/Telodendria.c b/src/Telodendria.c new file mode 100644 index 0000000..f5290bb --- /dev/null +++ b/src/Telodendria.c @@ -0,0 +1,157 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +typedef enum ArgFlag +{ + ARG_VERSION = (1 << 0), + ARG_USAGE = (1 << 1) +} ArgFlag; + +static void +TelodendriaPrintHeader(LogConfig *lc) +{ + Log(lc, LOG_MESSAGE, + " _____ _ _ _ _"); + Log(lc, LOG_MESSAGE, + "|_ _|__| | ___ __| | ___ _ __ __| |_ __(_) __ _"); + Log(lc, LOG_MESSAGE, + " | |/ _ \\ |/ _ \\ / _` |/ _ \\ '_ \\ / _` | '__| |/ _` |"); + Log(lc, LOG_MESSAGE, + " | | __/ | (_) | (_| | __/ | | | (_| | | | | (_| |"); + Log(lc, LOG_MESSAGE, + " |_|\\___|_|\\___/ \\__,_|\\___|_| |_|\\__,_|_| |_|\\__,_|"); + Log(lc, LOG_MESSAGE, "Telodendria v" TELODENDRIA_VERSION); + Log(lc, LOG_MESSAGE, ""); + Log(lc, LOG_MESSAGE, + "Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net>"); + Log(lc, LOG_MESSAGE, + "Documentation/Support: https://bancino.net/pub/Telodendria"); + Log(lc, LOG_MESSAGE, ""); +} + +static void +TelodendriaPrintUsage(LogConfig *lc) +{ + Log(lc, LOG_MESSAGE, "Usage:"); + Log(lc, LOG_MESSAGE, " -c Configuration file ('-' for stdin)."); + Log(lc, LOG_MESSAGE, " -V Print the header, then exit."); + Log(lc, LOG_MESSAGE, " -h Print this usage, then exit."); +} + +int main(int argc, char **argv) +{ + LogConfig *lc; + int exit = EXIT_SUCCESS; + + /* Arg parsing */ + int opt; + int flags = 0; + char *configArg = NULL; + + /* Config file */ + FILE *configFile = NULL; + ConfigParseResult *configParseResult = NULL; + HashMap *config = NULL; + + lc = LogConfigCreate(); + + /* TODO: Remove */ + LogConfigLevelSet(lc, LOG_DEBUG); + + if (!lc) + { + printf("Fatal error: unable to allocate memory for logger.\n"); + return EXIT_FAILURE; + } + + TelodendriaPrintHeader(lc); + + while ((opt = getopt(argc, argv, "c:Vh")) != -1) + { + switch (opt) + { + case 'c': + configArg = optarg; + break; + case 'V': + flags |= ARG_VERSION; + break; + case 'h': + flags |= ARG_USAGE; + default: + break; + } + } + + if (flags & ARG_VERSION) + { + goto finish; + } + + if (flags & ARG_USAGE) + { + TelodendriaPrintUsage(lc); + goto finish; + } + + if (!configArg) + { + Log(lc, LOG_ERROR, "No configuration file specified."); + TelodendriaPrintUsage(lc); + exit = EXIT_FAILURE; + goto finish; + } + + if (strcmp(configArg, "-") == 0) + { + configFile = stdout; + } + else + { + configFile = fopen(configArg, "r"); + if (!configFile) + { + Log(lc, LOG_ERROR, "Unable to open configuration file '%s' for reading.", configArg); + exit = EXIT_FAILURE; + goto finish; + } + } + + Log(lc, LOG_MESSAGE, "Using configuration file '%s'.", configArg); + + /* Read config here */ + configParseResult = ConfigParse(configFile); + + if (!ConfigParseResultOk(configParseResult)) + { + Log(lc, LOG_ERROR, "Syntax error on line %d.", + ConfigParseResultLineNumber(configParseResult)); + exit = EXIT_FAILURE; + goto finish; + } + + config = ConfigParseResultGet(configParseResult); + ConfigParseResultFree(configParseResult); + + Log(lc, LOG_DEBUG, "Closing configuration file."); + fclose(configFile); + + /* Configure log file */ + +finish: + if (config) + { + Log(lc, LOG_DEBUG, "Freeing configuration structure."); + ConfigFree(config); + } + Log(lc, LOG_DEBUG, "Freeing log configuration and exiting with code '%d'.", exit); + LogConfigFree(lc); + return exit; +} diff --git a/src/include/Array.h b/src/include/Array.h new file mode 100644 index 0000000..f377a18 --- /dev/null +++ b/src/include/Array.h @@ -0,0 +1,32 @@ +#ifndef TELODENDRIA_ARRAY_H +#define TELODENDRIA_ARRAY_H + +#include + +typedef struct Array Array; + +extern Array * +ArrayCreate(void); + +extern size_t +ArraySize(Array *array); + +extern void * +ArrayGet(Array *array, size_t index); + +extern int +ArrayInsert(Array *, void *value, size_t index); + +extern int +ArrayAdd(Array *array, void *value); + +extern void * +ArrayDelete(Array *array, size_t index); + +extern void +ArrayFree(Array *array); + +extern int +ArrayTrim(Array *array); + +#endif diff --git a/src/include/Base64.h b/src/include/Base64.h new file mode 100644 index 0000000..d45d014 --- /dev/null +++ b/src/include/Base64.h @@ -0,0 +1,25 @@ +#ifndef TELODENDRIA_BASE64_H +#define TELODENDRIA_BASE64_H + +#include + +extern size_t +Base64EncodedSize(size_t inputSize); + +extern size_t +Base64DecodedSize(const char *base64, size_t len); + +extern char * +Base64Encode(const char *input, size_t len); + +extern char * +Base64Decode(const char *input, size_t len); + +extern void +Base64Unpad(char *base64, size_t length); + +extern int +Base64Pad(char **base64Ptr, size_t length); + +#endif + diff --git a/src/include/Config.h b/src/include/Config.h new file mode 100644 index 0000000..0b445b1 --- /dev/null +++ b/src/include/Config.h @@ -0,0 +1,37 @@ +#ifndef TELODENDRIA_CONFIG_H +#define TELODENDRIA_CONFIG_H + +#include + +#include +#include + +typedef struct ConfigDirective ConfigDirective; + +typedef struct ConfigParseResult ConfigParseResult; + +extern ConfigParseResult * +ConfigParse(FILE *stream); + +extern unsigned int +ConfigParseResultOk(ConfigParseResult *result); + +extern size_t +ConfigParseResultLineNumber(ConfigParseResult *result); + +extern HashMap * +ConfigParseResultGet(ConfigParseResult *result); + +extern void +ConfigParseResultFree(ConfigParseResult *result); + +extern Array * +ConfigValuesGet(ConfigDirective *directive); + +extern HashMap * +ConfigChildrenGet(ConfigDirective *directive); + +extern void +ConfigFree(HashMap *conf); + +#endif /* TELODENDRIA_CONFIG_H */ diff --git a/src/include/HashMap.h b/src/include/HashMap.h new file mode 100644 index 0000000..659f3a3 --- /dev/null +++ b/src/include/HashMap.h @@ -0,0 +1,85 @@ +/* + * HashMap.h: The public interface for Telodendria's hash map + * implementation. This hash map is designed to be simple, well + * documented, and generally readable and understandable, yet also + * performant enough to be useful, because it is used extensively in + * Telodendria. + * + * Fundamentally, this is an entirely generic map implementation. It + * can be used for many general purposes, but it is designed to only + * implement the features that Telodendria needs to function well. + * + * Copyright (C) 2022 Jordan Bancino <@jordan:bancino.net> + */ +#ifndef TELODENDRIA_HASHMAP_H +#define TELODENDRIA_HASHMAP_H + +#include + +/* + * HashMap: The primary data structure used by this hash map algorithm. + * All data necessary for the algorithm to function is stored inside + * it. This is an opaque structure; use the methods defined by this + * interface to manipulate it. + */ +typedef struct HashMap HashMap; + +/* + * HashMapCreate: Create a new HashMap object. + * + * Returns: A HashMap object that is ready to be used by the rest of + * the HashMap functions, or NULL if memory could not be allocated on + * the heap. + */ +extern HashMap * +HashMapCreate(void); + +extern void +HashMapMaxLoadSet(HashMap *map, float load); + +/* + * HashMapSet: Set the given key in the HashMap to the given value. Note + * that the value is not copied into the HashMap's own memory space; + * only the pointer is stored. It is the caller's job to ensure that the + * value's memory remains valid for the life of the HashMap. + * + * Returns: The previous value at the given key, or NULL if the key did + * not previously exist or any of the parameters provided are NULL. All + * keys must have values; you can't set a key to NULL. To delete a key, + * use HashMapDelete. + */ +extern void * +HashMapSet(HashMap * map, const char *key, void *value); + +/* + * HashMapGet: Get the value for the given key. + * + * Returns: The value at the given key, or NULL if the key does not + * exist, no map was provided, or no key was provided. + */ +extern void * +HashMapGet(HashMap * map, const char *key); + +/* + * HashMapDelete: Delete the value for the given key. + * + * Returns: The value at the given key, or NULL if the key does not + * exist or the map or key was not provided. + */ +extern void * +HashMapDelete(HashMap *map, const char *key); + +extern void +HashMapIterate(HashMap *map, void (*iteratorFunc)(void *)); + +/* + * HashMapFree: Free the hash map, returning its memory to the operating + * system. Note that this function does not free the values stored in + * the map since this hash map implementation has no way of knowing + * what actually is stored in it. You should use HashMapIterate to + * free the values using your own algorithm. + */ +extern void +HashMapFree(HashMap *map); + +#endif /* TELODENDRIA_HASHMAP_H */ diff --git a/src/include/Http.h b/src/include/Http.h new file mode 100644 index 0000000..c27f589 --- /dev/null +++ b/src/include/Http.h @@ -0,0 +1,96 @@ +#ifndef TELODENDRIA_HTTP_H +#define TELODENDRIA_HTTP_H + +typedef enum HttpRequestMethod { + HTTP_GET, + HTTP_HEAD, + HTTP_POST, + HTTP_PUT, + HTTP_DELETE, + HTTP_CONNECT, + HTTP_OPTIONS, + HTTP_TRACE, + HTTP_PATCH +} HttpRequestMethod; + +typedef enum HttpStatus { + /* Informational responses */ + HTTP_CONTINUE = 100, + HTTP_SWITCHING_PROTOCOLS = 101, + HTTP_EARLY_HINTS = 103, + + /* Successful responses */ + HTTP_OK = 200, + HTTP_CREATED = 201, + HTTP_ACCEPTED = 202, + HTTP_NON_AUTHORITATIVE_INFORMATION = 203, + HTTP_NO_CONTENT = 204, + HTTP_RESET_CONTENT = 205, + HTTP_PARTIAL_CONTENT = 206, + + /* Redirection messages */ + HTTP_MULTIPLE_CHOICES = 300, + HTTP_MOVED_PERMANENTLY = 301, + HTTP_FOUND = 302, + HTTP_SEE_OTHER = 303, + HTTP_NOT_MODIFIED = 304, + HTTP_TEMPORARY_REDIRECT = 307, + HTTP_PERMANENT_REDIRECT = 308, + + /* Client error messages */ + HTTP_BAD_REQUEST = 400, + HTTP_UNAUTHORIZED = 401, + HTTP_FORBIDDEN = 403, + HTTP_NOT_FOUND = 404, + HTTP_METHOD_NOT_ALLOWED = 405, + HTTP_NOT_ACCEPTABLE = 406, + HTTP_PROXY_AUTH_REQUIRED = 407, + HTTP_REQUEST_TIMEOUT = 408, + HTTP_CONFLICT = 409, + HTTP_GONE = 410, + HTTP_LENGTH_REQUIRED = 411, + HTTP_PRECONDITION_FAILED = 412, + HTTP_PAYLOAD_TOO_LARGE = 413, + HTTP_URI_TOO_LONG = 414, + HTTP_UNSUPPORTED_MEDIA_TYPE = 415, + HTTP_RANGE_NOT_SATISFIABLE = 416, + HTTP_EXPECTATION_FAILED = 417, + HTTP_TEAPOT = 418, + HTTP_UPGRADE_REQUIRED = 426, + HTTP_PRECONDITION_REQUIRED = 428, + HTTP_TOO_MANY_REQUESTS = 429, + HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + /* Server error responses */ + HTTP_INTERNAL_SERVER_ERROR = 500, + HTTP_NOT_IMPLEMENTED = 501, + HTTP_BAD_GATEWAY = 502, + HTTP_SERVICE_UNAVAILABLE = 503, + HTTP_GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + HTTP_VARIANT_ALSO_NEGOTIATES = 506, + HTTP_NOT_EXTENDED = 510, + HTTP_NETWORK_AUTH_REQUIRED = 511 +} HttpStatus; + +struct HttpRequest { + HttpRequestMethod method; +}; + +struct HttpResponse { + HttpStatus status; +}; + +extern char * +HttpGetStatusString(const HttpStatus httpStatus); + +extern HttpRequestMethod +HttpRequestMethodFromString(const char *requestMethod); + +typedef struct HttpRequest HttpRequest; +typedef struct HttpResponse HttpResponse; + +typedef void (*HttpHandler)(HttpRequest *, HttpResponse *); + +#endif diff --git a/src/include/Json.h b/src/include/Json.h new file mode 100644 index 0000000..ead2453 --- /dev/null +++ b/src/include/Json.h @@ -0,0 +1,69 @@ +#ifndef TELODENDRIA_JSON_H +#define TELODENDRIA_JSON_H + +#include +#include + +typedef enum JsonType { + JSON_OBJECT, + JSON_ARRAY, + JSON_STRING, + JSON_INTEGER, + JSON_FLOAT, + JSON_BOOLEAN, + JSON_NULL +} JsonType; + +typedef struct JsonValue { + JsonType type; + union as { + HashMap *object; + Array *array; + char *string; + int64_t integer; + double floating; + int boolean : 1; + }; +} JsonValue; + + +extern JsonType +JsonValueType(JsonValue *value); + +extern JsonValue * +JsonValueObject(HashMap *object); + +extern HashMap * +JsonValueAsObject(JsonValue *value); + +extern JsonValue * +JsonValueArray(Array *array); + +extern Array * +JsonValueAsArray(JsonValue *value); + +extern JsonValue * +JsonValueString(char *string); + +extern JsonValue * +JsonValueInteger(int64_t integer); + +extern JsonValue * +JsonValueFloat(double floating); + +extern JsonValue * +JsonValueBoolean(int boolean); + +extern JsonValue * +JsonValueNull(void); + +extern void * +JsonValueFree(JsonValue *value); + +extern char * +JsonEncode(HashMap *object); + +extern HashMap * +JsonDecode(char *string); + +#endif diff --git a/src/include/Log.h b/src/include/Log.h new file mode 100644 index 0000000..3011692 --- /dev/null +++ b/src/include/Log.h @@ -0,0 +1,63 @@ +#ifndef TELODENDRIA_LOG_H +#define TELODENDRIA_LOG_H + +#include +#include + +typedef enum LogLevel { + LOG_ERROR, + LOG_WARNING, + LOG_TASK, + LOG_MESSAGE, + LOG_DEBUG +} LogLevel; + +typedef enum LogFlag { + LOG_FLAG_COLOR = (1 << 0) +} LogFlag; + +typedef struct LogConfig LogConfig; + +extern LogConfig * +LogConfigCreate(void); + +extern void +LogConfigFree(LogConfig *config); + +extern void +LogConfigLevelSet(LogConfig *config, LogLevel level); + +extern LogLevel +LogConfigLevelGet(LogConfig *config); + +extern void +LogConfigIndentSet(LogConfig *config, size_t indent); + +extern size_t +LogConfigIndentGet(LogConfig *config); + +extern void +LogConfigIndent(LogConfig *config); + +extern void +LogConfigUnindent(LogConfig *config); + +extern void +LogConfigOutputSet(LogConfig *config, FILE *out); + +extern void +LogConfigFlagSet(LogConfig *config, int flags); + +extern void +LogConfigFlagClear(LogConfig *config, int flags); + +extern int +LogConfigFlagGet(LogConfig *config, int flags); + +extern void +LogConfigTimeStampFormatSet(LogConfig *config, char *tsFmt); + +extern void +Log(LogConfig *config, LogLevel level, const char *msg, ...); + +#endif