From 3ab002db1d713146c266409fc6ef33e0f06b3cfc Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 30 Jun 2026 20:19:18 +0200 Subject: [PATCH] fix: make info strings case insensitive (#871) --- src/ObjCommon/InfoString/InfoString.cpp | 66 +++++++++++-------- src/ObjCommon/InfoString/InfoString.h | 34 ++++++++-- src/Utils/Utils/StringUtils.cpp | 13 ++++ src/Utils/Utils/StringUtils.h | 4 ++ .../InfoString/InfoStringTests.cpp | 63 ++++++++++++++++++ 5 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 test/ObjCommonTests/InfoString/InfoStringTests.cpp diff --git a/src/ObjCommon/InfoString/InfoString.cpp b/src/ObjCommon/InfoString/InfoString.cpp index c2c7a70e..41655857 100644 --- a/src/ObjCommon/InfoString/InfoString.cpp +++ b/src/ObjCommon/InfoString/InfoString.cpp @@ -1,24 +1,29 @@ #include "InfoString.h" #include "Utils/Logging/Log.h" +#include "Utils/StringUtils.h" #include #include #include #include -const std::string InfoString::EMPTY_VALUE; +namespace +{ + constexpr const char* GDT_PREFIX_FIELD = "configstringFileType"; + const std::string EMPTY_VALUE; +} // namespace bool InfoString::HasKey(const std::string& key) const { - return m_values.find(key) != m_values.end(); + return m_value_lookup.contains(key); } const std::string& InfoString::GetValueForKey(const std::string& key) const { - const auto& value = m_values.find(key); + const auto& value = m_value_lookup.find(key); - if (value == m_values.end()) + if (value == m_value_lookup.end()) return EMPTY_VALUE; return value->second; @@ -26,9 +31,9 @@ const std::string& InfoString::GetValueForKey(const std::string& key) const const std::string& InfoString::GetValueForKey(const std::string& key, bool* foundValue) const { - const auto& value = m_values.find(key); + const auto& value = m_value_lookup.find(key); - if (value == m_values.end()) + if (value == m_value_lookup.end()) { if (foundValue) *foundValue = false; @@ -43,17 +48,23 @@ const std::string& InfoString::GetValueForKey(const std::string& key, bool* foun void InfoString::SetValueForKey(const std::string& key, std::string value) { if (!HasKey(key)) - m_keys_by_insertion.push_back(key); + m_keys_by_insertion.emplace_back(key); - m_values[key] = std::move(value); + m_value_lookup[key] = std::move(value); } void InfoString::RemoveKey(const std::string& key) { - const auto& value = m_values.find(key); + const auto& value = m_value_lookup.find(key); - if (value != m_values.end()) - m_values.erase(value); + if (value != m_value_lookup.end()) + m_value_lookup.erase(value); + + const auto insertion = std::ranges::find_if(m_keys_by_insertion, + [&key](const std::string& keyByInsertion) + { + return utils::StringEqualsIgnoreCase(key, keyByInsertion); + }); } std::string InfoString::ToString() const @@ -63,7 +74,7 @@ std::string InfoString::ToString() const for (const auto& key : m_keys_by_insertion) { - const auto value = m_values.find(key); + const auto value = m_value_lookup.find(key); if (!first) ss << '\\'; else @@ -82,7 +93,7 @@ std::string InfoString::ToString(const std::string& prefix) const for (const auto& key : m_keys_by_insertion) { - const auto value = m_values.find(key); + const auto value = m_value_lookup.find(key); ss << '\\' << key << '\\' << value->second; } @@ -93,7 +104,7 @@ void InfoString::ToGdtProperties(const std::string& prefix, GdtEntry& gdtEntry) { for (const auto& key : m_keys_by_insertion) { - const auto value = m_values.find(key); + const auto value = m_value_lookup.find(key); gdtEntry.m_properties[key] = value->second; } @@ -102,9 +113,6 @@ void InfoString::ToGdtProperties(const std::string& prefix, GdtEntry& gdtEntry) class InfoStringInputStream { - std::istream& m_stream; - int m_last_separator; - public: explicit InfoStringInputStream(std::istream& stream) : m_stream(stream), @@ -139,6 +147,10 @@ public: value = str.str(); return true; } + +private: + std::istream& m_stream; + int m_last_separator; }; bool InfoString::FromStream(std::istream& stream) @@ -152,11 +164,11 @@ bool InfoString::FromStream(std::istream& stream) if (!infoStream.NextField(value)) return false; - const auto existingEntry = m_values.find(key); - if (existingEntry == m_values.end()) + const auto existingEntry = m_value_lookup.find(key); + if (existingEntry == m_value_lookup.end()) { m_keys_by_insertion.push_back(key); - m_values.emplace(std::make_pair(key, value)); + m_value_lookup.emplace(std::make_pair(key, value)); } else { @@ -180,7 +192,7 @@ bool InfoString::FromStream(const std::string& prefix, std::istream& stream) if (prefix != readPrefix) { - con::error("Invalid info string: Prefix \"{}\" did not match expected prefix \"{}\"", readPrefix, prefix); + con::error(R"(Invalid info string: Prefix "{}" did not match expected prefix "{}")", readPrefix, prefix); return false; } @@ -204,11 +216,11 @@ bool InfoString::FromStream(const std::string& prefix, std::istream& stream) return false; } - const auto existingEntry = m_values.find(key); - if (existingEntry == m_values.end()) + const auto existingEntry = m_value_lookup.find(key); + if (existingEntry == m_value_lookup.end()) { m_keys_by_insertion.push_back(key); - m_values.emplace(std::make_pair(key, value)); + m_value_lookup.emplace(std::make_pair(key, value)); } else { @@ -239,11 +251,11 @@ bool InfoString::FromGdtProperties(const GdtEntry& gdtEntry) for (const auto& [key, value] : currentEntry->m_properties) { - auto existingEntry = m_values.find(key); - if (existingEntry == m_values.end()) + auto existingEntry = m_value_lookup.find(key); + if (existingEntry == m_value_lookup.end()) { m_keys_by_insertion.push_back(key); - m_values.emplace(std::make_pair(key, value)); + m_value_lookup.emplace(std::make_pair(key, value)); } else { diff --git a/src/ObjCommon/InfoString/InfoString.h b/src/ObjCommon/InfoString/InfoString.h index 771461ed..9d45d08c 100644 --- a/src/ObjCommon/InfoString/InfoString.h +++ b/src/ObjCommon/InfoString/InfoString.h @@ -1,6 +1,8 @@ #pragma once #include "Obj/Gdt/GdtEntry.h" +#include "Utils/StreamUtils.h" +#include "Utils/StringUtils.h" #include #include @@ -9,13 +11,9 @@ class InfoString { - static constexpr const char* GDT_PREFIX_FIELD = "configstringFileType"; - - static const std::string EMPTY_VALUE; - std::unordered_map m_values; - std::vector m_keys_by_insertion; - public: + InfoString() = default; + [[nodiscard]] bool HasKey(const std::string& key) const; [[nodiscard]] const std::string& GetValueForKey(const std::string& key) const; const std::string& GetValueForKey(const std::string& key, bool* foundValue) const; @@ -29,4 +27,28 @@ public: bool FromStream(std::istream& stream); bool FromStream(const std::string& prefix, std::istream& stream); bool FromGdtProperties(const GdtEntry& gdtEntry); + +private: + class HashValue + { + public: + std::size_t operator()(const std::string& s) const noexcept + { + std::string lower(s); + utils::MakeStringLowerCase(lower); + return std::hash{}(lower); + } + }; + + class EqualValue + { + public: + constexpr bool operator()(const std::string& lhs, const std::string& rhs) const + { + return utils::StringEqualsIgnoreCase(lhs, rhs); + } + }; + + std::unordered_map m_value_lookup; + std::vector m_keys_by_insertion; }; diff --git a/src/Utils/Utils/StringUtils.cpp b/src/Utils/Utils/StringUtils.cpp index 3f885a86..5328ccaa 100644 --- a/src/Utils/Utils/StringUtils.cpp +++ b/src/Utils/Utils/StringUtils.cpp @@ -4,6 +4,14 @@ #include #include +namespace +{ + bool ichar_equals(const char a, const char b) + { + return std::tolower(static_cast(a)) == std::tolower(static_cast(b)); + } +} // namespace + namespace utils { std::string EscapeStringForQuotationMarks(const std::string_view& str) @@ -115,6 +123,11 @@ namespace utils c = static_cast(toupper(static_cast(c))); } + bool StringEqualsIgnoreCase(std::string_view lhs, std::string_view rhs) + { + return std::ranges::equal(lhs, rhs, ichar_equals); + } + void StringTrimL(std::string& str) { str.erase(str.begin(), diff --git a/src/Utils/Utils/StringUtils.h b/src/Utils/Utils/StringUtils.h index 4c1efdf9..bcee2731 100644 --- a/src/Utils/Utils/StringUtils.h +++ b/src/Utils/Utils/StringUtils.h @@ -1,5 +1,7 @@ #pragma once + #include +#include #include namespace utils @@ -18,6 +20,8 @@ namespace utils void MakeStringUpperCase(char* str); void MakeStringUpperCase(std::string& str); + bool StringEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); + void StringTrimL(std::string& str); void StringTrimR(std::string& str); void StringTrim(std::string& str); diff --git a/test/ObjCommonTests/InfoString/InfoStringTests.cpp b/test/ObjCommonTests/InfoString/InfoStringTests.cpp new file mode 100644 index 00000000..3355a261 --- /dev/null +++ b/test/ObjCommonTests/InfoString/InfoStringTests.cpp @@ -0,0 +1,63 @@ +#include "InfoString/InfoString.h" + +#include +#include +#include + +namespace +{ + TEST_CASE("InfoString: Can parse from stream") + { + InfoString info; + + SECTION("without prefix") + { + std::istringstream ss(R"(foo\bar\test\value)"); + info.FromStream(ss); + } + SECTION("with prefix") + { + std::istringstream ss(R"(FOOFILE\foo\bar\test\value)"); + info.FromStream("FOOFILE", ss); + } + SECTION("from gdt") + { + GdtEntry entry; + entry.m_properties.emplace("foo", "bar"); + entry.m_properties.emplace("test", "value"); + info.FromGdtProperties(entry); + } + + REQUIRE(info.HasKey("foo")); + REQUIRE(info.HasKey("test")); + + REQUIRE(info.GetValueForKey("foo") == "bar"); + REQUIRE(info.GetValueForKey("test") == "value"); + } + + TEST_CASE("InfoString: Can create string") + { + InfoString info; + std::istringstream ss(R"(foo\bar\test\value)"); + info.FromStream(ss); + + SECTION("without prefix") + { + REQUIRE(info.ToString() == R"(foo\bar\test\value)"); + } + SECTION("with prefix") + { + REQUIRE(info.ToString("FOOFILE") == R"(FOOFILE\foo\bar\test\value)"); + } + } + + TEST_CASE("InfoString: Is case insensitive") + { + InfoString info; + std::istringstream ss(R"(foo\bar\test\value)"); + info.FromStream(ss); + + REQUIRE(info.GetValueForKey("FOO") == "bar"); + REQUIRE(info.GetValueForKey("TEST") == "value"); + } +} // namespace