/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ // Original file Copyright Crytek GMBH or its affiliates, used under license. #include "StdAfx.h" #include "PathUtil.h" #include // for AZ_MAX_PATH_LEN #include // for ebus events #include #include 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); const char* pchAuxPosition(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; } ////////////////////////////////////////////////////////////////////////// void GetParentDirectoryString(QString& strInputParentDirectory) { size_t nLastFoundSlash(string::npos); size_t nFirstFoundNotSlash(string::npos); string strTempInputParentDirectory(strInputParentDirectory.toUtf8().data()); nFirstFoundNotSlash = strTempInputParentDirectory.find_last_not_of("\\/", nLastFoundSlash); // If we can't find a non-slash caracter, this is likely to be a mal formed path... // ...so we won't be able to determine a parent directory, if any. if (nFirstFoundNotSlash == string::npos) { return; } nLastFoundSlash = strTempInputParentDirectory.find_last_of("\\/", nFirstFoundNotSlash); // If we couldn't find any slash, this might be the root folder...and the root folder // has no parent at all. if (nLastFoundSlash == string::npos) { return; } strTempInputParentDirectory.erase(nLastFoundSlash, string::npos); strInputParentDirectory = strTempInputParentDirectory; return; } ////////////////////////////////////////////////////////////////////////// QString GetExecutableFullPath() { return QDir::toNativeSeparators(QCoreApplication::applicationFilePath()); } ////////////////////////////////////////////////////////////////////////// QString GetWindowsTempDirectory() { return QDir::tempPath(); } ////////////////////////////////////////////////////////////////////////// QString GetExecutableParentDirectory() { QString strExecutablePath; QString strDriveLetter; QString strDirectory; QString strFilename; QString strExtension; QString strReturnValue; strExecutablePath = GetExecutableFullPath(); SplitPath(strExecutablePath, strDriveLetter, strDirectory, strFilename, strExtension); strReturnValue = strDriveLetter; strReturnValue += strDirectory; static const char EditorBundleName[] = "/Editor/Contents/MacOS/"; if (strReturnValue.endsWith(EditorBundleName)) { // We are inside a bundle but the rest of the code is not setup to // handle that.So go up a few directories to get out of the App Bundle heirarchy... strReturnValue.chop(strlen(EditorBundleName)); } GetParentDirectoryString(strReturnValue); return strReturnValue; } ////////////////////////////////////////////////////////////////////////// QString GetExecutableParentDirectoryUnicode() { return QDir::currentPath(); } ////////////////////////////////////////////////////////////////////////// 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; } //! Set the current mod NAME for editing purposes. After doing this the above functions will take this into account //! name only, please! void SetModName(const char* input) { if ( (!input) || ((gEnv) && (gEnv->pSystem) && (!gEnv->pSystem->IsMODValid(input))) // we can only validate ) { AZ_Warning("PathUtil", false, "Invalid mod name supplied to SetModName: %s - ignored.", input ? input : "(NULL)"); return; } g_currentModName = input; } //! 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->pCryPak->AdjustFileName(adjustedFilePath.c_str(), szAdjustedFile, AZ_ARRAY_SIZE(szAdjustedFile), ICryPak::FLAGS_NO_LOWCASE); if ((strnicmp(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('/')); } }