Added support for multiple value types in config nodes.

Removed Ptr_t and ConstPtr_t from public interface, moved config tree builders into their own file, and made them pass by reference and value.
Exceptions can be thrown now on lookup failures.
Removed s_empty_node -- failed lookups in [] operator create a permanent "hidden" child.
Updated comment in NewConfig.cpp.
This commit is contained in:
Bart Trzynadlowski 2016-08-21 22:22:45 +00:00
parent 01a6ad1596
commit 2efe18b525
6 changed files with 763 additions and 496 deletions

235
Src/Util/ConfigBuilders.cpp Normal file
View file

@ -0,0 +1,235 @@
#include "Util/NewConfig.h"
#include "OSD/Logger.h"
#include "Pkgs/tinyxml2.h"
#include <algorithm>
#include <fstream>
#include <queue>
#include <cstdlib>
#include <iostream>
namespace Util
{
namespace Config
{
static void PopulateFromXML(Util::Config::Node *config, const tinyxml2::XMLDocument &xml)
{
using namespace tinyxml2;
std::queue<std::pair<const XMLElement *, Util::Config::Node *>> q;
// Push the top level of the XML tree
for (const XMLElement *e = xml.RootElement(); e != 0; e = e->NextSiblingElement())
q.push( { e, config } );
// Process the elements in order, pushing subsequent levels along with the
// config nodes to add them to
while (!q.empty())
{
const XMLElement *element = q.front().first;
Util::Config::Node *parent_node = q.front().second;
q.pop();
// Create a config entry for this XML element
Util::Config::Node *node = &parent_node->Add(element->Name(), element->GetText() ? std::string(element->GetText()) : std::string());
// Create entries for each attribute
for (const XMLAttribute *a = element->FirstAttribute(); a != 0; a = a->Next())
node->Add(a->Name(), a->Value());
// Push all child elements
for (const XMLElement *e = element->FirstChildElement(); e != 0; e = e->NextSiblingElement())
q.push( { e, node } );
}
}
bool FromXML(Node *config, const std::string &text)
{
*config = Node("xml");
using namespace tinyxml2;
XMLDocument xml;
auto ret = xml.Parse(text.c_str());
if (ret != XML_SUCCESS)
{
ErrorLog("Failed to parse XML (%s).", xml.ErrorName());
return true;
}
PopulateFromXML(config, xml);
return false;
}
bool FromXMLFile(Node *config, const std::string &filename)
{
*config = Node("xml");
using namespace tinyxml2;
XMLDocument xml;
auto ret = xml.LoadFile(filename.c_str());
if (ret != XML_SUCCESS)
{
ErrorLog("Failed to parse %s (%s).", filename.c_str(), xml.ErrorName());
return true;
}
PopulateFromXML(config, xml);
return false;
}
static std::string StripComment(const std::string &line)
{
// Find first semicolon not enclosed in ""
bool inside_quotes = false;
for (auto it = line.begin(); it != line.end(); ++it)
{
inside_quotes ^= (*it == '\"');
if (!inside_quotes && *it == ';')
return std::string(line.begin(), it);
}
return line;
}
static void ParseAssignment(Node *current_section, const std::string &filename, size_t line_num, const std::string &line)
{
size_t idx_equals = line.find_first_of('=');
if (idx_equals == std::string::npos)
{
ErrorLog("%s:%d: Syntax error. No '=' found.", filename.c_str(), line_num);
return;
}
std::string lvalue(TrimWhiteSpace(std::string(line.begin(), line.begin() + idx_equals)));
if (lvalue.empty())
{
ErrorLog("%s:%d: Syntax error. Setting name missing before '='.", filename.c_str(), line_num);
return;
}
// Get rvalue, which is allowed to be empty and strip off quotes if they
// exist. Only a single pair of quotes encasing the rvalue are permitted.
std::string rvalue(TrimWhiteSpace(std::string(line.begin() + idx_equals + 1, line.end())));
size_t idx_first_quote = rvalue.find_first_of('\"');
size_t idx_last_quote = rvalue.find_last_of('\"');
if (idx_first_quote == 0 && idx_last_quote == (rvalue.length() - 1) && idx_first_quote != idx_last_quote)
rvalue = std::string(rvalue.begin() + idx_first_quote + 1, rvalue.begin() + idx_last_quote);
if (std::count(rvalue.begin(), rvalue.end(), '\"') != 0)
{
ErrorLog("%s:%d: Warning: Extraneous quotes present on line.", filename.c_str(), line_num);
}
// In INI files, we do not allow multiple settings with the same key. If
// a setting is specified multiple times, previous ones are overwritten.
current_section->Set(lvalue, rvalue);
}
bool FromINIFile(Node *config, const std::string &filename)
{
std::ifstream file;
file.open(filename);
if (file.fail())
{
ErrorLog("Unable to open '%s'. Configuration will not be loaded.", filename.c_str());
return true;
}
*config = Node("Global"); // the root is also the "Global" section
Node &global = *config;
Node *current_section = &global;
size_t line_num = 1;
while (!file.eof())
{
if (file.fail())
{
ErrorLog("%s:%d: File read error. Configuration will be incomplete.");
return true;
}
std::string line;
std::getline(file, line);
line = StripComment(line);
line = TrimWhiteSpace(line);
if (!line.empty())
{
if (*line.begin() == '[' && *line.rbegin() == ']')
{
// Check if section exists else create a new one
std::string section(TrimWhiteSpace(std::string(line.begin() + 1, line.begin() + line.length() - 1)));
if (section.empty() || section == "Global") // empty section names (e.g., "[]") assigned to "[ Global ]"
current_section = &global;
else if (!global.TryGet(section))
{
Node &new_section = global.Add(section);
current_section = &new_section;
}
else
current_section = global.TryGet(section);
}
else
{
ParseAssignment(current_section, filename, line_num, line);
}
}
line_num++;
}
return false;
}
/*
* Produces a new INI section by merging two existing sections.
*
* - Nodes from x and y with children are ignored as per INI semantics.
* The presence of children indicates a section, not a setting.
* - Settings from y override settings in x if already present.
* - If multiple settings with the same key are present in either of the
* source configs (which technically violates INI semantics), the last
* ends up being used.
* - x's key is retained.
*/
void MergeINISections(Node *merged, const Node &x, const Node &y)
{
*merged = Node(x.Key()); // result has same key as x
// First copy settings from section x
for (auto it = x.begin(); it != x.end(); ++it)
{
if (it->IsLeaf())
{
// INI semantics: take care to only create a single setting per key
merged->Set(it->Key(), it->GetValue());
}
}
// Merge in settings from section y
for (auto it = y.begin(); it != y.end(); ++it)
{
if (it->IsLeaf() && it->Exists())
merged->Set(it->Key(), it->GetValue());
}
}
static void WriteSection(std::ofstream &file, const Node &section)
{
file << "[ " << section.Key() << " ]" << std::endl;
file << std::endl;
for (auto it = section.begin(); it != section.end(); ++it)
{
if (it->IsLeaf()) //TODO: check if value exists?
file << it->Key() << " = \"" << it->ValueAs<std::string>() << "\"" << std::endl;
}
file << std::endl << std::endl;
}
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment)
{
std::ofstream file;
file.open(filename);
if (file.fail())
{
ErrorLog("Unable to write to '%s'. Configuration will not be saved.");
return;
}
if (!header_comment.empty())
file << header_comment << std::endl << std::endl;
WriteSection(file, config);
for (auto it = config.begin(); it != config.end(); ++it)
{
if (it->HasChildren())
WriteSection(file, *it);
}
}
}
}

20
Src/Util/ConfigBuilders.h Normal file
View file

@ -0,0 +1,20 @@
#ifndef INCLUDED_UTIL_CONFIGBUILDERS_H
#define INCLUDED_UTIL_CONFIGBUILDERS_H
#include <string>
namespace Util
{
namespace Config
{
class Node;
bool FromXML(Node *config, const std::string &text);
bool FromXMLFile(Node *config, const std::string &filename);
bool FromINIFile(Node *config, const std::string &filename);
void MergeINISections(Node *merged, const Node &x, const Node &y);
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment);
}
}
#endif // INCLUDED_UTIL_CONFIGBUILDERS_H

124
Src/Util/GenericValue.h Normal file
View file

@ -0,0 +1,124 @@
#ifndef INCLUDED_UTIL_GENERICVALUE_H
#define INCLUDED_UTIL_GENERICVALUE_H
#include "Util/Format.h"
#include <typeinfo>
#include <typeindex>
#include <sstream>
#include <stdexcept>
#include <memory>
namespace Util
{
class GenericValue
{
private:
std::type_index m_type;
virtual void *GetData() = 0;
virtual const void *GetData() const = 0;
public:
template <typename T>
inline bool Is() const
{
return m_type == std::type_index(typeid(T));
}
template <typename T>
const T &Value() const
{
if (!Is<T>())
throw std::logic_error(Util::Format() << "GenericValue::Value(): cannot get value as " << std::type_index(typeid(T)).name() <<" because it is stored as " << m_type.name());
return *reinterpret_cast<const T *>(GetData());
}
template <typename T>
T ValueAs() const
{
if (m_type == std::type_index(typeid(T)))
return *reinterpret_cast<const T *>(GetData());
std::stringstream ss;
Serialize(&ss);
T tmp;
ss >> tmp;
return tmp;
}
template <typename T>
void Set(const T &value)
{
if (!Is<T>())
throw std::logic_error(Util::Format() << "GenericValue::Set(): cannot set value as " << std::type_index(typeid(T)).name() <<" because it is stored as " << m_type.name());
*reinterpret_cast<T *>(GetData()) = value;
}
void Set(const char *value)
{
Set<std::string>(value);
}
virtual void Serialize(std::ostream *os) const = 0;
virtual std::shared_ptr<GenericValue> MakeCopy() const = 0;
GenericValue(std::type_index type)
: m_type(type)
{}
virtual ~GenericValue()
{
}
};
template <typename T>
struct ValueInstance: public GenericValue
{
private:
T m_data;
void *GetData()
{
return reinterpret_cast<void *>(&m_data);
}
const void *GetData() const
{
return reinterpret_cast<const void *>(&m_data);
}
public:
void Serialize(std::ostream *os) const
{
*os << m_data;
}
std::shared_ptr<GenericValue> MakeCopy() const
{
return std::make_shared<ValueInstance<T>>(*this);
}
ValueInstance(const ValueInstance<T> &that)
: GenericValue(std::type_index(typeid(T))),
m_data(that.m_data)
{
}
ValueInstance(const T &data)
: GenericValue(std::type_index(typeid(T))),
m_data(data)
{
}
ValueInstance()
: GenericValue(std::type_index(typeid(T)))
{
}
~ValueInstance()
{
//std::cout << "ValueInstance destroyed" << std::endl;
}
};
} // Util
#endif // INCLUDED_UTIL_GENERICVALUE_H

View file

@ -1,15 +1,41 @@
/*
* Config Tree
* -----------
* ===========
*
* Accessing Hierarchical Nodes
* ----------------------------
*
* A hierarchical data structure supporting arbitrary nesting. Each node
* (Config::Node) has a key and either a value or children (in fact, it may
* have both, but this does not make semantic sense and so the config tree
* have both, but this rarely makes semantic sense and so the config tree
* builders take care not to allow it).
*
* All values are simply strings for now. They may have defaults, which are
* actually supplied by the caller and returned when a queried node cannot be
* found.
* Nesting is denoted with the '/' separator. For example:
*
* 1. node.ValueAs<int>()
* 2. node["foo"]
* 3. node["foo"].ValueAs<int>()
* 4. node["foo/bar"]
*
* [1] accesses the value of the node (more on this below). [2] accesses the
* a child node with key "foo". [3] accesses the value of the child node "foo".
* [4] accesses child "bar" of child "foo", and so forth.
*
* Similar to map semantics, the operator [] never fails. If the node does not
* exist, it creates a dummy "missing" node that is retained as a hidden child.
* This node will have an empty value, which cannot be accessed, except using
* the ValueAsDefault<> method. This scheme exists to simplify lookup code for
* keys known at compile time, the logic being that any "missing" key should
* could just as well have been there in the first place, thus making the added
* memory usage negligible. For example, it easily allows values with defaults
* to be expressed as:
*
* config["foo/bar"].ValueAsDefault<std::string>("default_value")
*
* If the lookups are not known at compile time (e.g., are driven by the
* program user or external data), it is safer to use Get() and TryGet(), which
* can throw and return nullptr, respectively, and avoid wasting memory with
* dummy nodes.
*
* Nodes at the same nesting level (siblings) are strung together in a
* linked list. Parents also maintain pointers to the first and last of their
@ -22,15 +48,51 @@
*
* Example:
*
* <game name="scud">
* <roms>
* <crom name="foo.bin" offset=0/>
* <crom name="bar.bin" offset=2/>
* </roms>
* <game name="scud">
* <roms>
* <crom name="foo.bin" offset=0/>
* <crom name="bar.bin" offset=2/>
* </roms>
*
* config["game/roms"].Value() <-- should return nothing
* config["game/roms/crom/offset"].Value() <-- "2", the second <crom> tag
* config["game/roms"].begin() <-- iterate over two <crom> tags
* 1. config["game/roms"].ValueAs<std::string>()
* 2. config["game/roms/crom/offset"].ValueAs<int>()
* 3. config["game/roms"].begin()
*
* [1] will either throw an exception or return an empty string (depending on
* how the XML was translated to a config tree), [2] will return 2 (the second
* "crom" tag), and [3] can be used to iterate over both "crom" tags in order.
*
* Values
* ------
*
* To check whether or not a value is defined, use the Exists() and Empty()
* methods.
*
* Values are stored in a special container and can be virtually anything.
* However, it is recommended that only PODs and std::string be used. Arrays
* and pointers will probably not behave as expected and should be avoided.
* Trees build from files have all values loaded as std::strings. The types can
* be changed by subsequent re-assignments or by manually building a tree.
*
* To access a value of a given known type, T, use:
*
* node.Value<T>()
*
* Conversions as supported but are implemented via serialization and de-
* serialization using sstream. Most "sane" conversions will work as expected.
* When a conversion to T is desired, or if the stored value type is not
* precisely known, use:
*
* node.ValueAs<T>()
*
* These functions will throw an exception if the value is not defined at all
* or if the node does not exist (i.e., a "missing" node from a failed lookup).
* An alternative that is guaranteed to succeed is:
*
* node.ValueAsDefault<T>(default_value)
*
* If the value or node does not exist, default_value is returned, otherwise
* the stored value is returned with conversion (if needed) to type T.
*
* INI Semantics
* -------------
@ -51,64 +113,48 @@
* INI files contain zero or more sections followed by zero or more settings
* each.
*
* Setting0 = 100
* [ Global ]
* Setting1 = 200
* [ Section1 ]
* SettingA = 1 ; this is a comment
* SettingB = foo
* [ Section2 ]
* SettingX = bar
* SettingZ = "baz" ; quotes are optional and are stripped off during parsing
* Setting0 = 100
* [ Global ]
* Setting1 = 200
* [ Section1 ]
* SettingA = 1 ; this is a comment
* SettingB = foo
* [ Section2 ]
* SettingX = bar
* SettingZ = "baz" ; quotes are optional and are stripped off during parsing
*
* Any setting not explicitly part of a section is assigned to the "Global"
* section. For example, Setting0 and Setting1 above are both part of "Global".
*
* TODO
* ----
* - Define our own exceptions?
*/
#include "Util/NewConfig.h"
#include "OSD/Logger.h"
#include "Pkgs/tinyxml2.h"
#include <algorithm>
#include <fstream>
#include <queue>
#include <cstdlib>
#include <iostream>
namespace Util
{
namespace Config
{
Node Node::s_empty_node;
bool Node::ValueAsBool() const
void Node::CheckEmptyOrMissing() const
{
const char *value = m_value.c_str();
if (!value || !stricmp(value, "false") || !stricmp(value, "off") || !stricmp(value, "no"))
return false;
else if (!stricmp(value, "true") || !stricmp(value, "on") || !stricmp(value, "yes"))
return true;
return bool(ValueAsUnsigned());
if (m_missing)
throw std::range_error(Util::Format() << "Node \"" << m_key << "\" does not exist");
if (Empty() && !m_missing)
throw std::logic_error(Util::Format() << "Node \"" << m_key << "\" has no value" );
}
bool Node::ValueAsBoolWithDefault(bool default_value) const
const Node &Node::MissingNode(const std::string &key) const
{
if (this == &s_empty_node)
return default_value;
return ValueAsBool();
}
uint64_t Node::ValueAsUnsigned() const
{
if (m_value.find("0x") == 0 || m_value.find("0X") == 0)
return strtoull(m_value.c_str() + 2, 0, 16);
return strtoull(m_value.c_str(), 0, 10);
}
uint64_t Node::ValueAsUnsignedWithDefault(uint64_t default_value) const
{
if (this == &s_empty_node)
return default_value;
return ValueAsUnsigned();
auto it = m_missing_nodes.find(key);
if (it == m_missing_nodes.end())
{
auto result = m_missing_nodes.emplace(key, key);
result.first->second.m_missing = true; // mark this node as missing
return result.first->second;
}
return it->second;
}
const Node &Node::operator[](const std::string &path) const
@ -119,7 +165,7 @@ namespace Util
{
auto it = e->m_children.find(key);
if (it == e->m_children.end())
return s_empty_node;
return e->MissingNode(key);
e = it->second.get();
}
return *e;
@ -127,68 +173,72 @@ namespace Util
Node &Node::Get(const std::string &path)
{
// This is probably dangerous and we should just have a non-const []
return const_cast<Node &>(operator[](path));
Node *node = TryGet(path);
if (!node)
throw std::range_error(Util::Format() << "Node \"" << path << "\" does not exist");
return *node;
}
const Node &Node::Get(const std::string &path) const
{
return operator[](path);
const Node *node = TryGet(path);
if (!node)
throw std::range_error(Util::Format() << "Node \"" << path << "\" does not exist");
return *node;
}
Node *Node::TryGet(const std::string &path)
{
Node *e = this;
std::vector<std::string> keys = Util::Format(path).Split('/');
for (auto &key: keys)
{
auto it = e->m_children.find(key);
if (it == e->m_children.end())
return nullptr;
e = it->second.get();
}
return e;
}
const Node *Node::TryGet(const std::string &path) const
{
const Node *e = this;
std::vector<std::string> keys = Util::Format(path).Split('/');
for (auto &key: keys)
{
auto it = e->m_children.find(key);
if (it == e->m_children.end())
return nullptr;
e = it->second.get();
}
return e;
}
void Node::Serialize(std::ostream *os, size_t indent_level) const
{
std::fill_n(std::ostream_iterator<char>(*os), 2 * indent_level, ' ');
*os << m_key << "=\"";
if (Exists())
m_value->Serialize(os);
*os << "\" children={";
for (auto v: m_children)
*os << ' ' << v.first;
*os << " }" << std::endl;
for (ptr_t child = m_first_child; child; child = child->m_next_sibling)
child->Serialize(os, indent_level + 1);
}
std::string Node::ToString(size_t indent_level) const
{
std::ostringstream os;
std::fill_n(std::ostream_iterator<char>(os), 2 * indent_level, ' ');
os << m_key;
if (m_value.length())
os << '=' << m_value;
os << " children={";
for (auto v: m_children)
os << ' ' << v.first;
os << " }" << std::endl;
for (Ptr_t child = m_first_child; child; child = child->m_next_sibling)
os << child->ToString(indent_level + 1);
Serialize(&os, indent_level);
return os.str();
}
void Node::Print(size_t indent_level) const
{
std::cout << ToString(indent_level);
}
Node &Node::Add(const std::string &path, const std::string &value)
{
std::vector<std::string> keys = Util::Format(path).Split('/');
Node *parent = this;
Ptr_t node;
for (size_t i = 0; i < keys.size(); i++)
{
// Create node at this level
node = std::make_shared<Node>(keys[i]);
// The leaf node gets the value
if (i == keys.size() - 1)
node->SetValue(value);
// Attach node to parent and move down to next nesting level: last
// created node is new parent
AddChild(*parent, node);
parent = node.get();
}
return *node;
}
void Node::Set(const std::string &key, const std::string &value)
{
Node &node = Get(key);
if (node.Empty())
Add(key, value);
else
node.SetValue(value);
}
// Adds a newly-created node (which, among other things, implies no
// children) as a child
void Node::AddChild(Node &parent, Ptr_t &node)
void Node::AddChild(Node &parent, ptr_t &node)
{
if (!parent.m_last_child)
{
@ -209,10 +259,11 @@ namespace Util
return;
Destroy();
*const_cast<std::string *>(&m_key) = that.m_key;
m_value = that.m_value;
for (Ptr_t child = that.m_first_child; child; child = child->m_next_sibling)
if (that.m_value)
m_value = that.m_value->MakeCopy();
for (ptr_t child = that.m_first_child; child; child = child->m_next_sibling)
{
Ptr_t copied_child = std::make_shared<Node>(*child);
ptr_t copied_child = std::make_shared<Node>(*child);
AddChild(*this, copied_child);
}
}
@ -240,26 +291,27 @@ namespace Util
}
Node::Node()
: m_key("config"), // a default key value
m_value(nullptr) // this node is empty
{
//std::cout << "<<< Created " << "<null>" << " (" << this << ")" << std::endl;
}
Node::Node(const std::string &key)
: m_key(key)
: m_key(key),
m_value(nullptr) // this node is empty
{
//std::cout << "<<< Created " << key << " (" << this << ")" << std::endl;
}
Node::Node(const std::string &key, const std::string &value)
: m_key(key),
m_value(value)
m_value(std::make_shared<ValueInstance<std::string>>(value))
{
//std::cout << "<<< Created " << key << '=' << value << " (" << this << ")" << std::endl;
}
Node::Node(const Node &that)
: m_key(that.m_key),
m_value(that.m_value)
{
DeepCopy(that);
}
@ -273,244 +325,5 @@ namespace Util
{
//std::cout << ">>> Destroyed " << m_key << " (" << this << ")" << std::endl;
}
// An explicit function so that users don't accidentally create empty nodes
Node::Ptr_t CreateEmpty()
{
return std::shared_ptr<Node>(new Node());
}
static void PopulateFromXML(Util::Config::Node::Ptr_t &config, const tinyxml2::XMLDocument &xml)
{
using namespace tinyxml2;
std::queue<std::pair<const XMLElement *, Util::Config::Node *>> q;
// Push the top level of the XML tree
for (const XMLElement *e = xml.RootElement(); e != 0; e = e->NextSiblingElement())
q.push( { e, config.get() } );
// Process the elements in order, pushing subsequent levels along with the
// config nodes to add them to
while (!q.empty())
{
const XMLElement *element = q.front().first;
Util::Config::Node *parent_node = q.front().second;
q.pop();
// Create a config entry for this XML element
Util::Config::Node *node = &parent_node->Add(element->Name(), element->GetText() ? std::string(element->GetText()) : std::string());
// Create entries for each attribute
for (const XMLAttribute *a = element->FirstAttribute(); a != 0; a = a->Next())
node->Add(a->Name(), a->Value());
// Push all child elements
for (const XMLElement *e = element->FirstChildElement(); e != 0; e = e->NextSiblingElement())
q.push( { e, node } );
}
}
Node::Ptr_t FromXML(const std::string &text)
{
Node::Ptr_t config = std::make_shared<Util::Config::Node>("xml");
using namespace tinyxml2;
XMLDocument xml;
auto ret = xml.Parse(text.c_str());
if (ret != XML_SUCCESS)
{
ErrorLog("Failed to parse XML (%s).", xml.ErrorName());
return CreateEmpty();
}
PopulateFromXML(config, xml);
return config;
}
Node::Ptr_t FromXMLFile(const std::string &filename)
{
Node::Ptr_t config = std::make_shared<Util::Config::Node>("xml");
using namespace tinyxml2;
XMLDocument xml;
auto ret = xml.LoadFile(filename.c_str());
if (ret != XML_SUCCESS)
{
ErrorLog("Failed to parse %s (%s).", filename.c_str(), xml.ErrorName());
return CreateEmpty();
}
PopulateFromXML(config, xml);
return config;
}
static std::string StripComment(const std::string &line)
{
// Find first semicolon not enclosed in ""
bool inside_quotes = false;
for (auto it = line.begin(); it != line.end(); ++it)
{
inside_quotes ^= (*it == '\"');
if (!inside_quotes && *it == ';')
return std::string(line.begin(), it);
}
return line;
}
static void ParseAssignment(Node &current_section, const std::string &filename, size_t line_num, const std::string &line)
{
size_t idx_equals = line.find_first_of('=');
if (idx_equals == std::string::npos)
{
ErrorLog("%s:%d: Syntax error. No '=' found.", filename.c_str(), line_num);
return;
}
std::string lvalue(TrimWhiteSpace(std::string(line.begin(), line.begin() + idx_equals)));
if (lvalue.empty())
{
ErrorLog("%s:%d: Syntax error. Setting name missing before '='.", filename.c_str(), line_num);
return;
}
// Get rvalue, which is allowed to be empty and strip off quotes if they
// exist. Only a single pair of quotes encasing the rvalue are permitted.
std::string rvalue(TrimWhiteSpace(std::string(line.begin() + idx_equals + 1, line.end())));
size_t idx_first_quote = rvalue.find_first_of('\"');
size_t idx_last_quote = rvalue.find_last_of('\"');
if (idx_first_quote == 0 && idx_last_quote == (rvalue.length() - 1) && idx_first_quote != idx_last_quote)
rvalue = std::string(rvalue.begin() + idx_first_quote + 1, rvalue.begin() + idx_last_quote);
if (std::count(rvalue.begin(), rvalue.end(), '\"') != 0)
{
ErrorLog("%s:%d: Warning: Extraneous quotes present on line.", filename.c_str(), line_num);
}
// In INI files, we do not allow multiple settings with the same key. If
// a setting is specified multiple times, previous ones are overwritten.
if (current_section[lvalue].Empty())
current_section.Add(lvalue, rvalue);
else
current_section.Get(lvalue).SetValue(rvalue);
}
Node::Ptr_t FromINIFile(const std::string &filename)
{
std::ifstream file;
file.open(filename);
if (file.fail())
{
ErrorLog("Unable to open '%s'. Configuration will not be loaded.", filename.c_str());
return CreateEmpty();
}
Node::Ptr_t global_ptr = std::make_shared<Node>("Global"); // the root is also the "Global" section
Node &global = *global_ptr;
Node *current_section = &global;
size_t line_num = 1;
while (!file.eof())
{
if (file.fail())
{
ErrorLog("%s:%d: File read error. Configuration will be incomplete.");
return CreateEmpty();
}
std::string line;
std::getline(file, line);
line = StripComment(line);
line = TrimWhiteSpace(line);
if (!line.empty())
{
if (*line.begin() == '[' && *line.rbegin() == ']')
{
// Check if section exists else create a new one
std::string section(TrimWhiteSpace(std::string(line.begin() + 1, line.begin() + line.length() - 1)));
if (section.empty() || section == "Global") // empty section names (e.g., "[]") assigned to "[ Global ]"
current_section = &global;
else if (global[section].Empty())
{
Node &new_section = global.Add(section);
current_section = &new_section;
}
else
current_section = &global.Get(section);
}
else
{
ParseAssignment(*current_section, filename, line_num, line);
}
}
line_num++;
}
return global_ptr;
}
/*
* Produces a new INI section by merging two existing sections.
*
* - Nodes from x and y with children are ignored as per INI semantics.
* The presence of children indicates a section, not a setting.
* - Settings from y override settings in x if already present.
* - If multiple settings with the same key are present in either of the
* source configs (which technically violates INI semantics), the last
* ends up being used.
* - x's key is retained.
*/
Node::Ptr_t MergeINISections(const Node &x, const Node &y)
{
Node::Ptr_t merged_ptr = std::make_shared<Node>(x.Key()); // result has same key as x
Node &merged = *merged_ptr;
// First copy settings from section x
for (auto it = x.begin(); it != x.end(); ++it)
{
auto &key = it->Key();
auto &value = it->Value();
if (it->IsLeaf())
{
// INI semantics: take care to only create a single setting per key
merged.Set(key, value);
}
}
// Merge in settings from section y
for (auto it = y.begin(); it != y.end(); ++it)
{
auto &key = it->Key();
auto &value = it->Value();
if (it->IsLeaf())
{
merged.Set(key, value);
}
}
return merged_ptr;
}
static void WriteSection(std::ofstream &file, const Node &section)
{
file << "[ " << section.Key() << " ]" << std::endl;
file << std::endl;
for (auto it = section.begin(); it != section.end(); ++it)
{
if (it->IsLeaf())
file << it->Key() << " = \"" << it->Value() << "\"" << std::endl;
}
file << std::endl << std::endl;
}
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment)
{
std::ofstream file;
file.open(filename);
if (file.fail())
{
ErrorLog("Unable to write to '%s'. Configuration will not be saved.");
return;
}
if (!header_comment.empty())
file << header_comment << std::endl << std::endl;
WriteSection(file, config);
for (auto it = config.begin(); it != config.end(); ++it)
{
if (it->HasChildren())
WriteSection(file, *it);
}
}
} // Config
} // Util

View file

@ -1,10 +1,11 @@
#ifndef INCLUDED_UTIL_CONFIG_H
#define INCLUDED_UTIL_CONFIG_H
#include "Util/Format.h"
#include "Util/GenericValue.h"
#include <map>
#include <memory>
#include <iterator>
#include <exception>
namespace Util
{
@ -12,33 +13,33 @@ namespace Util
{
class Node
{
public:
typedef std::shared_ptr<Node> Ptr_t;
typedef std::shared_ptr<const Node> ConstPtr_t;
private:
Ptr_t m_next_sibling;
Ptr_t m_first_child;
Ptr_t m_last_child;
std::map<std::string, Ptr_t> m_children;
const std::string m_key; // this cannot be changed (maps depend on it)
std::string m_value;
static Node s_empty_node; // key, value, and children must always be empty
typedef std::shared_ptr<Node> ptr_t;
typedef std::shared_ptr<const Node> const_ptr_t;
const std::string m_key; // this cannot be changed (maps depend on it)
std::shared_ptr<GenericValue> m_value; // null pointer marks this node as an invalid, empty node
ptr_t m_next_sibling;
ptr_t m_first_child;
ptr_t m_last_child;
std::map<std::string, ptr_t> m_children;
mutable std::map<std::string, Node> m_missing_nodes; // missing nodes from failed queries (must also be empty)
bool m_missing = false;
void Destroy()
{
m_value.reset();
m_next_sibling.reset();
m_first_child.reset();
m_last_child.reset();
m_children.clear();
m_value.clear();
}
void AddChild(Node &parent, Ptr_t &node);
void CheckEmptyOrMissing() const;
const Node &MissingNode(const std::string &key) const;
void AddChild(Node &parent, ptr_t &node);
void DeepCopy(const Node &that);
void Swap(Node &rhs);
Node(); // prohibit accidental/unintentional creation of blank nodes
friend Ptr_t CreateEmpty();
Node(); // prohibit accidental/unintentional creation of empty nodes
public:
class const_iterator;
@ -46,10 +47,10 @@ namespace Util
class iterator: public std::iterator<std::forward_iterator_tag, Node>
{
private:
Ptr_t m_node;
ptr_t m_node;
friend class const_iterator;
public:
inline iterator(Ptr_t node = Ptr_t())
inline iterator(ptr_t node = ptr_t())
: m_node(node)
{}
inline iterator(const iterator &it)
@ -72,7 +73,7 @@ namespace Util
{
return *m_node;
}
inline Ptr_t operator->() const
inline ptr_t operator->() const
{
return m_node;
}
@ -89,12 +90,12 @@ namespace Util
class const_iterator: public std::iterator<std::forward_iterator_tag, const Node>
{
private:
ConstPtr_t m_node;
const_ptr_t m_node;
public:
inline const_iterator(ConstPtr_t node = ConstPtr_t())
inline const_iterator(const_ptr_t node = const_ptr_t())
: m_node(node)
{}
inline const_iterator(Ptr_t node = Ptr_t())
inline const_iterator(ptr_t node = ptr_t())
: m_node(std::const_pointer_cast<const Node>(node))
{}
inline const_iterator(const const_iterator &it)
@ -120,7 +121,7 @@ namespace Util
{
return *m_node;
}
inline ConstPtr_t operator->() const
inline const_ptr_t operator->() const
{
return m_node;
}
@ -159,57 +160,135 @@ namespace Util
return m_key;
}
const inline std::string &ValueWithDefault(const std::string &default_value) const
inline std::shared_ptr<GenericValue> GetValue() const
{
if (this == &s_empty_node)
return m_value;
}
inline void SetValue(const std::shared_ptr<GenericValue> &value)
{
m_value = value;
}
template <typename T>
const T &Value() const
{
CheckEmptyOrMissing();
return m_value->Value<T>();
}
template <typename T>
T ValueAs() const
{
CheckEmptyOrMissing();
return m_value->ValueAs<T>();
}
template <typename T>
T ValueAsDefault(const T &default_value) const
{
if (Empty())
return default_value;
return m_value;
return m_value->ValueAs<T>();
}
const inline std::string &Value() const
// Set value of this node
template <typename T>
inline void SetValue(const T &value)
{
//TODO: if empty node, throw exception? Use ValueWithDefault() otherwise.
return m_value;
if (!m_missing)
{
if (!Empty() && m_value->Is<T>())
m_value->Set(value);
else
m_value = std::make_shared<ValueInstance<T>>(value);
}
else
throw std::range_error(Util::Format() << "Node \"" << m_key << "\" does not exist");
}
bool ValueAsBool() const;
bool ValueAsBoolWithDefault(bool default_value) const;
uint64_t ValueAsUnsigned() const;
uint64_t ValueAsUnsignedWithDefault(uint64_t default_value) const;
inline void SetValue(const std::string &value)
// char* is a troublesome case because we want to convert it to std::string
inline void SetValue(const char *value)
{
if (this != &s_empty_node) // not allowed to modify empty node
m_value = value;
SetValue<std::string>(value);
}
inline bool Empty() const
// Add a child node
template <typename T>
Node &Add(const std::string &path, const T &value)
{
return m_key.empty();
std::vector<std::string> keys = Util::Format(path).Split('/');
Node *parent = this;
ptr_t node;
for (size_t i = 0; i < keys.size(); i++)
{
// Create node at this level
node = std::make_shared<Node>(keys[i]);
// The leaf node gets the value
if (i == keys.size() - 1)
node->SetValue(value);
// Attach node to parent and move down to next nesting level: last
// created node is new parent
AddChild(*parent, node);
parent = node.get();
}
return *node;
}
Node &Add(const std::string &path)
{
return Add(path, std::string());
}
// Set the value of the matching child node if it exists, else add new
template <typename T>
void Set(const std::string &key, const T &value)
{
Node *node = TryGet(key);
if (node)
node->SetValue(value);
else
Add(key, value);
}
// True if value is empty (does not exist)
inline bool Empty() const
{
return !m_value;
}
// True if value exists (is not empty)
inline bool Exists() const
{
return !Empty();
}
// True if no keys under this node
inline bool IsLeaf() const
{
return m_children.empty();
}
// True if this node has keys
inline bool HasChildren() const
{
return !IsLeaf();
}
Node &Get(const std::string &path);
// Always succeeds -- failed lookups permanently create an empty node.
// Use with caution. Intended for hard-coded lookups.
const Node &operator[](const std::string &path) const;
// These throw if the node is missing
Node &Get(const std::string &path);
const Node &Get(const std::string &path) const;
void Print(size_t indent_level = 0) const;
// This returns nullptr if node is missing
Node *TryGet(const std::string &path);
const Node *TryGet(const std::string &path) const;
void Serialize(std::ostream *os, size_t indent_level = 0) const;
std::string ToString(size_t indent_level = 0) const;
Node &Add(const std::string &key, const std::string &value = "");
void Set(const std::string &key, const std::string &value);
Node &operator=(const Node &rhs);
Node &operator=(Node &&rhs);
Node(const std::string &key);
@ -218,15 +297,6 @@ namespace Util
Node(Node &&that);
~Node();
};
//TODO: CreateEmpty() should take a key that defaults to blank
//TODO: define deep-copy assignment operator and get rid of Ptr_t and ConstPtr_t
Node::Ptr_t CreateEmpty();
Node::Ptr_t FromXML(const std::string &text);
Node::Ptr_t FromXMLFile(const std::string &filename);
Node::Ptr_t FromINIFile(const std::string &filename);
Node::Ptr_t MergeINISections(const Node &x, const Node &y);
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment);
} // Config
} // Util

View file

@ -1,4 +1,5 @@
#include "Util/NewConfig.h"
#include "Util/ConfigBuilders.h"
#include <iostream>
static void PrintTestResults(std::vector<std::pair<std::string, bool>> results)
@ -30,58 +31,61 @@ int main()
* </roms>
* </game>
*/
Util::Config::Node::Ptr_t root = std::make_shared<Util::Config::Node>("global");
auto &game = root->Add("game");
game.Add("name", "scud");
Util::Config::Node root("global");
auto &game = root.Add("game");
game.Add<std::string>("name", "scud");
auto &roms = game.Add("roms");
auto &crom1 = roms.Add("crom");
crom1.Add("name", "foo.bin");
crom1.Add("offset", "0");
crom1.Add<std::string>("name", "foo.bin");
crom1.Add<std::string>("offset", "0");
auto &crom2 = roms.Add("crom");
crom2.Add("name", "bar.bin");
crom2.Add("offset", "2");
crom2.Add<std::string>("name", "bar.bin");
crom2.Add<std::string>("offset", "2");
const char *expected_output =
"global children={ game }\n"
" game children={ name roms }\n"
" name=scud children={ }\n"
" roms children={ crom }\n"
" crom children={ name offset }\n"
" name=foo.bin children={ }\n"
" offset=0 children={ }\n"
" crom children={ name offset }\n"
" name=bar.bin children={ }\n"
" offset=2 children={ }\n";
"global=\"\" children={ game }\n"
" game=\"\" children={ name roms }\n"
" name=\"scud\" children={ }\n"
" roms=\"\" children={ crom }\n"
" crom=\"\" children={ name offset }\n"
" name=\"foo.bin\" children={ }\n"
" offset=\"0\" children={ }\n"
" crom=\"\" children={ name offset }\n"
" name=\"bar.bin\" children={ }\n"
" offset=\"2\" children={ }\n";
std::cout << "Expected output:" << std::endl << std::endl << expected_output << std::endl;
std::cout << "Actual config tree:" << std::endl << std::endl;
root->Print();
root.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "Manual", root->ToString() == expected_output });
test_results.push_back({ "Manual", root.ToString() == expected_output });
// Expect second crom: bar.bin
auto &global = *root;
std::cout << "game/roms/crom/name=" << global["game/roms/crom/name"].Value() << " (expected: bar.bin)" << std::endl << std::endl;
test_results.push_back({ "Lookup", global["game/roms/crom/name"].Value() == "bar.bin" });
std::cout << "game/roms/crom/name=" << root["game/roms/crom/name"].Value<std::string>() << " (expected: bar.bin)" << std::endl << std::endl;
test_results.push_back({ "Lookup", root["game/roms/crom/name"].Value<std::string>() == "bar.bin" });
// Make a copy using copy constructor
std::cout << "Copy constructed:" << std::endl << std::endl;
Util::Config::Node::Ptr_t copy = std::make_shared<Util::Config::Node>(*root);
copy->Print();
Util::Config::Node copy(root);
copy.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "Copy constructed", copy->ToString() == root->ToString() });
test_results.push_back({ "Copy constructed", copy.ToString() == root.ToString() });
// Make a deep copy using assignment operator
// Make a deep copy using assignment operator, check equality, then modify a
// value and ensure it does not change in the copy
std::cout << "Copy by assignment:" << std::endl << std::endl;
Util::Config::Node copy2("foo", "bar");
copy2.Add("x/y/z", "test");
copy2.Add("dummy", "value");
copy2.Add<std::string>("x/y/z", "test");
copy2.Add<std::string>("dummy", "value");
std::cout << "Initially set to:" << std::endl;
copy2.Print();
copy2 = *root;
copy2.Serialize(&std::cout);
copy2 = root;
std::cout << "Copy of \"global\":" << std::endl;
copy2.Print();
copy2.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "Copy by assignment", copy2.ToString() == root->ToString() });
bool copies_are_equivalent = copy2.ToString() == root.ToString();
copy2.Get("game/name").SetValue("vf3");
bool copies_are_unique = copy2["game/name"].Value<std::string>() == "vf3" && root["game/name"].Value<std::string>() == "scud";
test_results.push_back({ "Copy by assignment", copies_are_equivalent && copies_are_unique });
// Parse from XML
const char *xml =
@ -102,77 +106,78 @@ int main()
" </roms> \n"
"</game> \n";
const char *expected_xml_config_tree =
"xml children={ game }\n"
" game children={ name roms }\n"
" name=scud children={ }\n"
" roms children={ region }\n"
" region children={ byte_swap file name stride }\n"
" name=crom children={ }\n"
" stride=8 children={ }\n"
" byte_swap=true children={ }\n"
" file children={ crc32 name offset }\n"
" offset=0 children={ }\n"
" name=epr-19734.20 children={ }\n"
" crc32=0xBE897336 children={ }\n"
" file children={ crc32 name offset }\n"
" offset=2 children={ }\n"
" name=epr-19733.19 children={ }\n"
" crc32=0x6565E29A children={ }\n"
" file children={ crc32 name offset }\n"
" offset=4 children={ }\n"
" name=epr-19732.18 children={ }\n"
" crc32=0x23E864BB children={ }\n"
" file children={ crc32 name offset }\n"
" offset=6 children={ }\n"
" name=epr-19731.17 children={ }\n"
" crc32=0x3EE6447E children={ }\n"
" region children={ byte_swap file name stride }\n"
" name=banked_crom children={ }\n"
" stride=8 children={ }\n"
" byte_swap=true children={ }\n"
" file children={ crc32 name offset }\n"
" offset=0x0000000 children={ }\n"
" name=mpr-20364.4 children={ }\n"
" crc32=0xA2A68EF2 children={ }\n"
" file children={ crc32 name offset }\n"
" offset=0x0000002 children={ }\n"
" name=mpr-20363.3 children={ }\n"
" crc32=0x3E3CC6FF children={ }\n"
" file children={ crc32 name offset }\n"
" offset=0x0000004 children={ }\n"
" name=mpr-20362.2 children={ }\n"
" crc32=0xF7E60DFD children={ }\n"
" file children={ crc32 name offset }\n"
" offset=0x0000006 children={ }\n"
" name=mpr-20361.1 children={ }\n"
" crc32=0xDDB66C2F children={ }\n";
"xml=\"\" children={ game }\n"
" game=\"\" children={ name roms }\n"
" name=\"scud\" children={ }\n"
" roms=\"\" children={ region }\n"
" region=\"\" children={ byte_swap file name stride }\n"
" name=\"crom\" children={ }\n"
" stride=\"8\" children={ }\n"
" byte_swap=\"true\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"0\" children={ }\n"
" name=\"epr-19734.20\" children={ }\n"
" crc32=\"0xBE897336\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"2\" children={ }\n"
" name=\"epr-19733.19\" children={ }\n"
" crc32=\"0x6565E29A\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"4\" children={ }\n"
" name=\"epr-19732.18\" children={ }\n"
" crc32=\"0x23E864BB\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"6\" children={ }\n"
" name=\"epr-19731.17\" children={ }\n"
" crc32=\"0x3EE6447E\" children={ }\n"
" region=\"\" children={ byte_swap file name stride }\n"
" name=\"banked_crom\" children={ }\n"
" stride=\"8\" children={ }\n"
" byte_swap=\"true\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"0x0000000\" children={ }\n"
" name=\"mpr-20364.4\" children={ }\n"
" crc32=\"0xA2A68EF2\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"0x0000002\" children={ }\n"
" name=\"mpr-20363.3\" children={ }\n"
" crc32=\"0x3E3CC6FF\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"0x0000004\" children={ }\n"
" name=\"mpr-20362.2\" children={ }\n"
" crc32=\"0xF7E60DFD\" children={ }\n"
" file=\"\" children={ crc32 name offset }\n"
" offset=\"0x0000006\" children={ }\n"
" name=\"mpr-20361.1\" children={ }\n"
" crc32=\"0xDDB66C2F\" children={ }\n";
std::cout << "Expected output:" << std::endl << std::endl << expected_xml_config_tree << std::endl;
std::cout << "Actual config tree:" << std::endl << std::endl;
Util::Config::Node::Ptr_t xml_config = Util::Config::FromXML(xml);
xml_config->Print();
Util::Config::Node xml_config("xml");
Util::Config::FromXML(&xml_config, xml);
xml_config.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "XML", xml_config->ToString() == expected_xml_config_tree });
test_results.push_back({ "XML", xml_config.ToString() == expected_xml_config_tree });
// Create a nested key
{
Util::Config::Node::Ptr_t config = Util::Config::CreateEmpty();
config->Add("foo/bar/baz", "bart");
config->Print();
Util::Config::Node config("global");
config.Add<std::string>("foo/bar/baz", "bart");
config.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "Nested key 1", config->Get("foo/bar/baz").Value() == "bart" });
config->Get("foo/bar/baz").Set("x", "xx");
config->Get("foo/bar/baz").Set("y/z", "zz");
config->Print();
test_results.push_back({ "Nested key 1", config["foo/bar/baz"].Value<std::string>() == "bart" });
config.Get("foo/bar/baz").Set<std::string>("x", "xx");
config.Get("foo/bar/baz").Set<std::string>("y/z", "zz");
config.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "Nested key 2", config->Get("foo/bar/baz/x").Value() == "xx" });
test_results.push_back({ "Nested key 3", config->Get("foo/bar/baz/y/z").Value() == "zz" });
config->Add("a/b/c");
config->Get("a/b/c").SetValue("d");
config->Print();
test_results.push_back({ "Nested key 2", config["foo/bar/baz/x"].Value<std::string>() == "xx" });
test_results.push_back({ "Nested key 3", config["foo/bar/baz/y/z"].Value<std::string>() == "zz" });
config.Add("a/b/c");
config.Get("a/b/c").SetValue<std::string>("d");
config.Serialize(&std::cout);
std::cout << std::endl;
test_results.push_back({ "Nested key 4", config->Get("a/b/c").Value() == "d" });
test_results.push_back({ "Nested key 4", config["a/b/c"].Value<std::string>() == "d" });
}
PrintTestResults(test_results);
return 0;
}