A simple .ini file interface.

Dependents:   Smart-WiFly-WebServer SignalGenerator WattEye X10Svr

IniManager.cpp

Committer:
WiredHome
Date:
2020-06-11
Revision:
28:4e7fc08a0fea
Parent:
27:611ffb74fd38
Parent:
26:e8d7b09a77a3

File content as of revision 28:4e7fc08a0fea:

// Simple INI file manager.
//
#ifdef WIN32
#include "string.h"
#include "stdlib.h"
#include "stdio.h"
#else
#include "mbed.h"
#endif

#include "IniManager.h"

//#include "Utility.h"            // private memory manager
#ifndef UTILITY_H
#define swMalloc malloc         // use the standard
#define swFree free
#endif

//#define DEBUG "INI "      //Debug is disabled by default

#include <cstdio>
#if (defined(DEBUG) && !defined(TARGET_LPC11U24))
#define DBG(x, ...)  std::printf("[DBG %s %4d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
#define WARN(x, ...) std::printf("[WRN %s %4d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
#define ERR(x, ...)  std::printf("[ERR %s %4d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
#define INFO(x, ...) std::printf("[INF %s %4d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
#else
#define DBG(x, ...)
#define WARN(x, ...)
#define ERR(x, ...)
#define INFO(x, ...)
#endif

// 2 versions, to translate new return values to old format
// return RetXLate[new value][version]
INI::INI_Return RetXLate[INI::INI_INTERNAL_ERROR+1][2] = {
//  Ver1,                   Ver2 return values.
    INI::INI_V1_SUCCESS,    INI::INI_SUCCESS,            /// Success - operation succeeded
    INI::INI_V1_FAIL,       INI::INI_NO_FILE_SPEC,       /// Fail - no file was specified
    INI::INI_V1_FAIL,       INI::INI_FILE_NOT_FOUND,     /// Fail - ini file not found, or failed to open
    INI::INI_V1_FAIL,       INI::INI_SECTION_NOT_FOUND,  /// Fail - section not found
    INI::INI_V1_FAIL,       INI::INI_KEY_NOT_FOUND,      /// Fail - key not found
    INI::INI_V1_FAIL,       INI::INI_BUF_TOO_SMALL,      /// Fail - buffer to small for value
    INI::INI_V1_FAIL,       INI::INI_INTERNAL_ERROR      /// Fail - internal error - can't alloc buffers
};

INI::INI(const char * file, int Version)
    : iniFile(0)
{
    SetFile(file);
    version = (Version == 2) ? 1 : 0;   // Version 1 or 2 is return value index 0 or 1
}


INI::~INI(void)
{
    if (iniFile)
        swFree(iniFile);
}


bool INI::GetNextSection(const char * after, char * buffer, size_t bufferSize) {
    bool returnNext = false;
    bool found = false;
    
    if (!iniFile)
        return found;
    CleanUp();
    INFO("GetNextSection after [%s]", after);
    FILE * fp = fopen(iniFile,"rt");
    if (fp) {
        char buf[INTERNAL_BUF_SIZE];

        if (after == NULL || *after == '\0')
            returnNext = true;
        while(fgets(buf, sizeof(buf), fp)) {
            int x = strlen(buf) - 1;        // remove trailing \r\n combinations
            while (x >= 0 && buf[x] < ' ')
                buf[x--] = '\0';
            INFO("  reading \"%s\"", buf);
            if (buf[0] == '[') {
                char * pStart = buf + 1;
                char * pRBrkt = strchr(buf, ']');
                if (pRBrkt) {
                    *pRBrkt = '\0';
                    if (returnNext) {
                        // Guard against "[]" which would cause it to recycle from the start
                        if (strlen(pStart) > 0 && strlen(pStart) < bufferSize) {
                            strcpy(buffer, pStart);
                            found = true;
                            break;
                        }
                    } else if (strcmp(after, pStart) == 0) {
                        returnNext = true;
                    }
                }
            }
        }
        fclose(fp);
    }
    return found;
}


bool INI::GetNextKey(const char * Section, const char * after, char * buffer, size_t bufferSize) {
    bool returnNext = false;
    bool inSection = false;
    bool found = false;
    
    if (!iniFile)
        return found;
    CleanUp();
    INFO("GetNextKey after [%s]", after);
    FILE * fp = fopen(iniFile,"rt");
    if (fp) {
        char buf[INTERNAL_BUF_SIZE];

        if (after == NULL || *after == '\0')
            returnNext = true;
        while(fgets(buf, sizeof(buf), fp)) {
            int x = strlen(buf) - 1;        // remove trailing \r\n combinations
            while (x >= 0 && buf[x] < ' ')
                buf[x--] = '\0';
            INFO("  reading \"%s\"", buf);
            if (!(buf[0] == '[' || (buf[0] >= 'A' && buf[0] <= 'Z') || (buf[0] >= 'a' && buf[0] <= 'z')))
                continue;
            if (buf[0] == '[') {
                char * pStart = buf + 1;
                char * pRBrkt = strchr(buf, ']');
                if (pRBrkt) {
                    *pRBrkt = '\0';
                    if (inSection == true) {        // section after wanted, so done.
                        break;
                    } else if (strcmp(pStart, Section) == 0) {
                        inSection = true;
                        continue;
                    }
                }
            } else if (inSection) {
                char * pStart = buf;
                char * pEqual = strchr(pStart, '=');
                if (pEqual) {
                    *pEqual = '\0';
                    if (returnNext) {
                        if (strlen(pStart) < bufferSize) {
                            strcpy(buffer, pStart);
                            found = true;
                            break;
                        }
                    } else if (strcmp(after, pStart) == 0) {
                        returnNext = true;
                    }
                }
            }
        }
        fclose(fp);
    }
    return found;
}


bool INI::Exists(const char * file)
{
    if (file == NULL)
        file = iniFile;
    INFO("Exists(%s)", file);
    FILE * fp = fopen(file, "r");
    if (fp) {
        fclose(fp);
        INFO("  [%s] exists", file);
        return true;
    } else {
        INFO("  [%s] does not exist", file);
        return false;
    }
}


bool INI::SetFile(const char * file, int Version)
{
    INFO("SetFile(%s,%d)", file, Version);
    version = (Version == 2) ? 1 : 0;   // Version 1 or 2 is return value index 0 or 1
    if (file) {
        if (iniFile)
            swFree(iniFile);
        iniFile = (char *)swMalloc(strlen(file)+1);
        if (iniFile) {
            strcpy(iniFile, file);
            INFO("  SetFile(%s) success", iniFile);
            return true;
        }
        else {
            iniFile = NULL;
            ERR("  SetFile(%s) failed to allocate memory", file);
        }
    }
    return false;
}

INI::INI_Return INI::ReadString(const char * section, const char * key, char * buffer, size_t bufferSize, const char * defaultString)
{
    INI_Return retVal;
    bool found = false;
    
    retVal = RetXLate[INI_SECTION_NOT_FOUND][version];     // assume we won't find the section, until we do.
    if (!iniFile)
        return RetXLate[INI_NO_FILE_SPEC][version];
    CleanUp();
    INFO("ReadString from %s", iniFile);
    FILE * fp = fopen(iniFile,"rt");
    if (!fp) {
        if (defaultString == NULL) {
            return RetXLate[INI_FILE_NOT_FOUND][version];
        }
    } else {
        char buf[INTERNAL_BUF_SIZE];
        bool inSection = (section == NULL) ? true : false;
        while(fgets(buf, sizeof(buf), fp)) {
            int x = strlen(buf) - 1;        // remove trailing \r\n combinations
            while (x >= 0 && buf[x] < ' ')
                buf[x--] = '\0';
            INFO("  reading \"%s\"", buf);
            if (!(buf[0] == '[' || (buf[0] >= 'A' && buf[0] <= 'Z') || (buf[0] >= 'a' && buf[0] <= 'z')))
                continue;
            
            if (inSection && buf[0] != '[') {
                char * eq = strchr(buf, '=');
                if (eq) {
                    *eq++ = '\0';
                    if (strcmp(buf,key) == 0) {        // Found the key of interest
                        if (strlen(eq) < bufferSize) {
                            strcpy(buffer, eq);
                            memset(buf, 0, INTERNAL_BUF_SIZE);  // secure the memory space
                            found = true;
                            retVal = RetXLate[INI_SUCCESS][version];
                        } else {
                            retVal = RetXLate[INI_BUF_TOO_SMALL][version];
                        }
                        break;
                    }
                }
            } else {
                if (buf[0] == '[') {
                    char * br = strchr(buf, ']');
                    if (inSection) {        // we were in the section of interest and just hit the next section...
                        break;
                    } else {
                        inSection = false;
                        if (br) {
                            *br = '\0';
                            if (strcmp(buf+1, section) == 0) {
                                inSection = true;
                                retVal = RetXLate[INI_KEY_NOT_FOUND][version];     // assume we won't find the key, until we do
                            }
                        }
                    }
                }
            }
        }
        fclose(fp);
    }
    if (!found && defaultString != NULL && *defaultString) {
        if (strlen(defaultString) < bufferSize) {
            strcpy(buffer, defaultString);
            retVal = RetXLate[INI_SUCCESS][version];
        } else {
            retVal = RetXLate[INI_BUF_TOO_SMALL][version];
        }
    }
    return retVal;
}


long int INI::ReadLongInt(const char * section, const char * key, long int defaultValue)
{
    char localBuf[16];
    
    if (INI::INI_SUCCESS == ReadString(section, key, localBuf, sizeof(localBuf))) {
        return atol(localBuf);
    } else {
        return defaultValue;
    }
}

bool INI::CleanUp()
{
    char * newFile = (char *)swMalloc(strlen(iniFile)+1);
    char * bakFile = (char *)swMalloc(strlen(iniFile)+1);

    if (newFile && bakFile) {
        INFO("CleanUp");
        strcpy(bakFile, iniFile);
        strcpy(newFile, iniFile);
        strcpy(bakFile + strlen(bakFile) - 4, ".bak");
        strcpy(newFile + strlen(newFile) - 4, ".new");

        if (Exists(newFile)) {
            int i;
            i = i;    // suppress warning about i not used when !DEBUG
            // helps recover if the system crashed before it could swap in the new file
            INFO("  *** found %s, repairing ...", newFile);
            i = remove(bakFile);            // remove an old .bak
            INFO("  remove(%s) returned %d", bakFile, i);
            i = Rename(iniFile, bakFile);   // move the existing .ini to .bak
            INFO("  rename(%s,%s) returned %d", iniFile, bakFile, i);
            i = Rename(newFile, iniFile);   // move the new .new to .ini
            INFO("  rename(%s,%s) returned %d", newFile, iniFile, i);
        } else {
            // nothing to do, move on silently.
        }
    }
    swFree(newFile);
    swFree(bakFile);
    return true;
}

INI::INI_Return INI::WriteLongInt(const char * section, const char * key, long int value)
{
    char buf[20];
    snprintf(buf, 20, "%ld", value);
    return WriteString(section, key, buf);
}


// Create the new version as .new
// once complete, if something actually changed, then rename the .ini to .bak and rename the .new to .ini
// once complete, if nothing actually changed, then delete the .new
//
INI::INI_Return INI::WriteString(const char * section, const char * key, const char * value, int len)
{
    bool found = false;
    bool fileChanged = false;
    INI_Return retVal;
    
    if (len == -1)
        len = strlen(value);
    INFO("WriteString(%s,%s,%s)", section, key, value);
    if (!iniFile)
        return RetXLate[INI_NO_FILE_SPEC][version];
        
    if (strlen(value) > INTERNAL_BUF_SIZE)
        return RetXLate[INI_INTERNAL_ERROR][version];

    char * newFile = (char *)swMalloc(strlen(iniFile)+1);
    if (!newFile)
        return RetXLate[INI_INTERNAL_ERROR][version];       // no memory
    char * bakFile = (char *)swMalloc(strlen(iniFile)+1);
    if (!bakFile) {
        swFree(newFile);
        return RetXLate[INI_INTERNAL_ERROR][version];
    }
    char * valBuf = (char *)swMalloc(len+1);
    if (!valBuf) {
        swFree(bakFile);
        swFree(newFile);
        return RetXLate[INI_INTERNAL_ERROR][version];
    }

    strcpy(bakFile, iniFile);
    strcpy(newFile, iniFile);
    strcpy(bakFile + strlen(bakFile) - 4, ".bak");
    strcpy(newFile + strlen(newFile) - 4, ".new");
    strncpy(valBuf, value, len);
    valBuf[len] = '\0';
    CleanUp();

    INFO("  Opening [%s] and [%s]", iniFile, newFile);
    FILE * fi = fopen(iniFile, "rt");
    FILE * fo = fopen(newFile, "wt");
    if (fo) {
        char buf[INTERNAL_BUF_SIZE];
        bool inSection = (section == NULL) ? true : false;

        if (fi) {
            INFO("  %s opened for reading", iniFile);
            while(fgets(buf, sizeof(buf), fi)) {
                // if not inSection, copy across
                // if inSection and not key, copy across
                // if InSection and key, write new value (or skip if value is null)
                int x = strlen(buf) - 1;        // remove trailing \r\n combinations
                while (x >= 0 && buf[x] < ' ')
                    buf[x--] = '\0';
                
                if (inSection && buf[0] != '[') {
                    char * eq = strchr(buf, '=');
                    if (eq) {
                        *eq++ = '\0';
                        if (strcmp(buf,key) == 0) {
                            // delete, or replace the old record
                            if (valBuf != NULL && strcmp(eq, valBuf) != 0) {
                                // replace the old record
                                if (valBuf != NULL) {
                                    fprintf(fo, "%s=%s\r\n", key, valBuf);
                                    INFO("  write: %s=%s", key, valBuf);
                                }
                            }
                            retVal = RetXLate[INI_SUCCESS][version];
                            fileChanged = true;
                            inSection = false;
                            found = true;
                        } else {
                            // write old record
                            fprintf(fo, "%s=%s\r\n", buf, eq);
                            INFO("  write: %s=%s", buf, eq);
                        }
                    } else {
                        // what to do with unknown record(s)?
                        // fprintf(fo, "%s\n", buf);    // eliminate them
                    }
                } else {
                    if (buf[0] == '[') {
                        char * br = strchr(buf, ']');
                        if (inSection) { // found next section while in good section
                            // Append new record to desired section
                            if (valBuf != NULL) {
                                fprintf(fo, "%s=%s\r\n", key, valBuf);
                                INFO("  write: %s=%s", key, valBuf);
                                fileChanged = true;
                            }
                            found = true;
                            retVal = RetXLate[INI_SUCCESS][version];
                        }
                        inSection = false;
                        // write old record
                        fprintf(fo, "\r\n%s\r\n", buf);
                        INFO("  write: %s", buf);
                        if (br) {
                            *br = '\0';
                            if (strcmp(buf+1, section) == 0)
                                inSection = true;
                        }
                    } else {
                        // copy unaltered records across
                        if (buf[0]) {
                            fprintf(fo, "%s\r\n", buf);
                            INFO("  write: %s", buf);
                        }
                    }
                }
            }
            INFO("close %s", iniFile);
            fclose(fi);
        } else {
            INFO("  %s did not previously exist.", iniFile);
        }
        if (!found) {
            // No old file, just create it now
            if (valBuf != NULL) {
                if (!inSection) {
                    fprintf(fo, "\r\n[%s]\r\n", section);
                    INFO("  write: [%s]", section);
                }
                fprintf(fo, "%s=%s\r\n", key, valBuf);
                INFO("  write: %s=%s", key, valBuf);
                fileChanged = true;
            }
            found = true;
            retVal = RetXLate[INI_SUCCESS][version];
        }
        INFO("  close %s", newFile);
        fclose(fo);
    } else {
        ERR("*** Failed to open %s", newFile);
        retVal = RetXLate[INI_FILE_NOT_FOUND][version];
    }
    if (fileChanged) {
        INFO("  File changed: remove bak, rename ini to bak, rename new to ini");
        remove(bakFile);            // remove an old .bak
        INFO("  a");
        Rename(iniFile, bakFile);   // move the existing .ini to .bak
        INFO("  b");
        Rename(newFile, iniFile);   // move the new .new to .ini
        INFO("  c");
        #ifdef RTOS_H
        Thread::wait(1000);     // this seems to help with file contention
        #else
        wait(1);
        #endif
        INFO("  d");
    }
    swFree(valBuf);
    swFree(newFile);
    swFree(bakFile);
    return retVal;
}


//***********************************************************
// Private version that also works with local file system
// by copying one file to the other.
//    Returns -1 = error; 0 = success
//***********************************************************
int INI::Rename(const char *oldfname, const char *newfname)
{
    int retval = 0;

    INFO("Rename(%s,%s)", oldfname, newfname);
    if (Copy(oldfname, newfname) == 0) {
        remove(oldfname);
        retval = 0;
    } else {
        retval = -1;
    }
    return (retval);
}

//***********************************************************
// Private version that also works with local file system
//            Returns -1 = error; 0 = success
//***********************************************************
int INI::Copy(const char *src, const char *dst)
{
    int retval = 0;
    int ch;

    INFO("Copy(%s,%s)", src, dst);
    FILE *fpsrc = fopen(src, "r");   // src file
    FILE *fpdst = fopen(dst, "w");   // dest file

    if (fpsrc) {
        INFO("  c1a");
        if (fpdst) {
            INFO("  c1b");
            while (1) {                  // Copy src to dest
                ch = fgetc(fpsrc);       // until src EOF read.
                if (ch == EOF) break;
                fputc(ch, fpdst);
            }
            INFO("  c2");
        fclose(fpsrc);
        fclose(fpdst);
        }
    }
    INFO("  c3");

    if (Exists(dst)) {
        retval = 0;
    } else {
        retval = -1;
    }
    INFO("  c4");
    return (retval);
}


const char * INI::GetReturnMessage(INI_Return retVal) {
    if (version == 0) {
        switch (retVal) {
            default:
            case INI_V1_FAIL:           return "INI Fail";
            case INI_V1_SUCCESS:        return "INI Success";
        }
    } else {
        switch (retVal) {
            case INI_SUCCESS:           return "INI Success - operation succeeded";
            case INI_NO_FILE_SPEC:      return "INI Fail - no file was specified";
            case INI_FILE_NOT_FOUND:    return "INI Fail - ini file not found, or failed to open";
            case INI_SECTION_NOT_FOUND: return "INI Fail - section not found";
            case INI_KEY_NOT_FOUND:     return "INI Fail - key not found";
            case INI_BUF_TOO_SMALL:     return "INI Fail - buffer to small for value";
            case INI_INTERNAL_ERROR:    return "INI Fail - internal error - can't malloc";
            default:                    return "INI Fail - Code Unknown";
        }
    }
}

#if 0
// Test code for basic regression testing
//
#include <stdio.h>
#include <assert.h>
#include <string.h>

#include "INI.h"

#define TESTFILE "test.ini"


//INI_V1_FAIL = 0,        ///< Version 1 return value - Fail
//INI_V1_SUCCESS = 1,     ///< Version 1 return value - Success
//INI_SUCCESS = 0,        ///< Success - operation succeeded
//INI_NO_FILE_SPEC,       ///< Fail - no file was specified
//INI_FILE_NOT_FOUND,     ///< Fail - ini file not found, or failed to open
//INI_SECTION_NOT_FOUND,  ///< Fail - section not found
//INI_KEY_NOT_FOUND,      ///< Fail - key not found
//INI_BUF_TOO_SMALL,      ///< Fail - buffer to small for value
//INI_INTERNAL_ERROR      ///< Fail - internal error - can't alloc buffers

int main(int argc, char * argv[])
{
    FILE * fp;
    char buffer[100];
    INI ini(TESTFILE, 2);

    // Start testing
    _unlink(TESTFILE);
    assert(INI::INI_FILE_NOT_FOUND == ini.ReadString("Section 1", "Name 1", buffer, sizeof(buffer)));

    fp = fopen(TESTFILE, "wt");
    assert(fp);
    fprintf(fp, "[Section 1]\n");
    fprintf(fp, "Name 1=Value 1\n");
    fprintf(fp, "Name 2=Value 2\n");
    fprintf(fp, "\n");
    fprintf(fp, "[Section 2]\n");
    fprintf(fp, "Name 1=Value 2\n");
    fprintf(fp, "Name 2=Value 2\n");
    fprintf(fp, "Name 3=Value 3\n");
    fprintf(fp, "\n");
    fclose(fp);

    assert(INI::INI_SUCCESS == ini.ReadString("Section 2", "Name 2", buffer, sizeof(buffer)));
    assert(strcmp("Value 2", buffer) == 0);

    assert(INI::INI_SECTION_NOT_FOUND == ini.ReadString("Section 3", "Name", buffer, sizeof(buffer)));
    assert(INI::INI_KEY_NOT_FOUND == ini.ReadString("Section 1", "Name 3", buffer, sizeof(buffer)));

    assert(INI::INI_SUCCESS == ini.WriteString("Section 1", "Name 4", "Value 4"));
    assert(INI::INI_SUCCESS == ini.ReadString("Section 1", "Name 2", buffer, sizeof(buffer)));
    assert(INI::INI_KEY_NOT_FOUND == ini.ReadString("Section 1", "Name 3", buffer, sizeof(buffer)));
    assert(INI::INI_SUCCESS == ini.ReadString("Section 1", "Name 4", buffer, sizeof(buffer)));
    assert(strcmp("Value 4", buffer) == 0);

    assert(INI::INI_SUCCESS == ini.WriteString("Section 1", "Name 4", NULL));
    assert(INI::INI_KEY_NOT_FOUND == ini.ReadString("Section 1", "Name 4", buffer, sizeof(buffer)));

    return 0;
}
#endif