diff --git a/CMakeLists.txt b/CMakeLists.txt index 419f0faff..b6db155d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ list(APPEND MINECRAFT_SERVER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandList.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandTp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandGamemode.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/FileUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/StringUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/vendor/linenoise/linenoise.c" ) diff --git a/Minecraft.Server/Common/FileUtils.cpp b/Minecraft.Server/Common/FileUtils.cpp new file mode 100644 index 000000000..50eeeb72e --- /dev/null +++ b/Minecraft.Server/Common/FileUtils.cpp @@ -0,0 +1,146 @@ +#include "stdafx.h" + +#include "FileUtils.h" + +#include "StringUtils.h" + +#include +#include + +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; + } + } +} \ No newline at end of file diff --git a/Minecraft.Server/Common/FileUtils.h b/Minecraft.Server/Common/FileUtils.h new file mode 100644 index 000000000..96e398ccf --- /dev/null +++ b/Minecraft.Server/Common/FileUtils.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +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(); + } +} \ No newline at end of file diff --git a/Minecraft.Server/Common/StringUtils.cpp b/Minecraft.Server/Common/StringUtils.cpp index b22819f3d..82646eeb5 100644 --- a/Minecraft.Server/Common/StringUtils.cpp +++ b/Minecraft.Server/Common/StringUtils.cpp @@ -37,6 +37,7 @@ namespace ServerRuntime 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) { @@ -60,6 +61,19 @@ namespace ServerRuntime 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; diff --git a/Minecraft.Server/Common/StringUtils.h b/Minecraft.Server/Common/StringUtils.h index fa1d5734a..a2585b732 100644 --- a/Minecraft.Server/Common/StringUtils.h +++ b/Minecraft.Server/Common/StringUtils.h @@ -9,6 +9,7 @@ namespace ServerRuntime 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); diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index 246da3068..a0a2e65c6 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -657,6 +657,7 @@ + @@ -684,6 +685,7 @@ + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index a4567a11f..3c8657e7b 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -48,6 +48,9 @@ Server + + Server\Common + @@ -572,6 +575,9 @@ Server\Console + + Server\Common + Server\Common