commit bb5e4eeaa960e0e8931212e4f159e8b27be1c449 Author: Mikhail Romanko Date: Tue Sep 22 19:00:03 2026 +0300 Initial commit diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..01e9e41 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches: [trunk] + pull_request: + branches: [trunk] + +jobs: + build-and-analyze: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install tools + run: | + sudo apt-get update -qq + sudo apt-get install -y build-essential clang clang-tools clang-tidy cppcheck gcc-mingw-w64-x86-64 cmake + + - name: Build with GCC + run: | + make -f Makefile.posix clean + make -f Makefile.posix CC=gcc CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with Clang + run: | + make -f Makefile.posix clean + make -f Makefile.posix CC=clang CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with MinGW + run: | + make -f Makefile.posix clean + make -f Makefile.mingw CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-ar CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with CMake + run: | + cmake -S . -B build + cmake --build build + + - name: Linting checks + run: | + echo "Running all linting checks..." + FAILED=0 + + # cppcheck + echo "=== Check 1/4: cppcheck ===" + if make -f Makefile.lint cppcheck; then + echo "cppcheck: PASSED" + else + echo "cppcheck: FAILED" + FAILED=1 + fi + + # clang-tidy + echo "=== Check 2/4: clang-tidy ===" + if make -f Makefile.lint clang-tidy; then + echo "clang-tidy: PASSED" + else + echo "clang-tidy: FAILED" + FAILED=1 + fi + + # scan-build + echo "=== Check 3/4: scan-build ===" + if make -f Makefile.lint scan-build; then + echo "scan-build: PASSED" + else + echo "scan-build: FAILED" + FAILED=1 + fi + + # security-check + echo "=== Check 4/4: security-check ===" + if make -f Makefile.lint security-check; then + echo "security-check: PASSED" + else + echo "security-check: FAILED" + FAILED=1 + fi + + # Final result + if [ $FAILED -ne 0 ]; then + echo "One or more linting checks failed." + exit 1 + else + echo "All linting checks passed." + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f765426 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# ---> C +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# ---> CMake +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5a4be30 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.10) +project(CgeConf LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(CONF_SOURCES + Conf.c + Query.c +) + +set(CONF_HEADERS + CgeConf.h + CgeConfQuery.h +) + +add_library(CgeConf STATIC ${CONF_SOURCES} ${CONF_HEADERS}) + +target_include_directories(CgeConf PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +install(TARGETS CgeConf + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(FILES ${CONF_HEADERS} DESTINATION include) diff --git a/CgeConf.h b/CgeConf.h new file mode 100644 index 0000000..7cdd843 --- /dev/null +++ b/CgeConf.h @@ -0,0 +1,33 @@ +#ifndef CGE_CONF_H +#define CGE_CONF_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CgeConfIO { + int (*read)(void *ctx, void *buffer, size_t size, size_t *actual); + void *ctx; +} CgeConfIO; + +enum CgeConfEvent { + CGE_CONF_SECTION, + CGE_CONF_KEYVALUE, +}; + +typedef void (*CgeConfCb)(int type, const char *key, const char *value, + void *user); + +int CgeConfFromString(const char *str, CgeConfCb callback, void *user); +int CgeConfFromMemory(const char *buffer, size_t size, CgeConfCb callback, + void *user); +int CgeConfFromFile(const char *path, CgeConfCb callback, void *user); +int CgeConfFromCb(CgeConfIO *io, CgeConfCb callback, void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* CGE_CONF_H */ diff --git a/CgeConfQuery.h b/CgeConfQuery.h new file mode 100644 index 0000000..af9a91c --- /dev/null +++ b/CgeConfQuery.h @@ -0,0 +1,30 @@ +#ifndef CGE_CONF_QUERY_H +#define CGE_CONF_QUERY_H + +#include "CgeConf.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CgeConfQuery CgeConfQuery; +typedef void (*CgeConfQueryIterCb)(const char *section, const char *key, + const char *value, void *user); + + +CgeConfQuery *CgeConfQueryFromCb(CgeConfIO *io); +CgeConfQuery *CgeConfQueryFromFile(const char *file); +CgeConfQuery *CgeConfQueryFromMemory(const char *buffer, size_t size); +CgeConfQuery *CgeConfQueryFromString(const char *str); +void CgeConfQueryFree(CgeConfQuery *query); +const char *CgeConfQueryGet(CgeConfQuery *query, const char *section, + const char *key, const char *defaultValue); + +void CgeConfQueryIter(CgeConfQuery *query, CgeConfQueryIterCb iter, + void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* CGE_CONF_QUERY_H */ diff --git a/Conf.c b/Conf.c new file mode 100644 index 0000000..6e5d60c --- /dev/null +++ b/Conf.c @@ -0,0 +1,188 @@ +#include "CgeConf.h" +#include +#include +#include + +#define MAX_LINE 4096 + +typedef struct StrView { + char *data; + size_t size; +} StrView; + +typedef struct MemCtx { + const char *buffer; + size_t size; + size_t pos; +} MemCtx; + +static int memRead(void *ctx, void *buffer, size_t size, size_t *actual) { + MemCtx *mem = (MemCtx *)ctx; + size_t remaining = mem->size - mem->pos; + + if (size > remaining) + size = remaining; + + memcpy(buffer, mem->buffer + mem->pos, size); + mem->pos += size; + *actual = size; + return 1; +} + +static int fileRead(void *ctx, void *buffer, size_t size, size_t *actual) { + *actual = fread(buffer, 1, size, (FILE *)ctx); + return 1; +} + +static void trimLeft(StrView *view) { + for (; view->size && isspace(*view->data); ++view->data, --view->size); +} + +static void trimRight(StrView *view) { + char *last = view->data + view->size; + for (; last > view->data && isspace(*(last - 1)); last--, view->size--); +} + +static void trim(StrView *view) { + trimLeft(view); + trimRight(view); +} + +static size_t firstIndex(StrView *view, char symbol) { + char *current, *end; + current = view->data; + end = view->data + view->size; + + for (; current < end; ++current) { + if (*current == symbol) + return current - view->data; + } + return -1; +} + +static int split(StrView *view, StrView *prefix, char delim) { + size_t position; + + if ((position = firstIndex(view, delim)) == -1) + return 0; + + prefix->data = view->data; + prefix->size = position; + view->data += position; + view->size -= position; + return 1; +} + +static void parseLine(StrView *view, CgeConfCb callback, void *user) { + StrView key; + + trim(view); + + if (!view->size || *view->data == '#' || *view->data == ';') + return; + + if (*view->data == '[' && *(view->data + view->size - 1) == ']') { + view->size -= 2; + view->data += 1; + trim(view); + + view->data[view->size] = 0; + callback(CGE_CONF_SECTION, NULL, view->data, user); + } else if (split(view, &key, '=')) { + trimRight(&key); + view->data++, view->size--; + trimLeft(view); + + if (view->size > 1 && *view->data == '"' && + *(view->data + view->size - 1) == '"') { + view->size -= 2; + view->data += 1; + } + + key.data[key.size] = 0; + view->data[view->size] = 0; + callback(CGE_CONF_KEYVALUE, key.data, view->data, user); + } +} + +static int readSymbol(CgeConfIO *io) { + unsigned char symbol; + size_t actual; + + if (!io->read(io->ctx, &symbol, 1, &actual) || !actual) + return -1; + + return (int)symbol; +} + +static size_t readLine(CgeConfIO *io, char *buffer, size_t size) { + int symbol; + size_t counter = 0; + + while ((symbol = readSymbol(io)) != -1) { + if (counter && symbol == '\n') + break; + else if (symbol == '\n') + continue; + + if (symbol == '\r') + continue; + + if (counter >= size - 1) + return -1; + + buffer[counter++] = symbol; + } + + buffer[counter] = 0; + return counter; +} + +int CgeConfFromString(const char *str, CgeConfCb callback, void *user) { + return CgeConfFromMemory(str, strlen(str), callback, user); +} + +int CgeConfFromMemory(const char *buffer, size_t size, CgeConfCb callback, + void *user) { + CgeConfIO io; + MemCtx mem; + + mem.buffer = buffer; + mem.size = size; + mem.pos = 0; + + io.read = memRead; + io.ctx = &mem; + + return CgeConfFromCb(&io, callback, user); +} + +int CgeConfFromFile(const char *path, CgeConfCb callback, void *user) { + CgeConfIO io; + FILE *file; + int ret; + + file = fopen(path, "rb"); + if (!file) + return 0; + + io.read = fileRead; + io.ctx = file; + + ret = CgeConfFromCb(&io, callback, user); + fclose(file); + return ret; +} + +int CgeConfFromCb(CgeConfIO *io, CgeConfCb callback, void *user) { + char buffer[MAX_LINE]; + StrView view; + + while ((view.size = readLine(io, buffer, MAX_LINE))) { + if (view.size == -1) + return 0; + view.data = buffer; + parseLine(&view, callback, user); + } + return 1; +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d44ffd --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (C) 2026 by blankhex me@blankhex.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile.lint b/Makefile.lint new file mode 100644 index 0000000..cac9110 --- /dev/null +++ b/Makefile.lint @@ -0,0 +1,29 @@ +# Makefile.lint - Individual linting targets + +.PHONY: all +all: cppcheck clang-tidy scan-build security-check + @echo "All linting scheduled" + +.PHONY: cppcheck +cppcheck: + @echo "Running cppcheck..." + @cppcheck --enable=warning,performance,portability --std=c99 --quiet -Iinclude -I. . + +.PHONY: clang-tidy +clang-tidy: + @echo "Running clang-tidy..." + @for file in $$(find . -name "*.c" -type f -not -path "*/win32/*"); do \ + clang-tidy "$$file" -- -I. -Iinclude -std=c99; \ + done + +.PHONY: scan-build +scan-build: + @echo "Running scan-build..." + @scan-build --status-bugs make -f Makefile.posix clean all CC=clang + +.PHONY: security-check +security-check: + @echo "Scanning for unsafe C functions..." + @grep -rnE '\b(strcpy|strcat|sprintf|gets|scanf|sscanf|realpath|mktemp|tempnam|tmpnam|getwd|getlogin)\b' \ + . --include="*.c" --include="*.h" | grep -v "^Binary file" && \ + exit 1 || exit 0 \ No newline at end of file diff --git a/Makefile.mingw b/Makefile.mingw new file mode 100644 index 0000000..a04de9b --- /dev/null +++ b/Makefile.mingw @@ -0,0 +1,23 @@ +# MinGW Makefile for CgeConf + +CC = gcc +AR = ar +CFLAGS = -std=c99 -O2 -Wall -Wextra +ARFLAGS = rcs +TARGET = libCgeConf.a + +SOURCES = Conf.c Query.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(AR) $(ARFLAGS) $@ $^ + +%.o: %.c CgeConf.h CgeConfQuery.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + del $(OBJECTS) $(TARGET) 2>nul || exit 0 diff --git a/Makefile.posix b/Makefile.posix new file mode 100644 index 0000000..8b34b05 --- /dev/null +++ b/Makefile.posix @@ -0,0 +1,35 @@ +# POSIX Makefile for CgeConf + +CC = gcc +AR = ar +CFLAGS = -std=c99 -O2 -Wall -Wextra -fPIC +ARFLAGS = rcs +TARGET = libCgeConf.a + +SOURCES = Conf.c Query.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean install + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(AR) $(ARFLAGS) $@ $^ + +%.o: %.c CgeConf.h CgeConfQuery.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJECTS) $(TARGET) + +install: $(TARGET) + cp $(TARGET) /usr/local/lib/ + cp CgeConf.h /usr/local/include/ + cp CgeConfQuery.h /usr/local/include/ + ldconfig || echo "Run ldconfig manually if needed" + +uninstall: + rm -f /usr/local/lib/libCgeConf.a + rm -f /usr/local/include/CgeConf.h + rm -f /usr/local/include/CgeConfQuery.h + ldconfig || true diff --git a/Makefile.win32 b/Makefile.win32 new file mode 100644 index 0000000..c7b9ee0 --- /dev/null +++ b/Makefile.win32 @@ -0,0 +1,23 @@ +# Makefile.win32 for MSVC (NMake) — CgeConf +# Usage: Open "x86 Native Tools Command Prompt", then: +# nmake -f Makefile.win32 + +CC = cl +LIB = lib +CFLAGS = /c /nologo /W3 /O2 +LIBFLAGS = /nologo +TARGET = CgeConf.lib + +SOURCES = Conf.c Query.c +OBJECTS = $(SOURCES:.c=.obj) + +$(TARGET): $(OBJECTS) + $(LIB) $(LIBFLAGS) /OUT:$(TARGET) $(OBJECTS) + +{.}.c{}.obj: + $(CC) $(CFLAGS) /Fo$@ $< + +clean: + del $(OBJECTS) $(TARGET) 2>nul + +.PHONY: clean diff --git a/Query.c b/Query.c new file mode 100644 index 0000000..883f0ac --- /dev/null +++ b/Query.c @@ -0,0 +1,258 @@ +#include "CgeConfQuery.h" + +#include +#include + +#define INIT_EXP 4 +#define INIT_CAPACITY ((size_t)1 << INIT_EXP) +#define MAX_SECTION 4096 + +typedef struct { + char *section; + char *key; + char *value; +} Entry; + +struct CgeConfQuery { + Entry *entryData; + size_t size; + size_t capacity; + Entry **slotData; + char currentSection[MAX_SECTION]; + int exp; +}; + +static char *stringDup(const char *str) { + size_t length; + char *dest; + + length = strlen(str); + if ((dest = malloc(length + 1))) + memcpy(dest, str, length + 1); + return dest; +} + +static size_t hashKey(const char *section, const char *key) { + size_t h = 5381; + const char *p; + int c; + + for (p = section; (c = *p); ++p) + h = ((h << 5) + h) + (unsigned char)c; + h = ((h << 5) + h); + for (p = key; (c = *p); ++p) + h = ((h << 5) + h) + (unsigned char)c; + + return h; +} + +static size_t lookup(size_t hash, int exp, size_t index) { + size_t mask = (1 << exp) - 1; + size_t step = (hash >> (sizeof(hash) * 8 - exp)) | 1; + return (index + step) & mask; +} + +static int reserveSpace(CgeConfQuery *query, size_t need) { + CgeConfQuery tmp; + size_t i, hash, index; + + if (need <= (query->capacity - (query->capacity >> 2))) + return 1; + + tmp.exp = query->exp ? query->exp + 1 : INIT_EXP; + tmp.capacity = query->capacity ? query->capacity * 2 : INIT_CAPACITY; + tmp.size = 0; + tmp.entryData = malloc(tmp.capacity * sizeof(*tmp.entryData)); + tmp.slotData = malloc(tmp.capacity * sizeof(*tmp.slotData)); + if (!tmp.entryData || !tmp.slotData) { + free(tmp.entryData); + free(tmp.slotData); + return 0; + } + memset(tmp.slotData, 0, sizeof(*tmp.slotData) * tmp.capacity); + + for (i = 0; i < query->size; ++i) { + Entry *entry = &query->entryData[i]; + + hash = hashKey(entry->section, entry->key); + index = lookup(hash, tmp.exp, 0); + while (tmp.slotData[index] != NULL) + index = lookup(hash, tmp.exp, index); + + tmp.entryData[tmp.size] = *entry; + tmp.slotData[index] = tmp.entryData + tmp.size++; + } + + free(query->entryData); + free(query->slotData); + query->entryData = tmp.entryData; + query->slotData = tmp.slotData; + query->capacity = tmp.capacity; + query->exp = tmp.exp; + return 1; +} + +static int insert(CgeConfQuery *query, const char *section, const char *key, + const char *value) { + size_t hash, index; + char *newValue; + Entry *entry; + + if (!reserveSpace(query, query->size + 1)) + return 0; + + hash = hashKey(section, key); + index = lookup(hash, query->exp, 0); + + while ((entry = query->slotData[index])) { + if (strcmp(entry->section, section) == 0 && + strcmp(entry->key, key) == 0) { + newValue = stringDup(value); + if (!newValue) + return 0; + free(entry->value); + entry->value = newValue; + return 1; + } + index = lookup(hash, query->exp, index); + } + + entry = &query->entryData[query->size]; + entry->section = stringDup(section); + entry->key = stringDup(key); + entry->value = stringDup(value); + if (!entry->section || !entry->key || !entry->value) { + free(entry->section); + free(entry->key); + free(entry->value); + return 0; + } + + query->size++; + query->slotData[index] = entry; + return 1; +} + +static void populateCb(int type, const char *key, const char *value, + void *user) { + CgeConfQuery *query = (CgeConfQuery *)user; + size_t len; + + if (type == CGE_CONF_SECTION) { + len = strlen(value); + if (len >= sizeof(query->currentSection)) + len = sizeof(query->currentSection) - 1; + memcpy(query->currentSection, value, len); + query->currentSection[len] = 0; + } else if (type == CGE_CONF_KEYVALUE) { + insert(query, query->currentSection, key, value); + } +} + +static CgeConfQuery *queryNew(void) { + CgeConfQuery *query; + + query = (CgeConfQuery *)malloc(sizeof(CgeConfQuery)); + if (!query) + return NULL; + + memset(query, 0, sizeof(*query)); + return query; +} + +CgeConfQuery *CgeConfQueryFromCb(CgeConfIO *io) { + CgeConfQuery *query; + + query = queryNew(); + if (!query) + return NULL; + + if (!CgeConfFromCb(io, populateCb, query)) { + CgeConfQueryFree(query); + return NULL; + } + return query; +} + +CgeConfQuery *CgeConfQueryFromFile(const char *file) { + CgeConfQuery *query; + + query = queryNew(); + if (!query) + return NULL; + + if (!CgeConfFromFile(file, populateCb, query)) { + CgeConfQueryFree(query); + return NULL; + } + return query; +} + +CgeConfQuery *CgeConfQueryFromMemory(const char *buffer, size_t size) { + CgeConfQuery *query; + + query = queryNew(); + if (!query) + return NULL; + + if (!CgeConfFromMemory(buffer, size, populateCb, query)) { + CgeConfQueryFree(query); + return NULL; + } + return query; +} + +CgeConfQuery *CgeConfQueryFromString(const char *str) { + return CgeConfQueryFromMemory(str, strlen(str)); +} + +const char *CgeConfQueryGet(CgeConfQuery *query, const char *section, + const char *key, const char *defaultValue) { + size_t hash, index, i; + Entry *entry; + + if (!query || !key) + return defaultValue; + if (!section) + section = ""; + + hash = hashKey(section, key); + index = lookup(hash, query->exp, 0); + + for (i = 0; i < query->capacity; ++i) { + entry = query->slotData[index]; + if (!entry) + break; + if (strcmp(entry->section, section) == 0 && + strcmp(entry->key, key) == 0) + return entry->value; + index = lookup(hash, query->exp, index); + } + return defaultValue; +} + +void CgeConfQueryFree(CgeConfQuery *query) { + size_t i; + + if (!query) + return; + + for (i = 0; i < query->size; ++i) { + free(query->entryData[i].section); + free(query->entryData[i].key); + free(query->entryData[i].value); + } + free(query->entryData); + free(query->slotData); + free(query); +} + +void CgeConfQueryIter(CgeConfQuery *query, CgeConfQueryIterCb iter, + void *user) { + Entry *entry, *end; + + entry = query->entryData; + end = query->entryData + query->size; + for (; entry != end; ++entry) + iter(entry->section, entry->key, entry->value, user); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c925e6 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# CgeArgs - CLI argument parsing + +TODO + +## License + +0BSD - a permissive license with no attribution required.