diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..335d6bd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop, feature/**, copilot/** ] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + permissions: + contents: read + container: + image: chocotechnologies/dmod:1.0.4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build dmini project + run: | + mkdir -p build + cd build + cmake .. -DDMOD_MODE=DMOD_MODULE + cmake --build . + + - name: Verify module files + run: | + echo "Checking for module files..." + ls -lh build/dmf/ + test -f build/dmf/dmini.dmf + test -f build/dmf/dmini_version.txt + test -f build/dmf/test_dmini.dmf + echo "Module files present" + + - name: Run tests with dmod_loader + run: | + export DMOD_DMF_DIR=$(pwd)/build/dmf + dmod_loader build/dmf/test_dmini.dmf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ef044ea --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,280 @@ +name: Release + +on: + release: + types: [created] + +jobs: + discover-architectures: + name: Discover Architectures + runs-on: ubuntu-latest + permissions: + contents: read + container: + image: chocotechnologies/dmod:1.0.4 + outputs: + architectures: ${{ steps.list-archs.outputs.architectures }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Fetch dmod to discover architectures + run: | + mkdir -p build_discovery + cd build_discovery + cmake .. -DDMOD_MODE=DMOD_MODULE + + - name: List available architectures + id: list-archs + run: | + DMOD_SRC_DIR=$(find build_discovery -path "*/_deps/dmod-src" -type d | head -1) + + # create JSON array + ARCHS=$(find ${DMOD_SRC_DIR}/configs/arch -name "tools-cfg.cmake" | \ + sed "s|${DMOD_SRC_DIR}/configs/arch/||g" | \ + sed 's|/tools-cfg.cmake||g' | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "Found architectures: $ARCHS" + echo "architectures=$ARCHS" >> $GITHUB_OUTPUT + + build-release: + name: Build Release for ${{ matrix.arch_name }} + needs: discover-architectures + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + arch_name: ${{ fromJson(needs.discover-architectures.outputs.architectures) }} + + container: + image: chocotechnologies/dmod:1.0.4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version from tag + id: get_version + run: | + # Extract version from tag (e.g., v1.2 -> 1.2) + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" # Remove 'v' prefix if present + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Build dmini for ${{ matrix.arch_name }} + run: | + set -e + ARCH_DIR_NAME=$(echo "${{ matrix.arch_name }}" | sed 's|/|-|') + echo "ARCH_DIR_NAME=$ARCH_DIR_NAME" >> $GITHUB_ENV + echo "ARTIFACT_NAME=release-$ARCH_DIR_NAME" >> $GITHUB_ENV + mkdir -p build_$ARCH_DIR_NAME + cd build_$ARCH_DIR_NAME + + # First run cmake to fetch dependencies with version from tag + cmake .. -DDMOD_TOOLS_NAME=arch/${{ matrix.arch_name }} -DDMOD_MODULE_VERSION="${{ steps.get_version.outputs.version }}" + + # Build the modules + cmake --build . + + echo "Build completed for ${{ matrix.arch_name }}" + ls -la dmf/ + ls -la dmfc/ + + - name: Prepare release package + run: | + set -e + mkdir -p release_package + BUILD_DIR=build_$ARCH_DIR_NAME + DMF_DIR="$BUILD_DIR/dmf" + DMFC_DIR="$BUILD_DIR/dmfc" + + cp $DMF_DIR/dmini.dmf release_package/ + cp $DMF_DIR/dmini_version.txt release_package/ + cp $DMFC_DIR/dmini.dmfc release_package/ 2>/dev/null || true + # Copy test_dmini module + cp $DMF_DIR/test_dmini.dmf release_package/ + cp $DMF_DIR/test_dmini_version.txt release_package/ + cp $DMFC_DIR/test_dmini.dmfc release_package/ 2>/dev/null || true + # Copy .dmd files if they exist + if [ -f $DMF_DIR/dmini.dmd ]; then + cp $DMF_DIR/dmini.dmd release_package/ + fi + if [ -f $DMF_DIR/test_dmini.dmd ]; then + cp $DMF_DIR/test_dmini.dmd release_package/ + fi + + # Copy documentation and license + cp README.md release_package/ + cp LICENSE release_package/ + + # Create release notes file from GitHub release + echo "${{ github.event.release.body }}" > release_package/RELEASE_NOTES.txt + + echo "Package contents:" + ls -la release_package/ + + - name: Create release archive + run: | + cd release_package + zip -r ../dmini-${{ github.event.release.tag_name }}-$ARCH_DIR_NAME.zip . + cd .. + echo "Created archive:" + ls -lh dmini-*.zip + + - name: Upload artifact + uses: actions/upload-artifact@v4.4.3 + with: + name: ${{ env.ARTIFACT_NAME }} + path: dmini-${{ github.event.release.tag_name }}-*.zip + retention-days: 1 + + generate-versions-manifest: + name: Generate versions.dmm + needs: discover-architectures + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history to get all tags + + - name: Generate versions.dmm + run: | + set -e + echo "# List of available versions for dmini modules" > versions.dmm + echo "# Generated automatically by CI" >> versions.dmm + echo "" >> versions.dmm + + # Get all version tags (starting with 'v') and extract version numbers + VERSIONS=$(git tag -l 'v*' | sed 's/^v//' | sort -V | tr '\n' ' ' | sed 's/ $//') + + # Remove vlatest from the list if present + VERSIONS=$(echo $VERSIONS | sed 's/\blatest\b//g' | xargs) + + if [ -z "$VERSIONS" ]; then + echo "Warning: No version tags found" + VERSIONS="${{ github.event.release.tag_name }}" + VERSIONS="${VERSIONS#v}" + fi + + echo "Found versions: $VERSIONS" + + # Add $version-available directives for the modules + echo "\$version-available dmini $VERSIONS" >> versions.dmm + echo "\$version-available test_dmini $VERSIONS" >> versions.dmm + + echo "Generated versions.dmm:" + cat versions.dmm + + - name: Upload versions.dmm as artifact + uses: actions/upload-artifact@v4.4.3 + with: + name: versions-manifest + path: versions.dmm + retention-days: 1 + + upload-release-assets: + name: Upload Release Assets + needs: [build-release, generate-versions-manifest] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4.1.3 + with: + path: artifacts + + - name: Display artifact structure + run: | + echo "Downloaded artifacts:" + ls -lR artifacts/ + + - name: Upload release assets to versioned tag + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + # Upload all release archives to the GitHub release + shopt -s nullglob + zip_files=(artifacts/release-*/*.zip) + + if [ ${#zip_files[@]} -eq 0 ]; then + echo "Error: No artifacts found to upload" + exit 1 + fi + + for zip_file in "${zip_files[@]}"; do + echo "Uploading $zip_file to ${{ github.event.release.tag_name }}..." + gh release upload ${{ github.event.release.tag_name }} \ + "$zip_file" \ + --repo ${{ github.repository }} \ + --clobber + done + + # Upload versions.dmm to the versioned release + if [ -f artifacts/versions-manifest/versions.dmm ]; then + echo "Uploading versions.dmm to ${{ github.event.release.tag_name }}..." + gh release upload ${{ github.event.release.tag_name }} \ + artifacts/versions-manifest/versions.dmm \ + --repo ${{ github.repository }} \ + --clobber + fi + + echo "Successfully uploaded ${#zip_files[@]} artifact(s) to ${{ github.event.release.tag_name }}" + + - name: Create or update latest release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + + # Check if vlatest release exists + if gh release view vlatest --repo ${{ github.repository }} >/dev/null 2>&1; then + echo "Release vlatest exists, deleting it..." + gh release delete vlatest --repo ${{ github.repository }} --yes + fi + + # Create new vlatest release + echo "Creating vlatest release..." + gh release create vlatest \ + --repo ${{ github.repository }} \ + --title "Latest Release (based on ${{ github.event.release.tag_name }})" \ + --notes "This release always points to the latest stable version. Currently based on ${{ github.event.release.tag_name }}." + + - name: Upload release assets to latest tag + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + shopt -s nullglob + zip_files=(artifacts/release-*/*.zip) + + for zip_file in "${zip_files[@]}"; do + echo "Uploading $zip_file to vlatest..." + gh release upload vlatest \ + "$zip_file" \ + --repo ${{ github.repository }} \ + --clobber + done + + # Upload versions.dmm to the latest release + if [ -f artifacts/versions-manifest/versions.dmm ]; then + echo "Uploading versions.dmm to vlatest..." + gh release upload vlatest \ + artifacts/versions-manifest/versions.dmm \ + --repo ${{ github.repository }} \ + --clobber + fi + + echo "Successfully uploaded ${#zip_files[@]} artifact(s) to vlatest" diff --git a/.gitignore b/.gitignore index 1f99f9d..4a38b77 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,19 @@ CTestTestfile.cmake _deps CMakeUserPresets.json +# Build directory +build/ +build_*/ +_codeql_build_dir/ +_codeql_detected_source_root +*.dmf +*.dmfc + + +# CodeQL build directory +_codeql_build_dir/ +_codeql_detected_source_root + # CLion # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d32fa98 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,84 @@ +# ===================================================================== +# DMOD INI File Parser Module +# ===================================================================== +cmake_minimum_required(VERSION 3.18) + + +# ====================================================================== +# DMINI Version +# ====================================================================== +# Allow version to be passed as a parameter, default to 0.1 +if(NOT DEFINED DMOD_MODULE_VERSION) + set(DMOD_MODULE_VERSION "0.1" CACHE STRING "DMOD module version") +endif() + +# ====================================================================== +# Fetch DMOD repository +# ====================================================================== +include(FetchContent) +FetchContent_Declare( + dmod + GIT_REPOSITORY https://github.com/choco-technologies/dmod.git + GIT_TAG develop +) + +# ====================================================================== +# DMOD Configuration +# ====================================================================== +set(DMOD_MODE "DMOD_MODULE" CACHE STRING "DMOD build mode") +set(DMOD_BUILD_TESTS OFF CACHE BOOL "Build tests") +set(DMOD_BUILD_EXAMPLES OFF CACHE BOOL "Build examples") +set(DMOD_BUILD_TOOLS OFF CACHE BOOL "Build tools") +set(DMOD_BUILD_TEMPLATES OFF CACHE BOOL "Build templates") + +FetchContent_MakeAvailable(dmod) + +project(dmini + VERSION ${DMOD_MODULE_VERSION} + DESCRIPTION "DMOD INI File Parser" + LANGUAGES C CXX) +set(DMOD_DIR ${dmod_SOURCE_DIR} CACHE PATH "DMOD source directory") + +# ====================================================================== +# Import dmod functions and macros +# ====================================================================== +set(DMOD_DIR ${dmod_SOURCE_DIR} CACHE PATH "DMOD source directory") +set(DMOD_SCRIPTS_DIR ${DMOD_DIR}/scripts CACHE PATH "DMOD scripts directory") +include(${DMOD_DIR}/paths.cmake) +dmod_setup_external_module() + +# ====================================================================== +# DMINI Module Configuration +# ====================================================================== +# Name of the module +set(DMOD_MODULE_NAME dmini) + +# Version is already set above and used in project() +# No need to set it again here + +# Author (should be string) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") + +# Stack size for the module (should be integer) +set(DMOD_STACK_SIZE 1024) + +# +# dmod_add_library - create a library module +# it has the same signature as add_library +# and can be used in the same way after the creation +# (for example, to link libraries) +# +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + # List of source files - can include C and C++ files + src/dmini.c +) + +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +# ====================================================================== +# test_dmini Application +# ====================================================================== +# Add test_dmini application subdirectory +add_subdirectory(apps/test_dmini) diff --git a/README.md b/README.md index d7b596e..86d2a4c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,139 @@ -# dmini -DMOD *.ini parsing module +# dmini - DMOD INI File Parser Module + +A DMOD module library for parsing and generating INI configuration files, optimized for embedded systems. + +## Overview + +dmini is a lightweight INI file parser/generator module designed for embedded systems with limited RAM. It uses only SAL (System Abstraction Layer) functions from DMOD and implements memory-efficient line-by-line file I/O operations. + +## Features + +- **INI File Parsing**: Read and parse INI files with sections, key-value pairs, and comments +- **INI File Generation**: Create INI files from in-memory data structures +- **Memory Efficient**: Line-by-line file I/O with 256-byte buffers (no large allocations) +- **User-Controlled Buffers**: Generate functions accept user-provided buffers to prevent memory leaks +- **SAL-Only**: Uses only DMOD SAL functions (Dmod_Malloc, Dmod_Free, Dmod_StrDup, etc.) +- **Global Section Support**: Handle keys without section headers +- **Comment Support**: Parse comments starting with `;` or `#` +- **Whitespace Trimming**: Automatic trimming of keys and values + +## API + +### Context Management +- `dmini_create()` - Create INI context +- `dmini_destroy()` - Free INI context + +### Parsing +- `dmini_parse_string(ctx, data)` - Parse INI from string +- `dmini_parse_file(ctx, filename)` - Parse INI from file (line-by-line) + +### Generation +- `dmini_generate_string(ctx, buffer, size)` - Generate INI to buffer (returns required size if buffer is NULL) +- `dmini_generate_file(ctx, filename)` - Generate INI directly to file (line-by-line) + +### Data Access +- `dmini_get_string(ctx, section, key, default)` - Get string value +- `dmini_get_int(ctx, section, key, default)` - Get integer value +- `dmini_set_string(ctx, section, key, value)` - Set string value +- `dmini_set_int(ctx, section, key, value)` - Set integer value + +### Queries +- `dmini_has_section(ctx, section)` - Check if section exists +- `dmini_has_key(ctx, section, key)` - Check if key exists + +### Removal +- `dmini_remove_section(ctx, section)` - Remove entire section +- `dmini_remove_key(ctx, section, key)` - Remove single key + +## Usage Example + +```c +#include "dmini.h" + +// Create context +dmini_context_t ctx = dmini_create(); + +// Parse INI file +dmini_parse_file(ctx, "config.ini"); + +// Read values +const char* host = dmini_get_string(ctx, "database", "host", "localhost"); +int port = dmini_get_int(ctx, "database", "port", 5432); + +// Modify values +dmini_set_string(ctx, "cache", "enabled", "true"); +dmini_set_int(ctx, "cache", "size", 1024); + +// Generate to buffer (query size first) +int size = dmini_generate_string(ctx, NULL, 0); +char* buffer = malloc(size); +dmini_generate_string(ctx, buffer, size); + +// Or generate directly to file +dmini_generate_file(ctx, "output.ini"); + +// Cleanup +dmini_destroy(ctx); +``` + +## Building + +```bash +mkdir build +cd build +cmake .. -DDMOD_MODE=DMOD_MODULE +cmake --build . +``` + +This generates: +- `dmf/dmini.dmf` - The INI parser library module (536B RAM, 5KB ROM) +- `dmf/test_dmini.dmf` - Test application (432B RAM, 7KB ROM) + +## Testing + +The test application (`test_dmini.dmf`) runs comprehensive tests covering all API functions: + +```bash +dmod_loader dmf/dmini.dmf dmf/test_dmini.dmf +``` + +Test coverage includes: +- Context creation/destruction +- String parsing with sections and keys +- Setting and getting values +- Section/key existence checks +- Removal operations +- Buffer generation with size queries +- File I/O operations +- Comments and whitespace handling + +## INI File Format + +```ini +; Comment line +global_key=global_value + +[section1] +key1=value1 +key2=value2 + +[section2] +number=42 +name=test +``` + +- Sections are defined by `[section_name]` +- Key-value pairs: `key=value` +- Comments start with `;` or `#` +- Whitespace is automatically trimmed +- Global section for keys without section headers + +## Memory Footprint + +- **dmini library**: 536B RAM, 5KB ROM +- **test_dmini application**: 432B RAM, 7KB ROM + +## License + +MIT License - see LICENSE file for details + diff --git a/apps/test_dmini/CMakeLists.txt b/apps/test_dmini/CMakeLists.txt new file mode 100644 index 0000000..b4755fd --- /dev/null +++ b/apps/test_dmini/CMakeLists.txt @@ -0,0 +1,37 @@ +# ===================================================================== +# test_dmini Test Application +# ===================================================================== +cmake_minimum_required(VERSION 3.18) + +# ====================================================================== +# test_dmini Application Configuration +# ====================================================================== +# Name of the application +set(DMOD_MODULE_NAME test_dmini) + +# Version is inherited from parent +if(NOT DEFINED DMOD_MODULE_VERSION) + set(DMOD_MODULE_VERSION "0.1") +endif() + +# Author +set(DMOD_AUTHOR_NAME "Patryk Kubiak") + +# Stack size for the application +set(DMOD_STACK_SIZE 1024) + +# ====================================================================== +# Build test_dmini Application +# ====================================================================== +dmod_add_executable(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + test_dmini.c +) + +# Include dmini headers +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../include + ${CMAKE_BINARY_DIR} # For dmini_defs.h +) + +# Note: dmini is loaded dynamically at runtime via dmod_loader +# No static linking needed - modules communicate via DMOD API diff --git a/apps/test_dmini/README.md b/apps/test_dmini/README.md new file mode 100644 index 0000000..7bae92e --- /dev/null +++ b/apps/test_dmini/README.md @@ -0,0 +1,115 @@ +# test_dmini - DMINI Test Application + +This is a DMOD application module that comprehensively tests the dmini INI parser module. + +## Overview + +test_dmini is a DMF (DMOD Module Format) application that loads the dmini module and runs a series of tests to verify all API functions work correctly. + +## Usage + +### With dmod_loader + +```bash +# Load both dmini and test_dmini modules +dmod_loader dmini.dmf test_dmini.dmf +``` + +### Expected Output + +``` +=== DMINI Functionality Tests === + +TEST: Create and destroy context + PASSED +TEST: Parse simple INI string + PASSED +TEST: Set and get values + PASSED +TEST: Check section and key existence + PASSED +TEST: Remove section and key + PASSED +TEST: Generate INI string + PASSED +TEST: File read/write + PASSED +TEST: Handle comments and whitespace + PASSED + +=== Test Summary === +Passed: 8 +Failed: 0 +All tests PASSED! +``` + +## Test Coverage + +The application tests the following dmini functionality: + +1. **Context Management** + - `dmini_create()` - Create INI context + - `dmini_destroy()` - Free INI context + +2. **Parsing** + - `dmini_parse_string()` - Parse from string + - `dmini_parse_file()` - Parse from file (line-by-line) + - Comment handling (`;` and `#`) + - Whitespace trimming + +3. **Data Access** + - `dmini_get_string()` - Get string values with defaults + - `dmini_get_int()` - Get integer values with defaults + - `dmini_set_string()` - Set string values + - `dmini_set_int()` - Set integer values + +4. **Queries** + - `dmini_has_section()` - Check section existence + - `dmini_has_key()` - Check key existence + +5. **Removal** + - `dmini_remove_section()` - Remove entire section + - `dmini_remove_key()` - Remove single key + +6. **Generation** + - `dmini_generate_string()` - Generate to buffer (size query supported) + - `dmini_generate_file()` - Write directly to file + +7. **Edge Cases** + - Global section (keys without section header) + - Comments (`;` and `#`) + - Whitespace trimming + - Empty sections + +## Integration with CI + +The test application is automatically run as part of the CI pipeline: + +```yaml +- name: Run tests with dmod_loader + run: | + dmod_loader build/dmf/dmini.dmf build/dmf/test_dmini.dmf +``` + +## Exit Codes + +- `0` - All tests passed +- `1` - One or more tests failed + +## Memory Footprint + +- RAM: 432 bytes +- ROM: 7 KB + +## Building + +The test application is built automatically when building the dmini project: + +```bash +mkdir build +cd build +cmake .. -DDMOD_MODE=DMOD_MODULE +cmake --build . +``` + +This generates `dmf/test_dmini.dmf` which can be loaded with dmod_loader. diff --git a/apps/test_dmini/test_dmini.c b/apps/test_dmini/test_dmini.c new file mode 100644 index 0000000..a5c52a8 --- /dev/null +++ b/apps/test_dmini/test_dmini.c @@ -0,0 +1,339 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmini.h" +#include + +/** + * @brief Test application for dmini module + * + * This application tests the dmini INI parser module functionality. + * It can be loaded with dmod_loader along with the dmini module. + */ + +// Test counter +static int tests_passed = 0; +static int tests_failed = 0; + +// Helper macros +#define TEST_START(name) Dmod_Printf("TEST: %s\n", name) +#define TEST_ASSERT(cond, msg) do { \ + if (!(cond)) { \ + DMOD_LOG_ERROR(" FAILED: %s\n", msg); \ + tests_failed++; \ + return; \ + } \ +} while(0) +#define TEST_PASS() do { \ + Dmod_Printf(" PASSED\n"); \ + tests_passed++; \ +} while(0) + +/** + * @brief Test: Create and destroy context + */ +static void test_create_destroy(void) +{ + TEST_START("Create and destroy context"); + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Test: Parse simple INI string + */ +static void test_parse_string(void) +{ + TEST_START("Parse simple INI string"); + + const char* ini_data = + "global_key=global_value\n" + "\n" + "[section1]\n" + "key1=value1\n" + "key2=value2\n" + "\n" + "[section2]\n" + "number=42\n"; + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + int result = dmini_parse_string(ctx, ini_data); + TEST_ASSERT(result == 0, "Failed to parse string"); + + // Check global section + const char* val = dmini_get_string(ctx, NULL, "global_key", ""); + TEST_ASSERT(strcmp(val, "global_value") == 0, "Wrong global value"); + + // Check section1 + val = dmini_get_string(ctx, "section1", "key1", ""); + TEST_ASSERT(strcmp(val, "value1") == 0, "Wrong section1/key1 value"); + + val = dmini_get_string(ctx, "section1", "key2", ""); + TEST_ASSERT(strcmp(val, "value2") == 0, "Wrong section1/key2 value"); + + // Check section2 + int num = dmini_get_int(ctx, "section2", "number", 0); + TEST_ASSERT(num == 42, "Wrong integer value"); + + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Test: Set and get values + */ +static void test_set_get(void) +{ + TEST_START("Set and get values"); + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + // Set string + int result = dmini_set_string(ctx, "database", "host", "localhost"); + TEST_ASSERT(result == 0, "Failed to set string"); + + const char* val = dmini_get_string(ctx, "database", "host", ""); + TEST_ASSERT(strcmp(val, "localhost") == 0, "Wrong retrieved value"); + + // Set integer + result = dmini_set_int(ctx, "database", "port", 5432); + TEST_ASSERT(result == 0, "Failed to set integer"); + + int num = dmini_get_int(ctx, "database", "port", 0); + TEST_ASSERT(num == 5432, "Wrong integer value"); + + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Test: Has section and key + */ +static void test_has_section_key(void) +{ + TEST_START("Check section and key existence"); + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + dmini_set_string(ctx, "section1", "key1", "value1"); + + TEST_ASSERT(dmini_has_section(ctx, "section1") == 1, "Section should exist"); + TEST_ASSERT(dmini_has_section(ctx, "section2") == 0, "Section should not exist"); + + TEST_ASSERT(dmini_has_key(ctx, "section1", "key1") == 1, "Key should exist"); + TEST_ASSERT(dmini_has_key(ctx, "section1", "key2") == 0, "Key should not exist"); + + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Test: Remove section and key + */ +static void test_remove(void) +{ + TEST_START("Remove section and key"); + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + dmini_set_string(ctx, "section1", "key1", "value1"); + dmini_set_string(ctx, "section1", "key2", "value2"); + + // Remove key + int result = dmini_remove_key(ctx, "section1", "key1"); + TEST_ASSERT(result == 0, "Failed to remove key"); + TEST_ASSERT(dmini_has_key(ctx, "section1", "key1") == 0, "Key should be removed"); + TEST_ASSERT(dmini_has_key(ctx, "section1", "key2") == 1, "Key2 should still exist"); + + // Remove section + result = dmini_remove_section(ctx, "section1"); + TEST_ASSERT(result == 0, "Failed to remove section"); + TEST_ASSERT(dmini_has_section(ctx, "section1") == 0, "Section should be removed"); + + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Simple substring search (replacement for strstr) + */ +static const char* find_substring(const char* haystack, const char* needle) +{ + if (!haystack || !needle) return NULL; + if (*needle == '\0') return haystack; + + for (; *haystack; haystack++) { + const char* h = haystack; + const char* n = needle; + + while (*h && *n && (*h == *n)) { + h++; + n++; + } + + if (*n == '\0') { + return haystack; + } + } + + return NULL; +} + +/** + * @brief Test: Generate string + */ +static void test_generate_string(void) +{ + TEST_START("Generate INI string"); + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + dmini_set_string(ctx, NULL, "global", "value"); + dmini_set_string(ctx, "section1", "key1", "value1"); + + // Get required size + int size = dmini_generate_string(ctx, NULL, 0); + TEST_ASSERT(size > 0, "Failed to get required size"); + + // Allocate buffer + char* buffer = (char*)Dmod_Malloc(size); + TEST_ASSERT(buffer != NULL, "Failed to allocate buffer"); + + // Generate string + int result = dmini_generate_string(ctx, buffer, size); + TEST_ASSERT(result == size, "Wrong size returned"); + + // Check content + TEST_ASSERT(find_substring(buffer, "global=value") != NULL, "Missing global key"); + TEST_ASSERT(find_substring(buffer, "[section1]") != NULL, "Missing section header"); + TEST_ASSERT(find_substring(buffer, "key1=value1") != NULL, "Missing section key"); + + Dmod_Free(buffer); + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Test: File I/O + */ +static void test_file_io(void) +{ + TEST_START("File read/write"); + + const char* test_file = "/tmp/test_dmini.ini"; + const char* test_data = + "[section1]\n" + "key1=value1\n" + "\n" + "[section2]\n" + "key2=value2\n"; + + // Write test file + void* file = Dmod_FileOpen(test_file, "w"); + TEST_ASSERT(file != NULL, "Failed to create test file"); + Dmod_FileWrite(test_data, 1, strlen(test_data), file); + Dmod_FileClose(file); + + // Parse file + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + int result = dmini_parse_file(ctx, test_file); + TEST_ASSERT(result == 0, "Failed to parse file"); + + const char* val = dmini_get_string(ctx, "section1", "key1", ""); + TEST_ASSERT(strcmp(val, "value1") == 0, "Wrong value from file"); + + // Generate to different file + const char* output_file = "/tmp/test_dmini_output.ini"; + result = dmini_generate_file(ctx, output_file); + TEST_ASSERT(result == 0, "Failed to generate file"); + + // Verify output file exists + file = Dmod_FileOpen(output_file, "r"); + TEST_ASSERT(file != NULL, "Output file not created"); + Dmod_FileClose(file); + + // Clean up + Dmod_FileRemove(test_file); + Dmod_FileRemove(output_file); + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Test: Comments and whitespace + */ +static void test_comments_whitespace(void) +{ + TEST_START("Handle comments and whitespace"); + + const char* ini_data = + "; Comment line\n" + " key1 = value1 \n" + "# Another comment\n" + "\n" + "[ section1 ]\n" + " key2 = value2 \n"; + + dmini_context_t ctx = dmini_create(); + TEST_ASSERT(ctx != NULL, "Failed to create context"); + + int result = dmini_parse_string(ctx, ini_data); + TEST_ASSERT(result == 0, "Failed to parse string with comments"); + + const char* val = dmini_get_string(ctx, NULL, "key1", ""); + TEST_ASSERT(strcmp(val, "value1") == 0, "Whitespace not trimmed"); + + val = dmini_get_string(ctx, "section1", "key2", ""); + TEST_ASSERT(strcmp(val, "value2") == 0, "Section whitespace not trimmed"); + + dmini_destroy(ctx); + TEST_PASS(); +} + +/** + * @brief Main entry point for test application + */ +int main(int argc, char** argv) +{ + DMOD_LOG_INFO("=== DMINI Functionality Tests ===\n\n"); + + tests_passed = 0; + tests_failed = 0; + + // Run all tests + test_create_destroy(); + test_parse_string(); + test_set_get(); + test_has_section_key(); + test_remove(); + test_generate_string(); + test_file_io(); + test_comments_whitespace(); + + // Print summary + Dmod_Printf("\n=== Test Summary ===\n"); + Dmod_Printf("Passed: %d\n", tests_passed); + Dmod_Printf("Failed: %d\n", tests_failed); + + if (tests_failed == 0) + { + Dmod_Printf("All tests PASSED!\n"); + return 0; + } + else + { + Dmod_Printf("Some tests FAILED!\n"); + return 1; + } +} diff --git a/include/dmini.h b/include/dmini.h new file mode 100644 index 0000000..22e3f81 --- /dev/null +++ b/include/dmini.h @@ -0,0 +1,210 @@ +#ifndef DMINI_H +#define DMINI_H + +#include "dmod.h" +#include "dmini_defs.h" // Generated by CMake from DMOD's api.h.in template + +/** + * @brief DMINI - DMOD INI File Parser Module + * + * This module provides an API for parsing and generating INI files. + * It uses only SAL (System Abstraction Layer) functions from DMOD. + * + * INI file format: + * - Sections are defined by [section_name] + * - Key-value pairs are defined by key=value + * - Comments start with ; or # + * - Whitespace is trimmed from keys and values + */ + +/** + * @brief Error codes + */ +#define DMINI_OK 0 +#define DMINI_ERR_GENERAL -1 +#define DMINI_ERR_MEMORY -2 +#define DMINI_ERR_INVALID -3 +#define DMINI_ERR_NOT_FOUND -4 +#define DMINI_ERR_FILE -5 + +/** + * @brief INI context type (opaque) + * + * This is an opaque pointer to the INI file context structure. + */ +typedef struct dmini_context* dmini_context_t; + +/** + * @brief Initialize INI context + * + * Creates a new INI context for storing sections and key-value pairs. + * + * @return Pointer to INI context or NULL on error + */ +dmod_dmini_api(1.0, dmini_context_t, _create, (void)); + +/** + * @brief Free INI context + * + * Frees all memory associated with the INI context. + * + * @param ctx INI context to free + */ +dmod_dmini_api(1.0, void, _destroy, (dmini_context_t ctx)); + +/** + * @brief Parse INI file from string + * + * Parses an INI file from a null-terminated string. + * + * @param ctx INI context + * @param data String containing INI file contents + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _parse_string, (dmini_context_t ctx, const char* data)); + +/** + * @brief Parse INI file + * + * Parses an INI file from a file path using SAL file functions. + * + * @param ctx INI context + * @param filename Path to INI file + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _parse_file, (dmini_context_t ctx, const char* filename)); + +/** + * @brief Generate INI file to string + * + * Generates an INI file string from the context. + * If buffer is NULL, returns the required buffer size. + * If buffer is not NULL, fills it with the INI data. + * + * @param ctx INI context + * @param buffer Buffer to write to (NULL to query size) + * @param buffer_size Size of the buffer + * @return Required buffer size, or negative error code + */ +dmod_dmini_api(1.0, int, _generate_string, (dmini_context_t ctx, char* buffer, size_t buffer_size)); + +/** + * @brief Generate INI file + * + * Generates an INI file from the context and writes it to a file. + * + * @param ctx INI context + * @param filename Path to output INI file + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _generate_file, (dmini_context_t ctx, const char* filename)); + +/** + * @brief Get string value from INI context + * + * Retrieves a string value for the given section and key. + * + * @param ctx INI context + * @param section Section name (NULL for global section) + * @param key Key name + * @param default_value Default value if key not found + * @return Value string or default_value if not found + */ +dmod_dmini_api(1.0, const char*, _get_string, (dmini_context_t ctx, + const char* section, + const char* key, + const char* default_value)); + +/** + * @brief Get integer value from INI context + * + * Retrieves an integer value for the given section and key. + * + * @param ctx INI context + * @param section Section name (NULL for global section) + * @param key Key name + * @param default_value Default value if key not found + * @return Integer value or default_value if not found + */ +dmod_dmini_api(1.0, int, _get_int, (dmini_context_t ctx, + const char* section, + const char* key, + int default_value)); + +/** + * @brief Set string value in INI context + * + * Sets a string value for the given section and key. + * Creates the section if it doesn't exist. + * + * @param ctx INI context + * @param section Section name (NULL for global section) + * @param key Key name + * @param value Value string + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _set_string, (dmini_context_t ctx, + const char* section, + const char* key, + const char* value)); + +/** + * @brief Set integer value in INI context + * + * Sets an integer value for the given section and key. + * Creates the section if it doesn't exist. + * + * @param ctx INI context + * @param section Section name (NULL for global section) + * @param key Key name + * @param value Integer value + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _set_int, (dmini_context_t ctx, + const char* section, + const char* key, + int value)); + +/** + * @brief Check if section exists + * + * @param ctx INI context + * @param section Section name + * @return 1 if section exists, 0 otherwise + */ +dmod_dmini_api(1.0, int, _has_section, (dmini_context_t ctx, const char* section)); + +/** + * @brief Check if key exists in section + * + * @param ctx INI context + * @param section Section name (NULL for global section) + * @param key Key name + * @return 1 if key exists, 0 otherwise + */ +dmod_dmini_api(1.0, int, _has_key, (dmini_context_t ctx, + const char* section, + const char* key)); + +/** + * @brief Remove section from INI context + * + * @param ctx INI context + * @param section Section name + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _remove_section, (dmini_context_t ctx, const char* section)); + +/** + * @brief Remove key from section + * + * @param ctx INI context + * @param section Section name (NULL for global section) + * @param key Key name + * @return DMINI_OK on success, error code on failure + */ +dmod_dmini_api(1.0, int, _remove_key, (dmini_context_t ctx, + const char* section, + const char* key)); + +#endif // DMINI_H diff --git a/manifest.dmm b/manifest.dmm new file mode 100644 index 0000000..3774270 --- /dev/null +++ b/manifest.dmm @@ -0,0 +1,8 @@ +# Include dynamically generated versions list from latest release +$include https://github.com/choco-technologies/dmini/releases/download/vlatest/versions.dmm + +# Module entries with version placeholder - will be expanded by $version-available +dmini https://github.com/choco-technologies/dmini/releases/download/v/dmini-v-.zip + +# Test application +test_dmini https://github.com/choco-technologies/dmini/releases/download/v/dmini-v-.zip diff --git a/src/dmini.c b/src/dmini.c new file mode 100644 index 0000000..4d35a0e --- /dev/null +++ b/src/dmini.c @@ -0,0 +1,1002 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmini.h" +#include + +/** + * @brief Key-value pair structure + */ +typedef struct dmini_pair +{ + char* key; + char* value; + struct dmini_pair* next; +} dmini_pair_t; + +/** + * @brief Section structure + */ +typedef struct dmini_section +{ + char* name; + dmini_pair_t* pairs; + struct dmini_section* next; +} dmini_section_t; + +/** + * @brief INI context structure + */ +struct dmini_context +{ + dmini_section_t* sections; +}; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * @brief Trim whitespace from beginning and end of string + * + * @param str String to trim (modified in place) + * @return Pointer to trimmed string + */ +static char* trim_whitespace(char* str) +{ + char* end; + + // Trim leading space + while (*str == ' ' || *str == '\t' || *str == '\r' || *str == '\n') + { + str++; + } + + if (*str == 0) + { + return str; + } + + // Trim trailing space + end = str + strlen(str) - 1; + while (end > str && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) + { + end--; + } + + // Write new null terminator + *(end + 1) = '\0'; + + return str; +} + + + +/** + * @brief Compare section names (handles NULL values) + */ +static int section_names_equal(const char* name1, const char* name2) +{ + if (name1 == NULL || name2 == NULL) + { + return name1 == name2; + } + return strcmp(name1, name2) == 0; +} + +/** + * @brief Find section by name + */ +static dmini_section_t* find_section(dmini_context_t ctx, const char* section_name) +{ + if (!ctx) + { + return NULL; + } + + dmini_section_t* section = ctx->sections; + while (section) + { + if (section_names_equal(section->name, section_name)) + { + return section; + } + section = section->next; + } + + return NULL; +} + +/** + * @brief Find key-value pair in section + */ +static dmini_pair_t* find_pair(dmini_section_t* section, const char* key) +{ + if (!section || !key) + { + return NULL; + } + + dmini_pair_t* pair = section->pairs; + while (pair) + { + if (strcmp(pair->key, key) == 0) + { + return pair; + } + pair = pair->next; + } + + return NULL; +} + +/** + * @brief Create new section + */ +static dmini_section_t* create_section(const char* name) +{ + dmini_section_t* section = (dmini_section_t*)Dmod_Malloc(sizeof(dmini_section_t)); + if (!section) + { + return NULL; + } + + if (name) + { + section->name = Dmod_StrDup(name); + if (!section->name) + { + Dmod_Free(section); + return NULL; + } + } + else + { + section->name = NULL; + } + + section->pairs = NULL; + section->next = NULL; + + return section; +} + +/** + * @brief Create new key-value pair + */ +static dmini_pair_t* create_pair(const char* key, const char* value) +{ + dmini_pair_t* pair = (dmini_pair_t*)Dmod_Malloc(sizeof(dmini_pair_t)); + if (!pair) + { + return NULL; + } + + pair->key = Dmod_StrDup(key); + pair->value = Dmod_StrDup(value); + pair->next = NULL; + + if (!pair->key || !pair->value) + { + if (pair->key) Dmod_Free(pair->key); + if (pair->value) Dmod_Free(pair->value); + Dmod_Free(pair); + return NULL; + } + + return pair; +} + +/** + * @brief Free key-value pair + */ +static void free_pair(dmini_pair_t* pair) +{ + if (!pair) + { + return; + } + + if (pair->key) + { + Dmod_Free(pair->key); + } + if (pair->value) + { + Dmod_Free(pair->value); + } + Dmod_Free(pair); +} + +/** + * @brief Free section and all its pairs + */ +static void free_section(dmini_section_t* section) +{ + if (!section) + { + return; + } + + // Free all pairs + dmini_pair_t* pair = section->pairs; + while (pair) + { + dmini_pair_t* next = pair->next; + free_pair(pair); + pair = next; + } + + // Free section name + if (section->name) + { + Dmod_Free(section->name); + } + + Dmod_Free(section); +} + +/** + * @brief Get or create section + */ +static dmini_section_t* get_or_create_section(dmini_context_t ctx, const char* section_name) +{ + if (!ctx) + { + return NULL; + } + + // Try to find existing section + dmini_section_t* section = find_section(ctx, section_name); + if (section) + { + return section; + } + + // Create new section + section = create_section(section_name); + if (!section) + { + return NULL; + } + + // Add to list + if (!ctx->sections) + { + ctx->sections = section; + } + else + { + dmini_section_t* last = ctx->sections; + while (last->next) + { + last = last->next; + } + last->next = section; + } + + return section; +} + +/** + * @brief Set key-value pair in section + */ +static int set_pair_in_section(dmini_section_t* section, const char* key, const char* value) +{ + if (!section || !key) + { + return DMINI_ERR_INVALID; + } + + // Try to find existing pair + dmini_pair_t* pair = find_pair(section, key); + if (pair) + { + // Update value + if (pair->value) + { + Dmod_Free(pair->value); + } + pair->value = Dmod_StrDup(value); + if (!pair->value) + { + return DMINI_ERR_MEMORY; + } + return DMINI_OK; + } + + // Create new pair + pair = create_pair(key, value); + if (!pair) + { + return DMINI_ERR_MEMORY; + } + + // Add to list + if (!section->pairs) + { + section->pairs = pair; + } + else + { + dmini_pair_t* last = section->pairs; + while (last->next) + { + last = last->next; + } + last->next = pair; + } + + return DMINI_OK; +} + +// ============================================================================ +// Module Interface Implementation +// ============================================================================ + +/** + * @brief Module initialization (optional) + */ +void dmod_preinit(void) +{ + // Nothing to do +} + +/** + * @brief Module initialization + */ +int dmod_init(const Dmod_Config_t *Config) +{ + // Nothing to do + return 0; +} + +/** + * @brief Module deinitialization + */ +void dmod_deinit(void) +{ + // Nothing to do +} + +// ============================================================================ +// Public API Implementation +// ============================================================================ + +dmini_context_t dmini_create(void) +{ + dmini_context_t ctx = (dmini_context_t)Dmod_Malloc(sizeof(struct dmini_context)); + if (!ctx) + { + return NULL; + } + + ctx->sections = NULL; + + // Create global section (unnamed section for keys without section) + ctx->sections = create_section(NULL); + if (!ctx->sections) + { + Dmod_Free(ctx); + return NULL; + } + + return ctx; +} + +void dmini_destroy(dmini_context_t ctx) +{ + if (!ctx) + { + return; + } + + // Free all sections + dmini_section_t* section = ctx->sections; + while (section) + { + dmini_section_t* next = section->next; + free_section(section); + section = next; + } + + Dmod_Free(ctx); +} + +int dmini_parse_string(dmini_context_t ctx, const char* data) +{ + if (!ctx || !data) + { + return DMINI_ERR_INVALID; + } + + // Duplicate data so we can modify it + char* buffer = Dmod_StrDup(data); + if (!buffer) + { + return DMINI_ERR_MEMORY; + } + + dmini_section_t* current_section = ctx->sections; // Start with global section + + char* line = buffer; + char* next_line; + + while (line && *line) + { + // Find end of line + next_line = line; + while (*next_line && *next_line != '\n' && *next_line != '\r') + { + next_line++; + } + + // Null terminate current line + if (*next_line) + { + *next_line = '\0'; + next_line++; + // Skip \r\n combinations + if (*next_line == '\n' || *next_line == '\r') + { + next_line++; + } + } + + // Trim whitespace + line = trim_whitespace(line); + + // Skip empty lines and comments + if (*line == '\0' || *line == ';' || *line == '#') + { + line = next_line; + continue; + } + + // Check for section header + if (*line == '[') + { + char* section_end = line + 1; + while (*section_end && *section_end != ']') + { + section_end++; + } + + if (*section_end == ']') + { + *section_end = '\0'; + char* section_name = trim_whitespace(line + 1); + + current_section = get_or_create_section(ctx, section_name); + if (!current_section) + { + Dmod_Free(buffer); + return DMINI_ERR_MEMORY; + } + } + + line = next_line; + continue; + } + + // Parse key=value + char* equals = line; + while (*equals && *equals != '=') + { + equals++; + } + + if (*equals == '=') + { + *equals = '\0'; + char* key = trim_whitespace(line); + char* value = trim_whitespace(equals + 1); + + if (*key) + { + int result = set_pair_in_section(current_section, key, value); + if (result != DMINI_OK) + { + Dmod_Free(buffer); + return result; + } + } + } + + line = next_line; + } + + Dmod_Free(buffer); + return DMINI_OK; +} + +int dmini_parse_file(dmini_context_t ctx, const char* filename) +{ + if (!ctx || !filename) + { + return DMINI_ERR_INVALID; + } + + // Open file using SAL + void* file = Dmod_FileOpen(filename, "r"); + if (!file) + { + return DMINI_ERR_FILE; + } + + dmini_section_t* current_section = ctx->sections; // Start with global section + if (!current_section) + { + Dmod_FileClose(file); + return DMINI_ERR_INVALID; + } + + char line_buffer[256]; + + // Read file line by line + while (Dmod_FileReadLine(line_buffer, sizeof(line_buffer), file) != NULL) + { + // Trim whitespace + char* line = trim_whitespace(line_buffer); + + // Skip empty lines and comments + if (*line == '\0' || *line == ';' || *line == '#') + { + continue; + } + + // Check for section header + if (*line == '[') + { + char* section_end = line + 1; + while (*section_end && *section_end != ']') + { + section_end++; + } + + if (*section_end == ']') + { + *section_end = '\0'; + char* section_name = trim_whitespace(line + 1); + + current_section = get_or_create_section(ctx, section_name); + if (!current_section) + { + Dmod_FileClose(file); + return DMINI_ERR_MEMORY; + } + } + + continue; + } + + // Parse key=value + char* equals = line; + while (*equals && *equals != '=') + { + equals++; + } + + if (*equals == '=') + { + *equals = '\0'; + char* key = trim_whitespace(line); + char* value = trim_whitespace(equals + 1); + + if (*key) + { + int result = set_pair_in_section(current_section, key, value); + if (result != DMINI_OK) + { + Dmod_FileClose(file); + return result; + } + } + } + } + + Dmod_FileClose(file); + return DMINI_OK; +} + +int dmini_generate_string(dmini_context_t ctx, char* buffer, size_t buffer_size) +{ + if (!ctx) + { + return DMINI_ERR_INVALID; + } + + // Calculate required buffer size + size_t required_size = 0; + + dmini_section_t* section = ctx->sections; + while (section) + { + // Section header (skip global section) + if (section->name) + { + required_size += strlen(section->name) + 3; // [name]\n + } + + // Key-value pairs + dmini_pair_t* pair = section->pairs; + while (pair) + { + // Validate pair data before calculating size + if (!pair->key || !pair->value) + { + return DMINI_ERR_INVALID; + } + required_size += strlen(pair->key) + strlen(pair->value) + 2; // key=value\n + pair = pair->next; + } + + // Empty line after section + if (section->name && section->next) + { + required_size += 1; + } + + section = section->next; + } + + // Add 1 for null terminator + required_size += 1; + + // If buffer is NULL, just return the required size + if (!buffer) + { + return (int)required_size; + } + + // Check if buffer is large enough + if (buffer_size < required_size) + { + return DMINI_ERR_MEMORY; + } + + // Generate INI string + char* pos = buffer; + section = ctx->sections; + + while (section) + { + // Section header (skip global section) + if (section->name) + { + *pos++ = '['; + size_t name_len = strlen(section->name); + memcpy(pos, section->name, name_len); + pos += name_len; + *pos++ = ']'; + *pos++ = '\n'; + } + + // Key-value pairs + dmini_pair_t* pair = section->pairs; + while (pair) + { + size_t key_len = strlen(pair->key); + memcpy(pos, pair->key, key_len); + pos += key_len; + *pos++ = '='; + size_t value_len = strlen(pair->value); + memcpy(pos, pair->value, value_len); + pos += value_len; + *pos++ = '\n'; + pair = pair->next; + } + + // Empty line after section + if (section->name && section->next) + { + *pos++ = '\n'; + } + + section = section->next; + } + + *pos = '\0'; + + return (int)required_size; +} + +int dmini_generate_file(dmini_context_t ctx, const char* filename) +{ + if (!ctx || !filename) + { + return DMINI_ERR_INVALID; + } + + // Open file for writing + void* file = Dmod_FileOpen(filename, "w"); + if (!file) + { + return DMINI_ERR_FILE; + } + + // Write sections directly to file + dmini_section_t* section = ctx->sections; + char line_buffer[256]; + + while (section) + { + // Section header (skip global section) + if (section->name) + { + int len = Dmod_SnPrintf(line_buffer, sizeof(line_buffer), "[%s]\n", section->name); + if (len > 0 && len < (int)sizeof(line_buffer)) + { + size_t written = Dmod_FileWrite(line_buffer, 1, len, file); + if (written != (size_t)len) + { + Dmod_FileClose(file); + return DMINI_ERR_FILE; + } + } + } + + // Key-value pairs + dmini_pair_t* pair = section->pairs; + while (pair) + { + if (!pair->key || !pair->value) + { + Dmod_FileClose(file); + return DMINI_ERR_INVALID; + } + + int len = Dmod_SnPrintf(line_buffer, sizeof(line_buffer), "%s=%s\n", + pair->key, pair->value); + if (len > 0 && len < (int)sizeof(line_buffer)) + { + size_t written = Dmod_FileWrite(line_buffer, 1, len, file); + if (written != (size_t)len) + { + Dmod_FileClose(file); + return DMINI_ERR_FILE; + } + } + else if (len >= (int)sizeof(line_buffer)) + { + // Line too long - truncated + Dmod_FileClose(file); + return DMINI_ERR_INVALID; + } + + pair = pair->next; + } + + // Empty line after section + if (section->name && section->next) + { + size_t written = Dmod_FileWrite("\n", 1, 1, file); + if (written != 1) + { + Dmod_FileClose(file); + return DMINI_ERR_FILE; + } + } + + section = section->next; + } + + Dmod_FileClose(file); + return DMINI_OK; +} + +const char* dmini_get_string(dmini_context_t ctx, const char* section, const char* key, const char* default_value) +{ + if (!ctx || !key) + { + return default_value; + } + + dmini_section_t* sec = find_section(ctx, section); + if (!sec) + { + return default_value; + } + + dmini_pair_t* pair = find_pair(sec, key); + if (!pair) + { + return default_value; + } + + return pair->value; +} + +int dmini_get_int(dmini_context_t ctx, const char* section, const char* key, int default_value) +{ + const char* value = dmini_get_string(ctx, section, key, NULL); + if (!value) + { + return default_value; + } + + // Simple integer conversion + int result = 0; + int sign = 1; + const char* p = value; + + // Skip whitespace + while (*p == ' ' || *p == '\t') + { + p++; + } + + // Check for sign + if (*p == '-') + { + sign = -1; + p++; + } + else if (*p == '+') + { + p++; + } + + // Convert digits + while (*p >= '0' && *p <= '9') + { + result = result * 10 + (*p - '0'); + p++; + } + + return result * sign; +} + +int dmini_set_string(dmini_context_t ctx, const char* section, const char* key, const char* value) +{ + if (!ctx || !key || !value) + { + return DMINI_ERR_INVALID; + } + + dmini_section_t* sec = get_or_create_section(ctx, section); + if (!sec) + { + return DMINI_ERR_MEMORY; + } + + return set_pair_in_section(sec, key, value); +} + +int dmini_set_int(dmini_context_t ctx, const char* section, const char* key, int value) +{ + // Convert integer to string + char buffer[32]; + char* p = buffer + sizeof(buffer) - 1; + *p = '\0'; + + int is_negative = 0; + unsigned int abs_value; + + if (value < 0) + { + is_negative = 1; + // Handle INT_MIN safely by casting to unsigned + abs_value = (unsigned int)(-(value + 1)) + 1; + } + else + { + abs_value = (unsigned int)value; + } + + // Convert digits + do + { + *--p = '0' + (abs_value % 10); + abs_value /= 10; + } while (abs_value > 0); + + // Add sign + if (is_negative) + { + *--p = '-'; + } + + return dmini_set_string(ctx, section, key, p); +} + +int dmini_has_section(dmini_context_t ctx, const char* section) +{ + if (!ctx) + { + return 0; + } + + return find_section(ctx, section) ? 1 : 0; +} + +int dmini_has_key(dmini_context_t ctx, const char* section, const char* key) +{ + if (!ctx || !key) + { + return 0; + } + + dmini_section_t* sec = find_section(ctx, section); + if (!sec) + { + return 0; + } + + return find_pair(sec, key) ? 1 : 0; +} + +int dmini_remove_section(dmini_context_t ctx, const char* section) +{ + if (!ctx || !section) + { + return DMINI_ERR_INVALID; + } + + dmini_section_t* prev = NULL; + dmini_section_t* curr = ctx->sections; + + while (curr) + { + if (curr->name && strcmp(curr->name, section) == 0) + { + // Remove from list + if (prev) + { + prev->next = curr->next; + } + else + { + ctx->sections = curr->next; + } + + free_section(curr); + return DMINI_OK; + } + + prev = curr; + curr = curr->next; + } + + return DMINI_ERR_NOT_FOUND; +} + +int dmini_remove_key(dmini_context_t ctx, const char* section, const char* key) +{ + if (!ctx || !key) + { + return DMINI_ERR_INVALID; + } + + dmini_section_t* sec = find_section(ctx, section); + if (!sec) + { + return DMINI_ERR_NOT_FOUND; + } + + dmini_pair_t* prev = NULL; + dmini_pair_t* curr = sec->pairs; + + while (curr) + { + if (strcmp(curr->key, key) == 0) + { + // Remove from list + if (prev) + { + prev->next = curr->next; + } + else + { + sec->pairs = curr->next; + } + + free_pair(curr); + return DMINI_OK; + } + + prev = curr; + curr = curr->next; + } + + return DMINI_ERR_NOT_FOUND; +}