Files
duckstation/src/util/ini_settings_interface.cpp
T

711 lines
20 KiB
C++

// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "ini_settings_interface.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/string_util.h"
#include "common/threading.h"
#include <algorithm>
#include <cstring>
#include <mutex>
LOG_CHANNEL(Settings);
// To prevent races between saving and loading settings, particularly with game settings,
// we only allow one ini to be parsed at any point in time.
static Threading::Mutex s_ini_load_save_mutex;
INISettingsInterface::INISettingsInterface() = default;
INISettingsInterface::INISettingsInterface(std::string path) : m_path(std::move(path))
{
}
INISettingsInterface::~INISettingsInterface()
{
if (m_dirty)
Save();
}
void INISettingsInterface::SetDirty(bool dirty)
{
m_dirty = dirty;
}
void INISettingsInterface::SetPath(std::string path)
{
m_dirty |= (path != m_path);
m_path = std::move(path);
}
std::string_view INISettingsInterface::GetPoolStringView(const PoolString& ps) const
{
return m_string_pool.GetString(static_cast<BumpUniqueStringPool::Offset>(ps.offset), static_cast<size_t>(ps.length));
}
INISettingsInterface::PoolString INISettingsInterface::AddPoolString(std::string_view str)
{
if (str.empty())
return PoolString{static_cast<u32>(BumpUniqueStringPool::InvalidOffset), 0};
const BumpUniqueStringPool::Offset off = m_string_pool.AddString(str);
return PoolString{static_cast<u32>(off), static_cast<u32>(str.size())};
}
INISettingsInterface::SectionList::const_iterator INISettingsInterface::FindSection(std::string_view name) const
{
auto it =
std::lower_bound(m_sections.begin(), m_sections.end(), name,
[this](const Section& s, const std::string_view& n) { return (GetPoolStringView(s.name) < n); });
if (it != m_sections.end() && GetPoolStringView(it->name) == name)
return it;
return m_sections.end();
}
INISettingsInterface::SectionList::iterator INISettingsInterface::FindSection(std::string_view name)
{
auto it =
std::lower_bound(m_sections.begin(), m_sections.end(), name,
[this](const Section& s, const std::string_view& n) { return (GetPoolStringView(s.name) < n); });
if (it != m_sections.end() && GetPoolStringView(it->name) == name)
return it;
return m_sections.end();
}
INISettingsInterface::Section& INISettingsInterface::GetOrCreateSection(std::string_view name)
{
auto it =
std::lower_bound(m_sections.begin(), m_sections.end(), name,
[this](const Section& s, const std::string_view& n) { return (GetPoolStringView(s.name) < n); });
if (it != m_sections.end() && GetPoolStringView(it->name) == name)
return *it;
Section sec;
sec.name = AddPoolString(name);
return *m_sections.insert(it, std::move(sec));
}
INISettingsInterface::SectionKeyValueList::const_iterator INISettingsInterface::FindKey(const Section& section,
std::string_view key) const
{
auto it = std::lower_bound(
section.entries.begin(), section.entries.end(), key,
[this](const KeyValuePair& kv, const std::string_view& k) { return (GetPoolStringView(kv.key) < k); });
if (it != section.entries.end() && GetPoolStringView(it->key) == key)
return it;
return section.entries.end();
}
INISettingsInterface::SectionKeyValueList::iterator INISettingsInterface::FindKey(Section& section,
std::string_view key)
{
auto it = std::lower_bound(
section.entries.begin(), section.entries.end(), key,
[this](const KeyValuePair& kv, const std::string_view& k) { return (GetPoolStringView(kv.key) < k); });
if (it != section.entries.end() && GetPoolStringView(it->key) == key)
return it;
return section.entries.end();
}
INISettingsInterface::SectionKeyValueList::const_iterator INISettingsInterface::FindKeyEnd(const Section& section,
std::string_view key) const
{
return std::upper_bound(
section.entries.begin(), section.entries.end(), key,
[this](const std::string_view& k, const KeyValuePair& kv) { return (k < GetPoolStringView(kv.key)); });
}
INISettingsInterface::SectionKeyValueList::iterator INISettingsInterface::FindKeyEnd(Section& section,
std::string_view key)
{
return std::upper_bound(
section.entries.begin(), section.entries.end(), key,
[this](const std::string_view& k, const KeyValuePair& kv) { return (k < GetPoolStringView(kv.key)); });
}
void INISettingsInterface::InsertKeyValue(Section& section, std::string_view key, std::string_view value)
{
auto it = std::lower_bound(
section.entries.begin(), section.entries.end(), key,
[this](const KeyValuePair& kv, const std::string_view& k) { return (GetPoolStringView(kv.key) < k); });
// Skip past existing entries with the same key to append at end of group.
while (it != section.entries.end() && GetPoolStringView(it->key) == key)
++it;
KeyValuePair kvp;
kvp.key = AddPoolString(key);
kvp.value = AddPoolString(value);
section.entries.insert(it, kvp);
}
const INISettingsInterface::KeyValuePair* INISettingsInterface::FindFirstKeyValue(std::string_view section,
std::string_view key) const
{
auto sit = FindSection(section);
if (sit == m_sections.end())
return nullptr;
auto kit = FindKey(*sit, key);
if (kit == sit->entries.end())
return nullptr;
return &(*kit);
}
// Strips inline comment from a raw value. Double-quoted values are returned verbatim (quotes removed).
static std::string_view StripValueComment(std::string_view value)
{
value = StringUtil::StripWhitespace(value);
if (value.size() >= 2 && value.front() == '"' && value.back() == '"')
return value.substr(1, value.size() - 2);
const size_t comment_pos = value.find_first_of("#;");
if (comment_pos != std::string_view::npos)
value = value.substr(0, comment_pos);
return StringUtil::StripWhitespace(value);
}
bool INISettingsInterface::Load(Error* error)
{
if (m_path.empty())
{
Error::SetStringView(error, "Filename is not set.");
return false;
}
std::lock_guard lock(s_ini_load_save_mutex);
std::optional<std::string> file_data = FileSystem::ReadFileToString(m_path.c_str(), error);
if (!file_data.has_value())
return false;
LoadFromString(file_data.value());
m_dirty = false;
return true;
}
bool INISettingsInterface::Load(std::string new_path, Error* error)
{
if (m_path != new_path)
m_path = std::move(new_path);
return Load(error);
}
bool INISettingsInterface::LoadFromString(std::string_view data)
{
m_string_pool.Clear();
m_sections.clear();
const size_t num_lines = std::ranges::count(data, '\n') + 1;
m_string_pool.Reserve(num_lines * 2, data.size());
Section* current_section = nullptr;
while (!data.empty())
{
std::string_view line;
const size_t nl = data.find('\n');
if (nl != std::string_view::npos)
{
line = data.substr(0, nl);
data = data.substr(nl + 1);
}
else
{
line = data;
data = {};
}
line = StringUtil::StripWhitespace(line);
if (line.empty() || line.front() == '#' || line.front() == ';')
continue;
// Section header.
if (line.front() == '[')
{
const size_t close = line.find(']', 1);
if (close != std::string_view::npos)
{
std::string_view section_name = StringUtil::StripWhitespace(line.substr(1, close - 1));
current_section = &GetOrCreateSection(section_name);
}
continue;
}
// Key = value pair.
const size_t eq = line.find('=');
if (eq == std::string_view::npos)
continue;
const std::string_view key = StringUtil::StripWhitespace(line.substr(0, eq));
const std::string_view value = StripValueComment(line.substr(eq + 1));
if (key.empty())
continue;
if (!current_section)
current_section = &GetOrCreateSection({});
InsertKeyValue(*current_section, key, value);
}
m_dirty = false;
return true;
}
static bool ValueNeedsQuoting(std::string_view value)
{
return value.find_first_of("#;") != std::string_view::npos;
}
std::string INISettingsInterface::SaveToString(SectionSaveOrder save_order /* = {} */) const
{
// Calculate the exact output size to preallocate.
size_t total_size = 0;
bool first_section = true;
for (const Section& section : m_sections)
{
if (!first_section)
total_size += 1; // blank line between sections
first_section = false;
const std::string_view section_name = GetPoolStringView(section.name);
if (!section_name.empty())
total_size += 1 + section_name.size() + 2; // "[name]\n"
for (const KeyValuePair& kv : section.entries)
{
const std::string_view key = GetPoolStringView(kv.key);
const std::string_view value = GetPoolStringView(kv.value);
// "key = value\n" or "key = \"value\"\n"
total_size += key.size() + 3 + value.size() + 1;
if (ValueNeedsQuoting(value))
total_size += 2; // quotes
}
}
std::string output;
output.reserve(total_size);
first_section = true;
// No save order is easier.
if (save_order.empty())
{
for (const Section& section : m_sections)
{
if (!first_section)
output += '\n';
first_section = false;
SaveSection(output, section);
}
}
else
{
// Save ordered sections first.
std::vector<bool> saved_sections(m_sections.size(), false);
for (const char* ordered_section : save_order)
{
const std::string_view ordered_section_v = ordered_section;
for (size_t i = 0; i < m_sections.size(); i++)
{
if (saved_sections[i])
continue;
const Section& section = m_sections[i];
const std::string_view section_name = GetPoolStringView(section.name);
// Save this section if it matches the ordered section name, or if it starts with it (for prefix matching).
if (section_name == ordered_section_v ||
(section_name.length() > ordered_section_v.size() && section_name[ordered_section_v.size()] == '/' &&
section_name.starts_with(ordered_section_v)))
{
if (!first_section)
output += '\n';
first_section = false;
saved_sections[i] = true;
SaveSection(output, section);
}
}
}
// And then anything remaining.
for (size_t i = 0; i < m_sections.size(); i++)
{
if (saved_sections[i])
continue;
if (!first_section)
output += '\n';
first_section = false;
SaveSection(output, m_sections[i]);
}
}
return output;
}
void INISettingsInterface::SaveSection(std::string& output, const Section& section) const
{
const std::string_view section_name = GetPoolStringView(section.name);
if (!section_name.empty())
{
output += '[';
output += section_name;
output += "]\n";
}
for (const KeyValuePair& kv : section.entries)
{
const std::string_view key = GetPoolStringView(kv.key);
const std::string_view value = GetPoolStringView(kv.value);
output += key;
output += " = ";
if (ValueNeedsQuoting(value))
{
output += '"';
output += value;
output += '"';
}
else
{
output += value;
}
output += '\n';
}
}
bool INISettingsInterface::Save(Error* error /* = nullptr */, SectionSaveOrder save_order /* = {} */)
{
if (m_path.empty())
{
Error::SetStringView(error, "Filename is not set.");
return false;
}
std::unique_lock lock(s_ini_load_save_mutex);
FileSystem::AtomicRenamedFile fp = FileSystem::CreateAtomicRenamedFile(m_path, error);
if (!fp)
{
WARNING_LOG("Failed to save settings to '{}'.", m_path);
return false;
}
const std::string data = SaveToString(save_order);
const bool write_ok = (std::fwrite(data.data(), 1, data.size(), fp.get()) == data.size());
if (!write_ok)
{
Error::SetStringView(error, "Failed to write INI data.");
FileSystem::DiscardAtomicRenamedFile(fp);
WARNING_LOG("Failed to save settings to '{}'.", m_path);
return false;
}
if (!FileSystem::CommitAtomicRenamedFile(fp, error))
{
WARNING_LOG("Failed to save settings to '{}'.", m_path);
return false;
}
m_dirty = false;
return true;
}
void INISettingsInterface::Clear(bool clear_memory /* = false */)
{
if (clear_memory)
{
m_string_pool = BumpUniqueStringPool();
m_sections = SectionList();
}
else
{
m_string_pool.Clear();
m_sections.clear();
}
}
void INISettingsInterface::ClearContentsAndDirty(bool clear_memory)
{
Clear(clear_memory);
m_dirty = false;
}
void INISettingsInterface::ClearPathAndContents()
{
Clear(true);
m_path = {};
m_dirty = false;
}
bool INISettingsInterface::IsEmpty()
{
for (const Section& section : m_sections)
{
if (!section.entries.empty())
return false;
}
return true;
}
void INISettingsInterface::CompactStrings()
{
size_t num_strings = 0;
for (const Section& section : m_sections)
num_strings += 1 + section.entries.size() * 2;
BumpUniqueStringPool new_pool;
new_pool.Reserve(num_strings, m_string_pool.GetSize());
for (Section& section : m_sections)
{
const std::string_view name = GetPoolStringView(section.name);
section.name = {static_cast<u32>(new_pool.AddString(name)), static_cast<u32>(name.size())};
for (KeyValuePair& kv : section.entries)
{
const std::string_view key = GetPoolStringView(kv.key);
kv.key = {static_cast<u32>(new_pool.AddString(key)), static_cast<u32>(key.size())};
const std::string_view value = GetPoolStringView(kv.value);
kv.value = {static_cast<u32>(new_pool.AddString(value)), static_cast<u32>(value.size())};
}
}
m_string_pool = std::move(new_pool);
}
bool INISettingsInterface::LookupValue(const char* section, const char* key, std::string_view* value) const
{
const KeyValuePair* kv = FindFirstKeyValue(section, key);
if (!kv)
return false;
*value = GetPoolStringView(kv->value);
return true;
}
void INISettingsInterface::StoreValue(const char* section, const char* key, std::string_view value)
{
const std::string_view key_sv(key);
Section& sec = GetOrCreateSection(section);
auto it = FindKey(sec, key_sv);
if (it != sec.entries.end())
{
if (GetPoolStringView(it->value) == value)
return;
// Update existing entry (old value string becomes waste in pool).
it->value = AddPoolString(value);
// Remove any duplicate keys beyond the first.
auto end_it = FindKeyEnd(sec, key_sv);
if ((end_it - it) > 1)
sec.entries.erase(it + 1, end_it);
}
else
{
InsertKeyValue(sec, key_sv, value);
}
m_dirty = true;
}
bool INISettingsInterface::ContainsValue(const char* section, const char* key) const
{
return FindFirstKeyValue(section, key) != nullptr;
}
void INISettingsInterface::DeleteValue(const char* section, const char* key)
{
auto sit = FindSection(section);
if (sit == m_sections.end())
return;
const std::string_view key_sv(key);
auto begin_it = FindKey(*sit, key_sv);
if (begin_it == sit->entries.end())
return;
auto end_it = FindKeyEnd(*sit, key_sv);
sit->entries.erase(begin_it, end_it);
m_dirty = true;
}
void INISettingsInterface::ClearSection(const char* section)
{
auto sit = FindSection(section);
if (sit == m_sections.end())
return;
sit->entries.clear();
m_dirty = true;
}
void INISettingsInterface::RemoveSection(const char* section)
{
auto sit = FindSection(section);
if (sit == m_sections.end())
return;
m_sections.erase(sit);
m_dirty = true;
}
void INISettingsInterface::RemoveEmptySections()
{
for (auto it = m_sections.begin(); it != m_sections.end();)
{
if (it->entries.empty())
{
it = m_sections.erase(it);
m_dirty = true;
}
else
{
++it;
}
}
}
INISettingsInterface::StringList INISettingsInterface::GetStringList(const char* section, const char* key) const
{
auto sit = FindSection(std::string_view(section));
if (sit == m_sections.end())
return {};
const std::string_view key_sv(key);
auto begin_it = FindKey(*sit, key_sv);
if (begin_it == sit->entries.end())
return {};
auto end_it = FindKeyEnd(*sit, key_sv);
StringList result;
result.reserve(static_cast<size_t>(end_it - begin_it));
for (auto it = begin_it; it != end_it; ++it)
result.emplace_back(GetPoolStringView(it->value));
return result;
}
void INISettingsInterface::SetStringList(const char* section, const char* key, const StringList& items)
{
const std::string_view section_sv(section);
const std::string_view key_sv(key);
Section& sec = GetOrCreateSection(section_sv);
// Remove existing entries for this key.
auto begin_it = FindKey(sec, key_sv);
if (begin_it != sec.entries.end())
{
auto end_it = FindKeyEnd(sec, key_sv);
sec.entries.erase(begin_it, end_it);
}
for (const std::string& item : items)
InsertKeyValue(sec, key_sv, std::string_view(item));
m_dirty = true;
}
bool INISettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
{
auto sit = FindSection(std::string_view(section));
if (sit == m_sections.end())
return false;
const std::string_view key_sv(key);
const std::string_view item_sv(item);
auto begin_it = FindKey(*sit, key_sv);
if (begin_it == sit->entries.end())
return false;
auto end_it = FindKeyEnd(*sit, key_sv);
for (auto it = begin_it; it != end_it; ++it)
{
if (GetPoolStringView(it->value) == item_sv)
{
sit->entries.erase(it);
m_dirty = true;
return true;
}
}
return false;
}
bool INISettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
{
const std::string_view section_sv(section);
const std::string_view key_sv(key);
const std::string_view item_sv(item);
auto sit = FindSection(section_sv);
if (sit != m_sections.end())
{
auto begin_it = FindKey(*sit, key_sv);
if (begin_it != sit->entries.end())
{
auto end_it = FindKeyEnd(*sit, key_sv);
for (auto it = begin_it; it != end_it; ++it)
{
if (GetPoolStringView(it->value) == item_sv)
return false;
}
}
}
Section& sec = GetOrCreateSection(section_sv);
InsertKeyValue(sec, key_sv, item_sv);
m_dirty = true;
return true;
}
INISettingsInterface::KeyValueList INISettingsInterface::GetKeyValueList(const char* section) const
{
auto sit = FindSection(section);
if (sit == m_sections.end())
return {};
std::vector<std::pair<std::string, std::string>> result;
result.reserve(sit->entries.size());
for (const KeyValuePair& kv : sit->entries)
result.emplace_back(std::string(GetPoolStringView(kv.key)), std::string(GetPoolStringView(kv.value)));
return result;
}
void INISettingsInterface::SetKeyValueList(const char* section, const KeyValueList& items)
{
Section& sec = GetOrCreateSection(section);
// don't dirty if it hasn't changed
if (sec.entries.size() == items.size())
{
size_t i;
for (i = 0; i < sec.entries.size(); i++)
{
if (GetPoolStringView(sec.entries[i].key) != items[i].first ||
GetPoolStringView(sec.entries[i].value) != items[i].second)
{
break;
}
}
if (i == sec.entries.size())
{
// no change
return;
}
}
sec.entries.clear();
for (const auto& [k, v] : items)
InsertKeyValue(sec, std::string_view(k), std::string_view(v));
m_dirty = true;
}