forked from cafeberry/cafeberry
feat: add smartcmd dedicated server
major bug: when connecting to the server a second time, it gets softlocked and keeps repeating ticking connection A\n in debug logs until it times out.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include "FileUtils.h"
|
||||
#include "StringUtils.h"
|
||||
|
||||
#include "../vendor/rapidjson/document.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
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 rapidjson::Value &object, const char *key, std::string *outValue)
|
||||
{
|
||||
if (key == nullptr || outValue == nullptr || !object.IsObject())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto it = object.FindMember(key);
|
||||
if (it == object.MemberEnd() || !it->value.IsString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*outValue = it->value.GetString();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
#include "FileUtils.h"
|
||||
|
||||
#include "StringUtils.h"
|
||||
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace ServerRuntime
|
||||
{
|
||||
namespace FileUtils
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static std::wstring ToWidePath(const std::string &filePath)
|
||||
{
|
||||
return StringUtils::Utf8ToWide(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long long GetCurrentUtcFileTime()
|
||||
{
|
||||
FILETIME now = {};
|
||||
GetSystemTimeAsFileTime(&now);
|
||||
|
||||
ULARGE_INTEGER value = {};
|
||||
value.LowPart = now.dwLowDateTime;
|
||||
value.HighPart = now.dwHighDateTime;
|
||||
return value.QuadPart;
|
||||
}
|
||||
|
||||
bool ReadTextFile(const std::string &filePath, std::string *outText)
|
||||
{
|
||||
if (outText == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
outText->clear();
|
||||
|
||||
const std::wstring widePath = ToWidePath(filePath);
|
||||
if (widePath.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *inFile = nullptr;
|
||||
if (_wfopen_s(&inFile, widePath.c_str(), L"rb") != 0 || inFile == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fseek(inFile, 0, SEEK_END) != 0)
|
||||
{
|
||||
fclose(inFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
long fileSize = ftell(inFile);
|
||||
if (fileSize < 0)
|
||||
{
|
||||
fclose(inFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fseek(inFile, 0, SEEK_SET) != 0)
|
||||
{
|
||||
fclose(inFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fileSize == 0)
|
||||
{
|
||||
fclose(inFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
outText->resize((size_t)fileSize);
|
||||
size_t bytesRead = fread(&(*outText)[0], 1, (size_t)fileSize, inFile);
|
||||
fclose(inFile);
|
||||
|
||||
if (bytesRead != (size_t)fileSize)
|
||||
{
|
||||
outText->clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteTextFileAtomic(const std::string &filePath, const std::string &text)
|
||||
{
|
||||
const std::wstring widePath = ToWidePath(filePath);
|
||||
if (widePath.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring tmpPath = widePath + L".tmp";
|
||||
|
||||
FILE *outFile = nullptr;
|
||||
if (_wfopen_s(&outFile, tmpPath.c_str(), L"wb") != 0 || outFile == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!text.empty())
|
||||
{
|
||||
size_t bytesWritten = fwrite(text.data(), 1, text.size(), outFile);
|
||||
if (bytesWritten != text.size())
|
||||
{
|
||||
fclose(outFile);
|
||||
DeleteFileW(tmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fflush(outFile) != 0 || _commit(_fileno(outFile)) != 0)
|
||||
{
|
||||
fclose(outFile);
|
||||
DeleteFileW(tmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
fclose(outFile);
|
||||
|
||||
DWORD attrs = GetFileAttributesW(widePath.c_str());
|
||||
if (attrs != INVALID_FILE_ATTRIBUTES && ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0))
|
||||
{
|
||||
// Replace the destination without deleting the last known-good file first.
|
||||
if (ReplaceFileW(widePath.c_str(), tmpPath.c_str(), nullptr, REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (MoveFileExW(tmpPath.c_str(), widePath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep the temp file on failure so the original file remains recoverable and the caller can inspect the write result.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ServerRuntime
|
||||
{
|
||||
namespace FileUtils
|
||||
{
|
||||
/**
|
||||
* Reads the full UTF-8 path target into memory without interpreting JSON or line endings
|
||||
* UTF-8パスのテキストファイル全体をそのまま読み込む
|
||||
*/
|
||||
bool ReadTextFile(const std::string &filePath, std::string *outText);
|
||||
/**
|
||||
* Writes text through a same-directory temporary file and publishes it with a single replacement step
|
||||
* 同一ディレクトリの一時ファイル経由で安全に書き換える
|
||||
*/
|
||||
bool WriteTextFileAtomic(const std::string &filePath, const std::string &text);
|
||||
/**
|
||||
* Returns the current UTC timestamp encoded in Windows FILETIME units for expiry comparisons
|
||||
* 期限判定用に現在UTC時刻をWindows FILETIME単位で返す
|
||||
*/
|
||||
unsigned long long GetCurrentUtcFileTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "StringUtils.h"
|
||||
|
||||
#include <WS2tcpip.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
#include "stdafx.h"
|
||||
|
||||
#include "StringUtils.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace ServerRuntime
|
||||
{
|
||||
namespace StringUtils
|
||||
{
|
||||
std::string WideToUtf8(const std::wstring &value)
|
||||
{
|
||||
if (value.empty())
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
int charCount = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), (int)value.length(), NULL, 0, NULL, NULL);
|
||||
if (charCount <= 0)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::string utf8;
|
||||
utf8.resize(charCount);
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), (int)value.length(), &utf8[0], charCount, NULL, NULL);
|
||||
return utf8;
|
||||
}
|
||||
|
||||
std::wstring Utf8ToWide(const char *value)
|
||||
{
|
||||
if (value == NULL || value[0] == 0)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
int wideCount = MultiByteToWideChar(CP_UTF8, 0, value, -1, NULL, 0);
|
||||
if (wideCount <= 0)
|
||||
{
|
||||
// Fall back to the current ANSI code page so legacy non-UTF-8 inputs remain readable.
|
||||
wideCount = MultiByteToWideChar(CP_ACP, 0, value, -1, NULL, 0);
|
||||
if (wideCount <= 0)
|
||||
{
|
||||
return std::wstring();
|
||||
}
|
||||
|
||||
std::wstring wide;
|
||||
wide.resize(wideCount - 1);
|
||||
MultiByteToWideChar(CP_ACP, 0, value, -1, &wide[0], wideCount);
|
||||
return wide;
|
||||
}
|
||||
|
||||
std::wstring wide;
|
||||
wide.resize(wideCount - 1);
|
||||
MultiByteToWideChar(CP_UTF8, 0, value, -1, &wide[0], wideCount);
|
||||
return wide;
|
||||
}
|
||||
|
||||
std::wstring Utf8ToWide(const std::string &value)
|
||||
{
|
||||
return Utf8ToWide(value.c_str());
|
||||
}
|
||||
|
||||
std::string StripUtf8Bom(const std::string &value)
|
||||
{
|
||||
if (value.size() >= 3 &&
|
||||
(unsigned char)value[0] == 0xEF &&
|
||||
(unsigned char)value[1] == 0xBB &&
|
||||
(unsigned char)value[2] == 0xBF)
|
||||
{
|
||||
return value.substr(3);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string TrimAscii(const std::string &value)
|
||||
{
|
||||
size_t start = 0;
|
||||
while (start < value.length() && std::isspace((unsigned char)value[start]))
|
||||
{
|
||||
++start;
|
||||
}
|
||||
|
||||
size_t end = value.length();
|
||||
while (end > start && std::isspace((unsigned char)value[end - 1]))
|
||||
{
|
||||
--end;
|
||||
}
|
||||
|
||||
return value.substr(start, end - start);
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(const std::string &value)
|
||||
{
|
||||
std::string lowered = value;
|
||||
for (size_t i = 0; i < lowered.length(); ++i)
|
||||
{
|
||||
lowered[i] = (char)std::tolower((unsigned char)lowered[i]);
|
||||
}
|
||||
return lowered;
|
||||
}
|
||||
|
||||
std::string JoinTokens(const std::vector<std::string> &tokens, size_t startIndex, const char *separator)
|
||||
{
|
||||
if (startIndex >= tokens.size())
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
const auto joinSeparator = std::string((separator != nullptr) ? separator : " ");
|
||||
size_t totalLength = 0;
|
||||
for (size_t i = startIndex; i < tokens.size(); ++i)
|
||||
{
|
||||
totalLength += tokens[i].size();
|
||||
}
|
||||
|
||||
totalLength += (tokens.size() - startIndex - 1) * joinSeparator.size();
|
||||
std::string joined;
|
||||
joined.reserve(totalLength);
|
||||
for (size_t i = startIndex; i < tokens.size(); ++i)
|
||||
{
|
||||
if (!joined.empty())
|
||||
{
|
||||
joined += joinSeparator;
|
||||
}
|
||||
|
||||
joined += tokens[i];
|
||||
}
|
||||
|
||||
return joined;
|
||||
}
|
||||
|
||||
bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix)
|
||||
{
|
||||
if (prefix.size() > value.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < prefix.size(); ++i)
|
||||
{
|
||||
unsigned char a = (unsigned char)value[i];
|
||||
unsigned char b = (unsigned char)prefix[i];
|
||||
if (std::tolower(a) != std::tolower(b))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ServerRuntime
|
||||
{
|
||||
namespace StringUtils
|
||||
{
|
||||
std::string WideToUtf8(const std::wstring &value);
|
||||
std::wstring Utf8ToWide(const char *value);
|
||||
std::wstring Utf8ToWide(const std::string &value);
|
||||
std::string StripUtf8Bom(const std::string &value);
|
||||
|
||||
std::string TrimAscii(const std::string &value);
|
||||
std::string ToLowerAscii(const std::string &value);
|
||||
std::string JoinTokens(const std::vector<std::string> &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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user