Index: mptrack/Mptrack.h =================================================================== --- mptrack/Mptrack.h (revision 24861) +++ mptrack/Mptrack.h (working copy) @@ -33,10 +33,10 @@ class CDLSBank; class DebugSettings; class TrackerSettings; -class ImmediateWindowsIniFileSettingsBackend; +class CachedIniFileSettingsBackend; template class FileSettingsContainer; -using IniFileSettingsBackend = ImmediateWindowsIniFileSettingsBackend; -using IniFileSettingsContainer = FileSettingsContainer; +using IniFileSettingsBackend = CachedIniFileSettingsBackend; +using IniFileSettingsContainer = FileSettingsContainer; class SettingsContainer; class ComponentManagerSettings; namespace mpt Index: mptrack/Settings.h =================================================================== --- mptrack/Settings.h (revision 24861) +++ mptrack/Settings.h (working copy) @@ -20,6 +20,7 @@ #include #include +#include #include Index: mptrack/SettingsIni.cpp =================================================================== --- mptrack/SettingsIni.cpp (revision 24861) +++ mptrack/SettingsIni.cpp (working copy) @@ -14,6 +14,7 @@ #include "SettingsIni.h" #include "mpt/base/bit.hpp" +#include "mpt/base/integer.hpp" #include "mpt/base/macros.hpp" #include "mpt/base/memory.hpp" #include "mpt/base/saturate_cast.hpp" @@ -20,11 +21,18 @@ #include "mpt/base/utility.hpp" #include "mpt/binary/hex.hpp" #include "mpt/format/message_macros.hpp" +#include "mpt/parse/parse.hpp" #include "mpt/string/types.hpp" #include "mpt/textfile/textfile.hpp" +#include +#include #include +#include +#include #include +#include +#include #include #include @@ -176,6 +184,67 @@ +IniVersion IniFileHelpers::ProbeVersion(const std::vector &lines) +{ + IniVersion result; + if(!((lines.size() > 0) && (lines[0] == MPT_ULITERAL("[!Type]")))) + { + return result; + } + if(!((lines.size() > 1) && (lines[1] == MPT_ULITERAL("!Format=org.openmpt.fileformat.ini")))) + { + return result; + } + // assume our version from now + result.major = 4; + result.minor = 0; + result.patch = 0; + if(!(lines.size() > 4)) + { + return result; + } + std::vector linemajor = mpt::split(lines[2], U_("=")); + std::vector lineminor = mpt::split(lines[3], U_("=")); + std::vector linepatch = mpt::split(lines[4], U_("=")); + if((linemajor.size() != 2) || (linemajor[0] != U_("!VersionMajor"))) + { + return result; + } + if((lineminor.size() != 2) || (lineminor[0] != U_("!VersionMinor"))) + { + return result; + } + if((linepatch.size() != 2) || (linepatch[0] != U_("!VersionPatch"))) + { + return result; + } + result.major = mpt::parse(linemajor[1]); + result.minor = mpt::parse(lineminor[1]); + result.patch = mpt::parse(linepatch[1]); + return result; +} + +std::list> IniFileHelpers::CreateIniHeader(IniVersion version) +{ + std::list> result; + switch(version.major) + { + case 1: + break; + case 2: + break; + case 3: + break; + case 4: + result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!Format")}, U_("org.openmpt.fileformat.ini"))); + result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!VersionMajor")}, static_cast(version.major))); + result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!VersionMinor")}, static_cast(version.minor))); + result.emplace_back(std::make_pair(SettingPath{U_("!Type"), U_("!VersionPatch")}, static_cast(version.patch))); + break; + } + return result; +} + mpt::ustring IniFileHelpers::FormatValueAsIni(const SettingValue &value) { switch(value.GetType()) @@ -647,5 +716,1085 @@ } +#if MPT_USTRING_MODE_UTF8 +// We cannot escape natively in UTF8 because ^x00 escapes would be different. +// This has a 1.5x cost in ustring UTF8 mode, but luckily we do not care +// much at the moment. + +static inline bool is_hex(char32_t c) +{ + return (U'0' <= c && c <= U'9') || (U'a' <= c && c <= U'f') || (U'A' <= c && c <= U'F'); +} + +static mpt::ustring escape(mpt::ustring_view text_) +{ + std::u32string text = mpt::transcode(text_); + std::u32string result; + const std::size_t len = text.length(); + result.reserve(len); + for(std::size_t pos = 0; pos < len; ++pos) + { + const char32_t c = text[pos]; + if((c == U'^') || (c == U';') || (c == U'[') || (c == U']') || (c == U'=') || (c == U'\"') || (c == U'\'')) + { + result.push_back(U'^'); + result.push_back(c); + } else if(mpt::char_value(c) < 32) + { + result.push_back(U'^'); + result.push_back(U'x'); + result.append(mpt::fmtT::hex0<2>(mpt::char_value(c))); + } else if((0x80 <= mpt::char_value(c)) && (mpt::char_value(c) <= 0x9f)) + { + result.push_back(U'^'); + result.push_back(U'x'); + result.append(mpt::fmtT::hex0<2>(mpt::char_value(c))); + } else + { + result.push_back(c); + } + } + return mpt::transcode(result); +} + +static mpt::ustring unescape(mpt::ustring_view text_) +{ + std::u32string text = mpt::transcode(text_); + std::u32string result; + const std::size_t len = text.length(); + result.reserve(len); + for(std::size_t pos = 0; pos < len; ++pos) + { + std::size_t rem = len - pos; + if(text[pos] == U'^') + { + if((rem >= 2) && (text[pos+1] == U'^')) + { + result.push_back(U'^'); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == U';')) + { + result.push_back(U';'); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == U'[')) + { + result.push_back(U'['); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == U']')) + { + result.push_back(U']'); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == U'=')) + { + result.push_back(U'='); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == U'\"')) + { + result.push_back(U'\"'); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == U'\'')) + { + result.push_back(U'\''); + pos += 1; + } else if((rem >= 4) && (text[pos+1] == U'x') && is_hex(text[pos+2]) && is_hex(text[pos+3])) + { + result.append(std::u32string(1, static_cast(mpt::parse_hex(std::u32string{text.substr(pos + 2, 4)})))); + pos += 3; + } else if((rem >= 6) && (text[pos+1] == U'u') && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5])) + { + result.append(std::u32string(1, static_cast(mpt::parse_hex(std::u32string{text.substr(pos + 2, 4)})))); + pos += 5; + } else if((rem >= 10) && (text[pos+1] == U'U') && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9])) + { + result.append(std::u32string(1, static_cast(mpt::parse_hex(std::u32string{text.substr(pos + 2, 8)})))); + pos += 9; + } else + { + result.push_back(text[pos]); + } + } else + { + result.push_back(text[pos]); + } + } + return mpt::transcode(result); +} + +#else + +static inline bool is_hex(mpt::uchar c) +{ + return (MPT_UCHAR('0') <= c && c <= MPT_UCHAR('9')) || (MPT_UCHAR('a') <= c && c <= MPT_UCHAR('f')) || (MPT_UCHAR('A') <= c && c <= MPT_UCHAR('F')); +} + +static mpt::ustring escape(mpt::ustring_view text) +{ + mpt::ustring result; + const std::size_t len = text.length(); + result.reserve(len); + for(std::size_t pos = 0; pos < len; ++pos) + { + const mpt::uchar c = text[pos]; + if((c == MPT_UCHAR('^')) || (c == MPT_UCHAR(';')) || (c == MPT_UCHAR('[')) || (c == MPT_UCHAR(']')) || (c == MPT_UCHAR('=')) || (c == MPT_UCHAR('\"')) || (c == MPT_UCHAR('\''))) + { + result.push_back(MPT_UCHAR('^')); + result.push_back(c); + } else if(mpt::char_value(c) < 32) + { + result.push_back(MPT_UCHAR('^')); + result.push_back(MPT_UCHAR('x')); + result.append(mpt::ufmt::hex0<2>(mpt::char_value(c))); + } else if((0x80 <= mpt::char_value(c)) && (mpt::char_value(c) <= 0x9f)) + { + result.push_back(MPT_UCHAR('^')); + result.push_back(MPT_UCHAR('x')); + result.append(mpt::ufmt::hex0<2>(mpt::char_value(c))); + } else + { + result.push_back(c); + } + } + return result; +} + +static mpt::ustring unescape(mpt::ustring_view text) +{ + mpt::ustring result; + const std::size_t len = text.length(); + result.reserve(len); + for(std::size_t pos = 0; pos < len; ++pos) + { + std::size_t rem = len - pos; + if(text[pos] == MPT_UCHAR('^')) + { + if((rem >= 2) && (text[pos+1] == MPT_UCHAR('^'))) + { + result.push_back(MPT_UCHAR('^')); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(';'))) + { + result.push_back(MPT_UCHAR(';')); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('['))) + { + result.push_back(MPT_UCHAR('[')); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(']'))) + { + result.push_back(MPT_UCHAR(']')); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('='))) + { + result.push_back(MPT_UCHAR('=')); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\"'))) + { + result.push_back(MPT_UCHAR('\"')); + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\''))) + { + result.push_back(MPT_UCHAR('\'')); + pos += 1; + } else if((rem >= 4) && (text[pos+1] == MPT_UCHAR('x')) && is_hex(text[pos+2]) && is_hex(text[pos+3])) + { + result.push_back(static_cast(mpt::parse_hex(mpt::ustring{text.substr(pos + 2, 2)}))); + pos += 3; + } else if((rem >= 6) && (text[pos+1] == MPT_UCHAR('u')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5])) + { + result.append(mpt::transcode(std::u16string(1, static_cast(mpt::parse_hex(mpt::ustring{text.substr(pos + 2, 4)}))))); + pos += 5; + } else if((rem >= 10) && (text[pos+1] == MPT_UCHAR('U')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9])) + { + result.append(mpt::transcode(std::u32string(1, static_cast(mpt::parse_hex(mpt::ustring{text.substr(pos + 2, 8)}))))); + pos += 9; + } else + { + result.push_back(text[pos]); + } + } else + { + result.push_back(text[pos]); + } + } + return result; +} + +#endif + +static inline std::size_t find_unescaped_first(mpt::ustring_view text, mpt::uchar c) +{ + const std::size_t len = text.length(); + for(std::size_t pos = 0; pos < len; ++pos) + { + if(text[pos] == c) + { + return pos; + } + std::size_t rem = len - pos; + if(text[pos] == MPT_UCHAR('^')) + { + if((rem >= 2) && (text[pos+1] == MPT_UCHAR('^'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(';'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('['))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(']'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('='))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\"'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\''))) + { + pos += 1; + } else if((rem >= 4) && (text[pos+1] == MPT_UCHAR('x')) && is_hex(text[pos+2]) && is_hex(text[pos+3])) + { + pos += 3; + } else if((rem >= 6) && (text[pos+1] == MPT_UCHAR('u')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5])) + { + pos += 5; + } else if((rem >= 10) && (text[pos+1] == MPT_UCHAR('U')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9])) + { + pos += 9; + } + } + } + return mpt::ustring_view::npos; +} + +static inline std::size_t find_unescaped_last(mpt::ustring_view text, mpt::uchar c) +{ + std::size_t result = mpt::ustring_view::npos; + const std::size_t len = text.length(); + for(std::size_t pos = 0; pos < len; ++pos) + { + if(text[pos] == c) + { + result = pos; + } + std::size_t rem = len - pos; + if(text[pos] == MPT_UCHAR('^')) + { + if((rem >= 2) && (text[pos+1] == MPT_UCHAR('^'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(';'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('['))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR(']'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('='))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\"'))) + { + pos += 1; + } else if((rem >= 2) && (text[pos+1] == MPT_UCHAR('\''))) + { + pos += 1; + } else if((rem >= 4) && (text[pos+1] == MPT_UCHAR('x')) && is_hex(text[pos+2]) && is_hex(text[pos+3])) + { + pos += 3; + } else if((rem >= 6) && (text[pos+1] == MPT_UCHAR('u')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5])) + { + pos += 5; + } else if((rem >= 10) && (text[pos+1] == MPT_UCHAR('U')) && is_hex(text[pos+2]) && is_hex(text[pos+3]) && is_hex(text[pos+4]) && is_hex(text[pos+5]) && is_hex(text[pos+6]) && is_hex(text[pos+7]) && is_hex(text[pos+8]) && is_hex(text[pos+9])) + { + pos += 9; + } + } + } + return result; +} + +struct line_endings { + uint8 CRLF : 1 = 1; // (Atari, DOS, OS/2, Windows) + uint8 LFCR : 1 = 1; // (Acorn) + uint8 LF : 1 = 1; // U+0000'000A LINE FEED (Amiga, POSIX, Unix) + uint8 VT : 1 = 0; // U+0000'000B VERTICAL TAB + uint8 FF : 1 = 0; // U+0000'000C FORM FEED + uint8 CR : 1 = 1; // U+0000'000D CARRIAGE RETURN (C64, Mac OS Classic) + uint8 RS : 1 = 0; // U+0000'001E RECORD SEPARATOR (QNX < v4) + uint8 NEL : 1 = 0; // U+0000'0085 NEXT LINE (z/OS) + uint8 LS : 1 = 0; // U+0000'2028 LINE SEPARATOR + uint8 PS : 1 = 0; // U+0000'2029 PARAGRAPH SEPARATOR + [[nodiscard]] inline constexpr static line_endings Windows() noexcept + { + return {1, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + } + [[nodiscard]] inline constexpr static line_endings Acorn() noexcept + { + return {1, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + } + [[nodiscard]] inline constexpr static line_endings Unix() noexcept + { + return {0, 0, 0, 0, 0, 1, 0, 0, 0, 0}; + } + [[nodiscard]] inline constexpr static line_endings Mac() noexcept + { + return {0, 0, 0, 0, 0, 1, 0, 0, 0, 0}; + } + [[nodiscard]] inline constexpr static line_endings AnyASCII() noexcept + { + return {1, 1, 1, 0, 0, 1, 0, 0, 0, 0}; + } + [[nodiscard]] inline constexpr static line_endings AnyUnicode() noexcept + { + return {1, 1, 1, 1, 1, 1, 0, 1, 1, 1}; + } + [[nodiscard]] inline constexpr static line_endings EBCDIC() noexcept + { + return {0, 0, 0, 0, 0, 0, 0, 1, 0, 0}; + } + [[nodiscard]] inline constexpr static line_endings QNX() noexcept + { + return {0, 0, 0, 0, 0, 0, 1, 0, 0, 0}; + } + [[nodiscard]] friend inline constexpr line_endings operator|(line_endings a, line_endings b) noexcept + { + return { + static_cast(a.CRLF | b.CRLF), + static_cast(a.LFCR | b.LFCR), + static_cast(a.LF | b.LF ), + static_cast(a.VT | b.VT ), + static_cast(a.FF | b.FF ), + static_cast(a.CR | b.CR ), + static_cast(a.RS | b.RS ), + static_cast(a.NEL | b.NEL ), + static_cast(a.LS | b.LS ), + static_cast(a.PS | b.PS ) + }; + } +}; + +static inline std::vector split_lines(mpt::ustring_view text, line_endings sep = line_endings::AnyASCII()) +{ + std::vector result; + const std::size_t len = text.length(); + std::size_t beg = 0; + for(std::size_t pos = 0; pos < len; ++pos) + { + std::size_t rem = len - pos; + if(sep.CRLF && (rem >= 2) && (text[pos+0] == MPT_UCHAR('\r')) && (text[pos+1] == MPT_UCHAR('\n'))) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 2; + beg = pos; + } else if(sep.LFCR && (rem >= 2) && (text[pos+0] == MPT_UCHAR('\n')) && (text[pos+1] == MPT_UCHAR('\r'))) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 2; + beg = pos; + } else if(sep.LF && (rem >= 1) && text[pos+0] == MPT_UCHAR('\n')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.VT && (rem >= 1) && text[pos+0] == MPT_UCHAR('\v')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.FF && (rem >= 1) && text[pos+0] == MPT_UCHAR('\f')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.CR && (rem >= 1) && text[pos+0] == MPT_UCHAR('\r')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.RS && (rem >= 1) && text[pos+0] == MPT_UCHAR('\x1E')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else + { + if constexpr(sizeof(mpt::uchar) >= 4) + { + if(sep.NEL && (rem >= 1) && text[pos+0] == MPT_UCHAR('\U00000085')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.LS && (rem >= 1) && text[pos+0] == MPT_UCHAR('\U00002028')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.PS && (rem >= 1) && text[pos+0] == MPT_UCHAR('\U00002029')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } + } else if constexpr(sizeof(mpt::uchar) >= 2) + { + if(sep.NEL && (rem >= 1) && text[pos+0] == MPT_UCHAR('\u0085')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.LS && (rem >= 1) && text[pos+0] == MPT_UCHAR('\u2028')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } else if(sep.PS && (rem >= 1) && text[pos+0] == MPT_UCHAR('\u2029')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 1; + beg = pos; + } + } else + { + if(sep.NEL && (rem >= 2) && text[pos+0] == MPT_UCHAR('\xc2') && text[pos+1] == MPT_UCHAR('\x85')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 2; + beg = pos; + } else if(sep.LS && (rem >= 3) && text[pos+0] == MPT_UCHAR('\xe2') && text[pos+1] == MPT_UCHAR('\x80') && text[pos+3] == MPT_UCHAR('\xa8')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 3; + beg = pos; + } else if(sep.PS && (rem >= 3) && text[pos+0] == MPT_UCHAR('\xe2') && text[pos+1] == MPT_UCHAR('\x80') && text[pos+2] == MPT_UCHAR('\xa9')) + { + result.emplace_back(text.substr(beg, pos - beg)); + pos += 3; + beg = pos; + } + } + } + } + if(!result.empty() || ((len - beg) > 0)) + { + result.emplace_back(text.substr(beg, len - beg)); + } + return result; +} + +static inline mpt::ustring_view trim_whitespace(mpt::ustring_view text) +{ + mpt::ustring_view::size_type beg_pos = text.find_first_not_of(MPT_ULITERAL(" \t\r\n")); + if(beg_pos != mpt::ustring_view::npos) + { + text = text.substr(beg_pos); + } else + { + text = text.substr(text.length(), 0); + } + mpt::ustring_view::size_type end_pos = text.find_last_not_of(MPT_ULITERAL(" \t\r\n")); + if(end_pos != mpt::ustring_view::npos) + { + text = text.substr(0, end_pos + 1); + } else + { + text = text.substr(0, 0); + } + return text; +} + +static inline mpt::ustring_view remove_quotes(mpt::ustring_view text) +{ + if(text.length() <= 1) + { + return text; + } + { + mpt::ustring_view::size_type beg_quote = find_unescaped_first(text, MPT_UCHAR('\"')); + mpt::ustring_view::size_type end_quote = find_unescaped_last(text, MPT_UCHAR('\"')); + if((beg_quote == 0) && (end_quote == (text.length() - 1))) + { + return text.substr(1, text.length() - 2); + } + } + { + mpt::ustring_view::size_type beg_quote = find_unescaped_first(text, MPT_UCHAR('\'')); + mpt::ustring_view::size_type end_quote = find_unescaped_last(text, MPT_UCHAR('\'')); + if((beg_quote == 0) && (end_quote == (text.length() - 1))) + { + return text.substr(1, text.length() - 2); + } + } + return text; +} + +#if MPT_SETTINGS_INI_CASE_INSENSITIVE +static mpt::ustring lowercase(mpt::ustring_view text) +{ + return mpt::ToLowerCaseAscii(text); +} +#endif + +void CachedIniFileSettingsBackend::ReadFileIntoCache() +{ + ReadLinesIntoCache(ReadFileAsLines()); +} + +std::vector CachedIniFileSettingsBackend::ReadFileAsLines() +{ + return split_lines(DecodeText(file.read(), filename));; +} + +void CachedIniFileSettingsBackend::ReadLinesIntoCache(const std::vector &lines) +{ + ReadLinesIntoCache(ProbeVersion(lines), lines); +} + +void CachedIniFileSettingsBackend::ReadLinesIntoCache(IniVersion version, const std::vector &lines) +{ + cache.clear(); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + casemap.clear(); + } +#endif + mpt::ustring current_section; + mpt::ustring last_key; + bool empty_line_after_last_key = false; + std::vector running_comments; + std::size_t line_number = 0; + bool last_line_empty = false; + const bool quirk_no_unescape = (version.major < 4); + auto store_previous_comments = [&]() + { + if(!empty_line_after_last_key && (running_comments.size() > 0)) + { + comments[std::make_pair(current_section, last_key)].after = std::move(running_comments); + running_comments = {}; + } + }; + for(const auto &raw_line : lines) + { + line_number += 1; + last_line_empty = (raw_line.length() == 0); + mpt::ustring_view line = trim_whitespace(raw_line); + std::size_t line_len = line.length(); + if(line_len == 0) + { // empty line + store_previous_comments(); + empty_line_after_last_key = true; + continue; + } + if(find_unescaped_first(line, MPT_UCHAR(';')) == 0) + { // comment + running_comments.push_back(mpt::ustring{trim_whitespace(line.substr(1))}); + continue; + } + if(find_unescaped_first(line, MPT_UCHAR('[')) == 0) + { // section start + mpt::ustring_view::size_type opening_pos = 0; + mpt::ustring_view::size_type closing_pos = find_unescaped_first(line, MPT_UCHAR(']')); + if(closing_pos == mpt::ustring_view::npos) + { // syntax error + store_previous_comments(); + empty_line_after_last_key = true; + MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}({}): No closing bracket in section header. ('{}').")(filename, line_number, raw_line)); + continue; + } + mpt::ustring_view remainder = line.substr(closing_pos + 1); + if(trim_whitespace(remainder).length() > 0) + { // syntax error + MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Junk after section header. ('{}').")(filename, line_number, raw_line)); + } + mpt::ustring_view escaped_section = trim_whitespace(line.substr(opening_pos + 1, closing_pos - opening_pos - 1)); + mpt::ustring section = (quirk_no_unescape ? mpt::ustring{escaped_section} : unescape(escaped_section)); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + if(casemap.find(std::make_pair(lowercase(section), mpt::ustring{})) != casemap.end()) + { + store_previous_comments(); + empty_line_after_last_key = true; + current_section.clear(); + MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate casefolded section '{}'. ('{}').")(filename, line_number, escape(section), raw_line)); + continue; + } + casemap.emplace(std::make_pair(lowercase(section), mpt::ustring{}), std::make_pair(section, mpt::ustring{})); + } +#endif + if(cache.find(section) != cache.end()) + { // semantic error + store_previous_comments(); + empty_line_after_last_key = true; + current_section.clear(); + MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate section '{}'. ('{}').")(filename, line_number, escape(section), raw_line)); + continue; + } + MPT_LOG_GLOBAL(LogInformation, "Settings", MPT_UFORMAT_MESSAGE("{}({}): [{}]")(filename, line_number, escape(section))); + comments[std::make_pair(section, mpt::ustring{})].before = std::move(running_comments); + running_comments = {}; + cache[section].emplace(); + current_section = std::move(section); + last_key.clear(); + empty_line_after_last_key = false; + continue; + } + mpt::ustring_view::size_type equal_pos = find_unescaped_first(line, MPT_UCHAR('=')); + if(equal_pos == mpt::ustring_view::npos) + { // syntax error + store_previous_comments(); + empty_line_after_last_key = true; + MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Invalid token. ('{}').")(filename, line_number, raw_line)); + continue; + } + // key value + if(current_section.empty()) + { // syntax error + store_previous_comments(); + empty_line_after_last_key = true; + MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring key-value without section. ('{}').")(filename, line_number, raw_line)); + continue; + } + mpt::ustring_view escaped_key = trim_whitespace(line.substr(0, equal_pos)); + mpt::ustring_view escaped_val = remove_quotes(trim_whitespace(line.substr(equal_pos + 1))); + if(escaped_key.empty()) + { // semantic error + store_previous_comments(); + empty_line_after_last_key = true; + MPT_LOG_GLOBAL(LogError, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Empty key. ('{}').")(filename, line_number, raw_line)); + continue; + } + mpt::ustring key = (quirk_no_unescape ? mpt::ustring{escaped_key} : unescape(escaped_key)); + mpt::ustring val = (quirk_no_unescape ? mpt::ustring{escaped_val} : unescape(escaped_val)); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + if(casemap.find(std::make_pair(lowercase(current_section), lowercase(key))) != casemap.end()) + { + store_previous_comments(); + empty_line_after_last_key = true; + MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate casefolded key '{}'. ('{}').")(filename, line_number, escape(key), raw_line)); + continue; + } + } +#endif + if(cache[current_section].value().find(key) != cache[current_section].value().end()) + { // semantic error + store_previous_comments(); + empty_line_after_last_key = true; + MPT_LOG_GLOBAL(LogWarning, "Settings", MPT_UFORMAT_MESSAGE("{}({}): Ignoring duplicate key '{}'. ('{}').")(filename, line_number, escape(key), raw_line)); + continue; + } + MPT_LOG_GLOBAL(LogInformation, "Settings", MPT_UFORMAT_MESSAGE("{}({}): {}={}")(filename, line_number, escape(key), escape(val))); + comments[std::make_pair(current_section, key)].before = std::move(running_comments); + running_comments = {}; +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + casemap.emplace(std::make_pair(lowercase(current_section), lowercase(key)), std::make_pair(current_section, key)); + } +#endif + cache[current_section].value()[key] = std::make_optional(std::move(val)); + last_key = std::move(key); + empty_line_after_last_key = false; + } + store_previous_comments(); + if(!last_line_empty) + { + MPT_LOG_GLOBAL(LogNotification, "Settings", MPT_UFORMAT_MESSAGE("{}({}): {}")(filename, line_number, U_("No newline after last line."))); + } +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + VerifyCasemap(); + } +#endif +} + +void CachedIniFileSettingsBackend::MergeSettingsIntoCache(const std::set &removeSections, const std::map> &settings) +{ +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + for(const auto §ion : removeSections) + { + const auto mapping = casemap.find(std::make_pair(lowercase(section), mpt::ustring{})); + if(mapping != casemap.end()) + { + if(mapping->second.first != section) + { + const auto entry = cache.find(mapping->second.first); + if(entry != cache.end()) + { + auto node = cache.extract(entry); + node.key() = section; + cache.insert(std::move(node)); + auto casenode = casemap.extract(mapping); + mapping->second.first = section; + } + } + } + } + } +#endif + for(const auto §ion : removeSections) + { + cache.erase(section); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + casemap.erase(std::make_pair(lowercase(section), mpt::ustring{})); + } +#endif + } + std::map>> sectionssettings; + for(const auto &[path, value] : settings) + { + sectionssettings[path.GetRefSection()][path] = value; + } + for(const auto &[section, sectionsettings] : sectionssettings) + { +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + const auto mapping = casemap.find(std::make_pair(lowercase(section), mpt::ustring{})); + if(mapping == casemap.end()) + { + casemap.emplace(std::make_pair(lowercase(section), mpt::ustring{}), std::make_pair(section, mpt::ustring{})); + } else + { + if(mapping->second.first != section) + { + const auto entry = cache.find(mapping->second.first); + if(entry != cache.end()) + { + auto node = cache.extract(entry); + node.key() = section; + cache.insert(std::move(node)); + auto casenode = casemap.extract(mapping); + mapping->second.first = section; + } + } + } + } +#endif + if(!cache[section].has_value()) + { + cache[section].emplace(); + } + std::map> §ioncache = cache[section].value(); + for(const auto &[path, value] : sectionsettings) + { +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + const auto mapping = casemap.find(std::make_pair(lowercase(section), lowercase(path.GetRefKey()))); + if(mapping == casemap.end()) + { + casemap.emplace(std::make_pair(lowercase(section), lowercase(path.GetRefKey())), std::make_pair(section, path.GetRefKey())); + } else + { + if(mapping->second.second != path.GetRefKey()) + { + const auto entry = sectioncache.find(mapping->second.second); + if(entry != sectioncache.end()) + { + auto node = sectioncache.extract(entry); + node.key() = path.GetRefKey(); + sectioncache.insert(std::move(node)); + auto casenode = casemap.extract(mapping); + mapping->second.second = path.GetRefKey(); + } + } + } + } +#endif + if(value.has_value()) + { + sectioncache[path.GetRefKey()] = FormatValueAsIni(value.value()); + } else + { + sectioncache.erase(path.GetRefKey()); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + casemap.erase(std::make_pair(lowercase(section), lowercase(path.GetRefKey()))); + } +#endif + } + } + } +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + VerifyCasemap(); + } +#endif +} + +void CachedIniFileSettingsBackend::WriteCacheIntoFile(std::optional sync_hint) +{ + mpt::ustring filetext; + const std::list> header = CreateIniHeader(IniVersion{4, 0, 0}); + for(const auto &path : header) + { + cache.erase(path.first.GetRefSection()); + } + const mpt::uchar *newline = MPT_ULITERAL("\r\n"); + mpt::ustring last_section; + for(const auto &[path, value] : header) + { + if(path.GetRefSection() != last_section) + { + last_section = path.GetRefSection(); + filetext += MPT_UFORMAT_MESSAGE("[{}]{}")(escape(path.GetRefSection()), newline); + } + filetext += MPT_UFORMAT_MESSAGE("{}={}{}")(escape(path.GetRefKey()), escape(FormatValueAsIni(value)), newline); + } + for(const auto &[section, sectionsettings] : cache) + { + filetext += newline; + auto sectioncomments = comments.find(std::make_pair(section, mpt::ustring{})); + if(sectioncomments != comments.end()) + { + for(const auto &comment : sectioncomments->second.before) + { + filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline); + } + } + filetext += MPT_UFORMAT_MESSAGE("[{}]{}")(escape(section), newline); + if(sectioncomments != comments.end()) + { + for(const auto &comment : sectioncomments->second.after) + { + filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline); + } + if(sectioncomments->second.after.size() > 0) + { + filetext += newline; + } + } + if(sectionsettings.has_value()) + { + for(const auto &[key, value] : sectionsettings.value()) + { + if(value.has_value()) + { + auto keycomments = comments.find(std::make_pair(section, key)); + if(keycomments != comments.end()) + { + for(const auto &comment : keycomments->second.before) + { + filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline); + } + } + const mpt::ustring escaped_value = escape(value.value()); + if(escaped_value != trim_whitespace(escaped_value)) + { + filetext += MPT_UFORMAT_MESSAGE("{}=\"{}\"{}")(escape(key), escaped_value, newline); + } else + { + filetext += MPT_UFORMAT_MESSAGE("{}={}{}")(escape(key), escaped_value, newline); + } + if(keycomments != comments.end()) + { + for(const auto &comment : keycomments->second.after) + { + filetext += MPT_UFORMAT_MESSAGE(";{}{}")(comment, newline); + } + if(keycomments->second.after.size() > 0) + { + filetext += newline; + } + } + } + } + } + } + file.write(mpt::textfile::encode(mpt::textfile::get_preferred_encoding(), filetext), sync_hint.value_or(sync_default.value_or(Caching::WriteBack)) == Caching::WriteThrough); +} + +#if MPT_SETTINGS_INI_CASE_INSENSITIVE +void CachedIniFileSettingsBackend::VerifyCasemap() const +{ +#ifndef NDEBUG + if(case_sensitivity == CaseSensitivity::Insensitive) + { + for(const auto &[section, optkeyvalues] : cache) + { + MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), mpt::ustring{})) != casemap.end()); + MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), mpt::ustring{}))->second == std::make_pair(section, mpt::ustring{})); + if(optkeyvalues.has_value()) + { + const auto §ioncache = optkeyvalues.value(); + for(const auto &[key, value] : sectioncache) + { + MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), lowercase(key))) != casemap.end()); + MPT_ASSERT(casemap.find(std::make_pair(lowercase(section), lowercase(key)))->second == std::make_pair(section, key)); + } + } + } + for(const auto &[folded, unfolded] : casemap) + { + MPT_ASSERT(folded.first == lowercase(unfolded.first)); + MPT_ASSERT(folded.second == lowercase(unfolded.second)); + MPT_ASSERT(cache.find(unfolded.first) != cache.end()); + if(folded.second != mpt::ustring{}) + { + MPT_ASSERT(cache.find(unfolded.first)->second.has_value()); + MPT_ASSERT(cache.find(unfolded.first)->second.value().find(unfolded.second) != cache.find(unfolded.first)->second.value().end()); + } + } + } +#endif +} +#endif + +void CachedIniFileSettingsBackend::InvalidateCache() +{ + OPENMPT_PROFILE_FUNCTION(Profiler::Settings); + std::shared_lock l(file); + ReadFileIntoCache(); +} + +SettingValue CachedIniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const +{ + OPENMPT_PROFILE_FUNCTION(Profiler::Settings); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + if(case_sensitivity == CaseSensitivity::Insensitive) + { + const auto mapping = casemap.find(std::make_pair(lowercase(path.GetRefSection()), lowercase(path.GetRefKey()))); + if(mapping == casemap.end()) + { + return def; + } + const SettingPath path2 = SettingPath{mapping->second.first, mapping->second.second}; + const auto sectionit = cache.find(path2.GetRefSection()); + if(sectionit == cache.end()) + { + return def; + } + if(!sectionit->second.has_value()) + { + return def; + } + const std::map> §ion = sectionit->second.value(); + const auto it = section.find(path2.GetRefKey()); + if(it == section.end()) + { + return def; + } + if(!it->second.has_value()) + { + return def; + } + return ParseValueFromIni(it->second.value(), def); + } +#endif + const auto sectionit = cache.find(path.GetRefSection()); + if(sectionit == cache.end()) + { + return def; + } + if(!sectionit->second.has_value()) + { + return def; + } + const std::map> §ion = sectionit->second.value(); + const auto it = section.find(path.GetRefKey()); + if(it == section.end()) + { + return def; + } + if(!it->second.has_value()) + { + return def; + } + return ParseValueFromIni(it->second.value(), def); +} + +void CachedIniFileSettingsBackend::WriteAllSettings(const std::set &removeSections, const std::map> &settings, std::optional sync_hint) +{ + OPENMPT_PROFILE_FUNCTION(Profiler::Settings); + std::unique_lock l(file); + ReadFileIntoCache(); + MergeSettingsIntoCache(removeSections, settings); + WriteCacheIntoFile(sync_hint); +} + +void CachedIniFileSettingsBackend::Init() +{ + OPENMPT_PROFILE_FUNCTION(Profiler::Settings); + std::shared_lock l(file); + const std::vector lines = ReadFileAsLines(); + const IniVersion version = ProbeVersion(lines); + if(version.major < 4) + { + MakeBackup(P_("win32"), sync_default); + } + ReadLinesIntoCache(version, lines); +} + +#if MPT_SETTINGS_INI_CASE_INSENSITIVE +CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_) + : IniFileBase(std::move(filename_), std::nullopt) + , case_sensitivity(CaseSensitivity::Insensitive) +{ + Init(); +} +CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_, std::optional sync_hint) + : IniFileBase(std::move(filename_), sync_hint) + , case_sensitivity(CaseSensitivity::Insensitive) +{ + Init(); +} +CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_, std::optional sync_hint, CaseSensitivity case_sensitivity_) + : IniFileBase(std::move(filename_), sync_hint) + , case_sensitivity(case_sensitivity_) +{ + Init(); +} +CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_, CaseSensitivity case_sensitivity_) + : IniFileBase(std::move(filename_), std::nullopt) + , case_sensitivity(case_sensitivity_) +{ + Init(); +} +CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_, CaseSensitivity case_sensitivity_, std::optional sync_hint) + : IniFileBase(std::move(filename_), sync_hint) + , case_sensitivity(case_sensitivity_) +{ + Init(); +} +#else +CachedIniFileSettingsBackend::CachedIniFileSettingsBackend(mpt::PathString filename_, std::optional sync_hint) + : IniFileBase(std::move(filename_), sync_hint) +{ + Init(); +} +#endif + +CachedIniFileSettingsBackend::~CachedIniFileSettingsBackend() +{ + return; +} + +CaseSensitivity CachedIniFileSettingsBackend::GetCaseSensitivity() const +{ +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + return case_sensitivity; +#else + return CaseSensitivity::Sensitive; +#endif +} + + + OPENMPT_NAMESPACE_END Index: mptrack/SettingsIni.h =================================================================== --- mptrack/SettingsIni.h (revision 24861) +++ mptrack/SettingsIni.h (working copy) @@ -15,11 +15,16 @@ #include "Settings.h" +#include "mpt/base/integer.hpp" +#include "mpt/base/span.hpp" #include "mpt/io_file_atomic/atomic_file.hpp" +#include "mpt/string/types.hpp" +#include #include #include #include +#include #include #include @@ -28,10 +33,54 @@ OPENMPT_NAMESPACE_BEGIN +#define MPT_SETTINGS_INI_CASE_INSENSITIVE 1 + +// Version 1: +// * encoding: ANSI + +// Version 2: +// * encoding: UTF-16LE with BOM + +// Version 3: +// * encoding: UTF-8 with BOM (Wine-only, not supported by Windows) + +// Version 1 / 2 / 3: +// * no header +// * no escaping +// * string quoting with " or ' to support whitespace +// * case-insensitive + +// Version 4: +// * encoding: any Unicode encoding with BOM, ANSI/Locale without BOM +// * header: +// * [!Type] +// !Format=org.openmpt.fileformat.ini +// !VersionMajor=4 +// !VersionMinor=0 +// !VersionPatch=0 +// * escaping: +// * ^xff / ^uffff / ^Uffffffff +// Unicode codepoint ([0x00..0x1f], [0x80..0x9f]) +// * ^^ / ^; / ^[ / ^] / ^= / ^" / ^' +// Significant syntax elements +// * string quoting with " or ' to support whitespace +// * optionally case-sensitive or case-insensitive +// * ; Comments on separate lines + + +struct IniVersion +{ + uint8 major = 0; + uint8 minor = 0; + uint8 patch = 0; +}; + class IniFileHelpers { protected: + static IniVersion ProbeVersion(const std::vector &lines); + static std::list> CreateIniHeader(IniVersion version); static mpt::ustring FormatValueAsIni(const SettingValue &value); static SettingValue ParseValueFromIni(const mpt::ustring &str, const SettingValue &def); }; @@ -125,9 +174,58 @@ }; -using IniFileSettingsBackend = ImmediateWindowsIniFileSettingsBackend; +class CachedIniFileSettingsBackend + : public ISettingsBackend + , public IniFileBase + , protected IniFileHelpers +{ +private: + struct comments + { + std::vector before; + std::vector after; + }; +private: + std::map>>> cache; + std::map, comments> comments; +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + const CaseSensitivity case_sensitivity; + std::map, std::pair> casemap; +#endif +private: + std::vector ReadFileAsLines(); + void ReadLinesIntoCache(const std::vector &lines); + void ReadLinesIntoCache(IniVersion version, const std::vector &lines); + void ReadFileIntoCache(); + void MergeSettingsIntoCache(const std::set &removeSections, const std::map> &settings); + void WriteCacheIntoFile(std::optional sync_hint); +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + void VerifyCasemap() const; +#endif +private: + void Init(); +public: +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + CachedIniFileSettingsBackend(mpt::PathString filename_); + CachedIniFileSettingsBackend(mpt::PathString filename_, std::optional sync_hint); + CachedIniFileSettingsBackend(mpt::PathString filename_, std::optional sync_hint, CaseSensitivity case_sensitivity_); + CachedIniFileSettingsBackend(mpt::PathString filename_, CaseSensitivity case_sensitivity_); + CachedIniFileSettingsBackend(mpt::PathString filename_, CaseSensitivity case_sensitivity_, std::optional sync_hint); +#else + CachedIniFileSettingsBackend(mpt::PathString filename_, std::optional sync_hint = std::nullopt); +#endif + ~CachedIniFileSettingsBackend() override; +public: + virtual CaseSensitivity GetCaseSensitivity() const override; + virtual void InvalidateCache() override; + virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override; + virtual void WriteAllSettings(const std::set &removeSections, const std::map> &settings, std::optional sync_hint) override; +}; -using IniFileSettingsContainer = FileSettingsContainer; +using IniFileSettingsBackend = CachedIniFileSettingsBackend; +using IniFileSettingsContainer = FileSettingsContainer; + + OPENMPT_NAMESPACE_END Index: test/test.cpp =================================================================== --- test/test.cpp (revision 24861) +++ test/test.cpp (working copy) @@ -3075,9 +3075,11 @@ TestIniSettingsBackendRead(filename); TestIniSettingsBackendRead(filename); + TestIniSettingsBackendRead(filename); TestIniSettingsBackendReadCaseInsensitive(filename); TestIniSettingsBackendReadCaseInsensitive(filename); + TestIniSettingsBackendReadCaseInsensitive(filename); { IniFileSettingsContainer conf{filename}; @@ -3197,6 +3199,28 @@ DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); } + { + { + FileSettingsContainer conf{filename}; + conf.Write(SettingPath(MPT_USTRING("Test"), MPT_USTRING("spacespacecharspacespace")), spacespacecharspacespace); + conf.Write(SettingPath(MPT_USTRING("Test"), MPT_USTRING("threespaces")), threespaces); + conf.Write(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tab")), tab); + conf.Write(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tokens")), tokens); + conf.Write(SettingPath(MPT_USTRING("Test"), MPT_USTRING("xcrlfy")), xcrlfy); + conf.Write(SettingPath(MPT_USTRING("Test"), MPT_USTRING("cc0")), cc0); + } + { + FileSettingsContainer conf{filename}; + VERIFY_EQUAL(conf.Read(SettingPath(MPT_USTRING("Test"), MPT_USTRING("spacespacecharspacespace"))), spacespacecharspacespace); + VERIFY_EQUAL(conf.Read(SettingPath(MPT_USTRING("Test"), MPT_USTRING("threespaces"))), threespaces); + VERIFY_EQUAL(conf.Read(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tab"))), tab); + VERIFY_EQUAL(conf.Read(SettingPath(MPT_USTRING("Test"), MPT_USTRING("tokens"))), tokens); + VERIFY_EQUAL(conf.Read(SettingPath(MPT_USTRING("Test"), MPT_USTRING("xcrlfy"))), xcrlfy); + VERIFY_EQUAL(conf.Read(SettingPath(MPT_USTRING("Test"), MPT_USTRING("cc0"))), cc0); + } + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); + } + // escaping { DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); @@ -3216,7 +3240,70 @@ } DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); } + { + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); + { + mpt::IO::SafeOutputFile outputfile{filename, std::ios::binary}; + mpt::IO::ofstream & outputstream = outputfile.stream(); + mpt::IO::WriteTextCRLF(outputstream, "[Test]"); + mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo1 = ^"))); + mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo2 = ^^"))); + mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo3 = ^^^"))); + } + { + FileSettingsContainer inifile{filename}; + VERIFY_EQUAL(inifile.Read(SettingPath{U_("Test"), U_("Foo1")}, U_("")), U_("^")); + VERIFY_EQUAL(inifile.Read(SettingPath{U_("Test"), U_("Foo2")}, U_("")), U_("^^")); + VERIFY_EQUAL(inifile.Read(SettingPath{U_("Test"), U_("Foo3")}, U_("")), U_("^^^")); + } + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); + } + { + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); + { + mpt::IO::SafeOutputFile outputfile{filename, std::ios::binary}; + mpt::IO::ofstream & outputstream = outputfile.stream(); + mpt::IO::WriteTextCRLF(outputstream, "[!Type]"); + mpt::IO::WriteTextCRLF(outputstream, "!Format=org.openmpt.fileformat.ini"); + mpt::IO::WriteTextCRLF(outputstream, "!VersionMajor=4"); + mpt::IO::WriteTextCRLF(outputstream, "!VersionMinor=0"); + mpt::IO::WriteTextCRLF(outputstream, "!VersionPatch=0"); + mpt::IO::WriteTextCRLF(outputstream, "[Test]"); + mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo1 = ^"))); + mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo2 = ^^"))); + mpt::IO::WriteTextCRLF(outputstream, mpt::ToCharset(mpt::Charset::UTF8, U_("Foo3 = ^^^"))); + } + { + FileSettingsContainer inifile{filename}; + VERIFY_EQUAL(inifile.Read(SettingPath{U_("Test"), U_("Foo1")}, U_("")), U_("^")); + VERIFY_EQUAL(inifile.Read(SettingPath{U_("Test"), U_("Foo2")}, U_("")), U_("^")); + VERIFY_EQUAL(inifile.Read(SettingPath{U_("Test"), U_("Foo3")}, U_("")), U_("^^")); + } + DeleteFile(mpt::support_long_path(filename.AsNative()).c_str()); + } + // case sensitivity + { + { + FileSettingsContainer inifile{filename}; + inifile.Write(SettingPath{U_("Test"), U_("foo")}, U_("a")); + } + { + FileSettingsContainer inifile{filename}; + inifile.Read(SettingPath{U_("Test"), U_("Foo")}, U_("b")); + inifile.Write(SettingPath{U_("Test"), U_("Foo")}, U_("c")); + } + { +#if MPT_SETTINGS_INI_CASE_INSENSITIVE + CachedIniFileSettingsBackend inifile{filename, CaseSensitivity::Sensitive}; +#else + CachedIniFileSettingsBackend inifile{filename}; +#endif + VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("Test"), U_("Foo")}, SettingValue{U_("")}).as(), U_("")); + VERIFY_EQUAL(inifile.ReadSetting(SettingPath{U_("Test"), U_("foo")}, SettingValue{U_("")}).as(), U_("c")); + } + } + #endif // MODPLUG_TRACKER }