diff --git a/Minecraft.Server/Access/Access.cpp b/Minecraft.Server/Access/Access.cpp index 30b1cf9b6..5767a9555 100644 --- a/Minecraft.Server/Access/Access.cpp +++ b/Minecraft.Server/Access/Access.cpp @@ -5,10 +5,8 @@ #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" -#include #include #include -#include namespace ServerRuntime { @@ -86,31 +84,13 @@ namespace ServerRuntime return false; } - std::string trimmed = StringUtils::TrimAscii(text); - if (trimmed.empty()) + unsigned long long parsed = 0; + if (!StringUtils::TryParseUnsignedLongLong(text, &parsed) || parsed == 0ULL) { return false; } - errno = 0; - char *end = nullptr; - unsigned long long value = _strtoui64(trimmed.c_str(), &end, 0); - if (end == trimmed.c_str() || errno != 0) - { - return false; - } - - while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') - { - ++end; - } - - if (*end != 0 || value == 0ULL) - { - return false; - } - - *outXuid = (PlayerUID)value; + *outXuid = (PlayerUID)parsed; return true; } diff --git a/Minecraft.Server/Access/BanManager.cpp b/Minecraft.Server/Access/BanManager.cpp index 9180b5d92..59f5bccc0 100644 --- a/Minecraft.Server/Access/BanManager.cpp +++ b/Minecraft.Server/Access/BanManager.cpp @@ -2,13 +2,14 @@ #include "BanManager.h" +#include "..\Common\AccessStorageUtils.h" #include "..\Common\FileUtils.h" +#include "..\Common\NetworkUtils.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" #include "..\vendor\nlohmann\json.hpp" #include -#include #include namespace ServerRuntime @@ -22,85 +23,6 @@ namespace ServerRuntime static const char *kBannedPlayersFileName = "banned-players.json"; static const char *kBannedIpsFileName = "banned-ips.json"; - static bool FileExists(const std::string &path) - { - const std::wstring widePath = StringUtils::Utf8ToWide(path); - if (widePath.empty()) - { - return false; - } - - DWORD attrs = GetFileAttributesW(widePath.c_str()); - return (attrs != INVALID_FILE_ATTRIBUTES) && ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0); - } - - /** - * Creates an empty array file for access lists that do not exist yet - * 未作成のアクセス一覧に空配列ファイルを作る - */ - static bool EnsureJsonListFile(const std::string &path) - { - if (FileExists(path)) - { - return true; - } - return FileUtils::WriteTextFileAtomic(path, "[]\n"); - } - - static bool TryParseNumericXuid(const std::string &text, unsigned long long *outValue) - { - if (outValue == nullptr) - { - return false; - } - - std::string trimmed = StringUtils::TrimAscii(text); - if (trimmed.empty()) - { - return false; - } - - errno = 0; - char *end = nullptr; - // Accept both decimal and hexadecimal XUID text so manual edits and tool output stay compatible. - unsigned long long value = _strtoui64(trimmed.c_str(), &end, 0); - if (end == trimmed.c_str() || errno != 0) - { - return false; - } - - while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') - { - ++end; - } - - if (*end != 0) - { - return false; - } - - *outValue = value; - return true; - } - - static bool TryGetStringField(const OrderedJson &object, const char *key, std::string *outValue) - { - if (key == nullptr || outValue == nullptr || !object.is_object()) - { - return false; - } - - OrderedJson::const_iterator it = object.find(key); - if (it == object.end() || !it->is_string()) - { - return false; - } - - *outValue = it->get(); - return true; - } - - static bool TryParseUtcTimestamp(const std::string &text, unsigned long long *outFileTime) { if (outFileTime == nullptr) @@ -173,8 +95,8 @@ namespace ServerRuntime const std::string playersPath = GetBannedPlayersFilePath(); const std::string ipsPath = GetBannedIpsFilePath(); - bool playersOk = EnsureJsonListFile(playersPath); - bool ipsOk = EnsureJsonListFile(ipsPath); + const bool playersOk = AccessStorageUtils::EnsureJsonListFileExists(playersPath); + const bool ipsOk = AccessStorageUtils::EnsureJsonListFileExists(ipsPath); if (!playersOk) { LogErrorf("access", "failed to create %s", playersPath.c_str()); @@ -253,9 +175,8 @@ namespace ServerRuntime } const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime(); - for (size_t i = 0; i < root.size(); ++i) + for (const auto &object : root) { - const OrderedJson &object = root[i]; if (!object.is_object()) { LogWarnf("access", "skipping banned player entry that is not an object in %s", path.c_str()); @@ -263,25 +184,25 @@ namespace ServerRuntime } std::string rawXuid; - if (!TryGetStringField(object, "xuid", &rawXuid)) + if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid)) { LogWarnf("access", "skipping banned player entry without xuid in %s", path.c_str()); continue; } BannedPlayerEntry entry; - entry.xuid = NormalizeXuid(rawXuid); + entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid); if (entry.xuid.empty()) { LogWarnf("access", "skipping banned player entry with empty xuid in %s", path.c_str()); continue; } - TryGetStringField(object, "name", &entry.name); - TryGetStringField(object, "created", &entry.metadata.created); - TryGetStringField(object, "source", &entry.metadata.source); - TryGetStringField(object, "expires", &entry.metadata.expires); - TryGetStringField(object, "reason", &entry.metadata.reason); + AccessStorageUtils::TryGetStringField(object, "name", &entry.name); + AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); + AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); + AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires); + AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason); NormalizeMetadata(&entry.metadata); // Ignore entries that already expired before reload so the in-memory cache starts from the active set. @@ -334,9 +255,8 @@ namespace ServerRuntime } const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime(); - for (size_t i = 0; i < root.size(); ++i) + for (const auto &object : root) { - const OrderedJson &object = root[i]; if (!object.is_object()) { LogWarnf("access", "skipping banned ip entry that is not an object in %s", path.c_str()); @@ -344,24 +264,24 @@ namespace ServerRuntime } std::string rawIp; - if (!TryGetStringField(object, "ip", &rawIp)) + if (!AccessStorageUtils::TryGetStringField(object, "ip", &rawIp)) { LogWarnf("access", "skipping banned ip entry without ip in %s", path.c_str()); continue; } BannedIpEntry entry; - entry.ip = NormalizeIp(rawIp); + entry.ip = NetworkUtils::NormalizeIpToken(rawIp); if (entry.ip.empty()) { LogWarnf("access", "skipping banned ip entry with empty ip in %s", path.c_str()); continue; } - TryGetStringField(object, "created", &entry.metadata.created); - TryGetStringField(object, "source", &entry.metadata.source); - TryGetStringField(object, "expires", &entry.metadata.expires); - TryGetStringField(object, "reason", &entry.metadata.reason); + AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); + AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); + AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires); + AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason); NormalizeMetadata(&entry.metadata); // Ignore entries that already expired before reload so the in-memory cache starts from the active set. @@ -378,15 +298,15 @@ namespace ServerRuntime bool BanManager::SavePlayers(const std::vector &entries) const { OrderedJson root = OrderedJson::array(); - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { OrderedJson object = OrderedJson::object(); - object["xuid"] = NormalizeXuid(entries[i].xuid); - object["name"] = entries[i].name; - object["created"] = entries[i].metadata.created; - object["source"] = entries[i].metadata.source; - object["expires"] = entries[i].metadata.expires; - object["reason"] = entries[i].metadata.reason; + object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid); + object["name"] = entry.name; + object["created"] = entry.metadata.created; + object["source"] = entry.metadata.source; + object["expires"] = entry.metadata.expires; + object["reason"] = entry.metadata.reason; root.push_back(object); } @@ -403,14 +323,14 @@ namespace ServerRuntime bool BanManager::SaveIps(const std::vector &entries) const { OrderedJson root = OrderedJson::array(); - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { OrderedJson object = OrderedJson::object(); - object["ip"] = NormalizeIp(entries[i].ip); - object["created"] = entries[i].metadata.created; - object["source"] = entries[i].metadata.source; - object["expires"] = entries[i].metadata.expires; - object["reason"] = entries[i].metadata.reason; + object["ip"] = NetworkUtils::NormalizeIpToken(entry.ip); + object["created"] = entry.metadata.created; + object["source"] = entry.metadata.source; + object["expires"] = entry.metadata.expires; + object["reason"] = entry.metadata.reason; root.push_back(object); } @@ -445,11 +365,11 @@ namespace ServerRuntime outEntries->reserve(m_bannedPlayers.size()); const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime(); - for (size_t i = 0; i < m_bannedPlayers.size(); ++i) + for (const auto &entry : m_bannedPlayers) { - if (!IsMetadataExpired(m_bannedPlayers[i].metadata, nowFileTime)) + if (!IsMetadataExpired(entry.metadata, nowFileTime)) { - outEntries->push_back(m_bannedPlayers[i]); + outEntries->push_back(entry); } } return true; @@ -466,51 +386,49 @@ namespace ServerRuntime outEntries->reserve(m_bannedIps.size()); const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime(); - for (size_t i = 0; i < m_bannedIps.size(); ++i) + for (const auto &entry : m_bannedIps) { - if (!IsMetadataExpired(m_bannedIps[i].metadata, nowFileTime)) + if (!IsMetadataExpired(entry.metadata, nowFileTime)) { - outEntries->push_back(m_bannedIps[i]); + outEntries->push_back(entry); } } return true; } bool BanManager::IsPlayerBannedByXuid(const std::string &xuid) const { - const std::string normalized = NormalizeXuid(xuid); + const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid); if (normalized.empty()) { return false; } const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime(); - for (size_t i = 0; i < m_bannedPlayers.size(); ++i) - { - if (m_bannedPlayers[i].xuid == normalized && !IsMetadataExpired(m_bannedPlayers[i].metadata, nowFileTime)) + return std::any_of( + m_bannedPlayers.begin(), + m_bannedPlayers.end(), + [&normalized, nowFileTime](const BannedPlayerEntry &entry) { - return true; - } - } - return false; + return entry.xuid == normalized && !IsMetadataExpired(entry.metadata, nowFileTime); + }); } bool BanManager::IsIpBanned(const std::string &ip) const { - const std::string normalized = NormalizeIp(ip); + const std::string normalized = NetworkUtils::NormalizeIpToken(ip); if (normalized.empty()) { return false; } const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime(); - for (size_t i = 0; i < m_bannedIps.size(); ++i) - { - if (m_bannedIps[i].ip == normalized && !IsMetadataExpired(m_bannedIps[i].metadata, nowFileTime)) + return std::any_of( + m_bannedIps.begin(), + m_bannedIps.end(), + [&normalized, nowFileTime](const BannedIpEntry &entry) { - return true; - } - } - return false; + return entry.ip == normalized && !IsMetadataExpired(entry.metadata, nowFileTime); + }); } bool BanManager::AddPlayerBan(const BannedPlayerEntry &entry) @@ -522,26 +440,30 @@ namespace ServerRuntime } BannedPlayerEntry normalized = entry; - normalized.xuid = NormalizeXuid(normalized.xuid); + normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid); NormalizeMetadata(&normalized.metadata); if (normalized.xuid.empty()) { return false; } - for (size_t i = 0; i < updatedEntries.size(); ++i) + const auto existing = std::find_if( + updatedEntries.begin(), + updatedEntries.end(), + [&normalized](const BannedPlayerEntry &candidate) + { + return candidate.xuid == normalized.xuid; + }); + if (existing != updatedEntries.end()) { // Update the existing entry in place so the stored list remains unique by canonical XUID. - if (updatedEntries[i].xuid == normalized.xuid) + *existing = normalized; + if (!SavePlayers(updatedEntries)) { - updatedEntries[i] = normalized; - if (!SavePlayers(updatedEntries)) - { - return false; - } - m_bannedPlayers.swap(updatedEntries); - return true; + return false; } + m_bannedPlayers.swap(updatedEntries); + return true; } updatedEntries.push_back(normalized); @@ -562,26 +484,30 @@ namespace ServerRuntime } BannedIpEntry normalized = entry; - normalized.ip = NormalizeIp(normalized.ip); + normalized.ip = NetworkUtils::NormalizeIpToken(normalized.ip); NormalizeMetadata(&normalized.metadata); if (normalized.ip.empty()) { return false; } - for (size_t i = 0; i < updatedEntries.size(); ++i) + const auto existing = std::find_if( + updatedEntries.begin(), + updatedEntries.end(), + [&normalized](const BannedIpEntry &candidate) + { + return candidate.ip == normalized.ip; + }); + if (existing != updatedEntries.end()) { // Update the existing entry in place so the stored list remains unique by normalized IP. - if (updatedEntries[i].ip == normalized.ip) + *existing = normalized; + if (!SaveIps(updatedEntries)) { - updatedEntries[i] = normalized; - if (!SaveIps(updatedEntries)) - { - return false; - } - m_bannedIps.swap(updatedEntries); - return true; + return false; } + m_bannedIps.swap(updatedEntries); + return true; } updatedEntries.push_back(normalized); @@ -595,7 +521,7 @@ namespace ServerRuntime bool BanManager::RemovePlayerBanByXuid(const std::string &xuid) { - const std::string normalized = NormalizeXuid(xuid); + const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid); if (normalized.empty()) { return false; @@ -630,7 +556,7 @@ namespace ServerRuntime bool BanManager::RemoveIpBan(const std::string &ip) { - const std::string normalized = NormalizeIp(ip); + const std::string normalized = NetworkUtils::NormalizeIpToken(ip); if (normalized.empty()) { return false; @@ -675,22 +601,7 @@ namespace ServerRuntime BanMetadata BanManager::BuildDefaultMetadata(const char *source) { BanMetadata metadata; - - SYSTEMTIME utc; - GetSystemTime(&utc); - - char created[64] = {}; - sprintf_s( - created, - sizeof(created), - "%04u-%02u-%02uT%02u:%02u:%02uZ", - (unsigned)utc.wYear, - (unsigned)utc.wMonth, - (unsigned)utc.wDay, - (unsigned)utc.wHour, - (unsigned)utc.wMinute, - (unsigned)utc.wSecond); - metadata.created = created; + metadata.created = StringUtils::GetCurrentUtcTimestampIso8601(); metadata.source = (source != nullptr) ? source : "Server"; metadata.expires = ""; metadata.reason = ""; @@ -698,38 +609,6 @@ namespace ServerRuntime } - std::string BanManager::NormalizeXuid(const std::string &xuid) - { - std::string trimmed = StringUtils::TrimAscii(xuid); - if (trimmed.empty()) - { - return ""; - } - - unsigned long long numericXuid = 0; - // Canonicalize numeric XUID input into a lowercase hexadecimal string so lookups stay stable. - if (TryParseNumericXuid(trimmed, &numericXuid)) - { - if (numericXuid == 0ULL) - { - return ""; - } - - char buffer[32] = {}; - sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid); - return buffer; - } - - return StringUtils::ToLowerAscii(trimmed); - } - - - std::string BanManager::NormalizeIp(const std::string &ip) - { - return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip)); - } - - void BanManager::NormalizeMetadata(BanMetadata *metadata) { if (metadata == nullptr) @@ -746,36 +625,7 @@ namespace ServerRuntime std::string BanManager::BuildPath(const char *fileName) const { - if (fileName == nullptr || fileName[0] == 0) - { - return ""; - } - - const std::wstring wideFileName = StringUtils::Utf8ToWide(fileName); - if (wideFileName.empty()) - { - return ""; - } - - if (m_baseDirectory.empty() || m_baseDirectory == ".") - { - return StringUtils::WideToUtf8(wideFileName); - } - - const std::wstring wideBaseDirectory = StringUtils::Utf8ToWide(m_baseDirectory); - if (wideBaseDirectory.empty()) - { - return StringUtils::WideToUtf8(wideFileName); - } - - // Join Unicode-aware path strings after conversion so non-ASCII dedicated-server directories remain supported. - const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1]; - if (last == L'\\' || last == L'/') - { - return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName); - } - - return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName); + return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName); } } } diff --git a/Minecraft.Server/Access/BanManager.h b/Minecraft.Server/Access/BanManager.h index bb44e3160..59103bec7 100644 --- a/Minecraft.Server/Access/BanManager.h +++ b/Minecraft.Server/Access/BanManager.h @@ -93,8 +93,6 @@ namespace ServerRuntime static BanMetadata BuildDefaultMetadata(const char *source = "Server"); private: - static std::string NormalizeXuid(const std::string &xuid); - static std::string NormalizeIp(const std::string &ip); static void NormalizeMetadata(BanMetadata *metadata); std::string BuildPath(const char *fileName) const; @@ -105,4 +103,4 @@ namespace ServerRuntime std::vector m_bannedIps; }; } -} \ No newline at end of file +} diff --git a/Minecraft.Server/Access/WhitelistManager.cpp b/Minecraft.Server/Access/WhitelistManager.cpp index 6dbc39a3b..33ea7e46a 100644 --- a/Minecraft.Server/Access/WhitelistManager.cpp +++ b/Minecraft.Server/Access/WhitelistManager.cpp @@ -2,15 +2,13 @@ #include "WhitelistManager.h" +#include "..\Common\AccessStorageUtils.h" #include "..\Common\FileUtils.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" #include "..\vendor\nlohmann\json.hpp" #include -#include -#include -#include namespace ServerRuntime { @@ -21,79 +19,6 @@ namespace ServerRuntime namespace { static const char *kWhitelistFileName = "whitelist.json"; - - static bool FileExists(const std::string &path) - { - const std::wstring widePath = StringUtils::Utf8ToWide(path); - if (widePath.empty()) - { - return false; - } - - DWORD attrs = GetFileAttributesW(widePath.c_str()); - return (attrs != INVALID_FILE_ATTRIBUTES) && ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0); - } - - static bool EnsureJsonListFile(const std::string &path) - { - if (FileExists(path)) - { - return true; - } - return FileUtils::WriteTextFileAtomic(path, "[]\n"); - } - - static bool TryParseNumericXuid(const std::string &text, unsigned long long *outValue) - { - if (outValue == nullptr) - { - return false; - } - - std::string trimmed = StringUtils::TrimAscii(text); - if (trimmed.empty()) - { - return false; - } - - errno = 0; - char *end = nullptr; - unsigned long long value = _strtoui64(trimmed.c_str(), &end, 0); - if (end == trimmed.c_str() || errno != 0) - { - return false; - } - - while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') - { - ++end; - } - - if (*end != 0) - { - return false; - } - - *outValue = value; - return true; - } - - static bool TryGetStringField(const OrderedJson &object, const char *key, std::string *outValue) - { - if (key == nullptr || outValue == nullptr || !object.is_object()) - { - return false; - } - - const auto it = object.find(key); - if (it == object.end() || !it->is_string()) - { - return false; - } - - *outValue = it->get(); - return true; - } } WhitelistManager::WhitelistManager(const std::string &baseDirectory) @@ -104,7 +29,7 @@ namespace ServerRuntime bool WhitelistManager::EnsureWhitelistFileExists() const { const std::string path = GetWhitelistFilePath(); - if (!EnsureJsonListFile(path)) + if (!AccessStorageUtils::EnsureJsonListFileExists(path)) { LogErrorf("access", "failed to create %s", path.c_str()); return false; @@ -177,23 +102,23 @@ namespace ServerRuntime } std::string rawXuid; - if (!TryGetStringField(object, "xuid", &rawXuid)) + if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid)) { LogWarnf("access", "skipping whitelist entry without xuid in %s", path.c_str()); continue; } WhitelistedPlayerEntry entry; - entry.xuid = NormalizeXuid(rawXuid); + entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid); if (entry.xuid.empty()) { LogWarnf("access", "skipping whitelist entry with empty xuid in %s", path.c_str()); continue; } - TryGetStringField(object, "name", &entry.name); - TryGetStringField(object, "created", &entry.metadata.created); - TryGetStringField(object, "source", &entry.metadata.source); + AccessStorageUtils::TryGetStringField(object, "name", &entry.name); + AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); + AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); NormalizeMetadata(&entry.metadata); outEntries->push_back(entry); @@ -208,7 +133,7 @@ namespace ServerRuntime for (const auto &entry : entries) { OrderedJson object = OrderedJson::object(); - object["xuid"] = NormalizeXuid(entry.xuid); + object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid); object["name"] = entry.name; object["created"] = entry.metadata.created; object["source"] = entry.metadata.source; @@ -243,7 +168,7 @@ namespace ServerRuntime bool WhitelistManager::IsPlayerWhitelistedByXuid(const std::string &xuid) const { - const auto normalized = NormalizeXuid(xuid); + const auto normalized = AccessStorageUtils::NormalizeXuid(xuid); if (normalized.empty()) { return false; @@ -267,7 +192,7 @@ namespace ServerRuntime } auto normalized = entry; - normalized.xuid = NormalizeXuid(normalized.xuid); + normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid); NormalizeMetadata(&normalized.metadata); if (normalized.xuid.empty()) { @@ -306,7 +231,7 @@ namespace ServerRuntime bool WhitelistManager::RemovePlayerByXuid(const std::string &xuid) { - const auto normalized = NormalizeXuid(xuid); + const auto normalized = AccessStorageUtils::NormalizeXuid(xuid); if (normalized.empty()) { return false; @@ -348,50 +273,11 @@ namespace ServerRuntime WhitelistMetadata WhitelistManager::BuildDefaultMetadata(const char *source) { WhitelistMetadata metadata; - - SYSTEMTIME utc; - GetSystemTime(&utc); - - char created[64] = {}; - sprintf_s( - created, - sizeof(created), - "%04u-%02u-%02uT%02u:%02u:%02uZ", - (unsigned)utc.wYear, - (unsigned)utc.wMonth, - (unsigned)utc.wDay, - (unsigned)utc.wHour, - (unsigned)utc.wMinute, - (unsigned)utc.wSecond); - metadata.created = created; + metadata.created = StringUtils::GetCurrentUtcTimestampIso8601(); metadata.source = (source != nullptr) ? source : "Server"; return metadata; } - std::string WhitelistManager::NormalizeXuid(const std::string &xuid) - { - std::string trimmed = StringUtils::TrimAscii(xuid); - if (trimmed.empty()) - { - return ""; - } - - unsigned long long numericXuid = 0; - if (TryParseNumericXuid(trimmed, &numericXuid)) - { - if (numericXuid == 0ULL) - { - return ""; - } - - char buffer[32] = {}; - sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid); - return buffer; - } - - return StringUtils::ToLowerAscii(trimmed); - } - void WhitelistManager::NormalizeMetadata(WhitelistMetadata *metadata) { if (metadata == nullptr) @@ -405,35 +291,7 @@ namespace ServerRuntime std::string WhitelistManager::BuildPath(const char *fileName) const { - if (fileName == nullptr || fileName[0] == 0) - { - return ""; - } - - const std::wstring wideFileName = StringUtils::Utf8ToWide(fileName); - if (wideFileName.empty()) - { - return ""; - } - - if (m_baseDirectory.empty() || m_baseDirectory == ".") - { - return StringUtils::WideToUtf8(wideFileName); - } - - const std::wstring wideBaseDirectory = StringUtils::Utf8ToWide(m_baseDirectory); - if (wideBaseDirectory.empty()) - { - return StringUtils::WideToUtf8(wideFileName); - } - - const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1]; - if (last == L'\\' || last == L'/') - { - return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName); - } - - return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName); + return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName); } } } diff --git a/Minecraft.Server/Access/WhitelistManager.h b/Minecraft.Server/Access/WhitelistManager.h index dbdc40eef..1c2c5a0b5 100644 --- a/Minecraft.Server/Access/WhitelistManager.h +++ b/Minecraft.Server/Access/WhitelistManager.h @@ -7,10 +7,6 @@ namespace ServerRuntime { namespace Access { - /** - * Information stored with dedicated-server whitelist entries - * ホワイトリスト項目に保存する補助情報 - */ struct WhitelistMetadata { std::string created; @@ -25,7 +21,7 @@ namespace ServerRuntime }; /** - * Dedicated server whitelist file manager. + * whitelist manager * * Files: * - whitelist.json @@ -56,7 +52,6 @@ namespace ServerRuntime static WhitelistMetadata BuildDefaultMetadata(const char *source = "Server"); private: - static std::string NormalizeXuid(const std::string &xuid); static void NormalizeMetadata(WhitelistMetadata *metadata); std::string BuildPath(const char *fileName) const; diff --git a/Minecraft.Server/Common/AccessStorageUtils.h b/Minecraft.Server/Common/AccessStorageUtils.h new file mode 100644 index 000000000..c5d3477c7 --- /dev/null +++ b/Minecraft.Server/Common/AccessStorageUtils.h @@ -0,0 +1,105 @@ +#pragma once + +#include "FileUtils.h" +#include "StringUtils.h" + +#include "..\vendor\nlohmann\json.hpp" + +#include + +namespace ServerRuntime +{ + namespace AccessStorageUtils + { + inline bool IsRegularFile(const std::string &path) + { + const std::wstring widePath = StringUtils::Utf8ToWide(path); + if (widePath.empty()) + { + return false; + } + + const DWORD attributes = GetFileAttributesW(widePath.c_str()); + return (attributes != INVALID_FILE_ATTRIBUTES) && ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0); + } + + inline bool EnsureJsonListFileExists(const std::string &path) + { + return IsRegularFile(path) || FileUtils::WriteTextFileAtomic(path, "[]\n"); + } + + inline bool TryGetStringField(const nlohmann::ordered_json &object, const char *key, std::string *outValue) + { + if (key == nullptr || outValue == nullptr || !object.is_object()) + { + return false; + } + + const auto it = object.find(key); + if (it == object.end() || !it->is_string()) + { + return false; + } + + *outValue = it->get(); + return true; + } + + inline std::string NormalizeXuid(const std::string &xuid) + { + const std::string trimmed = StringUtils::TrimAscii(xuid); + if (trimmed.empty()) + { + return ""; + } + + unsigned long long numericXuid = 0; + if (StringUtils::TryParseUnsignedLongLong(trimmed, &numericXuid)) + { + if (numericXuid == 0ULL) + { + return ""; + } + + char buffer[32] = {}; + sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid); + return buffer; + } + + return StringUtils::ToLowerAscii(trimmed); + } + + inline std::string BuildPathFromBaseDirectory(const std::string &baseDirectory, const char *fileName) + { + if (fileName == nullptr || fileName[0] == 0) + { + return ""; + } + + const std::wstring wideFileName = StringUtils::Utf8ToWide(fileName); + if (wideFileName.empty()) + { + return ""; + } + + if (baseDirectory.empty() || baseDirectory == ".") + { + return StringUtils::WideToUtf8(wideFileName); + } + + const std::wstring wideBaseDirectory = StringUtils::Utf8ToWide(baseDirectory); + if (wideBaseDirectory.empty()) + { + return StringUtils::WideToUtf8(wideFileName); + } + + const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1]; + if (last == L'\\' || last == L'/') + { + return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName); + } + + return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName); + } + } +} diff --git a/Minecraft.Server/Common/NetworkUtils.h b/Minecraft.Server/Common/NetworkUtils.h new file mode 100644 index 000000000..c77d4506b --- /dev/null +++ b/Minecraft.Server/Common/NetworkUtils.h @@ -0,0 +1,29 @@ +#pragma once + +#include "StringUtils.h" + +#include + +namespace ServerRuntime +{ + namespace NetworkUtils + { + inline std::string NormalizeIpToken(const std::string &ip) + { + return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip)); + } + + inline bool IsIpLiteral(const std::string &text) + { + const std::string trimmed = StringUtils::TrimAscii(text); + if (trimmed.empty()) + { + return false; + } + + IN_ADDR ipv4 = {}; + IN6_ADDR ipv6 = {}; + return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1; + } + } +} diff --git a/Minecraft.Server/Common/StringUtils.cpp b/Minecraft.Server/Common/StringUtils.cpp index a7c1bb493..40881ae7b 100644 --- a/Minecraft.Server/Common/StringUtils.cpp +++ b/Minecraft.Server/Common/StringUtils.cpp @@ -3,6 +3,9 @@ #include "StringUtils.h" #include +#include +#include +#include namespace ServerRuntime { @@ -150,6 +153,60 @@ namespace ServerRuntime return true; } + + bool TryParseUnsignedLongLong(const std::string &value, unsigned long long *outValue) + { + if (outValue == nullptr) + { + return false; + } + + const std::string trimmed = TrimAscii(value); + if (trimmed.empty()) + { + return false; + } + + errno = 0; + char *end = nullptr; + const unsigned long long parsed = _strtoui64(trimmed.c_str(), &end, 0); + if (end == trimmed.c_str() || errno != 0) + { + return false; + } + + while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') + { + ++end; + } + + if (*end != 0) + { + return false; + } + + *outValue = parsed; + return true; + } + + std::string GetCurrentUtcTimestampIso8601() + { + SYSTEMTIME utc = {}; + GetSystemTime(&utc); + + char created[64] = {}; + sprintf_s( + created, + sizeof(created), + "%04u-%02u-%02uT%02u:%02u:%02uZ", + (unsigned)utc.wYear, + (unsigned)utc.wMonth, + (unsigned)utc.wDay, + (unsigned)utc.wHour, + (unsigned)utc.wMinute, + (unsigned)utc.wSecond); + return created; + } } } diff --git a/Minecraft.Server/Common/StringUtils.h b/Minecraft.Server/Common/StringUtils.h index 7720cc443..68f818e1c 100644 --- a/Minecraft.Server/Common/StringUtils.h +++ b/Minecraft.Server/Common/StringUtils.h @@ -16,6 +16,8 @@ namespace ServerRuntime std::string ToLowerAscii(const std::string &value); std::string JoinTokens(const std::vector &tokens, size_t startIndex = 0, const char *separator = " "); bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix); + bool TryParseUnsignedLongLong(const std::string &value, unsigned long long *outValue); + std::string GetCurrentUtcTimestampIso8601(); } } diff --git a/Minecraft.Server/Console/commands/CliCommandBan.cpp b/Minecraft.Server/Console/commands/CliCommandBan.cpp index a8b26cb61..6cfc66c06 100644 --- a/Minecraft.Server/Console/commands/CliCommandBan.cpp +++ b/Minecraft.Server/Console/commands/CliCommandBan.cpp @@ -16,23 +16,9 @@ namespace ServerRuntime { namespace { - static std::string JoinTokens(const std::vector &tokens, size_t startIndex) - { - std::string joined; - for (size_t i = startIndex; i < tokens.size(); ++i) - { - if (!joined.empty()) - { - joined.push_back(' '); - } - joined += tokens[i]; - } - return joined; - } - static void AppendUniqueXuid(PlayerUID xuid, std::vector *out) { - if (out == NULL || xuid == INVALID_XUID) + if (out == nullptr || xuid == INVALID_XUID) { return; } @@ -45,7 +31,7 @@ namespace ServerRuntime static void CollectPlayerBanXuids(const std::shared_ptr &player, std::vector *out) { - if (player == NULL || out == NULL) + if (player == nullptr || out == nullptr) { return; } @@ -88,8 +74,8 @@ namespace ServerRuntime return false; } - std::shared_ptr target = engine->FindPlayerByNameUtf8(line.tokens[1]); - if (target == NULL) + const auto target = engine->FindPlayerByNameUtf8(line.tokens[1]); + if (target == nullptr) { engine->LogWarn("Unknown player: " + line.tokens[1] + " (this server build can only ban players that are currently online)."); return false; @@ -103,15 +89,10 @@ namespace ServerRuntime return false; } - bool hasUnbannedIdentity = false; - for (size_t i = 0; i < xuids.size(); ++i) - { - if (!ServerRuntime::Access::IsPlayerBanned(xuids[i])) - { - hasUnbannedIdentity = true; - break; - } - } + const bool hasUnbannedIdentity = std::any_of( + xuids.begin(), + xuids.end(), + [](PlayerUID xuid) { return !ServerRuntime::Access::IsPlayerBanned(xuid); }); if (!hasUnbannedIdentity) { engine->LogWarn("That player is already banned."); @@ -119,28 +100,28 @@ namespace ServerRuntime } ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console"); - metadata.reason = JoinTokens(line.tokens, 2); + metadata.reason = StringUtils::JoinTokens(line.tokens, 2); if (metadata.reason.empty()) { metadata.reason = "Banned by an operator."; } const std::string playerName = StringUtils::WideToUtf8(target->getName()); - for (size_t i = 0; i < xuids.size(); ++i) + for (const auto xuid : xuids) { - if (ServerRuntime::Access::IsPlayerBanned(xuids[i])) + if (ServerRuntime::Access::IsPlayerBanned(xuid)) { continue; } - if (!ServerRuntime::Access::AddPlayerBan(xuids[i], playerName, metadata)) + if (!ServerRuntime::Access::AddPlayerBan(xuid, playerName, metadata)) { engine->LogError("Failed to write player ban."); return false; } } - if (target->connection != NULL) + if (target->connection != nullptr) { target->connection->disconnect(DisconnectPacket::eDisconnect_Banned); } diff --git a/Minecraft.Server/Console/commands/CliCommandBanIp.cpp b/Minecraft.Server/Console/commands/CliCommandBanIp.cpp index 7c72a3433..c093698e7 100644 --- a/Minecraft.Server/Console/commands/CliCommandBanIp.cpp +++ b/Minecraft.Server/Console/commands/CliCommandBanIp.cpp @@ -5,6 +5,7 @@ #include "..\ServerCliEngine.h" #include "..\ServerCliParser.h" #include "..\..\Access\Access.h" +#include "..\..\Common\NetworkUtils.h" #include "..\..\Common\StringUtils.h" #include "..\..\ServerLogManager.h" #include "..\..\..\Minecraft.Client\MinecraftServer.h" @@ -14,51 +15,15 @@ #include "..\..\..\Minecraft.World\Connection.h" #include "..\..\..\Minecraft.World\DisconnectPacket.h" -#include - namespace ServerRuntime { namespace { - static std::string JoinTokens(const std::vector &tokens, size_t startIndex) - { - std::string joined; - for (size_t i = startIndex; i < tokens.size(); ++i) - { - if (!joined.empty()) - { - joined.push_back(' '); - } - joined += tokens[i]; - } - return joined; - } - - // Compare IPs in a canonical lowercase form so literal input and cached values match reliably. - static std::string NormalizeIpToken(const std::string &ip) - { - return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip)); - } - - // Accept both IPv4 and IPv6 literals because Java Edition style ban-ip can target either form. - static bool IsIpLiteral(const std::string &text) - { - const std::string trimmed = StringUtils::TrimAscii(text); - if (trimmed.empty()) - { - return false; - } - - IN_ADDR ipv4 = {}; - IN6_ADDR ipv6 = {}; - return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1; - } - // The dedicated server keeps the accepted remote IP in ServerLogManager, keyed by connection smallId. // It's a bit strange from a responsibility standpoint, so we'll need to implement it separately. static bool TryGetPlayerRemoteIp(const std::shared_ptr &player, std::string *outIp) { - if (outIp == NULL || player == NULL || player->connection == NULL || player->connection->connection == NULL || player->connection->connection->getSocket() == NULL) + if (outIp == nullptr || player == nullptr || player->connection == nullptr || player->connection->connection == nullptr || player->connection->connection->getSocket() == nullptr) { return false; } @@ -75,27 +40,26 @@ namespace ServerRuntime // After persisting the ban, walk a snapshot of current players so every matching session is removed. static int DisconnectPlayersByRemoteIp(const std::string &ip) { - MinecraftServer *server = MinecraftServer::getInstance(); - if (server == NULL || server->getPlayers() == NULL) + auto *server = MinecraftServer::getInstance(); + if (server == nullptr || server->getPlayers() == nullptr) { return 0; } - const std::string normalizedIp = NormalizeIpToken(ip); - std::vector > playerSnapshot = server->getPlayers()->players; + const std::string normalizedIp = NetworkUtils::NormalizeIpToken(ip); + const std::vector> playerSnapshot = server->getPlayers()->players; int disconnectedCount = 0; - for (size_t i = 0; i < playerSnapshot.size(); ++i) + for (const auto &player : playerSnapshot) { - std::shared_ptr player = playerSnapshot[i]; std::string playerIp; if (!TryGetPlayerRemoteIp(player, &playerIp)) { continue; } - if (NormalizeIpToken(playerIp) == normalizedIp) + if (NetworkUtils::NormalizeIpToken(playerIp) == normalizedIp) { - if (player != NULL && player->connection != NULL) + if (player != nullptr && player->connection != nullptr) { player->connection->disconnect(DisconnectPacket::eDisconnect_Banned); ++disconnectedCount; @@ -142,8 +106,8 @@ namespace ServerRuntime const std::string targetToken = line.tokens[1]; std::string remoteIp; // Match Java Edition behavior by accepting either a literal IP or an online player name. - std::shared_ptr targetPlayer = engine->FindPlayerByNameUtf8(targetToken); - if (targetPlayer != NULL) + const auto targetPlayer = engine->FindPlayerByNameUtf8(targetToken); + if (targetPlayer != nullptr) { if (!TryGetPlayerRemoteIp(targetPlayer, &remoteIp)) { @@ -151,7 +115,7 @@ namespace ServerRuntime return false; } } - else if (IsIpLiteral(targetToken)) + else if (NetworkUtils::IsIpLiteral(targetToken)) { remoteIp = StringUtils::TrimAscii(targetToken); } @@ -169,7 +133,7 @@ namespace ServerRuntime } ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console"); - metadata.reason = JoinTokens(line.tokens, 2); + metadata.reason = StringUtils::JoinTokens(line.tokens, 2); if (metadata.reason.empty()) { metadata.reason = "Banned by an operator."; diff --git a/Minecraft.Server/Console/commands/CliCommandBanList.cpp b/Minecraft.Server/Console/commands/CliCommandBanList.cpp index bac4fc1d7..86f10ae1b 100644 --- a/Minecraft.Server/Console/commands/CliCommandBanList.cpp +++ b/Minecraft.Server/Console/commands/CliCommandBanList.cpp @@ -15,7 +15,7 @@ namespace ServerRuntime { static void AppendUniqueText(const std::string &text, std::vector *out) { - if (out == NULL || text.empty()) + if (out == nullptr || text.empty()) { return; } @@ -41,16 +41,16 @@ namespace ServerRuntime } std::vector names; - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { - AppendUniqueText(entries[i].name, &names); + AppendUniqueText(entry.name, &names); } std::sort(names.begin(), names.end(), CompareLowerAscii); engine->LogInfo("There are " + std::to_string(names.size()) + " banned player(s)."); - for (size_t i = 0; i < names.size(); ++i) + for (const auto &name : names) { - engine->LogInfo(" " + names[i]); + engine->LogInfo(" " + name); } return true; } @@ -65,16 +65,16 @@ namespace ServerRuntime } std::vector ips; - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { - AppendUniqueText(entries[i].ip, &ips); + AppendUniqueText(entry.ip, &ips); } std::sort(ips.begin(), ips.end(), CompareLowerAscii); engine->LogInfo("There are " + std::to_string(ips.size()) + " banned IP(s)."); - for (size_t i = 0; i < ips.size(); ++i) + for (const auto &ip : ips) { - engine->LogInfo(" " + ips[i]); + engine->LogInfo(" " + ip); } return true; } diff --git a/Minecraft.Server/Console/commands/CliCommandPardon.cpp b/Minecraft.Server/Console/commands/CliCommandPardon.cpp index c43479cec..317f1993e 100644 --- a/Minecraft.Server/Console/commands/CliCommandPardon.cpp +++ b/Minecraft.Server/Console/commands/CliCommandPardon.cpp @@ -16,7 +16,7 @@ namespace ServerRuntime { static void AppendUniqueText(const std::string &text, std::vector *out) { - if (out == NULL || text.empty()) + if (out == nullptr || text.empty()) { return; } @@ -29,7 +29,7 @@ namespace ServerRuntime static void AppendUniqueXuid(PlayerUID xuid, std::vector *out) { - if (out == NULL || xuid == INVALID_XUID) + if (out == nullptr || xuid == INVALID_XUID) { return; } @@ -76,7 +76,7 @@ namespace ServerRuntime std::vector xuidsToRemove; std::vector matchedNames; std::shared_ptr onlineTarget = engine->FindPlayerByNameUtf8(line.tokens[1]); - if (onlineTarget != NULL) + if (onlineTarget != nullptr) { if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getXuid())) { @@ -96,16 +96,16 @@ namespace ServerRuntime } const std::string loweredTarget = StringUtils::ToLowerAscii(line.tokens[1]); - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { - if (StringUtils::ToLowerAscii(entries[i].name) == loweredTarget) + if (StringUtils::ToLowerAscii(entry.name) == loweredTarget) { - unsigned long long numericXuid = _strtoui64(entries[i].xuid.c_str(), NULL, 0); - if (numericXuid != 0ULL) + PlayerUID parsedXuid = INVALID_XUID; + if (ServerRuntime::Access::TryParseXuid(entry.xuid, &parsedXuid)) { - AppendUniqueXuid((PlayerUID)numericXuid, &xuidsToRemove); + AppendUniqueXuid(parsedXuid, &xuidsToRemove); } - AppendUniqueText(entries[i].name, &matchedNames); + AppendUniqueText(entry.name, &matchedNames); } } @@ -115,9 +115,9 @@ namespace ServerRuntime return false; } - for (size_t i = 0; i < xuidsToRemove.size(); ++i) + for (const auto xuid : xuidsToRemove) { - if (!ServerRuntime::Access::RemovePlayerBan(xuidsToRemove[i])) + if (!ServerRuntime::Access::RemovePlayerBan(xuid)) { engine->LogError("Failed to remove player ban."); return false; @@ -129,7 +129,7 @@ namespace ServerRuntime { playerName = matchedNames[0]; } - else if (onlineTarget != NULL) + else if (onlineTarget != nullptr) { playerName = StringUtils::WideToUtf8(onlineTarget->getName()); } @@ -144,7 +144,7 @@ namespace ServerRuntime */ void CliCommandPardon::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const { - if (context.currentTokenIndex != 1 || out == NULL) + if (context.currentTokenIndex != 1 || out == nullptr) { return; } @@ -152,18 +152,17 @@ namespace ServerRuntime std::vector entries; if (ServerRuntime::Access::SnapshotBannedPlayers(&entries)) { - const std::string loweredPrefix = StringUtils::ToLowerAscii(context.prefix); std::vector names; - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { - AppendUniqueText(entries[i].name, &names); + AppendUniqueText(entry.name, &names); } - for (size_t i = 0; i < names.size(); ++i) + for (const auto &name : names) { - if (StringUtils::ToLowerAscii(names[i]).compare(0, loweredPrefix.size(), loweredPrefix) == 0) + if (StringUtils::StartsWithIgnoreCase(name, context.prefix)) { - out->push_back(context.linePrefix + names[i]); + out->push_back(context.linePrefix + name); } } } diff --git a/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp b/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp index 7945f0711..195ee31d6 100644 --- a/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp +++ b/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp @@ -5,29 +5,11 @@ #include "..\ServerCliEngine.h" #include "..\ServerCliParser.h" #include "..\..\Access\Access.h" +#include "..\..\Common\NetworkUtils.h" #include "..\..\Common\StringUtils.h" -#include - namespace ServerRuntime { - namespace - { - // Keep validation shared with ban-ip so both commands agree on what counts as a literal address. - static bool IsIpLiteral(const std::string &text) - { - const std::string trimmed = StringUtils::TrimAscii(text); - if (trimmed.empty()) - { - return false; - } - - IN_ADDR ipv4 = {}; - IN6_ADDR ipv6 = {}; - return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1; - } - } - const char *CliCommandPardonIp::Name() const { return "pardon-ip"; @@ -62,7 +44,7 @@ namespace ServerRuntime // Java Edition pardon-ip only operates on a literal address, so do not resolve player names here. const std::string ip = StringUtils::TrimAscii(line.tokens[1]); - if (!IsIpLiteral(ip)) + if (!NetworkUtils::IsIpLiteral(ip)) { engine->LogWarn("Invalid IP address: " + line.tokens[1]); return false; @@ -91,7 +73,7 @@ namespace ServerRuntime { (void)engine; // Complete from the persisted IP-ban snapshot because this command only accepts already-banned literals. - if (context.currentTokenIndex != 1 || out == NULL) + if (context.currentTokenIndex != 1 || out == nullptr) { return; } @@ -103,11 +85,10 @@ namespace ServerRuntime } // Reuse the normalized prefix match used by other commands so completion stays case-insensitive. - const std::string loweredPrefix = StringUtils::ToLowerAscii(context.prefix); - for (size_t i = 0; i < entries.size(); ++i) + for (const auto &entry : entries) { - const std::string candidate = entries[i].ip; - if (StringUtils::ToLowerAscii(candidate).compare(0, loweredPrefix.size(), loweredPrefix) == 0) + const std::string &candidate = entry.ip; + if (StringUtils::StartsWithIgnoreCase(candidate, context.prefix)) { out->push_back(context.linePrefix + candidate); } diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index e9ffa47d9..f3d997918 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -705,6 +705,8 @@ + + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index d151cd3e8..50db0117f 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -620,6 +620,12 @@ Server\Common + + Server\Common + + + Server\Common + Server\Common