Index: mptrack/Settings.cpp =================================================================== --- mptrack/Settings.cpp (revision 24343) +++ mptrack/Settings.cpp (working copy) @@ -140,6 +140,16 @@ backend->RemoveSection(section); } +bool SettingsContainer::BackendsCanWriteMultipleSettings() const +{ + return backend->CanBatchedSettings(); +} + +void SettingsContainer::BackendsWriteMultipleSettings(const std::map &settings) +{ + backend->WriteMultipleSettings(settings); +} + SettingValue SettingsContainer::ReadSetting(const SettingPath &path, const SettingValue &def) const { ASSERT(theApp.InGuiThread()); @@ -242,6 +252,26 @@ { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + if(BackendsCanWriteMultipleSettings()) + { + std::map settings; + for(auto &[path, value] : map) + { + if(value.IsDirty()) + { + settings.insert(std::make_pair(path, value.GetRefValue())); + } + } + BackendsWriteMultipleSettings(settings); + for(auto &[path, value] : map) + { + if(value.IsDirty()) + { + value.Clean(); + } + } + return; + } for(auto &[path, value] : map) { if(value.IsDirty()) @@ -405,11 +435,22 @@ IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename) - : filename(filename) + : IniFileSettingsBackend(filename, CachePolicy::Cached) { return; } +IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename, CachePolicy cachePolicy) + : filename(filename) +{ + OPENMPT_PROFILE_FUNCTION(Profiler::Settings); + if(cachePolicy == CachePolicy::Cached) + { + ConvertToUnicode(); + cache = ReadAllSectionsRaw(); + } +} + IniFileSettingsBackend::~IniFileSettingsBackend() { return; @@ -469,14 +510,41 @@ SettingValue IniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const { OPENMPT_PROFILE_FUNCTION(Profiler::Settings); - switch(def.GetType()) + if(cache.has_value()) { - case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; - case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; - case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; - case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; - case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as >()), def.GetTypeTag()); break; - default: return SettingValue(); break; + const auto sectionit = (*cache).find(mpt::ToWin(path.GetRefSection())); + if(sectionit != (*cache).end()) + { + const std::map> §ion = sectionit->second; + const auto it = section.find(mpt::ToWin(path.GetRefKey())); + if(it != section.end()) + { + if(it->second.has_value()) + { + return ParseValueFromIni(it->second.value(), def); + } else + { + return def; + } + } else + { + return def; + } + } else + { + return def; + } + } else + { + switch(def.GetType()) + { + case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; + case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; + case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; + case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; + case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as >()), def.GetTypeTag()); break; + default: return SettingValue(); break; + } } } @@ -493,12 +561,22 @@ case SettingTypeBinary: WriteSettingRaw(path, val.as >()); break; default: break; } + if(cache.has_value()) + { + (*cache)[mpt::ToWin(path.GetRefSection())][mpt::ToWin(path.GetRefKey())] = FormatValueAsIni(val); + } } void IniFileSettingsBackend::RemoveSetting(const SettingPath &path) { OPENMPT_PROFILE_FUNCTION(Profiler::Settings); - RemoveSettingRaw(path); + if(cache.has_value()) + { + (*cache)[mpt::ToWin(path.GetRefSection())][mpt::ToWin(path.GetRefKey())] = std::nullopt; + } else + { + RemoveSettingRaw(path); + } } void IniFileSettingsBackend::RemoveSection(const mpt::ustring §ion) @@ -505,19 +583,255 @@ { OPENMPT_PROFILE_FUNCTION(Profiler::Settings); RemoveSectionRaw(section); + if(cache.has_value()) + { + const auto it = (*cache).find(mpt::ToWin(section)); + if(it != (*cache).end()) + { + (*cache).erase(it); + } + } } +std::set IniFileSettingsBackend::ReadSectionNamesRaw() const +{ + std::set result; + const std::vector sectionsstr = [&]() + { + std::vector buf; + buf.resize(mpt::IO::BUFFERSIZE_SMALL); + while(true) + { + DWORD bufused = ::GetPrivateProfileSectionNames(buf.data(), mpt::saturate_cast(buf.size()), filename.AsNative().c_str()); + if(bufused >= (buf.size() - 2)) + { + std::size_t newsize = mpt::exponential_grow(buf.size()); + buf.resize(0); + buf.resize(newsize); + continue; + } + if(bufused >= 1) + { + bufused -= 1; // terminating \0 + } + buf.resize(bufused); + break; + }; + return buf; + }(); + const std::vector sections = mpt::split(mpt::winstring(sectionsstr.data(), sectionsstr.size()), mpt::winstring(_T("\0"), 1)); + for(const auto §ion : sections) + { + result.insert(section); + } + return result; +} +std::map> IniFileSettingsBackend::ReadNamedSectionRaw(const mpt::winstring §ion) const +{ + std::map> result; + const std::vector keyvalsstr = [&]() + { + std::vector buf; + buf.resize(mpt::IO::BUFFERSIZE_SMALL); + while(true) + { + DWORD bufused = ::GetPrivateProfileSection(mpt::ToWin(section).c_str(), buf.data(), mpt::saturate_cast(buf.size()), filename.AsNative().c_str()); + if(bufused >= (buf.size() - 2)) + { + std::size_t newsize = mpt::exponential_grow(buf.size()); + buf.resize(0); + buf.resize(newsize); + continue; + } + if(bufused >= 1) + { + bufused -= 1; // terminating \0 + } + buf.resize(bufused); + break; + }; + return buf; + }(); + const std::vector keyvals = mpt::split(mpt::winstring(keyvalsstr.data(), keyvalsstr.size()), mpt::winstring(_T("\0"), 1)); + for(const auto &keyval : keyvals) + { + const auto equalpos = keyval.find(_T("=")); + if(equalpos == mpt::winstring::npos) + { + continue; + } + if(equalpos == 0) + { + continue; + } + result.insert(std::make_pair(keyval.substr(0, equalpos), std::make_optional(keyval.substr(equalpos + 1)))); + } + return result; +} +std::map>> IniFileSettingsBackend::ReadAllSectionsRaw() const +{ + std::map>> result; + const std::set sectionnames = ReadSectionNamesRaw(); + for(const mpt::winstring §ionname : sectionnames) + { + result.insert(std::make_pair(sectionname, ReadNamedSectionRaw(sectionname))); + } + return result; +} +mpt::winstring IniFileSettingsBackend::FormatValueAsIni(const SettingValue &value) +{ + switch(value.GetType()) + { + case SettingTypeBool: + return mpt::tfmt::val(value.as()); + break; + case SettingTypeInt: + return mpt::tfmt::val(value.as()); + break; + case SettingTypeFloat: + return mpt::tfmt::val(value.as()); + break; + case SettingTypeString: + return mpt::ToWin(value.as()); + break; + case SettingTypeBinary: + { + std::vector data = value.as>(); + uint8 checksum = 0; + for(const std::byte x : data) { + checksum += mpt::byte_cast(x); + } + return mpt::ToWin(mpt::encode_hex(mpt::as_span(data)) + mpt::ufmt::HEX0<2>(checksum)); + } + break; + case SettingTypeNone: + default: + return mpt::winstring(); + break; + } +} + +SettingValue IniFileSettingsBackend::ParseValueFromIni(const mpt::winstring &str, const SettingValue &def) +{ + switch(def.GetType()) + { + case SettingTypeBool: + return SettingValue(mpt::parse_or(mpt::trim(str), def.as()), def.GetTypeTag()); + break; + case SettingTypeInt: + return SettingValue(mpt::parse_or(mpt::trim(str), def.as()), def.GetTypeTag()); + break; + case SettingTypeFloat: + return SettingValue(mpt::parse_or(mpt::trim(str), def.as()), def.GetTypeTag()); + break; + case SettingTypeString: + return SettingValue(mpt::ToUnicode(str), def.GetTypeTag()); + break; + case SettingTypeBinary: + { + mpt::ustring ustr = mpt::trim(mpt::ToUnicode(str)); + if((ustr.length() % 2) != 0) + { + return SettingValue(def.as>(), def.GetTypeTag()); + } + std::vector data = mpt::decode_hex(ustr); + if(data.size() < 1) + { + return SettingValue(def.as>(), def.GetTypeTag()); + } + const uint8 storedchecksum = mpt::byte_cast(data[data.size() - 1]); + data.resize(data.size() - 1); + uint8 calculatedchecksum = 0; + for(const std::byte x : data) { + calculatedchecksum += mpt::byte_cast(x); + } + if(calculatedchecksum != storedchecksum) + { + return SettingValue(def.as>(), def.GetTypeTag()); + } + return SettingValue(data, def.GetTypeTag()); + } + break; + default: + return SettingValue(); + break; + } +} + +void IniFileSettingsBackend::WriteSectionRaw(const mpt::winstring §ion, const std::map> &keyvalues) +{ + mpt::winstring keyvals; + for(const auto &[key, val] : keyvalues) + { + if(val.has_value()) + { + keyvals.append(key); + keyvals.append(_T("=")); + keyvals.append(val.value()); + keyvals.append(mpt::winstring(_T("\0"), 1)); + } + } + keyvals.append(mpt::winstring(_T("\0"), 1)); + ::WritePrivateProfileSection(section.c_str(), keyvals.c_str(), filename.AsNative().c_str()); +} + +void IniFileSettingsBackend::WriteMultipleSettings(const std::map &settings) +{ + OPENMPT_PROFILE_FUNCTION(Profiler::Settings); + std::map> sectionssettings; + for(const auto &[path, value] : settings) + { + sectionssettings[path.GetRefSection()][path] = value; + } + for(const auto &[section, sectionsettings] : sectionssettings) + { + // we need to re-read the section before writing it out in order to not overwrite settings written by MFC + std::map> workingsectionsettings = ReadNamedSectionRaw(mpt::ToWin(section)); + // apply deleted settings from cache + for(const auto &[key, value] : (*cache)[mpt::ToWin(section)]) + { + if(!value.has_value()) + { + workingsectionsettings[key] = std::nullopt; + } + } + for(const auto &[path, value] : sectionsettings) + { + workingsectionsettings[mpt::ToWin(path.GetRefKey())] = FormatValueAsIni(value); + } + WriteSectionRaw(mpt::ToWin(section), workingsectionsettings); + if(cache.has_value()) + { + (*cache)[mpt::ToWin(section)] = std::move(workingsectionsettings); + } + } +} + +bool IniFileSettingsBackend::CanBatchedSettings() const +{ + return cache.has_value(); +} + + + IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename) - : IniFileSettingsBackend(filename) + : IniFileSettingsBackend(filename, CachePolicy::Cached) , SettingsContainer(this) { return; } +IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename, CachePolicy cachePolicy) + : IniFileSettingsBackend(filename, cachePolicy) + , SettingsContainer(this) +{ + return; +} + IniFileSettingsContainer::~IniFileSettingsContainer() { return; Index: mptrack/Settings.h =================================================================== --- mptrack/Settings.h (revision 24343) +++ mptrack/Settings.h (working copy) @@ -408,6 +408,8 @@ virtual void WriteSetting(const SettingPath &path, const SettingValue &val) = 0; virtual void RemoveSetting(const SettingPath &path) = 0; virtual void RemoveSection(const mpt::ustring §ion) = 0; + virtual bool CanBatchedSettings() const = 0; + virtual void WriteMultipleSettings(const std::map &settings) = 0; protected: virtual ~ISettingsBackend() = default; }; @@ -448,6 +450,8 @@ void BackendsWriteSetting(const SettingPath &path, const SettingValue &val); void BackendsRemoveSetting(const SettingPath &path); void BackendsRemoveSection(const mpt::ustring §ion); + bool BackendsCanWriteMultipleSettings() const; + void BackendsWriteMultipleSettings(const std::map &settings); void NotifyListeners(const SettingPath &path); SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const; bool IsDefaultSetting(const SettingPath &path) const; @@ -684,11 +688,26 @@ }; +enum class CachePolicy +{ + Direct, + Cached, +}; + class IniFileSettingsBackend : public ISettingsBackend { +public: private: const mpt::PathString filename; + std::optional>>> cache; +#ifdef ENABLE_TESTS +public: +#else private: +#endif + std::set ReadSectionNamesRaw() const; + std::map> ReadNamedSectionRaw(const mpt::winstring §ion) const; + std::map>> ReadAllSectionsRaw() const; std::vector ReadSettingRaw(const SettingPath &path, const std::vector &def) const; mpt::ustring ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const; double ReadSettingRaw(const SettingPath &path, double def) const; @@ -699,12 +718,17 @@ void WriteSettingRaw(const SettingPath &path, double val); void WriteSettingRaw(const SettingPath &path, int32 val); void WriteSettingRaw(const SettingPath &path, bool val); + void WriteSectionRaw(const mpt::winstring §ion, const std::map> &keyvalues); void RemoveSettingRaw(const SettingPath &path); void RemoveSectionRaw(const mpt::ustring §ion); +private: + static mpt::winstring FormatValueAsIni(const SettingValue &value); + static SettingValue ParseValueFromIni(const mpt::winstring &str, const SettingValue &def); static mpt::winstring GetSection(const SettingPath &path); static mpt::winstring GetKey(const SettingPath &path); public: - IniFileSettingsBackend(const mpt::PathString &filename); + [[deprecated]] IniFileSettingsBackend(const mpt::PathString &filename); + IniFileSettingsBackend(const mpt::PathString &filename, CachePolicy cachePolicy); ~IniFileSettingsBackend() override; void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring()); virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override; @@ -711,6 +735,9 @@ virtual void WriteSetting(const SettingPath &path, const SettingValue &val) override; virtual void RemoveSetting(const SettingPath &path) override; virtual void RemoveSection(const mpt::ustring §ion) override; + virtual bool CanBatchedSettings() const override; + virtual void WriteMultipleSettings(const std::map &settings) override; +public: const mpt::PathString& GetFilename() const { return filename; } }; @@ -717,7 +744,8 @@ class IniFileSettingsContainer : private IniFileSettingsBackend, public SettingsContainer { public: - IniFileSettingsContainer(const mpt::PathString &filename); + [[deprecated]] IniFileSettingsContainer(const mpt::PathString &filename); + IniFileSettingsContainer(const mpt::PathString &filename, CachePolicy cachePolicy); ~IniFileSettingsContainer() override; }; Index: test/test.cpp =================================================================== --- test/test.cpp (revision 24343) +++ test/test.cpp (working copy) @@ -2737,9 +2737,35 @@ VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("S1"), U_("bar1")}, U_("empty")).as(), U_("a")); VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("S1"), U_("bar2")}, U_("empty")).as(), U_("empty")); } + { + IniFileSettingsBackend inifile{filename}; + const std::set a = inifile.ReadSectionNamesRaw(); + for(const mpt::winstring & s : a) + { + std::map> b = inifile.ReadNamedSectionRaw(s); + } + } DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); } + { + const mpt::PathString filename = theApp.GetConfigPath() + P_("test.ini"); + std::vector data; + for(std::size_t i = 0; i < 10; ++i) + { + data.push_back(mpt::byte_cast(static_cast(i))); + } + { + IniFileSettingsBackend inifile{filename, CachePolicy::Cached}; + inifile.WriteMultipleSettings({{SettingPath(U_("Test"), U_("Data")), data}}); + } + { + IniFileSettingsBackend inifile{filename, CachePolicy::Cached}; + VERIFY_EQUAL(inifile.ReadSetting(SettingPath(U_("Test"), U_("Data")), SettingValue(data)).as>(), data); + } + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); + } + #endif // MODPLUG_TRACKER }