diff --git a/Generals/Code/GameEngine/Include/Common/FileSystem.h b/Generals/Code/GameEngine/Include/Common/FileSystem.h index abd90f0f08b..cac342148e3 100644 --- a/Generals/Code/GameEngine/Include/Common/FileSystem.h +++ b/Generals/Code/GameEngine/Include/Common/FileSystem.h @@ -136,6 +136,8 @@ class FileSystem : public SubsystemInterface Bool areMusicFilesOnCD(); void loadMusicFilesFromCD(); void unloadMusicFilesFromCD(); + AsciiString normalizePath(const AsciiString& path) const; ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure. + static Bool isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath); ///< determines if a file path is within a base path. Both paths must be absolute, but do not need to exist. protected: diff --git a/Generals/Code/GameEngine/Include/Common/LocalFileSystem.h b/Generals/Code/GameEngine/Include/Common/LocalFileSystem.h index 3e58119975e..87e78f173ac 100644 --- a/Generals/Code/GameEngine/Include/Common/LocalFileSystem.h +++ b/Generals/Code/GameEngine/Include/Common/LocalFileSystem.h @@ -50,6 +50,7 @@ class LocalFileSystem : public SubsystemInterface virtual void getFileListInDirectory(const AsciiString& currentDirectory, const AsciiString& originalDirectory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const = 0; ///< search the given directory for files matching the searchName (egs. *.ini, *.rep). Possibly search subdirectories. virtual Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const = 0; ///< see FileSystem.h virtual Bool createDirectory(AsciiString directory) = 0; ///< see FileSystem.h + virtual AsciiString normalizePath(const AsciiString& filePath) const = 0; ///< see FileSystem.h protected: }; diff --git a/Generals/Code/GameEngine/Source/Common/System/FileSystem.cpp b/Generals/Code/GameEngine/Source/Common/System/FileSystem.cpp index 2a3d17e3444..65e31318951 100644 --- a/Generals/Code/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/FileSystem.cpp @@ -322,3 +322,54 @@ void FileSystem::unloadMusicFilesFromCD() TheArchiveFileSystem->closeArchiveFile( MUSIC_BIG ); } + +//============================================================================ +// FileSystem::normalizePath +//============================================================================ +AsciiString FileSystem::normalizePath(const AsciiString& path) const +{ + return TheLocalFileSystem->normalizePath(path); +} + +//============================================================================ +// FileSystem::isPathInDirectory +//============================================================================ +Bool FileSystem::isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath) +{ + AsciiString testPathNormalized = TheFileSystem->normalizePath(testPath); + AsciiString basePathNormalized = TheFileSystem->normalizePath(basePath); + + if (basePathNormalized.isEmpty()) + { + DEBUG_CRASH(("Unable to normalize base directory path '%s'.\n", basePath.str())); + return false; + } + else if (testPathNormalized.isEmpty()) + { + DEBUG_CRASH(("Unable to normalize file path '%s'.\n", testPath.str())); + return false; + } + +#ifdef _WIN32 + const char* pathSep = "\\"; +#else + const char* pathSep = "/"; +#endif + + if (!basePathNormalized.endsWith(pathSep)) + { + basePathNormalized.concat(pathSep); + } + +#ifdef _WIN32 + if (!testPathNormalized.startsWithNoCase(basePathNormalized)) +#else + if (!testPathNormalized.startsWith(basePathNormalized)) +#endif + { + DEBUG_CRASH(("Normalized file path for '%s': '%s' was outside the expected base path of '%s' (normalized: '%s').\n", testPath.str(), testPathNormalized.str(), basePath.str(), basePathNormalized.str())); + return false; + } + + return true; +} \ No newline at end of file diff --git a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index c4935f26c8c..4922a96bbd0 100644 --- a/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -783,7 +783,7 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const //------------------------------------------------------------------------------------------------- Bool GameState::isInSaveDirectory(const AsciiString& path) const { - return path.startsWithNoCase(getSaveDirectory()); + return FileSystem::isPathInDirectory(path, getSaveDirectory()); } // ------------------------------------------------------------------------------------------------ @@ -902,10 +902,13 @@ AsciiString GameState::realMapPathToPortableMapPath(const AsciiString& in) const AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const { AsciiString prefix; + // The directory where the real map path should be contained in. + AsciiString containingBasePath; if (in.startsWithNoCase(PORTABLE_SAVE)) { // the save dir ends with "\\" prefix = getSaveDirectory(); + containingBasePath = prefix; prefix.concat(getMapLeafName(in)); } else if (in.startsWithNoCase(PORTABLE_MAPS)) @@ -913,6 +916,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const // the map dir DOES NOT end with "\\", must add it prefix = TheMapCache->getMapDir(); prefix.concat("\\"); + containingBasePath = prefix; prefix.concat(getMapLeafAndDirName(in)); } else if (in.startsWithNoCase(PORTABLE_USER_MAPS)) @@ -920,15 +924,21 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const // the map dir DOES NOT end with "\\", must add it prefix = TheMapCache->getUserMapDir(); prefix.concat("\\"); + containingBasePath = prefix; prefix.concat(getMapLeafAndDirName(in)); } else { DEBUG_CRASH(("Map file was not found in any of the expected directories; this is impossible")); - //throw INI_INVALID_DATA; - // uncaught exceptions crash us. better to just use a bad path. - prefix = in; + // Empty string represents a failure, either caused by an invalid prefix or a relative path leading outside the base path + return AsciiString::TheEmptyString; } + + if (!FileSystem::isPathInDirectory(prefix, containingBasePath)) + { + return AsciiString::TheEmptyString; + } + prefix.toLower(); return prefix; } diff --git a/Generals/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Generals/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp index 3ed78617532..b2df6bad2af 100644 --- a/Generals/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Generals/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -688,7 +688,17 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) DEBUG_LOG(("%ls\n", log.str())); #endif - if (TheFileSystem->doesFileExist(msg->getRealFilename().str())) + AsciiString realFileName = msg->getRealFilename(); + if (realFileName.isEmpty()) + { + // TheSuperHackers @security slurmlord 18/06/2025 As the file name/path from the NetFileCommandMsg failed to normalize, + // in other words is bogus and points outside of the approved target directory, avoid an arbitrary file overwrite vulnerability + // by simply returning and let the transfer time out. + DEBUG_LOG(("Got a file name transferred that failed to normalize: '%s'!\n", msg->getPortableFilename().str())); + return; + } + + if (TheFileSystem->doesFileExist(realFileName.str())) { DEBUG_LOG(("File exists already!\n")); //return; @@ -720,13 +730,13 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) } #endif // COMPRESS_TARGAS - File *fp = TheFileSystem->openFile(msg->getRealFilename().str(), File::CREATE | File::BINARY | File::WRITE); + File *fp = TheFileSystem->openFile(realFileName.str(), File::CREATE | File::BINARY | File::WRITE); if (fp) { fp->write(buf, len); fp->close(); fp = NULL; - DEBUG_LOG(("Wrote %d bytes to file %s!\n",len,msg->getRealFilename().str())); + DEBUG_LOG(("Wrote %d bytes to file %s!\n", len, realFileName.str())); } else diff --git a/Generals/Code/GameEngine/Source/GameNetwork/GameInfo.cpp b/Generals/Code/GameEngine/Source/GameNetwork/GameInfo.cpp index 24501b3dcc5..0d564421d8b 100644 --- a/Generals/Code/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Generals/Code/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -1044,7 +1044,17 @@ Bool ParseAsciiStringToGameInfo(GameInfo *game, AsciiString options) mapName.concat(token); mapName.concat('.'); mapName.concat(TheMapCache->getMapExtension()); - mapName = TheGameState->portableMapPathToRealMapPath(mapName); + AsciiString realMapName = TheGameState->portableMapPathToRealMapPath(mapName); + if (realMapName.isEmpty()) + { + // TheSuperHackers @security slurmlord 18/06/2025 As the map file name/path from the AsciiString failed to normalize, + // in other words is bogus and points outside of the approved target directory for maps, avoid an arbitrary file overwrite vulnerability + // if the save or network game embeds a custom map to store at the location, by flagging the options as not OK and rejecting the game. + optionsOk = FALSE; + DEBUG_LOG(("ParseAsciiStringToGameInfo - saw bogus map name ('%s'); quitting\n", mapName.str())); + break; + } + mapName = realMapName; sawMap = true; DEBUG_LOG(("ParseAsciiStringToGameInfo - map name is %s\n", mapName.str())); } diff --git a/Generals/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h b/Generals/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h index 8caf0a48720..4d25d27a109 100644 --- a/Generals/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h +++ b/Generals/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h @@ -49,6 +49,7 @@ class Win32LocalFileSystem : public LocalFileSystem virtual Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const; virtual Bool createDirectory(AsciiString directory); + virtual AsciiString normalizePath(const AsciiString& filePath) const; protected: }; diff --git a/Generals/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp b/Generals/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp index 3c274e4b9ed..c768553cfff 100644 --- a/Generals/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp +++ b/Generals/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp @@ -214,3 +214,23 @@ Bool Win32LocalFileSystem::createDirectory(AsciiString directory) } return FALSE; } + +AsciiString Win32LocalFileSystem::normalizePath(const AsciiString& filePath) const +{ + DWORD retval = GetFullPathNameA(filePath.str(), 0, NULL, NULL); + if (retval == 0) + { + DEBUG_LOG(("Unable to determine buffer size for normalized file path. Error=(%u).\n", GetLastError())); + return AsciiString::TheEmptyString; + } + + AsciiString normalizedFilePath; + retval = GetFullPathNameA(filePath.str(), retval, normalizedFilePath.getBufferForRead(retval - 1), NULL); + if (retval == 0) + { + DEBUG_LOG(("Unable to normalize file path '%s'. Error=(%u).\n", filePath.str(), GetLastError())); + return AsciiString::TheEmptyString; + } + + return normalizedFilePath; +} \ No newline at end of file diff --git a/GeneralsMD/Code/GameEngine/Include/Common/FileSystem.h b/GeneralsMD/Code/GameEngine/Include/Common/FileSystem.h index 381658cfc2d..755e1c76b98 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/FileSystem.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/FileSystem.h @@ -140,6 +140,8 @@ class FileSystem : public SubsystemInterface Bool areMusicFilesOnCD(); void loadMusicFilesFromCD(); void unloadMusicFilesFromCD(); + AsciiString normalizePath(const AsciiString& path) const; ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure. + static Bool isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath); ///< determines if a file path is within a base path. Both paths must be absolute, but do not need to exist. protected: mutable std::map m_fileExist; }; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/LocalFileSystem.h b/GeneralsMD/Code/GameEngine/Include/Common/LocalFileSystem.h index 0c54a4deba8..fcec9499477 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/LocalFileSystem.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/LocalFileSystem.h @@ -50,6 +50,7 @@ class LocalFileSystem : public SubsystemInterface virtual void getFileListInDirectory(const AsciiString& currentDirectory, const AsciiString& originalDirectory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const = 0; ///< search the given directory for files matching the searchName (egs. *.ini, *.rep). Possibly search subdirectories. virtual Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const = 0; ///< see FileSystem.h virtual Bool createDirectory(AsciiString directory) = 0; ///< see FileSystem.h + virtual AsciiString normalizePath(const AsciiString& filePath) const = 0; ///< see FileSystem.h protected: }; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp index 02f4b95abfd..4f8a7d63b3f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/FileSystem.cpp @@ -339,3 +339,54 @@ void FileSystem::unloadMusicFilesFromCD() TheArchiveFileSystem->closeArchiveFile( MUSIC_BIG ); } + +//============================================================================ +// FileSystem::normalizePath +//============================================================================ +AsciiString FileSystem::normalizePath(const AsciiString& path) const +{ + return TheLocalFileSystem->normalizePath(path); +} + +//============================================================================ +// FileSystem::isPathInDirectory +//============================================================================ +Bool FileSystem::isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath) +{ + AsciiString testPathNormalized = TheFileSystem->normalizePath(testPath); + AsciiString basePathNormalized = TheFileSystem->normalizePath(basePath); + + if (basePathNormalized.isEmpty()) + { + DEBUG_CRASH(("Unable to normalize base directory path '%s'.\n", basePath.str())); + return false; + } + else if (testPathNormalized.isEmpty()) + { + DEBUG_CRASH(("Unable to normalize file path '%s'.\n", testPath.str())); + return false; + } + +#ifdef _WIN32 + const char* pathSep = "\\"; +#else + const char* pathSep = "/"; +#endif + + if (!basePathNormalized.endsWith(pathSep)) + { + basePathNormalized.concat(pathSep); + } + +#ifdef _WIN32 + if (!testPathNormalized.startsWithNoCase(basePathNormalized)) +#else + if (!testPathNormalized.startsWith(basePathNormalized)) +#endif + { + DEBUG_CRASH(("Normalized file path for '%s': '%s' was outside the expected base path of '%s' (normalized: '%s').\n", testPath.str(), testPathNormalized.str(), basePath.str(), basePathNormalized.str())); + return false; + } + + return true; +} \ No newline at end of file diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index fb288a2e3b5..8444bfc9dd0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -783,7 +783,7 @@ AsciiString GameState::getFilePathInSaveDirectory(const AsciiString& leaf) const //------------------------------------------------------------------------------------------------- Bool GameState::isInSaveDirectory(const AsciiString& path) const { - return path.startsWithNoCase(getSaveDirectory()); + return FileSystem::isPathInDirectory(path, getSaveDirectory()); } // ------------------------------------------------------------------------------------------------ @@ -902,10 +902,13 @@ AsciiString GameState::realMapPathToPortableMapPath(const AsciiString& in) const AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const { AsciiString prefix; + // The directory where the real map path should be contained in. + AsciiString containingBasePath; if (in.startsWithNoCase(PORTABLE_SAVE)) { // the save dir ends with "\\" prefix = getSaveDirectory(); + containingBasePath = prefix; prefix.concat(getMapLeafName(in)); } else if (in.startsWithNoCase(PORTABLE_MAPS)) @@ -913,6 +916,7 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const // the map dir DOES NOT end with "\\", must add it prefix = TheMapCache->getMapDir(); prefix.concat("\\"); + containingBasePath = prefix; prefix.concat(getMapLeafAndDirName(in)); } else if (in.startsWithNoCase(PORTABLE_USER_MAPS)) @@ -920,15 +924,21 @@ AsciiString GameState::portableMapPathToRealMapPath(const AsciiString& in) const // the map dir DOES NOT end with "\\", must add it prefix = TheMapCache->getUserMapDir(); prefix.concat("\\"); + containingBasePath = prefix; prefix.concat(getMapLeafAndDirName(in)); } else { DEBUG_CRASH(("Map file was not found in any of the expected directories; this is impossible")); - //throw INI_INVALID_DATA; - // uncaught exceptions crash us. better to just use a bad path. - prefix = in; + // Empty string represents a failure, either caused by an invalid prefix or a relative path leading outside the base path. + return AsciiString::TheEmptyString; } + + if (!FileSystem::isPathInDirectory(prefix, containingBasePath)) + { + return AsciiString::TheEmptyString; + } + prefix.toLower(); return prefix; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp index 83f8bd5f144..9900f2e2c56 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -688,7 +688,17 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) DEBUG_LOG(("%ls\n", log.str())); #endif - if (TheFileSystem->doesFileExist(msg->getRealFilename().str())) + AsciiString realFileName = msg->getRealFilename(); + if (realFileName.isEmpty()) + { + // TheSuperHackers @security slurmlord 18/06/2025 As the file name/path from the NetFileCommandMsg failed to normalize, + // in other words is bogus and points outside of the approved target directory, avoid an arbitrary file overwrite vulnerability + // by simply returning and let the transfer time out. + DEBUG_LOG(("Got a file name transferred that failed to normalize: '%s'!\n", msg->getPortableFilename().str())); + return; + } + + if (TheFileSystem->doesFileExist(realFileName.str())) { DEBUG_LOG(("File exists already!\n")); //return; @@ -720,13 +730,13 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) } #endif // COMPRESS_TARGAS - File *fp = TheFileSystem->openFile(msg->getRealFilename().str(), File::CREATE | File::BINARY | File::WRITE); + File *fp = TheFileSystem->openFile(realFileName.str(), File::CREATE | File::BINARY | File::WRITE); if (fp) { fp->write(buf, len); fp->close(); fp = NULL; - DEBUG_LOG(("Wrote %d bytes to file %s!\n",len,msg->getRealFilename().str())); + DEBUG_LOG(("Wrote %d bytes to file %s!\n", len, realFileName.str())); } else diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp index ddc322f2518..8d6b22755f5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -1081,7 +1081,17 @@ Bool ParseAsciiStringToGameInfo(GameInfo *game, AsciiString options) mapName.concat(token); mapName.concat('.'); mapName.concat(TheMapCache->getMapExtension()); - mapName = TheGameState->portableMapPathToRealMapPath(mapName); + AsciiString realMapName = TheGameState->portableMapPathToRealMapPath(mapName); + if (realMapName.isEmpty()) + { + // TheSuperHackers @security slurmlord 18/06/2025 As the map file name/path from the AsciiString failed to normalize, + // in other words is bogus and points outside of the approved target directory for maps, avoid an arbitrary file overwrite vulnerability + // if the save or network game embeds a custom map to store at the location, by flagging the options as not OK and rejecting the game. + optionsOk = FALSE; + DEBUG_LOG(("ParseAsciiStringToGameInfo - saw bogus map name ('%s'); quitting\n", mapName.str())); + break; + } + mapName = realMapName; sawMap = true; DEBUG_LOG(("ParseAsciiStringToGameInfo - map name is %s\n", mapName.str())); } diff --git a/GeneralsMD/Code/GameEngineDevice/Include/StdDevice/Common/StdLocalFileSystem.h b/GeneralsMD/Code/GameEngineDevice/Include/StdDevice/Common/StdLocalFileSystem.h index 3008120f68e..b0409e59450 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/StdDevice/Common/StdLocalFileSystem.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/StdDevice/Common/StdLocalFileSystem.h @@ -49,6 +49,7 @@ class StdLocalFileSystem : public LocalFileSystem virtual Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const; virtual Bool createDirectory(AsciiString directory); + virtual AsciiString normalizePath(const AsciiString& filePath) const; protected: }; diff --git a/GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h b/GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h index 310a00b9a37..c2c1c1cd7b7 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32LocalFileSystem.h @@ -49,6 +49,7 @@ class Win32LocalFileSystem : public LocalFileSystem virtual Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo) const; virtual Bool createDirectory(AsciiString directory); + virtual AsciiString normalizePath(const AsciiString& filePath) const; protected: }; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/StdDevice/Common/StdLocalFileSystem.cpp b/GeneralsMD/Code/GameEngineDevice/Source/StdDevice/Common/StdLocalFileSystem.cpp index 24ec744934b..5b21112b064 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/StdDevice/Common/StdLocalFileSystem.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/StdDevice/Common/StdLocalFileSystem.cpp @@ -339,3 +339,14 @@ Bool StdLocalFileSystem::createDirectory(AsciiString directory) } return result; } + +AsciiString StdLocalFileSystem::normalizePath(const AsciiString& filePath) const +{ + std::string nonNormalized(filePath.str()); +#ifndef _WIN32 + // Replace backslashes with forward slashes on non-Windows platforms + std::replace(unNormalized.begin(), unNormalized.end(), '\\', '/'); +#endif + std::filesystem::path pathNonNormalized(nonNormalized); + return AsciiString(pathNonNormalized.lexically_normal().string().c_str()); +} diff --git a/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp b/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp index 34262dbb825..5595d538273 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/Win32Device/Common/Win32LocalFileSystem.cpp @@ -214,3 +214,23 @@ Bool Win32LocalFileSystem::createDirectory(AsciiString directory) } return FALSE; } + +AsciiString Win32LocalFileSystem::normalizePath(const AsciiString& filePath) const +{ + DWORD retval = GetFullPathNameA(filePath.str(), 0, NULL, NULL); + if (retval == 0) + { + DEBUG_LOG(("Unable to determine buffer size for normalized file path. Error=(%u).\n", GetLastError())); + return AsciiString::TheEmptyString; + } + + AsciiString normalizedFilePath; + retval = GetFullPathNameA(filePath.str(), retval, normalizedFilePath.getBufferForRead(retval - 1), NULL); + if (retval == 0) + { + DEBUG_LOG(("Unable to normalize file path '%s'. Error=(%u).\n", filePath.str(), GetLastError())); + return AsciiString::TheEmptyString; + } + + return normalizedFilePath; +}