From 62a4abb9bf6551417130c3c6f9bba147930895ef Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Tue, 7 Oct 2025 20:56:33 +0200 Subject: cfg: New module Reads config files --- src/cfg.cc | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/cfg.hh | 40 +++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/cfg.cc create mode 100644 src/cfg.hh (limited to 'src') diff --git a/src/cfg.cc b/src/cfg.cc new file mode 100644 index 0000000..0196bde --- /dev/null +++ b/src/cfg.cc @@ -0,0 +1,197 @@ +#include "cfg.hh" + +#include "io.hh" +#include "line.hh" +#include "paths.hh" +#include "str.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cfg { + +namespace { + +inline char ascii_lowercase(char c) { + if (c >= 'A' && c <= 'Z') { + // NOLINTNEXTLINE(bugprone-narrowing-conversions) + return c | 0x20; + } + return c; +} + +bool ascii_lowercase_eq(std::string_view a, std::string_view b) { + if (a.size() != b.size()) + return false; + auto it_a = a.begin(); + auto it_b = b.begin(); + for (; it_a != a.end(); ++it_a, ++it_b) { + if (ascii_lowercase(*it_a) != *it_b) + return false; + } + return true; +} + +class ConfigSingleImpl : public Config { + public: + ConfigSingleImpl() = default; + + bool load(std::filesystem::path const& path, + std::vector& errors) { + auto io_reader = io::open(std::string(path)); + if (!io_reader.has_value()) { + errors.push_back( + std::format("Unable to open {} for reading", path.string())); + return false; + } + bool all_ok = true; + auto line_reader = line::open(std::move(io_reader.value())); + while (true) { + auto line = line_reader->read(); + if (line.has_value()) { + auto trimmed = str::trim(line.value()); + if (trimmed.empty() || trimmed.front() == '#') + continue; + auto eq = trimmed.find('='); + if (eq == std::string_view::npos) { + errors.push_back( + std::format("{}:{}: Invalid line, expected key = value.", + path.string(), line_reader->number())); + all_ok = false; + continue; + } + auto key = str::trim(trimmed.substr(0, eq)); + auto value = str::trim(trimmed.substr(eq + 1)); + auto ret = values_.emplace(key, value); + if (!ret.second) { + errors.push_back(std::format("{}:{}: Duplicate key {} ignored.", + path.string(), line_reader->number(), + key)); + all_ok = false; + continue; + } + } else { + switch (line.error()) { + case io::ReadError::Eof: + break; + default: + errors.push_back(std::format("{}: Read error", path.string())); + all_ok = false; + break; + } + break; + } + } + return all_ok; + } + + [[nodiscard]] + std::optional get(std::string_view name) const override { + auto it = values_.find(name); + if (it == values_.end()) + return std::nullopt; + return it->second; + } + + private: + std::map> values_; +}; + +class ConfigXdgImpl : public Config { + public: + ConfigXdgImpl() = default; + + bool load(std::string_view name, std::vector& errors) { + bool all_ok = true; + for (auto const& dir : paths::config_dirs()) { + auto file = dir / name; + if (std::filesystem::exists(file)) { + auto cfg = std::make_unique(); + if (!cfg->load(file, errors)) + all_ok = false; + configs_.push_back(std::move(cfg)); + } + } + return all_ok; + } + + [[nodiscard]] + std::optional get(std::string_view name) const override { + for (auto const& config : configs_) { + auto ret = config->get(name); + if (ret.has_value()) + return ret; + } + return std::nullopt; + } + + private: + std::vector> configs_; +}; + +} // namespace + +bool Config::has(std::string_view name) const { return get(name).has_value(); } + +std::optional Config::get_int64(std::string_view name) const { + auto str = get(name); + if (str.has_value()) { + auto* const end = str->data() + str->size(); + int64_t ret; + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) + auto [ptr, ec] = std::from_chars(str->data(), end, ret); + if (ec == std::errc() && ptr == end) + return ret; + } + return std::nullopt; +} + +std::optional Config::get_uint64(std::string_view name) const { + auto str = get(name); + if (str.has_value()) { + auto* const end = str->data() + str->size(); + uint64_t ret; + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) + auto [ptr, ec] = std::from_chars(str->data(), end, ret); + if (ec == std::errc() && ptr == end) + return ret; + } + return std::nullopt; +} + +std::optional Config::get_bool(std::string_view name) const { + auto str = get(name); + if (str.has_value()) { + if (ascii_lowercase_eq(str.value(), "true") || + ascii_lowercase_eq(str.value(), "yes")) + return true; + if (ascii_lowercase_eq(str.value(), "false") || + ascii_lowercase_eq(str.value(), "no")) + return false; + } + return std::nullopt; +} + +std::unique_ptr Config::load(std::string_view name, + std::vector& errors) { + if (name.empty() || name.front() == '/') { + auto ret = std::make_unique(); + ret->load(name, errors); + return ret; + } + auto ret = std::make_unique(); + ret->load(name, errors); + return ret; +} + +} // namespace cfg diff --git a/src/cfg.hh b/src/cfg.hh new file mode 100644 index 0000000..a4a7865 --- /dev/null +++ b/src/cfg.hh @@ -0,0 +1,40 @@ +#ifndef CFG_HH +#define CFG_HH + +#include +#include +#include +#include + +namespace cfg { + +class Config { + public: + virtual ~Config() = default; + + [[nodiscard]] + virtual std::optional get(std::string_view name) const = 0; + + [[nodiscard]] + bool has(std::string_view name) const; + [[nodiscard]] + std::optional get_int64(std::string_view name) const; + [[nodiscard]] + std::optional get_uint64(std::string_view name) const; + [[nodiscard]] + std::optional get_bool(std::string_view name) const; + + [[nodiscard]] + static std::unique_ptr load(std::string_view name, + std::vector& errors); + + protected: + Config() = default; + + Config(Config const&) = delete; + Config& operator=(Config const&) = delete; +}; + +} // namespace cfg + +#endif // CFG_HH -- cgit v1.2.3-70-g09d2