Click here to Skip to main content
15,886,199 members
Articles / Programming Languages / C++

C++: INI File Parser Generator

Rate me:
Please Sign up or sign in to vote.
2.14/5 (5 votes)
17 Jan 2017CPOL3 min read 20.6K   276   3  
Generate a validating INI parser based on schema with file save capability. User types supported out of the box!

Table of Contents

Introduction

Recently, I am very interested in the parser generator like Flex/Bison and Boost Spirit v3 but Flex/Bison is C API and Boost Spirit x3 is still beta. I found out expression parsing is no simple task for parser generator as operator precedence has to be taken of. I decided to write my own simple parser generator for ini file and leave C++ expression alone. In other words, I just copy and paste the C++ expression into the generated C++ code. The reason for interest in parser generator is I am designing a new XML Schema scheme which is imperative, I use a simple ini format to explore its feasibility.

INI Schema

The Ini Schema use ini file format as well. My XML Schema uses XML format as well. The 1st token is the name and 2 token consists of C++ data type and validation expression separated by ;

C++
# ini schema
StartDate=string;true
EndDate=string;Exists("StartDate") && EndDate() >= StartDate()
C++
# ini file
StartDate=2016-03-01
EndDate=2016-03-02

The EndDate validation states StartDate must exist and EndDate must be greater or equal to StartDate. Exists("StartDate") is redundant here as the StartDate() call will check value existence. What if StartDate is specified after EndDate in the ini file?

C++
# ini file: Order does not matter
EndDate=2016-03-02
StartDate=2016-03-01

The order does not matter as the validation checks are done after all lines are read.
Note: This is simplified ini file format which does not have sections.

The following schema is Alpha color channel whose value is restricted between 0 and 255 and a CheckFolder value which does not have any validation constraint and is optional, so we just put true. If CheckFolder is a mandatory value, we should put Exists("CheckFolder").

C++
# ini schema
Alpha=int;Alpha() >= 0 && Alpha() <= 255
CheckFolder=bool;true

Generated Code

This is how to use the generated class, MyIniFile.

C++
MyIniFile ini_file;
std::string error;
if(ini_file.ParseFile("C:\\temp\\test.ini", error))
{
    std::cout << ini_file.StartDate() << std::endl;
    std::cout << ini_file.EndDate() << std::endl;
    std::cout << ini_file.Alpha() << std::endl;
    std::cout << std::boolalpha << ini_file.CheckFolder() << std::endl;
}
else
{
    std::cerr << error << std::endl;
}

This is the generated header-only class, MyIniFile.
Note: I use minicsv class to parse the ini file.

C++
#include <string>
#include <map>
#include <fstream>
#include <stdexcept>
#include "minicsv.h"
#include "Color.h"

class MyIniFile
{
private:
    std::map<std::string, std::string> m_NameValueMap;
    std::string m_File;
public:
    bool ParseFile(const std::string& file)
    {
        m_NameValueMap.clear();
        csv::ifstream is(file.c_str());
        is.set_delimiter('=', "$$");
        is.enable_terminate_on_blank_line(false);
        if (is.is_open())
        {
            m_File = file;
            while (is.read_line())
            {
                std::string name;
                std::string value;
                is >> name;
                value = is.get_rest_of_line();
                m_NameValueMap[csv::trim(name, " \t")] = csv::trim(value, " \t");
            }
            is.close();
            return Validate();
        }
        else
        {
            std::ostringstream oss;
            oss << "File cannot be opened:" << file;
            throw std::runtime_error(oss.str().c_str());
        }
        return true;
    }
private:
    bool WriteFile(const std::string& key, const std::string& val)
    {
        std::vector<std::pair<std::string, std::string> > vec;
        csv::ifstream is(m_File.c_str());
        is.set_delimiter('=', "$$");
        is.enable_terminate_on_blank_line(false);
        if (is.is_open())
        {
            bool found=false;
            while (is.read_line())
            {
                std::string name;
                std::string value;
                is >> name;
                value = is.get_rest_of_line();

                if (csv::trim(name, " \t") == key)
                {
                    value = val;
                    found = true;
                }
                vec.push_back(std::make_pair(name, value));
            }
            if(!found)
            {
                vec.push_back(std::make_pair(key, val));
            }
            is.close();

            csv::ofstream os(m_File.c_str());
            os.set_delimiter('=', "$$");
            if (os.is_open())
            {
                for (size_t i = 0; i<vec.size(); ++i)
                {
                    os << vec[i].first << vec[i].second << NEWLINE;
                }
                os.flush();
                os.close();
            }
            else
            {
                std::ostringstream oss;
                oss << "File cannot be opened for writing:" << m_File;
                throw std::runtime_error(oss.str().c_str());
            }
        }
        else
        {
            std::ostringstream oss;
            oss << "File cannot be opened for reading:" << m_File;
            throw std::runtime_error(oss.str().c_str());
        }
        return true;
    }
public:
    bool Exists(const std::string& name)
    {
        typedef std::map<std::string, std::string> Map;
        typedef Map::const_iterator MapConstIter;
        MapConstIter it = m_NameValueMap.find(name);
        return it != m_NameValueMap.end();
    }
    std::string StartDate()
    {
        if(!Exists("StartDate"))
        {
            throw std::runtime_error("StartDate does not exist");
        }
        std::string val;
        std::istringstream iss(m_NameValueMap["StartDate"]);
        iss >> val;
        return val;
    }
    std::string GetSafeStartDate(std::string default_val)
    {
        if(Exists("StartDate"))
            return StartDate();
        else
            return default_val;
    }
    bool IsValidStartDate()
    {
        bool ret = false;
        try
        {
            ret = true;
        }
        catch(std::exception&)
        {
        }
        return ret;
    }
    bool SetStartDate(std::string val)
    {
        std::ostringstream oss;
        oss << val;
        std::string str_val = oss.str(); 
        if (str_val != m_NameValueMap["StartDate"])
        {
            m_NameValueMap["StartDate"] = str_val;
        }
        return WriteFile("StartDate", str_val);
    }
    std::string EndDate()
    {
        if(!Exists("EndDate"))
        {
            throw std::runtime_error("EndDate does not exist");
        }
        std::string val;
        std::istringstream iss(m_NameValueMap["EndDate"]);
        iss >> val;
        return val;
    }
    std::string GetSafeEndDate(std::string default_val)
    {
        if(Exists("EndDate"))
            return EndDate();
        else
            return default_val;
    }
    bool IsValidEndDate()
    {
        bool ret = false;
        try
        {
            ret = Exists("StartDate") && EndDate() >= StartDate();
        }
        catch(std::exception&)
        {
        }
        return ret;
    }
    bool SetEndDate(std::string val)
    {
        std::ostringstream oss;
        oss << val;
        std::string str_val = oss.str(); 
        if (str_val != m_NameValueMap["EndDate"])
        {
            m_NameValueMap["EndDate"] = str_val;
        }
        return WriteFile("EndDate", str_val);
    }
    int Alpha()
    {
        if(!Exists("Alpha"))
        {
            throw std::runtime_error("Alpha does not exist");
        }
        int val;
        std::istringstream iss(m_NameValueMap["Alpha"]);
        iss >> val;
        return val;
    }
    int GetSafeAlpha(int default_val)
    {
        if(Exists("Alpha"))
            return Alpha();
        else
            return default_val;
    }
    bool IsValidAlpha()
    {
        bool ret = false;
        try
        {
            ret = Alpha() >= 0 && Alpha() <= 255;
        }
        catch(std::exception&)
        {
        }
        return ret;
    }
    bool SetAlpha(int val)
    {
        std::ostringstream oss;
        oss << val;
        std::string str_val = oss.str(); 
        if (str_val != m_NameValueMap["Alpha"])
        {
            m_NameValueMap["Alpha"] = str_val;
        }
        return WriteFile("Alpha", str_val);
    }
    bool CheckFolder()
    {
        if(!Exists("CheckFolder"))
        {
            throw std::runtime_error("CheckFolder does not exist");
        }
        bool val = false;
        std::string s(m_NameValueMap["CheckFolder"]);
        if(s=="Y"||s=="1"||s=="true") val=true;
        else if(s=="N"||s=="0"||s=="false") val=false;
        return val;
    }
    bool GetSafeCheckFolder(bool default_val)
    {
        if(Exists("CheckFolder"))
            return CheckFolder();
        else
            return default_val;
    }
    bool IsValidCheckFolder()
    {
        bool ret = false;
        try
        {
            ret = true;
        }
        catch(std::exception&)
        {
        }
        std::string s(m_NameValueMap["CheckFolder"]);
        return ret&&(s=="Y"||s=="1"||s=="true"||s=="N"||s=="0"||s=="false");
    }
    bool SetCheckFolder(bool val)
    {
        std::ostringstream oss;
        oss << std::boolalpha << val;
        std::string str_val = oss.str(); 
        if (str_val != m_NameValueMap["CheckFolder"])
        {
            m_NameValueMap["CheckFolder"] = str_val;
        }
        return WriteFile("CheckFolder", str_val);
    }
    Color TintedColor()
    {
        if(!Exists("TintedColor"))
        {
            throw std::runtime_error("TintedColor does not exist");
        }
        Color val;
        std::istringstream iss(m_NameValueMap["TintedColor"]);
        iss >> val;
        return val;
    }
    Color GetSafeTintedColor(Color default_val)
    {
        if(Exists("TintedColor"))
            return TintedColor();
        else
            return default_val;
    }
    bool IsValidTintedColor()
    {
        bool ret = false;
        try
        {
            ret = true;
        }
        catch(std::exception&)
        {
        }
        return ret;
    }
    bool SetTintedColor(Color val)
    {
        std::ostringstream oss;
        oss << val;
        std::string str_val = oss.str(); 
        if (str_val != m_NameValueMap["TintedColor"])
        {
            m_NameValueMap["TintedColor"] = str_val;
        }
        return WriteFile("TintedColor", str_val);
    }
    bool Validate()
    {
        std::ostringstream oss;
        if(!IsValidStartDate())
        {
            oss << "StartDate validation fails!" << std::endl;
        }
        if(!IsValidEndDate())
        {
            oss << "EndDate validation fails!" << std::endl;
        }
        if(!IsValidAlpha())
        {
            oss << "Alpha validation fails!" << std::endl;
        }
        if(!IsValidCheckFolder())
        {
            oss << "CheckFolder validation fails!" << std::endl;
        }
        if(!IsValidTintedColor())
        {
            oss << "TintedColor validation fails!" << std::endl;
        }
        std::string error = oss.str();
        if (!error.empty())
        {
            throw std::runtime_error(error.c_str());
        }
        return true;
    }
};
C++
# ini schema
TintedColor=Color;true
C++
# ini file
TintedColor=253,140,98

File Save

File save are done through the setters. The order of entries and whitespaces of the original file is preserved. The example below saves the alpha value.

C++
ini_file.SetAlpha(120);

Custom User Type

Custom user type is supported out of the box if C++ streams' << and >> operators are overloaded for the type. We use Color struct as an example. Note: we use minicsv stringstream to help do the conversion. Just include Color.h in the auto-generated MyIniFile, we are good to compile!

C++
// Color.h
#include "minicsv.h"

struct Color
{
    Color() : r(0), g(0), b(0){}
    Color(int _r, int _g, int _b) : r(_r), g(_g), b(_b){}
    Color(const Color& other) : r(other.r), g(other.g), b(other.b){}
    int r;
    int g;
    int b;
};

inline std::ostream & operator<<(std::ostream & os, Color const & val)
{
    csv::ostringstream csv_os;
    csv_os.set_delimiter(',', "^^");
    csv_os << val.r << val.g << val.b;
    return os << csv_os.get_text();
}

inline std::istream & operator>>(std::istream & is, Color & val)
{
    std::string str;
    is >> str;
    csv::istringstream csv_is(str.c_str());
    csv_is.set_delimiter(',', "^^");
    if(csv_is.read_line())
    {
        csv_is >> val.r >> val.g >> val.b;
    }

    return is;
}

Security

Using C++ expression for validation check is a security risk for XML schema as the hacker can put arbitrary C++ code in the XML schema. And generating the XML parser using C++ compiler is very slow: this is not acceptable for high-load server needs to handle many XML file. After this consideration, I decide to use Lua for the expression because I can limit the functions which can be called.

Conclusion

This is not a full-fledged parser generator for production use. This is merely for validating my XML schema design which is rather imperative than declarative. In my XML schema, element and attribute also consists of a data type and constraint. My XML schema works conceptually the same for textual and binary XML, knowing the data type can greatly speed up the binary XML processing. Grab the latest code at Github.

History

  • 2016-04-02: Initial Release
  • 2016-04-07: File save capability through setters with order of entries and whitespace of the original file preserved. Custom user type supported out of the box

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Singapore Singapore
Shao Voon is from Singapore. His interest lies primarily in computer graphics, software optimization, concurrency, security, and Agile methodologies.

In recent years, he shifted focus to software safety research. His hobby is writing a free C++ DirectX photo slideshow application which can be viewed here.

Comments and Discussions

 
-- There are no messages in this forum --