#include "utils/ini.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils/algorithm/container.hpp" #include "utils/str_cat.hpp" #include "utils/string_view_hash.hpp" #include "utils/utf8.hpp" namespace devilution { // We avoid including the "appfat.h" to avoid depending on SDL in tests. [[noreturn]] extern void app_fatal(std::string_view str); namespace { template bool OrderByValueIndex(const std::pair &a, const std::pair &b) { return a.second.index < b.second.index; }; // Returns a pointer to the first non-leading whitespace. // Only ' ' and '\t' are considered whitespace. // Requires: begin <= end. const char *SkipLeadingWhitespace(const char *begin, const char *end) { while (begin != end && (*begin == ' ' || *begin == '\t')) { ++begin; } return begin; } // Returns a pointer to the last non-whitespace. // Only ' ' and '\t' are considered whitespace. // Requires: begin <= end. const char *SkipTrailingWhitespace(const char *begin, const char *end) { while (begin != end && (*(end - 1) == ' ' || *(end - 1) == '\t')) { --end; } return end; } // Skips UTF-8 byte order mark. // See https://en.wikipedia.org/wiki/Byte_order_mark const char *SkipUtf8Bom(const char *begin, const char *end) { if (end - begin >= 3 && begin[0] == '\xEF' && begin[1] == '\xBB' && begin[2] == '\xBF') { return begin + 3; } return begin; } } // namespace Ini::Values::Values() : rep_(std::vector {}) { } Ini::Values::Values(const Value &data) : rep_(data) { } std::vector Ini::getKeys(std::string_view section) const { const auto sectionIt = data_.sections.find(section); if (sectionIt == data_.sections.end()) return {}; std::vector> entries(sectionIt->second.entries.begin(), sectionIt->second.entries.end()); c_sort(entries, OrderByValueIndex); std::vector keys; keys.reserve(entries.size()); for (const auto &[key, _] : entries) { keys.push_back(key); } return keys; } std::span Ini::Values::get() const { if (std::holds_alternative(rep_)) { return { &std::get(rep_), 1 }; } return std::get>(rep_); } std::span Ini::Values::get() { if (std::holds_alternative(rep_)) { return { &std::get(rep_), 1 }; } return std::get>(rep_); } void Ini::Values::append(const Ini::Value &value) { if (std::holds_alternative(rep_)) { rep_ = std::vector { std::get(rep_), value }; return; } std::get>(rep_).push_back(value); } tl::expected Ini::parse(std::string_view buffer) { Ini::FileData fileData; ankerl::unordered_dense::map *sectionEntries = nullptr; const char *eof = buffer.data() + buffer.size(); const char *lineBegin = SkipUtf8Bom(buffer.data(), eof); size_t lineNum = 0; const char *commentBegin = nullptr; const char *nextLineBegin; for (; lineBegin < eof; lineBegin = nextLineBegin) { ++lineNum; const char *lineEnd = static_cast(memchr(lineBegin, '\n', eof - lineBegin)); if (lineEnd == nullptr) { lineEnd = eof; nextLineBegin = eof; } else { nextLineBegin = lineEnd + 1; if (lineBegin + 1 <= lineEnd && *(lineEnd - 1) == '\r') --lineEnd; } const char *keyBegin = SkipLeadingWhitespace(lineBegin, lineEnd); if (keyBegin == lineEnd) continue; if (*keyBegin == ';') { if (commentBegin == nullptr) commentBegin = lineBegin; continue; } std::string_view comment; if (commentBegin != nullptr) { comment = std::string_view(commentBegin, lineBegin - commentBegin); } if (*keyBegin == '[') { const char *keyEnd = ++keyBegin; while (keyEnd < lineEnd && *keyEnd != ']') { ++keyEnd; } if (keyEnd == lineEnd) { return tl::make_unexpected(fmt::format("line {}: unclosed section name {}", lineNum, std::string_view(keyBegin, keyEnd - keyBegin))); } if (const char *after = SkipTrailingWhitespace(keyEnd + 1, lineEnd); after != lineEnd) { return tl::make_unexpected(fmt::format("line {}: content after section [{}]: {}", lineNum, std::string_view(keyBegin, keyEnd - keyBegin), std::string_view(after, lineEnd - after))); } const std::string_view sectionName = std::string_view(keyBegin, keyEnd - keyBegin); auto it = fileData.sections.find(sectionName); if (it == fileData.sections.end()) { it = fileData.sections.emplace_hint(it, sectionName, SectionData { .comment = std::string(comment), .entries = {}, .index = static_cast(fileData.sections.size()), }); } sectionEntries = &it->second.entries; commentBegin = nullptr; continue; } if (sectionEntries == nullptr) return tl::unexpected(fmt::format("line {}: key not in any section", lineNum)); const char *eqPos = static_cast(memchr(keyBegin, '=', lineEnd - keyBegin)); if (eqPos == nullptr) { return tl::make_unexpected(fmt::format("line {}: key {} has no value", lineNum, std::string_view(keyBegin, lineEnd - keyBegin))); } const char *keyEnd = SkipTrailingWhitespace(keyBegin, eqPos); const std::string_view key = std::string_view(keyBegin, keyEnd - keyBegin); const char *valueBegin = SkipLeadingWhitespace(eqPos + 1, lineEnd); const std::string_view value = std::string_view(valueBegin, lineEnd - valueBegin); if (const auto it = sectionEntries->find(key); it != sectionEntries->end()) { it->second.values.append(Value { std::string(comment), std::string(value) }); } else { sectionEntries->emplace_hint(it, key, ValuesData { .values = Values { Value { .comment = std::string(comment), .value = std::string(value), } }, .index = static_cast(sectionEntries->size()), }); } commentBegin = nullptr; } return Ini(std::move(fileData)); } std::span Ini::get(std::string_view section, std::string_view key) const { const auto sectionIt = data_.sections.find(section); if (sectionIt == data_.sections.end()) return {}; const auto it = sectionIt->second.entries.find(key); if (it == sectionIt->second.entries.end()) return {}; return it->second.values.get(); } std::string_view Ini::getString(std::string_view section, std::string_view key, std::string_view defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; return xs.back().value; } int Ini::getInt(std::string_view section, std::string_view key, int defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; const std::string_view str = xs.back().value; int value; const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); if (result.ec != std::errc()) { app_fatal(fmt::format("ini: Failed to parse {}.{}={} as int", section, key, str)); return defaultValue; } return value; } bool Ini::getBool(std::string_view section, std::string_view key, bool defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; const std::string_view str = xs.back().value; if (str == "0") return false; if (str == "1") return true; app_fatal(fmt::format("ini: Failed to parse {}.{}={} as bool", section, key, str)); } float Ini::getFloat(std::string_view section, std::string_view key, float defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; const std::string &str = xs.back().value; #if __cpp_lib_to_chars >= 201611L float value; const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); if (result.ec != std::errc()) { app_fatal(fmt::format("ini: Failed to parse {}.{}={} as float", section, key, str)); return defaultValue; } return value; #else return strtof(str.data(), nullptr); #endif } void Ini::getUtf8Buf(std::string_view section, std::string_view key, std::string_view defaultValue, char *dst, size_t dstSize) const { CopyUtf8(dst, getString(section, key, defaultValue), dstSize); } void Ini::set(std::string_view section, std::string_view key, Ini::Values &&values) { const std::span updated = values.get(); auto sectionIt = data_.sections.find(section); if (sectionIt == data_.sections.end()) { // Deleting a key from a non-existing section if (updated.empty()) return; // Adding a new section and key data_.sections.emplace_hint(sectionIt, section, SectionData { .comment = {}, .entries = { { std::string(key), ValuesData { .values = std::move(values), .index = 0 } } }, .index = static_cast(data_.sections.size()), }); changed_ = true; return; } const auto it = sectionIt->second.entries.find(key); if (it == sectionIt->second.entries.end()) { // Deleting a non-existing key if (updated.empty()) return; // Adding a new key to an existing section sectionIt->second.entries.emplace(key, ValuesData { .values = std::move(values), .index = static_cast(sectionIt->second.entries.size()), }); changed_ = true; return; } // Deleting an existing key if (updated.empty()) { sectionIt->second.entries.erase(it); if (sectionIt->second.entries.empty()) data_.sections.erase(sectionIt); changed_ = true; return; } // Overriding an existing key const std::span original = it->second.values.get(); if (original.size() == updated.size()) { bool equal = true; for (size_t i = 0; i < original.size(); ++i) { if (original[i].value != updated[i].value) { equal = false; break; } } if (equal) return; } // Preserve existing comments where not overriden. for (size_t i = 0, n = std::min(original.size(), updated.size()); i < n; ++i) { if (!updated[i].comment.has_value() && original[i].comment.has_value()) { updated[i].comment = std::move(original[i].comment); } } it->second.values = std::move(values); changed_ = true; } void Ini::set(std::string_view section, std::string_view key, std::span strings) { if (strings.empty()) { set(section, key, Values {}); } else if (strings.size() == 1) { set(section, key, Values { Value { .comment = {}, .value = strings[0] } }); } else { Values values; auto &items = std::get>(values.rep_); items.reserve(strings.size()); for (const std::string &str : strings) { items.push_back(Value { .comment = {}, .value = str }); } set(section, key, std::move(values)); } } void Ini::set(std::string_view section, std::string_view key, std::string &&value) { set(section, key, Values { Value { .comment = {}, .value = std::move(value) } }); } void Ini::set(std::string_view section, std::string_view key, std::string_view value) { set(section, key, std::string(value)); } void Ini::set(std::string_view section, std::string_view key, int value) { set(section, key, StrCat(value)); } void Ini::set(std::string_view section, std::string_view key, bool value) { set(section, key, std::string(value ? "1" : "0")); } void Ini::set(std::string_view section, std::string_view key, float value) { #if __cpp_lib_to_chars >= 201611L constexpr size_t BufSize = 64; char buf[BufSize] {}; const std::to_chars_result result = std::to_chars(buf, buf + BufSize, value); if (result.ec != std::errc()) { app_fatal("float->string failed"); // should never happen } set(section, key, std::string_view(buf, result.ptr - buf)); #else set(section, key, fmt::format("{}", value)); #endif } namespace { // Appends a possibly multi-line comment, converting \n to \r\n. void AppendComment(std::string_view comment, std::string &out) { bool prevR = false; for (const char c : comment) { if (c == '\r') { prevR = true; } else { if (c == '\n' && !prevR) out += '\r'; prevR = false; } out += c; } } void AppendSection(std::string_view sectionName, std::string &out) { out.append("[").append(sectionName).append("]\r\n"); } void AppendKeyValue(std::string_view key, std::string_view value, std::string &out) { out.append(key).append("=").append(value).append("\r\n"); } } // namespace std::string Ini::serialize() const { std::string result; std::vector> sections(data_.sections.begin(), data_.sections.end()); c_sort(sections, OrderByValueIndex); std::vector> entries; for (auto &[sectionName, section] : sections) { if (!result.empty()) result.append("\r\n"); if (!section.comment.empty()) AppendComment(section.comment, result); AppendSection(sectionName, result); entries.assign(section.entries.begin(), section.entries.end()); c_sort(entries, OrderByValueIndex); for (const auto &[key, entry] : entries) { for (const auto &[comment, value] : entry.values.get()) { if (comment.has_value() && !comment->empty()) AppendComment(*comment, result); AppendKeyValue(key, value, result); } } } return result; } } // namespace devilution