You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Editor/Util/PathUtil.cpp

562 lines
20 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "EditorDefs.h"
#include "PathUtil.h"
#include <AzCore/IO/SystemFile.h> // for AZ_MAX_PATH_LEN
#include <AzToolsFramework/API/EditorAssetSystemAPI.h> // for ebus events
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <QRegularExpression>
namespace
{
string g_currentModName; // folder name only!
}
namespace Path
{
//////////////////////////////////////////////////////////////////////////
void SplitPath(const QString& rstrFullPathFilename, QString& rstrDriveLetter, QString& rstrDirectory, QString& rstrFilename, QString& rstrExtension)
{
string strFullPathString(rstrFullPathFilename.toUtf8().data());
string strDriveLetter;
string strDirectory;
string strFilename;
string strExtension;
char* szPath((char*)strFullPathString.c_str());
char* pchLastPosition(szPath);
char* pchCurrentPosition(szPath);
char* pchAuxPosition(szPath);
// Directory named filenames containing ":" are invalid, so we can assume if there is a :
// it will be the drive name.
pchCurrentPosition = strchr(pchLastPosition, ':');
if (pchCurrentPosition == NULL)
{
rstrDriveLetter = "";
}
else
{
strDriveLetter.assign(pchLastPosition, pchCurrentPosition + 1);
pchLastPosition = pchCurrentPosition + 1;
}
pchCurrentPosition = strrchr(pchLastPosition, '\\');
pchAuxPosition = strrchr(pchLastPosition, '/');
if ((pchCurrentPosition == NULL) && (pchAuxPosition == NULL))
{
rstrDirectory = "";
}
else
{
// Since NULL is < valid pointer, so this will work.
if (pchAuxPosition > pchCurrentPosition)
{
pchCurrentPosition = pchAuxPosition;
}
strDirectory.assign(pchLastPosition, pchCurrentPosition + 1);
pchLastPosition = pchCurrentPosition + 1;
}
pchCurrentPosition = strrchr(pchLastPosition, '.');
if (pchCurrentPosition == NULL)
{
rstrExtension = "";
strFilename.assign(pchLastPosition);
}
else
{
strExtension.assign(pchCurrentPosition);
strFilename.assign(pchLastPosition, pchCurrentPosition);
}
rstrDriveLetter = strDriveLetter;
rstrDirectory = strDirectory;
rstrFilename = strFilename;
rstrExtension = strExtension;
}
//////////////////////////////////////////////////////////////////////////
void GetDirectoryQueue(const QString& rstrSourceDirectory, QStringList& rcstrDirectoryTree)
{
string strCurrentDirectoryName;
string strSourceDirectory(rstrSourceDirectory.toUtf8().data());
const char* szSourceDirectory(strSourceDirectory.c_str());
const char* pchCurrentPosition(szSourceDirectory);
const char* pchLastPosition(szSourceDirectory);
rcstrDirectoryTree.clear();
if (strSourceDirectory.empty())
{
return;
}
// It removes as many slashes the path has in its start...
// MAYBE and just maybe we should consider paths starting with
// more than 2 slashes invalid paths...
while ((*pchLastPosition == '\\') || (*pchLastPosition == '/'))
{
++pchLastPosition;
++pchCurrentPosition;
}
do
{
pchCurrentPosition = strpbrk(pchLastPosition, "\\/");
if (pchCurrentPosition == NULL)
{
break;
}
strCurrentDirectoryName.assign(pchLastPosition, pchCurrentPosition);
pchLastPosition = pchCurrentPosition + 1;
// Again, here we are skipping as many consecutive slashes.
while ((*pchLastPosition == '\\') || (*pchLastPosition == '/'))
{
++pchLastPosition;
}
rcstrDirectoryTree.push_back(strCurrentDirectoryName.c_str());
} while (true);
}
//////////////////////////////////////////////////////////////////////////
void ConvertSlashToBackSlash(QString& rstrStringToConvert)
{
rstrStringToConvert.replace('/', '\\');
rstrStringToConvert = CaselessPaths(rstrStringToConvert);
}
//////////////////////////////////////////////////////////////////////////
void ConvertBackSlashToSlash(QString& rstrStringToConvert)
{
rstrStringToConvert.replace('\\', '/');
rstrStringToConvert = CaselessPaths(rstrStringToConvert);
}
//////////////////////////////////////////////////////////////////////////
void SurroundWithQuotes(QString& rstrSurroundString)
{
QString strSurroundString(rstrSurroundString);
if (!strSurroundString.isEmpty())
{
if (strSurroundString[0] != '\"')
{
strSurroundString.insert(0, "\"");
}
if (strSurroundString[strSurroundString.size() - 1] != '\"')
{
strSurroundString.insert(strSurroundString.size(), "\"");
}
}
else
{
strSurroundString.insert(0, "\"");
strSurroundString.insert(strSurroundString.size(), "\"");
}
rstrSurroundString = strSurroundString;
}
//////////////////////////////////////////////////////////////////////////
QString GetExecutableFullPath()
{
return QDir::toNativeSeparators(QCoreApplication::applicationFilePath());
}
//////////////////////////////////////////////////////////////////////////
QString GetWindowsTempDirectory()
{
return QDir::tempPath();
}
//////////////////////////////////////////////////////////////////////////
QString GetEngineRootPath()
{
const char* engineRoot;
EBUS_EVENT_RESULT(engineRoot, AzFramework::ApplicationRequests::Bus, GetEngineRoot);
return QString(engineRoot);
}
//////////////////////////////////////////////////////////////////////////
QString& ReplaceFilename(const QString& strFilepath, const QString& strFilename, QString& strOutputFilename, bool bCallCaselessPath)
{
QString strDriveLetter;
QString strDirectory;
QString strOriginalFilename;
QString strExtension;
SplitPath(strFilepath, strDriveLetter, strDirectory, strOriginalFilename, strExtension);
strOutputFilename = strDriveLetter;
strOutputFilename += strDirectory;
strOutputFilename += strFilename;
strOutputFilename += strExtension;
if (bCallCaselessPath)
{
strOutputFilename = CaselessPaths(strOutputFilename);
}
return strOutputFilename;
}
bool IsFolder(const char* pPath)
{
DWORD attrs = GetFileAttributes(pPath);
if (attrs == FILE_ATTRIBUTE_DIRECTORY)
{
return true;
}
return false;
}
//////////////////////////////////////////////////////////////////////////
QString GetUserSandboxFolder()
{
return QString::fromUtf8("@user@/Sandbox/");
}
//////////////////////////////////////////////////////////////////////////
QString GetResolvedUserSandboxFolder()
{
char resolvedPath[AZ_MAX_PATH_LEN] = { 0 };
gEnv->pFileIO->ResolvePath(GetUserSandboxFolder().toUtf8().data(), resolvedPath, AZ_MAX_PATH_LEN);
return QString::fromLatin1(resolvedPath);
}
// internal function, you should use GetEditingGameDataFolder instead.
AZStd::string GetGameAssetsFolder()
{
const char* resultValue = nullptr;
EBUS_EVENT_RESULT(resultValue, AzToolsFramework::AssetSystemRequestBus, GetAbsoluteDevGameFolderPath);
if (!resultValue)
{
if ((gEnv) && (gEnv->pFileIO))
{
resultValue = gEnv->pFileIO->GetAlias("@devassets@");
}
}
if (!resultValue)
{
resultValue = ".";
}
return resultValue;
}
/// Get the data folder
AZStd::string GetEditingGameDataFolder()
{
// query the editor root. The bus exists in case we want tools to be able to override this.
if (g_currentModName.empty())
{
return GetGameAssetsFolder();
}
AZStd::string str(GetGameAssetsFolder());
str += "Mods\\";
str += g_currentModName;
return str;
}
//! Get the root folder (in source control or other writable assets) where you should save root data.
AZStd::string GetEditingRootFolder()
{
const char* resultValue = nullptr;
EBUS_EVENT_RESULT(resultValue, AzToolsFramework::AssetSystemRequestBus, GetAbsoluteDevRootFolderPath);
if (!resultValue)
{
if ((gEnv) && (gEnv->pFileIO))
{
resultValue = gEnv->pFileIO->GetAlias("@devassets@");
}
}
if (!resultValue)
{
resultValue = ".";
}
return resultValue;
}
AZStd::string MakeModPathFromGamePath(const char* relGamePath)
{
return GetEditingGameDataFolder() + "\\" + relGamePath;
}
QString FullPathToLevelPath(const QString& path)
{
if (path.isEmpty())
{
return "";
}
QString relGamePath;
if (!QFileInfo(path).isRelative())
{
relGamePath = GetRelativePath(path);
}
else
{
relGamePath = path;
}
QString levelpath = GetIEditor()->GetLevelFolder();
QString str = levelpath;
str.replace('/', '\\');
levelpath = CaselessPaths(str);
// Create relative path
QString relLevelPath = QDir(levelpath).relativeFilePath(relGamePath);
if (relLevelPath.isEmpty())
{
assert(0);
return path;
}
relLevelPath.remove(QRegularExpression(QStringLiteral(R"(^[\\/.]*)")));
return relLevelPath;
}
QString Make(const QString& path, const QString& file)
{
if (gEnv->pCryPak->IsAbsPath(file.toUtf8().data()))
{
return file;
}
return CaselessPaths(AddPathSlash(path) + file);
}
QString GetRelativePath(const QString& fullPath, bool bRelativeToGameFolder /*= false*/)
{
if (fullPath.isEmpty())
{
return "";
}
bool relPathfound = false;
AZStd::string relativePath;
AZStd::string fullAssetPath(fullPath.toUtf8().data());
EBUS_EVENT_RESULT(relPathfound, AzToolsFramework::AssetSystemRequestBus, GetRelativeProductPathFromFullSourceOrProductPath, fullAssetPath, relativePath);
if (relPathfound)
{
// do not normalize this path, it will already be an appropriate asset ID.
return CaselessPaths(relativePath.c_str());
}
char rootpath[_MAX_PATH] = { 0 };
azstrcpy(rootpath, _MAX_PATH, Path::GetEditingRootFolder().c_str());
if (bRelativeToGameFolder)
{
azstrcpy(rootpath, _MAX_PATH, Path::GetEditingGameDataFolder().c_str());
}
QString rootPathNormalized(rootpath);
QString srcPathNormalized(fullPath);
#if defined(AZ_PLATFORM_WINDOWS)
// avoid confusing PathRelativePathTo
rootPathNormalized.replace('/', '\\');
srcPathNormalized.replace('/', '\\');
#endif
// Create relative path
char resolvedSrcPath[AZ_MAX_PATH_LEN] = { 0 };
AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(srcPathNormalized.toUtf8().data(), resolvedSrcPath, AZ_MAX_PATH_LEN);
QByteArray path = QDir(rootPathNormalized).relativeFilePath(resolvedSrcPath).toUtf8();
if (path.isEmpty())
{
return fullPath;
}
// The following code is required because the windows PathRelativePathTo function will always return "./SomePath" instead of just "SomePath"
// Only remove single dot (.) and slash parts of a path, never the double dot (..)
const char* pBuffer = path.data();
bool bHasDot = false;
while (*pBuffer && pBuffer != path.end())
{
switch (*pBuffer)
{
case '.':
if (bHasDot)
{
// Found a double dot, rewind and stop removing
pBuffer--;
break;
}
// Fall through intended
case '/':
case '\\':
bHasDot = (*pBuffer == '.');
pBuffer++;
continue;
}
break;
}
QString relPath = pBuffer;
return CaselessPaths(relPath);
}
QString GamePathToFullPath(const QString& path)
{
using namespace AzToolsFramework;
AZ_Warning("GamePathToFullPath", path.size() <= AZ_MAX_PATH_LEN, "Path exceeds maximum path length of %d", AZ_MAX_PATH_LEN);
if ((gEnv) && (gEnv->pFileIO) && gEnv->pCryPak && path.size() <= AZ_MAX_PATH_LEN)
{
// first, adjust the file name for mods:
bool fullPathfound = false;
AZStd::string assetFullPath;
AZStd::string adjustedFilePath = path.toUtf8().data();
AssetSystemRequestBus::BroadcastResult(fullPathfound, &AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, adjustedFilePath, assetFullPath);
if (fullPathfound)
{
//if the bus message succeeds than normalize and lowercase the path
AzFramework::StringFunc::Path::Normalize(assetFullPath);
return assetFullPath.c_str();
}
// if the bus message didn't succeed, 'guess' the source assets:
else
{
// Not all systems have been converted to use local paths. Some editor files save XML files directly, and a full or correctly aliased path is already passed in.
// If the path passed in exists already, then return the resolved filepath
if (AZ::IO::FileIOBase::GetDirectInstance()->Exists(adjustedFilePath.c_str()))
{
char resolvedPath[AZ_MAX_PATH_LEN + PathUtil::maxAliasLength] = { 0 };
AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(adjustedFilePath.c_str(), resolvedPath, AZ_MAX_PATH_LEN + PathUtil::maxAliasLength);
return QString::fromUtf8(resolvedPath);
}
// if we get here it means that the Asset Processor does not know about this file. most of the time we should never get here
// the rest of this code just does a bunch of heuristic guesses in case of missing files or if the user has hand-edited
// the asset cache by moving files in via some other means or external process.
if (adjustedFilePath[0] != '@')
{
const char* prefix = (adjustedFilePath[0] == '/' || adjustedFilePath[0] == '\\') ? "@devassets@" : "@devassets@/";
adjustedFilePath = prefix + adjustedFilePath;
}
char szAdjustedFile[AZ_MAX_PATH_LEN + PathUtil::maxAliasLength] = { 0 };
gEnv->pFileIO->ResolvePath(adjustedFilePath.c_str(), szAdjustedFile, AZ_ARRAY_SIZE(szAdjustedFile));
if ((azstrnicmp(szAdjustedFile, "@devassets@", 11) == 0) && ((szAdjustedFile[11] == '/') || (szAdjustedFile[11] == '\\')))
{
if (!gEnv->pCryPak->IsFileExist(szAdjustedFile))
{
AZStd::string newName(szAdjustedFile);
AzFramework::StringFunc::Replace(newName, "@devassets@", "@devroot@/engine", false);
if (gEnv->pCryPak->IsFileExist(newName.c_str()))
{
azstrcpy(szAdjustedFile, AZ_ARRAY_SIZE(szAdjustedFile), newName.c_str());
}
else
{
// getting tricky here, try @devroot@ alone, in case its 'editor'
AzFramework::StringFunc::Replace(newName, "@devassets@", "@devroot@", false);
if (gEnv->pCryPak->IsFileExist(szAdjustedFile))
{
azstrcpy(szAdjustedFile, AZ_ARRAY_SIZE(szAdjustedFile), newName.c_str());
}
// give up, best guess is just @devassets@
}
}
}
// we should very rarely actually get to this point in the code.
// szAdjustedFile may contain an alias at this point. (@assets@/blah.whatever)
// there is a case in which the loose asset exists only within a pak file for some reason
// this is not recommended but it is possible.in that case, we want to return the original szAdjustedFile
// without touching it or resolving it so that crypak can open it successfully.
char adjustedPath[AZ_MAX_PATH_LEN + PathUtil::maxAliasLength] = { 0 };
if (gEnv->pFileIO->ResolvePath(szAdjustedFile, adjustedPath, AZ_MAX_PATH_LEN + PathUtil::maxAliasLength)) // resolve to full path
{
if ((gEnv->pCryPak->IsFileExist(adjustedPath)) || (!gEnv->pCryPak->IsFileExist(szAdjustedFile)))
{
// note that if we get here, then EITHER
// the file exists as a loose asset in the actual adjusted path
// OR the file does not exist in the original passed-in aliased name (like '@assets@/whatever')
// in which case we may as well just resolve the path to a full path and return it.
assetFullPath = adjustedPath;
AzFramework::StringFunc::Path::Normalize(assetFullPath);
azstrcpy(szAdjustedFile, AZ_MAX_PATH_LEN + PathUtil::maxAliasLength, assetFullPath.c_str());
}
// if the above case succeeded then it means that the file does NOT exist loose
// but DOES exist in a pak, in which case we leave szAdjustedFile with the alias on the front of it, meaning
// fopens via crypak will actually succeed.
}
return szAdjustedFile;
}
}
else
{
return "";
}
}
QString ToUnixPath(const QString& strPath, bool bCallCaselessPath)
{
QString str = strPath;
str.replace('\\', '/');
return bCallCaselessPath ? CaselessPaths(str) : str;
}
QString RemoveBackslash(QString path)
{
if (path.isEmpty())
{
return path;
}
int iLenMinus1 = path.length() - 1;
QChar cLastChar = path[iLenMinus1];
if (cLastChar == '\\' || cLastChar == '/')
{
return CaselessPaths(path.mid(0, iLenMinus1));
}
return CaselessPaths(path);
}
QString SubDirectoryCaseInsensitive(const QString& path, const QStringList& parts)
{
if (parts.isEmpty())
{
return path;
}
QStringList modifiedParts = parts;
auto currentPart = modifiedParts.takeFirst();
// case insensitive iterator
QDirIterator it(path);
while (it.hasNext())
{
it.next();
// the current part already exists, use it, case doesn't matter
auto actualName = it.fileName();
if (QString::compare(actualName, currentPart, Qt::CaseInsensitive) == 0)
{
return SubDirectoryCaseInsensitive(QDir(path).absoluteFilePath(actualName), modifiedParts);
}
}
// the current path doesn't exist yet, so just create the complete path in one rush
return QDir(path).absoluteFilePath(parts.join('/'));
}
}