From 8a0dcd1875a6565ee2f6cc98f91f05e07b61f858 Mon Sep 17 00:00:00 2001 From: captainurist <73941350+captainurist@users.noreply.github.com> Date: Sun, 5 Nov 2023 23:41:26 +0000 Subject: [PATCH] Full unicode support on Windows --- src/Application/CMakeLists.txt | 1 + src/Application/Game.cpp | 7 +- src/Application/Game.h | 5 +- src/Application/GamePathResolver.cpp | 34 +++--- src/Application/GamePathResolver.h | 8 +- src/Application/GameStarter.cpp | 14 ++- src/Application/GameStarter.h | 5 +- src/Bin/CodeGen/CodeGen.cpp | 2 + src/Bin/LodTool/LodTool.cpp | 2 + src/Bin/OpenEnroth/OpenEnroth.cpp | 2 + src/Library/CMakeLists.txt | 1 + .../Android/AndroidEnvironment.cpp | 33 ++++++ .../Environment/Android/AndroidEnvironment.h | 10 ++ .../Environment/Android/CMakeLists.txt | 16 +++ src/Library/Environment/CMakeLists.txt | 7 ++ .../Environment/Interface/CMakeLists.txt | 10 ++ .../Environment/Interface/Environment.h | 42 +++++++ .../Environment/Interface/EnvironmentEnums.h | 8 ++ src/Library/Environment/Posix/CMakeLists.txt | 16 +++ .../Environment/Posix/PosixEnvironment.cpp | 27 +++++ .../Environment/Posix/PosixEnvironment.h | 12 ++ src/Library/Environment/Test/CMakeLists.txt | 12 ++ .../Environment/Test/Environment_ut.cpp | 50 ++++++++ src/Library/Environment/Win/CMakeLists.txt | 16 +++ .../Win/WinEnvironment.cpp} | 56 ++++----- src/Library/Environment/Win/WinEnvironment.h | 12 ++ src/Library/Platform/CMakeLists.txt | 2 - src/Library/Platform/Interface/Platform.h | 15 --- .../Platform/Interface/PlatformEnums.h | 6 - src/Library/Platform/Null/NullPlatform.cpp | 9 -- src/Library/Platform/Null/NullPlatform.h | 2 - src/Library/Platform/Posix/CMakeLists.txt | 15 --- src/Library/Platform/Posix/PosixPlatform.cpp | 7 -- src/Library/Platform/Proxy/ProxyPlatform.cpp | 8 -- src/Library/Platform/Proxy/ProxyPlatform.h | 2 - src/Library/Platform/Sdl/CMakeLists.txt | 5 + src/Library/Platform/Sdl/SdlPlatform.cpp | 28 +---- src/Library/Platform/Sdl/SdlPlatform.h | 4 - src/Library/Platform/Win/CMakeLists.txt | 16 --- src/Library/Platform/Win/WinPlatform.h | 12 -- src/Utility/CMakeLists.txt | 15 ++- src/Utility/DataPath.cpp | 4 +- src/Utility/DataPath.h | 5 +- src/Utility/FileSystem.cpp | 17 +-- src/Utility/FileSystem.h | 3 +- src/Utility/Tests/UnicodeCrt_ut.cpp | 110 ++++++++++++++++++ src/Utility/UnicodeCrt.cpp | 53 +++++++++ src/Utility/UnicodeCrt.h | 36 ++++++ src/Utility/Win/Unicode.cpp | 28 +++++ src/Utility/Win/Unicode.h | 13 +++ test/Bin/GameTest/GameTestMain.cpp | 2 + test/Bin/UnitTest/UnitTestMain.cpp | 3 + 52 files changed, 613 insertions(+), 215 deletions(-) create mode 100644 src/Library/Environment/Android/AndroidEnvironment.cpp create mode 100644 src/Library/Environment/Android/AndroidEnvironment.h create mode 100644 src/Library/Environment/Android/CMakeLists.txt create mode 100644 src/Library/Environment/CMakeLists.txt create mode 100644 src/Library/Environment/Interface/CMakeLists.txt create mode 100644 src/Library/Environment/Interface/Environment.h create mode 100644 src/Library/Environment/Interface/EnvironmentEnums.h create mode 100644 src/Library/Environment/Posix/CMakeLists.txt create mode 100644 src/Library/Environment/Posix/PosixEnvironment.cpp create mode 100644 src/Library/Environment/Posix/PosixEnvironment.h create mode 100644 src/Library/Environment/Test/CMakeLists.txt create mode 100644 src/Library/Environment/Test/Environment_ut.cpp create mode 100644 src/Library/Environment/Win/CMakeLists.txt rename src/Library/{Platform/Win/WinPlatform.cpp => Environment/Win/WinEnvironment.cpp} (68%) create mode 100644 src/Library/Environment/Win/WinEnvironment.h delete mode 100644 src/Library/Platform/Posix/CMakeLists.txt delete mode 100644 src/Library/Platform/Posix/PosixPlatform.cpp delete mode 100644 src/Library/Platform/Win/CMakeLists.txt delete mode 100644 src/Library/Platform/Win/WinPlatform.h create mode 100644 src/Utility/Tests/UnicodeCrt_ut.cpp create mode 100644 src/Utility/UnicodeCrt.cpp create mode 100644 src/Utility/UnicodeCrt.h create mode 100644 src/Utility/Win/Unicode.cpp create mode 100644 src/Utility/Win/Unicode.h diff --git a/src/Application/CMakeLists.txt b/src/Application/CMakeLists.txt index 4748fe65b4da..1473e21667f9 100644 --- a/src/Application/CMakeLists.txt +++ b/src/Application/CMakeLists.txt @@ -42,4 +42,5 @@ target_link_libraries(application media library_platform_null library_platform_implementation + library_environment_implementation utility) diff --git a/src/Application/Game.cpp b/src/Application/Game.cpp index 4b52bdcb9aaf..10eedbbf359f 100644 --- a/src/Application/Game.cpp +++ b/src/Application/Game.cpp @@ -91,23 +91,24 @@ #include "Media/MediaPlayer.h" #include "Library/Platform/Application/PlatformApplication.h" +#include "Library/Environment/Interface/Environment.h" #include "Library/Random/Random.h" #include "Library/Logger/Logger.h" #include "Utility/Format.h" #include "Utility/DataPath.h" #include "Utility/Exception.h" +#include "Utility/FileSystem.h" void ShowMM7IntroVideo_and_LoadingScreen(); using Graphics::IRenderFactory; - -void initDataPath(const std::string &dataPath) { +void initDataPath(Environment *environment, Platform *platform, const std::string &dataPath) { std::string missing_file; if (validateDataPath(dataPath, missing_file)) { - setDataPath(dataPath); + setDataPath(expandUserPath(dataPath, environment->path(PATH_HOME))); std::string savesPath = makeDataPath("saves"); if (!std::filesystem::exists(savesPath)) { diff --git a/src/Application/Game.h b/src/Application/Game.h index 04fa7a513e1c..be2ace76dca1 100644 --- a/src/Application/Game.h +++ b/src/Application/Game.h @@ -16,10 +16,11 @@ using Io::Mouse; class IRender; -class Platform; class PlatformApplication; class GameTraceHandler; class NuklearEventHandler; +class Platform; +class Environment; class Game { public: @@ -49,6 +50,6 @@ class Game { std::shared_ptr _nuklear = nullptr; }; -void initDataPath(const std::string &dataPath); +void initDataPath(Environment *environment, Platform *platform, const std::string &dataPath); extern class GraphicsImage *gamma_preview_image; // 506E40 diff --git a/src/Application/GamePathResolver.cpp b/src/Application/GamePathResolver.cpp index 17a02ae375dc..7dbddc526025 100644 --- a/src/Application/GamePathResolver.cpp +++ b/src/Application/GamePathResolver.cpp @@ -3,13 +3,13 @@ #include "Application/GamePathResolver.h" #include "Library/Logger/Logger.h" -#include "Library/Platform/Interface/Platform.h" +#include "Library/Environment/Interface/Environment.h" -static std::string _resolvePath(Platform *platform, const char *envVarOverride, const std::vector ®istryKeys); +static std::string _resolvePath(Environment *environment, const char *envVarOverride, const std::vector ®istryKeys); -std::string resolveMm6Path(Platform *platform) { +std::string resolveMm6Path(Environment *environment) { return _resolvePath( - platform, + environment, mm6PathOverrideKey, { "HKEY_LOCAL_MACHINE/SOFTWARE/GOG.com/Games/1207661253/PATH", @@ -23,9 +23,9 @@ std::string resolveMm6Path(Platform *platform) { } -std::string resolveMm7Path(Platform *platform) { +std::string resolveMm7Path(Environment *environment) { return _resolvePath( - platform, + environment, mm7PathOverrideKey, { "HKEY_LOCAL_MACHINE/SOFTWARE/GOG.com/Games/1207658916/Path", @@ -39,9 +39,9 @@ std::string resolveMm7Path(Platform *platform) { } -std::string resolveMm8Path(Platform *platform) { +std::string resolveMm8Path(Environment *environment) { return _resolvePath( - platform, + environment, mm8PathOverrideKey, { "HKEY_LOCAL_MACHINE/SOFTWARE/GOG.com/GOGMM8/PATH", @@ -52,19 +52,15 @@ std::string resolveMm8Path(Platform *platform) { ); } - -static std::string _resolvePath( - Platform *platform, - const char *envVarOverride, - const std::vector ®istryKeys -) { +static std::string _resolvePath(Environment *environment, const char *envVarOverride, const std::vector ®istryKeys) { #ifdef __ANDROID__ // TODO: find a better way to deal with paths and remove this android specific block. - std::string result = platform->storagePath(ANDROID_STORAGE_EXTERNAL); - if (result.empty()) - result = platform->storagePath(ANDROID_STORAGE_INTERNAL); + std::string result = environment->path(PATH_ANDROID_STORAGE_EXTERNAL); if (result.empty()) - platform->showMessageBox("Device currently unsupported", "Your device doesn't have any storage so it is unsupported!"); + result = environment->path(PATH_ANDROID_STORAGE_INTERNAL); + // TODO(captainurist): need a mechanism to show user-visible errors. Commenting out for now. + //if (result.empty()) + // platform->showMessageBox("Device currently unsupported", "Your device doesn't have any storage so it is unsupported!"); return result; #else // TODO (captainurist): we should consider reading Unicode (utf8) strings from win32 registry, as it might contain paths @@ -84,7 +80,7 @@ static std::string _resolvePath( } for (auto key : registryKeys) { - envPath = platform->winQueryRegistry(key); + envPath = environment->queryRegistry(key); if (!envPath.empty()) { return envPath; } diff --git a/src/Application/GamePathResolver.h b/src/Application/GamePathResolver.h index 65537a494ab1..f2fdade7174c 100644 --- a/src/Application/GamePathResolver.h +++ b/src/Application/GamePathResolver.h @@ -1,11 +1,11 @@ #include -class Platform; +class Environment; constexpr char mm6PathOverrideKey[] = "OPENENROTH_MM6_PATH"; constexpr char mm7PathOverrideKey[] = "OPENENROTH_MM7_PATH"; constexpr char mm8PathOverrideKey[] = "OPENENROTH_MM8_PATH"; -std::string resolveMm6Path(Platform *platform); -std::string resolveMm7Path(Platform *platform); -std::string resolveMm8Path(Platform *platform); +std::string resolveMm6Path(Environment *environment); +std::string resolveMm7Path(Environment *environment); +std::string resolveMm8Path(Environment *environment); diff --git a/src/Application/GameStarter.cpp b/src/Application/GameStarter.cpp index 2297cf85e4d6..3eadbc09e43a 100644 --- a/src/Application/GameStarter.cpp +++ b/src/Application/GameStarter.cpp @@ -5,8 +5,8 @@ #include #include "Engine/Engine.h" -#include "Engine/EngineGlobals.h" +#include "Library/Environment/Interface/Environment.h" #include "Library/Platform/Application/PlatformApplication.h" #include "Library/Logger/Logger.h" #include "Library/Logger/LogSink.h" @@ -20,6 +20,9 @@ #include "Game.h" GameStarter::GameStarter(GameStarterOptions options): _options(std::move(options)) { + // Init environment. + _environment = Environment::createStandardEnvironment(); + // Init logger. _bufferSink = std::make_unique(); _defaultSink = LogSink::createDefaultSink(); @@ -32,7 +35,7 @@ GameStarter::GameStarter(GameStarterOptions options): _options(std::move(options } else { _platform = Platform::createStandardPlatform(_logger.get()); } - resolveDefaults(_platform.get(), &_options); + resolveDefaults(_environment.get(), &_options); // TODO(captainurist): move this call up? // Init config - needs data paths initialized. _config = std::make_shared(); @@ -58,8 +61,7 @@ GameStarter::GameStarter(GameStarterOptions options): _options(std::move(options _bufferSink->flush(_logger.get()); // Validate data paths. - ::platform = _platform.get(); // TODO(captainurist): a hack to make validateDataPath work. - initDataPath(_options.dataPath); + initDataPath(_environment.get(), _platform.get(), _options.dataPath); // Create application & game. _application = std::make_unique(_platform.get()); @@ -68,9 +70,9 @@ GameStarter::GameStarter(GameStarterOptions options): _options(std::move(options GameStarter::~GameStarter() = default; -void GameStarter::resolveDefaults(Platform *platform, GameStarterOptions* options) { +void GameStarter::resolveDefaults(Environment *environment, GameStarterOptions* options) { if (options->dataPath.empty()) - options->dataPath = resolveMm7Path(platform); + options->dataPath = resolveMm7Path(environment); if (options->useConfig && options->configPath.empty()) { options->configPath = "openenroth.ini"; diff --git a/src/Application/GameStarter.h b/src/Application/GameStarter.h index 0d07922e223c..770b7465d8d3 100644 --- a/src/Application/GameStarter.h +++ b/src/Application/GameStarter.h @@ -5,7 +5,7 @@ #include "GameStarterOptions.h" class Platform; -class PlatformLogger; +class Environment; class Logger; class BufferLogSink; class LogSink; @@ -29,10 +29,11 @@ class GameStarter { void run(); private: - static void resolveDefaults(Platform *platform, GameStarterOptions* options); + static void resolveDefaults(Environment *environment, GameStarterOptions* options); private: GameStarterOptions _options; + std::unique_ptr _environment; std::unique_ptr _bufferSink; std::unique_ptr _defaultSink; std::unique_ptr _logger; diff --git a/src/Bin/CodeGen/CodeGen.cpp b/src/Bin/CodeGen/CodeGen.cpp index cb26b09a9363..9ee5185801ac 100644 --- a/src/Bin/CodeGen/CodeGen.cpp +++ b/src/Bin/CodeGen/CodeGen.cpp @@ -25,6 +25,7 @@ #include "Utility/DataPath.h" #include "Utility/Exception.h" #include "Utility/String.h" +#include "Utility/UnicodeCrt.h" #include "CodeGenEnums.h" #include "CodeGenMap.h" @@ -389,6 +390,7 @@ int runBountyHuntCodeGen(CodeGenOptions options, GameResourceManager *resourceMa int platformMain(int argc, char **argv) { try { + UnicodeCrt _(argc, argv); CodeGenOptions options = CodeGenOptions::parse(argc, argv); if (options.helpPrinted) return 1; diff --git a/src/Bin/LodTool/LodTool.cpp b/src/Bin/LodTool/LodTool.cpp index 5b5011dc843c..fd2b249c9ba0 100644 --- a/src/Bin/LodTool/LodTool.cpp +++ b/src/Bin/LodTool/LodTool.cpp @@ -6,6 +6,7 @@ #include "Utility/Format.h" #include "Utility/String.h" +#include "Utility/UnicodeCrt.h" int runDump(const LodToolOptions &options) { LodReader reader(options.lodPath, LOD_ALLOW_DUPLICATES); @@ -41,6 +42,7 @@ int runDump(const LodToolOptions &options) { int main(int argc, char **argv) { try { + UnicodeCrt _(argc, argv); LodToolOptions options = LodToolOptions::parse(argc, argv); if (options.helpPrinted) return 1; diff --git a/src/Bin/OpenEnroth/OpenEnroth.cpp b/src/Bin/OpenEnroth/OpenEnroth.cpp index 1a933ab9944d..84923ed3f1a3 100644 --- a/src/Bin/OpenEnroth/OpenEnroth.cpp +++ b/src/Bin/OpenEnroth/OpenEnroth.cpp @@ -15,6 +15,7 @@ #include "Library/Trace/EventTrace.h" #include "Utility/Format.h" +#include "Utility/UnicodeCrt.h" #include "OpenEnrothOptions.h" @@ -55,6 +56,7 @@ int runOpenEnroth(OpenEnrothOptions options) { int openEnrothMain(int argc, char **argv) { try { + UnicodeCrt _(argc, argv); OpenEnrothOptions options = OpenEnrothOptions::parse(argc, argv); if (options.helpPrinted) return 1; diff --git a/src/Library/CMakeLists.txt b/src/Library/CMakeLists.txt index 51939b885668..63644c6f620c 100644 --- a/src/Library/CMakeLists.txt +++ b/src/Library/CMakeLists.txt @@ -6,6 +6,7 @@ add_subdirectory(Cli) add_subdirectory(Color) add_subdirectory(Compression) add_subdirectory(Config) +add_subdirectory(Environment) add_subdirectory(Geometry) add_subdirectory(Image) add_subdirectory(Json) diff --git a/src/Library/Environment/Android/AndroidEnvironment.cpp b/src/Library/Environment/Android/AndroidEnvironment.cpp new file mode 100644 index 000000000000..c651847cd6b6 --- /dev/null +++ b/src/Library/Environment/Android/AndroidEnvironment.cpp @@ -0,0 +1,33 @@ +#include "AndroidEnvironment.h" + +#include + +std::string AndroidEnvironment::queryRegistry(const std::string &path) const { + return {} +} + +std::string AndroidEnvironment::path(EnvironmentPath path) const { + const char *result = nullptr; + if (path == PATH_ANDROID_STORAGE_INTERNAL) { + result = SDL_AndroidGetInternalStoragePath(); + } else if (path == PATH_ANDROID_STORAGE_EXTERNAL) { + result = SDL_AndroidGetExternalStoragePath(); + } + + // TODO(captainurist): No PATH_HOME on Android? Verify & write a comment here. + + if (result) + return result; + return {}; +} + +std::string AndroidEnvironment::getenv(const std::string &key) const { + const char *result = SDL_getenv(key.c_str()); + if (result) + return result; + return {}; +} + +std::unique_ptr Environment::createStandardEnvironment() { + return std::make_unique(); +} diff --git a/src/Library/Environment/Android/AndroidEnvironment.h b/src/Library/Environment/Android/AndroidEnvironment.h new file mode 100644 index 000000000000..eec6873b3c9d --- /dev/null +++ b/src/Library/Environment/Android/AndroidEnvironment.h @@ -0,0 +1,10 @@ +#pragma once + +#include "Library/Environment/Interface/Environment.h" + +class AndroidEnvironment : public Environment { +public: + virtual std::string queryRegistry(const std::string &path) const override; + virtual std::string path(EnvironmentPath path) const override; + virtual std::string getenv(const std::string &key) const override; +}; diff --git a/src/Library/Environment/Android/CMakeLists.txt b/src/Library/Environment/Android/CMakeLists.txt new file mode 100644 index 000000000000..431a1ee68204 --- /dev/null +++ b/src/Library/Environment/Android/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +set(LIBRARY_ENVIRONMENT_ANDROID_SOURCES + AndroidEnvironment.cpp) + +set(LIBRARY_ENVIRONMENT_ANDROID_HEADERS + AndroidEnvironment.h) + +if(BUILD_PLATFORM STREQUAL "android") + add_library(library_environment_android STATIC ${LIBRARY_ENVIRONMENT_ANDROID_SOURCES} ${LIBRARY_ENVIRONMENT_ANDROID_HEADERS}) + target_check_style(library_environment_android) + target_link_libraries(library_environment_android PUBLIC library_environment_interface PRIVATE SDL2::SDL2OE) + + add_library(library_environment_implementation INTERFACE) + target_link_libraries(library_environment_implementation INTERFACE library_environment_android) +endif() diff --git a/src/Library/Environment/CMakeLists.txt b/src/Library/Environment/CMakeLists.txt new file mode 100644 index 000000000000..c0ff09f831ed --- /dev/null +++ b/src/Library/Environment/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +add_subdirectory(Android) +add_subdirectory(Interface) +add_subdirectory(Posix) +add_subdirectory(Test) +add_subdirectory(Win) diff --git a/src/Library/Environment/Interface/CMakeLists.txt b/src/Library/Environment/Interface/CMakeLists.txt new file mode 100644 index 000000000000..faecf1fa6c3b --- /dev/null +++ b/src/Library/Environment/Interface/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +set(LIBRARY_ENVIRONMENT_INTERFACE_SOURCES) + +set(LIBRARY_ENVIRONMENT_INTERFACE_HEADERS + Environment.h + EnvironmentEnums.h) + +add_library(library_environment_interface INTERFACE ${LIBRARY_ENVIRONMENT_INTERFACE_SOURCES} ${LIBRARY_ENVIRONMENT_INTERFACE_HEADERS}) +target_check_style(library_environment_interface) diff --git a/src/Library/Environment/Interface/Environment.h b/src/Library/Environment/Interface/Environment.h new file mode 100644 index 000000000000..fa0e4bd0a78d --- /dev/null +++ b/src/Library/Environment/Interface/Environment.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "EnvironmentEnums.h" + +class Environment { + public: + virtual ~Environment() = default; + + /** + * @return Newly created standard `Environment` instance. + */ + static std::unique_ptr createStandardEnvironment(); + + /** + * Windows-only function for querying the registry. Always returns an empty string on non-Windows systems. + * + * @param path Registry path to query. + * @return Value at the given path, or an empty string in case of an error. + */ + virtual std::string queryRegistry(const std::string &path) const = 0; + + /** + * Accessor for various system paths. + * + * @param path Path to get. + */ + virtual std::string path(EnvironmentPath path) const = 0; + + /** + * Same as `std::getenv`, but takes & returns UTF8-encoded keys and values on all platforms. + * + * Returns an empty string for non-existent environment variables, and thus doesn't distinguish between empty and + * non-existent values (and you shouldn't, either). + * + * @param key UTF8-encoded name of the environment variable to query. + * @return UTF8-encoded value of the environment variable. + */ + virtual std::string getenv(const std::string &key) const = 0; +}; diff --git a/src/Library/Environment/Interface/EnvironmentEnums.h b/src/Library/Environment/Interface/EnvironmentEnums.h new file mode 100644 index 000000000000..fc08aab62289 --- /dev/null +++ b/src/Library/Environment/Interface/EnvironmentEnums.h @@ -0,0 +1,8 @@ +#pragma once + +enum class EnvironmentPath { + PATH_HOME, + PATH_ANDROID_STORAGE_INTERNAL, + PATH_ANDROID_STORAGE_EXTERNAL +}; +using enum EnvironmentPath; diff --git a/src/Library/Environment/Posix/CMakeLists.txt b/src/Library/Environment/Posix/CMakeLists.txt new file mode 100644 index 000000000000..f436e07089bd --- /dev/null +++ b/src/Library/Environment/Posix/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +set(LIBRARY_ENVIRONMENT_POSIX_SOURCES + PosixEnvironment.cpp) + +set(LIBRARY_ENVIRONMENT_POSIX_HEADERS + PosixEnvironment.h) + +if(NOT BUILD_PLATFORM STREQUAL "windows" AND NOT BUILD_PLATFORM STREQUAL "android") + add_library(library_environment_posix STATIC ${LIBRARY_ENVIRONMENT_POSIX_SOURCES} ${LIBRARY_ENVIRONMENT_POSIX_HEADERS}) + target_check_style(library_environment_posix) + target_link_libraries(library_environment_posix PUBLIC library_environment_interface) + + add_library(library_environment_implementation INTERFACE) + target_link_libraries(library_environment_implementation INTERFACE library_environment_posix) +endif() diff --git a/src/Library/Environment/Posix/PosixEnvironment.cpp b/src/Library/Environment/Posix/PosixEnvironment.cpp new file mode 100644 index 000000000000..e59dbcad8bcd --- /dev/null +++ b/src/Library/Environment/Posix/PosixEnvironment.cpp @@ -0,0 +1,27 @@ +#include "PosixEnvironment.h" + +#include +#include + +std::string PosixEnvironment::queryRegistry(const std::string &path) const { + return {}; +} + +std::string PosixEnvironment::path(EnvironmentPath path) const { + if (path == PATH_HOME) { + return getenv("HOME"); + } else { + return {}; + } +} + +std::string PosixEnvironment::getenv(const std::string &key) const { + const char *result = std::getenv(key.c_str()); + if (result) + return result; + return {}; +} + +std::unique_ptr Environment::createStandardEnvironment() { + return std::make_unique(); +} diff --git a/src/Library/Environment/Posix/PosixEnvironment.h b/src/Library/Environment/Posix/PosixEnvironment.h new file mode 100644 index 000000000000..286f4856a868 --- /dev/null +++ b/src/Library/Environment/Posix/PosixEnvironment.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "Library/Environment/Interface/Environment.h" + +class PosixEnvironment : public Environment { + public: + virtual std::string queryRegistry(const std::string &path) const override; + virtual std::string path(EnvironmentPath path) const override; + virtual std::string getenv(const std::string &key) const override; +}; diff --git a/src/Library/Environment/Test/CMakeLists.txt b/src/Library/Environment/Test/CMakeLists.txt new file mode 100644 index 000000000000..075f5a0b8683 --- /dev/null +++ b/src/Library/Environment/Test/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +if(OE_BUILD_TESTS) + set(TEST_LIBRARY_ENVIRONMENT_SOURCES + Environment_ut.cpp) + + add_library(test_library_environment OBJECT ${TEST_LIBRARY_ENVIRONMENT_SOURCES}) + target_check_style(test_library_environment) + target_link_libraries(test_library_environment PUBLIC testing_unit library_environment_implementation) + + target_link_libraries(OpenEnroth_UnitTest PUBLIC test_library_environment) +endif() diff --git a/src/Library/Environment/Test/Environment_ut.cpp b/src/Library/Environment/Test/Environment_ut.cpp new file mode 100644 index 000000000000..5dc15f339856 --- /dev/null +++ b/src/Library/Environment/Test/Environment_ut.cpp @@ -0,0 +1,50 @@ +#include + +#include "Testing/Unit/UnitTest.h" + +#include "Library/Environment/Interface/Environment.h" + +static const char8_t *u8prefix = u8"\u0444\u0430\u0439\u043B"; // "File" in Russian. +static const char16_t *u16prefix = u"\u0444\u0430\u0439\u043B"; + +#ifdef _WINDOWS +static_assert(sizeof(char16_t) == sizeof(wchar_t)); +#endif + +UNIT_TEST(Environment, getenv_empty) { + std::unique_ptr environment = Environment::createStandardEnvironment(); + + std::string result = environment->getenv("_ABCDEFG_123456_"); // Getting a non-existent var should work. + EXPECT_TRUE(result.empty()); +} + +UNIT_TEST(Environment, getenv) { + std::unique_ptr environment = Environment::createStandardEnvironment(); + + const char *name = "_SOME_VAR_12345"; + const char *value = reinterpret_cast(u8prefix); + + const wchar_t *wname = L"_SOME_VAR_12345"; + const wchar_t *wvalue = reinterpret_cast(u16prefix); + +#ifdef _WINDOWS + errno_t status = _wputenv_s(wname, wvalue); + EXPECT_EQ(status, 0); +#else + int status = setenv(name, value, 1); + EXPECT_EQ(status, 0); +#endif + + std::string ourResult = environment->getenv(name); + EXPECT_EQ(std::string_view(ourResult), std::string_view(value)); + + const char *stdResultPtr = std::getenv(name); + std::string stdResult = stdResultPtr ? stdResultPtr : ""; + EXPECT_NE(stdResultPtr, nullptr); + +#ifdef _WINDOWS + EXPECT_EQ(stdResult, "????"); // Unfortunately, this is how it works. We just pin it in place with a test. +#else + EXPECT_EQ(stdResult, value); +#endif +} diff --git a/src/Library/Environment/Win/CMakeLists.txt b/src/Library/Environment/Win/CMakeLists.txt new file mode 100644 index 000000000000..25f7ef9c6af8 --- /dev/null +++ b/src/Library/Environment/Win/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) + +set(LIBRARY_ENVIRONMENT_WIN_SOURCES + WinEnvironment.cpp) + +set(LIBRARY_ENVIRONMENT_WIN_HEADERS + WinEnvironment.h) + +if(BUILD_PLATFORM STREQUAL "windows") + add_library(library_environment_win STATIC ${LIBRARY_ENVIRONMENT_WIN_SOURCES} ${LIBRARY_ENVIRONMENT_WIN_HEADERS}) + target_check_style(library_environment_win) + target_link_libraries(library_environment_win PUBLIC library_environment_interface utility) + + add_library(library_environment_implementation INTERFACE) + target_link_libraries(library_environment_implementation INTERFACE library_environment_win) +endif() diff --git a/src/Library/Platform/Win/WinPlatform.cpp b/src/Library/Environment/Win/WinEnvironment.cpp similarity index 68% rename from src/Library/Platform/Win/WinPlatform.cpp rename to src/Library/Environment/Win/WinEnvironment.cpp index dd2d730f8bf8..9d586a91b4c0 100644 --- a/src/Library/Platform/Win/WinPlatform.cpp +++ b/src/Library/Environment/Win/WinEnvironment.cpp @@ -1,37 +1,12 @@ -#include "WinPlatform.h" - -#include +#include "WinEnvironment.h" #define WIN32_LEAN_AND_MEAN #include -#include "Library/Platform/Sdl/SdlLogSource.h" - -#include "Utility/String.h" - -static std::string toUtf8(std::wstring_view wstr) { - std::string result; - - int len = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr.size(), nullptr, 0, nullptr, nullptr); - if (len == 0) - return result; - - result.resize(len); - WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr.size(), result.data(), len, nullptr, nullptr); - return result; -} - -static std::wstring toUtf16(std::string_view str) { - std::wstring result; - - int len = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.size(), nullptr, 0); - if (len == 0) - return result; +#include +#include - result.resize(len); - MultiByteToWideChar(CP_UTF8, 0, str.data(), str.size(), result.data(), len); - return result; -} +#include "Utility/Win/Unicode.h" // TODO(captainurist): revisit this code once I'm on a win machine. static std::wstring OS_GetAppStringRecursive(HKEY parent_key, const wchar_t *path, int flags) { @@ -101,10 +76,25 @@ static std::wstring OS_GetAppStringRecursive(HKEY parent_key, const wchar_t *pat } } -std::string WinPlatform::winQueryRegistry(const std::string &path) const { - return toUtf8(OS_GetAppStringRecursive(NULL, toUtf16(path).c_str(), 0)); +std::string WinEnvironment::queryRegistry(const std::string &path) const { + return win::toUtf8(OS_GetAppStringRecursive(NULL, win::toUtf16(path).c_str(), 0)); +} + +std::string WinEnvironment::path(EnvironmentPath path) const { + if (path == PATH_HOME) { + return getenv("USERPROFILE"); + } else { + return {}; + } +} + +std::string WinEnvironment::getenv(const std::string &key) const { + const wchar_t *result = _wgetenv(win::toUtf16(key).c_str()); + if (result) + return win::toUtf8(result); + return {}; } -std::unique_ptr Platform::createStandardPlatform(Logger *logger) { - return std::make_unique(logger); +std::unique_ptr Environment::createStandardEnvironment() { + return std::make_unique(); } diff --git a/src/Library/Environment/Win/WinEnvironment.h b/src/Library/Environment/Win/WinEnvironment.h new file mode 100644 index 000000000000..edece95bf7bf --- /dev/null +++ b/src/Library/Environment/Win/WinEnvironment.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Library/Environment/Interface/Environment.h" + +#include + +class WinEnvironment : public Environment { + public: + virtual std::string queryRegistry(const std::string &path) const override; + virtual std::string path(EnvironmentPath path) const override; + virtual std::string getenv(const std::string &key) const override; +}; diff --git a/src/Library/Platform/CMakeLists.txt b/src/Library/Platform/CMakeLists.txt index e1612ed5ad43..5df597fea5db 100644 --- a/src/Library/Platform/CMakeLists.txt +++ b/src/Library/Platform/CMakeLists.txt @@ -4,7 +4,5 @@ add_subdirectory(Application) add_subdirectory(Filters) add_subdirectory(Interface) add_subdirectory(Null) -add_subdirectory(Posix) add_subdirectory(Proxy) add_subdirectory(Sdl) -add_subdirectory(Win) diff --git a/src/Library/Platform/Interface/Platform.h b/src/Library/Platform/Interface/Platform.h index f83b05585983..a4bf3a5bdcb1 100644 --- a/src/Library/Platform/Interface/Platform.h +++ b/src/Library/Platform/Interface/Platform.h @@ -136,21 +136,6 @@ class Platform { * @return Current value of a monotonic clock in milliseconds. */ virtual int64_t tickCount() const = 0; - - /** - * Windows-only function for querying the registry. Always returns an empty string on non-Windows systems. - * - * @param path Registry path to query. - * @return Value at the given path, or an empty string in case of an error. - */ - virtual std::string winQueryRegistry(const std::string &path) const = 0; - - /** - * Get various application filesystem paths. - * - * @param type Storage type. - */ - virtual std::string storagePath(const PlatformStorage type) const = 0; }; /** diff --git a/src/Library/Platform/Interface/PlatformEnums.h b/src/Library/Platform/Interface/PlatformEnums.h index e9af376d3f5b..4e37a2dc8b5d 100644 --- a/src/Library/Platform/Interface/PlatformEnums.h +++ b/src/Library/Platform/Interface/PlatformEnums.h @@ -69,12 +69,6 @@ enum class PlatformEventType { }; using enum PlatformEventType; -enum class PlatformStorage { - ANDROID_STORAGE_INTERNAL, - ANDROID_STORAGE_EXTERNAL -}; -using enum PlatformStorage; - enum class PlatformKey : int { // usual text input KEY_CHAR, // TODO(captainurist): this doesn't belong here diff --git a/src/Library/Platform/Null/NullPlatform.cpp b/src/Library/Platform/Null/NullPlatform.cpp index b2d442c4238e..442c566985db 100644 --- a/src/Library/Platform/Null/NullPlatform.cpp +++ b/src/Library/Platform/Null/NullPlatform.cpp @@ -43,12 +43,3 @@ void NullPlatform::showMessageBox(const std::string &title, const std::string &m int64_t NullPlatform::tickCount() const { return 0; // Time's not flowing in null platform. } - -std::string NullPlatform::winQueryRegistry(const std::string &path) const { - return {}; -} - -std::string NullPlatform::storagePath(const PlatformStorage type) const { - return {}; -} - diff --git a/src/Library/Platform/Null/NullPlatform.h b/src/Library/Platform/Null/NullPlatform.h index 70223d62a080..c977457ae841 100644 --- a/src/Library/Platform/Null/NullPlatform.h +++ b/src/Library/Platform/Null/NullPlatform.h @@ -23,8 +23,6 @@ class NullPlatform : public Platform { virtual std::vector displayGeometries() const override; virtual void showMessageBox(const std::string &title, const std::string &message) const override; virtual int64_t tickCount() const override; - virtual std::string winQueryRegistry(const std::string &path) const override; - virtual std::string storagePath(const PlatformStorage type) const override; private: std::unique_ptr _state; diff --git a/src/Library/Platform/Posix/CMakeLists.txt b/src/Library/Platform/Posix/CMakeLists.txt deleted file mode 100644 index 8f138554daa3..000000000000 --- a/src/Library/Platform/Posix/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -cmake_minimum_required(VERSION 3.24 FATAL_ERROR) - -set(LIBRARY_PLATFORM_POSIX_SOURCES - PosixPlatform.cpp) - -set(LIBRARY_PLATFORM_POSIX_HEADERS) - -if(NOT BUILD_PLATFORM STREQUAL "windows") - add_library(library_platform_posix STATIC ${LIBRARY_PLATFORM_POSIX_SOURCES} ${LIBRARY_PLATFORM_POSIX_HEADERS}) - target_check_style(library_platform_posix) - target_link_libraries(library_platform_posix PUBLIC library_platform_sdl) - - add_library(library_platform_implementation INTERFACE) - target_link_libraries(library_platform_implementation INTERFACE library_platform_posix) -endif() diff --git a/src/Library/Platform/Posix/PosixPlatform.cpp b/src/Library/Platform/Posix/PosixPlatform.cpp deleted file mode 100644 index 17f283bb31d6..000000000000 --- a/src/Library/Platform/Posix/PosixPlatform.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include - -#include "Library/Platform/Sdl/SdlPlatform.h" - -std::unique_ptr Platform::createStandardPlatform(Logger *logger) { - return std::make_unique(logger); -} diff --git a/src/Library/Platform/Proxy/ProxyPlatform.cpp b/src/Library/Platform/Proxy/ProxyPlatform.cpp index 19edec665d8c..345a71edb2d0 100644 --- a/src/Library/Platform/Proxy/ProxyPlatform.cpp +++ b/src/Library/Platform/Proxy/ProxyPlatform.cpp @@ -36,11 +36,3 @@ void ProxyPlatform::showMessageBox(const std::string &title, const std::string & int64_t ProxyPlatform::tickCount() const { return nonNullBase()->tickCount(); } - -std::string ProxyPlatform::winQueryRegistry(const std::string &path) const { - return nonNullBase()->winQueryRegistry(path); -} - -std::string ProxyPlatform::storagePath(const PlatformStorage type) const { - return nonNullBase()->storagePath(type); -} diff --git a/src/Library/Platform/Proxy/ProxyPlatform.h b/src/Library/Platform/Proxy/ProxyPlatform.h index bdd13757fc92..d4958d7c1838 100644 --- a/src/Library/Platform/Proxy/ProxyPlatform.h +++ b/src/Library/Platform/Proxy/ProxyPlatform.h @@ -22,6 +22,4 @@ class ProxyPlatform : public ProxyBase { virtual std::vector displayGeometries() const override; virtual void showMessageBox(const std::string &title, const std::string &message) const override; virtual int64_t tickCount() const override; - virtual std::string winQueryRegistry(const std::string &path) const override; - virtual std::string storagePath(const PlatformStorage type) const override; }; diff --git a/src/Library/Platform/Sdl/CMakeLists.txt b/src/Library/Platform/Sdl/CMakeLists.txt index b0eb64f89a95..8e3a03bfaeca 100644 --- a/src/Library/Platform/Sdl/CMakeLists.txt +++ b/src/Library/Platform/Sdl/CMakeLists.txt @@ -33,3 +33,8 @@ target_link_libraries(library_platform_main PRIVATE SDL2::SDL2OE) add_library(library_platform_sdl STATIC ${PLATFORM_SDL_SOURCES} ${PLATFORM_SDL_HEADERS}) target_check_style(library_platform_sdl) target_link_libraries(library_platform_sdl PUBLIC utility library_logger PRIVATE SDL2::SDL2OE) + +# Currently we have only one implementation library defining Platform::createStandardPlatform function, but we used +# to have two. So we're keeping the old mechanism with an additional library_platform_implementation library. +add_library(library_platform_implementation INTERFACE) +target_link_libraries(library_platform_implementation INTERFACE library_platform_sdl) diff --git a/src/Library/Platform/Sdl/SdlPlatform.cpp b/src/Library/Platform/Sdl/SdlPlatform.cpp index cfa434a9a1b6..bb8b3d2bc2dd 100644 --- a/src/Library/Platform/Sdl/SdlPlatform.cpp +++ b/src/Library/Platform/Sdl/SdlPlatform.cpp @@ -137,30 +137,6 @@ int64_t SdlPlatform::tickCount() const { return SDL_GetTicks64(); } -std::string SdlPlatform::winQueryRegistry(const std::string &) const { - return {}; -} - -std::string SdlPlatform::storagePath(const PlatformStorage type) const { - std::string result{}; - const char *path = NULL; - - switch (type) { -#if __ANDROID__ - case (ANDROID_STORAGE_INTERNAL): - path = SDL_AndroidGetInternalStoragePath(); - if (path) - result = path; - break; - case (ANDROID_STORAGE_EXTERNAL): - path = SDL_AndroidGetExternalStoragePath(); - if (path) - result = path; - break; -#endif - default: - break; - } - - return result; +std::unique_ptr Platform::createStandardPlatform(Logger *logger) { + return std::make_unique(logger); } diff --git a/src/Library/Platform/Sdl/SdlPlatform.h b/src/Library/Platform/Sdl/SdlPlatform.h index 57f4a043664a..a54a9e97aaa5 100644 --- a/src/Library/Platform/Sdl/SdlPlatform.h +++ b/src/Library/Platform/Sdl/SdlPlatform.h @@ -29,10 +29,6 @@ class SdlPlatform: public Platform { virtual int64_t tickCount() const override; - virtual std::string winQueryRegistry(const std::string &path) const override; - - virtual std::string storagePath(const PlatformStorage type) const override; - private: bool _initialized = false; std::unique_ptr _state; diff --git a/src/Library/Platform/Win/CMakeLists.txt b/src/Library/Platform/Win/CMakeLists.txt deleted file mode 100644 index e3ec3ceaeb3b..000000000000 --- a/src/Library/Platform/Win/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -cmake_minimum_required(VERSION 3.24 FATAL_ERROR) - -set(LIBRARY_PLATFORM_WIN_SOURCES - WinPlatform.cpp) - -set(LIBRARY_PLATFORM_WIN_HEADERS - WinPlatform.h) - -if(BUILD_PLATFORM STREQUAL "windows") - add_library(library_platform_win STATIC ${LIBRARY_PLATFORM_WIN_SOURCES} ${LIBRARY_PLATFORM_WIN_HEADERS}) - target_check_style(library_platform_win) - target_link_libraries(library_platform_win PUBLIC library_platform_sdl) - - add_library(library_platform_implementation INTERFACE) - target_link_libraries(library_platform_implementation INTERFACE library_platform_win) -endif() diff --git a/src/Library/Platform/Win/WinPlatform.h b/src/Library/Platform/Win/WinPlatform.h deleted file mode 100644 index 965b0fdb4f71..000000000000 --- a/src/Library/Platform/Win/WinPlatform.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -#include "Library/Platform/Sdl/SdlPlatform.h" - -class WinPlatform : public SdlPlatform { - public: - using SdlPlatform::SdlPlatform; - - virtual std::string winQueryRegistry(const std::string &path) const override; -}; diff --git a/src/Utility/CMakeLists.txt b/src/Utility/CMakeLists.txt index 9d8e6f2396f7..57de53eac659 100644 --- a/src/Utility/CMakeLists.txt +++ b/src/Utility/CMakeLists.txt @@ -14,7 +14,8 @@ set(UTILITY_SOURCES Streams/MemoryInputStream.cpp Streams/StringOutputStream.cpp Streams/TempFileOutputStream.cpp - String.cpp) + String.cpp + UnicodeCrt.cpp) set(UTILITY_HEADERS DataPath.h @@ -41,8 +42,15 @@ set(UTILITY_HEADERS Streams/OutputStream.h Streams/StringOutputStream.h Streams/TempFileOutputStream.h + Win/Unicode.h + Workaround/ToUnderlying.h String.h - Unaligned.h) + Unaligned.h + UnicodeCrt.h) + +if(BUILD_PLATFORM STREQUAL "windows") + list(APPEND UTILITY_SOURCES Win/Unicode.cpp) +endif() add_library(utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) target_check_style(utility) @@ -60,7 +68,8 @@ if(OE_BUILD_TESTS) Tests/IndexedArray_ut.cpp Tests/IndexedBitset_ut.cpp Tests/Segment_ut.cpp - Tests/String_ut.cpp) + Tests/String_ut.cpp + Tests/UnicodeCrt_ut.cpp) add_library(test_utility OBJECT ${TEST_UTILITY_SOURCES}) target_link_libraries(test_utility PUBLIC testing_unit utility) diff --git a/src/Utility/DataPath.cpp b/src/Utility/DataPath.cpp index 2e06796864a6..71b925d77767 100644 --- a/src/Utility/DataPath.cpp +++ b/src/Utility/DataPath.cpp @@ -39,8 +39,8 @@ static const std::vector> globalValidateList = { {"shaders", "gltwodshader.vert"} }; -void setDataPath(const std::string &data_path) { - globalDataPath = expandUserPath(data_path); +void setDataPath(const std::filesystem::path &dataPath) { + globalDataPath = dataPath; } std::string makeDataPath(std::initializer_list paths) { diff --git a/src/Utility/DataPath.h b/src/Utility/DataPath.h index 4f041260dfcb..40d6764309a7 100644 --- a/src/Utility/DataPath.h +++ b/src/Utility/DataPath.h @@ -2,8 +2,11 @@ #include #include +#include -void setDataPath(const std::string &data_path); +// TODO(captainurist): this doesn't belong in Utility + +void setDataPath(const std::filesystem::path &dataPath); std::string makeDataPath(std::initializer_list paths); diff --git a/src/Utility/FileSystem.cpp b/src/Utility/FileSystem.cpp index f8f20c4d2764..13acc4aca3c9 100644 --- a/src/Utility/FileSystem.cpp +++ b/src/Utility/FileSystem.cpp @@ -2,22 +2,9 @@ #include "String.h" -static std::string homePath() { - const char *result = getenv("HOME"); - if (result) - return result; - - // TODO(captainurist): this will break with unicode usernames on Windows - result = getenv("USERPROFILE"); - if (result) - return result; - - return std::string(); -} - -std::filesystem::path expandUserPath(std::string path) { +std::filesystem::path expandUserPath(const std::string &path, const std::string &home) { if (path.starts_with("~/")) - return std::filesystem::path(homePath()) / path.substr(2); + return std::filesystem::path(home) / path.substr(2); return path; } diff --git a/src/Utility/FileSystem.h b/src/Utility/FileSystem.h index 78ea14786ccf..31c4be99a8b6 100644 --- a/src/Utility/FileSystem.h +++ b/src/Utility/FileSystem.h @@ -8,9 +8,10 @@ * absolute paths instead of calling `std::filesystem::path` constructor. * * @param path Path as a string. + * @param home Home path as a string. * @return Path as `std::filesystem::path`. */ -std::filesystem::path expandUserPath(std::string path); +std::filesystem::path expandUserPath(const std::string &path, const std::string &home); /** * This function emulates the behavior of a case-insensitive filesystem. You pass in a path, this function traverses diff --git a/src/Utility/Tests/UnicodeCrt_ut.cpp b/src/Utility/Tests/UnicodeCrt_ut.cpp new file mode 100644 index 000000000000..8b9acab5905f --- /dev/null +++ b/src/Utility/Tests/UnicodeCrt_ut.cpp @@ -0,0 +1,110 @@ +#include +#include +#include + +#include "Testing/Unit/UnitTest.h" + +#include "Utility/Streams/FileOutputStream.h" + +static const char8_t *u8prefix = u8"\u0444\u0430\u0439\u043B"; // "File" in Russian. +static const char16_t *u16prefix = u"\u0444\u0430\u0439\u043B"; + +#ifdef _WINDOWS +static_assert(sizeof(char16_t) == sizeof(wchar_t)); +#endif + +UNIT_TEST(UnicodeCrt, fopen) { + std::u8string u8path = std::u8string(u8prefix) + u8"_fopen"; + std::string path = reinterpret_cast(u8path.c_str()); + + const char *data = "data"; + const size_t dataSize = 4; + + FILE *f1 = fopen(path.c_str(), "w"); + EXPECT_NE(f1, nullptr); + + size_t written = fwrite(data, dataSize, 1, f1); + EXPECT_EQ(written, 1); + + int status = fclose(f1); + EXPECT_EQ(status, 0); + +#ifdef _WINDOWS + std::u16string u16path = std::u16string(u16prefix) + u"_fopen"; + std::wstring wpath = reinterpret_cast(u16path.c_str()); + + FILE *f2 = _wfopen(wpath.c_str(), L"r"); + EXPECT_NE(f2, nullptr); + + char buffer[10] = {}; + size_t read = fread(buffer, dataSize, 1, f2); + EXPECT_EQ(read, 1); + EXPECT_EQ(std::string_view(buffer), std::string_view(data)); + + int status2 = fclose(f2); + EXPECT_EQ(status2, 0); +#endif + + // Using UTF-8 api directly here. + EXPECT_TRUE(std::filesystem::exists(u8path)); + EXPECT_TRUE(std::filesystem::remove(u8path)); + EXPECT_FALSE(std::filesystem::exists(u8path)); +} + +UNIT_TEST(UnicodeCrt, filesystem_exists_remove) { + std::u8string u8path = std::u8string(u8prefix) + u8"_exists"; + std::string path = reinterpret_cast(u8path.c_str()); + + FileOutputStream s(path); + s.write("something"); + s.close(); + + // Using char * api here, expecting it to be handled as UTF-8. + EXPECT_TRUE(std::filesystem::exists(path)); + EXPECT_TRUE(std::filesystem::remove(path)); + EXPECT_FALSE(std::filesystem::exists(path)); +} + +UNIT_TEST(UnicodeCrt, filesystem_rename) { + std::u8string u8path = std::u8string(u8prefix) + u8"_rename"; + std::string path = reinterpret_cast(u8path.c_str()); + std::string path2 = path + "2"; + + FileOutputStream s(path); + s.write("something_else"); + s.close(); + + // Using char * api here, expecting it to be handled as UTF-8. + EXPECT_TRUE(std::filesystem::exists(path)); + std::filesystem::rename(path, path2); + EXPECT_FALSE(std::filesystem::exists(path)); + EXPECT_TRUE(std::filesystem::exists(path2)); + EXPECT_TRUE(std::filesystem::remove(path2)); + EXPECT_FALSE(std::filesystem::exists(path2)); +} + +UNIT_TEST(UnicodeCrt, fstreams) { + std::u8string u8path = std::u8string(u8prefix) + u8"_fstreams"; + std::string path = reinterpret_cast(u8path.c_str()); + + const char *data = "data"; + size_t dataSize = 4; + + std::ofstream f1; + f1.open(path); + f1.write(data, dataSize); + f1.close(); + + std::ifstream f2; + f2.open(path); + char buffer[10] = {}; + f2.read(buffer, dataSize); + f2.close(); + + EXPECT_EQ(std::string_view(buffer), std::string_view(data)); + + // Using UTF-8 api directly here. + EXPECT_TRUE(std::filesystem::exists(u8path)); + EXPECT_TRUE(std::filesystem::remove(u8path)); + EXPECT_FALSE(std::filesystem::exists(u8path)); +} diff --git a/src/Utility/UnicodeCrt.cpp b/src/Utility/UnicodeCrt.cpp new file mode 100644 index 000000000000..29ddfe806fd6 --- /dev/null +++ b/src/Utility/UnicodeCrt.cpp @@ -0,0 +1,53 @@ +#include "UnicodeCrt.h" + +#include + +#ifdef _WINDOWS +# define WIN32_LEAN_AND_MEAN +# include +# include +# include +# include "Utility/Win/Unicode.h" +# include "Utility/Exception.h" + +struct LocalFreeDeleter { + void operator()(HLOCAL ptr) { + LocalFree(ptr); + } +}; +#endif + +static bool globalUnicodeCrtInitialized = false; + +UnicodeCrt::UnicodeCrt(int &argc, char **&argv) { + assert(!globalUnicodeCrtInitialized); // Don't create several instances! + globalUnicodeCrtInitialized = true; + +#ifdef _WINDOWS + // Convert command line first. + // + // Note on SDL interop. SDL runs basically the same code before calling into SDL_main, and thus if we get here from + // platformMain, argc and argv are already UTF8-encoded. However, SDL doesn't add/remove arguments, so it's safe + // run the same code again. + // + // CommandLineToArgvW can return NULL when out of memory, which should never happen. We don't handle errors here. + std::unique_ptr argvw(CommandLineToArgvW(GetCommandLineW(), &argc); // NOLINT + + for (int i = 0; i < argc; i++) + _storage.push_back(win::toUtf8(argvw[i])); + for (int i = 0; i < argc; i++) + _argv.push_back(_storage[i].data()); + _argv.push_back(nullptr); + argv = _argv.data(); + + // Switch to UTF8 for CRT functions. Without this, std::filesystem won't be able to process UTF8 paths. + if (std::setlocale(LC_ALL, ".UTF-8") == nullptr) + throw Exception("Could not change system locale to UTF-8"); + + // Also use UTF8 for console io. + if (SetConsoleCP(CP_UTF8) == 0) + throw Exception("Could not set console input codepage to UTF-8"); + if (SetConsoleOutputCP(CP_UTF8) == 0) + throw Exception("Could not set console output codepage to UTF-8"); +#endif +} diff --git a/src/Utility/UnicodeCrt.h b/src/Utility/UnicodeCrt.h new file mode 100644 index 000000000000..0597dda38024 --- /dev/null +++ b/src/Utility/UnicodeCrt.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +/** + * Utility class that turns on UTF-8 for most of CRT, and converts command-line arguments to UTF-8. This is really only + * needed on Windows, and this class does nothing on POSIX. + * + * Use it like this: + * ``` + * int main(int argc, char **argv) { + * try { + * UnicodeCrt _(argc, argv); + * // Use argc & argv here. + * } catch (...) { + * // Your error processing here. + * } + * } + * ``` + * + * Note that for this to work on older Windows versions, CRT should be statically linked. This is how OE releases + * are built right now. + * + * Also note that while `UnicodeCrt` constructor shouldn't normally throw, it can throw in theory. + * + * @see https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170#utf-8-support + */ +class UnicodeCrt { + public: + UnicodeCrt(int &argc, char **&argv); + + private: + std::vector _argv; + std::vector _storage; +}; diff --git a/src/Utility/Win/Unicode.cpp b/src/Utility/Win/Unicode.cpp new file mode 100644 index 000000000000..e80cbb835563 --- /dev/null +++ b/src/Utility/Win/Unicode.cpp @@ -0,0 +1,28 @@ +#include "Unicode.h" + +#define WIN32_LEAN_AND_MEAN +#include + +std::string win::toUtf8(std::wstring_view wstr) { + std::string result; + + int len = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr.size(), nullptr, 0, nullptr, nullptr); + if (len == 0) + return result; + + result.resize(len); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), wstr.size(), result.data(), len, nullptr, nullptr); + return result; +} + +std::wstring win::toUtf16(std::string_view str) { + std::wstring result; + + int len = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.size(), nullptr, 0); + if (len == 0) + return result; + + result.resize(len); + MultiByteToWideChar(CP_UTF8, 0, str.data(), str.size(), result.data(), len); + return result; +} diff --git a/src/Utility/Win/Unicode.h b/src/Utility/Win/Unicode.h new file mode 100644 index 000000000000..91c36df76a8b --- /dev/null +++ b/src/Utility/Win/Unicode.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace win { + +// These are only available on Windows: + +std::string toUtf8(std::wstring_view wstr); +std::wstring toUtf16(std::string_view str); + +} // namespace win diff --git a/test/Bin/GameTest/GameTestMain.cpp b/test/Bin/GameTest/GameTestMain.cpp index 37783586f0ee..3066bca8e610 100644 --- a/test/Bin/GameTest/GameTestMain.cpp +++ b/test/Bin/GameTest/GameTestMain.cpp @@ -12,6 +12,7 @@ #include "Library/Platform/Application/PlatformApplication.h" #include "Utility/Format.h" +#include "Utility/UnicodeCrt.h" #include "GameTestOptions.h" @@ -24,6 +25,7 @@ void printGoogleTestHelp(char *app) { int platformMain(int argc, char **argv) { try { + UnicodeCrt _(argc, argv); GameTestOptions opts = GameTestOptions::parse(argc, argv); if (opts.helpPrinted) { fmt::print(stdout, "\n"); diff --git a/test/Bin/UnitTest/UnitTestMain.cpp b/test/Bin/UnitTest/UnitTestMain.cpp index 7cc76b25b79a..308e04162054 100644 --- a/test/Bin/UnitTest/UnitTestMain.cpp +++ b/test/Bin/UnitTest/UnitTestMain.cpp @@ -1,6 +1,9 @@ #include +#include "Utility/UnicodeCrt.h" + GTEST_API_ int main(int argc, char **argv) { + UnicodeCrt _(argc, argv); testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }