From 9e831289a6d0bf24a800e7d02c39f6f37b2bffd8 Mon Sep 17 00:00:00 2001 From: "Marian W." Date: Sat, 1 Feb 2025 07:31:50 +0100 Subject: [PATCH] Implement the parser and writer --- minipp/DoubleFileTest.cpp | 2 + minipp/minipp.vcxproj | 2 +- minipp/minipp.vcxproj.filters | 4 +- minipp/minipp/minipp.cpp | 436 ---------------- minipp/minipp/minipp.hpp | 933 +++++++++++++++++++++++++++++++--- minipp/test.cpp | 45 +- minipp/test.mini | 24 +- minipp/test_out.mini | 19 + 8 files changed, 950 insertions(+), 515 deletions(-) create mode 100644 minipp/DoubleFileTest.cpp delete mode 100644 minipp/minipp/minipp.cpp create mode 100644 minipp/test_out.mini diff --git a/minipp/DoubleFileTest.cpp b/minipp/DoubleFileTest.cpp new file mode 100644 index 0000000..4839d92 --- /dev/null +++ b/minipp/DoubleFileTest.cpp @@ -0,0 +1,2 @@ + +#include "minipp/minipp.hpp" \ No newline at end of file diff --git a/minipp/minipp.vcxproj b/minipp/minipp.vcxproj index 88359dd..8c081ca 100644 --- a/minipp/minipp.vcxproj +++ b/minipp/minipp.vcxproj @@ -130,8 +130,8 @@ + - diff --git a/minipp/minipp.vcxproj.filters b/minipp/minipp.vcxproj.filters index aee9209..32c0bd4 100644 --- a/minipp/minipp.vcxproj.filters +++ b/minipp/minipp.vcxproj.filters @@ -20,10 +20,10 @@ - + Quelldateien - + Quelldateien diff --git a/minipp/minipp/minipp.cpp b/minipp/minipp/minipp.cpp deleted file mode 100644 index f4ef2d0..0000000 --- a/minipp/minipp/minipp.cpp +++ /dev/null @@ -1,436 +0,0 @@ -#include "minipp.hpp" -#include - -#define MINIPP_ENABLE_DEBUG_OUTPUT true - -#if MINIPP_ENABLE_DEBUG_OUTPUT -#include -#define COUT(msg) std::cout << msg << std::endl -#else -#define COUT(msg) -#endif - -#define ASSERT(condition) if (!condition) abort(); - -#define COUT_SYNTAX_ERROR(line, msg) COUT("Error in line " << line << ": " << msg) - -minipp::MiniPPFile::Section::~Section() -{ - for (auto& pair : m_values) - delete pair.second; - for (auto& pair : m_subSections) - delete pair.second; -} - -minipp::MiniPPFile::Section* minipp::MiniPPFile::Section::GetSubSection(const std::string& key, EResult* result) const noexcept -{ - auto pathIndex = key.find('.'); - std::string thisKey = key; - std::string rest; - if (pathIndex != std::string::npos) - { - thisKey = key.substr(0, pathIndex); - rest = key.substr(pathIndex + 1); - } - - auto it = m_subSections.find(thisKey); - if (it == m_subSections.end()) - { - COUT("Sub-Section not found: " << thisKey); - if (result != nullptr) - *result = EResult::SectionNotPresent; - return nullptr; - } - if (result != nullptr) - *result = EResult::Success; - if (rest.empty()) - return it->second; - return it->second->GetSubSection(rest, result); -} - -minipp::MiniPPFile::Section* minipp::MiniPPFile::Section::SetSubSection(const std::string& name, std::unique_ptr
value, bool allowOverwrite, EResult* result) noexcept -{ - if (m_subSections.find(name) != m_subSections.end()) - if (!allowOverwrite) - { - if (result != nullptr) - *result = EResult::SectionAlreadyPresent; - return nullptr; - } - else - delete m_subSections[name]; - - auto val = value.release(); - m_subSections[name] = val; - if (result != nullptr) - *result = EResult::Success; - return val; -} - -minipp::MiniPPFile::StringValue::StringValue(const std::string& str) : m_value(str) {} - -minipp::EResult minipp::MiniPPFile::StringValue::Parse(const std::string& str) noexcept -{ - m_value = ""; - for (size_t i = 0; i < str.size(); ++i) - { - if (str[i] == '\\') - { - if (i + 1 >= str.size()) - return EResult::FormatError; - - switch (str[i + 1]) - { - case '\"': - m_value.push_back('\"'); - break; - case 'n': - m_value.push_back('\n'); - break; - case 't': - m_value.push_back('\t'); - break; - case 'r': - m_value.push_back('\r'); - break; - case '\\': - m_value.push_back('\\'); - break; - default: - return EResult::FormatError; - } - ++i; - } - else if (str[i] == '"') - return EResult::FormatError; - else - m_value.push_back(str[i]); - } - return EResult::Success; -} - -minipp::EResult minipp::MiniPPFile::StringValue::ToString(std::string& destination) const noexcept -{ - std::string sanitizedValue = m_value; - for (size_t i = 0; i < sanitizedValue.size(); ++i) - { - switch (sanitizedValue[i]) - { - case '\n': - sanitizedValue.replace(i, 1, "\\n"); - i++; - break; - case '\t': - sanitizedValue.replace(i, 1, "\\t"); - i++; - break; - case '\r': - sanitizedValue.replace(i, 1, "\\r"); - i++; - break; - case '\\': - sanitizedValue.insert(i, 1, '\\'); - i++; - break; - case '\"': - sanitizedValue.insert(i, 1, '\\'); - i++; - break; - } - } - destination = "\"" + sanitizedValue + "\""; - return EResult::Success; -} - -const std::string& minipp::MiniPPFile::StringValue::GetValue() const noexcept -{ - return m_value; -} - -minipp::EResult minipp::MiniPPFile::Parse(const std::string& path) noexcept -{ - std::ifstream ifs; - ifs.open(path); - if (!ifs.is_open()) - return EResult::FileIOError; - - int64_t lineCounter = 0; - - Section* currentSection = nullptr; - - std::string currentLine; - while (std::getline(ifs, currentLine)) - { - ++lineCounter; - Tools::StringTrim(currentLine); - if (currentLine.empty()) - continue; - char firstChar = currentLine[0]; - char lastChar = currentLine[currentLine.size() - 1]; - - if (firstChar == '#') - continue; - - if (firstChar == '[') - { - if (lastChar != ']') - { - COUT_SYNTAX_ERROR(lineCounter, "Expected ']' at the end of the line."); - return EResult::FormatError; - } - std::string sectionName = currentLine.substr(1, currentLine.size() - 2); - Tools::StringTrim(sectionName); - if (sectionName.empty()) - { - COUT_SYNTAX_ERROR(lineCounter, "Expected section path. Found empty section begin notation."); - return EResult::FormatError; - } - // Create section tree - Section* ubSection = &m_rootSection; - - std::vector sectionPath = Tools::SplitByDelimiter(sectionName, '.'); - for (size_t i = 0; i < sectionPath.size(); ++i) - { - const std::string& sectionName = sectionPath[i]; - if (!Tools::IsNameValid(sectionName)) - { - COUT_SYNTAX_ERROR(lineCounter, "Invalid section name. (\"" << sectionName << "\") May only contain [a - z][A - Z][0 - 9] and _."); - return EResult::FormatError; - } - - EResult result; - ubSection->SetSubSection(sectionName, std::make_unique
(), false, &result); // if this fails, the section is already present - if (result == EResult::SectionAlreadyPresent && i == sectionPath.size() - 1) - { - COUT_SYNTAX_ERROR(lineCounter, "All (sub-) sections may only be defined once."); - return EResult::FormatError; - } - ubSection = ubSection->GetSubSection(sectionName, &result); - ASSERT(Tools::IsResultOk(result)); // should never happen - } - currentSection = ubSection; - continue; - } - if (currentSection == nullptr) - { - COUT_SYNTAX_ERROR(lineCounter, "Expected section begin before key-value pair."); - return EResult::FormatError; - } - - int64_t keyValueDelimiterIndex = Tools::FirstIndexOf(currentLine, '='); - if (keyValueDelimiterIndex == -1) - { - COUT_SYNTAX_ERROR(lineCounter, "Expected '=' in line."); - return EResult::FormatError; - } - auto keyValuePair = Tools::SplitInTwo(currentLine, keyValueDelimiterIndex); - Tools::StringTrim(keyValuePair.first); - Tools::StringTrim(keyValuePair.second); - if (keyValuePair.first.empty()) - { - COUT_SYNTAX_ERROR(lineCounter, "Expected key in line."); - return EResult::FormatError; - } - if (keyValuePair.second.empty()) - { - COUT_SYNTAX_ERROR(lineCounter, "Empty keys are not allowed"); - return EResult::FormatError; - } - std::string& value = keyValuePair.second; - char valueFirstChar = value.front(); - char valueLastChar = value.back(); - if (valueFirstChar == '"') - { - if (valueLastChar != '"') - { - COUT_SYNTAX_ERROR(lineCounter, "Expected '\"' at the end of the value."); - return EResult::FormatError; - } - // is string - value = value.substr(1, value.size() - 2); - StringValue strValue; - auto parseResult = strValue.Parse(value); - if (!Tools::IsResultOk(parseResult)) - { - COUT_SYNTAX_ERROR(lineCounter, "Invalid string value."); - return EResult::FormatError; - } - currentSection->SetValue(keyValuePair.first, strValue, false); - continue; - } - else - { - IntValue intValue; - auto parseResult = intValue.Parse(value); - if (!Tools::IsResultOk(parseResult)) - { - COUT_SYNTAX_ERROR(lineCounter, "Invalid integer value."); - return EResult::FormatError; - } - currentSection->SetValue(keyValuePair.first, intValue, false); - continue; - } - - } - - return EResult::Success; -} - -void minipp::MiniPPFile::Write(const std::string& path) noexcept -{ -} - -bool minipp::MiniPPFile::Tools::StringStartsWith(const std::string& str, const std::string& beg) -{ - if (str.size() < beg.size()) - return false; - - for (size_t i = 0; i < beg.size(); ++i) - if (str[i] != beg[i]) - return false; - return true; -} - -bool minipp::MiniPPFile::Tools::StringEndsWith(const std::string& str, const std::string& end) -{ - if (str.size() < end.size()) - return false; - - for (size_t i = 0; i < end.size(); ++i) - if (str[str.size() - i - 1] != end[end.size() - i - 1]) - return false; - return true; -} - -void minipp::MiniPPFile::Tools::StringTrim(std::string& str) -{ - if (str.empty()) - return; - - size_t start = 0; - size_t end = str.size() - 1; - while (str[start] == ' ' || str[start] == '\t') - start++; - while (str[end] == ' ' || str[end] == '\t') - end--; - - str = str.substr(start, end - start + 1); -} - -bool minipp::MiniPPFile::Tools::IsNameValid(const std::string& name) noexcept -{ - for (const char c : name) - { - if ( - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || - (c == '_')) - continue; - return false; - } - return true; -} - -int64_t minipp::MiniPPFile::Tools::FirstIndexOf(const std::string& str, char c) noexcept -{ - int64_t index = 0; - for (const char ch : str) - { - if (ch == c) - return index; - ++index; - } - return -1; -} - -int64_t minipp::MiniPPFile::Tools::LastIndexOf(const std::string& str, char c) noexcept -{ - int64_t index = str.size() - 1; - for (int64_t i = str.size() - 1; i >= 0; --i) - { - if (str[i] == c) - return index; - --index; - } - return -1; -} - -std::pair minipp::MiniPPFile::Tools::SplitInTwo(const std::string& str, int64_t firstLength) noexcept -{ - return std::make_pair(str.substr(0, firstLength), str.substr(firstLength + 1)); -} - -std::vector minipp::MiniPPFile::Tools::SplitByDelimiter(const std::string& str, char delimiter) noexcept -{ - std::vector elements; - std::string tmp; - - for (const char c : str) - { - if (c == delimiter) - { - elements.push_back(tmp); - tmp.clear(); - } - else - tmp.push_back(c); - } - - if (!tmp.empty()) - elements.push_back(tmp); - return elements; -} - -bool minipp::MiniPPFile::Tools::IsResultOk(EResult result) noexcept -{ - return static_cast(result) > 0; -} - -void minipp::MiniPPFile::Tools::RemoveAll(std::string& str, char old) -{ - str.erase(std::remove(str.begin(), str.end(), old), str.end()); -} - -bool minipp::MiniPPFile::Tools::IsIntegerDecimal(const std::string& str) noexcept -{ - for (size_t i = 0; i < str.size(); ++i) - if (str[i] < '0' || str[i] > '9') - return false; - return true; -} - -minipp::MiniPPFile::IntValue::IntValue(int64_t value) : m_value(value) {} - -minipp::EResult minipp::MiniPPFile::IntValue::Parse(const std::string& str) noexcept -{ - std::string sanitizedValue = str; - Tools::RemoveAll(sanitizedValue, '_'); - if (sanitizedValue.empty()) - return EResult::FormatError; - - char lastCharacter = str.back(); - auto rest = str.substr(0, str.size() - 1); - if (lastCharacter == 'h') - m_value = std::stoll(rest, nullptr, 16); - else if (lastCharacter == 'b') - m_value = std::stoll(rest, nullptr, 2); - else - { - if (!Tools::IsIntegerDecimal(sanitizedValue)) - return EResult::FormatError; - m_value = std::stoll(sanitizedValue); - } - return EResult::Success; -} - -minipp::EResult minipp::MiniPPFile::IntValue::ToString(std::string& destination) const noexcept -{ - destination = std::to_string(m_value); - return EResult::Success; -} - -int64_t minipp::MiniPPFile::IntValue::GetValue() const noexcept -{ - return m_value; -} diff --git a/minipp/minipp/minipp.hpp b/minipp/minipp/minipp.hpp index b5b08c9..44a1a39 100644 --- a/minipp/minipp/minipp.hpp +++ b/minipp/minipp/minipp.hpp @@ -6,83 +6,147 @@ #include #include +#define MINIPP_ENABLE_DEBUG_OUTPUT true + +// define MINIPP_IMPLEMENTATION in one single translation unit before including this header! + namespace minipp { enum class EResult { /* Errors */ - KeyNotPresent = -1, - KeyAlreadyPresent = -2, - SectionNotPresent = -3, - SectionAlreadyPresent = -4, - FileIOError = -5, - InvalidDataType = -6, - FormatError = -7, + KeyNotPresent = -1, + KeyAlreadyPresent = -2, + SectionNotPresent = -3, + SectionAlreadyPresent = -4, + FileIOError = -5, + InvalidDataType = -6, + FormatError = -7, + ArrayDataTypeInconsitency = -8, /* OK Codes*/ - Success = 1, - ValueOverwritten, - ValueOverwrittenAndDataTypeChanged + Success = +1, + ValueOverwritten = +2 }; + enum class EIntStyle + { + Decimal, + Hexadecimal, + Binary + }; + class MiniPPFile { - private: - class Tools - { - public: - static bool StringStartsWith(const std::string& str, const std::string& beg); - static bool StringEndsWith(const std::string& str, const std::string& end); - static void StringTrim(std::string& str); - static bool IsNameValid(const std::string& name) noexcept; - static int64_t FirstIndexOf(const std::string& str, char c) noexcept; - static int64_t LastIndexOf(const std::string& str, char c) noexcept; - static std::pair SplitInTwo(const std::string& str, int64_t firstLength) noexcept; - static std::vector SplitByDelimiter(const std::string& str, char delimiter) noexcept; - static bool IsResultOk(EResult result) noexcept; - static void RemoveAll(std::string& str, char old); - static bool IsIntegerDecimal(const std::string& str) noexcept; - }; - public: class Value { + friend class MiniPPFile; + + protected: + std::vector m_comments; + public: virtual EResult Parse(const std::string& str) noexcept = 0; virtual EResult ToString(std::string& destination) const noexcept = 0; + virtual ~Value() = default; + std::vector& GetComments() noexcept { return m_comments; } + const std::vector& GetComments() const noexcept { return m_comments; } }; - class StringValue : public Value + class Values { - private: - std::string m_value; - public: - StringValue() = default; - StringValue(const std::string& str); - EResult Parse(const std::string& str) noexcept override; - EResult ToString(std::string& destination) const noexcept override; - const std::string& GetValue() const noexcept; + class StringValue : public Value + { + private: + std::string m_value; + + public: + StringValue() = default; + StringValue(const std::string& str); + EResult Parse(const std::string& str) noexcept override; + EResult ToString(std::string& destination) const noexcept override; + const std::string& GetValue() const noexcept; + }; + + class IntValue : public Value + { + private: + int64_t m_value = 0; + EIntStyle m_style = EIntStyle::Decimal; + + public: + IntValue() = default; + IntValue(int64_t value); + EResult Parse(const std::string& str) noexcept override; + EResult ToString(std::string& destination) const noexcept override; + int64_t GetValue() const noexcept; + }; + + class BooleanValue : public Value + { + private: + bool m_value = false; + + public: + BooleanValue() = default; + BooleanValue(bool value); + EResult Parse(const std::string& str) noexcept override; + EResult ToString(std::string& destination) const noexcept override; + bool GetValue() const noexcept; + }; + + class FloatValue : public Value + { + private: + double m_value = 0.0f; + + public: + FloatValue() = default; + FloatValue(double value); + EResult Parse(const std::string& str) noexcept override; + EResult ToString(std::string& destination) const noexcept override; + double GetValue() const noexcept; + }; + + class ArrayValue : public Value + { + private: + std::vector> m_values; + + public: + ArrayValue() = default; + ArrayValue(const ArrayValue&) = delete; + ArrayValue& operator=(const ArrayValue&) = delete; + + public: + EResult Parse(const std::string& str) noexcept override; + EResult ToString(std::string& destination) const noexcept override; + std::vector>& GetValues() noexcept { return m_values; } + const std::vector>& GetValues() const noexcept { return m_values; } + + Value* operator[](size_t index) noexcept + { + if (index >= m_values.size()) + return nullptr; + return m_values[index].get(); + } + }; }; - - class IntValue : public Value - { - private: - int64_t m_value = 0; - - public: - IntValue() = default; - IntValue(int64_t value); - EResult Parse(const std::string& str) noexcept override; - EResult ToString(std::string& destination) const noexcept override; - int64_t GetValue() const noexcept; - }; - + class Section { + friend class MiniPPFile; + private: std::unordered_map m_values; std::unordered_map m_subSections; + std::vector m_comments; + + public: + std::vector& GetComments() noexcept { return m_comments; } + const std::vector& GetComments() const noexcept { return m_comments; } public: Section() = default; @@ -92,7 +156,7 @@ namespace minipp public: template - EResult GetValue(T** target, const std::string& key) noexcept + EResult GetValue(const std::string& key, T** target) noexcept { auto it = m_values.find(key); if (it == m_values.end()) @@ -107,7 +171,7 @@ namespace minipp } template - EResult SetValue(const std::string& name, const T& value, bool allowOverwrite = false) noexcept + EResult SetValue(const std::string& name, std::unique_ptr value, bool allowOverwrite = false) noexcept { if (m_values.find(name) != m_values.end()) if (!allowOverwrite) @@ -115,13 +179,13 @@ namespace minipp else delete m_values[name]; - m_values[name] = new T(value); + m_values[name] = value.release(); return EResult::Success; } public: - Section* GetSubSection(const std::string& key, EResult* result = nullptr) const noexcept; - Section* SetSubSection(const std::string& name, std::unique_ptr
value, bool allowOverwrite = false, EResult* result = nullptr) noexcept; + EResult GetSubSection(const std::string& key, Section** destination) const noexcept; + EResult SetSubSection(const std::string& name, std::unique_ptr
value, bool allowOverwrite = false) noexcept; }; public: @@ -130,12 +194,767 @@ namespace minipp private: Section m_rootSection{}; + private: + static std::unique_ptr ParseValue(std::string value); + static minipp::EResult WriteSection(const Section* section, std::ofstream& ofs) noexcept; + public: EResult Parse(const std::string& path) noexcept; - void Write(const std::string& path) noexcept; + void Write(const std::string& path) const noexcept; public: const Section& GetRoot() const noexcept { return m_rootSection; } Section& GetRoot() noexcept { return m_rootSection; } + + public: + static bool IsResultOk(EResult result) noexcept; + + private: + class Tools + { + public: + static bool StringStartsWith(const std::string& str, const std::string& beg); + static bool StringEndsWith(const std::string& str, const std::string& end); + static void StringTrim(std::string& str); + static bool IsNameValid(const std::string& name) noexcept; + static int64_t FirstIndexOf(const std::string& str, char c) noexcept; + static int64_t LastIndexOf(const std::string& str, char c) noexcept; + static std::pair SplitInTwo(const std::string& str, int64_t firstLength) noexcept; + static std::vector SplitByDelimiter(const std::string& str, char delimiter) noexcept; + static void RemoveAll(std::string& str, char old); + static bool IsIntegerDecimal(const std::string& str) noexcept; + }; }; -} \ No newline at end of file +} + +#ifdef MINIPP_IMPLEMENTATION + +#include +#include +#include +#include +#include + +#if MINIPP_ENABLE_DEBUG_OUTPUT +#include +#define COUT(msg) std::cout << "[minipp] " << msg << std::endl +#else +#define COUT(msg) +#endif + +#define ASSERT(condition) if (!condition) abort(); + +#define COUT_SYNTAX_ERROR(line, msg) COUT("Error in line " << line << ": " << msg) + +#pragma region Value Types + +minipp::MiniPPFile::Values::StringValue::StringValue(const std::string& str) : m_value(str) {} + +minipp::EResult minipp::MiniPPFile::Values::StringValue::Parse(const std::string& str) noexcept +{ + m_value = ""; + for (size_t i = 0; i < str.size(); ++i) + { + if (str[i] == '\\') + { + if (i + 1 >= str.size()) + return EResult::FormatError; + + switch (str[i + 1]) + { + case '\"': + m_value.push_back('\"'); + break; + case 'n': + m_value.push_back('\n'); + break; + case 't': + m_value.push_back('\t'); + break; + case 'r': + m_value.push_back('\r'); + break; + case '\\': + m_value.push_back('\\'); + break; + default: + return EResult::FormatError; + } + ++i; + } + else if (str[i] == '"') + return EResult::FormatError; + else + m_value.push_back(str[i]); + } + return EResult::Success; +} + +minipp::EResult minipp::MiniPPFile::Values::StringValue::ToString(std::string& destination) const noexcept +{ + std::string sanitizedValue = m_value; + for (size_t i = 0; i < sanitizedValue.size(); ++i) + { + switch (sanitizedValue[i]) + { + case '\n': + sanitizedValue.replace(i, 1, "\\n"); + i++; + break; + case '\t': + sanitizedValue.replace(i, 1, "\\t"); + i++; + break; + case '\r': + sanitizedValue.replace(i, 1, "\\r"); + i++; + break; + case '\\': + sanitizedValue.insert(i, 1, '\\'); + i++; + break; + case '\"': + sanitizedValue.insert(i, 1, '\\'); + i++; + break; + } + } + destination = "\"" + sanitizedValue + "\""; + return EResult::Success; +} + +const std::string& minipp::MiniPPFile::Values::StringValue::GetValue() const noexcept +{ + return m_value; +} + +minipp::MiniPPFile::Values::IntValue::IntValue(int64_t value) : m_value(value) {} + +minipp::EResult minipp::MiniPPFile::Values::IntValue::Parse(const std::string& str) noexcept +{ + std::string sanitizedValue = str; + Tools::RemoveAll(sanitizedValue, '_'); + if (sanitizedValue.empty()) + return EResult::FormatError; + + char lastCharacter = str.back(); + auto rest = str.substr(0, str.size() - 1); + if (lastCharacter == 'h') + { + m_value = std::stoll(rest, nullptr, 16); + m_style = EIntStyle::Hexadecimal; + } + else if (lastCharacter == 'b') + { + m_value = std::stoll(rest, nullptr, 2); + m_style = EIntStyle::Binary; + } + else + { + if (!Tools::IsIntegerDecimal(sanitizedValue)) + return EResult::FormatError; + m_value = std::stoll(sanitizedValue); + m_style = EIntStyle::Decimal; + } + return EResult::Success; +} + +minipp::EResult minipp::MiniPPFile::Values::IntValue::ToString(std::string& destination) const noexcept +{ + switch (m_style) + { + case EIntStyle::Decimal: + { + destination = std::to_string(m_value); + break; + } + case EIntStyle::Hexadecimal: + { + std::stringstream ss; + ss << std::hex << m_value << "h"; + destination = ss.str(); + break; + } + case EIntStyle::Binary: + { + std::bitset<64> bs(m_value); + destination = bs.to_string() + "b"; + size_t i; + for (i = 0; i < destination.size(); ++i) + if (destination[i] == '1') + { + destination = destination.substr(i); + break; + } + if (i == destination.size()) + destination = "0b"; + break; + } + default: + return EResult::FormatError; + } + + return EResult::Success; +} + +int64_t minipp::MiniPPFile::Values::IntValue::GetValue() const noexcept +{ + return m_value; +} + +minipp::MiniPPFile::Values::BooleanValue::BooleanValue(bool value) : m_value(value) {} + +minipp::EResult minipp::MiniPPFile::Values::BooleanValue::Parse(const std::string& str) noexcept +{ + if (str == "true") + m_value = true; + else if (str == "false") + m_value = false; + else + return EResult::FormatError; + return EResult::Success; +} + +minipp::EResult minipp::MiniPPFile::Values::BooleanValue::ToString(std::string& destination) const noexcept +{ + destination = m_value ? "true" : "false"; + return EResult::Success; +} + +bool minipp::MiniPPFile::Values::BooleanValue::GetValue() const noexcept +{ + return m_value; +} + +minipp::MiniPPFile::Values::FloatValue::FloatValue(double value) : m_value(value) {} + +minipp::EResult minipp::MiniPPFile::Values::FloatValue::Parse(const std::string& str) noexcept +{ + m_value = std::stod(str); + return EResult::Success; +} + +minipp::EResult minipp::MiniPPFile::Values::FloatValue::ToString(std::string& destination) const noexcept +{ + destination = std::to_string(m_value); + return EResult::Success; +} + +double minipp::MiniPPFile::Values::FloatValue::GetValue() const noexcept +{ + return m_value; +} + +minipp::EResult minipp::MiniPPFile::Values::ArrayValue::Parse(const std::string& str) noexcept +{ + std::string nValue = str; + if (str.front() != '[' || str.back() != ']') + return EResult::FormatError; + nValue = str.substr(1, str.size() - 2); + if (nValue.empty()) + return EResult::Success; + + int64_t bracketCounter = 0; + bool isInString = false; + + std::vector elements; + std::string currentElement; + + for (size_t i = 0; i < str.size(); ++i) + { + char c = str[i]; + + if (isInString) + { + if (c == '\\') + ++i; + else if (c == '"') + { + isInString = false; + currentElement += c; + } + else + currentElement += c; + } + else if (c == '\\') + ++i; + else if (c == '"') + { + currentElement += c; + isInString = true; + } + else + { + if (c == '[') + { + if (++bracketCounter > 1) + currentElement += c; + } + else if (c == ']') + { + --bracketCounter; + if (bracketCounter < 0) + return EResult::FormatError; + else if (bracketCounter >= 1) + currentElement += c; + } + else if (c == ',' && bracketCounter == 1) + { + elements.push_back(currentElement); + currentElement = ""; + } + else if (c != ' ' && c != '\t') + currentElement += c; + } + } + + if (bracketCounter != 0) + return EResult::FormatError; + + if (!currentElement.empty()) + elements.push_back(currentElement); + size_t lastTypeIdHash = 0; + bool hasTypeHash = false; + + for (auto& elem : elements) + { + auto parsed = ParseValue(elem); + if (parsed == nullptr) + return EResult::FormatError; + + if (!hasTypeHash) + { + lastTypeIdHash = typeid(*parsed).hash_code(); + hasTypeHash = true; + } + else if (typeid(*parsed).hash_code() != lastTypeIdHash) + return EResult::FormatError; + + m_values.push_back(std::move(parsed)); + } + return EResult::Success; +} + +minipp::EResult minipp::MiniPPFile::Values::ArrayValue::ToString(std::string& destination) const noexcept +{ + std::string buf; + size_t lastTypeIdHash = 0; + bool hasTypeHash = false; + + std::stringstream ss; + for (const auto& val : m_values) + { + EResult result = val->ToString(buf); + if (!IsResultOk(result)) + return result; + + if (!hasTypeHash) + { + lastTypeIdHash = typeid(*val).hash_code(); + hasTypeHash = true; + } + else if (typeid(*val).hash_code() != lastTypeIdHash) + return EResult::ArrayDataTypeInconsitency; + + ss << buf << ", "; + } + std::string valueString = ss.str(); + if (!valueString.empty()) + valueString = valueString.substr(0, valueString.size() - 2); + destination = "[" + valueString + "]"; + return EResult::Success; +} + +#pragma endregion + +minipp::MiniPPFile::Section::~Section() +{ + for (auto& pair : m_values) + delete pair.second; + for (auto& pair : m_subSections) + delete pair.second; +} + +minipp::EResult minipp::MiniPPFile::Section::GetSubSection(const std::string& key, Section** destination) const noexcept +{ + auto pathIndex = key.find('.'); + std::string thisKey = key; + std::string rest; + if (pathIndex != std::string::npos) + { + thisKey = key.substr(0, pathIndex); + rest = key.substr(pathIndex + 1); + } + + auto it = m_subSections.find(thisKey); + if (it == m_subSections.end()) + { + COUT("Sub-Section not found: " << thisKey); + return EResult::SectionNotPresent; + } + if (rest.empty()) + { + *destination = it->second; + return EResult::Success; + } + + return it->second->GetSubSection(rest, destination); +} + +minipp::EResult minipp::MiniPPFile::Section::SetSubSection(const std::string& name, std::unique_ptr
value, bool allowOverwrite) noexcept +{ + if (m_subSections.find(name) != m_subSections.end()) + if (!allowOverwrite) + return EResult::SectionAlreadyPresent; + else + delete m_subSections[name]; + + m_subSections[name] = value.release(); + return EResult::Success; +} + +std::unique_ptr minipp::MiniPPFile::ParseValue(std::string value) +{ + char valueFirstChar = value.front(); + char valueLastChar = value.back(); + if (valueFirstChar == '"') + { + if (valueLastChar != '"') + return nullptr; + + // is string + value = value.substr(1, value.size() - 2); + auto strValue = std::make_unique(); + auto parseResult = strValue->Parse(value); + if (!IsResultOk(parseResult)) + return nullptr; + + return strValue; + } + else if (valueLastChar == 'e') + { + auto boolValue = std::make_unique(); + auto parseResult = boolValue->Parse(value); + if (!IsResultOk(parseResult)) + return nullptr; + + return boolValue; + } + else if (valueLastChar == 'f') + { + auto floatValue = std::make_unique(); + auto parseResult = floatValue->Parse(value); + if (!IsResultOk(parseResult)) + return nullptr; + + return floatValue; + } + else if (valueLastChar == ']') + { + auto arrayValue = std::make_unique(); + auto parseResult = arrayValue->Parse(value); + if (!IsResultOk(parseResult)) + return nullptr; + + return arrayValue; + } + else + { + auto intValue = std::make_unique(); + auto parseResult = intValue->Parse(value); + if (!IsResultOk(parseResult)) + return nullptr; + + return intValue; + } +} + +minipp::EResult minipp::MiniPPFile::WriteSection(const Section* section, std::ofstream& ofs) noexcept +{ + if (section->m_values.size() > 0) + { + for (const auto& pair : section->m_values) + { + if (!Tools::IsNameValid(pair.first)) + { + COUT("Invalid name for key: " << pair.first); + return EResult::FormatError; + } + + for (const auto& comment : pair.second->m_comments) + ofs << comment << std::endl; + + ofs << pair.first << " = "; + std::string valueString; + auto result = pair.second->ToString(valueString); + if (!IsResultOk(result)) + return result; + ofs << valueString << std::endl; + } + ofs << std::endl; + } + + for (const auto& pair : section->m_subSections) + { + if (!Tools::IsNameValid(pair.first)) + { + COUT("Invalid name for section: " << pair.first); + return EResult::FormatError; + } + + for (const auto& comment : pair.second->m_comments) + ofs << comment << std::endl; + + ofs << "[" << pair.first << "]" << std::endl; + auto result = WriteSection(pair.second, ofs); + if (!IsResultOk(result)) + return result; + } + + return EResult::Success; +} + +minipp::EResult minipp::MiniPPFile::Parse(const std::string& path) noexcept +{ + std::ifstream ifs; + ifs.open(path); + if (!ifs.is_open()) + return EResult::FileIOError; + + int64_t lineCounter = 0; + Section* currentSection = nullptr; + + std::vector commentBuffer; + + std::string currentLine; + while (std::getline(ifs, currentLine)) + { + ++lineCounter; + Tools::StringTrim(currentLine); + if (currentLine.empty()) + continue; + char firstChar = currentLine[0]; + char lastChar = currentLine[currentLine.size() - 1]; + + if (firstChar == '#') + { + commentBuffer.push_back(currentLine); + continue; + } + + if (firstChar == '[') + { + if (lastChar != ']') + { + COUT_SYNTAX_ERROR(lineCounter, "Expected ']' at the end of the line."); + return EResult::FormatError; + } + std::string sectionName = currentLine.substr(1, currentLine.size() - 2); + Tools::StringTrim(sectionName); + if (sectionName.empty()) + { + COUT_SYNTAX_ERROR(lineCounter, "Expected section path. Found empty section begin notation."); + return EResult::FormatError; + } + // Create section tree + Section* ubSection = &m_rootSection; + + std::vector sectionPath = Tools::SplitByDelimiter(sectionName, '.'); + for (size_t i = 0; i < sectionPath.size(); ++i) + { + const std::string& sectionName = sectionPath[i]; + if (!Tools::IsNameValid(sectionName)) + { + COUT_SYNTAX_ERROR(lineCounter, "Invalid section name. (\"" << sectionName << "\") May only contain [a - z][A - Z][0 - 9] and _."); + return EResult::FormatError; + } + + EResult result; + result = ubSection->SetSubSection(sectionName, std::make_unique
(), false); + if (result == EResult::SectionAlreadyPresent && i == sectionPath.size() - 1) + { + COUT_SYNTAX_ERROR(lineCounter, "All (sub-) sections may only be defined once."); + return EResult::FormatError; + } + result = ubSection->GetSubSection(sectionName, &ubSection); + ASSERT(IsResultOk(result)); // should never happen + } + currentSection = ubSection; + currentSection->m_comments = commentBuffer; + commentBuffer.clear(); + continue; + } + if (currentSection == nullptr) + { + COUT_SYNTAX_ERROR(lineCounter, "Expected section begin before key-value pair."); + return EResult::FormatError; + } + + int64_t keyValueDelimiterIndex = Tools::FirstIndexOf(currentLine, '='); + if (keyValueDelimiterIndex == -1) + { + COUT_SYNTAX_ERROR(lineCounter, "Expected '=' in line."); + return EResult::FormatError; + } + + auto keyValuePair = Tools::SplitInTwo(currentLine, keyValueDelimiterIndex); + Tools::StringTrim(keyValuePair.first); + Tools::StringTrim(keyValuePair.second); + if (keyValuePair.first.empty()) + { + COUT_SYNTAX_ERROR(lineCounter, "Expected key in line."); + return EResult::FormatError; + } + if (keyValuePair.second.empty()) + { + COUT_SYNTAX_ERROR(lineCounter, "Empty keys are not allowed"); + return EResult::FormatError; + } + auto parsedValue = ParseValue(keyValuePair.second); + if (parsedValue == nullptr) + { + COUT_SYNTAX_ERROR(lineCounter, "Invalid value"); + return EResult::FormatError; + } + parsedValue->m_comments = commentBuffer; + commentBuffer.clear(); + + currentSection->SetValue(keyValuePair.first, std::move(parsedValue), false); + } + + return EResult::Success; +} + +void minipp::MiniPPFile::Write(const std::string& path) const noexcept +{ + std::ofstream ofs; + ofs.open(path); + if (!ofs.is_open()) + return; + + WriteSection(&m_rootSection, ofs); +} + +bool minipp::MiniPPFile::IsResultOk(EResult result) noexcept +{ + return static_cast(result) > 0; +} + +#pragma region Tools +bool minipp::MiniPPFile::Tools::StringStartsWith(const std::string& str, const std::string& beg) +{ + if (str.size() < beg.size()) + return false; + + for (size_t i = 0; i < beg.size(); ++i) + if (str[i] != beg[i]) + return false; + return true; +} + +bool minipp::MiniPPFile::Tools::StringEndsWith(const std::string& str, const std::string& end) +{ + if (str.size() < end.size()) + return false; + + for (size_t i = 0; i < end.size(); ++i) + if (str[str.size() - i - 1] != end[end.size() - i - 1]) + return false; + return true; +} + +void minipp::MiniPPFile::Tools::StringTrim(std::string& str) +{ + if (str.empty()) + return; + + size_t start = 0; + size_t end = str.size() - 1; + while (str[start] == ' ' || str[start] == '\t') + start++; + while (str[end] == ' ' || str[end] == '\t') + end--; + + str = str.substr(start, end - start + 1); +} + +bool minipp::MiniPPFile::Tools::IsNameValid(const std::string& name) noexcept +{ + for (const char c : name) + { + if ( + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + (c == '_')) + continue; + return false; + } + return true; +} + +int64_t minipp::MiniPPFile::Tools::FirstIndexOf(const std::string& str, char c) noexcept +{ + int64_t index = 0; + for (const char ch : str) + { + if (ch == c) + return index; + ++index; + } + return -1; +} + +int64_t minipp::MiniPPFile::Tools::LastIndexOf(const std::string& str, char c) noexcept +{ + int64_t index = str.size() - 1; + for (int64_t i = str.size() - 1; i >= 0; --i) + { + if (str[i] == c) + return index; + --index; + } + return -1; +} + +std::pair minipp::MiniPPFile::Tools::SplitInTwo(const std::string& str, int64_t firstLength) noexcept +{ + return std::make_pair(str.substr(0, firstLength), str.substr(firstLength + 1)); +} + +std::vector minipp::MiniPPFile::Tools::SplitByDelimiter(const std::string& str, char delimiter) noexcept +{ + std::vector elements; + std::string tmp; + + for (const char c : str) + { + if (c == delimiter) + { + elements.push_back(tmp); + tmp.clear(); + } + else + tmp.push_back(c); + } + + if (!tmp.empty()) + elements.push_back(tmp); + return elements; +} + +void minipp::MiniPPFile::Tools::RemoveAll(std::string& str, char old) +{ + str.erase(std::remove(str.begin(), str.end(), old), str.end()); +} + +bool minipp::MiniPPFile::Tools::IsIntegerDecimal(const std::string& str) noexcept +{ + for (size_t i = 0; i < str.size(); ++i) + if (str[i] < '0' || str[i] > '9') + return false; + return true; +} + +#pragma endregion +#endif \ No newline at end of file diff --git a/minipp/test.cpp b/minipp/test.cpp index 7fbda9c..c19a471 100644 --- a/minipp/test.cpp +++ b/minipp/test.cpp @@ -1,22 +1,45 @@ +#define MINIPP_IMPLEMENTATION #include "minipp/minipp.hpp" +using namespace minipp; + int main() { - minipp::MiniPPFile file; - auto result = file.Parse("test.mini"); + EResult result; - auto& section = file.GetRoot(); - - minipp::MiniPPFile::Section* profileSection = section.GetSubSection("profile", &result); + MiniPPFile file; + result = file.Parse("test.mini"); - minipp::MiniPPFile::IntValue* value; - auto result1 = profileSection->GetValue(&value, "window_width"); + auto& root = file.GetRoot(); - auto result2 = value->GetValue(); + MiniPPFile::Section* gameSection = nullptr; + result = root.GetSubSection("game", &gameSection); + MiniPPFile::Values::StringValue* nameValue = nullptr; + result = gameSection->GetValue("name", &nameValue); + MiniPPFile::Values::IntValue* yearValue = nullptr; + result = gameSection->GetValue("year", &yearValue); + MiniPPFile::Values::FloatValue* completionPercentage = nullptr; + result = gameSection->GetValue("completionPercentage", &completionPercentage); + MiniPPFile::Values::BooleanValue* isCompleted = nullptr; + result = gameSection->GetValue("is_completed", &isCompleted); - auto marianProfile = section.GetSubSection("profile.marian", &result); - minipp::MiniPPFile::StringValue* dockOutlinerType; - marianProfile->GetValue(&dockOutlinerType, "dock_outliner"); + MiniPPFile::Section* windowSection = nullptr; + result = gameSection->GetSubSection("window", &windowSection); + MiniPPFile::Values::ArrayValue* dimensionsValue = nullptr; + result = windowSection->GetValue("dimensions", &dimensionsValue); + MiniPPFile::Values::IntValue* closeFlags = nullptr; + result = windowSection->GetValue("close_flags", &closeFlags); + MiniPPFile::Values::StringValue* hexTest = nullptr; + result = windowSection->GetValue("hex_test", &hexTest); + + MiniPPFile::Section* windowPlatformSection = nullptr; + result = gameSection->GetSubSection("window.platform", &windowPlatformSection); + MiniPPFile::Values::ArrayValue* targetsValue = nullptr; + result = windowPlatformSection->GetValue("targets", &targetsValue); + MiniPPFile::Values::ArrayValue* pointsValue = nullptr; + result = windowPlatformSection->GetValue("points", &pointsValue); + + file.Write("test_out.mini"); return 0; } \ No newline at end of file diff --git a/minipp/test.mini b/minipp/test.mini index f03c0d5..7c63239 100644 --- a/minipp/test.mini +++ b/minipp/test.mini @@ -1,10 +1,18 @@ -[profile] -dock_outliner="left" -window_width=1000000b -window_height=fh +[game] +name = "Test Game" +version = "1.0.0" +year = 2025 +completionPercentage = 50.0f +# Should only be true if completionPercentage is 100 +is_completed = false -[profile.marian] -dock_outliner="ri\\ght\\" +# This section is about +# the settings of a game window +[game.window] +dimensions = [1280, 720] +close_flags = 101011b +hex_test = DEADBEEFh -[profile.toyb] -dock_outliner="top" \ No newline at end of file +[game.window.platform] +targets = ["win32", "macOS", "linux"] +points = [[0, 0], [1, 0], [1, 1], [0, 1]] \ No newline at end of file diff --git a/minipp/test_out.mini b/minipp/test_out.mini new file mode 100644 index 0000000..8e63016 --- /dev/null +++ b/minipp/test_out.mini @@ -0,0 +1,19 @@ +[game] +name = "Test Game" +completionPercentage = 50.000000 +version = "1.0.0" +year = 2025 +# Should only be true if completionPercentage is 100 +is_completed = false + +# This section is about +# the settings of a game window +[window] +dimensions = [1280, 720] +close_flags = 101011b +hex_test = deadbeefh + +[platform] +targets = ["win32", "macOS", "linux"] +points = [[0, 0], [1, 0], [1, 1], [0, 1]] +