From e15526ee9117a087075d428e44eaf89a11b25923 Mon Sep 17 00:00:00 2001 From: vaxerski Date: Thu, 28 Dec 2023 20:38:01 +0100 Subject: [PATCH] initial commit parses variables. Progress, I guess. --- .clang-format | 65 +++++++++ .gitignore | 2 + CMakeLists.txt | 22 ++++ include/hyprlang.hpp | 10 ++ src/common.cpp | 98 ++++++++++++++ src/config.cpp | 276 +++++++++++++++++++++++++++++++++++++++ src/config.hpp | 17 +++ src/core.hpp | 1 + src/logger.hpp | 3 + src/public.hpp | 103 +++++++++++++++ tests/config/config.conf | 26 ++++ tests/main.cpp | 58 ++++++++ 12 files changed, 681 insertions(+) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 include/hyprlang.hpp create mode 100644 src/common.cpp create mode 100644 src/config.cpp create mode 100644 src/config.hpp create mode 100644 src/core.hpp create mode 100644 src/logger.hpp create mode 100644 src/public.hpp create mode 100644 tests/config/config.conf create mode 100644 tests/main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +ColumnLimit: 180 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentWidth: 4 +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never + +AllowShortEnumsOnASingleLine: false + +BraceWrapping: + AfterEnum: false + +AlignConsecutiveDeclarations: AcrossEmptyLines + +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore index 46f42f8..a24f382 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps +.vscode +build/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d6a85f0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.19) +project(hyprlang + VERSION "0.1" + DESCRIPTION "A library to parse hypr config files" +) + +include(CTest) + +set(CMAKE_CXX_STANDARD 23) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/hyprlang.hpp") + +add_library(hyprlang ${SRCFILES}) +target_include_directories( hyprlang + PUBLIC "./include" + PRIVATE "./src" +) + + +add_executable(hyprlang_test "tests/main.cpp") +target_link_libraries(hyprlang_test PRIVATE hyprlang) +add_test(NAME "Parsing" WORKING_DIRECTORY "./tests/" COMMAND hyprlang_test "parse") diff --git a/include/hyprlang.hpp b/include/hyprlang.hpp new file mode 100644 index 0000000..52f926e --- /dev/null +++ b/include/hyprlang.hpp @@ -0,0 +1,10 @@ +#pragma once + +#ifndef HYPRLANG_HPP +#define HYPRLANG_HPP + +#include "../src/public.hpp" + +namespace Hyprlang {}; + +#endif \ No newline at end of file diff --git a/src/common.cpp b/src/common.cpp new file mode 100644 index 0000000..837ad7a --- /dev/null +++ b/src/common.cpp @@ -0,0 +1,98 @@ +#include "public.hpp" +#include + +using namespace Hyprlang; + +void CParseResult::setError(const std::string& err) { + error = true; + errorStdString = err; + errorString = errorStdString.c_str(); +} + +void CParseResult::setError(const char* err) { + error = true; + errorStdString = err; + errorString = errorStdString.c_str(); +} + +CConfigValue::~CConfigValue() { + if (m_pData) + free(m_pData); +} + +CConfigValue::CConfigValue(const int64_t value) { + m_pData = calloc(1, sizeof(int64_t)); + *reinterpret_cast(m_pData) = value; + m_eType = CONFIGDATATYPE_INT; +} + +CConfigValue::CConfigValue(const float value) { + m_pData = calloc(1, sizeof(float)); + *reinterpret_cast(m_pData) = value; + m_eType = CONFIGDATATYPE_FLOAT; +} + +CConfigValue::CConfigValue(const char* value) { + m_pData = calloc(1, strlen(value) + 1); + strncpy((char*)m_pData, value, strlen(value)); + m_eType = CONFIGDATATYPE_STR; +} + +CConfigValue::CConfigValue(const CConfigValue& ref) { + m_eType = ref.m_eType; + switch (ref.m_eType) { + case eDataType::CONFIGDATATYPE_INT: { + m_pData = calloc(1, sizeof(int64_t)); + *reinterpret_cast(m_pData) = std::any_cast(ref.getValue()); + break; + } + case eDataType::CONFIGDATATYPE_FLOAT: { + m_pData = calloc(1, sizeof(float)); + *reinterpret_cast(m_pData) = std::any_cast(ref.getValue()); + break; + } + case eDataType::CONFIGDATATYPE_STR: { + auto str = std::any_cast(ref.getValue()); + m_pData = calloc(1, strlen(str) + 1); + strncpy((char*)m_pData, str, strlen(str)); + break; + } + } +} + +void CConfigValue::operator=(const CConfigValue& ref) { + m_eType = ref.m_eType; + switch (ref.m_eType) { + case eDataType::CONFIGDATATYPE_INT: { + m_pData = calloc(1, sizeof(int64_t)); + *reinterpret_cast(m_pData) = std::any_cast(ref.getValue()); + break; + } + case eDataType::CONFIGDATATYPE_FLOAT: { + m_pData = calloc(1, sizeof(float)); + *reinterpret_cast(m_pData) = std::any_cast(ref.getValue()); + break; + } + case eDataType::CONFIGDATATYPE_STR: { + auto str = std::any_cast(ref.getValue()); + m_pData = calloc(1, strlen(str) + 1); + strncpy((char*)m_pData, str, strlen(str)); + break; + } + } +} + +CConfigValue::CConfigValue(CConfigValue&& ref) { + m_pData = ref.dataPtr(); + m_eType = ref.m_eType; + ref.m_eType = eDataType::CONFIGDATATYPE_EMPTY; + ref.m_pData = nullptr; +} + +CConfigValue::CConfigValue() { + ; +} + +void* CConfigValue::dataPtr() const { + return m_pData; +} diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..591ad93 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,276 @@ +#include "config.hpp" +#include +#include +#include +#include + +using namespace Hyprlang; + +static std::string removeBeginEndSpacesTabs(std::string str) { + if (str.empty()) + return str; + + int countBefore = 0; + while (str[countBefore] == ' ' || str[countBefore] == '\t') { + countBefore++; + } + + int countAfter = 0; + while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) { + countAfter++; + } + + str = str.substr(countBefore, str.length() - countBefore - countAfter); + + return str; +} + +CConfig::CConfig(const char* path) { + impl = new CConfigImpl; + try { + impl->path = std::filesystem::canonical(path); + } catch (std::exception& e) { throw "Couldn't open file. File does not exist"; } + + if (!std::filesystem::exists(impl->path)) + throw "File does not exist"; +} + +CConfig::~CConfig() { + delete impl; +} + +void CConfig::addConfigValue(const char* name, const CConfigValue value) { + if (m_bCommenced) + throw "Cannot addConfigValue after commence()"; + + impl->defaultValues[std::string{name}] = value; +} + +void CConfig::commence() { + m_bCommenced = true; + for (auto& [k, v] : impl->defaultValues) { + impl->values[k] = v; + } +} + +bool isNumber(const std::string& str, bool allowfloat) { + + std::string copy = str; + if (*copy.begin() == '-') + copy = copy.substr(1); + + if (copy.empty()) + return false; + + bool point = !allowfloat; + for (auto& c : copy) { + if (c == '.') { + if (point) + return false; + point = true; + continue; + } + + if (!std::isdigit(c)) + return false; + } + + return true; +} + +int64_t configStringToInt(const std::string& VALUE) { + if (VALUE.starts_with("0x")) { + // Values with 0x are hex + const auto VALUEWITHOUTHEX = VALUE.substr(2); + return stol(VALUEWITHOUTHEX, nullptr, 16); + } else if (VALUE.starts_with("rgba(") && VALUE.ends_with(')')) { + const auto VALUEWITHOUTFUNC = VALUE.substr(5, VALUE.length() - 6); + + if (removeBeginEndSpacesTabs(VALUEWITHOUTFUNC).length() != 8) { + throw std::invalid_argument("rgba() expects length of 8 characters (4 bytes)"); + } + + const auto RGBA = std::stol(VALUEWITHOUTFUNC, nullptr, 16); + + // now we need to RGBA -> ARGB. The config holds ARGB only. + return (RGBA >> 8) + 0x1000000 * (RGBA & 0xFF); + } else if (VALUE.starts_with("rgb(") && VALUE.ends_with(')')) { + const auto VALUEWITHOUTFUNC = VALUE.substr(4, VALUE.length() - 5); + + if (removeBeginEndSpacesTabs(VALUEWITHOUTFUNC).length() != 6) { + throw std::invalid_argument("rgb() expects length of 6 characters (3 bytes)"); + } + + const auto RGB = std::stol(VALUEWITHOUTFUNC, nullptr, 16); + + return RGB + 0xFF000000; // 0xFF for opaque + } else if (VALUE.starts_with("true") || VALUE.starts_with("on") || VALUE.starts_with("yes")) { + return 1; + } else if (VALUE.starts_with("false") || VALUE.starts_with("off") || VALUE.starts_with("no")) { + return 0; + } + + if (VALUE.empty() || !isNumber(VALUE, false)) + return 0; + + return std::stoll(VALUE); +} + +CParseResult CConfig::configSetValueSafe(const std::string& command, const std::string& value) { + CParseResult result; + + std::string valueName; + for (auto& c : impl->categories) { + valueName += c + ':'; + } + + valueName += command; + + const auto VALUEIT = impl->values.find(valueName); + if (VALUEIT == impl->values.end()) { + result.setError("config option doesn't exist"); + return result; + } + + switch (VALUEIT->second.m_eType) { + case CConfigValue::eDataType::CONFIGDATATYPE_INT: { + try { + VALUEIT->second = {configStringToInt(value)}; + } catch (std::exception& e) { + result.setError(std::format("failed parsing an int: {}", e.what())); + return result; + } + break; + } + case CConfigValue::eDataType::CONFIGDATATYPE_FLOAT: { + try { + VALUEIT->second = {std::stof(value)}; + } catch (std::exception& e) { + result.setError(std::format("failed parsing a float: {}", e.what())); + return result; + } + break; + } + case CConfigValue::eDataType::CONFIGDATATYPE_STR: { + VALUEIT->second = {value.c_str()}; + break; + } + default: { + result.setError("internal error: invalid value found (no type?)"); + return result; + } + } + + return result; +} + +CParseResult CConfig::parseLine(std::string line) { + CParseResult result; + + auto commentPos = line.find('#'); + size_t lastHashPos = 0; + + while (commentPos != std::string::npos) { + bool escaped = false; + if (commentPos < line.length() - 1) { + if (line[commentPos + 1] == '#') { + lastHashPos = commentPos + 2; + escaped = true; + } + } + + if (!escaped) { + line = line.substr(0, commentPos); + break; + } else { + commentPos = line.find('#', lastHashPos); + } + } + + line = removeBeginEndSpacesTabs(line); + + auto equalsPos = line.find('='); + + if (equalsPos == std::string::npos && !line.ends_with("{") && line != "}" && !line.empty()) { + // invalid line + result.setError("Invalid config line"); + return result; + } + + if (equalsPos != std::string::npos) { + // set value + CParseResult ret = configSetValueSafe(removeBeginEndSpacesTabs(line.substr(0, equalsPos)), removeBeginEndSpacesTabs(line.substr(equalsPos + 1))); + if (ret.error) { + return ret; + } + } else if (!line.empty()) { + // has to be a set + if (line.contains("}")) { + // easiest. } or invalid. + if (line != "}") { + result.setError("Invalid config line"); + return result; + } + + if (impl->categories.empty()) { + result.setError("Stray category close"); + return result; + } + + impl->categories.pop_back(); + } else { + // open a category. + if (!line.ends_with("{")) { + result.setError("Invalid category open, garbage after {"); + return result; + } + + line.pop_back(); + line = removeBeginEndSpacesTabs(line); + impl->categories.push_back(line); + } + } + + return result; +} + +CParseResult CConfig::parse() { + if (!m_bCommenced) + throw "Cannot parse: not commenced. You have to .commence() first."; + + impl->parseError = ""; + + for (auto& [k, v] : impl->defaultValues) { + impl->values.at(k) = v; + } + + std::ifstream iffile(impl->path); + if (!iffile.good()) + throw "Config file failed to open"; + + std::string line = ""; + int linenum = 1; + + CParseResult fileParseResult; + + while (std::getline(iffile, line)) { + + const auto RET = parseLine(line); + + if (RET.error && impl->parseError.empty()) { + impl->parseError = RET.getError(); + fileParseResult.setError(std::format("Config error at line {}: {}", linenum, RET.errorStdString)); + } + + ++linenum; + } + + iffile.close(); + + return fileParseResult; +} + +CConfigValue* CConfig::getConfigValuePtr(const char* name) { + const auto IT = impl->values.find(std::string{name}); + return IT == impl->values.end() ? nullptr : &IT->second; +} diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 0000000..e117386 --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,17 @@ +#include "public.hpp" + +#include +#include +#include + +class CConfigImpl { + public: + std::string path = ""; + + std::unordered_map values; + std::unordered_map defaultValues; + + std::vector categories; + + std::string parseError = ""; +}; \ No newline at end of file diff --git a/src/core.hpp b/src/core.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/src/core.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/logger.hpp b/src/logger.hpp new file mode 100644 index 0000000..43454bb --- /dev/null +++ b/src/logger.hpp @@ -0,0 +1,3 @@ +#pragma once + +namespace Logger {} \ No newline at end of file diff --git a/src/public.hpp b/src/public.hpp new file mode 100644 index 0000000..bfe15b6 --- /dev/null +++ b/src/public.hpp @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include + +class CConfigImpl; + +namespace Hyprlang { + + struct SConfigValueImpl; + /* Container for a config value */ + class CConfigValue { + public: + CConfigValue(); + CConfigValue(const int64_t value); + CConfigValue(const float value); + CConfigValue(const char* value); + CConfigValue(const CConfigValue&); + CConfigValue(CConfigValue&&); + void operator=(const CConfigValue&); + ~CConfigValue(); + + void* dataPtr() const; + std::any getValue() const { + switch (m_eType) { + case CONFIGDATATYPE_EMPTY: throw; + case CONFIGDATATYPE_INT: return std::any(*reinterpret_cast(m_pData)); + case CONFIGDATATYPE_FLOAT: return std::any(*reinterpret_cast(m_pData)); + case CONFIGDATATYPE_STR: return std::any(reinterpret_cast(m_pData)); + default: throw; + } + return {}; // unreachable + } + + private: + enum eDataType { + CONFIGDATATYPE_EMPTY, + CONFIGDATATYPE_INT, + CONFIGDATATYPE_FLOAT, + CONFIGDATATYPE_STR, + }; + eDataType m_eType = eDataType::CONFIGDATATYPE_EMPTY; + void* m_pData = nullptr; + + friend class CConfig; + }; + + class CParseResult { + public: + bool error = false; + const char* getError() const { + return errorString; + } + void setError(const char* err); + + private: + void setError(const std::string& err); + + std::string errorStdString = ""; + const char* errorString = nullptr; + + friend class CConfig; + }; + + /* Base class for a config file */ + class CConfig { + public: + CConfig(const char* configPath); + ~CConfig(); + + /* Add a config value, for example myCategory:myValue. + This has to be done before commence() + Value provided becomes default */ + void addConfigValue(const char* name, const CConfigValue value); + + /* Commence the config state. Config becomes immutable, as in + no new values may be added or removed. Required for parsing. */ + void commence(); + + /* Parse the config. Refresh the values. */ + CParseResult parse(); + + /* Get a config's value ptr. These are static. + nullptr on fail */ + CConfigValue* getConfigValuePtr(const char* name); + + /* Get a config value's stored value. Empty on fail*/ + std::any getConfigValue(const char* name) { + CConfigValue* val = getConfigValuePtr(name); + if (!val) + return {}; + return val->getValue(); + } + + private: + bool m_bCommenced = false; + + CConfigImpl* impl; + + CParseResult parseLine(std::string line); + CParseResult configSetValueSafe(const std::string& command, const std::string& value); + }; +}; \ No newline at end of file diff --git a/tests/config/config.conf b/tests/config/config.conf new file mode 100644 index 0000000..f63fe0d --- /dev/null +++ b/tests/config/config.conf @@ -0,0 +1,26 @@ + +# Test comment + +testInt = 123 +testFloat = 123.456 +testString = Hello World! ## This is not a comment! # This is! + +testCategory { + testValueInt = 123456 + testValueHex = 0xFFFFaabb + + nested1 { + testValueNest = 1 + nested2 { + testValueNest = 1 + } + } +} + +testStringQuotes = "Hello World!" +#testDefault = 123 + +#doABarrelRoll = woohoo, some, params # Funny! +#doSomethingFunny = 1, 2, 3, 4 # Funnier! +#testSpaces = abc , def # many spaces, should be trimmed + diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..f55bd1e --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,58 @@ +#include + +#include + +#define EXPECT(expr, val) \ + if (const auto RESULT = expr; RESULT != (val)) { \ + std::cout << "Failed: " << #expr << ", expected " << #val << " but got " << RESULT << "\n"; \ + ret = 1; \ + } else { \ + std::cout << "Passed " << #expr << ". Got " << #val << "\n"; \ + } + +int main(int argc, char** argv, char** envp) { + int ret = 0; + + try { + std::cout << "Starting test\n"; + + Hyprlang::CConfig config("./config/config.conf"); + + // setup config + config.addConfigValue("testInt", 0L); + config.addConfigValue("testFloat", 0.F); + config.addConfigValue("testString", ""); + config.addConfigValue("testStringQuotes", ""); + config.addConfigValue("testCategory:testValueInt", 0L); + config.addConfigValue("testCategory:testValueHex", 0xAL); + config.addConfigValue("testCategory:nested1:testValueNest", 0L); + config.addConfigValue("testCategory:nested1:nested2:testValueNest", 0L); + config.addConfigValue("testDefault", 123L); + + config.commence(); + + const auto PARSERESULT = config.parse(); + if (PARSERESULT.error) { + std::cout << "Parse error: " << PARSERESULT.getError() << "\n"; + return 1; + } + + EXPECT(PARSERESULT.error, false); + + // test values + EXPECT(std::any_cast(config.getConfigValue("testInt")), 123); + EXPECT(std::any_cast(config.getConfigValue("testFloat")), 123.456f); + EXPECT(std::any_cast(config.getConfigValue("testString")), std::string{"Hello World! ## This is not a comment!"}); + EXPECT(std::any_cast(config.getConfigValue("testStringQuotes")), std::string{"\"Hello World!\""}); + EXPECT(std::any_cast(config.getConfigValue("testCategory:testValueInt")), 123456L); + EXPECT(std::any_cast(config.getConfigValue("testCategory:testValueHex")), 0xFFFFAABBL); + EXPECT(std::any_cast(config.getConfigValue("testCategory:nested1:testValueNest")), 1L); + EXPECT(std::any_cast(config.getConfigValue("testCategory:nested1:nested2:testValueNest")), 1L); + EXPECT(std::any_cast(config.getConfigValue("testDefault")), 123L); + } catch (const char* e) { + std::cout << "Error: " << e << "\n"; + return 1; + } + + return ret; +} \ No newline at end of file