3
\$\begingroup\$

I have written a basic ini file parser in C. It won't support big ini files, and isn't very fast when fetching configuration values, but should be enough for small ones.

I couldn't find "proper" definition of ini file standard, so there is no handling screening symbols, and all and any error encountered during parsing is silently swallowed.

inifile.h

/** \file inifile.h
 * Functions for reading configuration from disk
 * Configuration options have single lines
 * Comments start with ';'
 */

#ifndef _INIFILE_H_
#define _INIFILE_H_

typedef void* pinifile_data;

/** Open and parse an ini file
 * \param[in] fname name of file to read
 * \param[out] pinidb pointer to the variable for storing reference for
 * inifile data
 * \returns 0 on success
 */
int open_ini_file(const char* fname, pinifile_data* pinidb);

int get_integer(const pinifile_data inidb, const char* section, const char* entry, int default_value);
double get_real(const pinifile_data inidb, const char* section, const char* entry, double default_value);

/** Free memory used by ini file database
 * \param[in] inidb ini file database from open_ini_file
 */
void close_ini_file(pinifile_data inidb);

/** Create ini file database */
pinifile_data create_inidb();

/** Add or change value in ini file database
 * If there is no specified section or entry in database, they will be created
 * \param inidb ini file database
 * \param[in] section name of a section
 * \param[in] entry name of an entry
 * \param[in] value new value of entry in string form
 */
void set_entry(pinifile_data inidb, const char* section, const char* entry, const char* value);

/** Dump ini file database on disc
 * \param fname name of a file to dump into
 * \param inidb ini file database
 * \returns 0 on success
 */
int dump_ini_file(const char* fname, pinifile_data inidb);

#endif // _INIFILE_H_

inifile.c

#include "inifile.h"

#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <ctype.h>

struct inifile_entry
{
    const char* entry_name;
    const char* entry_value;
    struct inifile_entry* next_entry;
};

struct inifile_section
{
    const char* section_name;
    struct inifile_entry* first_entry;
    struct inifile_section* next_section;
};

struct inifile_data
{
    struct inifile_section* first_section;
};

void close_ini_file(pinifile_data inidb)
{
    struct inifile_data* pinidb = (struct inifile_data*)inidb;
    if(pinidb == NULL)
        return;

    struct inifile_section* pnext_s = NULL, * pcur_s = NULL;
    struct inifile_entry* pnext_e = NULL, * pcur_e = NULL;

    pcur_s = pinidb->first_section;
    while(pcur_s != NULL)
    {
        pnext_s = pcur_s->next_section;

        pcur_e = pcur_s->first_entry;
        while(pcur_e != NULL)
        {
            pnext_e = pcur_e->next_entry;
            if(pcur_e->entry_name != NULL)
                free((void*)pcur_e->entry_name);
            if(pcur_e->entry_value != NULL)
                free((void*)pcur_e->entry_value);
            free((void*)pcur_e);
            pcur_e = pnext_e;
        }
        if(pcur_s->section_name != NULL)
            free((void*)pcur_s->section_name); 
        free((void*)pcur_s);
        pcur_s = pnext_s;
    }
    free((void*)pinidb);
}

void init_section(struct inifile_section* psection)
{
    psection->section_name = NULL;
    psection->first_entry = NULL;
    psection->next_section = NULL;
}

void init_entry(struct inifile_entry* pentry)
{
    pentry->entry_name = NULL;
    pentry->entry_value = NULL;
    pentry->next_entry = NULL;
}

enum parser_states
{
    comment = 0,
    linestart,
    sectionentry,
    entryname,
    entryvalue,
    states_number
};

struct parser_state
{
    struct inifile_data context;
    struct inifile_section* section_cursor;
    struct inifile_entry* entry_cursor;
    char buffer[255];
    int buffer_cursor;
};

typedef enum parser_states (*ini_file_state_transition)(char next_char, struct parser_state* pastate);

enum parser_states comment_state(char next_char, struct parser_state* pastate)
{
    if(next_char == '\n')
    {
        pastate->buffer_cursor = 0;
    return linestart;
    }
    return comment;
}

enum parser_states linestart_state(char next_char, struct parser_state* pastate)
{
    if(next_char == '\n')
    {
        pastate->buffer_cursor = 0;
    return linestart;
    }
    if(next_char == ';')
    {
        pastate->buffer_cursor = 0;
        return comment;
    }
    if(next_char == ']' || next_char == '=')
    {
        // this is error, discard the string
        pastate->buffer_cursor = 0;
        return comment;
    }
    if(isspace(next_char))
        return linestart;
    if(next_char == '[')
    {
        pastate->buffer_cursor = 0;
    return sectionentry;
    }
    // in all other cases, start of entry
    if(pastate->section_cursor == NULL)
    {
        //entries without section are not considered
        pastate->buffer_cursor = 0;
    return comment;
    }
    pastate->buffer[pastate->buffer_cursor++] = next_char;
    return entryname;    
}

enum parser_states sectionentry_state(char next_char, struct parser_state* pastate)
{
    if(next_char == '\n')
    {
        // this is an error; data are discarded
        pastate->buffer_cursor = 0;
    return linestart;
    }
    if(next_char == '[' || next_char == ';')
        return comment;

    if(next_char != ']')
        pastate->buffer[pastate->buffer_cursor++] = next_char;
    // protection from buffer overflow
    if(next_char == ']' || pastate->buffer_cursor > 254)
    {
        // this is section name's end
    // if we have non-empty name, create new section
    if(pastate->buffer_cursor > 0)
    {
            // prepare next section
            if(pastate->section_cursor == NULL)
            {
                // make first section
                pastate->context.first_section = (struct inifile_section*)malloc(sizeof(struct inifile_section));
                init_section(pastate->context.first_section);
                pastate->section_cursor = pastate->context.first_section;
            }
            else
            {
                pastate->section_cursor->next_section = (struct inifile_section*)malloc(sizeof(struct inifile_section));
                init_section(pastate->section_cursor->next_section);
                pastate->section_cursor = pastate->section_cursor->next_section;
            }
            pastate->entry_cursor = NULL;
        pastate->section_cursor->section_name = (char*)malloc(pastate->buffer_cursor + 1);
        memcpy((void*)pastate->section_cursor->section_name, pastate->buffer, pastate->buffer_cursor);
        *(char*)(pastate->section_cursor->section_name + pastate->buffer_cursor) = '\0';
    }
    // discard the rest of the string
    return comment;
    }

    return sectionentry;

}

enum parser_states entryname_state(char next_char, struct parser_state* pastate)
{
    if(next_char == '\n')
    {
        // this is an error; data are discarded
        pastate->buffer_cursor = 0;
    return linestart;
    }
    if(next_char == ';' || next_char == '[' || next_char == ']')
    {
        // this is an error; data are discarded
        pastate->buffer_cursor = 0;
    return comment;
    }
    if(next_char != '=')
        pastate->buffer[pastate->buffer_cursor++] = next_char;
    // protection from buffer overflow
    if(pastate->buffer_cursor > 254)
    {
        // too long, discard everything
        pastate->buffer_cursor = 0;
    return comment;
    }

    if(next_char == '=')
    {
        // make new entry
        if(pastate->entry_cursor == NULL)
        {
            // make first entry
            pastate->section_cursor->first_entry = (struct inifile_entry*)malloc(sizeof(struct inifile_entry));
            init_entry(pastate->section_cursor->first_entry);
            pastate->entry_cursor = pastate->section_cursor->first_entry;
        }
        else
        {
            pastate->entry_cursor->next_entry = (struct inifile_entry*)malloc(sizeof(struct inifile_entry));
            init_entry(pastate->entry_cursor->next_entry);
        pastate->entry_cursor = pastate->entry_cursor->next_entry;
        }
        pastate->entry_cursor->entry_name = (char*)malloc(pastate->buffer_cursor + 1);
        memcpy((void*)pastate->entry_cursor->entry_name, pastate->buffer, pastate->buffer_cursor);
        *(char*)(pastate->entry_cursor->entry_name + pastate->buffer_cursor) = '\0';
        pastate->buffer_cursor = 0;
    return entryvalue;
    }
    return entryname;
}

enum parser_states entryvalue_state(char next_char, struct parser_state* pastate)
{
    enum parser_states res = entryvalue;
    if(next_char == '\n')
    {
        res = linestart;
    }
    else if(next_char == ';')
    {
        res = comment;
    }
    else
    {
        pastate->buffer[pastate->buffer_cursor++] = next_char;
    if(pastate->buffer_cursor > 254)
        res = comment;
    }
    if(res != entryvalue)
    {
        pastate->entry_cursor->entry_value = (char*)malloc(pastate->buffer_cursor + 1);
        memcpy((void*)pastate->entry_cursor->entry_value, pastate->buffer, pastate->buffer_cursor);
        *(char*)(pastate->entry_cursor->entry_value + pastate->buffer_cursor) = '\0';
        pastate->buffer_cursor = 0;
    }

    return res;

}

int open_ini_file(const char* fname, pinifile_data* pinidb)
{
    if(fname == NULL || pinidb == NULL)
        return 2;

    FILE* fstream = fopen(fname, "r");
    if(!fstream)
        return 1;

    struct parser_state context;
    memset(&context, 0, sizeof(struct parser_state));
    int character = 0, state_index = linestart;
    ini_file_state_transition parse_functions_table[] = {
        comment_state,
    linestart_state,
    sectionentry_state,
    entryname_state,
    entryvalue_state
    };
    do
    {
        character = fgetc(fstream);
    if(character != EOF)
    {
        state_index = parse_functions_table[state_index](character, &context);
    }
    } while(!(feof(fstream) || ferror(fstream)));
    fclose(fstream);

    *pinidb = malloc(sizeof(struct inifile_data));
    // here we are copying a pointer, yes
    memcpy(*pinidb, &context.context, sizeof(struct inifile_data));

    return 0;    
}

const char* get_value(const pinifile_data inidb, const char* section, const char* entry)
{
    struct inifile_data* pinidb = (struct inifile_data*)inidb;
    if(pinidb == NULL || section == NULL || entry == NULL)
        return NULL;

    struct inifile_section* pcur_s = NULL;
    struct inifile_entry* pcur_e = NULL;

    pcur_s = pinidb->first_section;
    while(pcur_s != NULL)
    {
        pcur_e = pcur_s->first_entry;

    if(!strcmp(section, pcur_s->section_name))
            while(pcur_e != NULL)
            {
                if(!strcmp(entry, pcur_e->entry_name))
                    return pcur_e->entry_value;
                pcur_e = pcur_e->next_entry;
            }
        pcur_s = pcur_s->next_section;
    }
    return NULL;

}

int get_integer(const pinifile_data inidb, const char* section, const char* entry, int default_value)
{
    const char* evalue = get_value(inidb, section, entry);
    if(!evalue)
        return default_value;
    return strtol(evalue, NULL, 10);
}

double get_real(const pinifile_data inidb, const char* section, const char* entry, double default_value)
{
    const char* evalue = get_value(inidb, section, entry);
    if(!evalue)
        return default_value;
    return strtod(evalue, NULL);

}

pinifile_data create_inidb()
{
    void* result = malloc(sizeof(struct inifile_data));
    memset(result, 0, sizeof(struct inifile_data));
    // Here we are copying a pointer, yes
    return result;
}

void set_entry(pinifile_data inidb, const char* section, const char* entry, const char* value)
{
    struct inifile_data* pinidb = (struct inifile_data*)inidb;
    if(pinidb == NULL || section == NULL || entry == NULL || value == NULL)
        return;

    struct inifile_section* target_section = NULL,* section_cursor = NULL;

    section_cursor = pinidb->first_section;
    while(section_cursor != NULL)
    {
        if(!strcmp(section, section_cursor->section_name))
    {
        target_section = section_cursor;
        break;
    }
    if(NULL == section_cursor->next_section)
        break;
        section_cursor = section_cursor->next_section;
    }
    if(target_section == NULL)
    {
         // prepare next section
         if(section_cursor == NULL)
         {
             // make first section
             pinidb->first_section = (struct inifile_section*)malloc(sizeof(struct inifile_section));
             init_section(pinidb->first_section);
             target_section = pinidb->first_section;
         }
         else
         {
             section_cursor->next_section = (struct inifile_section*)malloc(sizeof(struct inifile_section));
             init_section(section_cursor->next_section);
             target_section = section_cursor->next_section;
         }
         char* sname = (char*)malloc(strlen(section) + 1);
         target_section->section_name = strcpy(sname, section);
    }

    struct inifile_entry* target_entry = NULL,* entry_cursor = NULL;
    entry_cursor = target_section->first_entry;
    if(entry_cursor != NULL)
    {
        for(; entry_cursor->next_entry != NULL; entry_cursor = entry_cursor->next_entry)
        if(!strcmp(entry, entry_cursor->entry_name))
        {
            target_entry = entry_cursor;
        break;
        }
    }
    if(target_entry == NULL)
    {
        // prepare next entry
        if(entry_cursor == NULL)
        {
            // make first entry
            target_section->first_entry = (struct inifile_entry*)malloc(sizeof(struct inifile_entry));
            init_entry(target_section->first_entry);
            target_entry = target_section->first_entry;
        }
        else
        {
            entry_cursor->next_entry = (struct inifile_entry*)malloc(sizeof(struct inifile_entry));
            init_entry(entry_cursor->next_entry);
            target_entry = entry_cursor->next_entry;
        }
        char* ename = (char*)malloc(strlen(entry) + 1);
        target_entry->entry_name = strcpy(ename, entry);
    }
    if(target_entry->entry_value != NULL)
        free((void*)target_entry->entry_value);

    char* eval = (char*)malloc(strlen(value) + 1);
    target_entry->entry_value = strcpy(eval, value);
}

int dump_ini_file(const char* fname, pinifile_data inidb)
{
    struct inifile_data* pinidb = (struct inifile_data*)inidb;
    if(fname == NULL || pinidb == NULL)
        return 1;
    FILE* fstream = fopen(fname, "w");
    if(!fstream)
        return 1;

    struct inifile_section* section_cursor = pinidb->first_section;
    while(section_cursor != NULL)
    {
        fprintf(fstream, "[%s]\n", section_cursor->section_name);
    struct inifile_entry* entry_cursor = section_cursor->first_entry;
    while(entry_cursor != NULL)
    {
        fprintf(fstream, "%s=%s\n", entry_cursor->entry_name, entry_cursor->entry_value);
        entry_cursor = entry_cursor->next_entry;
    }
        section_cursor = section_cursor->next_section;
    }
    fclose(fstream);
    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ en.wikipedia.org/wiki/INI_file \$\endgroup\$ – Martin York Oct 21 '15 at 18:02
  • \$\begingroup\$ Not sure why are you bringing this up \$\endgroup\$ – Srv19 Oct 21 '15 at 19:45
  • \$\begingroup\$ @Srv19 because the article mentions that "The INI file format is not well defined", so you most probably won't find a definitive INI standard, as you already wrote. Thus you should also document what kind of files your parser accepts, that too helps with the review part. \$\endgroup\$ – ferada Oct 22 '15 at 15:08
  • \$\begingroup\$ It is preferable to post another question with the updated code, if you're interested in a follow-up. Code should not be edited after receiving answers, to avoid creating out of context reviews. Please take a moment to read: What you may and may not do after receiving answers \$\endgroup\$ – glampert Oct 22 '15 at 18:23
4
\$\begingroup\$

From a quick look, there are a few things I see that can be improved:

  • Your code has a lot of unnecessary type casts. In special, casting to void* is not just useless, but also dangerous, since it might hide an error like passing a value to a function that expects a pointer. Never explicitly cast to void* when you call free, memcpy, etc.

  • Casting from void* to T* is also unnecessary in C. void* converts implicitly both ways. Unless you really care about having the same code compile as C and C++, no need to cast in places like the return value from malloc.

  • You can save type-name repetition in places like this:

    pinifile_data create_inidb()
    {
        void* result = malloc(sizeof(struct inifile_data));
        memset(result, 0, sizeof(struct inifile_data));
        // Here we are copying a pointer, yes
        return result;
    }
    

    By instead using the variable name in the sizeof expression. sizeof is compile-time, so it is legal. E.g.:

    pinifile_data create_inidb(void)
    {
        struct inifile_data* result = malloc(sizeof(*result));
        memset(result, 0, sizeof(*result));
        return result;
    }
    

    Take advantage of the little type inference you have and Don't Repeat Yourself. If you change the name of that type, now it's just one place to update in that function.

    But going further, since that function wants to allocate zero-initialized memory, why not use calloc() instead?

    pinifile_data create_inidb(void)
    {
        return calloc(1, sizeof(struct inifile_data));
    }
    
  • I personally find impractical prefixing every instantiation of a structure with struct, so I always typedef them on declaration to avoid that. Same for enums.

  • You have a few formatting issues in your code, which hurt readability. A tool like Clang Format can automate fixing that.

  • Adding curly braces { } on single line statements is optional, but a good idea. It will make future updates to your code less painful and less error prone.

\$\endgroup\$
  • \$\begingroup\$ +1 for discussion of drying out type names, didn't know I could do it that way. \$\endgroup\$ – sqykly Oct 22 '15 at 23:12
2
\$\begingroup\$

The set_entry function is too large. Refactor it to several ones.

Section, entry, value can't be NULL, so it's better to use const reference.

open_ini_file returns 1,2,0 What do they mean? Use human readable defines or enumeration.

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! I edited your post a bit to make it clearer. Please make sure I haven't made any mistakes and feel free to edit it if I've misinterpreted your meaning. \$\endgroup\$ – SuperBiasedMan Oct 21 '15 at 11:56
  • 1
    \$\begingroup\$ Const reference is a C++ thing, this is plain C, so no other option but pointer. \$\endgroup\$ – glampert Oct 21 '15 at 13:56
  • \$\begingroup\$ @SuperBiasedMan thank you! I will try hard to improve my english! \$\endgroup\$ – Gard Nov 6 '15 at 16:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.