commit 40eac30b5c94e3e26cf745948265a0eb99d18a94 Author: Jordan Bancino Date: Sat May 13 17:30:09 2023 +0000 Import new Cytoplasm library based off of code from Telodendria. Telodendria doesn't use this library yet, but it will soon. diff --git a/.cvsignore b/.cvsignore new file mode 100644 index 0000000..e94bc95 --- /dev/null +++ b/.cvsignore @@ -0,0 +1,3 @@ +build +out +*-leaked.txt diff --git a/.indent.pro b/.indent.pro new file mode 100644 index 0000000..5c1d1ec --- /dev/null +++ b/.indent.pro @@ -0,0 +1,28 @@ +-bad +-bap +-bbb +-nbc +-bl +-c36 +-cd36 +-ncdb +-nce +-ci8 +-cli1 +-d0 +-di1 +-ndj +-ei +-fc1 +-i4 +-ip +-l72 +-lc72 +-lp +-npcs +-psl +-sc +-nsob +-nut +-nv + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..9d1c610 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,24 @@ +/* + * Cytoplasm (libcytoplasm) + * Copyright (C) 2022-2023 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 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. + */ diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..89b743f --- /dev/null +++ b/README.txt @@ -0,0 +1,158 @@ +Cytoplasm (libcytoplasm) +======================================================================== + +Cytoplasm is a general-purpose C library and runtime stub for +creating high-level (particularly networked and multi-threaded) C +applications. It allows applications to take advantage of the speed, +flexibility, and simplicity of the C programming language, while +providing helpful code to perform various complex tasks. Cytoplasm +provides high-level data structures, a basic logging facility, an +HTTP client and server, and more. + +Cytoplasm aims not to only do one thing well, but to do many things +good enough. The primary target of Cytoplasm is simple, yet higher +level C applications that have to perform relatively complex tasks, +but don't want to pull in a large number of dependencies. + +Cytoplasm is extremely opinionated on the way programs using it are +written. It strives to create a comprehensive and tightly-integrated +programming environment, while also maintaining C programming +correctness. It doesn't do any macro magic or make C look like +anything other than C. It is written entirely in C89, and depends +only on a POSIX environment. This differentiates it from other +general-purpose libraries that often require modern compilers and +non-standard language and environment features. Cytoplasm is intended +to be extremely portable and simple, while still providing some of +the functionality expected in higher-level programming languages +in a platform-agnostic manner. In the case of TLS, Cytoplasm wraps +low-level TLS libraries to offer a single, unified interface to TLS +so that programs do not have to care about the underlying +implementation. + +Cytoplasm is probably not suitable for embedded programming. It makes +liberal use of the heap, and while data structures are designed to +conserve memory where possible and practical, minimal memory usage +is not really a design goal for Cytoplasm, although Cytoplasm takes +care not to use any more memory than it absolutely needs. Cytoplasm +also wraps a few standard libraries with additional logic and +checking. While this ensures better runtime safetly, this inevitably +adds a little overhead. + +Originally a part of Telodendria (https://telodendria.io), a Matrix +homeserver written in C, Cytoplasm was split off into its own project +due to the desire of some Telodendria developers to use Telodendria's +code in other projects. Cytoplasm is still a Telodendria project, +and is maintained along side of Telodendria itself, even living in +the same CVS module, but it is designed specifically to be distributed +and used totally independent of Telodendria. + +The name "Cytoplasm" was chosen for a few reasons. It plays off the +precedent set up by the Matrix organization in naming projects after +the parts of a neuron. It also speaks to the function of Cytoplasm. +The cytoplasm of a cell is the supporting material. It is what gives +the cell its shape, and it facilitates the movement of materials +to the other cell parts. Likewise, Cytoplasm aims to provide a +support mechanism for C applications that have to perform complex +tasks. + +Cytoplasm also starts with a C, which I think is a nice touch for C +libraries. It's also fun to say and unique enough that searching for +"libcytoplasm" should bring you to this project and not some other +one. + +Building +------------------------------------------------------------------------ + +If your operating system or software distribution provides a pre-built +package of Cytoplasm, you should prefer to use that instead of +building it from source. + +Cytoplasm aims to have zero dependencies beyond what is mandated +by POSIX. You only need the standard math and pthread libraries to +build it. TLS support can optionally be enabled by setting the +TLS_IMPL environment variable. The supported TLS implementations +are as follows: + + * OpenSSL + * LibreSSL + +If TLS support is not enabled, all APIs that use it should fall +back to non-TLS behavior in a sensible manner. For example, if TLS +support is not enabled, then the HTTP client API will simply return +an error if a TLS connection is requested. + +Cytoplasm uses a custom build script instead of Make, for the sake +of portability. To build everything, just run the script: + + $ sh make.sh + +This will produce the following out/ directory: + + out/ + lib/ + libcytoplasm.so - The Cytoplasm shared library. + libcytoplasm.a - The Cytoplasm static library. + cytoplasm.o - The Cytoplasm runtime stub. + bin/ + hdoc - The documentation generator tool. + man/ - All Cytoplasm API documentation. + +Usage +------------------------------------------------------------------------ + +Cytoplasm provides the typical .so and .a files, which can be used +to link programs with it in the usual way. However, Cytoplasm also +provides a minimal runtime environment that is intended to be used +with the library. As such, it also provides a runtime stub, which +is intended to be linked in with programs using Cytoplasm. This +stub is responsible for setting up and tearing down some Cytoplasm +APIs. While it isn't required by any means, it makes Cytoplasm a +lot easier to use for programmers by abstracting out all of the +common logic that most programs will want to use. + +Here is the canonical Hello World written with Cytoplasm: + + #include + + int Main(void) + { + Log(LOG_INFO, "Hello World!"); + return 0; + } + +If this file is Hello.c, then you can compile it by doing something +like this: + + $ cc -o hello Hello.c cytoplasm.o -lcytoplasm + +This command assumes that the runtime stub resides in the current +working directory, and that libcytoplasm.so is in your library path. +If you're using the version of Cytoplasm installed by your operating +system or software distribution, consult the documentation for the +location of the runtime stub. It may be located in +/usr/local/libexec or some other similar location. If you've built +Cytoplasm from source and wish to link to it from there, you may wish +to do something like this: + + $ export CYTOPLASM=/path/to/Cytoplasm/out/lib + $ cc -o hello Hello.c "${CYTOPLASM}/cytoplasm.o" \ + "-L${CYTOPLASM}" -lcytoplasm + +As you may have noticed, C programs using Cytoplasm's runtime stub +don't write the main() function. Instead, they use Main(). The main() +function is provided by the runtime stub. The full form of Main() +expected by the stub is as follows: + + int Main(Array *args, HashMap *env); + +The first argument is a Cytoplasm array of the command line +arguments, and the second is a Cytoplasm hash map of environment +variables. Most linkers will let programs omit the env argument, +or both arguments if you don't need either. The return value of +Main() is returned to the operating system. + +Note that both arguments to Main may be treated like any other +array or hash map. However, do not invoke ArrayFree() or HashMapFree() +on the passed pointers, because memory is cleaned up after Main() +returns. + diff --git a/make.sh b/make.sh new file mode 100755 index 0000000..c87a3e2 --- /dev/null +++ b/make.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env sh + +addprefix() { + prefix="$1" + shift + for thing in "$@"; do + echo "${prefix}${thing}" + done + + unset prefix + unset thing +} + +: "${NAME:=Cytoplasm}" +: "${LIB_NAME:=$(echo ${NAME} | tr '[A-Z]' '[a-z]')}" +: "${VERSION:=0.3.0}" + +: "${CVS_TAG:=${NAME}-$(echo ${VERSION} | sed 's/\./_/g')}" + + +: "${SRC:=src}" +: "${TOOLS:=tools}" +: "${BUILD:=build}" +: "${OUT:=out}" +: "${STUB:=RtStub}" +: "${LICENSE:=LICENSE.txt}" + +: "${CC:=cc}" +: "${AR:=ar}" + +: "${DEFINES:=-D_DEFAULT_SOURCE -DLIB_NAME=\"${NAME}\" -DLIB_VERSION=\"${VERSION}\"}" +: "${INCLUDE:=${SRC}/include}" + +: "${CFLAGS:=-Wall -Wextra -pedantic -std=c89 -O3 -pipe}" +: "${LD_EXTRA:=-flto -fdata-sections -ffunction-sections -s -Wl,-gc-sections}" +: "${LDFLAGS:=-lm -pthread}" + +if [ -n "${TLS_IMPL}" ]; then + case "${TLS_IMPL}" in + "LIBRESSL") + TLS_LIBS="-ltls -lcrypto -lssl" + ;; + "OPENSSL") + TLS_LIBS="-lcrypto -lssl" + ;; + *) + echo "Unrecognized TLS implementation: ${TLS_IMPL}" + echo "Consult Tls.h for supported implementations." + echo "Note that the TLS_ prefix is omitted in TLS_IMPL." + exit 1 + ;; + esac + + DEFINES="${DEFINES} -DTLS_IMPL=TLS_${TLS_IMPL}" + LDFLAGS="${LDFLAGS} ${TLS_LIBS}" +fi + +CFLAGS="${CFLAGS} ${DEFINES} $(addprefix -I$(pwd)/ ${INCLUDE})" +LDFLAGS="${LDFLAGS} ${LD_EXTRA}" + +# Check the modificiation time of a file. This is used to do +# incremental builds; we only want to rebuild files that have +# have changed. +mod_time() { + if [ -n "$1" ] && [ -f "$1" ]; then + case "$(uname)" in + Linux | CYGWIN_NT* | Haiku) + stat -c %Y "$1" + ;; + *BSD | DragonFly | Minix) + stat -f %m "$1" + ;; + *) + # Platform unknown, force rebuilding the whole + # project every time. + echo "0" + ;; + esac + else + echo "0" + fi +} + +# Substitute shell variables in a stream with their actual value +# in this shell. +setsubst() { + SED="/tmp/sed-$RANDOM.txt" + + ( + set | while IFS='=' read -r var val; do + val=$(echo "$val" | cut -d "'" -f 2- | rev | cut -d "'" -f 2- | rev) + echo "s|\\\${$var}|$val|g" + done + + echo "s|\\\${[a-zA-Z_]*}||g" + echo "s|'''|'|g" + ) >"$SED" + + sed -f "$SED" $@ + rm "$SED" +} + +recipe_docs() { + export LD_LIBRARY_PATH="${OUT}/lib" + mkdir -p "${OUT}/man/man3" + + for header in $(find ${INCLUDE} -name '*.h'); do + basename=$(basename "$header") + man=$(echo "${OUT}/man/man3/$basename" | sed -e 's/\.h$/\.3/') + + if [ $(mod_time "$header") -ge $(mod_time "$man") ]; then + echo "DOC $basename" + if ! "${OUT}/bin/hdoc" -D "Os=${NAME}" -i "$header" -o "$man"; then + rm "$man" + exit 1 + fi + fi + done + + if which makewhatis 2>&1 > /dev/null; then + makewhatis "${OUT}/man" + fi +} + +recipe_build() { + mkdir -p "${BUILD}" ${OUT}/{bin,lib} + cd "${SRC}" + + echo "CC = ${CC}" + echo "CFLAGS = ${CFLAGS}" + echo "LDFLAGS = ${LDFLAGS}" + echo + + do_rebuild=0 + objs="" + for src in $(find . -name '*.c' | cut -d '/' -f 2-); do + obj=$(echo "${BUILD}/$src" | sed -e 's/\.c$/\.o/') + + if [ $(basename "$obj" .o) != "${STUB}" ]; then + objs="$objs $obj" + fi + + if [ $(mod_time "$src") -ge $(mod_time "../$obj") ]; then + echo "CC $(basename $obj)" + obj_dir=$(dirname "../$obj") + mkdir -p "$obj_dir" + if ! $CC $CFLAGS -fPIC -c -o "../$obj" "$src"; then + exit 1 + fi + do_rebuild=1 + fi + done + + cd .. + + if [ $do_rebuild -eq 1 ] || [ ! -f "${OUT}/lib/lib${LIB_NAME}.a" ]; then + echo "AR lib${LIB_NAME}.a" + if ! $AR rcs "${OUT}/lib/lib${LIB_NAME}.a" $objs; then + exit 1 + fi + fi + + if [ $do_rebuild -eq 1 ] || [ ! -f "${OUT}/lib/lib${LIB_NAME}.so" ]; then + echo "LD lib${LIB_NAME}.so" + if ! $CC -shared -o "${OUT}/lib/lib${LIB_NAME}.so" $objs ${LDFLAGS}; then + exit 1 + fi + fi + + cp "${BUILD}/${STUB}.o" "${OUT}/lib/${LIB_NAME}.o" + + for src in $(find "${TOOLS}" -name '*.c'); do + out=$(basename "$src" .c) + out="${OUT}/bin/$out" + + if [ $(mod_time "$src") -ge $(mod_time "$out") ] || [ $do_rebuild -eq 1 ]; then + echo "CC $(basename $out)" + mkdir -p "$(dirname $out)" + if ! $CC $CFLAGS -o "$out" "$src" "${OUT}/lib/${LIB_NAME}.o" "-L${OUT}/lib" "-l${LIB_NAME}" ${LDFLAGS}; then + exit 1 + fi + fi + done + + recipe_docs +} + +recipe_clean() { + rm -r "${BUILD}" "${OUT}" +} + +# Update copyright comments in sources and header files. +recipe_license() { + find . -name '*.[ch]' | while IFS= read -r src; do + if [ -t 1 ]; then + printf "LICENSE %s%*c\r" $(basename "$src") "16" " " + fi + srcHeader=$(grep -n -m 1 '^ \*/' "$src" | cut -d ':' -f 1) + head "-n$srcHeader" "$src" | + diff -u -p - "${LICENSE}" | + patch "$src" | grep -v "^Hmm" + done + if [ -t 1 ]; then + printf "%*c\n" "50" " " + fi +} + +# Format source code files by running indent(1) on them. +recipe_format() { + find . -name '*.c' | while IFS= read -r src; do + if [ -t 1 ]; then + printf "FMT %s%*c\r" $(basename "$src") "16" " " + fi + if indent "$src"; then + rm $(basename "$src").BAK + fi + done + if [ -t 1 ]; then + printf "%*c\n" "50" " " + fi +} + +# Generate a release tarball, checksum and sign it, and push it to +# the web root. +recipe_release() { + # Tag the release at this point in time. + cvs tag "$CVS_TAG" + + mkdir -p "${OUT}/release" + cd "${OUT}/release" + + # Generate the release tarball. + cvs export "-r$CVS_TAG" "${NAME}" + mv "${NAME}" "${NAME}-v${VERSION}" + tar -czf "${NAME}-v${VERSION}.tar.gz" "${NAME}-v${VERSION}" + rm -r "${NAME}-v${VERSION}" + + # Checksum the release tarball. + sha256 "${NAME}-v${VERSION}.tar.gz" > "${NAME}-v${VERSION}.tar.gz.sha256" + + # Sign the release tarball. + if [ ! -z "${SIGNIFY_SECRET}" ]; then + signify -S -s "${SIGNIFY_SECRET}" \ + -m "${NAME}-v${VERSION}.tar.gz" \ + -x "${NAME}-v${VERSION}.tar.gz.sig" + else + echo "Warning: SIGNIFY_SECRET not net." + echo "The built tarball will not be signed." + fi +} + +recipe_patch() { + # If the user has not set their MXID, try to deduce one from + # their system. + if [ -z "$MXID" ]; then + MXID="@${USER}:$(hostname)" + fi + + # If the user has not set their EDITOR, use a safe default. + # (vi should be available on any POSIX system) + if [ -z "$EDITOR" ]; then + EDITOR=vi + fi + + NORMALIZED_MXID=$(echo "$MXID" | sed -e 's/@//g' -e 's/:/-/g') + PATCH_FILE="${NORMALIZED_MXID}_$(date +%s).patch" + + # Generate the actual patch file + # Here we write some nice headers, and then do a cvs diff. + ( + printf 'From: "%s" <%s>\n' "$DISPLAY_NAME" "$MXID" + echo "Date: $(date)" + echo "Subject: " + echo + cvs -q diff -uNp $PATCHSET | grep -v '^\? ' + ) >"$PATCH_FILE" + + "$EDITOR" "$PATCH_FILE" + echo "$PATCH_FILE" +} + +recipe_diff() { + tmp_patch="/tmp/${NAME}-$(date +%s).patch" + cvs -q diff -uNp $PATCHSET >"$tmp_patch" + if [ -z "$PAGER" ]; then + PAGER="less -F" + fi + + $PAGER "$tmp_patch" + rm "$tmp_patch" +} + +# Execute the user-specified recipes. +for recipe in $@; do + recipe_$recipe +done + +# If no recipe was provided, run a build. +if [ -z "$1" ]; then + recipe_build +fi diff --git a/src/Args.c b/src/Args.c new file mode 100644 index 0000000..c9f3000 --- /dev/null +++ b/src/Args.c @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include +#include + +void +ArgParseStateInit(ArgParseState * state) +{ + state->optPos = 1; + state->optErr = 1; + state->optInd = 1; + state->optOpt = 0; + state->optArg = NULL; +} + +int +ArgParse(ArgParseState * state, Array * args, const char *optStr) +{ + const char *arg; + + arg = ArrayGet(args, state->optInd); + + if (arg && StrEquals(arg, "--")) + { + state->optInd++; + return -1; + } + else if (!arg || arg[0] != '-' || !isalnum((unsigned char) arg[1])) + { + return -1; + } + else + { + const char *opt = strchr(optStr, arg[state->optPos]); + + state->optOpt = arg[state->optPos]; + + if (!opt) + { + if (state->optErr && *optStr != ':') + { + Log(LOG_ERR, "Illegal option: %c", ArrayGet(args, 0), state->optOpt); + } + if (!arg[++state->optPos]) + { + state->optInd++; + state->optPos = 1; + } + return '?'; + } + else if (opt[1] == ':') + { + if (arg[state->optPos + 1]) + { + state->optArg = (char *) arg + state->optPos + 1; + state->optInd++; + state->optPos = 1; + return state->optOpt; + } + else if (ArrayGet(args, state->optInd + 1)) + { + state->optArg = (char *) ArrayGet(args, state->optInd + 1); + state->optInd += 2; + state->optPos = 1; + return state->optOpt; + } + else + { + if (state->optErr && *optStr != ':') + { + Log(LOG_ERR, "Option requires an argument: %c", state->optOpt); + } + if (!arg[++state->optPos]) + { + state->optInd++; + state->optPos = 1; + } + return *optStr == ':' ? ':' : '?'; + } + } + else + { + if (!arg[++state->optPos]) + { + state->optInd++; + state->optPos = 1; + } + return state->optOpt; + } + } +} diff --git a/src/Array.c b/src/Array.c new file mode 100644 index 0000000..948d9d9 --- /dev/null +++ b/src/Array.c @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#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, array->size, value); +} + +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 || array->size <= index) + { + 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, size_t index, void *value) +{ + 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; +} + +extern void * +ArraySet(Array * array, size_t index, void *value) +{ + void *oldValue; + + if (!value) + { + return ArrayDelete(array, index); + } + + if (!array) + { + return NULL; + } + + if (index >= array->size) + { + return NULL; + } + + oldValue = array->entries[index]; + array->entries[index] = value; + + return oldValue; +} + +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; +} + +static void +ArraySwap(Array * array, size_t i, size_t j) +{ + void *p = array->entries[i]; + + array->entries[i] = array->entries[j]; + array->entries[j] = p; +} + +static size_t +ArrayPartition(Array * array, size_t low, size_t high, int (*compare) (void *, void *)) +{ + void *pivot = array->entries[high]; + size_t i = low - 1; + size_t j; + + for (j = low; j <= high - 1; j++) + { + if (compare(array->entries[j], pivot) < 0) + { + i++; + ArraySwap(array, i, j); + } + } + ArraySwap(array, i + 1, high); + return i + 1; +} + +static void +ArrayQuickSort(Array * array, size_t low, size_t high, int (*compare) (void *, void *)) +{ + if (low < high) + { + size_t pi = ArrayPartition(array, low, high, compare); + + ArrayQuickSort(array, low, pi - 1, compare); + ArrayQuickSort(array, pi + 1, high, compare); + } +} + +void +ArraySort(Array * array, int (*compare) (void *, void *)) +{ + if (!array) + { + return; + } + ArrayQuickSort(array, 0, array->size, compare); +} + +/* Even though the following operations could be done using only the + * public Array API defined above, I opted for low-level struct + * manipulation because it allows much more efficient copying; we only + * allocate what we for sure need upfront, and don't have to + * re-allocate during the operation. */ + +Array * +ArrayFromVarArgs(size_t n, va_list ap) +{ + size_t i; + Array *arr = Malloc(sizeof(Array)); + + if (!arr) + { + return NULL; + } + + arr->size = n; + arr->allocated = n; + arr->entries = Malloc(sizeof(void *) * arr->allocated); + + if (!arr->entries) + { + Free(arr); + return NULL; + } + + for (i = 0; i < n; i++) + { + arr->entries[i] = va_arg(ap, void *); + } + + return arr; +} + +Array * +ArrayDuplicate(Array * arr) +{ + size_t i; + Array *arr2 = Malloc(sizeof(Array)); + + if (!arr2) + { + return NULL; + } + + arr2->size = arr->size; + arr2->allocated = arr->size; + arr2->entries = Malloc(sizeof(void *) * arr->allocated); + + if (!arr2->entries) + { + Free(arr2); + return NULL; + } + + for (i = 0; i < arr2->size; i++) + { + arr2->entries[i] = arr->entries[i]; + } + + return arr2; +} diff --git a/src/Base64.c b/src/Base64.c new file mode 100644 index 0000000..fc43be8 --- /dev/null +++ b/src/Base64.c @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#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/Cron.c b/src/Cron.c new file mode 100644 index 0000000..b6abdc6 --- /dev/null +++ b/src/Cron.c @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include + +struct Cron +{ + unsigned long tick; + Array *jobs; + pthread_mutex_t lock; + volatile unsigned int stop:1; + pthread_t thread; +}; + +typedef struct Job +{ + unsigned long interval; + unsigned long lastExec; + JobFunc *func; + void *args; +} Job; + +static Job * +JobCreate(long interval, JobFunc * func, void *args) +{ + Job *job; + + if (!func) + { + return NULL; + } + + job = Malloc(sizeof(Job)); + if (!job) + { + return NULL; + } + + job->interval = interval; + job->lastExec = 0; + job->func = func; + job->args = args; + + return job; +} + +static void * +CronThread(void *args) +{ + Cron *cron = args; + + while (!cron->stop) + { + size_t i; + unsigned long ts; /* tick start */ + unsigned long te; /* tick end */ + + pthread_mutex_lock(&cron->lock); + + ts = UtilServerTs(); + + for (i = 0; i < ArraySize(cron->jobs); i++) + { + Job *job = ArrayGet(cron->jobs, i); + + if (ts - job->lastExec > job->interval) + { + job->func(job->args); + job->lastExec = ts; + } + + if (!job->interval) + { + ArrayDelete(cron->jobs, i); + Free(job); + } + } + te = UtilServerTs(); + + pthread_mutex_unlock(&cron->lock); + + /* Only sleep if the jobs didn't overrun the tick */ + if (cron->tick > (te - ts)) + { + const unsigned long microTick = 100; + unsigned long remainingTick = cron->tick - (te - ts); + + /* Only sleep for microTick ms at a time because if the job + * scheduler is supposed to stop before the tick is up, we + * don't want to be stuck in a long sleep */ + while (remainingTick >= microTick && !cron->stop) + { + UtilSleepMillis(microTick); + remainingTick -= microTick; + } + + if (remainingTick && !cron->stop) + { + UtilSleepMillis(remainingTick); + } + } + } + + return NULL; +} + +Cron * +CronCreate(unsigned long tick) +{ + Cron *cron = Malloc(sizeof(Cron)); + + if (!cron) + { + return NULL; + } + + cron->jobs = ArrayCreate(); + if (!cron->jobs) + { + Free(cron); + return NULL; + } + + cron->tick = tick; + cron->stop = 1; + + pthread_mutex_init(&cron->lock, NULL); + + return cron; +} + +void +CronOnce(Cron * cron, JobFunc * func, void *args) +{ + Job *job; + + if (!cron || !func) + { + return; + } + + job = JobCreate(0, func, args); + if (!job) + { + return; + } + + pthread_mutex_lock(&cron->lock); + ArrayAdd(cron->jobs, job); + pthread_mutex_unlock(&cron->lock); +} + +void +CronEvery(Cron * cron, unsigned long interval, JobFunc * func, void *args) +{ + Job *job; + + if (!cron || !func) + { + return; + } + + job = JobCreate(interval, func, args); + if (!job) + { + return; + } + + pthread_mutex_lock(&cron->lock); + ArrayAdd(cron->jobs, job); + pthread_mutex_unlock(&cron->lock); +} + +void +CronStart(Cron * cron) +{ + if (!cron || !cron->stop) + { + return; + } + + cron->stop = 0; + + pthread_create(&cron->thread, NULL, CronThread, cron); +} + +void +CronStop(Cron * cron) +{ + if (!cron || cron->stop) + { + return; + } + + cron->stop = 1; + + pthread_join(cron->thread, NULL); +} + +void +CronFree(Cron * cron) +{ + size_t i; + + if (!cron) + { + return; + } + + CronStop(cron); + + pthread_mutex_lock(&cron->lock); + for (i = 0; i < ArraySize(cron->jobs); i++) + { + Free(ArrayGet(cron->jobs, i)); + } + + ArrayFree(cron->jobs); + pthread_mutex_unlock(&cron->lock); + pthread_mutex_destroy(&cron->lock); + + Free(cron); +} diff --git a/src/Db.c b/src/Db.c new file mode 100644 index 0000000..e777f63 --- /dev/null +++ b/src/Db.c @@ -0,0 +1,968 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +struct Db +{ + char *dir; + pthread_mutex_t lock; + + size_t cacheSize; + size_t maxCache; + HashMap *cache; + + /* + * The cache uses a double linked list (see DbRef + * below) to know which objects are most and least + * recently used. The following diagram helps me + * know what way all the pointers go, because it + * can get very confusing sometimes. For example, + * there's nothing stopping "next" from pointing to + * least recent, and "prev" from pointing to most + * recent, so hopefully this clarifies the pointer + * terminology used when dealing with the linked + * list: + * + * mostRecent leastRecent + * | prev prev | prev + * +---+ ---> +---+ ---> +---+ ---> NULL + * |ref| |ref| |ref| + * NULL <--- +---+ <--- +---+ <--- +---+ + * next next next + */ + DbRef *mostRecent; + DbRef *leastRecent; +}; + +struct DbRef +{ + HashMap *json; + + unsigned long ts; + size_t size; + + Array *name; + + DbRef *prev; + DbRef *next; + + int fd; + Stream *stream; +}; + +static void +StringArrayFree(Array * arr) +{ + size_t i; + + for (i = 0; i < ArraySize(arr); i++) + { + Free(ArrayGet(arr, i)); + } + + ArrayFree(arr); +} + +static ssize_t DbComputeSize(HashMap *); + +static ssize_t +DbComputeSizeOfValue(JsonValue * val) +{ + MemoryInfo *a; + ssize_t total = 0; + + size_t i; + + union + { + char *str; + Array *arr; + } u; + + if (!val) + { + return -1; + } + + a = MemoryInfoGet(val); + if (a) + { + total += MemoryInfoGetSize(a); + } + + switch (JsonValueType(val)) + { + case JSON_OBJECT: + total += DbComputeSize(JsonValueAsObject(val)); + break; + case JSON_ARRAY: + u.arr = JsonValueAsArray(val); + a = MemoryInfoGet(u.arr); + + if (a) + { + total += MemoryInfoGetSize(a); + } + + for (i = 0; i < ArraySize(u.arr); i++) + { + total += DbComputeSizeOfValue(ArrayGet(u.arr, i)); + } + break; + case JSON_STRING: + u.str = JsonValueAsString(val); + a = MemoryInfoGet(u.str); + if (a) + { + total += MemoryInfoGetSize(a); + } + break; + case JSON_NULL: + case JSON_INTEGER: + case JSON_FLOAT: + case JSON_BOOLEAN: + default: + /* These don't use any extra heap space */ + break; + } + return total; +} + +static ssize_t +DbComputeSize(HashMap * json) +{ + char *key; + JsonValue *val; + MemoryInfo *a; + size_t total; + + if (!json) + { + return -1; + } + + total = 0; + + a = MemoryInfoGet(json); + if (a) + { + total += MemoryInfoGetSize(a); + } + + while (HashMapIterate(json, &key, (void **) &val)) + { + a = MemoryInfoGet(key); + if (a) + { + total += MemoryInfoGetSize(a); + } + + total += DbComputeSizeOfValue(val); + } + + return total; +} + +static char * +DbHashKey(Array * args) +{ + size_t i; + char *str = NULL; + + for (i = 0; i < ArraySize(args); i++) + { + char *tmp = StrConcat(2, str, ArrayGet(args, i)); + + Free(str); + str = tmp; + } + + return str; +} + +static char * +DbDirName(Db * db, Array * args, size_t strip) +{ + size_t i; + char *str = StrConcat(2, db->dir, "/"); + + for (i = 0; i < ArraySize(args) - strip; i++) + { + char *tmp; + + tmp = StrConcat(3, str, ArrayGet(args, i), "/"); + + Free(str); + + str = tmp; + } + + return str; +} + +static char * +DbFileName(Db * db, Array * args) +{ + size_t i; + char *str = StrConcat(2, db->dir, "/"); + + for (i = 0; i < ArraySize(args); i++) + { + char *tmp; + char *arg = StrDuplicate(ArrayGet(args, i)); + size_t j = 0; + + /* Sanitize name to prevent directory traversal attacks */ + while (arg[j]) + { + switch (arg[j]) + { + case '/': + arg[j] = '_'; + break; + case '.': + arg[j] = '-'; + break; + default: + break; + } + j++; + } + + tmp = StrConcat(3, str, arg, + (i < ArraySize(args) - 1) ? "/" : ".json"); + + Free(arg); + Free(str); + + str = tmp; + } + + return str; +} + +static void +DbCacheEvict(Db * db) +{ + DbRef *ref = db->leastRecent; + DbRef *tmp; + + while (ref && db->cacheSize > db->maxCache) + { + char *hash; + + JsonFree(ref->json); + + hash = DbHashKey(ref->name); + HashMapDelete(db->cache, hash); + Free(hash); + + StringArrayFree(ref->name); + + db->cacheSize -= ref->size; + + if (ref->next) + { + ref->next->prev = ref->prev; + } + else + { + db->mostRecent = ref->prev; + } + + if (ref->prev) + { + ref->prev->next = ref->next; + } + else + { + db->leastRecent = ref->next; + } + + tmp = ref->next; + Free(ref); + + ref = tmp; + } +} + +Db * +DbOpen(char *dir, size_t cache) +{ + Db *db; + pthread_mutexattr_t attr; + + if (!dir) + { + return NULL; + } + + db = Malloc(sizeof(Db)); + if (!db) + { + return NULL; + } + + db->dir = dir; + db->maxCache = cache; + + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&db->lock, &attr); + pthread_mutexattr_destroy(&attr); + + db->mostRecent = NULL; + db->leastRecent = NULL; + db->cacheSize = 0; + + if (db->maxCache) + { + db->cache = HashMapCreate(); + if (!db->cache) + { + return NULL; + } + } + else + { + db->cache = NULL; + } + + return db; +} + +void +DbMaxCacheSet(Db * db, size_t cache) +{ + if (!db) + { + return; + } + + pthread_mutex_lock(&db->lock); + + db->maxCache = cache; + if (db->maxCache && !db->cache) + { + db->cache = HashMapCreate(); + db->cacheSize = 0; + } + + DbCacheEvict(db); + + pthread_mutex_unlock(&db->lock); +} + +void +DbClose(Db * db) +{ + if (!db) + { + return; + } + + pthread_mutex_lock(&db->lock); + + DbMaxCacheSet(db, 0); + DbCacheEvict(db); + HashMapFree(db->cache); + + pthread_mutex_unlock(&db->lock); + pthread_mutex_destroy(&db->lock); + + Free(db); +} + +static DbRef * +DbLockFromArr(Db * db, Array * args) +{ + char *file; + char *hash; + DbRef *ref; + struct flock lock; + + int fd; + Stream *stream; + + if (!db || !args) + { + return NULL; + } + + ref = NULL; + hash = NULL; + + pthread_mutex_lock(&db->lock); + + /* Check if the item is in the cache */ + hash = DbHashKey(args); + ref = HashMapGet(db->cache, hash); + file = DbFileName(db, args); + + fd = open(file, O_RDWR); + if (fd == -1) + { + if (ref) + { + HashMapDelete(db->cache, hash); + JsonFree(ref->json); + StringArrayFree(ref->name); + + db->cacheSize -= ref->size; + + if (ref->next) + { + ref->next->prev = ref->prev; + } + else + { + db->mostRecent = ref->prev; + } + + if (ref->prev) + { + ref->prev->next = ref->next; + } + else + { + db->leastRecent = ref->next; + } + + if (!db->leastRecent) + { + db->leastRecent = db->mostRecent; + } + + Free(ref); + } + ref = NULL; + goto finish; + } + + stream = StreamFd(fd); + + lock.l_start = 0; + lock.l_len = 0; + lock.l_type = F_WRLCK; + lock.l_whence = SEEK_SET; + + /* Lock the file on the disk */ + if (fcntl(fd, F_SETLK, &lock) < 0) + { + StreamClose(stream); + ref = NULL; + goto finish; + } + + if (ref) /* In cache */ + { + unsigned long diskTs = UtilLastModified(file); + + ref->fd = fd; + ref->stream = stream; + + if (diskTs > ref->ts) + { + /* File was modified on disk since it was cached */ + HashMap *json = JsonDecode(ref->stream); + + if (!json) + { + StreamClose(ref->stream); + ref = NULL; + goto finish; + } + + JsonFree(ref->json); + ref->json = json; + ref->ts = diskTs; + ref->size = DbComputeSize(ref->json); + } + + /* Float this ref to mostRecent */ + if (ref->next) + { + ref->next->prev = ref->prev; + + if (!ref->prev) + { + db->leastRecent = ref->next; + } + else + { + ref->prev->next = ref->next; + } + + ref->prev = db->mostRecent; + ref->next = NULL; + if (db->mostRecent) + { + db->mostRecent->next = ref; + } + db->mostRecent = ref; + } + + /* If there is no least recent, this is the only thing in the + * cache, so it is also least recent. */ + if (!db->leastRecent) + { + db->leastRecent = ref; + } + + /* The file on disk may be larger than what we have in memory, + * which may require items in cache to be evicted. */ + DbCacheEvict(db); + } + else + { + Array *name; + size_t i; + + /* Not in cache; load from disk */ + + ref = Malloc(sizeof(DbRef)); + if (!ref) + { + StreamClose(stream); + goto finish; + } + + ref->json = JsonDecode(stream); + + if (!ref->json) + { + Free(ref); + StreamClose(stream); + ref = NULL; + goto finish; + } + + ref->fd = fd; + ref->stream = stream; + + name = ArrayCreate(); + for (i = 0; i < ArraySize(args); i++) + { + ArrayAdd(name, StrDuplicate(ArrayGet(args, i))); + } + ref->name = name; + + if (db->cache) + { + ref->ts = UtilServerTs(); + ref->size = DbComputeSize(ref->json); + HashMapSet(db->cache, hash, ref); + db->cacheSize += ref->size; + + ref->next = NULL; + ref->prev = db->mostRecent; + if (db->mostRecent) + { + db->mostRecent->next = ref; + } + db->mostRecent = ref; + + if (!db->leastRecent) + { + db->leastRecent = ref; + } + + /* Adding this item to the cache may case it to grow too + * large, requiring some items to be evicted */ + DbCacheEvict(db); + } + } + +finish: + if (!ref) + { + pthread_mutex_unlock(&db->lock); + } + + Free(file); + Free(hash); + + return ref; +} + +DbRef * +DbCreate(Db * db, size_t nArgs,...) +{ + Stream *fp; + char *file; + char *dir; + va_list ap; + Array *args; + DbRef *ret; + + if (!db) + { + return NULL; + } + + va_start(ap, nArgs); + args = ArrayFromVarArgs(nArgs, ap); + va_end(ap); + + if (!args) + { + return NULL; + } + + pthread_mutex_lock(&db->lock); + + file = DbFileName(db, args); + + if (UtilLastModified(file)) + { + Free(file); + ArrayFree(args); + pthread_mutex_unlock(&db->lock); + return NULL; + } + + dir = DbDirName(db, args, 1); + if (UtilMkdir(dir, 0750) < 0) + { + Free(file); + ArrayFree(args); + Free(dir); + pthread_mutex_unlock(&db->lock); + return NULL; + } + + Free(dir); + + fp = StreamOpen(file, "w"); + Free(file); + if (!fp) + { + ArrayFree(args); + pthread_mutex_unlock(&db->lock); + return NULL; + } + + StreamPuts(fp, "{}"); + StreamClose(fp); + + /* DbLockFromArr() will lock again for us */ + pthread_mutex_unlock(&db->lock); + + ret = DbLockFromArr(db, args); + + ArrayFree(args); + + return ret; +} + +int +DbDelete(Db * db, size_t nArgs,...) +{ + va_list ap; + Array *args; + char *file; + char *hash; + int ret = 1; + DbRef *ref; + + if (!db) + { + return 0; + } + + va_start(ap, nArgs); + args = ArrayFromVarArgs(nArgs, ap); + va_end(ap); + + pthread_mutex_lock(&db->lock); + + hash = DbHashKey(args); + file = DbFileName(db, args); + + ref = HashMapGet(db->cache, hash); + if (ref) + { + HashMapDelete(db->cache, hash); + JsonFree(ref->json); + StringArrayFree(ref->name); + + db->cacheSize -= ref->size; + + if (ref->next) + { + ref->next->prev = ref->prev; + } + else + { + db->mostRecent = ref->prev; + } + + if (ref->prev) + { + ref->prev->next = ref->next; + } + else + { + db->leastRecent = ref->next; + } + + if (!db->leastRecent) + { + db->leastRecent = db->mostRecent; + } + + Free(ref); + } + + Free(hash); + + if (UtilLastModified(file)) + { + ret = remove(file) == 0; + } + + pthread_mutex_unlock(&db->lock); + + ArrayFree(args); + Free(file); + return ret; +} + +DbRef * +DbLock(Db * db, size_t nArgs,...) +{ + va_list ap; + Array *args; + DbRef *ret; + + va_start(ap, nArgs); + args = ArrayFromVarArgs(nArgs, ap); + va_end(ap); + + if (!args) + { + return NULL; + } + + ret = DbLockFromArr(db, args); + + ArrayFree(args); + + return ret; +} + +int +DbUnlock(Db * db, DbRef * ref) +{ + int destroy; + + if (!db || !ref) + { + return 0; + } + + lseek(ref->fd, 0L, SEEK_SET); + if (ftruncate(ref->fd, 0) < 0) + { + pthread_mutex_unlock(&db->lock); + Log(LOG_ERR, "Failed to truncate file on disk."); + Log(LOG_ERR, "Error on fd %d: %s", ref->fd, strerror(errno)); + return 0; + } + + JsonEncode(ref->json, ref->stream, JSON_DEFAULT); + StreamClose(ref->stream); + + if (db->cache) + { + char *key = DbHashKey(ref->name); + + if (HashMapGet(db->cache, key)) + { + db->cacheSize -= ref->size; + ref->size = DbComputeSize(ref->json); + db->cacheSize += ref->size; + + /* If this ref has grown significantly since we last + * computed its size, it may have filled the cache and + * require some items to be evicted. */ + DbCacheEvict(db); + + destroy = 0; + } + else + { + destroy = 1; + } + + Free(key); + } + else + { + destroy = 1; + } + + if (destroy) + { + JsonFree(ref->json); + StringArrayFree(ref->name); + + Free(ref); + } + + pthread_mutex_unlock(&db->lock); + return 1; +} + +int +DbExists(Db * db, size_t nArgs,...) +{ + va_list ap; + Array *args; + char *file; + int ret; + + va_start(ap, nArgs); + args = ArrayFromVarArgs(nArgs, ap); + va_end(ap); + + if (!args) + { + return 0; + } + + pthread_mutex_lock(&db->lock); + + file = DbFileName(db, args); + ret = UtilLastModified(file); + + pthread_mutex_unlock(&db->lock); + + Free(file); + ArrayFree(args); + + return ret; +} + +Array * +DbList(Db * db, size_t nArgs,...) +{ + Array *result; + Array *path; + DIR *files; + struct dirent *file; + char *dir; + va_list ap; + + if (!db || !nArgs) + { + return NULL; + } + + result = ArrayCreate(); + if (!result) + { + return NULL; + } + + va_start(ap, nArgs); + path = ArrayFromVarArgs(nArgs, ap); + dir = DbDirName(db, path, 0); + + pthread_mutex_lock(&db->lock); + + files = opendir(dir); + if (!files) + { + ArrayFree(path); + ArrayFree(result); + Free(dir); + pthread_mutex_unlock(&db->lock); + return NULL; + } + while ((file = readdir(files))) + { + size_t namlen = strlen(file->d_name); + + if (namlen > 5) + { + int nameOffset = namlen - 5; + + if (StrEquals(file->d_name + nameOffset, ".json")) + { + file->d_name[nameOffset] = '\0'; + ArrayAdd(result, StrDuplicate(file->d_name)); + } + } + } + closedir(files); + + ArrayFree(path); + Free(dir); + pthread_mutex_unlock(&db->lock); + + return result; +} + +void +DbListFree(Array * arr) +{ + StringArrayFree(arr); +} + +HashMap * +DbJson(DbRef * ref) +{ + return ref ? ref->json : NULL; +} + +int +DbJsonSet(DbRef * ref, HashMap * json) +{ + if (!ref || !json) + { + return 0; + } + + JsonFree(ref->json); + ref->json = JsonDuplicate(json); + return 1; +} diff --git a/src/HashMap.c b/src/HashMap.c new file mode 100644 index 0000000..620092d --- /dev/null +++ b/src/HashMap.c @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include + +#include +#include + +typedef struct HashMapBucket +{ + unsigned long hash; + char *key; + void *value; +} HashMapBucket; + +struct HashMap +{ + size_t count; + size_t capacity; + HashMapBucket **entries; + + unsigned long (*hashFunc) (const char *); + + float maxLoad; + size_t iterator; +}; + +static unsigned long +HashMapHashKey(const char *key) +{ + unsigned long hash = 2166136261u; + size_t i = 0; + + while (key[i]) + { + hash ^= (unsigned char) 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 = Malloc(map->capacity * sizeof(HashMapBucket *)); + if (!newEntries) + { + map->capacity /= 2; + return 0; + } + + memset(newEntries, 0, map->capacity * sizeof(HashMapBucket *)); + + 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->iterator = 0; + map->hashFunc = HashMapHashKey; + + map->entries = Malloc(map->capacity * sizeof(HashMapBucket *)); + if (!map->entries) + { + Free(map); + return NULL; + } + + memset(map->entries, 0, map->capacity * sizeof(HashMapBucket *)); + + return map; +} + +void * +HashMapDelete(HashMap * map, const char *key) +{ + unsigned long hash; + size_t index; + + if (!map || !key) + { + return NULL; + } + + hash = map->hashFunc(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]->key); + Free(map->entries[i]); + } + } + Free(map->entries); + Free(map); + } +} + +void * +HashMapGet(HashMap * map, const char *key) +{ + unsigned long hash; + size_t index; + + if (!map || !key) + { + return NULL; + } + + hash = map->hashFunc(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; +} + +int +HashMapIterateReentrant(HashMap * map, char **key, void **value, size_t * i) +{ + if (!map) + { + return 0; + } + + if (*i >= map->capacity) + { + *i = 0; + *key = NULL; + *value = NULL; + return 0; + } + + while (*i < map->capacity) + { + HashMapBucket *bucket = map->entries[*i]; + + *i = *i + 1; + + if (bucket && bucket->hash) + { + *key = bucket->key; + *value = bucket->value; + return 1; + } + } + + *i = 0; + return 0; +} + +int +HashMapIterate(HashMap * map, char **key, void **value) +{ + if (!map) + { + return 0; + } + else + { + return HashMapIterateReentrant(map, key, value, &map->iterator); + } +} + +void +HashMapMaxLoadSet(HashMap * map, float load) +{ + if (!map || (load > 1.0 || load <= 0)) + { + return; + } + + map->maxLoad = load; +} + +void +HashMapFunctionSet(HashMap * map, unsigned long (*hashFunc) (const char *)) +{ + if (!map || !hashFunc) + { + return; + } + + map->hashFunc = hashFunc; +} + +void * +HashMapSet(HashMap * map, char *key, void *value) +{ + unsigned long hash; + size_t index; + + if (!map || !key || !value) + { + return NULL; + } + + key = StrDuplicate(key); + if (!key) + { + return NULL; + } + + if (map->count + 1 > map->capacity * map->maxLoad) + { + HashMapGrow(map); + } + + hash = map->hashFunc(key); + index = hash % map->capacity; + + for (;;) + { + HashMapBucket *bucket = map->entries[index]; + + if (!bucket) + { + bucket = Malloc(sizeof(HashMapBucket)); + if (!bucket) + { + break; + } + + bucket->hash = hash; + bucket->key = key; + bucket->value = value; + map->entries[index] = bucket; + map->count++; + break; + } + + if (!bucket->hash) + { + bucket->hash = hash; + Free(bucket->key); + bucket->key = key; + bucket->value = value; + break; + } + + if (bucket->hash == hash) + { + void *oldValue = bucket->value; + + Free(bucket->key); + bucket->key = key; + + bucket->value = value; + return oldValue; + } + + index = (index + 1) % map->capacity; + } + + return NULL; +} + +void +HashMapIterateFree(char *key, void *value) +{ + if (key) + { + Free(key); + } + + if (value) + { + Free(value); + } +} diff --git a/src/HeaderParser.c b/src/HeaderParser.c new file mode 100644 index 0000000..9f03ac5 --- /dev/null +++ b/src/HeaderParser.c @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include + +#include +#include + +static int +HeaderConsumeWhitespace(HeaderExpr * expr) +{ + int c; + + while (1) + { + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_EOF; + expr->data.error.msg = "End of stream reached."; + expr->data.error.lineNo = expr->state.lineNo; + break; + } + + if (isspace(c)) + { + if (c == '\n') + { + expr->state.lineNo++; + } + } + else + { + break; + } + } + + return c; +} + +static char * +HeaderConsumeWord(HeaderExpr * expr) +{ + char *str = Malloc(16 * sizeof(char)); + int len = 16; + int i; + int c; + + if (!str) + { + return NULL; + } + + c = HeaderConsumeWhitespace(expr); + + i = 0; + str[i] = c; + i++; + + while (!isspace(c = StreamGetc(expr->state.stream))) + { + if (i >= len) + { + len *= 2; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = c; + i++; + } + + if (i >= len) + { + len++; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = '\0'; + + if (c != EOF) + { + StreamUngetc(expr->state.stream, c); + } + + return str; +} + +static char * +HeaderConsumeAlnum(HeaderExpr * expr) +{ + char *str = Malloc(16 * sizeof(char)); + int len = 16; + int i; + int c; + + if (!str) + { + return NULL; + } + + c = HeaderConsumeWhitespace(expr); + + i = 0; + str[i] = c; + i++; + + while (isalnum(c = StreamGetc(expr->state.stream))) + { + if (i >= len) + { + len *= 2; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = c; + i++; + } + + if (i >= len) + { + len++; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = '\0'; + + if (c != EOF) + { + StreamUngetc(expr->state.stream, c); + } + + return str; +} + +static char * +HeaderConsumeArg(HeaderExpr * expr) +{ + char *str = Malloc(16 * sizeof(char)); + int len = 16; + int i; + int c; + int block = 0; + + if (!str) + { + return NULL; + } + + c = HeaderConsumeWhitespace(expr); + + i = 0; + str[i] = c; + i++; + + while (((c = StreamGetc(expr->state.stream)) != ',' && c != ')') || block > 0) + { + if (i >= len) + { + len *= 2; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = c; + i++; + + if (c == '(') + { + block++; + } + else if (c == ')') + { + block--; + } + else if (c == '\n') + { + expr->state.lineNo++; + } + } + + if (i >= len) + { + len++; + str = Realloc(str, len * sizeof(char)); + } + + str[i] = '\0'; + + if (c != EOF) + { + StreamUngetc(expr->state.stream, c); + } + + return str; +} + +void +HeaderParse(Stream * stream, HeaderExpr * expr) +{ + int c; + + if (!expr) + { + return; + } + + if (!stream) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "NULL pointer to stream."; + expr->data.error.lineNo = -1; + return; + } + + if (expr->type == HP_DECLARATION && expr->data.declaration.args) + { + size_t i; + + for (i = 0; i < ArraySize(expr->data.declaration.args); i++) + { + Free(ArrayGet(expr->data.declaration.args, i)); + } + + ArrayFree(expr->data.declaration.args); + } + + expr->state.stream = stream; + if (!expr->state.lineNo) + { + expr->state.lineNo = 1; + } + + c = HeaderConsumeWhitespace(expr); + + if (StreamEof(stream) || StreamError(stream)) + { + expr->type = HP_EOF; + expr->data.error.msg = "End of stream reached."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + if (c == '/') + { + int i = 0; + + c = StreamGetc(expr->state.stream); + if (c != '*') + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected comment opening."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + expr->type = HP_COMMENT; + while (1) + { + if (i >= HEADER_EXPR_MAX - 1) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit exceeded while parsing comment."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated comment."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + if (c == '*') + { + c = StreamGetc(expr->state.stream); + if (c == '/') + { + expr->data.text[i] = '\0'; + break; + } + else + { + expr->data.text[i] = '*'; + i++; + expr->data.text[i] = c; + i++; + if (c == '\n') + { + expr->state.lineNo++; + } + } + } + else + { + expr->data.text[i] = c; + i++; + + if (c == '\n') + { + expr->state.lineNo++; + } + } + } + } + else if (c == '#') + { + int i = 0; + char *word; + + expr->type = HP_PREPROCESSOR_DIRECTIVE; + expr->data.text[i] = '#'; + i++; + + word = HeaderConsumeWord(expr); + + strncpy(expr->data.text + i, word, HEADER_EXPR_MAX - i - 1); + i += strlen(word); + + if (StrEquals(word, "include") || + StrEquals(word, "undef") || + StrEquals(word, "ifdef") || + StrEquals(word, "ifndef")) + { + /* Read one more word */ + Free(word); + word = HeaderConsumeWord(expr); + + if (i + strlen(word) + 1 >= HEADER_EXPR_MAX) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit reached parsing preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + } + else + { + strncpy(expr->data.text + i + 1, word, HEADER_EXPR_MAX - i - 1); + expr->data.text[i] = ' '; + } + + Free(word); + } + else if (StrEquals(word, "define") || + StrEquals(word, "if") || + StrEquals(word, "elif") || + StrEquals(word, "error")) + { + int pC = 0; + + Free(word); + expr->data.text[i] = ' '; + i++; + + while (1) + { + if (i >= HEADER_EXPR_MAX - 1) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit reached parsing preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + if (c == '\n') + { + expr->state.lineNo++; + if (pC != '\\') + { + expr->data.text[i] = '\0'; + break; + } + } + + expr->data.text[i] = c; + i++; + + pC = c; + } + } + else if (StrEquals(word, "else") || + StrEquals(word, "endif")) + { + /* Read no more words, that's the whole directive */ + } + else + { + Free(word); + + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unknown preprocessor directive."; + expr->data.error.lineNo = expr->state.lineNo; + } + } + else + { + char *word; + + StreamUngetc(expr->state.stream, c); + word = HeaderConsumeWord(expr); + + if (StrEquals(word, "typedef")) + { + int block = 0; + int i = 0; + + expr->type = HP_TYPEDEF; + strncpy(expr->data.text, word, HEADER_EXPR_MAX - 1); + i += strlen(word); + expr->data.text[i] = ' '; + i++; + + while (1) + { + if (i >= HEADER_EXPR_MAX - 1) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit exceeded while parsing typedef."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated typedef."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + expr->data.text[i] = c; + i++; + + if (c == '{') + { + block++; + } + else if (c == '}') + { + block--; + } + else if (c == '\n') + { + expr->state.lineNo++; + } + + if (block <= 0 && c == ';') + { + expr->data.text[i] = '\0'; + break; + } + } + } + else if (StrEquals(word, "extern")) + { + int wordLimit = sizeof(expr->data.declaration.returnType) - 8; + int wordLen; + + Free(word); + + word = HeaderConsumeWord(expr); + wordLen = strlen(word); + if (wordLen > wordLimit) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Return of declaration exceeds length limit."; + expr->data.error.lineNo = expr->state.lineNo; + } + else + { + int i = wordLen; + + expr->type = HP_GLOBAL; + strncpy(expr->data.global.type, word, wordLimit); + + if (StrEquals(word, "struct") || + StrEquals(word, "enum") || + StrEquals(word, "const") || + StrEquals(word, "unsigned")) + { + Free(word); + word = HeaderConsumeWord(expr); + wordLen = strlen(word); + expr->data.global.type[i] = ' '; + + strncpy(expr->data.global.type + i + 1, word, wordLen + 1); + i += wordLen + 1; + } + + Free(word); + + c = HeaderConsumeWhitespace(expr); + if (c == '*') + { + expr->data.global.type[i] = ' '; + + i++; + expr->data.global.type[i] = '*'; + + i++; + while ((c = HeaderConsumeWhitespace(expr)) == '*') + { + expr->data.global.type[i] = c; + + i++; + } + } + + StreamUngetc(expr->state.stream, c); + word = HeaderConsumeAlnum(expr); + + wordLen = strlen(word); + wordLimit = sizeof(expr->data.declaration.name) - 1; + + if (wordLen > wordLimit) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Function name too long."; + expr->data.error.lineNo = expr->state.lineNo; + } + else + { + strncpy(expr->data.global.name, word, wordLimit); + Free(word); + word = NULL; + + c = HeaderConsumeWhitespace(expr); + + if (c == ';') + { + /* That's the end of the global. */ + } + else if (c == '[') + { + /* Looks like we have an array. Slurp all the + * dimensions */ + int i = wordLen; + + expr->data.global.name[i] = '['; + + i++; + + while (1) + { + if (i >= HEADER_EXPR_MAX - wordLen) + { + expr->type = HP_PARSE_ERROR; + expr->data.error.msg = "Memory limit exceeded while parsing global array."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = StreamGetc(expr->state.stream); + + if (StreamEof(expr->state.stream) || StreamError(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Unterminated global array."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + if (c == ';') + { + expr->data.global.name[i] = '\0'; + + break; + } + else + { + expr->data.global.name[i] = c; + + i++; + } + } + } + else if (c == '(') + { + expr->type = HP_DECLARATION; + expr->data.declaration.args = ArrayCreate(); + do + { + word = HeaderConsumeArg(expr); + ArrayAdd(expr->data.declaration.args, word); + word = NULL; + } + while ((!StreamEof(expr->state.stream)) && ((c = HeaderConsumeWhitespace(expr)) != ')')); + + if (StreamEof(expr->state.stream)) + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "End of file reached before ')'."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + c = HeaderConsumeWhitespace(expr); + if (c != ';') + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected ';'."; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + } + else + { + expr->type = HP_SYNTAX_ERROR; + expr->data.error.msg = "Expected ';', '[', or '('"; + expr->data.error.lineNo = expr->state.lineNo; + return; + } + + } + } + } + else + { + /* Cope with preprocessor macro expansions at the top + * level. */ + expr->type = HP_UNKNOWN; + strncpy(expr->data.text, word, HEADER_EXPR_MAX); + } + + Free(word); + } +} diff --git a/src/Http.c b/src/Http.c new file mode 100644 index 0000000..cab1220 --- /dev/null +++ b/src/Http.c @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef CYTOPLASM_STRING_CHUNK +#define CYTOPLASM_STRING_CHUNK 64 +#endif + +const char * +HttpRequestMethodToString(const HttpRequestMethod method) +{ + switch (method) + { + case HTTP_GET: + return "GET"; + case HTTP_HEAD: + return "HEAD"; + case HTTP_POST: + return "POST"; + case HTTP_PUT: + return "PUT"; + case HTTP_DELETE: + return "DELETE"; + case HTTP_CONNECT: + return "CONNECT"; + case HTTP_OPTIONS: + return "OPTIONS"; + case HTTP_TRACE: + return "TRACE"; + case HTTP_PATCH: + return "PATCH"; + default: + return NULL; + } +} + +HttpRequestMethod +HttpRequestMethodFromString(const char *str) +{ + if (StrEquals(str, "GET")) + { + return HTTP_GET; + } + + if (StrEquals(str, "HEAD")) + { + return HTTP_HEAD; + } + + if (StrEquals(str, "POST")) + { + return HTTP_POST; + } + + if (StrEquals(str, "PUT")) + { + return HTTP_PUT; + } + + if (StrEquals(str, "DELETE")) + { + return HTTP_DELETE; + } + + if (StrEquals(str, "CONNECT")) + { + return HTTP_CONNECT; + } + + if (StrEquals(str, "OPTIONS")) + { + return HTTP_OPTIONS; + } + + if (StrEquals(str, "TRACE")) + { + return HTTP_TRACE; + } + + if (StrEquals(str, "PATCH")) + { + return HTTP_PATCH; + } + + return HTTP_METHOD_UNKNOWN; +} + +const char * +HttpStatusToString(const HttpStatus status) +{ + switch (status) + { + case HTTP_CONTINUE: + return "Continue"; + case HTTP_SWITCHING_PROTOCOLS: + return "Switching Protocols"; + case HTTP_EARLY_HINTS: + return "Early Hints"; + case HTTP_OK: + return "Ok"; + case HTTP_CREATED: + return "Created"; + case HTTP_ACCEPTED: + return "Accepted"; + case HTTP_NON_AUTHORITATIVE_INFORMATION: + return "Non-Authoritative Information"; + case HTTP_NO_CONTENT: + return "No Content"; + case HTTP_RESET_CONTENT: + return "Reset Content"; + case HTTP_PARTIAL_CONTENT: + return "Partial Content"; + case HTTP_MULTIPLE_CHOICES: + return "Multiple Choices"; + case HTTP_MOVED_PERMANENTLY: + return "Moved Permanently"; + case HTTP_FOUND: + return "Found"; + case HTTP_SEE_OTHER: + return "See Other"; + case HTTP_NOT_MODIFIED: + return "Not Modified"; + case HTTP_TEMPORARY_REDIRECT: + return "Temporary Redirect"; + case HTTP_PERMANENT_REDIRECT: + return "Permanent Redirect"; + case HTTP_BAD_REQUEST: + return "Bad Request"; + case HTTP_UNAUTHORIZED: + return "Unauthorized"; + case HTTP_FORBIDDEN: + return "Forbidden"; + case HTTP_NOT_FOUND: + return "Not Found"; + case HTTP_METHOD_NOT_ALLOWED: + return "Method Not Allowed"; + case HTTP_NOT_ACCEPTABLE: + return "Not Acceptable"; + case HTTP_PROXY_AUTH_REQUIRED: + return "Proxy Authentication Required"; + case HTTP_REQUEST_TIMEOUT: + return "Request Timeout"; + case HTTP_CONFLICT: + return "Conflict"; + case HTTP_GONE: + return "Gone"; + case HTTP_LENGTH_REQUIRED: + return "Length Required"; + case HTTP_PRECONDITION_FAILED: + return "Precondition Failed"; + case HTTP_PAYLOAD_TOO_LARGE: + return "Payload Too Large"; + case HTTP_URI_TOO_LONG: + return "URI Too Long"; + case HTTP_UNSUPPORTED_MEDIA_TYPE: + return "Unsupported Media Type"; + case HTTP_RANGE_NOT_SATISFIABLE: + return "Range Not Satisfiable"; + case HTTP_EXPECTATION_FAILED: + return "Expectation Failed"; + case HTTP_TEAPOT: + return "I'm a Teapot"; + case HTTP_UPGRADE_REQUIRED: + return "Upgrade Required"; + case HTTP_PRECONDITION_REQUIRED: + return "Precondition Required"; + case HTTP_TOO_MANY_REQUESTS: + return "Too Many Requests"; + case HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE: + return "Request Header Fields Too Large"; + case HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + return "Unavailable For Legal Reasons"; + case HTTP_INTERNAL_SERVER_ERROR: + return "Internal Server Error"; + case HTTP_NOT_IMPLEMENTED: + return "Not Implemented"; + case HTTP_BAD_GATEWAY: + return "Bad Gateway"; + case HTTP_SERVICE_UNAVAILABLE: + return "Service Unavailable"; + case HTTP_GATEWAY_TIMEOUT: + return "Gateway Timeout"; + case HTTP_VERSION_NOT_SUPPORTED: + return "Version Not Supported"; + case HTTP_VARIANT_ALSO_NEGOTIATES: + return "Variant Also Negotiates"; + case HTTP_NOT_EXTENDED: + return "Not Extended"; + case HTTP_NETWORK_AUTH_REQUIRED: + return "Network Authentication Required"; + default: + return NULL; + } +} + +char * +HttpUrlEncode(char *str) +{ + size_t size; + size_t len; + char *encoded; + + if (!str) + { + return NULL; + } + + size = CYTOPLASM_STRING_CHUNK; + len = 0; + encoded = Malloc(size); + if (!encoded) + { + return NULL; + } + + while (*str) + { + char c = *str; + + if (len >= size - 4) + { + char *tmp; + + size += CYTOPLASM_STRING_CHUNK; + tmp = Realloc(encoded, size); + if (!tmp) + { + Free(encoded); + return NULL; + } + + encoded = tmp; + } + + /* Control characters and extended characters */ + if (c <= 0x1F || c >= 0x7F) + { + goto percentEncode; + } + + /* Reserved and unsafe characters */ + switch (c) + { + case '$': + case '&': + case '+': + case ',': + case '/': + case ':': + case ';': + case '=': + case '?': + case '@': + case ' ': + case '"': + case '<': + case '>': + case '#': + case '%': + case '{': + case '}': + case '|': + case '\\': + case '^': + case '~': + case '[': + case ']': + case '`': + goto percentEncode; + break; + default: + encoded[len] = c; + len++; + str++; + continue; + } + +percentEncode: + encoded[len] = '%'; + len++; + snprintf(encoded + len, 3, "%2X", c); + len += 2; + + str++; + } + + encoded[len] = '\0'; + return encoded; +} + +char * +HttpUrlDecode(char *str) +{ + size_t i; + size_t inputLen; + char *decoded; + + if (!str) + { + return NULL; + } + + i = 0; + inputLen = strlen(str); + decoded = Malloc(inputLen + 1); + + if (!decoded) + { + return NULL; + } + + while (*str) + { + char c = *str; + + if (c == '%') + { + unsigned int d; + + str++; + + if (sscanf(str, "%2X", &d) != 1) + { + /* Decoding error */ + Free(decoded); + return NULL; + } + + if (!d) + { + /* Null character given, don't put that in the string. */ + continue; + } + + c = (char) d; + + str++; + } + + decoded[i] = c; + i++; + + str++; + } + + decoded[i] = '\0'; + + return decoded; +} + +HashMap * +HttpParamDecode(char *in) +{ + HashMap *params; + + if (!in) + { + return NULL; + } + + params = HashMapCreate(); + if (!params) + { + return NULL; + } + + while (*in) + { + char *buf; + size_t allocated; + size_t len; + + char *decKey; + char *decVal; + + /* Read in key */ + + allocated = CYTOPLASM_STRING_CHUNK; + buf = Malloc(allocated); + len = 0; + + while (*in && *in != '=') + { + if (len >= allocated - 1) + { + allocated += CYTOPLASM_STRING_CHUNK; + buf = Realloc(buf, allocated); + } + + buf[len] = *in; + len++; + in++; + } + + buf[len] = '\0'; + + /* Sanity check */ + if (*in != '=') + { + /* Malformed param */ + Free(buf); + HashMapFree(params); + return NULL; + } + + in++; + + /* Decode key */ + decKey = HttpUrlDecode(buf); + Free(buf); + + if (!decKey) + { + /* Decoding error */ + HashMapFree(params); + return NULL; + } + + /* Read in value */ + allocated = CYTOPLASM_STRING_CHUNK; + buf = Malloc(allocated); + len = 0; + + while (*in && *in != '&') + { + if (len >= allocated - 1) + { + allocated += CYTOPLASM_STRING_CHUNK; + buf = Realloc(buf, allocated); + } + + buf[len] = *in; + len++; + in++; + } + + buf[len] = '\0'; + + /* Decode value */ + decVal = HttpUrlDecode(buf); + Free(buf); + + if (!decVal) + { + /* Decoding error */ + HashMapFree(params); + return NULL; + } + + buf = HashMapSet(params, decKey, decVal); + if (buf) + { + Free(buf); + } + Free(decKey); + + if (*in == '&') + { + in++; + continue; + } + else + { + break; + } + } + + return params; +} + +char * +HttpParamEncode(HashMap * params) +{ + char *key; + char *val; + char *out = NULL; + + if (!params || !out) + { + return NULL; + } + + while (HashMapIterate(params, &key, (void *) &val)) + { + char *encKey; + char *encVal; + + encKey = HttpUrlEncode(key); + encVal = HttpUrlEncode(val); + + if (!encKey || !encVal) + { + /* Memory error */ + Free(encKey); + Free(encVal); + return NULL; + } + + /* TODO */ + + Free(encKey); + Free(encVal); + } + + return out; +} + +HashMap * +HttpParseHeaders(Stream * fp) +{ + HashMap *headers; + + char *line; + ssize_t lineLen; + size_t lineSize; + + char *headerKey; + char *headerValue; + + if (!fp) + { + return NULL; + } + + + headers = HashMapCreate(); + if (!headers) + { + return NULL; + } + + line = NULL; + lineLen = 0; + + while ((lineLen = UtilGetLine(&line, &lineSize, fp)) != -1) + { + char *headerPtr; + + ssize_t i; + size_t len; + + if (StrEquals(line, "\r\n") || StrEquals(line, "\n")) + { + break; + } + + for (i = 0; i < lineLen; i++) + { + if (line[i] == ':') + { + line[i] = '\0'; + break; + } + + line[i] = tolower((unsigned char) line[i]); + } + + len = i + 1; + headerKey = Malloc(len * sizeof(char)); + if (!headerKey) + { + goto error; + } + + strncpy(headerKey, line, len); + + headerPtr = line + i + 1; + + while (isspace((unsigned char) *headerPtr)) + { + headerPtr++; + } + + for (i = lineLen - 1; i > (line + lineLen) - headerPtr; i--) + { + if (!isspace((unsigned char) line[i])) + { + break; + } + line[i] = '\0'; + } + + len = strlen(headerPtr) + 1; + headerValue = Malloc(len * sizeof(char)); + if (!headerValue) + { + Free(headerKey); + goto error; + } + + strncpy(headerValue, headerPtr, len); + + HashMapSet(headers, headerKey, headerValue); + Free(headerKey); + } + + Free(line); + return headers; + +error: + Free(line); + + while (HashMapIterate(headers, &headerKey, (void **) &headerValue)) + { + Free(headerValue); + } + + HashMapFree(headers); + + return NULL; +} diff --git a/src/HttpClient.c b/src/HttpClient.c new file mode 100644 index 0000000..2857016 --- /dev/null +++ b/src/HttpClient.c @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +struct HttpClientContext +{ + HashMap *responseHeaders; + Stream *stream; +}; + +HttpClientContext * +HttpRequest(HttpRequestMethod method, int flags, unsigned short port, char *host, char *path) +{ + HttpClientContext *context; + + int sd = -1; + struct addrinfo hints, *res, *res0; + int error; + + char serv[8]; + + if (!method || !host || !path) + { + return NULL; + } + +#ifndef TLS_IMPL + if (flags & HTTP_FLAG_TLS) + { + return NULL; + } +#endif + + if (!port) + { + if (flags & HTTP_FLAG_TLS) + { + strcpy(serv, "https"); + } + else + { + strcpy(serv, "www"); + } + } + else + { + snprintf(serv, sizeof(serv), "%hu", port); + } + + + context = Malloc(sizeof(HttpClientContext)); + if (!context) + { + return NULL; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(host, serv, &hints, &res0); + + if (error) + { + Free(context); + return NULL; + } + + for (res = res0; res; res = res->ai_next) + { + sd = socket(res->ai_family, res->ai_socktype, + res->ai_protocol); + + if (sd < 0) + { + continue; + } + + if (connect(sd, res->ai_addr, res->ai_addrlen) < 0) + { + close(sd); + sd = -1; + continue; + } + + break; + } + + if (sd < 0) + { + Free(context); + return NULL; + } + + freeaddrinfo(res0); + +#ifdef TLS_IMPL + if (flags & HTTP_FLAG_TLS) + { + context->stream = TlsClientStream(sd, host); + } + else + { + context->stream = StreamFd(sd); + } +#else + context->stream = StreamFd(sd); +#endif + + if (!context->stream) + { + Free(context); + close(sd); + return NULL; + } + + StreamPrintf(context->stream, "%s %s HTTP/1.0\r\n", + HttpRequestMethodToString(method), path); + + HttpRequestHeader(context, "Connection", "close"); + HttpRequestHeader(context, "User-Agent", LIB_NAME "/" LIB_VERSION); + HttpRequestHeader(context, "Host", host); + + return context; +} + +void +HttpRequestHeader(HttpClientContext * context, char *key, char *val) +{ + if (!context || !key || !val) + { + return; + } + + StreamPrintf(context->stream, "%s: %s\r\n", key, val); +} + +void +HttpRequestSendHeaders(HttpClientContext * context) +{ + if (!context) + { + return; + } + + StreamPuts(context->stream, "\r\n"); + StreamFlush(context->stream); +} + +HttpStatus +HttpRequestSend(HttpClientContext * context) +{ + HttpStatus status; + + char *line = NULL; + ssize_t lineLen; + size_t lineSize = 0; + char *tmp; + + if (!context) + { + return 0; + } + + StreamFlush(context->stream); + + lineLen = UtilGetLine(&line, &lineSize, context->stream); + + while (lineLen == -1 && errno == EAGAIN) + { + StreamClearError(context->stream); + lineLen = UtilGetLine(&line, &lineSize, context->stream); + } + + if (lineLen == -1) + { + return 0; + } + + /* Line must contain at least "HTTP/x.x xxx" */ + if (lineLen < 12) + { + return 0; + } + + if (!(strncmp(line, "HTTP/1.0", 8) == 0 || + strncmp(line, "HTTP/1.1", 8) == 0)) + { + return 0; + } + + tmp = line + 9; + + while (isspace((unsigned char) *tmp) && *tmp != '\0') + { + tmp++; + } + + if (!*tmp) + { + return 0; + } + + status = atoi(tmp); + + if (!status) + { + return 0; + } + + context->responseHeaders = HttpParseHeaders(context->stream); + if (!context->responseHeaders) + { + return 0; + } + + return status; +} + +HashMap * +HttpResponseHeaders(HttpClientContext * context) +{ + if (!context) + { + return NULL; + } + + return context->responseHeaders; +} + +Stream * +HttpClientStream(HttpClientContext * context) +{ + if (!context) + { + return NULL; + } + + return context->stream; +} + +void +HttpClientContextFree(HttpClientContext * context) +{ + char *key; + void *val; + + if (!context) + { + return; + } + + while (HashMapIterate(context->responseHeaders, &key, &val)) + { + Free(val); + } + + HashMapFree(context->responseHeaders); + + StreamClose(context->stream); + Free(context); +} diff --git a/src/HttpRouter.c b/src/HttpRouter.c new file mode 100644 index 0000000..87ddae0 --- /dev/null +++ b/src/HttpRouter.c @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include +#include + +#define REG_FLAGS (REG_EXTENDED) +#define REG_MAX_SUB 8 + +typedef struct RouteNode +{ + HttpRouteFunc *exec; + HashMap *children; + + regex_t regex; +} RouteNode; + +struct HttpRouter +{ + RouteNode *root; +}; + +static RouteNode * +RouteNodeCreate(char *regex, HttpRouteFunc * exec) +{ + RouteNode *node; + + if (!regex) + { + return NULL; + } + + node = Malloc(sizeof(RouteNode)); + + if (!node) + { + return NULL; + } + + node->children = HashMapCreate(); + if (!node->children) + { + Free(node); + return NULL; + } + + /* Force the regex to match the entire path part exactly. */ + regex = StrConcat(3, "^", regex, "$"); + if (!regex) + { + Free(node); + return NULL; + } + + if (regcomp(&node->regex, regex, REG_FLAGS) != 0) + { + HashMapFree(node->children); + Free(node); + Free(regex); + return NULL; + } + + node->exec = exec; + + Free(regex); + return node; +} + +static void +RouteNodeFree(RouteNode * node) +{ + char *key; + RouteNode *val; + + if (!node) + { + return; + } + + while (HashMapIterate(node->children, &key, (void **) &val)) + { + RouteNodeFree(val); + } + + HashMapFree(node->children); + + regfree(&node->regex); + + Free(node); +} + +HttpRouter * +HttpRouterCreate(void) +{ + HttpRouter *router = Malloc(sizeof(HttpRouter)); + + if (!router) + { + return NULL; + } + + router->root = RouteNodeCreate("/", NULL); + + return router; +} + +void +HttpRouterFree(HttpRouter * router) +{ + if (!router) + { + return; + } + + RouteNodeFree(router->root); + Free(router); +} + +int +HttpRouterAdd(HttpRouter * router, char *regPath, HttpRouteFunc * exec) +{ + RouteNode *node; + char *pathPart; + char *tmp; + + if (!router || !regPath || !exec) + { + return 0; + } + + if (StrEquals(regPath, "/")) + { + router->root->exec = exec; + return 1; + } + + regPath = StrDuplicate(regPath); + if (!regPath) + { + return 0; + } + + tmp = regPath; + node = router->root; + + while ((pathPart = strtok_r(tmp, "/", &tmp))) + { + RouteNode *tNode = HashMapGet(node->children, pathPart); + + if (!tNode) + { + tNode = RouteNodeCreate(pathPart, NULL); + RouteNodeFree(HashMapSet(node->children, pathPart, tNode)); + } + + node = tNode; + } + + node->exec = exec; + + Free(regPath); + + return 1; +} + +int +HttpRouterRoute(HttpRouter * router, char *path, void *args, void **ret) +{ + RouteNode *node; + char *pathPart; + char *tmp; + HttpRouteFunc *exec = NULL; + Array *matches; + size_t i; + int retval; + + if (!router || !path) + { + return 0; + } + + matches = ArrayCreate(); + if (!matches) + { + return 0; + } + + node = router->root; + + if (StrEquals(path, "/")) + { + exec = node->exec; + } + else + { + path = StrDuplicate(path); + tmp = path; + while ((pathPart = strtok_r(tmp, "/", &tmp))) + { + char *key; + RouteNode *val = NULL; + + regmatch_t pmatch[REG_MAX_SUB]; + + i = 0; + + while (HashMapIterateReentrant(node->children, &key, (void **) &val, &i)) + { + if (regexec(&val->regex, pathPart, REG_MAX_SUB, pmatch, 0) == 0) + { + break; + } + + val = NULL; + } + + if (!val) + { + exec = NULL; + break; + } + + node = val; + exec = node->exec; + + /* If we want to pass an arg, the match must be in parens */ + if (val->regex.re_nsub) + { + /* pmatch[0] is the whole string, not the first + * subexpression */ + for (i = 1; i < REG_MAX_SUB; i++) + { + if (pmatch[i].rm_so == -1) + { + break; + } + + ArrayAdd(matches, StrSubstr(pathPart, pmatch[i].rm_so, pmatch[i].rm_eo)); + } + } + } + Free(path); + } + + if (!exec) + { + retval = 0; + goto finish; + } + + if (ret) + { + *ret = exec(matches, args); + } + else + { + exec(matches, args); + } + + retval = 1; + +finish: + for (i = 0; i < ArraySize(matches); i++) + { + Free(ArrayGet(matches, i)); + } + ArrayFree(matches); + + return retval; +} diff --git a/src/HttpServer.c b/src/HttpServer.c new file mode 100644 index 0000000..e620983 --- /dev/null +++ b/src/HttpServer.c @@ -0,0 +1,753 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +static const char ENABLE = 1; + +struct HttpServer +{ + HttpServerConfig config; + int sd; + pthread_t socketThread; + + volatile unsigned int stop:1; + volatile unsigned int isRunning:1; + + Queue *connQueue; + pthread_mutex_t connQueueMutex; + + Array *threadPool; +}; + +struct HttpServerContext +{ + HashMap *requestHeaders; + HttpRequestMethod requestMethod; + char *requestPath; + HashMap *requestParams; + + HashMap *responseHeaders; + HttpStatus responseStatus; + + Stream *stream; +}; + +typedef struct HttpServerWorkerThreadArgs +{ + HttpServer *server; + int id; + pthread_t thread; +} HttpServerWorkerThreadArgs; + +static HttpServerContext * +HttpServerContextCreate(HttpRequestMethod requestMethod, + char *requestPath, HashMap * requestParams, Stream * stream) +{ + HttpServerContext *c; + + c = Malloc(sizeof(HttpServerContext)); + if (!c) + { + return NULL; + } + + c->responseHeaders = HashMapCreate(); + if (!c->responseHeaders) + { + Free(c->requestHeaders); + Free(c); + return NULL; + } + + c->requestMethod = requestMethod; + c->requestPath = requestPath; + c->requestParams = requestParams; + c->stream = stream; + c->responseStatus = HTTP_OK; + + return c; +} + +static void +HttpServerContextFree(HttpServerContext * c) +{ + char *key; + void *val; + + if (!c) + { + return; + } + + while (HashMapIterate(c->requestHeaders, &key, &val)) + { + Free(val); + } + HashMapFree(c->requestHeaders); + + while (HashMapIterate(c->responseHeaders, &key, &val)) + { + /* + * These are generated by code. As such, they may be either + * on the heap, or on the stack, depending on how they were + * added. + * + * Basically, if the memory API knows about a pointer, then + * it can be freed. If it doesn't know about a pointer, skip + * freeing it because it's probably a stack pointer. + */ + + if (MemoryInfoGet(val)) + { + Free(val); + } + } + + HashMapFree(c->responseHeaders); + + while (HashMapIterate(c->requestParams, &key, &val)) + { + Free(val); + } + + HashMapFree(c->requestParams); + + Free(c->requestPath); + StreamClose(c->stream); + + Free(c); +} + +HashMap * +HttpRequestHeaders(HttpServerContext * c) +{ + if (!c) + { + return NULL; + } + + return c->requestHeaders; +} + +HttpRequestMethod +HttpRequestMethodGet(HttpServerContext * c) +{ + if (!c) + { + return HTTP_METHOD_UNKNOWN; + } + + return c->requestMethod; +} + +char * +HttpRequestPath(HttpServerContext * c) +{ + if (!c) + { + return NULL; + } + + return c->requestPath; +} + +HashMap * +HttpRequestParams(HttpServerContext * c) +{ + if (!c) + { + return NULL; + } + + return c->requestParams; +} + +char * +HttpResponseHeader(HttpServerContext * c, char *key, char *val) +{ + if (!c) + { + return NULL; + } + + return HashMapSet(c->responseHeaders, key, val); +} + +void +HttpResponseStatus(HttpServerContext * c, HttpStatus status) +{ + if (!c) + { + return; + } + + c->responseStatus = status; +} + +HttpStatus +HttpResponseStatusGet(HttpServerContext * c) +{ + if (!c) + { + return HTTP_STATUS_UNKNOWN; + } + + return c->responseStatus; +} + +Stream * +HttpServerStream(HttpServerContext * c) +{ + if (!c) + { + return NULL; + } + + return c->stream; +} + +void +HttpSendHeaders(HttpServerContext * c) +{ + Stream *fp = c->stream; + + char *key; + char *val; + + StreamPrintf(fp, "HTTP/1.0 %d %s\n", c->responseStatus, HttpStatusToString(c->responseStatus)); + + while (HashMapIterate(c->responseHeaders, &key, (void **) &val)) + { + StreamPrintf(fp, "%s: %s\n", key, val); + } + + StreamPuts(fp, "\n"); +} + +static Stream * +DequeueConnection(HttpServer * server) +{ + Stream *fp; + + if (!server) + { + return NULL; + } + + pthread_mutex_lock(&server->connQueueMutex); + fp = QueuePop(server->connQueue); + pthread_mutex_unlock(&server->connQueueMutex); + + return fp; +} + +HttpServer * +HttpServerCreate(HttpServerConfig * config) +{ + HttpServer *server; + struct sockaddr_in sa; + + if (!config) + { + return NULL; + } + + if (!config->handler) + { + return NULL; + } + +#ifndef TLS_IMPL + if (config->flags & HTTP_FLAG_TLS) + { + return NULL; + } +#endif + + server = Malloc(sizeof(HttpServer)); + if (!server) + { + goto error; + } + + memset(server, 0, sizeof(HttpServer)); + + server->config = *config; + server->config.tlsCert = StrDuplicate(config->tlsCert); + server->config.tlsKey = StrDuplicate(config->tlsKey); + + server->threadPool = ArrayCreate(); + if (!server->threadPool) + { + goto error; + } + + server->connQueue = QueueCreate(config->maxConnections); + if (!server->connQueue) + { + goto error; + } + + if (pthread_mutex_init(&server->connQueueMutex, NULL) != 0) + { + goto error; + } + + server->sd = socket(AF_INET, SOCK_STREAM, 0); + + if (server->sd < 0) + { + goto error; + } + + if (fcntl(server->sd, F_SETFL, O_NONBLOCK) == -1) + { + goto error; + } + + if (setsockopt(server->sd, SOL_SOCKET, SO_REUSEADDR, &ENABLE, sizeof(int)) < 0) + { + goto error; + } + +#ifdef SO_REUSEPORT + if (setsockopt(server->sd, SOL_SOCKET, SO_REUSEPORT, &ENABLE, sizeof(int)) < 0) + { + goto error; + } +#endif + + memset(&sa, 0, sizeof(struct sockaddr_in)); + + sa.sin_family = AF_INET; + sa.sin_port = htons(config->port); + sa.sin_addr.s_addr = htonl(INADDR_ANY); + + if (bind(server->sd, (struct sockaddr *) & sa, sizeof(sa)) < 0) + { + goto error; + } + + if (listen(server->sd, config->maxConnections) < 0) + { + goto error; + } + + server->stop = 0; + server->isRunning = 0; + + return server; + +error: + if (server) + { + if (server->connQueue) + { + QueueFree(server->connQueue); + } + + pthread_mutex_destroy(&server->connQueueMutex); + + if (server->threadPool) + { + ArrayFree(server->threadPool); + } + + if (server->sd) + { + close(server->sd); + } + + Free(server); + } + return NULL; +} + +HttpServerConfig * +HttpServerConfigGet(HttpServer * server) +{ + if (!server) + { + return NULL; + } + + return &server->config; +} + +void +HttpServerFree(HttpServer * server) +{ + if (!server) + { + return; + } + + close(server->sd); + QueueFree(server->connQueue); + pthread_mutex_destroy(&server->connQueueMutex); + ArrayFree(server->threadPool); + Free(server->config.tlsCert); + Free(server->config.tlsKey); + Free(server); +} + +static void * +HttpServerWorkerThread(void *args) +{ + HttpServerWorkerThreadArgs *wArgs = (HttpServerWorkerThreadArgs *) args; + HttpServer *server = wArgs->server; + + while (!server->stop) + { + Stream *fp; + HttpServerContext *context; + + char *line = NULL; + size_t lineSize = 0; + ssize_t lineLen = 0; + + char *requestMethodPtr; + char *pathPtr; + char *requestPath; + char *requestProtocol; + + HashMap *requestParams; + ssize_t requestPathLen; + + ssize_t i = 0; + HttpRequestMethod requestMethod; + + long firstRead; + + fp = DequeueConnection(server); + + if (!fp) + { + /* Block for 1 millisecond before continuing so we don't + * murder the CPU if the queue is empty. */ + UtilSleepMillis(1); + continue; + } + + /* Get the first line of the request. + * + * Every once in a while, we're too fast for the client. When this + * happens, UtilGetLine() sets errno to EAGAIN. If we get + * EAGAIN, then clear the error on the stream and try again + * after a few ms. This is typically more than enough time for + * the client to send data. */ + firstRead = UtilServerTs(); + while ((lineLen = UtilGetLine(&line, &lineSize, fp)) == -1 + && errno == EAGAIN) + { + StreamClearError(fp); + + /* If the server is stopped, or it's been a while, just + * give up so we aren't wasting a thread on this client. */ + if (server->stop || (UtilServerTs() - firstRead) > 1000 * 30) + { + goto finish; + } + + UtilSleepMillis(5); + } + + if (lineLen == -1) + { + goto bad_request; + } + + requestMethodPtr = line; + for (i = 0; i < lineLen; i++) + { + if (line[i] == ' ') + { + line[i] = '\0'; + break; + } + } + + if (i == lineLen) + { + goto bad_request; + } + + requestMethod = HttpRequestMethodFromString(requestMethodPtr); + if (requestMethod == HTTP_METHOD_UNKNOWN) + { + goto bad_request; + } + + pathPtr = line + i + 1; + + for (i = 0; i < (line + lineLen) - pathPtr; i++) + { + if (pathPtr[i] == ' ') + { + pathPtr[i] = '\0'; + break; + } + } + + requestPathLen = i; + requestPath = Malloc(((requestPathLen + 1) * sizeof(char))); + strncpy(requestPath, pathPtr, requestPathLen + 1); + + requestProtocol = &pathPtr[i + 1]; + line[lineLen - 2] = '\0'; /* Get rid of \r and \n */ + + if (!StrEquals(requestProtocol, "HTTP/1.1") && !StrEquals(requestProtocol, "HTTP/1.0")) + { + Free(requestPath); + goto bad_request; + } + + /* Find request params */ + for (i = 0; i < requestPathLen; i++) + { + if (requestPath[i] == '?') + { + break; + } + } + + requestPath[i] = '\0'; + requestParams = (i == requestPathLen) ? NULL : HttpParamDecode(requestPath + i + 1); + + context = HttpServerContextCreate(requestMethod, requestPath, requestParams, fp); + if (!context) + { + Free(requestPath); + goto internal_error; + } + + context->requestHeaders = HttpParseHeaders(fp); + if (!context->requestHeaders) + { + goto internal_error; + } + + server->config.handler(context, server->config.handlerArgs); + + HttpServerContextFree(context); + fp = NULL; /* The above call will close this + * Stream */ + goto finish; + +internal_error: + StreamPuts(fp, "HTTP/1.0 500 Internal Server Error\n"); + StreamPuts(fp, "Connection: close\n"); + goto finish; + +bad_request: + StreamPuts(fp, "HTTP/1.0 400 Bad Request\n"); + StreamPuts(fp, "Connection: close\n"); + goto finish; + +finish: + Free(line); + if (fp) + { + StreamClose(fp); + } + } + + return NULL; +} + +static void * +HttpServerEventThread(void *args) +{ + HttpServer *server = (HttpServer *) args; + struct pollfd pollFds[1]; + Stream *fp; + size_t i; + + server->isRunning = 1; + server->stop = 0; + + pollFds[0].fd = server->sd; + pollFds[0].events = POLLIN; + + for (i = 0; i < server->config.threads; i++) + { + HttpServerWorkerThreadArgs *workerThread = Malloc(sizeof(HttpServerWorkerThreadArgs)); + + if (!workerThread) + { + /* TODO: Make the event thread return an error to the main + * thread */ + return NULL; + } + + workerThread->server = server; + workerThread->id = i; + + if (pthread_create(&workerThread->thread, NULL, HttpServerWorkerThread, workerThread) != 0) + { + /* TODO: Make the event thread return an error to the main + * thread */ + return NULL; + } + + ArrayAdd(server->threadPool, workerThread); + } + + while (!server->stop) + { + struct sockaddr_storage addr; + socklen_t addrLen = sizeof(addr); + int connFd; + int pollResult; + + + pollResult = poll(pollFds, 1, 500); + + if (pollResult < 0) + { + /* The poll either timed out, or was interrupted. */ + continue; + } + + pthread_mutex_lock(&server->connQueueMutex); + + /* Don't even accept connections if the queue is full. */ + if (!QueueFull(server->connQueue)) + { + connFd = accept(server->sd, (struct sockaddr *) & addr, &addrLen); + + if (connFd < 0) + { + pthread_mutex_unlock(&server->connQueueMutex); + continue; + } + +#ifdef TLS_IMPL + if (server->config.flags & HTTP_FLAG_TLS) + { + fp = TlsServerStream(connFd, server->config.tlsCert, server->config.tlsKey); + } + else + { + fp = StreamFd(connFd); + } +#else + fp = StreamFd(connFd); +#endif + + if (!fp) + { + pthread_mutex_unlock(&server->connQueueMutex); + close(connFd); + continue; + } + + QueuePush(server->connQueue, fp); + } + pthread_mutex_unlock(&server->connQueueMutex); + } + + for (i = 0; i < server->config.threads; i++) + { + HttpServerWorkerThreadArgs *workerThread = ArrayGet(server->threadPool, i); + + pthread_join(workerThread->thread, NULL); + Free(workerThread); + } + + while ((fp = DequeueConnection(server))) + { + StreamClose(fp); + } + + server->isRunning = 0; + + return NULL; +} + +int +HttpServerStart(HttpServer * server) +{ + if (!server) + { + return 0; + } + + if (server->isRunning) + { + return 1; + } + + if (pthread_create(&server->socketThread, NULL, HttpServerEventThread, server) != 0) + { + return 0; + } + + return 1; +} + +void +HttpServerJoin(HttpServer * server) +{ + if (!server) + { + return; + } + + pthread_join(server->socketThread, NULL); +} + +void +HttpServerStop(HttpServer * server) +{ + if (!server) + { + return; + } + + server->stop = 1; +} diff --git a/src/Io.c b/src/Io.c new file mode 100644 index 0000000..54d250c --- /dev/null +++ b/src/Io.c @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include + +#include +#include +#include +#include + +struct Io +{ + IoFunctions io; + void *cookie; +}; + +Io * +IoCreate(void *cookie, IoFunctions funcs) +{ + Io *io; + + /* Must have at least read or write */ + if (!funcs.read && !funcs.write) + { + return NULL; + } + + io = Malloc(sizeof(Io)); + + if (!io) + { + return NULL; + } + + io->cookie = cookie; + + io->io.read = funcs.read; + io->io.write = funcs.write; + io->io.seek = funcs.seek; + io->io.close = funcs.close; + + return io; +} + +ssize_t +IoRead(Io * io, void *buf, size_t nBytes) +{ + if (!io || !io->io.read) + { + errno = EBADF; + return -1; + } + + return io->io.read(io->cookie, buf, nBytes); +} + +ssize_t +IoWrite(Io * io, void *buf, size_t nBytes) +{ + if (!io || !io->io.write) + { + errno = EBADF; + return -1; + } + + return io->io.write(io->cookie, buf, nBytes); +} + +off_t +IoSeek(Io * io, off_t offset, int whence) +{ + if (!io) + { + errno = EBADF; + return -1; + } + + if (!io->io.seek) + { + errno = EINVAL; + return -1; + } + + return io->io.seek(io->cookie, offset, whence); +} + +int +IoClose(Io * io) +{ + int ret; + + if (!io) + { + errno = EBADF; + return -1; + } + + if (io->io.close) + { + ret = io->io.close(io->cookie); + } + else + { + ret = 0; + } + + Free(io); + + return ret; +} + +int +IoVprintf(Io * io, const char *fmt, va_list ap) +{ + char *buf = NULL; + size_t bufSize = 0; + FILE *fp; + + int ret; + + if (!io || !fmt) + { + return -1; + } + + fp = open_memstream(&buf, &bufSize); + if (!fp) + { + return -1; + } + + ret = vfprintf(fp, fmt, ap); + fclose(fp); + + if (ret >= 0) + { + ret = IoWrite(io, buf, bufSize); + } + + free(buf); /* Allocated by stdlib, not Memory + * API */ + return ret; +} + +int +IoPrintf(Io * io, const char *fmt,...) +{ + va_list ap; + int ret; + + va_start(ap, fmt); + ret = IoVprintf(io, fmt, ap); + va_end(ap); + + return ret; +} + +ssize_t +IoCopy(Io * in, Io * out) +{ + ssize_t nBytes = 0; + char buf[IO_BUFFER]; + ssize_t rRes; + ssize_t wRes; + + if (!in || !out) + { + errno = EBADF; + return -1; + } + + while ((rRes = IoRead(in, &buf, IO_BUFFER)) != 0) + { + if (rRes == -1) + { + return -1; + } + + wRes = IoWrite(out, &buf, rRes); + + if (wRes == -1) + { + return -1; + } + + nBytes += wRes; + } + + return nBytes; +} diff --git a/src/Io/IoFd.c b/src/Io/IoFd.c new file mode 100644 index 0000000..eaa1539 --- /dev/null +++ b/src/Io/IoFd.c @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include + +#include + +static ssize_t +IoReadFd(void *cookie, void *buf, size_t nBytes) +{ + int fd = *((int *) cookie); + + return read(fd, buf, nBytes); +} + +static ssize_t +IoWriteFd(void *cookie, void *buf, size_t nBytes) +{ + int fd = *((int *) cookie); + + return write(fd, buf, nBytes); +} + +static off_t +IoSeekFd(void *cookie, off_t offset, int whence) +{ + int fd = *((int *) cookie); + + return lseek(fd, offset, whence); +} + +static int +IoCloseFd(void *cookie) +{ + int fd = *((int *) cookie); + + Free(cookie); + return close(fd); +} + +Io * +IoFd(int fd) +{ + int *cookie = Malloc(sizeof(int)); + IoFunctions f; + + if (!cookie) + { + return NULL; + } + + *cookie = fd; + + f.read = IoReadFd; + f.write = IoWriteFd; + f.seek = IoSeekFd; + f.close = IoCloseFd; + + return IoCreate(cookie, f); +} + +Io * +IoOpen(const char *path, int flags, mode_t mode) +{ + int fd = open(path, flags, mode); + + if (fd == -1) + { + return NULL; + } + + return IoFd(fd); +} diff --git a/src/Io/IoFile.c b/src/Io/IoFile.c new file mode 100644 index 0000000..cdd40ab --- /dev/null +++ b/src/Io/IoFile.c @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include + +static ssize_t +IoReadFile(void *cookie, void *buf, size_t nBytes) +{ + FILE *fp = cookie; + + return fread(buf, 1, nBytes, fp); +} + +static ssize_t +IoWriteFile(void *cookie, void *buf, size_t nBytes) +{ + FILE *fp = cookie; + size_t res = fwrite(buf, 1, nBytes, fp); + + /* + * fwrite() may be buffered on some platforms, but at this low level, + * it should not be; buffering happens in Stream, not Io. + */ + fflush(fp); + + return res; +} + +static off_t +IoSeekFile(void *cookie, off_t offset, int whence) +{ + FILE *fp = cookie; + off_t ret = fseeko(fp, offset, whence); + + if (ret > -1) + { + return ftello(fp); + } + else + { + return ret; + } +} + +static int +IoCloseFile(void *cookie) +{ + FILE *fp = cookie; + + return fclose(fp); +} + +Io * +IoFile(FILE * fp) +{ + IoFunctions f; + + if (!fp) + { + return NULL; + } + + f.read = IoReadFile; + f.write = IoWriteFile; + f.seek = IoSeekFile; + f.close = IoCloseFile; + + return IoCreate(fp, f); +} diff --git a/src/Json.c b/src/Json.c new file mode 100644 index 0000000..a9f8ce9 --- /dev/null +++ b/src/Json.c @@ -0,0 +1,1384 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +struct JsonValue +{ + JsonType type; + union + { + HashMap *object; + Array *array; + char *string; + long integer; + double floating; + int boolean:1; + } as; +}; + +typedef enum JsonToken +{ + TOKEN_UNKNOWN, + TOKEN_COLON, + TOKEN_COMMA, + TOKEN_OBJECT_OPEN, + TOKEN_OBJECT_CLOSE, + TOKEN_ARRAY_OPEN, + TOKEN_ARRAY_CLOSE, + TOKEN_STRING, + TOKEN_INTEGER, + TOKEN_FLOAT, + TOKEN_BOOLEAN, + TOKEN_NULL, + TOKEN_EOF +} JsonToken; + +typedef struct JsonParserState +{ + Stream *stream; + + JsonToken tokenType; + char *token; + size_t tokenLen; +} JsonParserState; + +JsonType +JsonValueType(JsonValue * value) +{ + if (!value) + { + return JSON_NULL; + } + + return value->type; +} + +static JsonValue * +JsonValueAllocate(void) +{ + return Malloc(sizeof(JsonValue)); +} + +JsonValue * +JsonValueObject(HashMap * object) +{ + JsonValue *value; + + if (!object) + { + return NULL; + } + + value = JsonValueAllocate(); + if (!value) + { + return NULL; + } + + value->type = JSON_OBJECT; + value->as.object = object; + + return value; +} + +HashMap * +JsonValueAsObject(JsonValue * value) +{ + if (!value || value->type != JSON_OBJECT) + { + return NULL; + } + + return value->as.object; +} + + +JsonValue * +JsonValueArray(Array * array) +{ + JsonValue *value; + + if (!array) + { + return NULL; + } + + value = JsonValueAllocate(); + if (!value) + { + return NULL; + } + + value->type = JSON_ARRAY; + value->as.array = array; + + return value; +} + +Array * +JsonValueAsArray(JsonValue * value) +{ + if (!value || value->type != JSON_ARRAY) + { + return NULL; + } + + return value->as.array; +} + + +JsonValue * +JsonValueString(char *string) +{ + JsonValue *value; + + if (!string) + { + return NULL; + } + + value = JsonValueAllocate(); + if (!value) + { + return NULL; + } + + value->type = JSON_STRING; + + value->as.string = StrDuplicate(string); + if (!value->as.string) + { + Free(value); + return NULL; + } + + return value; +} + +char * +JsonValueAsString(JsonValue * value) +{ + if (!value || value->type != JSON_STRING) + { + return NULL; + } + + return value->as.string; +} + +JsonValue * +JsonValueInteger(long integer) +{ + JsonValue *value; + + value = JsonValueAllocate(); + if (!value) + { + return 0; + } + + value->type = JSON_INTEGER; + value->as.integer = integer; + + return value; +} + +long +JsonValueAsInteger(JsonValue * value) +{ + if (!value || value->type != JSON_INTEGER) + { + return 0; + } + + return value->as.integer; +} + + +JsonValue * +JsonValueFloat(double floating) +{ + JsonValue *value; + + value = JsonValueAllocate(); + if (!value) + { + return NULL; + } + + value->type = JSON_FLOAT; + value->as.floating = floating; + + return value; +} + +double +JsonValueAsFloat(JsonValue * value) +{ + if (!value || value->type != JSON_FLOAT) + { + return 0; + } + + return value->as.floating; +} + +JsonValue * +JsonValueBoolean(int boolean) +{ + JsonValue *value; + + value = JsonValueAllocate(); + if (!value) + { + return NULL; + } + + value->type = JSON_BOOLEAN; + value->as.boolean = boolean; + + return value; +} + +int +JsonValueAsBoolean(JsonValue * value) +{ + if (!value || value->type != JSON_BOOLEAN) + { + return 0; + } + + return value->as.boolean; +} + +JsonValue * +JsonValueNull(void) +{ + JsonValue *value; + + value = JsonValueAllocate(); + if (!value) + { + return NULL; + } + + value->type = JSON_NULL; + + return value; +} + +void +JsonValueFree(JsonValue * value) +{ + size_t i; + Array *arr; + + if (!value) + { + return; + } + + switch (value->type) + { + case JSON_OBJECT: + JsonFree(value->as.object); + break; + case JSON_ARRAY: + arr = value->as.array; + for (i = 0; i < ArraySize(arr); i++) + { + JsonValueFree((JsonValue *) ArrayGet(arr, i)); + } + ArrayFree(arr); + break; + case JSON_STRING: + Free(value->as.string); + break; + default: + break; + } + + Free(value); +} + +int +JsonEncodeString(const char *str, Stream * out) +{ + size_t i; + char c; + int length = 0; + + StreamPutc(out, '"'); + length++; + + i = 0; + while ((c = str[i]) != '\0') + { + switch (c) + { + case '\\': + case '"': + case '/': + StreamPutc(out, '\\'); + StreamPutc(out, c); + length += 2; + break; + case '\b': + StreamPuts(out, "\\b"); + length += 2; + break; + case '\t': + StreamPuts(out, "\\t"); + length += 2; + break; + case '\n': + StreamPuts(out, "\\n"); + length += 2; + break; + case '\f': + StreamPuts(out, "\\f"); + length += 2; + break; + case '\r': + StreamPuts(out, "\\r"); + length += 2; + break; + default: /* Assume UTF-8 input */ + /* + * RFC 4627: "All Unicode characters may be placed + * within the quotation marks except for the characters + * that must be escaped: quotation mark, reverse solidus, + * and the control characters (U+0000 through U+001F)." + * + * This technically covers the above cases for backspace, + * tab, newline, feed, and carriage return characters, + * but we can save bytes if we encode those as their + * more human-readable representation. + */ + if (c <= 0x001F) + { + length += StreamPrintf(out, "\\u%04x", c); + } + else + { + StreamPutc(out, c); + length++; + } + break; + } + i++; + } + + StreamPutc(out, '"'); + length++; + + return length; +} + +static char * +JsonDecodeString(Stream * in) +{ + const size_t strBlockSize = 16; + + size_t i; + size_t len; + size_t allocated; + char *str; + int c; + char a[5]; + + unsigned long utf8; + char *utf8Ptr; + + len = 0; + allocated = strBlockSize; + + str = Malloc(allocated * sizeof(char)); + if (!str) + { + return NULL; + } + + while ((c = StreamGetc(in)) != EOF) + { + if (c <= 0x001F) + { + /* Bad byte; these must be escaped */ + Free(str); + return NULL; + } + + switch (c) + { + case '"': + if (len >= allocated) + { + char *tmp; + + allocated += 1; + tmp = Realloc(str, allocated * sizeof(char)); + if (!tmp) + { + Free(str); + return NULL; + } + + str = tmp; + } + str[len] = '\0'; + return str; + break; + case '\\': + c = StreamGetc(in); + switch (c) + { + case '\\': + case '"': + case '/': + a[0] = c; + a[1] = '\0'; + break; + case 'b': + a[0] = '\b'; + a[1] = '\0'; + break; + case 't': + a[0] = '\t'; + a[1] = '\0'; + break; + case 'n': + a[0] = '\n'; + a[1] = '\0'; + break; + case 'f': + a[0] = '\f'; + a[1] = '\0'; + break; + case 'r': + a[0] = '\r'; + a[1] = '\0'; + break; + case 'u': + /* Read 4 characters into a */ + if (!StreamGets(in, a, sizeof(a))) + { + Free(str); + return NULL; + } + /* Interpret characters as a hex number */ + if (sscanf(a, "%04lx", &utf8) != 1) + { + /* Bad hex value */ + Free(str); + return NULL; + } + + if (utf8 == 0) + { + /* + * We read in a 0000, null. There is no + * circumstance in which putting a null + * character into our buffer will end well. + * + * There's also really no legitimate use + * for the null character in our JSON anyway; + * it's likely an attempted exploit. + * + * So lets just strip it out. Don't even + * include it in the string. There should be + * no harm in ignoring it. + */ + continue; + } + + /* Encode the 4-byte UTF-8 buffer into a series + * of 1-byte characters */ + utf8Ptr = StrUtf8Encode(utf8); + if (!utf8Ptr) + { + /* Mem error */ + Free(str); + return NULL; + } + + /* Move the output of StrUtf8Encode() into our + * local buffer */ + strncpy(a, utf8Ptr, sizeof(a) - 1); + Free(utf8Ptr); + break; + default: + /* Bad escape value */ + Free(str); + return NULL; + } + break; + default: + a[0] = c; + a[1] = '\0'; + break; + } + + /* Append buffer a */ + i = 0; + while (a[i] != '\0') + { + if (len >= allocated) + { + char *tmp; + + allocated += strBlockSize; + tmp = Realloc(str, allocated * sizeof(char)); + if (!tmp) + { + Free(str); + return NULL; + } + + str = tmp; + } + + str[len] = a[i]; + len++; + i++; + } + } + + Free(str); + return NULL; +} + +int +JsonEncodeValue(JsonValue * value, Stream * out, int level) +{ + size_t i; + size_t len; + Array *arr; + int length = 0; + + switch (value->type) + { + case JSON_OBJECT: + length += JsonEncode(value->as.object, out, level >= 0 ? level : level); + break; + case JSON_ARRAY: + arr = value->as.array; + len = ArraySize(arr); + + StreamPutc(out, '['); + length++; + for (i = 0; i < len; i++) + { + if (level >= 0) + { + length += StreamPrintf(out, "\n%*s", level + 2, ""); + } + length += JsonEncodeValue(ArrayGet(arr, i), out, level >= 0 ? level + 2 : level); + if (i < len - 1) + { + StreamPutc(out, ','); + length++; + } + } + + if (level >= 0) + { + length += StreamPrintf(out, "\n%*s", level, ""); + } + StreamPutc(out, ']'); + length++; + break; + case JSON_STRING: + length += JsonEncodeString(value->as.string, out); + break; + case JSON_INTEGER: + length += StreamPrintf(out, "%ld", value->as.integer); + break; + case JSON_FLOAT: + length += StreamPrintf(out, "%f", value->as.floating); + break; + case JSON_BOOLEAN: + if (value->as.boolean) + { + StreamPuts(out, "true"); + length += 4; + } + else + { + StreamPuts(out, "false"); + length += 5; + } + break; + case JSON_NULL: + StreamPuts(out, "null"); + length += 4; + break; + default: + return -1; + } + + return length; +} + +int +JsonEncode(HashMap * object, Stream * out, int level) +{ + size_t index; + size_t count; + char *key; + JsonValue *value; + int length; + + if (!object) + { + return -1; + } + + count = 0; + while (HashMapIterate(object, &key, (void **) &value)) + { + count++; + } + + /* The total number of bytes written */ + length = 0; + + StreamPutc(out, '{'); + length++; + + if (level >= 0) + { + StreamPutc(out, '\n'); + length++; + } + + index = 0; + while (HashMapIterate(object, &key, (void **) &value)) + { + if (level >= 0) + { + StreamPrintf(out, "%*s", level + 2, ""); + length += level + 2; + } + + length += JsonEncodeString(key, out); + + StreamPutc(out, ':'); + length++; + if (level >= 0) + { + StreamPutc(out, ' '); + length++; + } + + length += JsonEncodeValue(value, out, level >= 0 ? level + 2 : level); + + if (index < count - 1) + { + StreamPutc(out, ','); + length++; + } + + if (level >= 0) + { + StreamPutc(out, '\n'); + length++; + } + + index++; + } + + if (level >= 0) + { + StreamPrintf(out, "%*s", level, ""); + length += level; + } + StreamPutc(out, '}'); + length++; + + return length; +} + +void +JsonFree(HashMap * object) +{ + char *key; + JsonValue *value; + + if (!object) + { + return; + } + + while (HashMapIterate(object, &key, (void **) &value)) + { + JsonValueFree(value); + } + + HashMapFree(object); +} + +JsonValue * +JsonValueDuplicate(JsonValue * val) +{ + JsonValue *new; + size_t i; + + if (!val) + { + return NULL; + } + + new = JsonValueAllocate(); + if (!new) + { + return NULL; + } + + new->type = val->type; + + switch (val->type) + { + case JSON_OBJECT: + new->as.object = JsonDuplicate(val->as.object); + break; + case JSON_ARRAY: + new->as.array = ArrayCreate(); + for (i = 0; i < ArraySize(val->as.array); i++) + { + ArrayAdd(new->as.array, JsonValueDuplicate(ArrayGet(val->as.array, i))); + } + break; + case JSON_STRING: + new->as.string = StrDuplicate(val->as.string); + break; + case JSON_INTEGER: + case JSON_FLOAT: + case JSON_BOOLEAN: + /* These are by value, not by reference */ + new->as = val->as; + case JSON_NULL: + default: + break; + } + + return new; +} + +HashMap * +JsonDuplicate(HashMap * object) +{ + HashMap *new; + char *key; + JsonValue *val; + + if (!object) + { + return NULL; + } + + new = HashMapCreate(); + if (!new) + { + return NULL; + } + + while (HashMapIterate(object, &key, (void **) &val)) + { + HashMapSet(new, key, JsonValueDuplicate(val)); + } + + return new; +} + +static int +JsonConsumeWhitespace(JsonParserState * state) +{ + static const int nRetries = 5; + static const int delay = 2; + + int c; + int tries = 0; + int readFlg = 0; + + while (1) + { + c = StreamGetc(state->stream); + + if (StreamEof(state->stream)) + { + break; + } + + if (StreamError(state->stream)) + { + if (errno == EAGAIN) + { + StreamClearError(state->stream); + tries++; + + if (tries >= nRetries || readFlg) + { + break; + } + else + { + UtilSleepMillis(delay); + continue; + } + } + else + { + break; + } + } + + /* As soon as we've successfully read a byte, treat future + * EAGAINs as EOF, because some clients don't properly shutdown + * their sockets. */ + readFlg = 1; + tries = 0; + + if (!isspace(c)) + { + break; + } + } + + return c; +} + +static void +JsonTokenSeek(JsonParserState * state) +{ + int c = JsonConsumeWhitespace(state); + + if (StreamEof(state->stream)) + { + state->tokenType = TOKEN_EOF; + return; + } + + if (state->token) + { + Free(state->token); + state->token = NULL; + } + + switch (c) + { + case ':': + state->tokenType = TOKEN_COLON; + break; + case ',': + state->tokenType = TOKEN_COMMA; + break; + case '{': + state->tokenType = TOKEN_OBJECT_OPEN; + break; + case '}': + state->tokenType = TOKEN_OBJECT_CLOSE; + break; + case '[': + state->tokenType = TOKEN_ARRAY_OPEN; + break; + case ']': + state->tokenType = TOKEN_ARRAY_CLOSE; + break; + case '"': + state->token = JsonDecodeString(state->stream); + if (!state->token) + { + state->tokenType = TOKEN_EOF; + return; + } + state->tokenType = TOKEN_STRING; + state->tokenLen = strlen(state->token); + break; + default: + if (c == '-' || isdigit(c)) + { + int isFloat = 0; + size_t allocated = 16; + + state->tokenLen = 1; + state->token = Malloc(allocated); + if (!state->token) + { + state->tokenType = TOKEN_EOF; + return; + } + state->token[0] = c; + + while ((c = StreamGetc(state->stream)) != EOF) + { + if (c == '.') + { + if (state->tokenLen > 1 && !isFloat) + { + isFloat = 1; + } + else + { + state->tokenType = TOKEN_UNKNOWN; + return; + } + } + else if (!isdigit(c)) + { + StreamUngetc(state->stream, c); + break; + } + + if (state->tokenLen >= allocated) + { + char *tmp; + + allocated += 16; + + tmp = Realloc(state->token, allocated); + if (!tmp) + { + state->tokenType = TOKEN_EOF; + return; + } + + state->token = tmp; + } + + state->token[state->tokenLen] = c; + state->tokenLen++; + } + + if (state->token[state->tokenLen - 1] == '.') + { + state->tokenType = TOKEN_UNKNOWN; + return; + } + + state->token[state->tokenLen] = '\0'; + if (isFloat) + { + state->tokenType = TOKEN_FLOAT; + } + else + { + state->tokenType = TOKEN_INTEGER; + } + } + else + { + state->tokenLen = 8; + state->token = Malloc(state->tokenLen); + if (!state->token) + { + state->tokenType = TOKEN_EOF; + return; + } + + state->token[0] = c; + + switch (c) + { + case 't': + if (!StreamGets(state->stream, state->token + 1, 4)) + { + state->tokenType = TOKEN_EOF; + Free(state->token); + state->token = NULL; + return; + } + + if (StrEquals("true", state->token)) + { + state->tokenType = TOKEN_BOOLEAN; + state->tokenLen = 5; + } + else + { + state->tokenType = TOKEN_UNKNOWN; + Free(state->token); + state->token = NULL; + } + break; + case 'f': + if (!StreamGets(state->stream, state->token + 1, 5)) + { + state->tokenType = TOKEN_EOF; + Free(state->token); + state->token = NULL; + return; + } + + if (StrEquals("false", state->token)) + { + state->tokenType = TOKEN_BOOLEAN; + state->tokenLen = 6; + } + else + { + state->tokenType = TOKEN_UNKNOWN; + Free(state->token); + state->token = NULL; + } + break; + case 'n': + if (!StreamGets(state->stream, state->token + 1, 4)) + { + state->tokenType = TOKEN_EOF; + Free(state->token); + state->token = NULL; + return; + } + + if (StrEquals("null", state->token)) + { + state->tokenType = TOKEN_NULL; + } + else + { + state->tokenType = TOKEN_UNKNOWN; + Free(state->token); + state->token = NULL; + } + break; + default: + state->tokenType = TOKEN_UNKNOWN; + Free(state->token); + state->token = NULL; + break; + } + } + } +} + +static int +JsonExpect(JsonParserState * state, JsonToken token) +{ + return state->tokenType == token; +} + +static Array * + JsonDecodeArray(JsonParserState *); + +static HashMap * + JsonDecodeObject(JsonParserState *); + +static JsonValue * +JsonDecodeValue(JsonParserState * state) +{ + JsonValue *value; + char *strValue; + + switch (state->tokenType) + { + case TOKEN_OBJECT_OPEN: + value = JsonValueObject(JsonDecodeObject(state)); + break; + case TOKEN_ARRAY_OPEN: + value = JsonValueArray(JsonDecodeArray(state)); + break; + case TOKEN_STRING: + strValue = Malloc(state->tokenLen + 1); + if (!strValue) + { + return NULL; + } + strncpy(strValue, state->token, state->tokenLen + 1); + value = JsonValueString(strValue); + Free(strValue); + break; + case TOKEN_INTEGER: + value = JsonValueInteger(atol(state->token)); + break; + case TOKEN_FLOAT: + value = JsonValueFloat(atof(state->token)); + break; + case TOKEN_BOOLEAN: + value = JsonValueBoolean(state->token[0] == 't'); + break; + case TOKEN_NULL: + value = JsonValueNull(); + break; + default: + value = NULL; + break; + } + + return value; +} + +static HashMap * +JsonDecodeObject(JsonParserState * state) +{ + HashMap *obj = HashMapCreate(); + int comma = 0; + + if (!obj) + { + return NULL; + } + + do + { + JsonTokenSeek(state); + if (JsonExpect(state, TOKEN_STRING)) + { + char *key = Malloc(state->tokenLen + 1); + JsonValue *value; + + if (!key) + { + goto error; + } + strncpy(key, state->token, state->tokenLen + 1); + + JsonTokenSeek(state); + if (!JsonExpect(state, TOKEN_COLON)) + { + Free(key); + goto error; + } + + JsonTokenSeek(state); + value = JsonDecodeValue(state); + + if (!value) + { + Free(key); + goto error; + } + + /* If there's an existing value at this key, discard it. */ + JsonValueFree(HashMapSet(obj, key, value)); + Free(key); + + JsonTokenSeek(state); + + if (JsonExpect(state, TOKEN_COMMA)) + { + comma = 1; + continue; + } + + if (JsonExpect(state, TOKEN_OBJECT_CLOSE)) + { + break; + } + + goto error; + } + else if (!comma && JsonExpect(state, TOKEN_OBJECT_CLOSE)) + { + break; + } + else + { + goto error; + } + } while (!JsonExpect(state, TOKEN_EOF)); + + return obj; +error: + JsonFree(obj); + return NULL; +} + +static Array * +JsonDecodeArray(JsonParserState * state) +{ + Array *arr = ArrayCreate(); + size_t i; + int comma = 0; + + if (!arr) + { + return NULL; + } + + do + { + JsonValue *value; + + JsonTokenSeek(state); + + if (!comma && JsonExpect(state, TOKEN_ARRAY_CLOSE)) + { + break; + } + + value = JsonDecodeValue(state); + + if (!value) + { + goto error; + } + + ArrayAdd(arr, value); + + JsonTokenSeek(state); + + if (JsonExpect(state, TOKEN_COMMA)) + { + comma = 1; + continue; + } + + if (JsonExpect(state, TOKEN_ARRAY_CLOSE)) + { + break; + } + + goto error; + } while (!JsonExpect(state, TOKEN_EOF)); + + return arr; +error: + for (i = 0; i < ArraySize(arr); i++) + { + JsonValueFree((JsonValue *) ArrayGet(arr, i)); + } + ArrayFree(arr); + return NULL; +} + +HashMap * +JsonDecode(Stream * stream) +{ + HashMap *result; + + JsonParserState state; + + state.stream = stream; + state.token = NULL; + + JsonTokenSeek(&state); + if (!JsonExpect(&state, TOKEN_OBJECT_OPEN)) + { + return NULL; + } + + result = JsonDecodeObject(&state); + + if (state.token) + { + Free(state.token); + } + + return result; +} + +JsonValue * +JsonGet(HashMap * json, size_t nArgs,...) +{ + va_list argp; + + HashMap *tmp = json; + JsonValue *val = NULL; + size_t i; + + if (!json || !nArgs) + { + return NULL; + } + + va_start(argp, nArgs); + for (i = 0; i < nArgs - 1; i++) + { + char *key = va_arg(argp, char *); + + val = HashMapGet(tmp, key); + if (!val) + { + goto finish; + } + + if (JsonValueType(val) != JSON_OBJECT) + { + val = NULL; + goto finish; + } + + tmp = JsonValueAsObject(val); + } + + val = HashMapGet(tmp, va_arg(argp, char *)); + +finish: + va_end(argp); + return val; +} + +JsonValue * +JsonSet(HashMap * json, JsonValue * newVal, size_t nArgs,...) +{ + HashMap *tmp = json; + JsonValue *val = NULL; + size_t i; + + va_list argp; + + if (!json || !newVal || !nArgs) + { + return NULL; + } + + va_start(argp, nArgs); + + for (i = 0; i < nArgs - 1; i++) + { + char *key = va_arg(argp, char *); + + val = HashMapGet(tmp, key); + if (!val) + { + val = JsonValueObject(HashMapCreate()); + HashMapSet(tmp, key, val); + } + + if (JsonValueType(val) != JSON_OBJECT) + { + val = NULL; + goto finish; + } + + tmp = JsonValueAsObject(val); + } + + val = HashMapSet(tmp, va_arg(argp, char *), newVal); + +finish: + va_end(argp); + return val; +} diff --git a/src/Log.c b/src/Log.c new file mode 100644 index 0000000..f24ee23 --- /dev/null +++ b/src/Log.c @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#define LOG_TSBUFFER 64 + +struct LogConfig +{ + int level; + size_t indent; + Stream *out; + int flags; + char *tsFmt; + + pthread_mutex_t lock; +}; + +LogConfig *globalConfig = NULL; + +LogConfig * +LogConfigCreate(void) +{ + LogConfig *config; + + config = Malloc(sizeof(LogConfig)); + + if (!config) + { + return NULL; + } + + memset(config, 0, sizeof(LogConfig)); + + LogConfigLevelSet(config, LOG_INFO); + 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; +} + +LogConfig * +LogConfigGlobal(void) +{ + if (!globalConfig) + { + globalConfig = LogConfigCreate(); + } + + return globalConfig; +} + +void +LogConfigFlagClear(LogConfig * config, int flags) +{ + if (!config) + { + return; + } + + config->flags &= ~flags; +} + +static 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) +{ + if (!config) + { + return; + } + + Free(config); + + if (config == globalConfig) + { + globalConfig = NULL; + } +} + +void +LogConfigIndent(LogConfig * config) +{ + if (config) + { + config->indent += 2; + } +} + +void +LogConfigIndentSet(LogConfig * config, size_t indent) +{ + if (!config) + { + return; + } + + config->indent = indent; +} + +int +LogConfigLevelGet(LogConfig * config) +{ + if (!config) + { + return -1; + } + + return config->level; +} + +void +LogConfigLevelSet(LogConfig * config, int level) +{ + if (!config) + { + return; + } + + switch (level) + { + case LOG_ERR: + case LOG_WARNING: + case LOG_INFO: + case LOG_DEBUG: + config->level = level; + default: + break; + } +} + +void +LogConfigOutputSet(LogConfig * config, Stream * out) +{ + if (!config) + { + return; + } + + if (out) + { + config->out = out; + } + else + { + config->out = StreamStdout(); + } + +} + +void +LogConfigTimeStampFormatSet(LogConfig * config, char *tsFmt) +{ + if (config) + { + config->tsFmt = tsFmt; + } +} + +void +LogConfigUnindent(LogConfig * config) +{ + if (config && config->indent >= 2) + { + config->indent -= 2; + } +} + +void +Logv(LogConfig * config, int level, const char *msg, va_list argp) +{ + size_t i; + int doColor; + char indicator; + + /* + * 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; + } + + pthread_mutex_lock(&config->lock); + + if (LogConfigFlagGet(config, LOG_FLAG_SYSLOG)) + { + /* No further print logic is needed; syslog will handle it all + * for us. */ + vsyslog(level, msg, argp); + pthread_mutex_unlock(&config->lock); + return; + } + + doColor = LogConfigFlagGet(config, LOG_FLAG_COLOR) + && isatty(StreamFileno(config->out)); + + if (doColor) + { + char *ansi; + + switch (level) + { + case LOG_EMERG: + case LOG_ALERT: + case LOG_CRIT: + case LOG_ERR: + /* Bold Red */ + ansi = "\033[1;31m"; + break; + case LOG_WARNING: + /* Bold Yellow */ + ansi = "\033[1;33m"; + break; + case LOG_NOTICE: + /* Bold Magenta */ + ansi = "\033[1;35m"; + break; + case LOG_INFO: + /* Bold Green */ + ansi = "\033[1;32m"; + break; + case LOG_DEBUG: + /* Bold Blue */ + ansi = "\033[1;34m"; + break; + default: + ansi = ""; + break; + } + + StreamPuts(config->out, ansi); + } + + StreamPutc(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) + { + StreamPuts(config->out, tsBuffer); + if (!isspace((unsigned char) tsBuffer[tsLength - 1])) + { + StreamPutc(config->out, ' '); + } + } + } + + switch (level) + { + case LOG_EMERG: + indicator = '#'; + break; + case LOG_ALERT: + indicator = '@'; + break; + case LOG_CRIT: + indicator = 'X'; + break; + case LOG_ERR: + indicator = 'x'; + break; + case LOG_WARNING: + indicator = '!'; + break; + case LOG_NOTICE: + indicator = '~'; + break; + case LOG_INFO: + indicator = '>'; + break; + case LOG_DEBUG: + indicator = '*'; + break; + default: + indicator = '?'; + break; + } + + StreamPrintf(config->out, "%c]", indicator); + + if (doColor) + { + /* ANSI Reset */ + StreamPuts(config->out, "\033[0m"); + } + + StreamPutc(config->out, ' '); + for (i = 0; i < config->indent; i++) + { + StreamPutc(config->out, ' '); + } + + StreamVprintf(config->out, msg, argp); + StreamPutc(config->out, '\n'); + + StreamFlush(config->out); + + pthread_mutex_unlock(&config->lock); +} + +void +LogTo(LogConfig * config, int level, const char *fmt,...) +{ + va_list argp; + + va_start(argp, fmt); + Logv(config, level, fmt, argp); + va_end(argp); +} + +extern void +Log(int level, const char *fmt,...) +{ + va_list argp; + + va_start(argp, fmt); + Logv(LogConfigGlobal(), level, fmt, argp); + va_end(argp); +} diff --git a/src/Memory.c b/src/Memory.c new file mode 100644 index 0000000..09963fd --- /dev/null +++ b/src/Memory.c @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include +#include + +#ifndef MEMORY_TABLE_CHUNK +#define MEMORY_TABLE_CHUNK 256 +#endif + +#ifndef MEMORY_HEXDUMP_WIDTH +#define MEMORY_HEXDUMP_WIDTH 16 +#endif + +struct MemoryInfo +{ + size_t size; + const char *file; + int line; + void *pointer; +}; + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static void (*hook) (MemoryAction, MemoryInfo *, void *) = NULL; +static void *hookArgs = NULL; + +static MemoryInfo **allocations = NULL; +static size_t allocationsSize = 0; +static size_t allocationsLen = 0; + +static size_t +MemoryHash(void *p) +{ + return (((size_t) p) >> 2 * 7) % allocationsSize; +} + +static int +MemoryInsert(MemoryInfo * a) +{ + size_t hash; + + if (!allocations) + { + allocationsSize = MEMORY_TABLE_CHUNK; + allocations = calloc(allocationsSize, sizeof(void *)); + if (!allocations) + { + return 0; + } + } + + /* If the next insertion would cause the table to be at least 3/4 + * full, re-allocate and re-hash. */ + if ((allocationsLen + 1) >= ((allocationsSize * 3) >> 2)) + { + size_t i; + size_t tmpAllocationsSize = allocationsSize; + MemoryInfo **tmpAllocations; + + allocationsSize += MEMORY_TABLE_CHUNK; + tmpAllocations = calloc(allocationsSize, sizeof(void *)); + + if (!tmpAllocations) + { + return 0; + } + + for (i = 0; i < tmpAllocationsSize; i++) + { + if (allocations[i]) + { + hash = MemoryHash(allocations[i]->pointer); + + while (tmpAllocations[hash]) + { + hash = (hash + 1) % allocationsSize; + } + + tmpAllocations[hash] = allocations[i]; + } + } + + free(allocations); + allocations = tmpAllocations; + } + + hash = MemoryHash(a->pointer); + + while (allocations[hash]) + { + hash = (hash + 1) % allocationsSize; + } + + allocations[hash] = a; + allocationsLen++; + + return 1; +} + +static void +MemoryDelete(MemoryInfo * a) +{ + size_t hash = MemoryHash(a->pointer); + size_t count = 0; + + while (count <= allocationsSize) + { + if (allocations[hash] && allocations[hash] == a) + { + allocations[hash] = NULL; + allocationsLen--; + return; + } + else + { + hash = (hash + 1) % allocationsSize; + count++; + } + } +} + +void * +MemoryAllocate(size_t size, const char *file, int line) +{ + void *p; + MemoryInfo *a; + + pthread_mutex_lock(&lock); + + p = malloc(size); + if (!p) + { + pthread_mutex_unlock(&lock); + return NULL; + } + + a = malloc(sizeof(MemoryInfo)); + if (!a) + { + free(p); + pthread_mutex_unlock(&lock); + return NULL; + } + + a->size = size; + a->file = file; + a->line = line; + a->pointer = p; + + if (!MemoryInsert(a)) + { + free(a); + free(p); + pthread_mutex_unlock(&lock); + return NULL; + } + + if (hook) + { + hook(MEMORY_ALLOCATE, a, hookArgs); + } + + pthread_mutex_unlock(&lock); + return p; +} + +void * +MemoryReallocate(void *p, size_t size, const char *file, int line) +{ + MemoryInfo *a; + void *new = NULL; + + if (!p) + { + return MemoryAllocate(size, file, line); + } + + a = MemoryInfoGet(p); + if (a) + { + pthread_mutex_lock(&lock); + new = realloc(a->pointer, size); + if (new) + { + MemoryDelete(a); + a->size = size; + a->file = file; + a->line = line; + + a->pointer = new; + MemoryInsert(a); + + if (hook) + { + hook(MEMORY_REALLOCATE, a, hookArgs); + } + + } + pthread_mutex_unlock(&lock); + } + else if (hook) + { + a = malloc(sizeof(MemoryInfo)); + if (a) + { + a->size = 0; + a->file = file; + a->line = line; + a->pointer = p; + hook(MEMORY_BAD_POINTER, a, hookArgs); + free(a); + } + } + + + return new; +} + +void +MemoryFree(void *p, const char *file, int line) +{ + MemoryInfo *a; + + if (!p) + { + return; + } + + a = MemoryInfoGet(p); + if (a) + { + pthread_mutex_lock(&lock); + if (hook) + { + a->file = file; + a->line = line; + hook(MEMORY_FREE, a, hookArgs); + } + MemoryDelete(a); + free(a->pointer); + free(a); + + pthread_mutex_unlock(&lock); + } + else if (hook) + { + a = malloc(sizeof(MemoryInfo)); + if (a) + { + a->file = file; + a->line = line; + a->size = 0; + a->pointer = p; + hook(MEMORY_BAD_POINTER, a, hookArgs); + free(a); + } + } +} + +size_t +MemoryAllocated(void) +{ + size_t i; + size_t total = 0; + + pthread_mutex_lock(&lock); + + for (i = 0; i < allocationsSize; i++) + { + if (allocations[i]) + { + total += allocations[i]->size; + } + } + + pthread_mutex_unlock(&lock); + + return total; +} + +void +MemoryFreeAll(void) +{ + size_t i; + + pthread_mutex_lock(&lock); + + for (i = 0; i < allocationsSize; i++) + { + if (allocations[i]) + { + free(allocations[i]->pointer); + free(allocations[i]); + } + } + + free(allocations); + allocations = NULL; + allocationsSize = 0; + allocationsLen = 0; + + pthread_mutex_unlock(&lock); +} + +MemoryInfo * +MemoryInfoGet(void *p) +{ + size_t hash, count; + + pthread_mutex_lock(&lock); + + hash = MemoryHash(p); + + count = 0; + while (count <= allocationsSize) + { + if (!allocations[hash] || allocations[hash]->pointer != p) + { + hash = (hash + 1) % allocationsSize; + count++; + } + else + { + pthread_mutex_unlock(&lock); + return allocations[hash]; + } + } + + pthread_mutex_unlock(&lock); + return NULL; +} + +size_t +MemoryInfoGetSize(MemoryInfo * a) +{ + if (!a) + { + return 0; + } + + return a->size; +} + +const char * +MemoryInfoGetFile(MemoryInfo * a) +{ + if (!a) + { + return NULL; + } + + return a->file; +} + +int +MemoryInfoGetLine(MemoryInfo * a) +{ + if (!a) + { + return -1; + } + + return a->line; +} + +void * +MemoryInfoGetPointer(MemoryInfo * a) +{ + if (!a) + { + return NULL; + } + + return a->pointer; +} + +void + MemoryIterate(void (*iterFunc) (MemoryInfo *, void *), void *args) +{ + size_t i; + + pthread_mutex_lock(&lock); + + for (i = 0; i < allocationsSize; i++) + { + if (allocations[i]) + { + iterFunc(allocations[i], args); + } + } + + pthread_mutex_unlock(&lock); +} + +void + MemoryHook(void (*memHook) (MemoryAction, MemoryInfo *, void *), void *args) +{ + pthread_mutex_lock(&lock); + hook = memHook; + hookArgs = args; + pthread_mutex_unlock(&lock); +} + +void + MemoryHexDump(MemoryInfo * info, void (*printFunc) (size_t, char *, char *, void *), void *args) +{ + char hexBuf[(MEMORY_HEXDUMP_WIDTH * 2) + MEMORY_HEXDUMP_WIDTH + 1]; + char asciiBuf[MEMORY_HEXDUMP_WIDTH + 1]; + size_t pI = 0; + size_t hI = 0; + size_t aI = 0; + const unsigned char *pc; + + if (!info || !printFunc) + { + return; + } + + pc = MemoryInfoGetPointer(info); + + for (pI = 0; pI < MemoryInfoGetSize(info); pI++) + { + if (pI > 0 && pI % MEMORY_HEXDUMP_WIDTH == 0) + { + hexBuf[hI - 1] = '\0'; + asciiBuf[aI] = '\0'; + + printFunc(pI - MEMORY_HEXDUMP_WIDTH, hexBuf, asciiBuf, args); + + snprintf(hexBuf, 4, "%02x ", pc[pI]); + hI = 3; + + asciiBuf[0] = isprint(pc[pI]) ? pc[pI] : '.'; + asciiBuf[1] = '\0'; + aI = 1; + } + else + { + asciiBuf[aI] = isprint(pc[pI]) ? pc[pI] : '.'; + aI++; + + snprintf(hexBuf + hI, 4, "%02x ", pc[pI]); + hI += 3; + } + } + + hexBuf[hI] = '\0'; + hI--; + + while (hI < sizeof(hexBuf) - 2) + { + hexBuf[hI] = ' '; + hI++; + } + + while (aI < sizeof(asciiBuf) - 1) + { + asciiBuf[aI] = ' '; + aI++; + } + + hexBuf[hI] = '\0'; + asciiBuf[aI] = '\0'; + + printFunc(pI - ((pI % MEMORY_HEXDUMP_WIDTH) ? + (pI % MEMORY_HEXDUMP_WIDTH) : MEMORY_HEXDUMP_WIDTH), + hexBuf, asciiBuf, args); + printFunc(pI, NULL, NULL, args); +} diff --git a/src/Queue.c b/src/Queue.c new file mode 100644 index 0000000..dbb0595 --- /dev/null +++ b/src/Queue.c @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include + +struct Queue +{ + void **items; + size_t size; + size_t front; + size_t rear; +}; + +Queue * +QueueCreate(size_t size) +{ + Queue *q; + + if (!size) + { + /* Can't have a queue of length zero */ + return NULL; + } + + q = Malloc(sizeof(Queue)); + if (!q) + { + return NULL; + } + + q->items = Malloc(size * sizeof(void *)); + if (!q->items) + { + Free(q); + return NULL; + } + + q->size = size; + q->front = size + 1; + q->rear = size + 1; + + return q; +} + +void +QueueFree(Queue * q) +{ + if (q) + { + Free(q->items); + } + + Free(q); +} + +int +QueueFull(Queue * q) +{ + if (!q) + { + return 0; + } + + return ((q->front == q->rear + 1) || (q->front == 0 && q->rear == q->size - 1)); +} + +int +QueueEmpty(Queue * q) +{ + if (!q) + { + return 0; + } + + return q->front == q->size + 1; +} + +int +QueuePush(Queue * q, void *element) +{ + if (!q || !element) + { + return 0; + } + + if (QueueFull(q)) + { + return 0; + } + + if (q->front == q->size + 1) + { + q->front = 0; + } + + if (q->rear == q->size + 1) + { + q->rear = 0; + } + else + { + q->rear = (q->rear + 1) % q->size; + } + + q->items[q->rear] = element; + + return 1; +} + +void * +QueuePop(Queue * q) +{ + void *element; + + if (!q) + { + return NULL; + } + + if (QueueEmpty(q)) + { + return NULL; + } + + element = q->items[q->front]; + + if (q->front == q->rear) + { + q->front = q->size + 1; + q->rear = q->size + 1; + } + else + { + q->front = (q->front + 1) % q->size; + } + + return element; +} + +void * +QueuePeek(Queue * q) +{ + if (!q) + { + return NULL; + } + + if (QueueEmpty(q)) + { + return NULL; + } + + return q->items[q->front]; +} diff --git a/src/Rand.c b/src/Rand.c new file mode 100644 index 0000000..9236e4d --- /dev/null +++ b/src/Rand.c @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include +#include +#include + +#define RAND_STATE_VECTOR_LENGTH 624 +#define RAND_STATE_VECTOR_M 397 + +#define RAND_UPPER_MASK 0x80000000 +#define RAND_LOWER_MASK 0x7FFFFFFF +#define RAND_TEMPER_B 0x9D2C5680 +#define RAND_TEMPER_C 0xEFC60000 + +typedef struct RandState +{ + UInt32 mt[RAND_STATE_VECTOR_LENGTH]; + int index; +} RandState; + +static void +RandSeed(RandState * state, UInt32 seed) +{ + state->mt[0] = seed & 0xFFFFFFFF; + + for (state->index = 1; state->index < RAND_STATE_VECTOR_LENGTH; state->index++) + { + state->mt[state->index] = (6069 * state->mt[state->index - 1]) & 0xFFFFFFFF; + } +} + +static UInt32 +RandGenerate(RandState * state) +{ + static const UInt32 mag[2] = {0x0, 0x9908B0DF}; + + UInt32 result; + + if (state->index >= RAND_STATE_VECTOR_LENGTH || state->index < 0) + { + int kk; + + if (state->index >= RAND_STATE_VECTOR_LENGTH + 1 || state->index < 0) + { + RandSeed(state, 4357); + } + + for (kk = 0; kk < RAND_STATE_VECTOR_LENGTH - RAND_STATE_VECTOR_M; kk++) + { + result = (state->mt[kk] & RAND_UPPER_MASK) | (state->mt[kk + 1] & RAND_LOWER_MASK); + state->mt[kk] = state->mt[kk + RAND_STATE_VECTOR_M] ^ (result >> 1) ^ mag[result & 0x1]; + } + + for (; kk < RAND_STATE_VECTOR_LENGTH - 1; kk++) + { + result = (state->mt[kk] & RAND_UPPER_MASK) | (state->mt[kk + 1] & RAND_LOWER_MASK); + state->mt[kk] = state->mt[kk + (RAND_STATE_VECTOR_M - RAND_STATE_VECTOR_LENGTH)] ^ (result >> 1) ^ mag[result & 0x1]; + } + + result = (state->mt[RAND_STATE_VECTOR_LENGTH - 1] & RAND_UPPER_MASK) | (state->mt[0] & RAND_LOWER_MASK); + state->mt[RAND_STATE_VECTOR_LENGTH - 1] = state->mt[RAND_STATE_VECTOR_M - 1] ^ (result >> 1) ^ mag[result & 0x1]; + state->index = 0; + } + + result = state->mt[state->index++]; + result ^= (result >> 11); + result ^= (result << 7) & RAND_TEMPER_B; + result ^= (result << 15) & RAND_TEMPER_C; + result ^= (result >> 18); + + return result; +} + +static void +RandDestructor(void *p) +{ + Free(p); +} + +/* Generate random numbers using rejection sampling. The basic idea is + * to "reroll" if a number happens to be outside the range. However + * this could be extremely inefficient. + * + * Another idea would just be to "reroll" if the generated number ends up + * in the previously "biased" range, and THEN do a modulo. + * + * This would be far more efficient for small values of max, and fixes the + * bias issue. */ + +/* This algorithm therefore computes N random numbers generally in O(N) + * time, while being less biased. */ +void +RandIntN(int *buf, size_t size, unsigned int max) +{ + static pthread_key_t stateKey; + static int createdKey = 0; + + /* Limit the range to banish all previously biased results */ + const int allowed = RAND_MAX - RAND_MAX % max; + + RandState *state; + int tmp; + size_t i; + + if (!createdKey) + { + pthread_key_create(&stateKey, RandDestructor); + createdKey = 1; + } + + state = pthread_getspecific(stateKey); + + if (!state) + { + /* Generate a seed from the system time, PID, and TID */ + UInt32 seed = UtilServerTs() ^ getpid() ^ (unsigned long) pthread_self(); + + state = Malloc(sizeof(RandState)); + RandSeed(state, seed); + + pthread_setspecific(stateKey, state); + } + + /* Generate {size} random numbers. */ + for (i = 0; i < size; i++) + { + /* Most of the time, this will take about 1 loop */ + do + { + tmp = RandGenerate(state); + } while (tmp > allowed); + + buf[i] = tmp % max; + } +} + +/* Generate just 1 random number */ +int +RandInt(unsigned int max) +{ + int val = 0; + + RandIntN(&val, 1, max); + return val; +} diff --git a/src/RtStub.c b/src/RtStub.c new file mode 100644 index 0000000..8302f40 --- /dev/null +++ b/src/RtStub.c @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/* Specified by POSIX to contain environment variables */ +extern char **environ; + +/* The linking program is expected to provide Main */ +extern int Main(Array *, HashMap *); + +typedef struct MainArgs +{ + Array *args; + HashMap *env; +} MainArgs; + +static void * +MainThread(void *argp) +{ + long ret; + MainArgs *args = argp; + + args = argp; + ret = Main(args->args, args->env); + + return (void *) ret; +} + +int +main(int argc, char **argv) +{ + pthread_t mainThread; + size_t i; + int ret; + + char *key; + char *val; + char **envp; + + MainArgs args; + + args.args = NULL; + args.env = NULL; + + args.args = ArrayCreate(); + + if (!args.args) + { + Log(LOG_ERR, "Bootstrap error: Unable to allocate memory for arguments."); + ret = EXIT_FAILURE; + goto finish; + } + + args.env = HashMapCreate(); + if (!args.env) + { + Log(LOG_ERR, "Bootstrap error: Unable to allocate memory for environment."); + ret = EXIT_FAILURE; + goto finish; + } + + for (i = 0; i < (size_t) argc; i++) + { + ArrayAdd(args.args, StrDuplicate(argv[i])); + } + + envp = environ; + while (*envp) + { + size_t valInd; + + /* It is unclear whether or not envp strings are writable, so + * we make our own copy to manipulate it */ + key = StrDuplicate(*envp); + valInd = strcspn(key, "="); + + key[valInd] = '\0'; + val = key + valInd + 1; + HashMapSet(args.env, key, StrDuplicate(val)); + Free(key); + envp++; + } + + if (pthread_create(&mainThread, NULL, MainThread, &args) != 0) + { + Log(LOG_ERR, "Bootstrap error: Unable to create main thread."); + ret = EXIT_FAILURE; + goto finish; + } + + if (pthread_join(mainThread, (void **) &ret) != 0) + { + /* Should never happen */ + Log(LOG_ERR, "Unable to join main thread."); + ret = EXIT_FAILURE; + goto finish; + } + +finish: + if (args.args) + { + for (i = 0; i < ArraySize(args.args); i++) + { + Free(ArrayGet(args.args, i)); + } + ArrayFree(args.args); + } + + if (args.env) + { + while (HashMapIterate(args.env, &key, (void **) &val)) + { + Free(val); + } + HashMapFree(args.env); + } + + Log(LOG_DEBUG, "Exitting with code: %d", ret); + + LogConfigFree(LogConfigGlobal()); + + StreamClose(StreamStdout()); + StreamClose(StreamStdin()); + StreamClose(StreamStderr()); + + GenerateMemoryReport(argv[0]); + + MemoryFreeAll(); + + return ret; +} diff --git a/src/Runtime.c b/src/Runtime.c new file mode 100644 index 0000000..f933ffc --- /dev/null +++ b/src/Runtime.c @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include +#include +#include +#include +#include + +#include +#include +#include + +static void +HexDump(size_t off, char *hexBuf, char *asciiBuf, void *args) +{ + FILE *report = args; + + if (hexBuf && asciiBuf) + { + fprintf(report, "%04lx: %s | %s |\n", off, hexBuf, asciiBuf); + } + else + { + fprintf(report, "%04lx\n", off); + } +} + + +static void +MemoryIterator(MemoryInfo * i, void *args) +{ + FILE *report = args; + + fprintf(report, "%s:%d: %lu bytes at %p\n", + MemoryInfoGetFile(i), MemoryInfoGetLine(i), + MemoryInfoGetSize(i), MemoryInfoGetPointer(i)); + + MemoryHexDump(i, HexDump, report); + + fprintf(report, "\n"); +} + +void +GenerateMemoryReport(const char *prog) +{ + char reportName[128]; + char *namePtr; + + /* + * Use C standard IO instead of the Stream or Io APIs, because + * those use the Memory API, and that's exactly what we're trying + * to assess, so using it would generate false positives. None of + * this code should leak memory. + */ + FILE *report; + time_t currentTime; + struct tm *timeInfo; + char tsBuffer[1024]; + + if (!MemoryAllocated()) + { + /* No memory leaked, no need to write the report. This is the + * ideal situation; we only want the report to show up if leaks + * occurred. */ + return; + } + + snprintf(reportName, sizeof(reportName), "%s-leaked.txt", prog); + /* Make sure the report goes in the current working directory. */ + namePtr = basename(reportName); + report = fopen(namePtr, "a"); + if (!report) + { + return; + } + + currentTime = time(NULL); + timeInfo = localtime(¤tTime); + strftime(tsBuffer, sizeof(tsBuffer), "%c", timeInfo); + + fprintf(report, "---------- Memory Report ----------\n"); + fprintf(report, "Program: %s\n", prog); + fprintf(report, "Date: %s\n", tsBuffer); + fprintf(report, "Total Bytes: %lu\n", MemoryAllocated()); + fprintf(report, "\n"); + + MemoryIterate(MemoryIterator, report); + + fclose(report); +} diff --git a/src/Sha2.c b/src/Sha2.c new file mode 100644 index 0000000..613d420 --- /dev/null +++ b/src/Sha2.c @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include +#include +#include + +#include +#include + +#include + +#define GET_UINT32(x) \ + (((UInt32)(x)[0] << 24) | \ + ((UInt32)(x)[1] << 16) | \ + ((UInt32)(x)[2] << 8) | \ + ((UInt32)(x)[3])) + +#define PUT_UINT32(dst, x) { \ + (dst)[0] = (x) >> 24; \ + (dst)[1] = (x) >> 16; \ + (dst)[2] = (x) >> 8; \ + (dst)[3] = (x); \ +} + +#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) + +#define S0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ (x >> 3)) +#define S1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ (x >> 10)) + +#define T0(x) (ROTR(x, 6) ^ ROTR(x, 11) ^ ROTR(x, 25)) +#define T1(x) (ROTR(x, 2) ^ ROTR(x, 13) ^ ROTR(x, 22)) + +#define CH(a, b, c) (((a) & (b)) ^ ((~(a)) & (c))) +#define MAJ(a, b, c) (((a) & (b)) ^ ((a) & (c)) ^ ((b) & (c))) +#define WW(i) (w[i] = w[i - 16] + S0(w[i - 15]) + w[i - 7] + S1(w[i - 2])) + +#define ROUND(a, b, c, d, e, f, g, h, k, w) { \ + UInt32 tmp0 = h + T0(e) + CH(e, f, g) + k + w; \ + UInt32 tmp1 = T1(a) + MAJ(a, b, c); \ + h = tmp0 + tmp1; \ + d += tmp0; \ +} + +typedef struct Sha256Context +{ + size_t length; + UInt32 state[8]; + size_t bufLen; + unsigned char buffer[64]; +} Sha256Context; + +static void +Sha256Chunk(Sha256Context * context, unsigned char chunk[64]) +{ + const UInt32 rk[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + + UInt32 w[64]; + UInt32 a, b, c, d, e, f, g, h; + + int i; + + for (i = 0; i < 16; i++) + { + w[i] = GET_UINT32(&chunk[4 * i]); + } + + a = context->state[0]; + b = context->state[1]; + c = context->state[2]; + d = context->state[3]; + e = context->state[4]; + f = context->state[5]; + g = context->state[6]; + h = context->state[7]; + + for (i = 0; i < 16; i += 8) + { + ROUND(a, b, c, d, e, f, g, h, rk[i], w[i]); + ROUND(h, a, b, c, d, e, f, g, rk[i + 1], w[i + 1]); + ROUND(g, h, a, b, c, d, e, f, rk[i + 2], w[i + 2]); + ROUND(f, g, h, a, b, c, d, e, rk[i + 3], w[i + 3]); + ROUND(e, f, g, h, a, b, c, d, rk[i + 4], w[i + 4]); + ROUND(d, e, f, g, h, a, b, c, rk[i + 5], w[i + 5]); + ROUND(c, d, e, f, g, h, a, b, rk[i + 6], w[i + 6]); + ROUND(b, c, d, e, f, g, h, a, rk[i + 7], w[i + 7]); + } + + for (i = 16; i < 64; i += 8) + { + ROUND(a, b, c, d, e, f, g, h, rk[i], WW(i)); + ROUND(h, a, b, c, d, e, f, g, rk[i + 1], WW(i + 1)); + ROUND(g, h, a, b, c, d, e, f, rk[i + 2], WW(i + 2)); + ROUND(f, g, h, a, b, c, d, e, rk[i + 3], WW(i + 3)); + ROUND(e, f, g, h, a, b, c, d, rk[i + 4], WW(i + 4)); + ROUND(d, e, f, g, h, a, b, c, rk[i + 5], WW(i + 5)); + ROUND(c, d, e, f, g, h, a, b, rk[i + 6], WW(i + 6)); + ROUND(b, c, d, e, f, g, h, a, rk[i + 7], WW(i + 7)); + } + + context->state[0] += a; + context->state[1] += b; + context->state[2] += c; + context->state[3] += d; + context->state[4] += e; + context->state[5] += f; + context->state[6] += g; + context->state[7] += h; +} + +static void +Sha256Process(Sha256Context * context, unsigned char *data, size_t length) +{ + context->length += length; + + if (context->bufLen && context->bufLen + length >= 64) + { + int len = 64 - context->bufLen; + + memcpy(context->buffer + context->bufLen, data, len); + Sha256Chunk(context, context->buffer); + data += len; + length -= len; + context->bufLen = 0; + } + + while (length >= 64) + { + Sha256Chunk(context, data); + data += 64; + length -= 64; + } + + if (length) + { + memcpy(context->buffer + context->bufLen, data, length); + context->bufLen += length; + } +} + +char * +Sha256(char *str) +{ + Sha256Context context; + size_t i; + unsigned char out[32]; + char *outStr; + + unsigned char fill[64]; + UInt32 fillLen; + unsigned char buf[8]; + UInt32 hiLen; + UInt32 loLen; + + if (!str) + { + return NULL; + } + + outStr = Malloc(65); + if (!outStr) + { + return NULL; + } + + context.state[0] = 0x6a09e667; + context.state[1] = 0xbb67ae85; + context.state[2] = 0x3c6ef372; + context.state[3] = 0xa54ff53a; + context.state[4] = 0x510e527f; + context.state[5] = 0x9b05688c; + context.state[6] = 0x1f83d9ab; + context.state[7] = 0x5be0cd19; + + context.bufLen = 0; + context.length = 0; + memset(context.buffer, 0, 64); + + Sha256Process(&context, (unsigned char *) str, strlen(str)); + + memset(fill, 0, 64); + fill[0] = 0x80; + + fillLen = (context.bufLen < 56) ? 56 - context.bufLen : 120 - context.bufLen; + hiLen = (UInt32) (context.length >> 29); + loLen = (UInt32) (context.length << 3); + + PUT_UINT32(&buf[0], hiLen); + PUT_UINT32(&buf[4], loLen); + + Sha256Process(&context, fill, fillLen); + Sha256Process(&context, buf, 8); + + for (i = 0; i < 8; i++) + { + PUT_UINT32(&out[4 * i], context.state[i]); + } + + /* Convert to string */ + for (i = 0; i < 32; i++) + { + snprintf(outStr + (2 * i), 3, "%02x", out[i]); + } + + return outStr; +} diff --git a/src/Str.c b/src/Str.c new file mode 100644 index 0000000..8bd8c73 --- /dev/null +++ b/src/Str.c @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +char * +StrUtf8Encode(unsigned long utf8) +{ + char *str; + + str = Malloc(5 * sizeof(char)); + if (!str) + { + return NULL; + } + + if (utf8 <= 0x7F) /* Plain ASCII */ + { + str[0] = (char) utf8; + str[1] = '\0'; + } + else if (utf8 <= 0x07FF) /* 2-byte */ + { + str[0] = (char) (((utf8 >> 6) & 0x1F) | 0xC0); + str[1] = (char) (((utf8 >> 0) & 0x3F) | 0x80); + str[2] = '\0'; + } + else if (utf8 <= 0xFFFF) /* 3-byte */ + { + str[0] = (char) (((utf8 >> 12) & 0x0F) | 0xE0); + str[1] = (char) (((utf8 >> 6) & 0x3F) | 0x80); + str[2] = (char) (((utf8 >> 0) & 0x3F) | 0x80); + str[3] = '\0'; + } + else if (utf8 <= 0x10FFFF) /* 4-byte */ + { + str[0] = (char) (((utf8 >> 18) & 0x07) | 0xF0); + str[1] = (char) (((utf8 >> 12) & 0x3F) | 0x80); + str[2] = (char) (((utf8 >> 6) & 0x3F) | 0x80); + str[3] = (char) (((utf8 >> 0) & 0x3F) | 0x80); + str[4] = '\0'; + } + else + { + /* Send replacement character */ + str[0] = (char) 0xEF; + str[1] = (char) 0xBF; + str[2] = (char) 0xBD; + str[3] = '\0'; + } + + return str; +} + +char * +StrDuplicate(const char *inStr) +{ + size_t len; + char *outStr; + + if (!inStr) + { + return NULL; + } + + len = strlen(inStr); + outStr = Malloc(len + 1); /* For the null terminator */ + if (!outStr) + { + return NULL; + } + + strncpy(outStr, inStr, len + 1); + + return outStr; +} + +char * +StrSubstr(const char *inStr, size_t start, size_t end) +{ + size_t len; + size_t i; + size_t j; + + char *outStr; + + if (!inStr) + { + return NULL; + } + + if (start >= end) + { + return NULL; + } + + len = end - start; + + outStr = Malloc(len + 1); + if (!outStr) + { + return NULL; + } + + j = 0; + for (i = start; i < end; i++) + { + if (inStr[i] == '\0') + { + break; + } + + outStr[j] = inStr[i]; + j++; + } + + outStr[j] = '\0'; + + return outStr; +} + +char * +StrConcat(size_t nStr,...) +{ + va_list argp; + char *str; + char *strp; + size_t strLen = 0; + size_t i; + + va_start(argp, nStr); + for (i = 0; i < nStr; i++) + { + char *argStr = va_arg(argp, char *); + + if (argStr) + { + strLen += strlen(argStr); + } + } + va_end(argp); + + str = Malloc(strLen + 1); + strp = str; + + va_start(argp, nStr); + + for (i = 0; i < nStr; i++) + { + /* Manually copy chars instead of using strcopy() so we don't + * have to call strlen() on the strings again, and we aren't + * writing useless null chars. */ + + char *argStr = va_arg(argp, char *); + + if (argStr) + { + while (*argStr) + { + *strp = *argStr; + strp++; + argStr++; + } + } + } + + va_end(argp); + str[strLen] = '\0'; + return str; +} + +int +StrBlank(const char *str) +{ + int blank = 1; + size_t i = 0; + + while (str[i]) + { + blank &= isspace((unsigned char) str[i]); + /* No need to continue if we don't have a blank */ + if (!blank) + { + break; + } + i++; + } + return blank; +} + +char * +StrRandom(size_t len) +{ + static const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + char *str; + int *nums; + size_t i; + + if (!len) + { + return NULL; + } + + str = Malloc(len + 1); + + if (!str) + { + return NULL; + } + + nums = Malloc(len * sizeof(int)); + if (!nums) + { + Free(str); + return NULL; + } + + /* TODO: This seems slow. */ + RandIntN(nums, len, sizeof(charset) - 1); + for (i = 0; i < len; i++) + { + str[i] = charset[nums[i]]; + } + + Free(nums); + str[len] = '\0'; + return str; +} + +char * +StrInt(long i) +{ + char *str; + int len; + + len = snprintf(NULL, 0, "%ld", i); + str = Malloc(len + 1); + if (!str) + { + return NULL; + } + + snprintf(str, len + 1, "%ld", i); + + return str; +} + +int +StrEquals(const char *str1, const char *str2) +{ + /* Both strings are NULL, they're equal */ + if (!str1 && !str2) + { + return 1; + } + + /* One or the other is NULL, they're not equal */ + if (!str1 || !str2) + { + return 0; + } + + /* Neither are NULL, do a regular string comparison */ + return strcmp(str1, str2) == 0; +} diff --git a/src/Stream.c b/src/Stream.c new file mode 100644 index 0000000..2cb299f --- /dev/null +++ b/src/Stream.c @@ -0,0 +1,657 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifndef STREAM_RETRIES +#define STREAM_RETRIES 10 +#endif + +#ifndef STREAM_DELAY +#define STREAM_DELAY 2 +#endif + +#define STREAM_EOF (1 << 0) +#define STREAM_ERR (1 << 1) +#define STREAM_TTY (1 << 2) + +struct Stream +{ + Io *io; + + char *rBuf; + size_t rLen; + size_t rOff; + + char *wBuf; + size_t wLen; + + char *ugBuf; + size_t ugSize; + size_t ugLen; + + int flags; + + int fd; +}; + +Stream * +StreamIo(Io * io) +{ + Stream *stream; + + if (!io) + { + return NULL; + } + + stream = Malloc(sizeof(Stream)); + if (!stream) + { + return NULL; + } + + memset(stream, 0, sizeof(Stream)); + stream->io = io; + stream->fd = -1; + + return stream; +} + +Stream * +StreamFd(int fd) +{ + Io *io = IoFd(fd); + Stream *stream; + + if (!io) + { + return NULL; + } + + stream = StreamIo(io); + if (!stream) + { + return NULL; + } + + stream->fd = fd; + + if (isatty(stream->fd)) + { + stream->flags |= STREAM_TTY; + } + + return stream; +} + +Stream * +StreamFile(FILE * fp) +{ + Io *io = IoFile(fp); + Stream *stream; + + if (!io) + { + return NULL; + } + + stream = StreamIo(io); + if (!stream) + { + return NULL; + } + + stream->fd = fileno(fp); + + if (isatty(stream->fd)) + { + stream->flags |= STREAM_TTY; + } + + return stream; +} + +Stream * +StreamOpen(const char *path, const char *mode) +{ + FILE *fp = fopen(path, mode); + + if (!fp) + { + return NULL; + } + + return StreamFile(fp); +} + +Stream * +StreamStdout(void) +{ + static Stream *stdOut = NULL; + + if (!stdOut) + { + stdOut = StreamFd(STDOUT_FILENO); + } + + return stdOut; +} + +Stream * +StreamStderr(void) +{ + static Stream *stdErr = NULL; + + if (!stdErr) + { + stdErr = StreamFd(STDERR_FILENO); + } + + return stdErr; +} + +Stream * +StreamStdin(void) +{ + static Stream *stdIn = NULL; + + if (!stdIn) + { + stdIn = StreamFd(STDIN_FILENO); + } + + return stdIn; +} + +int +StreamClose(Stream * stream) +{ + int ret = 0; + + if (!stream) + { + errno = EBADF; + return EOF; + } + + if (stream->rBuf) + { + Free(stream->rBuf); + } + + if (stream->wBuf) + { + ssize_t writeRes = IoWrite(stream->io, stream->wBuf, stream->wLen); + + Free(stream->wBuf); + + if (writeRes == -1) + { + ret = EOF; + } + } + + if (stream->ugBuf) + { + Free(stream->ugBuf); + } + + ret = IoClose(stream->io); + Free(stream); + + return ret; +} + +int +StreamVprintf(Stream * stream, const char *fmt, va_list ap) +{ + /* This might look like very similar code to IoVprintf(), but I + * chose not to defer to IoVprintf() because that would require us + * to immediately flush the buffer, since the Io API is unbuffered. + * StreamPuts() uses StreamPutc() under the hood, which is + * buffered. It therefore allows us to finish filling the buffer + * and then only flush it when necessary, preventing superfluous + * writes. */ + + char *buf = NULL; + size_t bufSize = 0; + FILE *fp; + + int ret; + + if (!fmt) + { + return -1; + } + + fp = open_memstream(&buf, &bufSize); + if (!fp) + { + return -1; + } + + ret = vfprintf(fp, fmt, ap); + fclose(fp); + + if (ret >= 0 && stream) + { + if (StreamPuts(stream, buf) < 0) + { + ret = -1; + }; + } + + free(buf); /* Allocated by stdlib, not Memory + * API */ + + return ret; +} + +int +StreamPrintf(Stream * stream, const char *fmt,...) +{ + int ret; + va_list ap; + + va_start(ap, fmt); + ret = StreamVprintf(stream, fmt, ap); + va_end(ap); + + return ret; +} + +int +StreamGetc(Stream * stream) +{ + int c; + + if (!stream) + { + errno = EBADF; + return EOF; + } + + /* Empty the ungetc stack first */ + if (stream->ugLen) + { + c = stream->ugBuf[stream->ugLen - 1]; + stream->ugLen--; + return c; + } + + if (stream->flags & STREAM_EOF) + { + return EOF; + } + + if (!stream->rBuf) + { + /* No buffer allocated yet */ + stream->rBuf = Malloc(IO_BUFFER); + if (!stream->rBuf) + { + stream->flags |= STREAM_ERR; + return EOF; + } + + stream->rOff = 0; + stream->rLen = 0; + } + + if (stream->rOff >= stream->rLen) + { + /* We read through the entire buffer; get a new one */ + ssize_t readRes = IoRead(stream->io, stream->rBuf, IO_BUFFER); + + if (readRes == 0) + { + stream->flags |= STREAM_EOF; + return EOF; + } + + if (readRes == -1) + { + stream->flags |= STREAM_ERR; + return EOF; + } + + stream->rOff = 0; + stream->rLen = readRes; + } + + /* Read the character in the buffer and advance the offset */ + c = stream->rBuf[stream->rOff]; + stream->rOff++; + + return c; +} + +int +StreamUngetc(Stream * stream, int c) +{ + if (!stream) + { + errno = EBADF; + return EOF; + } + + if (!stream->ugBuf) + { + stream->ugSize = IO_BUFFER; + stream->ugBuf = Malloc(stream->ugSize); + + if (!stream->ugBuf) + { + stream->flags |= STREAM_ERR; + return EOF; + } + } + + if (stream->ugLen >= stream->ugSize) + { + char *new; + + stream->ugSize += IO_BUFFER; + new = Realloc(stream->ugBuf, stream->ugSize); + if (!new) + { + stream->flags |= STREAM_ERR; + Free(stream->ugBuf); + stream->ugBuf = NULL; + return EOF; + } + + Free(stream->ugBuf); + stream->ugBuf = new; + } + + stream->ugBuf[stream->ugLen] = c; + stream->ugLen++; + + return c; +} + +int +StreamPutc(Stream * stream, int c) +{ + if (!stream) + { + errno = EBADF; + return EOF; + } + + if (!stream->wBuf) + { + stream->wBuf = Malloc(IO_BUFFER); + if (!stream->wBuf) + { + stream->flags |= STREAM_ERR; + return EOF; + } + } + + if (stream->wLen == IO_BUFFER) + { + /* Buffer full; write it */ + ssize_t writeRes = IoWrite(stream->io, stream->wBuf, stream->wLen); + + if (writeRes == -1) + { + stream->flags |= STREAM_ERR; + return EOF; + } + + stream->wLen = 0; + } + + stream->wBuf[stream->wLen] = c; + stream->wLen++; + + if (stream->flags & STREAM_TTY && c == '\n') + { + /* Newline encountered on a TTY; write now. This fixes some + * strange behavior on certain TTYs where a newline is written + * to the screen upon flush even when no newline exists in the + * stream. We just flush on newlines, but only if we're + * directly writing to a TTY. */ + ssize_t writeRes = IoWrite(stream->io, stream->wBuf, stream->wLen); + + if (writeRes == -1) + { + stream->flags |= STREAM_ERR; + return EOF; + } + + stream->wLen = 0; + } + + return c; +} + +int +StreamPuts(Stream * stream, char *str) +{ + int ret = 0; + + if (!stream) + { + errno = EBADF; + return -1; + } + + while (*str) + { + if (StreamPutc(stream, *str) == EOF) + { + ret = -1; + break; + } + + str++; + } + + return ret; +} + +char * +StreamGets(Stream * stream, char *str, int size) +{ + int i; + + if (!stream) + { + errno = EBADF; + return NULL; + } + + if (size <= 0) + { + errno = EINVAL; + return NULL; + } + + for (i = 0; i < size - 1; i++) + { + int c = StreamGetc(stream); + + if (StreamEof(stream) || StreamError(stream)) + { + break; + } + + str[i] = c; + + if (c == '\n') + { + i++; + break; + } + } + + str[i] = '\0'; + + return str; +} + +off_t +StreamSeek(Stream * stream, off_t offset, int whence) +{ + off_t result; + + if (!stream) + { + errno = EBADF; + return -1; + } + + result = IoSeek(stream->io, offset, whence); + if (result < 0) + { + return result; + } + + /* Successful seek; clear the buffers */ + stream->rOff = 0; + stream->wLen = 0; + stream->ugLen = 0; + + return result; +} + +int +StreamEof(Stream * stream) +{ + return stream && (stream->flags & STREAM_EOF); +} + +int +StreamError(Stream * stream) +{ + return stream && (stream->flags & STREAM_ERR); +} + +void +StreamClearError(Stream * stream) +{ + if (stream) + { + stream->flags &= ~STREAM_ERR; + } +} + +int +StreamFlush(Stream * stream) +{ + if (!stream) + { + errno = EBADF; + return EOF; + } + + if (stream->wLen) + { + ssize_t writeRes = IoWrite(stream->io, stream->wBuf, stream->wLen); + + if (writeRes == -1) + { + stream->flags |= STREAM_ERR; + return EOF; + } + + stream->wLen = 0; + } + + return 0; +} + +ssize_t +StreamCopy(Stream * in, Stream * out) +{ + ssize_t nBytes = 0; + int c; + int tries = 0; + int readFlg = 0; + + while (1) + { + c = StreamGetc(in); + + if (StreamEof(in)) + { + break; + } + + if (StreamError(in)) + { + if (errno == EAGAIN) + { + StreamClearError(in); + tries++; + + if (tries >= STREAM_RETRIES || readFlg) + { + break; + } + else + { + UtilSleepMillis(STREAM_DELAY); + continue; + } + } + else + { + break; + } + } + + /* As soon as we've successfully read a byte, treat future + * EAGAINs as EOF, because somebody might have forgotten to + * close their stream. */ + + readFlg = 1; + tries = 0; + + StreamPutc(out, c); + nBytes++; + } + + StreamFlush(out); + return nBytes; +} + +int +StreamFileno(Stream * stream) +{ + return stream ? stream->fd : -1; +} diff --git a/src/Tls.c b/src/Tls.c new file mode 100644 index 0000000..d3ad08f --- /dev/null +++ b/src/Tls.c @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#ifdef TLS_IMPL + +#include +#include + +Stream * +TlsClientStream(int fd, const char *serverName) +{ + Io *io; + void *cookie; + IoFunctions funcs; + + cookie = TlsInitClient(fd, serverName); + if (!cookie) + { + return NULL; + } + + funcs.read = TlsRead; + funcs.write = TlsWrite; + funcs.seek = NULL; + funcs.close = TlsClose; + + io = IoCreate(cookie, funcs); + if (!io) + { + return NULL; + } + + return StreamIo(io); +} + +Stream * +TlsServerStream(int fd, const char *crt, const char *key) +{ + Io *io; + void *cookie; + IoFunctions funcs; + + cookie = TlsInitServer(fd, crt, key); + if (!cookie) + { + return NULL; + } + + funcs.read = TlsRead; + funcs.write = TlsWrite; + funcs.seek = NULL; + funcs.close = TlsClose; + + io = IoCreate(cookie, funcs); + if (!io) + { + return NULL; + } + + return StreamIo(io); +} + +#endif diff --git a/src/Tls/TlsLibreSSL.c b/src/Tls/TlsLibreSSL.c new file mode 100644 index 0000000..27b9fa7 --- /dev/null +++ b/src/Tls/TlsLibreSSL.c @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#if TLS_IMPL == TLS_LIBRESSL + +#include +#include + +#include + +#include /* LibreSSL TLS */ + +typedef struct LibreSSLCookie +{ + int fd; + struct tls *ctx; + struct tls *cctx; + struct tls_config *cfg; +} LibreSSLCookie; + +void * +TlsInitClient(int fd, const char *serverName) +{ + LibreSSLCookie *cookie = Malloc(sizeof(LibreSSLCookie)); + + if (!cookie) + { + return NULL; + } + + cookie->ctx = tls_client(); + cookie->cctx = NULL; + cookie->cfg = tls_config_new(); + cookie->fd = fd; + + + if (!cookie->ctx || !cookie->cfg) + { + goto error; + } + + if (tls_config_set_ca_file(cookie->cfg, tls_default_ca_cert_file()) == -1) + { + goto error; + } + + if (tls_configure(cookie->ctx, cookie->cfg) == -1) + { + goto error; + } + + if (tls_connect_socket(cookie->ctx, fd, serverName) == -1) + { + goto error; + } + + if (tls_handshake(cookie->ctx) == -1) + { + goto error; + } + + return cookie; + +error: + if (cookie->ctx) + { + if (tls_error(cookie->ctx)) + { + Log(LOG_ERR, "TlsInitClient(): %s", tls_error(cookie->ctx)); + } + + tls_free(cookie->ctx); + } + + if (cookie->cfg) + { + tls_config_free(cookie->cfg); + } + + Free(cookie); + + return NULL; +} + +void * +TlsInitServer(int fd, const char *crt, const char *key) +{ + LibreSSLCookie *cookie = Malloc(sizeof(LibreSSLCookie)); + + if (!cookie) + { + return NULL; + } + + cookie->ctx = tls_server(); + cookie->cctx = NULL; + cookie->cfg = tls_config_new(); + cookie->fd = fd; + + if (!cookie->ctx || !cookie->cfg) + { + goto error; + } + + if (tls_config_set_cert_file(cookie->cfg, crt) == -1) + { + goto error; + } + + if (tls_config_set_key_file(cookie->cfg, key) == -1) + { + goto error; + } + + if (tls_configure(cookie->ctx, cookie->cfg) == -1) + { + goto error; + } + + if (tls_accept_fds(cookie->ctx, &cookie->cctx, fd, fd) == -1) + { + goto error; + } + + if (tls_handshake(cookie->cctx) == -1) + { + goto error; + } + + return cookie; + +error: + if (cookie->ctx) + { + if (tls_error(cookie->ctx)) + { + Log(LOG_ERR, "TlsInitServer(): %s", tls_error(cookie->ctx)); + } + tls_free(cookie->ctx); + } + + if (cookie->cctx) + { + if (tls_error(cookie->cctx)) + { + Log(LOG_ERR, "TlsInitServer(): %s", tls_error(cookie->cctx)); + } + + tls_free(cookie->cctx); + } + + if (cookie->cfg) + { + tls_config_free(cookie->cfg); + } + + Free(cookie); + + return NULL; +} + +ssize_t +TlsRead(void *cookie, void *buf, size_t nBytes) +{ + LibreSSLCookie *tls = cookie; + struct tls *ctx = tls->cctx ? tls->cctx : tls->ctx; + ssize_t ret = tls_read(ctx, buf, nBytes); + + if (ret == -1) + { + errno = EIO; + } + else if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) + { + errno = EAGAIN; + ret = -1; + } + + return ret; +} + +ssize_t +TlsWrite(void *cookie, void *buf, size_t nBytes) +{ + LibreSSLCookie *tls = cookie; + struct tls *ctx = tls->cctx ? tls->cctx : tls->ctx; + ssize_t ret = tls_write(ctx, buf, nBytes); + + if (ret == -1) + { + errno = EIO; + } + else if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) + { + errno = EAGAIN; + ret = -1; + } + + return ret; +} + +int +TlsClose(void *cookie) +{ + LibreSSLCookie *tls = cookie; + + int tlsRet = tls_close(tls->cctx ? tls->cctx : tls->ctx); + int sdRet; + + if (tls->cctx) + { + tls_free(tls->cctx); + } + + tls_free(tls->ctx); + tls_config_free(tls->cfg); + + sdRet = close(tls->fd); + + Free(tls); + + return (tlsRet == -1 || sdRet == -1) ? -1 : 0; +} + +#endif diff --git a/src/Tls/TlsOpenSSL.c b/src/Tls/TlsOpenSSL.c new file mode 100644 index 0000000..c2e950e --- /dev/null +++ b/src/Tls/TlsOpenSSL.c @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#if TLS_IMPL == TLS_OPENSSL + +#include +#include + +#include + +#include +#include + +typedef struct OpenSSLCookie +{ + int fd; + const SSL_METHOD *method; + SSL_CTX *ctx; + SSL *ssl; +} OpenSSLCookie; + +static char * +SSLErrorString(int err) +{ + switch (err) + { + case SSL_ERROR_NONE: + return "No error."; + case SSL_ERROR_ZERO_RETURN: + return "The TLS/SSL connection has been closed."; + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + return "The operation did not complete."; + case SSL_ERROR_WANT_X509_LOOKUP: + return "X509 lookup failed."; + case SSL_ERROR_SYSCALL: + return "I/O Error."; + case SSL_ERROR_SSL: + return "SSL library error."; + } + return NULL; +} + +void * +TlsInitClient(int fd, const char *serverName) +{ + OpenSSLCookie *cookie; + char errorStr[256]; + + /* + * TODO: Seems odd that this isn't needed to make the + * connection... we should figure out how to verify the + * certificate matches the server we think we're + * connecting to. + */ + (void) serverName; + + cookie = Malloc(sizeof(OpenSSLCookie)); + if (!cookie) + { + return NULL; + } + + memset(cookie, 0, sizeof(OpenSSLCookie)); + + cookie->method = TLS_client_method(); + cookie->ctx = SSL_CTX_new(cookie->method); + if (!cookie->ctx) + { + goto error; + } + + cookie->ssl = SSL_new(cookie->ctx); + if (!cookie->ssl) + { + goto error; + } + + if (!SSL_set_fd(cookie->ssl, fd)) + { + goto error; + } + + if (SSL_connect(cookie->ssl) <= 0) + { + goto error; + } + + return cookie; + +error: + Log(LOG_ERR, "TlsClientInit(): %s", ERR_error_string(ERR_get_error(), errorStr)); + + if (cookie->ssl) + { + SSL_shutdown(cookie->ssl); + SSL_free(cookie->ssl); + } + + close(cookie->fd); + + if (cookie->ctx) + { + SSL_CTX_free(cookie->ctx); + } + + return NULL; +} + +void * +TlsInitServer(int fd, const char *crt, const char *key) +{ + OpenSSLCookie *cookie; + char errorStr[256]; + int acceptRet = 0; + + cookie = Malloc(sizeof(OpenSSLCookie)); + if (!cookie) + { + return NULL; + } + + memset(cookie, 0, sizeof(OpenSSLCookie)); + + cookie->method = TLS_server_method(); + cookie->ctx = SSL_CTX_new(cookie->method); + if (!cookie->ctx) + { + Log(LOG_ERR, "TlsInitServer(): Unable to create SSL Context."); + goto error; + } + + if (SSL_CTX_use_certificate_file(cookie->ctx, crt, SSL_FILETYPE_PEM) <= 0) + { + Log(LOG_ERR, "TlsInitServer(): Unable to set certificate file: %s", crt); + goto error; + } + + if (SSL_CTX_use_PrivateKey_file(cookie->ctx, key, SSL_FILETYPE_PEM) <= 0) + { + Log(LOG_ERR, "TlsInitServer(): Unable to set key file."); + goto error; + } + + cookie->ssl = SSL_new(cookie->ctx); + if (!cookie->ssl) + { + Log(LOG_ERR, "TlsInitServer(): Unable to create SSL object."); + goto error; + } + + if (!SSL_set_fd(cookie->ssl, fd)) + { + Log(LOG_ERR, "TlsInitServer(): Unable to set file descriptor."); + goto error; + } + + while ((acceptRet = SSL_accept(cookie->ssl)) <= 0) + { + switch (SSL_get_error(cookie->ssl, acceptRet)) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + continue; + default: + Log(LOG_ERR, "TlsInitServer(): Unable to accept connection."); + goto error; + } + } + + return cookie; + +error: + Log(LOG_ERR, "TlsServerInit(): %s", SSLErrorString(SSL_get_error(cookie->ssl, acceptRet))); + Log(LOG_ERR, "TlsServerInit(): %s", ERR_error_string(ERR_get_error(), errorStr)); + + if (cookie->ssl) + { + SSL_shutdown(cookie->ssl); + SSL_free(cookie->ssl); + } + + close(cookie->fd); + + if (cookie->ctx) + { + SSL_CTX_free(cookie->ctx); + } + + Free(cookie); + + return NULL; +} + +ssize_t +TlsRead(void *cookie, void *buf, size_t nBytes) +{ + OpenSSLCookie *ssl = cookie; + int ret = SSL_read(ssl->ssl, buf, nBytes); + + if (ret <= 0) + { + switch (SSL_get_error(ssl->ssl, ret)) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: + errno = EAGAIN; + break; + case SSL_ERROR_ZERO_RETURN: + ret = 0; + break; + default: + errno = EIO; + break; + } + ret = -1; + } + + return ret; +} + +ssize_t +TlsWrite(void *cookie, void *buf, size_t nBytes) +{ + OpenSSLCookie *ssl = cookie; + int ret = SSL_write(ssl->ssl, buf, nBytes); + + if (ret <= 0) + { + switch (SSL_get_error(ssl->ssl, ret)) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: + errno = EAGAIN; + break; + case SSL_ERROR_ZERO_RETURN: + ret = 0; + break; + default: + errno = EIO; + break; + } + ret = -1; + } + + return ret; +} + +int +TlsClose(void *cookie) +{ + OpenSSLCookie *ssl = cookie; + + SSL_shutdown(ssl->ssl); + SSL_free(ssl->ssl); + close(ssl->fd); + SSL_CTX_free(ssl->ctx); + + Free(ssl); + + return 0; +} + +#endif diff --git a/src/Uri.c b/src/Uri.c new file mode 100644 index 0000000..ca1ad96 --- /dev/null +++ b/src/Uri.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include + +#include + +Uri * +UriParse(const char *str) +{ + Uri *uri; + int res; + + if (!str) + { + return NULL; + } + + uri = Malloc(sizeof(Uri)); + if (!uri) + { + return NULL; + } + + memset(uri, 0, sizeof(Uri)); + + res = sscanf(str, "%7[^:]://%127[^:]:%hu%255[^\n]", uri->proto, uri->host, &uri->port, uri->path) == 4 + || sscanf(str, "%7[^:]://%127[^/]%255[^\n]", uri->proto, uri->host, uri->path) == 3 + || sscanf(str, "%7[^:]://%127[^:]:%hu[^\n]", uri->proto, uri->host, &uri->port) == 3 + || sscanf(str, "%7[^:]://%127[^\n]", uri->proto, uri->host) == 2; + + if (!res) + { + Free(uri); + return NULL; + } + + if (!uri->path[0]) + { + strcpy(uri->path, "/"); + } + + return uri; +} + +void +UriFree(Uri * uri) +{ + Free(uri); +} diff --git a/src/Util.c b/src/Util.c new file mode 100644 index 0000000..8a1b62d --- /dev/null +++ b/src/Util.c @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 256 +#endif + +#ifndef SSIZE_MAX +#define SSIZE_MAX LONG_MAX +#endif + +unsigned long +UtilServerTs(void) +{ + struct timeval tv; + unsigned long ts; + + gettimeofday(&tv, NULL); + ts = (tv.tv_sec * 1000) + (tv.tv_usec / 1000); + + return ts; +} + +unsigned long +UtilLastModified(char *path) +{ + struct stat st; + unsigned long ts; + + if (stat(path, &st) == 0) + { + ts = (st.st_mtim.tv_sec * 1000) + (st.st_mtim.tv_nsec / 1000000); + return ts; + } + else + { + return 0; + } +} + +int +UtilMkdir(const char *dir, const mode_t mode) +{ + char tmp[PATH_MAX]; + char *p = NULL; + + struct stat st; + + size_t len; + + len = strnlen(dir, PATH_MAX); + if (!len || len == PATH_MAX) + { + errno = ENAMETOOLONG; + return -1; + } + + memcpy(tmp, dir, len); + tmp[len] = '\0'; + + if (tmp[len - 1] == '/') + { + tmp[len - 1] = '\0'; + } + + if (stat(tmp, &st) == 0 && S_ISDIR(st.st_mode)) + { + return 0; + } + + for (p = tmp + 1; *p; p++) + { + if (*p == '/') + { + *p = 0; + + if (stat(tmp, &st) != 0) + { + if (mkdir(tmp, mode) < 0) + { + /* errno already set by mkdir() */ + return -1; + } + } + else if (!S_ISDIR(st.st_mode)) + { + errno = ENOTDIR; + return -1; + } + + *p = '/'; + } + } + + if (stat(tmp, &st) != 0) + { + if (mkdir(tmp, mode) < 0) + { + /* errno already set by mkdir() */ + return -1; + } + } + else if (!S_ISDIR(st.st_mode)) + { + errno = ENOTDIR; + return -1; + } + + return 0; +} + +int +UtilSleepMillis(long ms) +{ + struct timespec ts; + int res; + + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000; + + res = nanosleep(&ts, &ts); + + return res; +} + +ssize_t +UtilGetDelim(char **linePtr, size_t * n, int delim, Stream * stream) +{ + char *curPos, *newLinePtr; + size_t newLinePtrLen; + int c; + + if (!linePtr || !n || !stream) + { + errno = EINVAL; + return -1; + } + + if (*linePtr == NULL) + { + *n = 128; + + if (!(*linePtr = Malloc(*n))) + { + errno = ENOMEM; + return -1; + } + } + + curPos = *linePtr; + + while (1) + { + c = StreamGetc(stream); + + if (StreamError(stream) || (c == EOF && curPos == *linePtr)) + { + return -1; + } + + if (c == EOF) + { + break; + } + + if ((*linePtr + *n - curPos) < 2) + { + if (SSIZE_MAX / 2 < *n) + { +#ifdef EOVERFLOW + errno = EOVERFLOW; +#else + errno = ERANGE; +#endif + return -1; + } + + newLinePtrLen = *n * 2; + + if (!(newLinePtr = Realloc(*linePtr, newLinePtrLen))) + { + errno = ENOMEM; + return -1; + } + + curPos = newLinePtr + (curPos - *linePtr); + *linePtr = newLinePtr; + *n = newLinePtrLen; + } + + *curPos++ = (char) c; + + if (c == delim) + { + break; + } + } + + *curPos = '\0'; + return (ssize_t) (curPos - *linePtr); +} + +ssize_t +UtilGetLine(char **linePtr, size_t * n, Stream * stream) +{ + return UtilGetDelim(linePtr, n, '\n', stream); +} diff --git a/src/include/Args.h b/src/include/Args.h new file mode 100644 index 0000000..3108de3 --- /dev/null +++ b/src/include/Args.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_ARGS_H +#define CYTOPLASM_ARGS_H + +/*** + * @Nm Args + * @Nd Getopt-style argument parser that operates on arrays. + * @Dd May 12 2023 + * @Xr Array + * + * .Nm + * provides a simple argument parser in the style of + * .Xr getopt 3 . + * It exists because the runtime passes the program arguments as + * an Array, and it is often useful to parse the arguments to + * provide the standard command line interface. + */ + +#include + +/** + * All state is stored in this structure, instead of global + * variables. This makes + * .Nm + * thread-safe and easy to reset. + */ +typedef struct ArgParseState +{ + int optInd; + int optErr; + int optOpt; + char *optArg; + + int optPos; +} ArgParseState; + +/** + * Initialize the variables in the given parser state structure + * to their default values. This should be called before + * .Fn ArgParse + * is called with the parser state. It should also be called if + * .Fn ArgParse + * will be used again on a different array, or the same array all + * over again. + */ +extern void ArgParseStateInit(ArgParseState *); + +/** + * Parse command line arguments stored in the given array, using + * the given state and option string. This function behaves + * identically to the POSIX + * .Fn getopt + * function, and should be used in exactly the same way. Refer to + * your system's + * .Xr getopt 3 + * page for details. + */ +extern int ArgParse(ArgParseState *, Array *, const char *); + +#endif /* CYTOPLASM_ARGS_H */ diff --git a/src/include/Array.h b/src/include/Array.h new file mode 100644 index 0000000..edd7231 --- /dev/null +++ b/src/include/Array.h @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_ARRAY_H +#define CYTOPLASM_ARRAY_H + +/*** + * @Nm Array + * @Nd A simple dynamic array data structure. + * @Dd November 24 2022 + * @Xr HashMap Queue + * + * These functions implement a simple array data structure that is + * automatically resized as necessary when new values are added. This + * implementation does not actually store the values of the items in it; + * it only stores pointers to the data. As such, you will still have to + * manually maintain all your data. The advantage of this is that these + * functions don't have to copy data, and thus don't care how big the + * data is. Furthermore, arbitrary data can be stored in the array. + * .Pp + * This array implementation is optimized for storage space and + * appending. Deletions are expensive in that all the items of the list + * above a deletion are moved down to fill the hole where the deletion + * occurred. Insertions are also expensive in that all the elements + * above the given index must be shifted up to make room for the new + * element. + * .Pp + * Due to these design choices, this array implementation is best + * suited to linear writing, and then linear or random reading. + */ + +#include +#include + +/** + * The functions in this API operate on an array structure which is + * opaque to the caller. + */ +typedef struct Array Array; + +/** + * Allocate a new array. This function returns a pointer that can be + * used with the other functions in this API, or NULL if there was an + * error allocating memory for the array. + */ +extern Array * ArrayCreate(void); + +/** + * Deallocate an array. Note that this function does not free any of + * the values stored in the array; it is the caller's job to manage the + * memory for each item. Typically, the caller would iterate over all + * the items in the array and free them before freeing the array. + */ +extern void ArrayFree(Array *); + +/** + * Get the size, in number of elements, of the given array. + */ +extern size_t ArraySize(Array *); + +/** + * Get the element at the specified index from the specified array. + * This function will return NULL if the array is NULL, or the index + * is out of bounds. Otherwise, it will return a pointer to a value + * put into the array using + * .Fn ArrayInsert + * or + * .Fn ArraySet . + */ +extern void * ArrayGet(Array *, size_t); + +/** + * Insert the specified element at the specified index in the specified + * array. This function will shift the element currently at that index, + * and any elements after it before inserting the given element. + * .Pp + * This function returns a boolean value indicating whether or not it + * suceeded. + */ +extern int ArrayInsert(Array *, size_t, void *); + +/** + * Set the value at the specified index in the specified array to the + * specified value. This function will return the old value at that + * index, if any. + */ +extern void * ArraySet(Array *, size_t, void *); + +/** + * Append the specified element to the end of the specified array. This + * function uses + * .Fn ArrayInsert + * under the hood to insert an element at the end. It thus has the same + * return value as + * .Fn ArrayInsert . + */ +extern int ArrayAdd(Array *, void *); + +/** + * Remove the element at the specified index from the specified array. + * This function returns the element removed, if any. + */ +extern void * ArrayDelete(Array *, size_t); + +/** + * Sort the specified array using the specified sort function. The + * sort function compares two elements. It takes two void pointers as + * parameters, and returns an integer. The return value indicates to + * the sorting algorithm how the elements relate to each other. A + * return value of 0 indicates that the elements are identical. A + * return value greater than 0 indicates that the first item is + * ``bigger'' than the second item and should thus appear after it in + * the array. A return value less than 0 indicates the opposite: the + * second element should appear after the first in the array. + */ +extern void ArraySort(Array *, int (*) (void *, void *)); + +/** + * If possible, reduce the amount of memory allocated to this array + * by calling + * .Fn Realloc + * on the internal structure to perfectly fit the elements in the + * array. This function is intended to be used by functions that return + * relatively read-only arrays that will be long-lived. + */ +extern int ArrayTrim(Array *); + +/** + * Convert a variadic arguments list into an Array. In most cases, the + * Array API is much easier to work with than + * .Fn va_arg + * and friends. + */ +extern Array * ArrayFromVarArgs(size_t, va_list); + +/** + * Duplicate an existing array. Note that arrays only hold pointers to + * their data, not the data itself, so the duplicated array will point + * to the same places in memory as the original array. + */ +extern Array * ArrayDuplicate(Array *); + +#endif /* CYTOPLASM_ARRAY_H */ diff --git a/src/include/Base64.h b/src/include/Base64.h new file mode 100644 index 0000000..b36a48c --- /dev/null +++ b/src/include/Base64.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_BASE64_H +#define CYTOPLASM_BASE64_H + +/*** + * @Nm Base64 + * @Nd A simple base64 encoder/decoder with unpadded base64 support. + * @Dd September 30 2022 + * @Xr Sha2 + * + * This is an efficient yet simple base64 encoding and decoding API + * that supports regular base64, as well as the Matrix specification's + * extension to base64, called ``unpadded base64.'' This API provides + * the ability to convert between the two, instead of just implementing + * unpadded base64. + */ + +#include + +/** + * This function computes the amount of bytes needed to store a message + * of the specified number of bytes as base64. + */ +extern size_t + Base64EncodedSize(size_t); + +/** + * This function computes the amount of bytes needed to store a decoded + * representation of the encoded message. It takes a pointer to the + * encoded string because it must read a few bytes off the end in order + * to accurately compute the size. + */ +extern size_t + Base64DecodedSize(const char *, size_t); + +/** + * Encode the specified number of bytes from the specified buffer as + * base64. This function returns a string on the heap that should be + * freed with + * .Fn Free , + * or NULL if a memory allocation error ocurred. + */ +extern char * + Base64Encode(const char *, size_t); + +/** + * Decode the specified number of bytes from the specified buffer of + * base64. This function returns a string on the heap that should be + * freed with + * .Fn Free , + * or NULL if a memory allocation error occured. + */ +extern char * + Base64Decode(const char *, size_t); + +/** + * Remove the padding from a specified base64 string. This function + * modifies the specified string in place. It thus has no return value + * because it cannot fail. If the passed pointer is invalid, the + * behavior is undefined. + */ +extern void + Base64Unpad(char *, size_t); + +/** + * Add padding to an unpadded base64 string. This function takes a + * pointer to a pointer because it may be necessary to grow the memory + * allocated to the string. This function returns a boolean value + * indicating whether the pad operation was successful. In practice, + * this means it will only fail if a bigger string is necessary, but it + * could not be automatically allocated on the heap. + */ +extern int + Base64Pad(char **, size_t); + +#endif /* CYTOPLASM_BASE64_H */ diff --git a/src/include/Cron.h b/src/include/Cron.h new file mode 100644 index 0000000..f3cb3cf --- /dev/null +++ b/src/include/Cron.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_CRON_H +#define CYTOPLASM_CRON_H + +/*** + * @Nm Cron + * @Nd Basic periodic job scheduler. + * @Dd December 24 2022 + * + * This is an extremely basic job scheduler. So basic, in fact, that + * it currently runs all jobs on a single thread, which means that it + * is intended for short-lived jobs. In the future, it might be + * extended to support a one-thread-per-job model, but for now, jobs + * should take into consideration the fact that they are sharing their + * thread, so they should not be long-running. + * .Pp + * .Nm + * works by ``ticking'' at an interval defined by the caller of + * .Fn CronCreate . + * At each tick, all the registered jobs are queried, and if they are + * due to run again, their function is executed. As much as possible, + * .Nm + * tries to tick at constant intervals, however it is possible that one + * or more jobs may overrun the tick duration. If this happens, + * .Nm + * ticks again immediately after all the jobs for the previous tick + * have completed. This is in an effort to compensate for lost time, + * however it is important to note that when jobs overrun the tick + * interval, the interval is pushed back by the amount that it was + * overrun. Because of this, + * .Nm + * is best suited for scheduling jobs that should happen + * ``aproximately'' every so often; it is not a real-time scheduler + * by any means. + */ + +/** + * All functions defined here operate on a structure opaque to the + * caller. + */ +typedef struct Cron Cron; + +/** + * A job function is a function that takes a void pointer and returns + * nothing. The pointer is passed when the job is scheduled, and + * is expected to remain valid until the job is no longer registered. + * The pointer is passed each time the job executes. + */ +typedef void (JobFunc) (void *); + +/** + * Create a new + * .Nm + * object that all other functions operate on. Like most of the other + * APIs in this project, it must be freed with + * .Fn CronFree + * when it is no longer needed. + * .Pp + * This function takes the tick interval in milliseconds. + */ +extern Cron * + CronCreate(unsigned long); + +/** + * Schedule a one-off job to be executed only at the next tick, and + * then immediately discarded. This is useful for scheduling tasks that + * only have to happen once, or very infrequently depending on + * conditions other than the current time, but don't have to happen + * immediately. The caller simply indicates that it wishes for the task + * to execute at some time in the future. How far into the future this + * practically ends up being is determined by how long it takes for + * other registered jobs to finish, and what the tick interval is. + * .Pp + * This function takes a job function and a pointer to pass to that + * function when it is executed. + */ +extern void + CronOnce(Cron *, JobFunc *, void *); + +/** + * Schedule a repetitive task to be executed at aproximately the given + * interval. As stated above, this is as fuzzy interval; depending on + * the jobs being run and the tick interval, tasks may not execute at + * exactly the scheduled time, but they will eventually execute. + * .Pp + * This function takes an interval in milliseconds, a job function, + * and a pointer to pass to that function when it is executed. + */ +extern void + CronEvery(Cron *, unsigned long, JobFunc *, void *); + +/** + * Start ticking the clock and executing registered jobs. + */ +extern void + CronStart(Cron *); + +/** + * Stop ticking the clock. Jobs that are already executing will + * continue to execute, but when they are finished, no new jobs will + * be executed until + * .Fn CronStart + * is called. + */ +extern void + CronStop(Cron *); + +/** + * Discard all job references and free all memory associated with the + * given + * .Nm Cron + * instance. + */ +extern void + CronFree(Cron *); + +#endif /* CYTOPLASM_CRON_H */ diff --git a/src/include/Db.h b/src/include/Db.h new file mode 100644 index 0000000..618a2e2 --- /dev/null +++ b/src/include/Db.h @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_DB_H +#define CYTOPLASM_DB_H + +/*** + * @Nm Db + * @Nd A minimal flat-file database with mutex locking and cache. + * @Dd April 27 2023 + * @Xr Json + * + * Cytoplasm operates on a flat-file database instead of a + * traditional relational database. This greatly simplifies the + * persistent storage code, and creates a relatively basic API, + * described here. + */ + +#include + +#include +#include + +/** + * All functions in this API operate on a database structure that is + * opaque to the caller. + */ +typedef struct Db Db; + +/** + * When an object is locked, a reference is returned. This reference + * is owned by the current thread, and the database is inaccessible to + * other threads until all references have been returned to the + * database. + * .Pp + * This reference is opaque, but can be manipulated by the functions + * defined here. + */ +typedef struct DbRef DbRef; + +/** + * Open a data directory. This function takes a path to open, and a + * cache size in bytes. If the cache size is 0, then caching is + * disabled and objects are loaded off the disk every time they are + * locked. Otherwise, objects are stored in the cache, and they are + * evicted in a least-recently-used manner. + */ +extern Db * DbOpen(char *, size_t); + +/** + * Close the database. This function will flush anything in the cache + * to the disk, and then close the data directory. It assumes that + * all references have been unlocked. If a reference has not been + * unlocked, undefined behavior results. + */ +extern void DbClose(Db *); + +/** + * Set the maximum cache size allowed before + * .Nm + * starts evicting old objects. If this is set to 0, everything in the + * cache is immediately evicted and caching is disabled. If the + * database was opened with a cache size of 0, setting this will + * initialize the cache, and subsequent calls to + * .Fn DbLock + * will begin caching objects. + */ +extern void DbMaxCacheSet(Db *, size_t); + +/** + * Create a new object in the database with the specified name. This + * function will fail if the object already exists in the database. It + * takes a variable number of C strings, with the exact number being + * specified by the second parameter. These C strings are used to + * generate a filesystem path at which to store the object. These paths + * ensure each object is uniquely identifiable, and provides semantic + * meaning to an object. + */ +extern DbRef * DbCreate(Db *, size_t,...); + +/** + * Lock an existing object in the database. This function will fail + * if the object does not exist. It takes a variable number of C + * strings, with the exact number being specified by the second + * parameter. These C strings are used to generate the filesystem path + * at which to load the object. These paths ensure each object is + * uniquely identifiable, and provides semantic meaning to an object. + */ +extern DbRef * DbLock(Db *, size_t,...); + +/** + * Immediately and permanently remove an object from the database. + * This function assumes the object is not locked, otherwise undefined + * behavior will result. + */ +extern int DbDelete(Db *, size_t,...); + +/** + * Unlock an object and return it back to the database. This function + * immediately syncs the object to the filesystem. The cache is a + * read cache; writes are always immediate to ensure data integrity in + * the event of a system failure. + */ +extern int DbUnlock(Db *, DbRef *); + +/** + * Check the existence of the given database object in a more efficient + * manner than attempting to lock it with + * .Fn DbLock . + * This function does not lock the object, nor does it load it into + * memory if it exists. + */ +extern int DbExists(Db *, size_t,...); + +/** + * List all of the objects at a given path. Unlike the other varargs + * functions, this one does not take a path to a specific object; it + * takes a directory to be iterated, where each path part is its own + * C string. Note that the resulting list only contains the objects + * in the specified directory, it does not list any subdirectories. + * .Pp + * The array returned is an array of C strings containing the object + * name. + */ +extern Array * DbList(Db *, size_t,...); + +/** + * Free the list returned by + * .Fn DbListFree . + */ +extern void DbListFree(Array *); + +/** + * Convert a database reference into JSON that can be manipulated. + * At this time, the database actually stores objects as JSON on the + * disk, so this function just returns an internal pointer, but in the + * future it may have to be generated by decompressing a binary blob, + * or something of that nature. + */ +extern HashMap * DbJson(DbRef *); + +/** + * Free the existing JSON associated with the given reference, and + * replace it with new JSON. This is more efficient than duplicating + * a separate object into the database reference. + */ +extern int DbJsonSet(DbRef *, HashMap *); + +#endif diff --git a/src/include/HashMap.h b/src/include/HashMap.h new file mode 100644 index 0000000..57ed882 --- /dev/null +++ b/src/include/HashMap.h @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_HASHMAP_H +#define CYTOPLASM_HASHMAP_H + +/*** + * @Nm HashMap + * @Nd A simple hash map implementation. + * @Dd October 11 2022 + * @Xr Array Queue + * + * This is the public interface for Cytoplasm'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 + * throughout the project. + * .Pp + * Fundamentally, this is an entirely generic map implementation. It + * can be used for many general purposes, but it is designed only to + * implement the features Cytoplasm needs to be functional. One + * example of a Cytoplasm-specific feature is that keys cannot be + * arbitrary data; they are NULL-terminated C strings. + */ + +#include + +/** + * These functions operate on an opaque structure, which the caller + * has no knowledge about. + */ +typedef struct HashMap HashMap; + +/** + * Create a new hash map that is ready to be used with the rest of the + * functions defined here. + */ +extern HashMap * HashMapCreate(void); + +/** + * Free the specified hash map such that it becomes invalid and any + * future use results in undefined behavior. Note that this function + * does not free the values stored in the hash map, but since it stores + * the keys internally, it will free the keys. You should use + * .Fn HashMapIterate + * to free the values stored in this map appropriately before calling + * this function. + */ +extern void HashMapFree(HashMap *); + +/** + * Control the maximum load of the hash map before it is expanded. + * When the hash map reaches the given capacity, it is grown. You + * don't want to only grow hash maps when they are full, because that + * makes them perform very poorly. The maximum load value is a + * percentage of how full the hash map is, and it should be between + * 0 and 1, where 0 means that no elements will cause the map to be + * expanded, and 1 means that the hash map must be completely full + * before it is expanded. The default maximum load on a new hash map + * is 0.75, which should be good enough for most purposes, however, + * this function exists specifically so that the maximum load can be + * fine-tuned. + */ +extern void HashMapMaxLoadSet(HashMap *, float); + +/** + * Use a custom hashing function with the given hash map. New hash + * maps have a sane hashing function that should work okay for most + * use cases, but if you have a better hashing function, it can be + * specified this way. Do not change the hash function after keys have + * been added; doing so results in undefined behavior. Only set a new + * hash function immediately after constructing a new hash map, before + * anything has been added to it. + * .Pp + * The hash function takes a pointer to a C string, and is expected + * to return a fairly unique numerical hash value which will be + * converted into an array index. + */ +extern void +HashMapFunctionSet(HashMap *, unsigned long (*) (const char *)); + +/** + * Set the given string key to the given value. Note that the key is + * copied into the hash map's own memory space, but the value is not. + * It is the caller's job to ensure that the value pointer remains + * valid for the life of the hash map, and are freed when no longer + * needed. + */ +extern void * HashMapSet(HashMap *, char *, void *); + +/** + * Retrieve the value for the given key, or return NULL if no such + * key exists in the hash map. + */ +extern void * HashMapGet(HashMap *, const char *); + +/** + * Remove a value specified by the given key from the hash map, and + * return it to the caller to deal with. This function returns NULL + * if no such key exists. + */ +extern void * HashMapDelete(HashMap *, const char *); + +/** + * Iterate over all the keys and values of a hash map. This function + * works very similarly to + * .Xr getopt 3 , + * where calls are repeatedly made in a while loop until there are no + * more items to go over. The difference is that this function does not + * rely on globals; it takes pointer pointers, and stores all + * necessary state inside the hash map itself. + * .Pp + * Note that this function is not thread-safe; two threads cannot be + * iterating over any given hash map at the same time, though they + * can each be iterating over different hash maps. + * .Pp + * This function can be tricky to use in some scenarios, as it + * continues where it left off on each call, until there are no more + * elements to go through in the hash map. If you are not iterating + * over the entire map in one go, and happen to break the loop, then + * the next time you attempt to iterate the hash map, you'll start + * somewhere in the middle, which is most likely not the intended + * behavior. Thus, it is always recommended to iterate over the entire + * hash map if you're going to use this function. + * .Pp + * Also note that the behavior of this function is undefined if + * insertions or deletions occur during the iteration. This + * functionality has not been tested, and will likely not work. + */ +extern int HashMapIterate(HashMap *, char **, void **); + +/** + * A reentrant version of + * .Fn HashMapIterate + * that allows the caller to overcome the flaws of that function by + * storing the cursor outside of the hash map structure itself. This + * allows multiple threads to iterate over the same hash map at the + * same time, and it allows the iteration to be halted midway through + * without causing any unintended side effects. + * .Pp + * The cursor should be initialized to 0 at the start of iteration. + */ +extern int +HashMapIterateReentrant(HashMap *, char **, void **, size_t *); + +#endif /* CYTOPLASM_HASHMAP_H */ diff --git a/src/include/HeaderParser.h b/src/include/HeaderParser.h new file mode 100644 index 0000000..ffb3d0b --- /dev/null +++ b/src/include/HeaderParser.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_HEADERPARSER_H +#define CYTOPLASM_HEADERPARSER_H + +/*** + * @Nm HeaderParser + * @Nd Parse simple C header files. + * @Dd April 29 2023 + * + * .Nm + * is an extremely simple parser that lacks most of the functionality + * one would expect from a C code parser. It simply maps a stream + * of tokens into a few categories, parsing major ``milestones'' in + * a header, without actually understanding any of the details. + * .Pp + * This exists because it is used to generate man pages from headers. + * See + * .Xr hdoc 1 + * for example usage of this parser. + */ + +#include +#include + +#define HEADER_EXPR_MAX 4096 + +/** + * Headers are parsed as expressions. These are the expressions that + * this parser recognizes. + */ +typedef enum HeaderExprType +{ + HP_COMMENT, + HP_PREPROCESSOR_DIRECTIVE, + HP_TYPEDEF, + HP_DECLARATION, + HP_GLOBAL, + HP_UNKNOWN, + HP_SYNTAX_ERROR, + HP_PARSE_ERROR, + HP_EOF +} HeaderExprType; + +/** + * A representation of a function declaration. + */ +typedef struct HeaderDeclaration +{ + char returnType[64]; + char name[32]; /* Enforced by ANSI C */ + Array *args; +} HeaderDeclaration; + +/** + * A global variable declaration. The type must be of the same size + * as the function declaration's return type due to the way parsing + * them is implemented. + */ +typedef struct HeaderGlobal +{ + char type[64]; + char name[HEADER_EXPR_MAX - 64]; +} HeaderGlobal; + +/** + * A representation of a single header expression. Note that that state + * structure is entirely internally managed, so it should not be + * accessed or manipulated by functions outside the functions defined + * here. + * .Pp + * The type field should be used to determine which field in the data + * union is valid. + */ +typedef struct HeaderExpr +{ + HeaderExprType type; + union + { + char text[HEADER_EXPR_MAX]; + HeaderDeclaration declaration; + HeaderGlobal global; + struct + { + int lineNo; + char *msg; + } error; + } data; + + struct + { + Stream *stream; + int lineNo; + } state; +} HeaderExpr; + +/** + * Parse the next expression into the given header expression structure. + * To parse an entire C header, this function should be called in a + * loop until the type of the expression is HP_EOF. + */ +extern void HeaderParse(Stream *, HeaderExpr *); + +#endif /* CYTOPLASM_HEADERPARSER_H */ diff --git a/src/include/Http.h b/src/include/Http.h new file mode 100644 index 0000000..d2de020 --- /dev/null +++ b/src/include/Http.h @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_HTTP_H +#define CYTOPLASM_HTTP_H + +/*** + * @Nm Http + * @Nd Encode and decode various parts of the HTTP protocol. + * @Dd March 12 2023 + * @Xr HttpClient HttpServer HashMap Queue Memory + * + * .Nm + * is a collection of utility functions and type definitions that are + * useful for dealing with HTTP. HTTP is not a complex protocol, but + * this API makes it a lot easier to work with. + * .Pp + * Note that this API doesn't target any particular HTTP version, but + * it is currently used with HTTP 1.0 clients and servers, and + * therefore may be lacking functionality added in later HTTP versions. + */ + +#include + +#include +#include + +#define HTTP_FLAG_NONE 0 +#define HTTP_FLAG_TLS (1 << 0) + +/** + * The request methods defined by the HTTP standard. These numeric + * constants should be preferred to strings when building HTTP APIs + * because they are more efficient. + */ +typedef enum HttpRequestMethod +{ + HTTP_METHOD_UNKNOWN, + HTTP_GET, + HTTP_HEAD, + HTTP_POST, + HTTP_PUT, + HTTP_DELETE, + HTTP_CONNECT, + HTTP_OPTIONS, + HTTP_TRACE, + HTTP_PATCH +} HttpRequestMethod; + +/** + * An enumeration that corresponds to the actual integer values of the + * valid HTTP response codes. + */ +typedef enum HttpStatus +{ + HTTP_STATUS_UNKNOWN = 0, + + /* 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; + +/** + * Convert an HTTP status enumeration value into a string description + * of the status, which is to be used in server response to a client, + * or a client response to a user. For example, calling + * .Fn HttpStatusToString "HTTP_GATEWAY_TIMEOUT" + * (or + * .Fn HttpStatusToString "504" ) + * produces the string "Gateway Timeout". Note that the returned + * pointers point to static space, so their manipulation is forbidden. + */ +extern const char * HttpStatusToString(const HttpStatus); + +/** + * Convert a string into a numeric code that can be used throughout + * the code of a program in an efficient manner. See the definition + * of HttpRequestMethod. This function does case-sensitive matching, + * and does not trim or otherwise process the input string. + */ +extern HttpRequestMethod HttpRequestMethodFromString(const char *); + +/** + * Convert a numeric code as defined by HttpRequestMethod into a + * string that can be sent to a server. Note that the returned pointers + * point to static space, so their manipulation is forbidden. + */ +extern const char * HttpRequestMethodToString(const HttpRequestMethod); + +/** + * Encode a C string such that it can safely appear in a URL by + * performing the necessary percent escaping. A new string on the + * heap is returned. It should be freed with + * .Fn Free , + * defined in the + * .Xr Memory 3 + * API. + */ +extern char * HttpUrlEncode(char *); + +/** + * Decode a percent-encoded string into a C string, ignoring encoded + * null characters entirely, because those would do nothing but cause + * problems. + */ +extern char * HttpUrlDecode(char *); + +/** + * Decode an encoded parameter string in the form of + * ``key=val&key2=val2'' into a hash map whose values are C strings. + * This function properly decodes keys and values using the functions + * defined above. + */ +extern HashMap * HttpParamDecode(char *); + +/** + * Encode a hash map whose values are strings as an HTTP parameter + * string suitable for GET or POST requests. + */ +extern char * HttpParamEncode(HashMap *); + +/** + * Read HTTP headers from a stream and return a hash map whose values + * are strings. All keys are lowercased to make querying them + * consistent and not dependent on the case that was read from the + * stream. This is useful for both client and server code, since the + * headers are in the same format. This function should be used after + * parsing the HTTP status line, because it does not parse that line. + * It will stop when it encounters the first blank line, which + * indicates that the body is beginning. After this function completes, + * the body may be immediately read from the stream without any + * additional processing. + */ +extern HashMap * HttpParseHeaders(Stream *); + +#endif diff --git a/src/include/HttpClient.h b/src/include/HttpClient.h new file mode 100644 index 0000000..f757041 --- /dev/null +++ b/src/include/HttpClient.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_HTTPCLIENT_H +#define CYTOPLASM_HTTPCLIENT_H + +/*** + * @Nm HttpClient + * @Nd Extremely simple HTTP client. + * @Dd April 29 2023 + * @Xr Http HttpServer Tls + * + * .Nm + * HttpClient + * builds on the HTTP API to provide a simple yet functional HTTP + * client. It aims at being easy to use and minimal, yet also + * efficient. + */ + +#include + +#include +#include + +/** + * A server response is represented by a client context. It is + * opaque, so the functions defined in this API should be used to + * fetch data from and manipulate it. + */ +typedef struct HttpClientContext HttpClientContext; + +/** + * Make an HTTP request. This function takes the request method, + * any flags defined in the HTTP API, the port, hostname, and path, + * all in that order. It returns NULL if there was an error making + * the request. Otherwise it returns a client context. Note that this + * function does not actually send any data, it simply makes the + * connection. Use + * .Fn HttpRequestHeader + * to add headers to the request. Then, send headers with + * .Fn HttpRequestSendHeaders . + * Finally, the request body, if any, can be written to the output + * stream, and then the request can be fully sent using + * .Fn HttpRequestSend . + */ +extern HttpClientContext * + HttpRequest(HttpRequestMethod, int, unsigned short, char *, char *); + +/** + * Set a request header to send to the server when making the + * request. + */ +extern void HttpRequestHeader(HttpClientContext *, char *, char *); + +/** + * Send the request headers to the server. This must be called before + * the request body can be written or a response body can be read. + */ +extern void HttpRequestSendHeaders(HttpClientContext *); + +/** + * Flush the request stream to the server. This function should be + * called before the response body is read. + */ +extern HttpStatus HttpRequestSend(HttpClientContext *); + +/** + * Get the headers returned by the server. + */ +extern HashMap * HttpResponseHeaders(HttpClientContext *); + +/** + * Get the stream used to write the request body and read the + * response body. + */ +extern Stream * HttpClientStream(HttpClientContext *); + +/** + * Free all memory associated with the client context. This closes the + * connection, if it was still open. + */ +extern void HttpClientContextFree(HttpClientContext *); + +#endif /* CYTOPLASM_HTTPCLIENT_H */ diff --git a/src/include/HttpRouter.h b/src/include/HttpRouter.h new file mode 100644 index 0000000..6816178 --- /dev/null +++ b/src/include/HttpRouter.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_HTTPROUTER_H +#define CYTOPLASM_HTTPROUTER_H + +/*** + * @Nm HttpRouter + * @Nd Simple HTTP request router with regular expression support. + * @Dd April 29 2023 + * @Xr HttpServer Http + * + * .Nm + * provides a simple mechanism for assigning functions to an HTTP + * request path. It is a simple tree data structure that parses the + * registered request paths and maps functions onto each part of the + * path. Then, requests can be easily routed to their appropriate + * handler functions. + */ + +#include + +/** + * The router structure is opaque and thus managed entirely by the + * functions defined in this API. + */ +typedef struct HttpRouter HttpRouter; + +/** + * A function written to handle an HTTP request takes an array + * consisting of the matched path parts in the order they appear in + * the path, and a pointer to caller-provided arguments, if any. + * It returns a pointer that the caller is assumed to know how to + * handle. + */ +typedef void *(HttpRouteFunc) (Array *, void *); + +/** + * Create a new empty routing tree. + */ +extern HttpRouter * HttpRouterCreate(void); + +/** + * Free all the memory associated with the given routing tree. + */ +extern void HttpRouterFree(HttpRouter *); + +/** + * Register the specified route function to be executed upon requests + * for the specified HTTP path. The path is parsed by splitting at + * each path separator. Each part of the path is a regular expression + * that matches the entire path part. A regular expression cannot + * match more than one path part. This allows for paths like + * .Pa /some/path/(.*)/parts + * to work as one would expect. + */ +extern int HttpRouterAdd(HttpRouter *, char *, HttpRouteFunc *); + +/** + * Route the specified request path using the specified routing + * tree. This function will parse the path and match it to the + * appropriate route handler function. The return value is a boolean + * value that indicates whether or not an appropriate route function + * was found. If an appropriate function was found, then the void + * pointer is passed to it as arguments that it is expected to know + * how to handle, and the pointer to a void pointer is where the + * route function's response will be placed. + */ +extern int HttpRouterRoute(HttpRouter *, char *, void *, void **); + +#endif /* CYTOPLASM_HTTPROUTER_H */ diff --git a/src/include/HttpServer.h b/src/include/HttpServer.h new file mode 100644 index 0000000..f8e449a --- /dev/null +++ b/src/include/HttpServer.h @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_HTTPSERVER_H +#define CYTOPLASM_HTTPSERVER_H + +/*** + * @Nm HttpServer + * @Nd Extremely simple HTTP server. + * @Dd December 13 2022 + * @Xr Http HttpClient + * + * .Nm + * builds on the + * .Xr Http 3 + * API, and provides a very simple, yet very functional API for + * creating an HTTP server. It aims at being easy to use and minimal, + * yet also efficient. It uses non-blocking I/O, is fully + * multi-threaded, and is very configurable. It can be set up in just + * two function calls and minimal supporting code. + * .Pp + * This API should be familar to those that have dealt with the HTTP + * server libraries of other programming languages, particularly Java. + * In fact, much of the terminology used in this API came from Java, + * and you'll notice that the way responses are sent and received very + * closely resembles Java. + */ + +#include + +#include + +#include +#include + +/** + * The functions on this API operate on an opaque structure. + */ +typedef struct HttpServer HttpServer; + +/** + * Each request receives a context structure. It is opaque, so the + * functions defined in this API should be used to fetch data from + * it. These functions allow the handler to figure out the context of + * the request, which includes the path requested, any parameters, + * and the headers and method used to make the request. The context + * also provides the means by which the handler responds to the + * request, allowing it to set the status code, headers, and body. + */ +typedef struct HttpServerContext HttpServerContext; + +/** + * The request handler function is executed when an HTTP request is + * received. It takes a request context, and a pointer as specified + * in the server configuration. + */ +typedef void (HttpHandler) (HttpServerContext *, void *); + +/** + * The number of arguments to + * .Fn HttpServerCreate + * has grown so large that arguments are now stuffed into a + * configuration structure, which is in turn passed to + * .Fn HttpServerCreate . + * This configuration is copied by value into the internal + * structures of the server. It is copied with very minimal + * validation, so ensure that all values are sensible. It may + * make sense to use + * .Fn memset + * to zero out everything in here before assigning values. + */ +typedef struct HttpServerConfig +{ + unsigned short port; + unsigned int threads; + unsigned int maxConnections; + + int flags; /* Http(3) flags */ + char *tlsCert; /* File path */ + char *tlsKey; /* File path */ + + HttpHandler *handler; + void *handlerArgs; +} HttpServerConfig; + +/** + * Create a new HTTP server using the specified configuration. + * This will set up all internal structures used by the server, + * and bind the socket and start listening for connections. However, + * it will not start accepting connections. + */ +extern HttpServer * HttpServerCreate(HttpServerConfig *); + +/** + * Retrieve the configuration that was used to instantiate the given + * server. Note that this configuration is not necessarily the exact + * one that was provided; even though its values are the same, it + * should be treated as an entirely separate configuration with no + * connection to the original. + */ +extern HttpServerConfig * HttpServerConfigGet(HttpServer *); + +/** + * Free the resources associated with the given HTTP server. Note that + * the server can only be freed after it has been stopped. Calling this + * function while the server is still running results in undefined + * behavior. + */ +extern void HttpServerFree(HttpServer *); + +/** + * Attempt to start the HTTP server, and return immediately with the + * status. This API is fully multi-threaded and asynchronous, so the + * caller can continue working while the HTTP server is running in a + * separate thread and managing a pool of threads to handle responses. + */ +extern int HttpServerStart(HttpServer *); + +/** + * Typically, at some point after calling + * .Fn HttpServerStart , + * the program will have no more work to do, so it will want to wait + * for the HTTP server to finish. This is accomplished via this + * function, which joins the HTTP worker thread to the calling thread, + * pausing the calling thread until the HTTP server has stopped. + */ +extern void HttpServerJoin(HttpServer *); + +/** + * Stop the HTTP server. Only the execution of this function will + * cause the proper shutdown of the HTTP server. If the main program + * is joined to the HTTP thread, then either another thread or a + * signal handler will have to stop the server using this function. + * The typical use case is to install a signal handler that executes + * this function on a global HTTP server. + */ +extern void HttpServerStop(HttpServer *); + +/** + * Get the request headers for the request represented by the given + * context. The data in the returned hash map should be treated as + * read only and should not be freed; it is managed entirely by the + * server. + */ +extern HashMap * HttpRequestHeaders(HttpServerContext *); + +/** + * Get the request method used to make the request represented by + * the given context. + */ +extern HttpRequestMethod HttpRequestMethodGet(HttpServerContext *); + +/** + * Get the request path for the request represented by the given + * context. The return value of this function should be treated as + * read-only, and should not be freed; it is managed entirely by the + * server. + */ +extern char * HttpRequestPath(HttpServerContext *); + +/** + * Retrieve the parsed GET parameters for the request represented by + * the given context. The returned hash map should be treated as + * read-only, and should not be freed; it is managed entirely by the + * server. + */ +extern HashMap * HttpRequestParams(HttpServerContext *); + +/** + * Set a response header to return to the client. The old value for + * the given header is returned, if any, otherwise NULL is returned. + */ +extern char * HttpResponseHeader(HttpServerContext *, char *, char *); + +/** + * Set the response status to return to the client. + */ +extern void HttpResponseStatus(HttpServerContext *, HttpStatus); + +/** + * Get the current response status that will be sent to the client + * making the request represented by the given context. + */ +extern HttpStatus HttpResponseStatusGet(HttpServerContext *); + +/** + * Send the response headers to the client that made the request + * represented by the specified context. This function must be called + * before the response body can be written, otherwise a malformed + * response will be sent. + */ +extern void HttpSendHeaders(HttpServerContext *); + +/** + * Get a stream that is both readable and writable. Reading from the + * stream reads the request body that the client sent, if there is one. + * Note that the rquest headers have already been read, so the stream + * is correctly positioned at the beginning of the body of the request. + * .Fn HttpSendHeaders + * must be called before the stream is written, otherwise a malformed + * HTTP response will be sent. An HTTP handler should properly set all + * the headers it itends to send, send those headers, and then write + * the response body to this stream. + * .Pp + * Note that the stream does not need to be closed by the HTTP + * handler; in fact doing so results in undefined behavior. The stream + * is managed entirely by the server itself, so it will close it when + * necessary. This allows the underlying protocol to differ: for + * instance, an HTTP/1.1 connection may stay for multiple requests and + * responses. + */ +extern Stream * HttpServerStream(HttpServerContext *); + +#endif /* CYTOPLASM_HTTPSERVER_H */ diff --git a/src/include/Int.h b/src/include/Int.h new file mode 100644 index 0000000..bcedc91 --- /dev/null +++ b/src/include/Int.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_INT_H +#define CYTOPLASM_INT_H + +/*** + * @Nm Int + * @Nd Fixed-width integer types. + * @Dd April 27 2023 + * + * This header provides cross-platform, fixed-width integer types. + * Specifically, it uses preprocessor magic to define the following + * types: + * .Bl -bullet -offset indent + * .It + * Int8 and UInt8 + * .It + * Int16 and UInt16 + * .It + * Int32 and UInt32 + * .El + * .Pp + * Note that there is no 64-bit integer type, because the ANSI C + * standard makes no guarantee that such a type will exist, even + * though it does on most platforms. + * .Pp + * The reason Cytoplasm provides its own header for this is + * because ANSI C does not define fixed-width types, and while it + * should be safe to rely on C99 fixed-width types in most cases, + * there may be cases where even that is not possible. + * + * @ignore-typedefs + */ + +#include + +#define BIT64_MAX 18446744073709551615 +#define BIT32_MAX 4294967295 +#define BIT16_MAX 65535 +#define BIT8_MAX 255 + +#ifndef UCHAR_MAX +#error Size of char data type is unknown. Define UCHAR_MAX. +#endif + +#ifndef USHRT_MAX +#error Size of short data type is unknown. Define USHRT_MAX. +#endif + +#ifndef UINT_MAX +#error Size of int data type is unknown. Define UINT_MAX. +#endif + +#ifndef ULONG_MAX +#error Size of long data type is unknown. Define ULONG_MAX. +#endif + +#if UCHAR_MAX == BIT8_MAX +typedef signed char Int8; +typedef unsigned char UInt8; + +#else +#error Unable to determine suitable data type for 8-bit integers. +#endif + +#if UINT_MAX == BIT16_MAX +typedef signed int Int16; +typedef unsigned int UInt16; + +#elif USHRT_MAX == BIT16_MAX +typedef signed short Int16; +typedef unsigned short UInt16; + +#elif UCHAR_MAX == BIT16_MAX +typedef signed char Int16; +typedef unsigned char UInt16; + +#else +#error Unable to determine suitable data type for 16-bit integers. +#endif + +#if ULONG_MAX == BIT32_MAX +typedef signed long Int32; +typedef unsigned long UInt32; + +#elif UINT_MAX == BIT32_MAX +typedef signed int Int32; +typedef unsigned int UInt32; + +#elif USHRT_MAX == BIT32_MAX +typedef signed short Int32; +typedef unsigned short UInt32; + +#elif UCHAR_MAX == BIT32_MAX +typedef signed char Int32; +typedef unsigned char UInt32; + +#else +#error Unable to determine suitable data type for 32-bit integers. +#endif + +/* The ANSI C standard only guarantees a data size of up to 32 bits. */ + +#endif diff --git a/src/include/Io.h b/src/include/Io.h new file mode 100644 index 0000000..06afb77 --- /dev/null +++ b/src/include/Io.h @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_IO_H +#define CYTOPLASM_IO_H + +/*** + * @Nm Io + * @Nd Source/sink-agnostic I/O for implementing custom streams. + * @Dd April 29 2023 + * @Xr Stream Tls + * + * Many systems provide platform-specific means of implementing custom + * streams using file pointers. However, POSIX does not define a way + * of programmatically creating custom streams. + * .Nm + * therefore fills this gap in POSIX by mimicking all of the + * functionality of these platform-specific functions, but in pure + * POSIX C. It defines a number of callback funtions to be executed + * in place of the standard POSIX I/O functions, which are used to + * implement arbitrary streams that may not be to a file or socket. + * Additionally, streams can now be pipelined; the sink of one stream + * may be the source of another lower-level stream. Additionally, all + * streams, regardless of their source or sink, share the same API, so + * streams can be handled in a much more generic manner. This allows + * the HTTP client and server libraries to seemlessly support TLS and + * plain connections without having to handle each separately. + * .Pp + * .Nm + * was heavily inspired by GNU's + * .Fn fopencookie + * and BSD's + * .Fn funopen . + * It aims to combine the best of both of these functions into a single + * API that is intuitive and easy to use. + */ + +#include +#include +#include +#include + +#ifndef IO_BUFFER +#define IO_BUFFER 4096 +#endif + +/** + * An opaque structure analogous to a POSIX file descriptor. + */ +typedef struct Io Io; + +/** + * Read input from the source of a stream. This function should + * attempt to read the specified number of bytes of data from the + * given cookie into the given buffer. It should behave identically + * to the POSIX + * .Xr read 2 + * system call, except instead of using an integer descriptor as the + * first parameter, a pointer to an implementation-defined cookie + * stores any information the function needs to read from the source. + */ +typedef ssize_t (IoReadFunc) (void *, void *, size_t); + +/** + * Write output to a sink. This function should attempt to write the + * specified number of bytes of data from the given buffer into the + * stream described by the given cookie. It should behave identically + * to the POSIX + * .Xr write 2 + * system call, except instead of using an integer descriptor as the + * first parameter, a pointer to an implementation-defined cookie + * stores any information the function needs to write to the sink. + */ +typedef ssize_t (IoWriteFunc) (void *, void *, size_t); + +/** + * Repositions the offset of the stream described by the specified + * cookie. This function should behave identically to the POSIX + * .Xr lseek 2 + * system call, except instead of using an integer descriptor as the + * first parameter, a pointer to an implementation-defined cookie + * stores any information the function needs to seek the stream. + */ +typedef off_t (IoSeekFunc) (void *, off_t, int); + +/** + * Close the given stream, making future reads or writes result in + * undefined behavior. This function should also free all memory + * associated with the cookie. It should behave identically to the + * .Xr close 2 + * system call, except instead of using an integer descriptor for the + * parameter, a pointer to an implementation-defined cookie stores any + * information the function needs to close the stream. + */ +typedef int (IoCloseFunc) (void *); + +/** + * A simple mechanism for grouping together a set of stream functions, + * to be passed to + * .Fn IoCreate . + */ +typedef struct IoFunctions +{ + IoReadFunc *read; + IoWriteFunc *write; + IoSeekFunc *seek; + IoCloseFunc *close; +} IoFunctions; + +/** + * Create a new stream using the specified cookie and the specified + * I/O functions. + */ +extern Io * IoCreate(void *, IoFunctions); + +/** + * Read the specified number of bytes from the specified stream into + * the specified buffer. This calls the stream's underlying IoReadFunc, + * which should behave identically to the POSIX + * .Xr read 2 + * system call. + */ +extern ssize_t IoRead(Io *, void *, size_t); + +/** + * Write the specified number of bytes from the specified stream into + * the specified buffer. This calls the stream's underlying + * IoWriteFunc, which should behave identically to the POSIX + * .Xr write 2 + * system call. + */ +extern ssize_t IoWrite(Io *, void *, size_t); + +/** + * Seek the specified stream using the specified offset and whence + * value. This calls the stream's underlying IoSeekFunc, which should + * behave identically to the POSIX + * .Xr lseek 2 + * system call. + */ +extern off_t IoSeek(Io *, off_t, int); + +/** + * Close the specified stream. This calls the stream's underlying + * IoCloseFunc, which should behave identically to the POSIX + * .Xr close 2 + * system call. + */ +extern int IoClose(Io *); + +/** + * Print a formatted string to the given stream. This is a + * re-implementation of the standard library function + * .Xr vfprintf 3 , + * and behaves identically. + */ +extern int IoVprintf(Io *, const char *, va_list); + +/** + * Print a formatted string to the given stream. This is a + * re-implementation of the standard library function + * .Xr fprintf 3 , + * and behaves identically. + */ +extern int IoPrintf(Io *, const char *,...); + +/** + * Read all the bytes from the first stream and write them to the + * second stream. Neither stream is closed upon the completion of this + * function. This can be used for quick and convenient buffered + * copying of data from one stream into another. + */ +extern ssize_t IoCopy(Io *, Io *); + +/** + * Wrap a POSIX file descriptor to take advantage of this API. The + * common use case for this function is when a regular file descriptor + * needs to be accessed by an application that uses this API to also + * access non-POSIX streams. + */ +extern Io * IoFd(int); + +/** + * Open or create a file for reading or writing. The specified file + * name is opened for reading or writing as specified by the given + * flags and mode. This function is a simple convenience wrapper around + * the POSIX + * .Xr open 2 + * system call that passes the opened file descriptor into + * .Fn IoFd . + */ +extern Io * IoOpen(const char *, int, mode_t); + +/** + * Wrap a standard C file pointer to take advantage of this API. The + * common use case for this function is when a regular C file pointer + * needs to be accessed by an application that uses this API to also + * access custom streams. + */ +extern Io * IoFile(FILE *); + +#endif /* CYTOPLASM_IO_H */ diff --git a/src/include/Json.h b/src/include/Json.h new file mode 100644 index 0000000..03bcfae --- /dev/null +++ b/src/include/Json.h @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_JSON_H +#define CYTOPLASM_JSON_H + +/*** + * @Nm Json + * @Nd A fully-featured JSON API. + * @Dd March 12 2023 + * @Xr HashMap Array Stream + * + * .Nm + * is a fully-featured JSON API for C using the array and hash map + * APIs. It can parse JSON, ans serialize an in-memory structure to + * JSON. It build on the foundation of Array and HashMap because that's + * all JSON really is, just arrays and maps. + * .Nm + * also provides a structure for encapsulating an arbitrary value and + * identifying its type, making it easy for a strictly-typed language + * like C to work with loosely-typed JSON data. + * .Nm + * is very strict and tries to adhere as closely as possible to the + * proper definition of JSON. It will fail on syntax errors of any + * kind, which is fine for a Matrix homeserver that can just return + * M_BAD_JSON if anything in here fails, but this behavior may not be + * suitable for other purposes. + * .Pp + * This JSON implementation focuses primarily on serialization and + * deserialization to and from streams. It does not provide facilities + * for handling JSON strings; it only writes JSON to output streams, + * and reads them from input streams. Of course, you can use the + * POSIX + * .Xr fmemopen 3 + * and + * .Xr open_memstream 3 + * functions if you want to deal with JSON strings, but JSON is + * intended to be an exchange format. Data should be converted to JSON + * right when it is leaving the program, and converted from JSON to the + * in-memory format as soon as it is coming in. + * .Pp + * JSON objects are represented as hash maps consisting entirely of + * JsonValue structures, and arrays are represented as arrays + * consisting entirely of JsonValue structures. When generating a + * JSON object, any attempt to stuff a value into a hash map or array + * without first encoding it as a JsonValue will result in undefined + * behavior. + */ + +#include +#include +#include + +#include +#include + +#define JSON_DEFAULT -1 +#define JSON_PRETTY 0 + +/** + * This opaque structure encapsulates all the possible types that can + * be stored in JSON. It is managed entirely by the functions defined + * in this API. It is important to note that strings, integers, floats, + * booleans, and the NULL value are all stored by value, but objects + * and arrays are stored by reference. That is, it doesn't store these + * itself, just pointers to them, however, the data + * .Em is + * freed when using + * .Fn JsonFree . + */ +typedef struct JsonValue JsonValue; + +/** + * These are the types that can be used to identify a JsonValue + * and act on it accordingly. + */ +typedef enum JsonType +{ + JSON_NULL, /* Maps to a C NULL */ + JSON_OBJECT, /* Maps to a HashMap of JsonValues */ + JSON_ARRAY, /* Maps to an Array of JsonValues */ + JSON_STRING, /* Maps to a null-terminated C string */ + JSON_INTEGER, /* Maps to a C long */ + JSON_FLOAT, /* Maps to a C double */ + JSON_BOOLEAN /* Maps to a C integer of either 0 or 1 */ +} JsonType; + +/** + * Determine the type of the specified JSON value. + */ +extern JsonType JsonValueType(JsonValue *); + +/** + * Encode a JSON object as a JSON value that can be added to another + * object, or an array. + */ +extern JsonValue * JsonValueObject(HashMap *); + +/** + * Unwrap a JSON value that represents an object. This function will + * return NULL if the value is not actually an object. + */ +extern HashMap * JsonValueAsObject(JsonValue *); + +/** + * Encode a JSON array as a JSON value that can be added to an object + * or another array. + */ +extern JsonValue * JsonValueArray(Array *); + +/** + * Unwrap a JSON value that represents an array. This function will + * return NULL if the value is not actually an array. + */ +extern Array * JsonValueAsArray(JsonValue *); + +/** + * Encode a C string as a JSON value that can be added to an object or + * an array. + */ +extern JsonValue * JsonValueString(char *); + +/** + * Unwrap a JSON value that represents a string. This function will + * return NULL if the value is not actually a string. + */ +extern char * JsonValueAsString(JsonValue *); + +/** + * Encode a number as a JSON value that can be added to an object or + * an array. + */ +extern JsonValue * JsonValueInteger(long); + +/** + * Unwrap a JSON value that represents a number. This function will + * return 0 if the value is not actually a number, which may be + * misleading. Check the type of the value before making assumptions + * about its value. + */ +extern long JsonValueAsInteger(JsonValue *); + +/** + * Encode a floating point number as a JSON value that can be added + * to an object or an array. + */ +extern JsonValue * JsonValueFloat(double); + +/** + * Unwrap a JSON value that represents a floating point number. This + * function will return 0 if the value is not actually a floating + * point number, which may be misleading. Check the type of the value + * before making assumptions about its type. + */ +extern double JsonValueAsFloat(JsonValue *); + +/** + * Encode a C integer according to the way C treats integers in boolean + * expressions as a JSON value that can be added to an object or an + * array. + */ +extern JsonValue * JsonValueBoolean(int); + +/** + * Unwrap a JSON value that represents a boolean. This function will + * return 0 if the value is not actually a boolean, which may be + * misleading. Check the type of the value before making assumptions + * about its type. + */ +extern int JsonValueAsBoolean(JsonValue *); + +/** + * This is a special case that represents a JSON null. Because the + * Array and HashMap APIs do not accept NULL values, this function + * should be used to represent NULL in JSON. Even though a small + * amount of memory is allocated just to be a placeholder for nothing, + * this keeps the APIs clean. + */ +extern JsonValue * JsonValueNull(void); + +/** + * Free the memory being used by a JSON value. Note that this will + * recursively free all Arrays, HashMaps, and other JsonValues that are + * reachable from the given value, including any strings attached to + * this value. + */ +extern void JsonValueFree(JsonValue *); + +/** + * Recursively duplicate the given JSON value. This returns a new + * JSON value that is completely identical to the specified value, but + * in no way connected to it. + */ +extern JsonValue * JsonValueDuplicate(JsonValue *); + +/** + * Recursively duplicate the given JSON object. This returns a new + * JSON object that is completely identical to the specified object, + * but in no way connect to it. + */ +extern HashMap * JsonDuplicate(HashMap *); + +/** + * Recursively free a JSON object by iterating over all of its values + * and freeing them using + * .Fn JsonValueFree . + */ +extern void JsonFree(HashMap *); + +/** + * Encode the given string in such a way that it can be safely + * embedded in a JSON stream. This entails: + * .Bl -bullet -offset indent + * .It + * Escaping quotes, backslashes, and other special characters using + * their backslash escape. + * .It + * Encoding bytes that are not UTF-8 using escapes. + * .It + * Wrapping the entire string in double quotes. + * .El + * .Pp + * This function is only provided via the public + * .Nm + * API so that it is accessible to custom JSON encoders, such as the + * CanonicalJson encoder. This will typically be used for encoding + * object keys; to encode values, just use + * .Fn JsonEncodeValue . + * .Pp + * This function returns the number of bytes written to the stream, + * or if the stream is NULL, the number of bytes that would have + * been written. + */ +extern int JsonEncodeString(const char *, Stream *); + +/** + * Serialize a JSON value as it would appear in JSON output. This is + * a recursive function that also encodes all child values reachable + * from the given value. This function is exposed via the public + * .Nm + * API so that it is accessible to custom JSON encoders. Normal users + * that are not writing custom encoders should in most cases just use + * .Fn JsonEncode + * to encode an entire object. + * .Pp + * The third parameter is an integer that represents the indent level + * of the value to be printed, or a negative number if pretty-printing + * should be disabled and JSON should be printed as minimized as + * possible. To pretty-print a JSON object, set this to + * .Va JSON_PRETTY . + * To get minified output, set it to + * .Va JSON_DEFAULT . + * .Pp + * This function returns the number of bytes written to the stream, + * or if the stream is NULL, the number of bytes that would have + * been written. + */ +extern int JsonEncodeValue(JsonValue *, Stream *, int); + +/** + * Encode a JSON object as it would appear in JSON output, writing it + * to the given output stream. This function is recursive; it will + * serialize everything accessible from the passed object. The third + * parameter has the same behavior as described above. + * .Pp + * This function returns the number of bytes written to the stream, + * or if the stream is NULL, the number of bytes that would have + * been written. + */ +extern int JsonEncode(HashMap *, Stream *, int); + +/** + * Decode a JSON object from the given input stream and parse it into + * a hash map of JSON values. + */ +extern HashMap * JsonDecode(Stream *); + +/** + * A convenience function that allows the caller to retrieve and + * arbitrarily deep keys within a JSON object. It takes a root JSON + * object, the number of levels deep to go, and then that number of + * keys as a varargs list. All keys must have objects as values, with + * the exception of the last one, which is the value that will be + * returned. Otherwise, NULL indicates the specified path doas not + * exist. + */ +extern JsonValue * JsonGet(HashMap *, size_t,...); + +/** + * A convenience function that allows the caller to set arbitrarily + * deep keys within a JSON object. It takes a root JSON object, the + * number of levels deep to go, and then that number of keys as a + * varargs list. All keys must have object as values, with the + * exception of the last one, which is the value that will be set. + * The value currently at that key, if any, will be returned. + * This function will create any intermediate objects as necessary to + * set the proper key. + */ +extern JsonValue * JsonSet(HashMap *, JsonValue *, size_t,...); + +#endif /* CYTOPLASM_JSON_H */ diff --git a/src/include/Log.h b/src/include/Log.h new file mode 100644 index 0000000..d0300ca --- /dev/null +++ b/src/include/Log.h @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_LOG_H +#define CYTOPLASM_LOG_H + +/*** + * @Nm Log + * @Nd A simple logging framework for logging to multiple destinations. + * @Dd April 27 2023 + * @Xr Stream + * + * .Nm + * is a simple C logging library that allows for colorful outputs, + * timestamps, and custom log levels. It also features the ability to + * have multiple logs open at one time, although Cytoplasm primarily + * utilizes the global log. All logs are thread safe. + */ + +#include +#include +#include + +#include + +#define LOG_FLAG_COLOR (1 << 0) +#define LOG_FLAG_SYSLOG (1 << 1) + +/** + * A log is defined as a configuration that describes the properties + * of the log. This opaque structure can be manipulated by the + * functions defined in this API. + */ +typedef struct LogConfig LogConfig; + +/** + * Create a new log configuration with sane defaults that can be used + * immediately with the logging functions. + */ +extern LogConfig * LogConfigCreate(void); + +/** + * Get the global log configuration, creating a new one with + * .Fn LogConfigCreate + * if necessary. + */ +extern LogConfig * LogConfigGlobal(void); + +/** + * Free the given log configuration. Note that this does not close the + * underlying stream associated with the log, if there is one. Also + * note that to avoid memory leaks, the global log configuration must + * also be freed, but it cannot be used after it is freed. + */ +extern void LogConfigFree(LogConfig *); + +/** + * Set the current log level on the specified log configuration. + * This indicates that only messages at or above this level should be + * logged; all others are silently discarded. The passed log level + * should be one of the log levels defined by + * .Xr syslog 3 . + * Refer to that page for a complete list of acceptable log levels, + * and note that passing an invalid log level will result in undefined + * behavior. + */ +extern void LogConfigLevelSet(LogConfig *, int); + +/** + * Cause the log output to be indented two more spaces than it was + * previously. This can be helpful when generating stack traces or + * other hierarchical output. This is a simple convenience wrapper + * around + * .Fn LogConfigIndentSet . + */ +extern void LogConfigIndent(LogConfig *); + +/** + * Cause the log output to be indented two less spaces than it was + * previously. This is a simple convenience wrapper around + * .Fn LogConfigIndentSet . + */ +extern void LogConfigUnindent(LogConfig *); + +/** + * Indent future log output using the specified config by some + * arbitrary amount. + */ +extern void LogConfigIndentSet(LogConfig *, size_t); + +/** + * Set the file stream that logging output should be written to. This + * defaults to standard output, but it can be set to standard error, + * or any other arbitrary stream. Passing a NULL value for the stream + * pointer sets the log output to the standard output. Note that the + * output stream is only used if + * .Va LOG_FLAG_SYSLOG + * is not set. + */ +extern void LogConfigOutputSet(LogConfig *, Stream *); + +/** + * Set a number of boolean options on a log configuration. This + * function uses bitwise operators, so multiple options can be set with + * a single function call using bitwise OR operators. The flags are + * defined as preprocessor macros, and are as follows: + * .Bl -tag -width Ds + * .It LOG_FLAG_COLOR + * When set, enable color-coded output on TTYs. Note that colors are + * implemented as ANSI escape sequences, and are not written to file + * streams that are not actually connected to a TTY, to prevent those + * sequences from being written to a file. + * .Xr isatty 3 + * is checked before writing any terminal sequences. + * .It LOG_FLAG_SYSLOG + * When set, log output to the syslog using + * .Xr syslog 3 , + * instead of logging to the file set by + * .Fn LogConfigOutputSet . + * This flag always overrides the stream set by that function, + * regardless of when it was set, even if it was set after this flag + * was set. + * .El + */ +extern void LogConfigFlagSet(LogConfig *, int); + +/** + * Clear a boolean flag from the specified log format. See above for + * the list of flags. + */ +extern void LogConfigFlagClear(LogConfig *, int); + +/** + * Set a custom timestamp to be prepended to each message if the + * output is not going to the system log. Consult your system's + * documentation for + * .Xr strftime 3 . + * A value of NULL disables the timestamp output before messages. + */ +extern void LogConfigTimeStampFormatSet(LogConfig *, char *); + +/** + * This function does the actual logging of messages using a + * specified configuration. It takes the configuration, the log + * level, a format string, and then a list of arguments, all in that + * order. This function only logs messages if their level is above + * or equal to the currently configured log level, making it easy to + * turn some messages on or off. + * .Pp + * This function has the same usage as + * .Xr vprintf 3 . + * Consult that page for the list of format specifiers and their + * arguments. This function is typically not used directly, see the + * other log functions for the most common use cases. + */ +extern void Logv(LogConfig *, int, const char *, va_list); + +/** + * Log a message using + * .Fn Logv . + * with the specified configuration. This function has the same usage + * as + * .Xr printf 3 . + */ +extern void LogTo(LogConfig *, int, const char *, ...); + +/** + * Log a message to the global log using + * .Fn Logv . + * This function has the same usage as + * .Xr printf 3 . + */ +extern void Log(int, const char *, ...); + +#endif diff --git a/src/include/Memory.h b/src/include/Memory.h new file mode 100644 index 0000000..9f2c412 --- /dev/null +++ b/src/include/Memory.h @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_MEMORY_H +#define CYTOPLASM_MEMORY_H + +/*** + * @Nm Memory + * @Nd Smart memory management. + * @Dd January 9 2023 + * + * .Nm + * is an API that allows for smart memory management and profiling. It + * wraps the standard library functions + * .Xr malloc 3 , + * .Xr realloc 3 , + * and + * .Xr free 3 , + * and offers identical semantics, while providing functionality that + * the standard library doesn't have, such as getting statistics on the + * total memory allocated on the heap, and getting the size of a block + * given a pointer. Additionally, thanks to preprocessor macros, the + * exact file and line number at which an allocation, re-allocation, or + * free occured can be obtained given a pointer. Finally, all the + * blocks allocated on the heap can be iterated and evaluated, and a + * callback function can be executed every time a memory operation + * occurs. + * .Pp + * In the future, this API could include a garbage collector that + * automatically frees memory it detects as being no longer in use. + * However, this feature does not yet exist. + * .Pp + * A number of macros are available, which make the + * .Nm + * API much easier to use. They are as follows: + * .Bl -bullet -offset indent + * .It + * .Fn Malloc "x" + * .It + * .Fn Realloc "x" "y" + * .It + * .Fn Free "x" + * .El + * .Pp + * These macros expand to + * .Fn MemoryAllocate , + * .Fn MemoryReallocate , + * and + * .Fn MemoryFree + * with the second and third parameters set to __FILE__ and __LINE__. + * This allows + * .Nm + * to be used exactly how the standard library functions would be + * used. In fact, the functions to which these macros expand are not + * intended to be used directly; for the best results, use these + * macros. + */ +#include + +/** + * These values are passed into the memory hook function to indicate + * the action that just happened. + */ +typedef enum MemoryAction +{ + MEMORY_ALLOCATE, + MEMORY_REALLOCATE, + MEMORY_FREE, + MEMORY_BAD_POINTER +} MemoryAction; + +#define Malloc(x) MemoryAllocate(x, __FILE__, __LINE__) +#define Realloc(x, s) MemoryReallocate(x, s, __FILE__, __LINE__) +#define Free(x) MemoryFree(x, __FILE__, __LINE__) + +/** + * The memory information is opaque, but can be accessed using the + * functions defined by this API. + */ +typedef struct MemoryInfo MemoryInfo; + +/** + * Allocate the specified number of bytes on the heap. This function + * has the same semantics as + * .Xr malloc 3 , + * except that it takes the file name and line number at which the + * allocation occurred. + */ +extern void * MemoryAllocate(size_t, const char *, int); + +/** + * Change the size of the object pointed to by the given pointer + * to the given number of bytes. This function has the same semantics + * as + * .Xr realloc 3 , + * except that it takes the file name and line number at which the + * reallocation occurred. + */ +extern void * MemoryReallocate(void *, size_t, const char *, int); + +/** + * Free the memory at the given pointer. This function has the same + * semantics as + * .Xr free 3 , + * except that it takes the file name and line number at which the + * free occurred. + */ +extern void MemoryFree(void *, const char *, int); + +/** + * Get the total number of bytes that the program has allocated on the + * heap. This operation iterates over all heap allocations made with + * .Fn MemoryAllocate + * and then returns a total count, in bytes. + */ +extern size_t MemoryAllocated(void); + +/** + * Iterate over all heap allocations made with + * .Fn MemoryAllocate + * and call + * .Fn MemoryFree + * on them. This function immediately invalidates all pointers to + * blocks on the heap, and any subsequent attempt to read or write to + * data on the heap will result in undefined behavior. This is + * typically called at the end of the program, just before exit. + */ +extern void MemoryFreeAll(void); + +/** + * Fetch information about an allocation. This function takes a raw + * pointer, and if + * . Nm + * knows about the pointer, it returns a structure that can be used + * to obtain information about the block of memory that the pointer + * points to. + */ +extern MemoryInfo * MemoryInfoGet(void *); + +/** + * Get the size in bytes of the block of memory represented by the + * specified memory info structure. + */ +extern size_t MemoryInfoGetSize(MemoryInfo *); + +/** + * Get the file name in which the block of memory represented by the + * specified memory info structure was allocated. + */ +extern const char * MemoryInfoGetFile(MemoryInfo *); + +/** + * Get the line number on which the block of memory represented by the + * specified memory info structure was allocated. + */ +extern int MemoryInfoGetLine(MemoryInfo *); + +/** + * Get a pointer to the block of memory represented by the specified + * memory info structure. + */ +extern void * MemoryInfoGetPointer(MemoryInfo *); + +/** + * This function takes a pointer to a function that takes the memory + * info structure, as well as a void pointer for caller-provided + * arguments. It iterates over all the heap memory currently allocated + * at the time of calling, executing the function on each allocation. + */ +extern void MemoryIterate(void (*) (MemoryInfo *, void *), void *); + +/** + * Specify a function to be executed whenever a memory operation + * occurs. The MemoryAction argument specifies the operation that + * occurred on the block of memory represented by the memory info + * structure. The function also takes a void pointer to caller-provided + * arguments. + */ +extern void MemoryHook(void (*) (MemoryAction, MemoryInfo *, void *), void *); + +/** + * Read over the block of memory represented by the given memory info + * structure and generate a hexadecimal and ASCII string for each + * chunk of the block. This function takes a callback function that + * takes the following parameters in order: + * .Bl -bullet -offset indent + * .It + * The current offset from the beginning of the block of memory in + * bytes. + * .It + * A null-terminated string containing the next 16 bytes of the block + * encoded as space-separated hex values. + * .It + * A null-terminated string containing the ASCII representation of the + * same 16 bytes of memory. This ASCII representation is safe to print + * to a terminal or other text device, because non-printable characters + * are encoded as a . (period). + * .It + * Caller-passed pointer. + * .El + */ +extern void +MemoryHexDump(MemoryInfo *, void (*) (size_t, char *, char *, void *), void *); + +#endif diff --git a/src/include/Queue.h b/src/include/Queue.h new file mode 100644 index 0000000..35821d3 --- /dev/null +++ b/src/include/Queue.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_QUEUE_H +#define CYTOPLASM_QUEUE_H + +/*** + * @Nm Queue + * @Nd A simple static queue data structure. + * @Dd November 25 2022 + * @Xr Array HashMap + * + * .Nm + * implements a simple queue data structure that is statically sized. + * This implementation does not actually store the values of the items + * in it; it only stores pointers to the data. As such, you will have + * to manually maintain data and make sure it remains valid as long as + * it is in the queue. The advantage of this is that + * .Nm + * doesn't have to copy data, and thus doesn't care how big the data + * is. Furthermore, any arbitrary data can be stored in the queue. + * .Pp + * This queue implementation operates on the heap. It is a circular + * queue, and it does not grow as it is used. Once the size is set, + * the queue never gets any bigger. + */ + +#include + +/** + * These functions operate on a queue structure that is opaque to the + * caller. + */ +typedef struct Queue Queue; + +/** + * Allocate a new queue that is able to store the specified number of + * items in it. + */ +extern Queue * QueueCreate(size_t); + +/** + * Free the memory associated with the specified queue structure. Note + * that this function does not free any of the values stored in the + * queue; it is the caller's job to manage memory for each item. + * Typically, the caller would dequeue all the items in the queue and + * deal with them before freeing the queue itself. + */ +extern void QueueFree(Queue *); + +/** + * Push an element into the queue. This function returns a boolean + * value indicating whether or not the push succeeded. Pushing items + * into the queue will fail if the queue is full. + */ +extern int QueuePush(Queue *, void *); + +/** + * Pop an element out of the queue. This function returns NULL if the + * queue is empty. Otherwise, it returns a pointer to the item that is + * next up in the queue. + */ +extern void * QueuePop(Queue *); + +/** + * Retrieve a pointer to the item that is next up in the queue without + * actually discarding it, such that the next call to + * .Fn QueuePeek + * or + * .Fn QueuePop + * will return the same pointer. + */ +extern void * QueuePeek(Queue *); + +/** + * Determine whether or not the queue is full. + */ +extern int QueueFull(Queue *); + +/** + * Determine whether or not the queue is empty. + */ +extern int QueueEmpty(Queue *); + +#endif diff --git a/src/include/Rand.h b/src/include/Rand.h new file mode 100644 index 0000000..a5be3ce --- /dev/null +++ b/src/include/Rand.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_RAND_H +#define CYTOPLASM_RAND_H + +/*** + * @Nm Rand + * @Nd Thread-safe random numbers. + * @Dd February 16 2023 + * @Xr Util + * + * .Nm + * is used for generating random numbers in a thread-safe way. + * Currently, one generator state is shared across all threads, which + * means that only one thread can generate random numbers at a time. + * This state is protected with a mutex to guarantee this behavior. + * In the future, a seed pool may be maintained to allow multiple + * threads to generate random numbers at the same time. + * .Pp + * The generator state is seeded on the first call to a function that + * needs it. The seed is determined by the current timestamp, the ID + * of the process, and the thread ID. These should all be sufficiently + * random sources, so the seed should be secure enough. + * .Pp + * .Nm + * currently uses a simple Mersenne Twister algorithm to generate + * random numbers. This algorithm was chosen because it is extremely + * popular and widespread. While it is likely not cryptographically + * secure, and does suffer some unfortunate pitfalls, this algorithm + * has stood the test of time and is simple enough to implement, so + * it was chosen over the alternatives. + * .Pp + * .Nm + * does not use any random number generator functions from the C + * standard library, since these are often flawed. + */ + +#include + +/** + * Generate a single random integer between 0 and the passed value. + */ +extern int RandInt(unsigned int); + +/** + * Generate the number of integers specified by the second argument + * storing them into the buffer pointed to in the first argument. + * Ensure that each number is between 0 and the third argument. + * .Pp + * This function allows a caller to get multiple random numbers at once + * in a more efficient manner than repeatedly calling + * .Fn RandInt , + * since each call to these functions + * has to lock and unlock a mutex. It is therefore better to obtain + * multiple random numbers in one pass if multiple are needed. + */ +extern void RandIntN(int *, size_t, unsigned int); + +#endif /* CYTOPLASM_RAND_H */ diff --git a/src/include/Runtime.h b/src/include/Runtime.h new file mode 100644 index 0000000..f3bb6b2 --- /dev/null +++ b/src/include/Runtime.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_RUNTIME_H +#define CYTOPLASM_RUNTIME_H + +/*** + * @Nm Runtime + * @Nd Supporting functions for the Cytoplasm runtime. + * @Dd May 23 2023 + * @Xr Memory + * + * .Nm + * provides supporting functions for the Cytoplasm runtime. These + * functions are not intended to be called directly by programs, + * but are used internally. They're exposed via a header because + * the runtime stub needs to know their definitions. + */ + +#include + +/** + * Write a memory report to a file in the current directory, using + * the provided string as the name of the program currently being + * executed. This function is to be called after all memory is + * supposed to have been freed. It iterates over all remaining + * memory and generates a text file containing all of the + * recorded information about each block, including a hex dump of + * the data stored in them. + */ +extern void GenerateMemoryReport(const char *); + +#endif /* CYTOPLASM_RUNTIME_H */ diff --git a/src/include/Sha2.h b/src/include/Sha2.h new file mode 100644 index 0000000..6d582f4 --- /dev/null +++ b/src/include/Sha2.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_SHA2_H +#define CYTOPLASM_SHA2_H + +/*** + * @Nm Sha2 + * @Nd A simple implementation of the SHA2 hashing functions. + * @Dd December 19 2022 + * @Xr Memory Base64 + * + * This API defines simple functions for computing SHA2 hashes. + * At the moment, it only defines + * .Fn Sha256 , + * which computes the SHA-256 hash of the given C string. It is + * not trivial to implement SHA-512 in ANSI C due to the lack of + * a 64-bit integer type, so that hash function has been omitted. + */ + +/** + * This function takes a pointer to a NULL-terminated C string, and + * returns a string allocated on the heap using the Memory API, or + * NULL if there was an error allocating memory. The returned string + * should be freed when it is no longer needed. + */ +extern char * Sha256(char *); + +#endif /* CYTOPLASM_SHA2_H */ diff --git a/src/include/Str.h b/src/include/Str.h new file mode 100644 index 0000000..21fdeb0 --- /dev/null +++ b/src/include/Str.h @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_STR_H +#define CYTOPLASM_STR_H + +/*** + * @Nm Str + * @Nd Functions for creating and manipulating strings. + * @Dd February 15 2023 + * @Xr Memory + * + * .Nm + * provides string-related functions. It is called + * .Nm , + * not String, because some platforms (Windows) do not have + * case-sensitive filesystems, which poses a problem since + * .Pa string.h + * is a standard library header. + */ + +#include + +/** + * Take a UTF-8 codepoint and encode it into a string buffer containing + * between 1 and 4 bytes. The string buffer is allocated on the heap, + * so it should be freed when it is no longer needed. + */ +extern char * StrUtf8Encode(unsigned long); + +/** + * Duplicate a null-terminated string, returning a new string on the + * heap. This is useful when a function takes in a string that it needs + * to store for long amounts of time, even perhaps after the original + * string is gone. + */ +extern char * StrDuplicate(const char *); + +/** + * Extract part of a null-terminated string, returning a new string on + * the heap containing only the requested subsection. Like the + * substring functions included with most programming languages, the + * starting index is inclusive, and the ending index is exclusive. + */ +extern char * StrSubstr(const char *, size_t, size_t); + +/** + * A varargs function that takes a number of null-terminated strings + * specified by the first argument, and returns a new string that + * contains their concatenation. It works similarly to + * .Xr strcat 3 , + * but it takes care of allocating memory big enough to hold all the + * strings. Any string in the list may be NULL. If a NULL pointer is + * passed, it is treated like an empty string. + */ +extern char * StrConcat(size_t,...); + +/** + * Return a boolean value indicating whether or not the null-terminated + * string consists only of blank characters, as determined by + * .Xr isblank 3 . + */ +extern int StrBlank(const char *str); + +/** + * Generate a string of the specified length, containing random + * lowercase and uppercase letters. + */ +extern char * StrRandom(size_t); + +/** + * Convert the specified integer into a string, returning the string + * on the heap, or NULL if there was a memory allocation error. The + * returned string should be freed by the caller after it is no longer + * needed. + */ +extern char * StrInt(long); + +/** + * Compare two strings and determine whether or not they are equal. + * This is the most common use case of strcmp() in Cytoplasm, but + * strcmp() doesn't like NULL pointers, so these have to be checked + * explicitly and can cause problems if they aren't. This function, + * on the other hand, makes NULL pointers special cases. If both + * arguments are NULL, then they are considered equal. If only one + * argument is NULL, they are considered not equal. Otherwise, if + * no arguments are NULL, a regular strcmp() takes place and this + * function returns a boolean value indicating whether or not + * strcmp() returned 0. + */ +extern int StrEquals(const char *, const char *); + +#endif /* CYTOPLASM_STR_H */ diff --git a/src/include/Stream.h b/src/include/Stream.h new file mode 100644 index 0000000..3ddacc6 --- /dev/null +++ b/src/include/Stream.h @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_STREAM_H +#define CYTOPLASM_STREAM_H + +/*** + * @Nm Stream + * @Nd An abstraction over the Io API that implements standard C I/O. + * @Dd April 29 2023 + * @Xr Io + * + * .Nm + * implements an abstraction layer over the Io API. This layer buffers + * I/O and makes it much easier to work with, mimicking the standard + * C library and offering some more convenience features. + */ + +#include + +#include + +/** + * An opaque structure analogous to C's FILE pointers. + */ +typedef struct Stream Stream; + +/** + * Create a new stream using the specified Io for underlying I/O + * operations. + */ +extern Stream * StreamIo(Io * io); + +/** + * Create a new stream using the specified POSIX file descriptor. + * This is a convenience function for calling + * .Fn IoFd + * and then passing the result into + * .Fn StreamIo . + */ +extern Stream * StreamFd(int); + +/** + * Create a new stream using the specified C FILE pointer. This is a + * convenience function for calling + * .Fn IoFile + * and then passing the result into + * .Fn StreamIo . + */ +extern Stream * StreamFile(FILE *); + +/** + * Create a new stream using the specified path and mode. This is a + * convenience function for calling + * .Xr fopen 3 + * and then passing the result into + * .Fn StreamFile . + */ +extern Stream * StreamOpen(const char *, const char *); + +/** + * Get a stream that writes to the standard output. + */ +extern Stream * StreamStdout(void); + +/** + * Get a stream that writes to the standard error. + */ +extern Stream * StreamStderr(void); + +/** + * Get a stream that reads from the standard input. + */ +extern Stream * StreamStdin(void); + +/** + * Close the stream. This flushes the buffers and closes the underlying + * Io. It is analogous to the standard + * .Xr fclose 3 + * function. + */ +extern int StreamClose(Stream *); + +/** + * Print a formatted string. This function is analogous to the standard + * .Xr vfprintf 3 + * function. + */ +extern int StreamVprintf(Stream *, const char *, va_list); + +/** + * Print a formatted string. This function is analogous to the + * standard + * .Xr fprintf 3 + * function. + */ +extern int StreamPrintf(Stream *, const char *,...); + +/** + * Get a single character from a stream. This function is analogous to + * the standard + * .Xr fgetc 3 + * function. + */ +extern int StreamGetc(Stream *); + +/** + * Push a character back onto the input stream. This function is + * analogous to the standard + * .Xr ungetc 3 + * function. + */ +extern int StreamUngetc(Stream *, int); + +/** + * Write a single character to the stream. This function is analogous + * to the standard + * .Xr fputc 3 + * function. + */ +extern int StreamPutc(Stream *, int); + +/** + * Write a null-terminated string to the stream. This function is + * analogous to the standard + * .Xr fputs 3 + * function. + */ +extern int StreamPuts(Stream *, char *); + +/** + * Read at most the specified number of characters minus 1 from the + * specified stream and store them at the memory located at the + * specified pointer. This function is analogous to the standard + * .Xr fgets 3 + * function. + */ +extern char * StreamGets(Stream *, char *, int); + +/** + * Set the file position indicator for the specified stream. This + * function is analogous to the standard + * .Xr fseeko + * function. + */ +extern off_t StreamSeek(Stream *, off_t, int); + +/** + * Test the end-of-file indicator for the given stream, returning a + * boolean value indicating whether or not it is set. This is analogous + * to the standard + * .Xr feof 3 + * function. + */ +extern int StreamEof(Stream *); + +/** + * Test the stream for an error condition, returning a boolean value + * indicating whether or not one is present. This is analogous to the + * standard + * .Xr ferror 3 + * function. + */ +extern int StreamError(Stream *); + +/** + * Clear the error condition associated with the given stream, allowing + * future reads or writes to potentially be successful. This functio + * is analogous to the standard + * .Xr clearerr 3 + * function. + */ +extern void StreamClearError(Stream *); + +/** + * Flush all buffered data using the streams underlying write function. + * This function is analogous to the standard + * .Xr fflush 3 + * function. + */ +extern int StreamFlush(Stream *); + +/** + * Read all the bytes from the first stream and write them to the + * second stream. This is analogous to + * .Fn IoCopy , + * but it uses the internal buffers of the streams. It is probably + * less efficient than doing a + * .Fn IoCopy + * instead, but it is more convenient. + */ +extern ssize_t StreamCopy(Stream *, Stream *); + +/** + * Get the file descriptor associated with the given stream, or -1 if + * the stream is not associated with any file descriptor. This function + * is analogous to the standard + * .Xr fileno 3 + * function. + */ +extern int StreamFileno(Stream *); + +#endif /* CYTOPLASM_STREAM_H */ diff --git a/src/include/Tls.h b/src/include/Tls.h new file mode 100644 index 0000000..4407ed0 --- /dev/null +++ b/src/include/Tls.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_TLS_H +#define CYTOPLASM_TLS_H + +/*** + * @Nm Tls + * @Nd Interface to platform-dependent TLS libraries. + * @Dd April 29 2023 + * @Xr Stream Io + * + * .Nm + * provides an interface to platform-dependent TLS libraries. It allows + * Cytoplasm to support any TLS library with no changes to existing + * code. Support for additional TLS libraries is added by creating a + * new compilation unit that implements all the functions here, with + * the exception of a few, which are noted. + * .Pp + * Currently, Cytoplasm has support for the following TLS libraries: + * .Bl -bullet -offset indent + * .It + * LibreSSL + * .It + * OpenSSL + * .El + */ + +#include + +#define TLS_LIBRESSL 2 +#define TLS_OPENSSL 3 + +/** + * Create a new TLS client stream using the given file descriptor and + * the given server hostname. The hostname should be used to verify + * that the server actually is who it says it is. + * .Pp + * This function does not need to be implemented by the individual + * TLS support stubs. + */ +extern Stream * TlsClientStream(int, const char *); + +/** + * Create a new TLS server stream using the given certificate and key + * file, in the format natively supported by the TLS library. + * .Pp + * This function does not need to be implemented by the individual + * TLS support stubs. + */ +extern Stream * TlsServerStream(int, const char *, const char *); + +/** + * Initialize a cookie that stores information about the given client + * connection. This cookie will be passed into the other functions + * defined by this API. + */ +extern void * TlsInitClient(int, const char *); + +/** + * Initialize a cookie that stores information about the given + * server connection. This cookie will be passed into the other + * functions defined by this API. + */ +extern void * TlsInitServer(int, const char *, const char *); + +/** + * Read from a TLS stream, decrypting it and storing the result in the + * specified buffer. This function takes the cookie, buffer, and + * number of decrypted bytes to read into it. See the documentation for + * .Fn IoRead . + */ +extern ssize_t TlsRead(void *, void *, size_t); + +/** + * Write to a TLS stream, encrypting the buffer. This function takes + * the cookie, buffer, and number of unencrypted bytes to write to + * the stream. See the documentation for + * .Fn IoWrite . + */ +extern ssize_t TlsWrite(void *, void *, size_t); + +/** + * Close the TLS stream, also freeing all memory associated with the + * cookie. + */ +extern int TlsClose(void *); + +#endif /* CYTOPLASM_TLS_H */ diff --git a/src/include/Uri.h b/src/include/Uri.h new file mode 100644 index 0000000..a47bf28 --- /dev/null +++ b/src/include/Uri.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#ifndef CYTOPLASM_URI_H +#define CYTOPLASM_URI_H + +/*** + * @Nm Uri + * @Nd Parse a URI. Typically used to parse HTTP(s) URLs. + * @Dd April 29 2023 + * @Xr Http + * + * .Nm + * provides a simple mechanism for parsing URIs. This is an extremely + * basic parser that (ab)uses + * .Xr sscanf 3 + * to parse URIs, so it may not be the most reliable, but it should + * work in most cases and on reasonable URIs that aren't too long, as + * the _MAX definitions are modest. + */ + +#define URI_PROTO_MAX 8 +#define URI_HOST_MAX 128 +#define URI_PATH_MAX 256 + +/** + * The parsed URI is stored in this structure. + */ +typedef struct Uri +{ + char proto[URI_PROTO_MAX]; + char host[URI_HOST_MAX]; + char path[URI_PATH_MAX]; + unsigned short port; +} Uri; + +/** + * Parse a URI string into the Uri structure as described above, or + * return NULL if there was a parsing error. + */ +extern Uri * UriParse(const char *); + +/** + * Free the memory associated with a Uri structure returned by + * .Fn UriParse . + */ +extern void UriFree(Uri *); + +#endif /* CYTOPLASM_URI_H */ diff --git a/src/include/Util.h b/src/include/Util.h new file mode 100644 index 0000000..0a3e02d --- /dev/null +++ b/src/include/Util.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ + +#ifndef CYTOPLASM_UTIL_H +#define CYTOPLASM_UTIL_H + +/*** + * @Nm Util + * @Nd Some misc. helper functions that don't need their own headers. + * @Dd February 15 2023 + * + * This header holds a number of random functions related to strings, + * time, the filesystem, and other simple tasks that don't require a + * full separate API. For the most part, the functions here are + * entirely standalone, depending only on POSIX functions, however + * there are a few that depend explicitly on a few other APIs. Those + * are noted. + */ + +#include +#include +#include + +#include + +/** + * Get the current timestamp in milliseconds since the Unix epoch. This + * uses + * .Xr gettimeofday 2 + * and time_t, and converts it to a single number, which is then + * returned to the caller. + * .Pp + * A note on the 2038 problem: as long as sizeof(long) >= 8, that is, + * as long as the long data type is 64 bits or more, then everything + * should be fine. On most, if not, all, 64-bit systems, long is 64 + * bits. I would expect Cytoplasm to break for 32 bit systems + * eventually, but we should have a ways to go before that happens. + * I didn't want to try to hack together some system to store larger + * numbers than the architecture supports. But we can always + * re-evaluate over the next few years. + */ +extern unsigned long UtilServerTs(void); + +/** + * Use + * .Xr stat 2 + * to get the last modified time of the given file, or zero if there + * was an error getting the last modified time of a file. This is + * primarily useful for caching file data. + */ +extern unsigned long UtilLastModified(char *); + +/** + * This function behaves just like the system call + * .Xr mkdir 2 , + * but it creates any intermediate directories as necessary, unlike + * .Xr mkdir 2 . + */ +extern int UtilMkdir(const char *, const mode_t); + +/** + * Sleep the calling thread for the given number of milliseconds. + * POSIX does not have a very friendly way to sleep, so this wraps + * .Xr nanosleep 2 + * to make its usage much, much simpler. + */ +extern int UtilSleepMillis(long); + +/** + * This function works identically to the POSIX + * .Xr getdelim 3 , + * except that it assumes pointers were allocated with the Memory API + * and it reads from a Stream instead of a file pointer. + */ +extern ssize_t UtilGetDelim(char **, size_t *, int, Stream *); + +/** + * This function is just a special case of + * .Fn UtilGetDelim + * that sets the delimiter to the newline character. + */ +extern ssize_t UtilGetLine(char **, size_t *, Stream *); + +#endif /* CYTOPLASM_UTIL_H */ diff --git a/tools/hdoc.c b/tools/hdoc.c new file mode 100644 index 0000000..51c7999 --- /dev/null +++ b/tools/hdoc.c @@ -0,0 +1,556 @@ +/* + * Copyright (C) 2022-2023 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 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. + */ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +typedef struct DocDecl +{ + char docs[HEADER_EXPR_MAX]; + HeaderDeclaration decl; +} DocDecl; + +typedef struct DocTypedef +{ + char docs[HEADER_EXPR_MAX]; + char text[HEADER_EXPR_MAX]; +} DocTypedef; + +typedef struct DocGlobal +{ + char docs[HEADER_EXPR_MAX]; + HeaderGlobal global; +} DocGlobal; + +static void +ParseMainBlock(HashMap * registers, Array * descr, char *comment) +{ + char *line = strtok(comment, "\n"); + + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (!*line) + { + goto next; + } + + if (*line == '@') + { + int i = 0; + + line++; + + while (!isspace(line[i])) + { + i++; + } + + line[i] = '\0'; + + Free(HashMapSet(registers, line, StrDuplicate(line + i + 1))); + } + else + { + ArrayAdd(descr, StrDuplicate(line)); + } + +next: + line = strtok(NULL, "\n"); + } +} + +int +Main(Array * args) +{ + HeaderExpr expr; + size_t i; + char *key; + char *val; + int exit = EXIT_SUCCESS; + ArgParseState arg; + + HashMap *registers = HashMapCreate(); + Array *descr = ArrayCreate(); + + Array *declarations = ArrayCreate(); + DocDecl *decl = NULL; + + Array *typedefs = ArrayCreate(); + DocTypedef *type = NULL; + + Array *globals = ArrayCreate(); + DocGlobal *global = NULL; + + char comment[HEADER_EXPR_MAX]; + int isDocumented = 0; + + Stream *in = NULL; + Stream *out = NULL; + + int opt; + + ArgParseStateInit(&arg); + while ((opt = ArgParse(&arg, args, "i:o:D:")) != -1) + { + switch (opt) + { + case 'i': + if (in) + { + break; + } + + if (StrEquals(arg.optArg, "-")) + { + in = StreamStdin(); + } + else + { + int len = strlen(arg.optArg); + + in = StreamOpen(arg.optArg, "r"); + if (!in) + { + StreamPrintf(StreamStderr(), "Error: %s:%s", + arg.optArg, strerror(errno)); + exit = EXIT_FAILURE; + goto finish; + } + + while (arg.optArg[len - 1] != '.') + { + arg.optArg[len - 1] = '\0'; + len--; + } + + arg.optArg[len - 1] = '\0'; + len--; + + HashMapSet(registers, "Nm", StrDuplicate(arg.optArg)); + } + break; + case 'o': + if (out) + { + break; + } + + if (StrEquals(arg.optArg, "-")) + { + out = StreamStdout(); + } + else + { + out = StreamOpen(arg.optArg, "w"); + if (!out) + { + StreamPrintf(StreamStderr(), "Error: %s:%s", + arg.optArg, strerror(errno)); + exit = EXIT_FAILURE; + goto finish; + } + } + break; + case 'D': + val = arg.optArg; + while (*val && *val != '=') + { + val++; + } + if (!*val || *val != '=') + { + StreamPrintf(StreamStderr(), "Bad register definition: %s", + arg.optArg); + exit = EXIT_FAILURE; + goto finish; + } + + *val = '\0'; + val++; + HashMapSet(registers, arg.optArg, StrDuplicate(val)); + break; + } + } + + if (!in) + { + in = StreamStdin(); + } + + if (!out) + { + out = StreamStdout(); + } + + memset(&expr, 0, sizeof(expr)); + + while (1) + { + HeaderParse(in, &expr); + + switch (expr.type) + { + case HP_PREPROCESSOR_DIRECTIVE: + /* Ignore */ + break; + case HP_EOF: + /* Done parsing */ + goto last; + case HP_PARSE_ERROR: + case HP_SYNTAX_ERROR: + StreamPrintf(StreamStderr(), "Parse Error: (line %d) %s\n", + expr.data.error.lineNo, expr.data.error.msg); + exit = EXIT_FAILURE; + goto finish; + case HP_COMMENT: + if (expr.data.text[0] != '*') + { + break; + } + + if (strncmp(expr.data.text, "**", 2) == 0) + { + ParseMainBlock(registers, descr, expr.data.text); + } + else + { + strncpy(comment, expr.data.text, sizeof(comment)); + isDocumented = 1; + } + break; + case HP_TYPEDEF: + if (HashMapGet(registers, "ignore-typedefs")) + { + break; + } + + if (!isDocumented) + { + StreamPrintf(StreamStderr(), + "Error: Undocumented typedef:\n%s\n", + expr.data.text); + exit = EXIT_FAILURE; + goto finish; + } + else + { + type = Malloc(sizeof(DocTypedef)); + strncpy(type->docs, comment, sizeof(type->docs)); + strncpy(type->text, expr.data.text, sizeof(type->text)); + ArrayAdd(typedefs, type); + isDocumented = 0; + } + break; + case HP_DECLARATION: + if (!isDocumented) + { + StreamPrintf(StreamStderr(), + "Error: %s() is undocumented.\n", + expr.data.declaration.name); + exit = EXIT_FAILURE; + goto finish; + } + else + { + decl = Malloc(sizeof(DocDecl)); + decl->decl = expr.data.declaration; + decl->decl.args = ArrayCreate(); + strncpy(decl->docs, comment, sizeof(decl->docs)); + for (i = 0; i < ArraySize(expr.data.declaration.args); i++) + { + ArrayAdd(decl->decl.args, StrDuplicate(ArrayGet(expr.data.declaration.args, i))); + } + ArrayAdd(declarations, decl); + isDocumented = 0; + } + break; + case HP_GLOBAL: + if (!isDocumented) + { + StreamPrintf(StreamStderr(), + "Error: Global %s is undocumented.\n", + expr.data.global.name); + exit = EXIT_FAILURE; + goto finish; + } + else + { + global = Malloc(sizeof(DocGlobal)); + global->global = expr.data.global; + + strncpy(global->docs, comment, sizeof(global->docs)); + ArrayAdd(globals, global); + isDocumented = 0; + } + break; + case HP_UNKNOWN: + if (HashMapGet(registers, "suppress-warnings")) + { + break; + } + StreamPrintf(StreamStderr(), "Warning: Unknown expression: %s\n", + expr.data.text); + break; + default: + StreamPrintf(StreamStderr(), "Unknown header type: %d\n", expr.type); + StreamPrintf(StreamStderr(), "This is a programming error.\n"); + exit = EXIT_FAILURE; + goto finish; + } + } + +last: + val = HashMapGet(registers, "Nm"); + if (!val) + { + HashMapSet(registers, "Nm", StrDuplicate("Unnamed")); + } + + val = HashMapGet(registers, "Dd"); + if (!val) + { + time_t currentTime; + struct tm *timeInfo; + char tsBuf[1024]; + + currentTime = time(NULL); + timeInfo = localtime(¤tTime); + strftime(tsBuf, sizeof(tsBuf), "%B %d %Y", timeInfo); + + val = tsBuf; + } + StreamPrintf(out, ".Dd $%s: %s $\n", "Mdocdate", val); + + val = HashMapGet(registers, "Os"); + if (val) + { + StreamPrintf(out, ".Os %s\n", val); + } + + val = HashMapGet(registers, "Nm"); + StreamPrintf(out, ".Dt %s 3\n", val); + StreamPrintf(out, ".Sh NAME\n"); + StreamPrintf(out, ".Nm %s\n", val); + + val = HashMapGet(registers, "Nd"); + if (!val) + { + val = "No Description."; + } + StreamPrintf(out, ".Nd %s\n", val); + + StreamPrintf(out, ".Sh SYNOPSIS\n"); + val = HashMapGet(registers, "Nm"); + StreamPrintf(out, ".In %s.h\n", val); + for (i = 0; i < ArraySize(declarations); i++) + { + size_t j; + + decl = ArrayGet(declarations, i); + StreamPrintf(out, ".Ft %s\n", decl->decl.returnType); + StreamPrintf(out, ".Fn %s ", decl->decl.name); + for (j = 0; j < ArraySize(decl->decl.args); j++) + { + StreamPrintf(out, "\"%s\" ", ArrayGet(decl->decl.args, j)); + } + StreamPutc(out, '\n'); + } + + if (ArraySize(globals)) + { + StreamPrintf(out, ".Sh GLOBALS\n"); + for (i = 0; i < ArraySize(globals); i++) + { + char *line; + global = ArrayGet(globals, i); + + StreamPrintf(out, ".Ss %s %s\n", global->global.type, global->global.name); + + line = strtok(global->docs, "\n"); + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (*line) + { + StreamPrintf(out, "%s\n", line); + } + + line = strtok(NULL, "\n"); + } + } + } + + if (ArraySize(typedefs)) + { + StreamPrintf(out, ".Sh TYPE DECLARATIONS\n"); + for (i = 0; i < ArraySize(typedefs); i++) + { + char *line; + + type = ArrayGet(typedefs, i); + StreamPrintf(out, ".Bd -literal -offset indent\n"); + StreamPrintf(out, "%s\n", type->text); + StreamPrintf(out, ".Ed\n.Pp\n"); + + line = strtok(type->docs, "\n"); + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (*line) + { + StreamPrintf(out, "%s\n", line); + } + + line = strtok(NULL, "\n"); + } + } + } + + StreamPrintf(out, ".Sh DESCRIPTION\n"); + for (i = 0; i < ArraySize(descr); i++) + { + StreamPrintf(out, "%s\n", ArrayGet(descr, i)); + } + + for (i = 0; i < ArraySize(declarations); i++) + { + size_t j; + char *line; + + decl = ArrayGet(declarations, i); + StreamPrintf(out, ".Ss %s %s(", + decl->decl.returnType, decl->decl.name); + for (j = 0; j < ArraySize(decl->decl.args); j++) + { + StreamPrintf(out, "%s", ArrayGet(decl->decl.args, j)); + if (j < ArraySize(decl->decl.args) - 1) + { + StreamPuts(out, ", "); + } + } + StreamPuts(out, ")\n"); + + line = strtok(decl->docs, "\n"); + while (line) + { + while (*line && (isspace(*line) || *line == '*')) + { + line++; + } + + if (*line) + { + StreamPrintf(out, "%s\n", line); + } + + line = strtok(NULL, "\n"); + } + } + + val = HashMapGet(registers, "Xr"); + if (val) + { + char *xr = strtok(val, " "); + + StreamPrintf(out, ".Sh SEE ALSO\n"); + while (xr) + { + if (*xr) + { + StreamPrintf(out, ".Xr %s 3 ", xr); + } + + xr = strtok(NULL, " "); + + if (xr) + { + StreamPutc(out, ','); + } + StreamPutc(out, '\n'); + } + } + +finish: + if (in != StreamStdin()) + { + StreamClose(in); + } + + if (out != StreamStdout()) + { + StreamClose(out); + } + + for (i = 0; i < ArraySize(declarations); i++) + { + Free(ArrayGet(declarations, i)); + } + + for (i = 0; i < ArraySize(typedefs); i++) + { + Free(ArrayGet(typedefs, i)); + } + + for (i = 0; i < ArraySize(globals); i++) + { + Free(ArrayGet(globals, i)); + } + + for (i = 0; i < ArraySize(descr); i++) + { + Free(ArrayGet(descr, i)); + } + + while (HashMapIterate(registers, &key, (void **) &val)) + { + Free(val); + } + + HashMapFree(registers); + + return exit; +}