You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

417 lines
13 KiB

#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 - 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<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 - 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<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 - buf));
}
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