Index: binaries/data/mods/public/gui/replaymenu/replay_actions.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_actions.js +++ binaries/data/mods/public/gui/replaymenu/replay_actions.js @@ -161,7 +161,7 @@ messageBox( 500, 200, translate("Are you sure you want to delete this replay permanently?") + "\n" + - escapeText(Engine.GetReplayDirectoryName(replay.directory)), + escapeText(Engine.GetFullReplayPathPrintable(replay.directory)), translate("Delete replay"), [translate("No"), translate("Yes")], [null, function() { reallyDeleteReplay(replay.directory); }] Index: binaries/data/mods/public/gui/replaymenu/replay_menu.js =================================================================== --- binaries/data/mods/public/gui/replaymenu/replay_menu.js +++ binaries/data/mods/public/gui/replaymenu/replay_menu.js @@ -271,7 +271,7 @@ Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType); Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"), { "numberOfPlayers": replay.attribs.settings.PlayerData.length }); - Engine.GetGUIObjectByName("replayFilename").caption = escapeText(Engine.GetReplayDirectoryName(replay.directory)); + Engine.GetGUIObjectByName("replayFilename").caption = escapeText(Engine.GetFullReplayPathPrintable(replay.directory)); let metadata = Engine.GetReplayMetadata(replay.directory); Engine.GetGUIObjectByName("sgPlayersNames").caption = Index: source/gui/scripting/ScriptFunctions.cpp =================================================================== --- source/gui/scripting/ScriptFunctions.cpp +++ source/gui/scripting/ScriptFunctions.cpp @@ -211,7 +211,7 @@ return std::wstring(); if (g_Game->IsVisualReplay()) - return OsPath(g_Game->GetReplayPath()).Parent().Filename().string(); + return g_Game->GetReplayPath().Parent().Filename().string(); return g_Game->GetReplayLogger().GetDirectory().Filename().string(); } Index: source/lib/path.h =================================================================== --- source/lib/path.h +++ source/lib/path.h @@ -1,4 +1,4 @@ -/* Copyright (c) 2013 Wildfire Games +/* Copyright (c) 2017 Wildfire Games * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -135,11 +135,20 @@ */ std::string string8() const { - // TODO: On Unixes, this will only be correct for ASCII or ISO-8859-1 - // encoded paths; we should probably assume UTF-8 encoding by default - // (but take care to handle non-valid-UTF-8 paths safely). + Status err; +#if !OS_WIN + // On Unix, assume paths consisting of 8-bit charactes saved in this wide string. + std::string spath(path.begin(), path.end()); + + // Return it if it's valid UTF-8 + wstring_from_utf8(spath, &err); + if(err == INFO::OK) + return spath; - return utf8_from_wstring(path); + // Otherwise assume ISO-8859-1 and let utf8_from_wstring treat each character as a Unicode code point. +#endif + // On Windows, paths are UTF-16 strings. We don't support non-BMP characters so we can assume it's simply a wstring. + return utf8_from_wstring(path, &err); } bool operator<(const Path& rhs) const Index: source/main.cpp =================================================================== --- source/main.cpp +++ source/main.cpp @@ -467,20 +467,20 @@ const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); - const CStr replayFile = + const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : - isNonVisualReplay ? args.Get("replay") : ""; + isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { - if (!FileExists(OsPath(replayFile))) + if (!FileExists(replayFile)) { - debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.c_str()); + debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } - if (DirectoryExists(OsPath(replayFile))) + if (DirectoryExists(replayFile)) { - debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.c_str()); + debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } Index: source/ps/Game.h =================================================================== --- source/ps/Game.h +++ source/ps/Game.h @@ -18,9 +18,10 @@ #ifndef INCLUDED_GAME #define INCLUDED_GAME -#include "ps/Errors.h" #include +#include "ps/Errors.h" +#include "ps/Filesystem.h" #include "scriptinterface/ScriptVal.h" #include "simulation2/helpers/Player.h" @@ -91,7 +92,7 @@ void StartGame(JS::MutableHandleValue attribs, const std::string& savedState); PSRETURN ReallyStartGame(); - bool StartVisualReplay(const std::string& replayPath); + bool StartVisualReplay(const OsPath& replayPath); /** * Periodic heartbeat that controls the process. performs all per-frame updates. @@ -196,7 +197,7 @@ inline float GetSimRate() const { return m_SimRate; } - inline std::string GetReplayPath() const + inline OsPath GetReplayPath() const { return m_ReplayPath; } /** @@ -222,7 +223,7 @@ bool m_IsSavedGame; // true if loading a saved game; false for a new game int LoadVisualReplayData(); - std::string m_ReplayPath; + OsPath m_ReplayPath; bool m_IsVisualReplay; std::istream* m_ReplayStream; u32 m_FinalReplayTurn; Index: source/ps/Game.cpp =================================================================== --- source/ps/Game.cpp +++ source/ps/Game.cpp @@ -168,9 +168,9 @@ return 0; } -bool CGame::StartVisualReplay(const std::string& replayPath) +bool CGame::StartVisualReplay(const OsPath& replayPath) { - debug_printf("Starting to replay %s\n", replayPath.c_str()); + debug_printf("Starting to replay %s\n", replayPath.string8().c_str()); m_IsVisualReplay = true; ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); @@ -178,7 +178,7 @@ SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger())); m_ReplayPath = replayPath; - m_ReplayStream = new std::ifstream(m_ReplayPath.c_str()); + m_ReplayStream = new std::ifstream(OsString(replayPath).c_str()); std::string type; ENSURE((*m_ReplayStream >> type).good() && type == "start"); Index: source/ps/Replay.h =================================================================== --- source/ps/Replay.h +++ source/ps/Replay.h @@ -96,7 +96,7 @@ CReplayPlayer(); ~CReplayPlayer(); - void Load(const std::string& path); + void Load(const OsPath& path); void Replay(bool serializationtest, int rejointestturn, bool ooslog); private: Index: source/ps/Replay.cpp =================================================================== --- source/ps/Replay.cpp +++ source/ps/Replay.cpp @@ -106,11 +106,11 @@ delete m_Stream; } -void CReplayPlayer::Load(const std::string& path) +void CReplayPlayer::Load(const OsPath& path) { ENSURE(!m_Stream); - m_Stream = new std::ifstream(path.c_str()); + m_Stream = new std::ifstream(OsString(path).c_str()); ENSURE(m_Stream->good()); } Index: source/ps/VisualReplay.h =================================================================== --- source/ps/VisualReplay.h +++ source/ps/VisualReplay.h @@ -39,7 +39,7 @@ /** * Replays the commands.txt file in the given subdirectory visually. */ -void StartVisualReplay(const CStrW& directory); +void StartVisualReplay(const OsPath& directory); /** * Reads the replay Cache file and parses it into a jsObject @@ -89,22 +89,22 @@ * @param replayFile - path to commands.txt, whose parent directory will be deleted. * @return true if deletion was successful, false on error */ -bool DeleteReplay(const CStrW& replayFile); +bool DeleteReplay(const OsPath& replayFile); /** * Returns the parsed header of the replay file (commands.txt). */ -JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); +JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName); /** * Returns whether or not the metadata / summary screen data has been saved properly when the game ended. */ -bool HasReplayMetadata(const CStrW& directoryName); +bool HasReplayMetadata(const OsPath& directoryName); /** * Returns the metadata of a replay. */ -JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); +JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName); /** * Saves the metadata from the session to metadata.json. Index: source/ps/VisualReplay.cpp =================================================================== --- source/ps/VisualReplay.cpp +++ source/ps/VisualReplay.cpp @@ -46,10 +46,28 @@ OsPath VisualReplay::GetDirectoryName() { const Paths paths(g_args); - return OsPath(paths.UserData() / "replays" / engine_version); + + ////////////////////////////////////////////////////////////////////////////// + ///////////// ORIGINAL CODE + ////////////////////////////////////////////////////////////////////////////// + //return OsPath(paths.UserData() / "replays" / engine_version); + + ////////////////////////////////////////////////////////////////////////////// + ///////////// DEBUG CODE + ///////////// This can be used to reproduce the bug, even if the current username doesn't contain a non-ASCI character. + ///////////// Create a "replay" directory that starts with "a" and contains a non-ASCII character like e + ^ + ////////////////////////////////////////////////////////////////////////////// + DirectoryNames directories; + GetDirectoryEntries(paths.UserData(), nullptr, &directories); + OsPath replaydir; + for (OsPath p : directories) + if (p.string8().substr(0, 1) == "a") + replaydir = p; + + return OsPath(paths.UserData() / replaydir / engine_version); } -void VisualReplay::StartVisualReplay(const CStrW& directory) +void VisualReplay::StartVisualReplay(const OsPath& directory) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); @@ -61,7 +79,7 @@ return; g_Game = new CGame(false, false); - g_Game->StartVisualReplay(replayFile.string8()); + g_Game->StartVisualReplay(replayFile); } bool VisualReplay::ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject) @@ -72,7 +90,7 @@ if (!FileExists(cacheFileName)) return false; - std::ifstream cacheStream(cacheFileName.string8().c_str()); + std::ifstream cacheStream(OsString(cacheFileName).c_str()); CStr cacheStr((std::istreambuf_iterator(cacheStream)), std::istreambuf_iterator()); cacheStream.close(); @@ -95,7 +113,7 @@ JSAutoRequest rq(cx); JS::RootedValue replaysRooted(cx, JS::ObjectValue(*replays)); - std::ofstream cacheStream(tempCacheFileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc); + std::ofstream cacheStream(OsString(tempCacheFileName).c_str(), std::ofstream::out | std::ofstream::trunc); cacheStream << scriptInterface.StringifyJSON(&replaysRooted); cacheStream.close(); @@ -111,7 +129,7 @@ JSAutoRequest rq(cx); // Maps the filename onto the index and size - typedef std::map> replayCacheMap; + typedef std::map> replayCacheMap; replayCacheMap fileList; @@ -127,7 +145,7 @@ JS_GetElement(cx, cachedReplaysObject, j, &replay); JS::RootedValue file(cx); - CStr fileName; + OsPath fileName; double fileSize; scriptInterface.GetProperty(replay, "directory", fileName); scriptInterface.GetProperty(replay, "fileSize", fileSize); @@ -158,7 +176,7 @@ continue; bool isNew = true; - replayCacheMap::iterator it = fileList.find(directory.string8()); + replayCacheMap::iterator it = fileList.find(directory); if (it != fileList.end()) { if (compareFiles) @@ -180,7 +198,7 @@ CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); scriptInterface.Eval("({})", &replayData); - scriptInterface.SetProperty(replayData, "directory", directory); + scriptInterface.SetProperty(replayData, "directory", directory.string()); scriptInterface.SetProperty(replayData, "fileSize", (double)fileInfo.Size()); } JS_SetElement(cx, replays, i++, replayData); @@ -241,7 +259,7 @@ * * @return The current cursor position or -1 on error. */ -inline int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64& fileSize) +inline off_t goBackToLineBeginning(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { int currentPos; char character; @@ -255,7 +273,7 @@ if (!replayStream->good()) { - LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.c_str()); + LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.string8().c_str()); return -1; } @@ -269,7 +287,7 @@ replayStream->seekg(-2, std::ios_base::cur); } - LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.c_str()); + LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.string8().c_str()); return -1; } @@ -279,7 +297,7 @@ * * @return seconds or -1 on error */ -inline int getReplayDuration(std::istream* replayStream, const CStr& fileName, const u64& fileSize) +inline int getReplayDuration(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { CStr type; @@ -290,7 +308,7 @@ // There should be about 5 lines to read until a turn is found. for (int linesRead = 1; linesRead < 1000; ++linesRead) { - int currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); + off_t currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); // Read error or reached file beginning. No turns exist. if (currentPosition < 1) @@ -298,12 +316,12 @@ if (!replayStream->good()) { - LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.c_str()); + LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.string8().c_str()); return -1; } // Found last turn, compute duration. - if ((u64) currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") + if (currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") { u32 turn = 0, turnLength = 0; *replayStream >> turn >> turnLength; @@ -314,7 +332,7 @@ replayStream->seekg(currentPosition - 2, std::ios_base::beg); } - LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str()); + LOGERROR("Infinite loop when determining replay duration for %s", fileName.string8().c_str()); return -1; } @@ -329,26 +347,24 @@ // Get file size and modification date CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); - const u64 fileSize = (u64)fileInfo.Size(); + const off_t fileSize = fileInfo.Size(); if (fileSize == 0) return JSVAL_NULL; - // Open file - const CStr fileName = replayFile.string8(); - std::ifstream* replayStream = new std::ifstream(fileName.c_str()); + std::ifstream* replayStream = new std::ifstream(OsString(replayFile).c_str()); - // File must begin with "start" CStr type; if (!(*replayStream >> type).good()) { - LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str()); + LOGERROR("Couldn't open %s.", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } + if (type != "start") { - LOGWARNING("The replay %s is broken!", fileName.c_str()); + LOGWARNING("The replay %s doesn't begin with 'start'!", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } @@ -361,7 +377,7 @@ JS::RootedValue attribs(cx); if (!scriptInterface.ParseJSON(header, &attribs)) { - LOGERROR("Couldn't parse replay header of %s", fileName.c_str()); + LOGERROR("Couldn't parse replay header of %s", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JSVAL_NULL; } @@ -382,7 +398,7 @@ return JSVAL_NULL; } - int duration = getReplayDuration(replayStream, fileName, fileSize); + int duration = getReplayDuration(replayStream, replayFile, fileSize); SAFE_DELETE(replayStream); @@ -393,14 +409,14 @@ // Return the actual data JS::RootedValue replayData(cx); scriptInterface.Eval("({})", &replayData); - scriptInterface.SetProperty(replayData, "directory", directory); + scriptInterface.SetProperty(replayData, "directory", directory.string()); scriptInterface.SetProperty(replayData, "fileSize", (double)fileSize); scriptInterface.SetProperty(replayData, "attribs", attribs); scriptInterface.SetProperty(replayData, "duration", duration); return replayData; } -bool VisualReplay::DeleteReplay(const CStrW& replayDirectory) +bool VisualReplay::DeleteReplay(const OsPath& replayDirectory) { if (replayDirectory.empty()) return false; @@ -409,7 +425,7 @@ return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; } -JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) +JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName) { // Create empty JS object JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); @@ -423,7 +439,7 @@ return attribs; // Open file - std::istream* replayStream = new std::ifstream(replayFile.string8().c_str()); + std::istream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type, line; ENSURE((*replayStream >> type).good() && type == "start"); @@ -470,16 +486,16 @@ } // Get the directory of the currently active replay - const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json"; + const OsPath fileName(g_Game->GetReplayLogger().GetDirectory() / L"metadata.json"); CreateDirectories(fileName.Parent(), 0700); - std::ofstream stream (fileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc); + std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc); stream << scriptInterface->StringifyJSON(&metadata, false); stream.close(); debug_printf("Saved replay metadata to %s\n", fileName.string8().c_str()); } -bool VisualReplay::HasReplayMetadata(const CStrW& directoryName) +bool VisualReplay::HasReplayMetadata(const OsPath& directoryName) { const OsPath filePath(GetDirectoryName() / directoryName / L"metadata.json"); @@ -492,7 +508,7 @@ return fileInfo.Size() > 0; } -JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) +JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const OsPath& directoryName) { if (!HasReplayMetadata(directoryName)) return JSVAL_NULL; @@ -501,7 +517,7 @@ JSAutoRequest rq(cx); JS::RootedValue metadata(cx); - std::ifstream* stream = new std::ifstream(OsPath(GetDirectoryName() / directoryName / L"metadata.json").string8()); + std::ifstream* stream = new std::ifstream(OsString(GetDirectoryName() / directoryName / L"metadata.json").c_str()); ENSURE(stream->good()); CStr line; std::getline(*stream, line); Index: source/ps/scripting/JSInterface_VisualReplay.h =================================================================== --- source/ps/scripting/JSInterface_VisualReplay.h +++ source/ps/scripting/JSInterface_VisualReplay.h @@ -31,7 +31,7 @@ JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); void AddReplayToCache(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); void RegisterScriptFunctions(ScriptInterface& scriptInterface); - CStrW GetReplayDirectoryName(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); + CStrW GetFullReplayPathPrintable(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); } #endif Index: source/ps/scripting/JSInterface_VisualReplay.cpp =================================================================== --- source/ps/scripting/JSInterface_VisualReplay.cpp +++ source/ps/scripting/JSInterface_VisualReplay.cpp @@ -58,9 +58,9 @@ VisualReplay::AddReplayToCache(*(pCxPrivate->pScriptInterface), directoryName); } -CStrW JSI_VisualReplay::GetReplayDirectoryName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName) +CStrW JSI_VisualReplay::GetFullReplayPathPrintable(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName) { - return OsPath(VisualReplay::GetDirectoryName() / directoryName).string(); + return wstring_from_utf8(OsPath(VisualReplay::GetDirectoryName() / directoryName).string8()); } void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface) @@ -72,5 +72,5 @@ scriptInterface.RegisterFunction("GetReplayMetadata"); scriptInterface.RegisterFunction("HasReplayMetadata"); scriptInterface.RegisterFunction("AddReplayToCache"); - scriptInterface.RegisterFunction("GetReplayDirectoryName"); + scriptInterface.RegisterFunction("GetFullReplayPathPrintable"); }