Browse Source
Our implementation has a more modern interface and only supports the features that we care about. It always outputs `\n` as newlines and does not output BOM. The modern interface eliminates awkward `c_str()/data()` conversions. This implementation preserves comments and the file order of sections and keys. New keys are written in insertion order. We now also support modifying and adding default comments, which may be a useful thing to do for the especially tricky ini options (this PR doesn't add any but adds the ability to do so). Sadly, this increases the RG99 binary size by 24 KiB. I'm guessing this is because the map implementation generates quite a bit of code. Note that while it might seem that using `std::string` for every key and value would do a lot of allocations, most of these strings are small and thus benefit from Small String Optimization (= no allocations).pull/7453/merge
18 changed files with 749 additions and 252 deletions
@ -1,12 +0,0 @@
|
||||
include(functions/FetchContent_MakeAvailableExcludeFromAll) |
||||
|
||||
include(FetchContent) |
||||
FetchContent_Declare(simpleini |
||||
URL https://github.com/brofield/simpleini/archive/56499b5af5d2195c6acfc58c4630b70e0c9c4c21.tar.gz |
||||
URL_HASH MD5=02a561cea03ea11acb65848318ec4a81 |
||||
) |
||||
FetchContent_MakeAvailableExcludeFromAll(simpleini) |
||||
|
||||
add_library(simpleini INTERFACE) |
||||
target_include_directories(simpleini INTERFACE ${simpleini_SOURCE_DIR}) |
||||
add_library(simpleini::simpleini ALIAS simpleini) |
||||
@ -0,0 +1,415 @@
|
||||
#include "utils/ini.hpp" |
||||
|
||||
#include <algorithm> |
||||
#include <charconv> |
||||
#include <cstddef> |
||||
#include <cstdint> |
||||
#include <cstring> |
||||
#include <span> |
||||
#include <string> |
||||
#include <string_view> |
||||
#include <system_error> |
||||
#include <utility> |
||||
#include <variant> |
||||
#include <vector> |
||||
|
||||
#include <ankerl/unordered_dense.h> |
||||
#include <expected.hpp> |
||||
#include <fmt/core.h> |
||||
|
||||
#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 { |
||||
|
||||
// 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<Value> {}) |
||||
{ |
||||
} |
||||
|
||||
Ini::Values::Values(const Value &data) |
||||
: rep_(data) |
||||
{ |
||||
} |
||||
|
||||
std::span<const Ini::Value> Ini::Values::get() const |
||||
{ |
||||
if (std::holds_alternative<Ini::Value>(rep_)) { |
||||
return { &std::get<Ini::Value>(rep_), 1 }; |
||||
} |
||||
return std::get<std::vector<Ini::Value>>(rep_); |
||||
} |
||||
|
||||
std::span<Ini::Value> Ini::Values::get() |
||||
{ |
||||
if (std::holds_alternative<Ini::Value>(rep_)) { |
||||
return { &std::get<Ini::Value>(rep_), 1 }; |
||||
} |
||||
return std::get<std::vector<Ini::Value>>(rep_); |
||||
} |
||||
|
||||
void Ini::Values::append(const Ini::Value &value) |
||||
{ |
||||
if (std::holds_alternative<Value>(rep_)) { |
||||
rep_ = std::vector<Value> { std::get<Value>(rep_), value }; |
||||
return; |
||||
} |
||||
std::get<std::vector<Value>>(rep_).push_back(value); |
||||
} |
||||
|
||||
tl::expected<Ini, std::string> Ini::parse(std::string_view buffer) |
||||
{ |
||||
Ini::FileData fileData; |
||||
ankerl::unordered_dense::map<std::string, ValuesData, StringViewHash, StringViewEquals> *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<const char *>(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 }; |
||||
} |
||||
|
||||
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))); |
||||
} |
||||
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), std::string_view(after, lineEnd))); |
||||
} |
||||
const std::string_view sectionName = std::string_view(keyBegin, keyEnd); |
||||
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<uint32_t>(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<const char *>(memchr(keyBegin, '=', lineEnd - keyBegin)); |
||||
if (eqPos == nullptr) { |
||||
return tl::make_unexpected(fmt::format("line {}: key {} has no value", lineNum, std::string_view(keyBegin, lineEnd))); |
||||
} |
||||
const std::string_view key = std::string_view(keyBegin, SkipTrailingWhitespace(keyBegin, eqPos)); |
||||
const std::string_view value = std::string_view(SkipLeadingWhitespace(eqPos + 1, lineEnd), lineEnd); |
||||
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<uint32_t>(sectionEntries->size()), |
||||
}); |
||||
} |
||||
commentBegin = nullptr; |
||||
} |
||||
return Ini(std::move(fileData)); |
||||
} |
||||
|
||||
std::span<const Ini::Value> 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<const Ini::Value> 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<const Ini::Value> 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<const Ini::Value> 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<const Ini::Value> xs = get(section, key); |
||||
if (xs.empty() || xs.back().value.empty()) return defaultValue; |
||||
const std::string_view str = xs.back().value; |
||||
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; |
||||
} |
||||
|
||||
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<Value> 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<uint32_t>(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<uint32_t>(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<Value> 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<const std::string> 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<std::vector<Value>>(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) |
||||
{ |
||||
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)); |
||||
} |
||||
|
||||
namespace { |
||||
|
||||
template <typename T> |
||||
bool OrderByValueIndex(const std::pair<std::string, T> &a, const std::pair<std::string, T> &b) |
||||
{ |
||||
return a.second.index < b.second.index; |
||||
}; |
||||
|
||||
// Appends a possibly multi-line comment, converting \r\n to \n.
|
||||
void AppendComment(std::string_view comment, std::string &out) |
||||
{ |
||||
bool prevR = false; |
||||
for (const char c : comment) { |
||||
if (prevR) { |
||||
prevR = c != '\r'; |
||||
out += c; |
||||
} else if (c == '\r') { |
||||
prevR = true; |
||||
} else { |
||||
out += c; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void AppendSection(std::string_view sectionName, std::string &out) |
||||
{ |
||||
out.append("[").append(sectionName).append("]\n"); |
||||
} |
||||
|
||||
void AppendKeyValue(std::string_view key, std::string_view value, std::string &out) |
||||
{ |
||||
out.append(key).append("=").append(value).append("\n"); |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
std::string Ini::serialize() const |
||||
{ |
||||
std::string result; |
||||
std::vector<std::pair<std::string, SectionData>> sections(data_.sections.begin(), data_.sections.end()); |
||||
c_sort(sections, OrderByValueIndex<SectionData>); |
||||
|
||||
std::vector<std::pair<std::string, ValuesData>> entries; |
||||
for (auto &[sectionName, section] : sections) { |
||||
if (!result.empty()) result += '\n'; |
||||
if (!section.comment.empty()) AppendComment(section.comment, result); |
||||
AppendSection(sectionName, result); |
||||
entries.assign(section.entries.begin(), section.entries.end()); |
||||
c_sort(entries, OrderByValueIndex<ValuesData>); |
||||
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
|
||||
@ -0,0 +1,121 @@
|
||||
#pragma once |
||||
|
||||
#include <cstddef> |
||||
#include <cstdint> |
||||
#include <optional> |
||||
#include <span> |
||||
#include <string> |
||||
#include <string_view> |
||||
#include <utility> |
||||
#include <variant> |
||||
#include <vector> |
||||
|
||||
#include <ankerl/unordered_dense.h> |
||||
#include <expected.hpp> |
||||
|
||||
#include "utils/string_view_hash.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
class Ini { |
||||
public: |
||||
// A single value associated with a section and key.
|
||||
struct Value { |
||||
// When setting a value, `nullopt` results
|
||||
// in preserving the existing comment if any.
|
||||
std::optional<std::string> comment; |
||||
std::string value; |
||||
}; |
||||
|
||||
// All the values associated with a section and key.
|
||||
class Values { |
||||
public: |
||||
/**
|
||||
* @brief Constructs an empty set of values. |
||||
* |
||||
* If passed to `set`, the key is deleted. |
||||
*/ |
||||
Values(); |
||||
|
||||
explicit Values(const Value &data); |
||||
|
||||
[[nodiscard]] std::span<const Value> get() const; |
||||
[[nodiscard]] std::span<Value> get(); |
||||
void append(const Value &value); |
||||
|
||||
private: |
||||
// Most keys only have a single value, so we use
|
||||
// a representation that avoids allocations in that case.
|
||||
std::variant<Value, std::vector<Value>> rep_; |
||||
|
||||
friend class Ini; |
||||
}; |
||||
|
||||
static tl::expected<Ini, std::string> parse(std::string_view buffer); |
||||
[[nodiscard]] std::string serialize() const; |
||||
|
||||
/** @return all the values associated with this section and key in the ini */ |
||||
[[nodiscard]] std::span<const Value> get(std::string_view section, std::string_view key) const; |
||||
|
||||
/** @return the default value if the ini value is unset or empty */ |
||||
[[nodiscard]] std::string_view getString(std::string_view section, std::string_view key, std::string_view defaultValue = {}) const; |
||||
|
||||
/** @return the default value if the ini value is unset or empty */ |
||||
[[nodiscard]] bool getBool(std::string_view section, std::string_view key, bool defaultValue) const; |
||||
|
||||
/** @return the default value if the ini value is unset or empty */ |
||||
[[nodiscard]] int getInt(std::string_view section, std::string_view key, int defaultValue) const; |
||||
|
||||
/** @return the default value if the ini value is unset or empty */ |
||||
[[nodiscard]] float getFloat(std::string_view section, std::string_view key, float defaultValue) const; |
||||
|
||||
void getUtf8Buf(std::string_view section, std::string_view key, std::string_view defaultValue, char *dst, size_t dstSize) const; |
||||
|
||||
void getUtf8Buf(std::string_view section, std::string_view key, char *dst, size_t dstSize) const |
||||
{ |
||||
getUtf8Buf(section, key, /*defaultValue=*/ {}, dst, dstSize); |
||||
} |
||||
|
||||
[[nodiscard]] bool changed() const { return changed_; } |
||||
void markAsUnchanged() { changed_ = false; } |
||||
|
||||
// If values are empty, deletes the entry.
|
||||
void set(std::string_view section, std::string_view key, Values &&values); |
||||
|
||||
void set(std::string_view section, std::string_view key, std::span<const std::string> value); |
||||
void set(std::string_view section, std::string_view key, std::string &&value); |
||||
void set(std::string_view section, std::string_view key, std::string_view value); |
||||
void set(std::string_view section, std::string_view key, const char *value) |
||||
{ |
||||
set(section, key, std::string_view(value)); |
||||
} |
||||
void set(std::string_view section, std::string_view key, bool value); |
||||
void set(std::string_view section, std::string_view key, int value); |
||||
void set(std::string_view section, std::string_view key, float value); |
||||
|
||||
private: |
||||
struct ValuesData { |
||||
Values values; |
||||
uint32_t index; |
||||
}; |
||||
|
||||
struct SectionData { |
||||
std::string comment; |
||||
ankerl::unordered_dense::map<std::string, ValuesData, StringViewHash, StringViewEquals> entries; |
||||
uint32_t index; |
||||
}; |
||||
|
||||
struct FileData { |
||||
ankerl::unordered_dense::map<std::string, SectionData, StringViewHash, StringViewEquals> sections; |
||||
}; |
||||
|
||||
explicit Ini(FileData &&data) |
||||
: data_(std::move(data)) |
||||
{ |
||||
} |
||||
|
||||
FileData data_; |
||||
bool changed_ = false; |
||||
}; |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,86 @@
|
||||
#include "utils/ini.hpp" |
||||
|
||||
#include <gmock/gmock.h> |
||||
#include <gtest/gtest.h> |
||||
|
||||
#include <string_view> |
||||
|
||||
namespace devilution { |
||||
namespace { |
||||
|
||||
using ::testing::ElementsAre; |
||||
using ::testing::Eq; |
||||
using ::testing::Field; |
||||
|
||||
} // namespace
|
||||
|
||||
TEST(IniTest, BasicTest) |
||||
{ |
||||
tl::expected<Ini, std::string> result = Ini::parse(R"( |
||||
; Section A comment |
||||
[sectionA] |
||||
key1 = value1 |
||||
key2 = value2 |
||||
; comment multi 1 |
||||
multi = a |
||||
; comment multi 2 |
||||
multi = b |
||||
int=-3 |
||||
float=2.5 |
||||
; bool yes comment |
||||
bool yes=1 |
||||
bool no=0 |
||||
|
||||
; Section B comment line 1 |
||||
; Section B comment line 2 |
||||
[sectionB] |
||||
key = value |
||||
)"); |
||||
|
||||
ASSERT_TRUE(result.has_value()) << result.error(); |
||||
EXPECT_EQ(result->getString("sectionA", "key1"), "value1"); |
||||
EXPECT_EQ(result->getString("sectionA", "key2"), "value2"); |
||||
{ |
||||
const std::span<const Ini::Value> multiVals = result->get("sectionA", "multi"); |
||||
std::vector<std::string> multiStrs; |
||||
for (const Ini::Value &val : multiVals) { |
||||
multiStrs.push_back(val.value); |
||||
} |
||||
EXPECT_THAT(multiStrs, ElementsAre(Eq("a"), Eq("b"))); |
||||
} |
||||
EXPECT_EQ(result->getInt("sectionA", "int", 0), -3); |
||||
EXPECT_NEAR(result->getFloat("sectionA", "float", 0.0f), 2.5f, 0.001f); |
||||
EXPECT_EQ(result->getString("sectionB", "key"), "value"); |
||||
|
||||
result->set("newSection", "newKey", "hello"); |
||||
result->set("sectionA", "key1", "newValue"); |
||||
result->set("sectionA", "int", 1337); |
||||
result->set("sectionA", "bool yes", false); |
||||
const std::vector<std::string> newMulti { "x", "y", "z" }; |
||||
result->set("sectionA", "multi", newMulti); |
||||
result->set("sectionA", "float", 10.5F); |
||||
EXPECT_EQ(result->serialize(), std::string_view(R"(; Section A comment |
||||
[sectionA] |
||||
key1=newValue |
||||
key2=value2 |
||||
; comment multi 1 |
||||
multi=x |
||||
; comment multi 2 |
||||
multi=y |
||||
multi=z |
||||
int=1337 |
||||
float=10.5 |
||||
; bool yes comment |
||||
bool yes=0 |
||||
bool no=0 |
||||
|
||||
; Section B comment line 1 |
||||
; Section B comment line 2 |
||||
[sectionB] |
||||
key=value |
||||
|
||||
[newSection] |
||||
newKey=hello |
||||
)")); |
||||
} |
||||
} // namespace devilution
|
||||
Loading…
Reference in new issue