// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= UObjectHash.cpp: Unreal object name hashes =============================================================================*/ #include "Misc/PackageName.h" #include "Containers/StringView.h" #include "GenericPlatform/GenericPlatformFile.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Stats/Stats.h" #include "Misc/CoreDelegates.h" #include "Misc/App.h" #include "Modules/ModuleManager.h" #include "UObject/Package.h" #include "UObject/PackageFileSummary.h" #include "UObject/Linker.h" #include "Interfaces/IPluginManager.h" #include "Internationalization/PackageLocalizationManager.h" #include "HAL/CriticalSection.h" #include "HAL/ThreadHeartBeat.h" #include "Misc/AutomationTest.h" #include "Misc/PathViews.h" #include "Misc/ScopeRWLock.h" #include "Misc/StringBuilder.h" #include "IO/IoDispatcher.h" #include "ProfilingDebugging/LoadTimeTracker.h" DEFINE_LOG_CATEGORY_STATIC(LogPackageName, Log, All); FString FPackageName::AssetPackageExtension = TEXT(".uasset"); FString FPackageName::MapPackageExtension = TEXT(".umap"); FString FPackageName::TextAssetPackageExtension = TEXT(".utxt"); FString FPackageName::TextMapPackageExtension = TEXT(".utxtmap"); static FRWLock ContentMountPointCriticalSection; /** Event that is triggered when a new content path is mounted */ FPackageName::FOnContentPathMountedEvent FPackageName::OnContentPathMountedEvent; /** Event that is triggered when a content path is dismounted */ FPackageName::FOnContentPathDismountedEvent FPackageName::OnContentPathDismountedEvent; /** Delegate used to check whether a package exist without using the filesystem. */ FPackageName::FDoesPackageExistOverride FPackageName::DoesPackageExistOverrideDelegate; namespace PackageNameConstants { // Minimum theoretical package name length ("/A/B") is 4 const int32 MinPackageNameLength = 4; } // namespace PackageNameConstants bool FPackageName::IsShortPackageName(FStringView PossiblyLongName) { // Long names usually have / as first character so check from the front for (TCHAR Char: PossiblyLongName) { if (Char == '/') { return false; } } return true; } bool FPackageName::IsShortPackageName(const FString& PossiblyLongName) { return IsShortPackageName(FStringView(PossiblyLongName)); } bool FPackageName::IsShortPackageName(FName PossiblyLongName) { // Only get "plain" part of the name. The number suffix, e.g. "_123", can't contain slashes. TCHAR Buffer[NAME_SIZE]; uint32 Len = PossiblyLongName.GetPlainNameString(Buffer); return IsShortPackageName(FStringView(Buffer, static_cast(Len))); } FString FPackageName::GetShortName(const FString& LongName) { // Get everything after the last slash int32 IndexOfLastSlash = INDEX_NONE; LongName.FindLastChar('/', IndexOfLastSlash); return LongName.Mid(IndexOfLastSlash + 1); } FString FPackageName::GetShortName(const UPackage* Package) { check(Package != NULL); return GetShortName(Package->GetName()); } FString FPackageName::GetShortName(const FName& LongName) { return GetShortName(LongName.ToString()); } FString FPackageName::GetShortName(const TCHAR* LongName) { return GetShortName(FString(LongName)); } FName FPackageName::GetShortFName(const FString& LongName) { return GetShortFName(*LongName); } FName FPackageName::GetShortFName(const FName& LongName) { TCHAR LongNameStr[FName::StringBufferSize]; LongName.ToString(LongNameStr); if (const TCHAR* Slash = FCString::Strrchr(LongNameStr, '/')) { return FName(Slash + 1); } return LongName; } FName FPackageName::GetShortFName(const TCHAR* LongName) { if (LongName == nullptr) { return FName(); } if (const TCHAR* Slash = FCString::Strrchr(LongName, '/')) { return FName(Slash + 1); } return FName(LongName); } bool FPackageName::TryConvertGameRelativePackagePathToLocalPath(FStringView RelativePackagePath, FString& OutLocalPath) { if (RelativePackagePath.StartsWith(TEXT("/"), ESearchCase::CaseSensitive)) { // If this starts with /, this includes a root like /engine FString AbsolutePackagePath(RelativePackagePath); if (FPackageName::TryConvertLongPackageNameToFilename(AbsolutePackagePath, OutLocalPath)) { return true; } // Workaround a problem with TryConvertLongPackageNameToFilename: If the PackagePath is a content root itself (/Some/Content/Root) // and is missing a terminating /, it will not match the existing content root which does have the / (/Some/Content/Root/) if (!AbsolutePackagePath.EndsWith(TEXT("/"))) { AbsolutePackagePath = AbsolutePackagePath + TEXT("/"); return FPackageName::TryConvertLongPackageNameToFilename(AbsolutePackagePath, OutLocalPath); } return false; } else { // This is relative to /game const FString AbsoluteGameContentDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()); OutLocalPath = AbsoluteGameContentDir / FString(RelativePackagePath); return true; } } struct FPathPair { // The virtual path (e.g., "/Engine/") const FString RootPath; // The physical relative path (e.g., "../../../Engine/Content/") const FString ContentPath; bool operator==(const FPathPair& Other) const { return RootPath == Other.RootPath && ContentPath == Other.ContentPath; } // Construct a path pair FPathPair(const FString& InRootPath, const FString& InContentPath) : RootPath(InRootPath), ContentPath(InContentPath) { } }; struct FLongPackagePathsSingleton { FString ConfigRootPath; FString EngineRootPath; FString GameRootPath; FString ScriptRootPath; FString ExtraRootPath; FString MemoryRootPath; FString TempRootPath; TArray MountPointRootPaths; FString EngineContentPath; FString ContentPathShort; FString EngineShadersPath; FString EngineShadersPathShort; FString GameContentPath; FString GameConfigPath; FString GameScriptPath; FString GameExtraPath; FString GameSavedPath; FString GameContentPathRebased; FString GameConfigPathRebased; FString GameScriptPathRebased; FString GameExtraPathRebased; FString GameSavedPathRebased; //@TODO: Can probably consolidate these into a single array, if it weren't for EngineContentPathShort TArray ContentRootToPath; TArray ContentPathToRoot; // singleton static FLongPackagePathsSingleton& Get() { static FLongPackagePathsSingleton Singleton; return Singleton; } void GetValidLongPackageRoots(TArray& OutRoots, bool bIncludeReadOnlyRoots) const { OutRoots.Add(EngineRootPath); OutRoots.Add(GameRootPath); { FReadScopeLock ScopeLock(ContentMountPointCriticalSection); OutRoots += MountPointRootPaths; } if (bIncludeReadOnlyRoots) { OutRoots.Add(ConfigRootPath); OutRoots.Add(ScriptRootPath); OutRoots.Add(ExtraRootPath); OutRoots.Add(MemoryRootPath); OutRoots.Add(TempRootPath); } } // Given a content path ensure it is consistent, specifically with FileManager relative paths static FString ProcessContentMountPoint(const FString& ContentPath) { FString MountPath = ContentPath; // If a relative path is passed, convert to an absolute path if (FPaths::IsRelative(MountPath)) { MountPath = FPaths::ConvertRelativePathToFull(ContentPath); // Revert to original path if unable to convert to full path if (MountPath.Len() <= 1) { MountPath = ContentPath; UE_LOG(LogPackageName, Warning, TEXT("Unable to convert mount point relative path: %s"), *ContentPath); } } // Convert to a relative path using the FileManager return IFileManager::Get().ConvertToRelativePath(*MountPath); } // This will insert a mount point at the head of the search chain (so it can overlap an existing mount point and win) void InsertMountPoint(const FString& RootPath, const FString& ContentPath) { // Make sure the content path is stored as a relative path, consistent with the other paths we have FString RelativeContentPath = ProcessContentMountPoint(ContentPath); // Make sure the path ends in a trailing path separator. We are expecting that in the InternalFilenameToLongPackageName code. if (!RelativeContentPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive)) { RelativeContentPath += TEXT("/"); } FPathPair Pair(RootPath, RelativeContentPath); { FWriteScopeLock ScopeLock(ContentMountPointCriticalSection); ContentRootToPath.Insert(Pair, 0); ContentPathToRoot.Insert(Pair, 0); MountPointRootPaths.Add(RootPath); } // Let subscribers know that a new content path was mounted FPackageName::OnContentPathMounted().Broadcast(RootPath, RelativeContentPath); } // This will remove a previously inserted mount point void RemoveMountPoint(const FString& RootPath, const FString& ContentPath) { // Make sure the content path is stored as a relative path, consistent with the other paths we have FString RelativeContentPath = ProcessContentMountPoint(ContentPath); // Make sure the path ends in a trailing path separator. We are expecting that in the InternalFilenameToLongPackageName code. if (!RelativeContentPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive)) { RelativeContentPath += TEXT("/"); } bool bFirePathDismountedDelegate = false; { FWriteScopeLock ScopeLock(ContentMountPointCriticalSection); if (MountPointRootPaths.Remove(RootPath) > 0) { FPathPair Pair(RootPath, RelativeContentPath); ContentRootToPath.Remove(Pair); ContentPathToRoot.Remove(Pair); MountPointRootPaths.Remove(RootPath); // Let subscribers know that a new content path was unmounted bFirePathDismountedDelegate = true; } } if (bFirePathDismountedDelegate) { FPackageName::OnContentPathDismounted().Broadcast(RootPath, RelativeContentPath); } } // Checks whether the specific root path is a valid mount point. bool MountPointExists(const FString& RootPath) { FReadScopeLock ScopeLock(ContentMountPointCriticalSection); return MountPointRootPaths.Contains(RootPath); } private: FLongPackagePathsSingleton() { ConfigRootPath = TEXT("/Config/"); EngineRootPath = TEXT("/Engine/"); GameRootPath = TEXT("/Game/"); ScriptRootPath = TEXT("/Script/"); ExtraRootPath = TEXT("/Extra/"); MemoryRootPath = TEXT("/Memory/"); TempRootPath = TEXT("/Temp/"); EngineContentPath = FPaths::EngineContentDir(); ContentPathShort = TEXT("../../Content/"); EngineShadersPath = FPaths::EngineDir() / TEXT("Shaders/"); EngineShadersPathShort = TEXT("../../Shaders/"); GameContentPath = FPaths::ProjectContentDir(); GameConfigPath = FPaths::ProjectConfigDir(); GameScriptPath = FPaths::ProjectDir() / TEXT("Script/"); GameExtraPath = FPaths::ProjectDir() / TEXT("Extra/"); GameSavedPath = FPaths::ProjectSavedDir(); FString RebasedGameDir = FString::Printf(TEXT("../../../%s/"), FApp::GetProjectName()); GameContentPathRebased = RebasedGameDir / TEXT("Content/"); GameConfigPathRebased = RebasedGameDir / TEXT("Config/"); GameScriptPathRebased = RebasedGameDir / TEXT("Script/"); GameExtraPathRebased = RebasedGameDir / TEXT("Extra/"); GameSavedPathRebased = RebasedGameDir / TEXT("Saved/"); FWriteScopeLock ScopeLock(ContentMountPointCriticalSection); ContentPathToRoot.Empty(13); ContentPathToRoot.Emplace(EngineRootPath, EngineContentPath); if (FPaths::IsSamePath(GameContentPath, ContentPathShort)) { ContentPathToRoot.Emplace(GameRootPath, ContentPathShort); } else { ContentPathToRoot.Emplace(EngineRootPath, ContentPathShort); } ContentPathToRoot.Emplace(EngineRootPath, EngineShadersPath); ContentPathToRoot.Emplace(EngineRootPath, EngineShadersPathShort); ContentPathToRoot.Emplace(GameRootPath, GameContentPath); ContentPathToRoot.Emplace(ScriptRootPath, GameScriptPath); ContentPathToRoot.Emplace(TempRootPath, GameSavedPath); ContentPathToRoot.Emplace(GameRootPath, GameContentPathRebased); ContentPathToRoot.Emplace(ScriptRootPath, GameScriptPathRebased); ContentPathToRoot.Emplace(TempRootPath, GameSavedPathRebased); ContentPathToRoot.Emplace(ConfigRootPath, GameConfigPath); ContentPathToRoot.Emplace(ExtraRootPath, GameExtraPath); ContentPathToRoot.Emplace(ExtraRootPath, GameExtraPathRebased); ContentRootToPath.Empty(11); ContentRootToPath.Emplace(EngineRootPath, EngineContentPath); ContentRootToPath.Emplace(EngineRootPath, EngineShadersPath); ContentRootToPath.Emplace(GameRootPath, GameContentPath); ContentRootToPath.Emplace(ScriptRootPath, GameScriptPath); ContentRootToPath.Emplace(TempRootPath, GameSavedPath); ContentRootToPath.Emplace(GameRootPath, GameContentPathRebased); ContentRootToPath.Emplace(ScriptRootPath, GameScriptPathRebased); ContentRootToPath.Emplace(ExtraRootPath, GameExtraPath); ContentRootToPath.Emplace(ExtraRootPath, GameExtraPathRebased); ContentRootToPath.Emplace(TempRootPath, GameSavedPathRebased); ContentRootToPath.Emplace(ConfigRootPath, GameConfigPathRebased); // Allow the plugin manager to mount new content paths by exposing access through a delegate. PluginManager is // a Core class, but content path functionality is added at the CoreUObject level. IPluginManager::Get().SetRegisterMountPointDelegate(IPluginManager::FRegisterMountPointDelegate::CreateStatic(&FPackageName::RegisterMountPoint)); IPluginManager::Get().SetUnRegisterMountPointDelegate(IPluginManager::FRegisterMountPointDelegate::CreateStatic(&FPackageName::UnRegisterMountPoint)); } }; void FPackageName::InternalFilenameToLongPackageName(FStringView InFilename, FStringBuilderBase& OutPackageName) { const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); FString Filename(InFilename); FPaths::NormalizeFilename(Filename); // Convert to relative path if it's not already a long package name bool bIsValidLongPackageName = false; { FReadScopeLock ScopeLock(ContentMountPointCriticalSection); for (const auto& Pair: Paths.ContentRootToPath) { if (Filename.StartsWith(Pair.RootPath)) { bIsValidLongPackageName = true; break; } } } if (!bIsValidLongPackageName) { Filename = IFileManager::Get().ConvertToRelativePath(*Filename); if (InFilename.Len() > 0 && InFilename[InFilename.Len() - 1] == '/') { // If InFilename ends in / but converted doesn't, add the / back bool bEndsInSlash = Filename.Len() > 0 && Filename[Filename.Len() - 1] == '/'; if (!bEndsInSlash) { Filename += TEXT("/"); } } } FStringView Result = FPathViews::GetBaseFilenameWithPath(Filename); { FReadScopeLock ScopeLock(ContentMountPointCriticalSection); for (const auto& Pair: Paths.ContentPathToRoot) { if (Result.StartsWith(Pair.ContentPath)) { OutPackageName << Pair.RootPath << Result.RightChop(Pair.ContentPath.Len()); return; } } } OutPackageName << Result; } bool FPackageName::TryConvertFilenameToLongPackageName(const FString& InFilename, FString& OutPackageName, FString* OutFailureReason) { TStringBuilder<256> LongPackageNameBuilder; InternalFilenameToLongPackageName(InFilename, LongPackageNameBuilder); FStringView LongPackageName = LongPackageNameBuilder.ToString(); // we don't support loading packages from outside of well defined places int32 CharacterIndex; const bool bContainsDot = LongPackageName.FindChar(TEXT('.'), CharacterIndex); const bool bContainsBackslash = LongPackageName.FindChar(TEXT('\\'), CharacterIndex); const bool bContainsColon = LongPackageName.FindChar(TEXT(':'), CharacterIndex); if (!(bContainsDot || bContainsBackslash || bContainsColon)) { OutPackageName = LongPackageName; return true; } // if the package name resolution failed and a relative path was provided, convert to an absolute path // as content may be mounted in a different relative path to the one given if (FPaths::IsRelative(InFilename)) { FString AbsPath = FPaths::ConvertRelativePathToFull(InFilename); if (!FPaths::IsRelative(AbsPath) && AbsPath.Len() > 1) { if (TryConvertFilenameToLongPackageName(AbsPath, OutPackageName, nullptr)) { return true; } } } if (OutFailureReason != nullptr) { FString InvalidChars; if (bContainsDot) { InvalidChars += TEXT("."); } if (bContainsBackslash) { InvalidChars += TEXT("\\"); } if (bContainsColon) { InvalidChars += TEXT(":"); } *OutFailureReason = FString::Printf(TEXT("FilenameToLongPackageName failed to convert '%s'. Attempt result was '%.*s', but the path contains illegal characters '%s'"), *InFilename, LongPackageName.Len(), LongPackageName.GetData(), *InvalidChars); } return false; } FString FPackageName::FilenameToLongPackageName(const FString& InFilename) { FString FailureReason; FString Result; if (!TryConvertFilenameToLongPackageName(InFilename, Result, &FailureReason)) { UE_LOG(LogPackageName, Fatal, TEXT("%s"), *FailureReason); } return Result; } bool FPackageName::TryConvertLongPackageNameToFilename(const FString& InLongPackageName, FString& OutFilename, const FString& InExtension) { const auto& Paths = FLongPackagePathsSingleton::Get(); FReadScopeLock ScopeLock(ContentMountPointCriticalSection); for (const auto& Pair: Paths.ContentRootToPath) { if (InLongPackageName.StartsWith(Pair.RootPath)) { OutFilename = Pair.ContentPath + InLongPackageName.Mid(Pair.RootPath.Len()) + InExtension; return true; } } // This is not a long package name or the root folder is not handled in the above cases return false; } bool FPackageName::ConvertRootPathToContentPath(const FString& RootPath, FString& OutContentPath) { const auto& Paths = FLongPackagePathsSingleton::Get(); FReadScopeLock ScopeLock(ContentMountPointCriticalSection); for (const auto& Pair: Paths.ContentRootToPath) { if (RootPath.StartsWith(Pair.RootPath)) { OutContentPath = Pair.ContentPath; return true; } } // This is not a long package name or the root folder is not handled in the above cases return false; } FString FPackageName::LongPackageNameToFilename(const FString& InLongPackageName, const FString& InExtension) { FString Result; if (!TryConvertLongPackageNameToFilename(InLongPackageName, Result, InExtension)) { UE_LOG(LogPackageName, Fatal, TEXT("LongPackageNameToFilename failed to convert '%s'. Path does not map to any roots."), *InLongPackageName); } return Result; } FString FPackageName::GetLongPackagePath(const FString& InLongPackageName) { int32 IndexOfLastSlash = INDEX_NONE; if (InLongPackageName.FindLastChar('/', IndexOfLastSlash)) { return InLongPackageName.Left(IndexOfLastSlash); } else { return InLongPackageName; } } bool FPackageName::SplitLongPackageName(const FString& InLongPackageName, FString& OutPackageRoot, FString& OutPackagePath, FString& OutPackageName, const bool bStripRootLeadingSlash) { const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); const bool bIncludeReadOnlyRoots = true; TArray ValidRoots; Paths.GetValidLongPackageRoots(ValidRoots, bIncludeReadOnlyRoots); // Check to see whether our package came from a valid root OutPackageRoot.Empty(); for (auto RootIt = ValidRoots.CreateConstIterator(); RootIt; ++RootIt) { const FString& PackageRoot = *RootIt; if (InLongPackageName.StartsWith(PackageRoot)) { OutPackageRoot = PackageRoot / ""; break; } } if (OutPackageRoot.IsEmpty() || InLongPackageName.Len() <= OutPackageRoot.Len()) { // Path is not part of a valid root, or the path given is too short to continue; splitting failed return false; } // Use the standard path functions to get the rest const FString RemainingPackageName = InLongPackageName.Mid(OutPackageRoot.Len()); OutPackagePath = FPaths::GetPath(RemainingPackageName) / ""; OutPackageName = FPaths::GetCleanFilename(RemainingPackageName); if (bStripRootLeadingSlash && OutPackageRoot.StartsWith(TEXT("/"), ESearchCase::CaseSensitive)) { OutPackageRoot.RemoveAt(0); } return true; } void FPackageName::SplitFullObjectPath(const FString& InFullObjectPath, FString& OutClassName, FString& OutPackageName, FString& OutObjectName, FString& OutSubObjectName) { FString Sanitized = InFullObjectPath.TrimStartAndEnd(); const TCHAR* Cur = *Sanitized; auto ExtractBeforeDelim = [&Cur](TCHAR Delim, FString& OutString) { const TCHAR* Start = Cur; while (*Cur != '\0' && *Cur != Delim) { ++Cur; } OutString = FString(Cur - Start, Start); if (*Cur == Delim) { ++Cur; } }; ExtractBeforeDelim(' ', OutClassName); ExtractBeforeDelim('.', OutPackageName); ExtractBeforeDelim(':', OutObjectName); ExtractBeforeDelim('\0', OutSubObjectName); } FString FPackageName::GetLongPackageAssetName(const FString& InLongPackageName) { return GetShortName(InLongPackageName); } bool FPackageName::DoesPackageNameContainInvalidCharacters(FStringView InLongPackageName, FText* OutReason /*= NULL*/) { // See if the name contains invalid characters. TStringBuilder<32> MatchedInvalidChars; for (const TCHAR* InvalidCharacters = INVALID_LONGPACKAGE_CHARACTERS; *InvalidCharacters; ++InvalidCharacters) { FStringView::SizeType OutIndex; if (InLongPackageName.FindChar(*InvalidCharacters, OutIndex)) { MatchedInvalidChars += *InvalidCharacters; } } if (MatchedInvalidChars.Len()) { if (OutReason) { FFormatNamedArguments Args; Args.Add(TEXT("IllegalNameCharacters"), FText::FromString(FString(MatchedInvalidChars))); *OutReason = FText::Format(NSLOCTEXT("Core", "PackageNameContainsInvalidCharacters", "Name may not contain the following characters: '{IllegalNameCharacters}'"), Args); } return true; } return false; } bool FPackageName::IsValidLongPackageName(const FString& InLongPackageName, bool bIncludeReadOnlyRoots /*= false*/, FText* OutReason /*= NULL*/) { // All package names must contain a leading slash, root, slash and name, at minimum theoretical length ("/A/B") is 4 if (InLongPackageName.Len() < PackageNameConstants::MinPackageNameLength) { if (OutReason) { *OutReason = FText::Format(NSLOCTEXT("Core", "LongPackageNames_PathTooShort", "Path should be no less than {0} characters long."), FText::AsNumber(PackageNameConstants::MinPackageNameLength)); } return false; } // Package names start with a leading slash. if (InLongPackageName[0] != '/') { if (OutReason) { *OutReason = NSLOCTEXT("Core", "LongPackageNames_PathWithNoStartingSlash", "Path should start with a '/'"); } return false; } // Package names do not end with a trailing slash. if (InLongPackageName[InLongPackageName.Len() - 1] == '/') { if (OutReason) { *OutReason = NSLOCTEXT("Core", "LongPackageNames_PathWithTrailingSlash", "Path may not end with a '/'"); } return false; } // Check for invalid characters if (DoesPackageNameContainInvalidCharacters(InLongPackageName, OutReason)) { return false; } // Check valid roots const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); TArray ValidRoots; bool bValidRoot = false; Paths.GetValidLongPackageRoots(ValidRoots, bIncludeReadOnlyRoots); for (int32 RootIdx = 0; RootIdx < ValidRoots.Num(); ++RootIdx) { const FString& Root = ValidRoots[RootIdx]; if (InLongPackageName.StartsWith(Root)) { bValidRoot = true; break; } } if (!bValidRoot && OutReason) { if (ValidRoots.Num() == 0) { *OutReason = NSLOCTEXT("Core", "LongPackageNames_NoValidRoots", "No valid roots exist!"); } else { FString ValidRootsString = TEXT(""); if (ValidRoots.Num() == 1) { ValidRootsString = FString::Printf(TEXT("'%s'"), *ValidRoots[0]); } else { for (int32 RootIdx = 0; RootIdx < ValidRoots.Num(); ++RootIdx) { if (RootIdx < ValidRoots.Num() - 1) { ValidRootsString += FString::Printf(TEXT("'%s', "), *ValidRoots[RootIdx]); } else { ValidRootsString += FString::Printf(TEXT("or '%s'"), *ValidRoots[RootIdx]); } } } *OutReason = FText::Format(NSLOCTEXT("Core", "LongPackageNames_InvalidRoot", "Path does not start with a valid root. Path must begin with: {0}"), FText::FromString(ValidRootsString)); } } return bValidRoot; } bool FPackageName::IsValidObjectPath(const FString& InObjectPath, FText* OutReason) { FString PackageName; FString RemainingObjectPath; // Check for package delimiter int32 ObjectDelimiterIdx; if (InObjectPath.FindChar('.', ObjectDelimiterIdx)) { if (ObjectDelimiterIdx == InObjectPath.Len() - 1) { if (OutReason) { *OutReason = NSLOCTEXT("Core", "ObjectPath_EndWithPeriod", "Object Path may not end with ."); } return false; } PackageName = InObjectPath.Mid(0, ObjectDelimiterIdx); RemainingObjectPath = InObjectPath.Mid(ObjectDelimiterIdx + 1); } else { PackageName = InObjectPath; } if (!IsValidLongPackageName(PackageName, true, OutReason)) { return false; } if (RemainingObjectPath.Len() > 0) { FText PathContext = NSLOCTEXT("Core", "ObjectPathContext", "Object Path"); if (!FName::IsValidXName(RemainingObjectPath, INVALID_OBJECTPATH_CHARACTERS, OutReason, &PathContext)) { return false; } TCHAR LastChar = RemainingObjectPath[RemainingObjectPath.Len() - 1]; if (LastChar == '.' || LastChar == ':') { if (OutReason) { *OutReason = NSLOCTEXT("Core", "ObjectPath_PathWithTrailingSeperator", "Object Path may not end with : or ."); } return false; } int32 SlashIndex; if (RemainingObjectPath.FindChar('/', SlashIndex)) { if (OutReason) { *OutReason = NSLOCTEXT("Core", "ObjectPath_SlashAfterPeriod", "Object Path may not have / after first ."); } return false; } } return true; } bool FPackageName::IsValidPath(const FString& InPath) { const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); FReadScopeLock ScopeLock(ContentMountPointCriticalSection); for (const FPathPair& Pair: Paths.ContentRootToPath) { if (InPath.StartsWith(Pair.RootPath)) { return true; } } // The root folder is not handled in the above cases return false; } void FPackageName::RegisterMountPoint(const FString& RootPath, const FString& ContentPath) { FLongPackagePathsSingleton::Get().InsertMountPoint(RootPath, ContentPath); } void FPackageName::UnRegisterMountPoint(const FString& RootPath, const FString& ContentPath) { FLongPackagePathsSingleton::Get().RemoveMountPoint(RootPath, ContentPath); } bool FPackageName::MountPointExists(const FString& RootPath) { return FLongPackagePathsSingleton::Get().MountPointExists(RootPath); } FName FPackageName::GetPackageMountPoint(const FString& InPackagePath, bool InWithoutSlashes) { FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); TArray MountPoints; Paths.GetValidLongPackageRoots(MountPoints, true); int32 WithoutSlashes = InWithoutSlashes ? 1 : 0; for (auto RootIt = MountPoints.CreateConstIterator(); RootIt; ++RootIt) { if (InPackagePath.StartsWith(*RootIt)) { return FName(*RootIt->Mid(WithoutSlashes, RootIt->Len() - (2 * WithoutSlashes))); } } return FName(); } FString FPackageName::ConvertToLongScriptPackageName(const TCHAR* InShortName) { if (IsShortPackageName(FString(InShortName))) { return FString::Printf(TEXT("/Script/%s"), InShortName); } else { return InShortName; } } // Short to long script package name map. static TMap ScriptPackageNames; // @todo: This stuff needs to be eliminated as soon as we can make sure that no legacy short package names // are in use when referencing class names in UObject module "class packages" void FPackageName::RegisterShortPackageNamesForUObjectModules() { // @todo: Ideally we'd only be processing UObject modules, not every module, but we have // no way of knowing which modules may contain UObjects (without say, having UBT save a manifest.) // @todo: This stuff is a bomb waiting to explode. Because short package names can // take precedent over other object names, modules can reserve names for other types! TArray AllModuleNames; FModuleManager::Get().FindModules(TEXT("*"), AllModuleNames); for (TArray::TConstIterator ModuleNameIt(AllModuleNames); ModuleNameIt; ++ModuleNameIt) { ScriptPackageNames.Add(*ModuleNameIt, *ConvertToLongScriptPackageName(*ModuleNameIt->ToString())); } } FName* FPackageName::FindScriptPackageName(FName InShortName) { return ScriptPackageNames.Find(InShortName); } bool FPackageName::FindPackageFileWithoutExtension(const FString& InPackageFilename, FString& OutFilename, bool InAllowTextFormats) { auto& FileManager = IFileManager::Get(); { static const FString* PackageExtensions[] = { &AssetPackageExtension, &MapPackageExtension}; // Loop through all known extensions and check if the file exists for (int32 ExtensionIndex = 0; ExtensionIndex < UE_ARRAY_COUNT(PackageExtensions); ++ExtensionIndex) { FString PackageFilename = InPackageFilename + *PackageExtensions[ExtensionIndex]; if (FileManager.FileExists(*PackageFilename)) { // The package exists so exit. From now on InPackageFilename can be equal to OutFilename so // don't attempt to use it anymore (case where &InPackageFilename == &OutFilename). OutFilename = MoveTemp(PackageFilename); return true; } } } #if WITH_TEXT_ARCHIVE_SUPPORT if (InAllowTextFormats) { static const FString* TextPackageExtensions[] = { &TextAssetPackageExtension, &TextMapPackageExtension}; for (int32 ExtensionIndex = 0; ExtensionIndex < UE_ARRAY_COUNT(TextPackageExtensions); ++ExtensionIndex) { FString PackageFilename = InPackageFilename + *TextPackageExtensions[ExtensionIndex]; if (FileManager.FileExists(*PackageFilename)) { // The package exists so exit. From now on InPackageFilename can be equal to OutFilename so // don't attempt to use it anymore (case where &InPackageFilename == &OutFilename). OutFilename = MoveTemp(PackageFilename); return true; } } } #endif return false; } bool FPackageName::FixPackageNameCase(FString& LongPackageName, FStringView Extension) { // Find the matching long package root const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); FReadScopeLock ScopeLock(ContentMountPointCriticalSection); for (const FPathPair& Pair: Paths.ContentRootToPath) { if (LongPackageName.StartsWith(Pair.RootPath)) { FString RelativePackageName = LongPackageName.Mid(Pair.RootPath.Len()); FString FileName = Pair.ContentPath / RelativePackageName; int ExtensionLen = Extension.Len(); if (Extension.Len() > 0 && Extension[0] != '.') { FileName.AppendChar('.'); ExtensionLen++; } FileName += Extension; FString CorrectFileName = IFileManager::Get().GetFilenameOnDisk(*FileName); if (CorrectFileName.Len() >= RelativePackageName.Len() + ExtensionLen) { FString NewRelativePackageName = CorrectFileName.Mid(CorrectFileName.Len() - RelativePackageName.Len() - ExtensionLen, RelativePackageName.Len()); if (NewRelativePackageName == RelativePackageName) { LongPackageName.RemoveAt(Pair.RootPath.Len(), LongPackageName.Len() - Pair.RootPath.Len()); LongPackageName.Append(*NewRelativePackageName); return true; } } break; } } return false; } bool FPackageName::DoesPackageExist(const FString& LongPackageName, const FGuid* Guid, FString* OutFilename, bool InAllowTextFormats) { SCOPED_LOADTIMER(FPackageName_DoesPackageExist); bool bFoundFile = false; // Make sure passing filename as LongPackageName is supported. FString PackageName; FText Reason; if (!FPackageName::TryConvertFilenameToLongPackageName(LongPackageName, PackageName)) { verify(!FPackageName::IsValidLongPackageName(LongPackageName, true, &Reason)); UE_LOG(LogPackageName, Error, TEXT("Illegal call to DoesPackageExist: '%s' is not a standard unreal filename or a long path name. Reason: %s"), *LongPackageName, *Reason.ToString()); ensureMsgf(false, TEXT("Illegal call to DoesPackageExist: '%s' is not a standard unreal filename or a long path name. Reason: %s"), *LongPackageName, *Reason.ToString()); return false; } // Once we have the real Package Name, we can exit early if it's a script package - they exist only in memory. if (IsScriptPackage(PackageName)) { return false; } if (IsMemoryPackage(PackageName)) { return false; } if (!FPackageName::IsValidLongPackageName(PackageName, true, &Reason)) { UE_LOG(LogPackageName, Error, TEXT("DoesPackageExist: DoesPackageExist FAILED: '%s' is not a standard unreal filename or a long path name. Reason: %s"), *LongPackageName, *Reason.ToString()); return false; } // Used when I/O dispatcher is enabled if (DoesPackageExistOverrideDelegate.IsBound()) { if (DoesPackageExistOverrideDelegate.Execute(FName(*PackageName))) { if (OutFilename) { *OutFilename = LongPackageNameToFilename(PackageName, TEXT("")); } return true; } // Try to find uncooked packages on disk when I/O store is enabled in editor builds #if !WITH_IOSTORE_IN_EDITOR return false; #endif } // Convert to filename (no extension yet). FString Filename = LongPackageNameToFilename(PackageName, TEXT("")); // Find the filename (with extension). bFoundFile = FindPackageFileWithoutExtension(Filename, Filename, InAllowTextFormats); // On consoles, we don't support package downloading, so no need to waste any extra cycles/disk io dealing with it if (!FPlatformProperties::RequiresCookedData() && bFoundFile && Guid != NULL) { // @todo: If we could get to list of linkers here, it would be faster to check // then to open the file and read it FArchive* PackageReader = IFileManager::Get().CreateFileReader(*Filename); // This had better open check(PackageReader != NULL); // Read in the package summary FPackageFileSummary Summary; *PackageReader << Summary; // Compare Guids PRAGMA_DISABLE_DEPRECATION_WARNINGS if (Summary.Guid != *Guid) PRAGMA_ENABLE_DEPRECATION_WARNINGS { bFoundFile = false; } // Close package file delete PackageReader; } if (OutFilename && bFoundFile) { *OutFilename = Filename; } return bFoundFile; } bool FPackageName::SearchForPackageOnDisk(const FString& PackageName, FString* OutLongPackageName, FString* OutFilename) { TRACE_CPUPROFILER_EVENT_SCOPE(FPackageName::SearchForPackageOnDisk); DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FPackageName::SearchForPackageOnDisk"), STAT_PackageName_SearchForPackageOnDisk, STATGROUP_LoadTime); // This function may take a long time to complete, so suspend heartbeat measure while we're here FSlowHeartBeatScope SlowHeartBeatScope; bool bResult = false; double StartTime = FPlatformTime::Seconds(); if (FPackageName::IsShortPackageName(PackageName) == false) { // If this is long package name, revert to using DoesPackageExist because it's a lot faster. FString Filename; if (DoesPackageExist(PackageName, NULL, &Filename)) { if (OutLongPackageName) { *OutLongPackageName = PackageName; } if (OutFilename) { *OutFilename = Filename; } bResult = true; } } else { // Attempt to find package by its short name by searching in the known content paths. TArray Paths; { TArray RootContentPaths; FPackageName::QueryRootContentPaths(RootContentPaths); for (TArray::TConstIterator RootPathIt(RootContentPaths); RootPathIt; ++RootPathIt) { const FString& RootPath = *RootPathIt; const FString& ContentFolder = FPackageName::LongPackageNameToFilename(RootPath, TEXT("")); Paths.Add(ContentFolder); } } const FString PackageWildcard = (PackageName.Find(TEXT("."), ESearchCase::CaseSensitive) != INDEX_NONE ? PackageName : PackageName + TEXT(".*")); TArray Results; for (int32 PathIndex = 0; PathIndex < Paths.Num() && !bResult; ++PathIndex) { // Search directly on disk. Very slow! IFileManager::Get().FindFilesRecursive(Results, *Paths[PathIndex], *PackageWildcard, true, false); for (int32 FileIndex = 0; FileIndex < Results.Num(); ++FileIndex) { FString Filename(Results[FileIndex]); if (IsPackageFilename(Results[FileIndex])) { // Convert to long package name. FString LongPackageName; if (TryConvertFilenameToLongPackageName(Filename, LongPackageName)) { if (OutLongPackageName) { if (bResult) { UE_LOG(LogPackageName, Warning, TEXT("Found ambiguous long package name for '%s'. Returning '%s', but could also be '%s'."), *PackageName, **OutLongPackageName, *LongPackageName); } else { *OutLongPackageName = LongPackageName; } } if (OutFilename) { FPaths::MakeStandardFilename(Filename); if (bResult) { UE_LOG(LogPackageName, Warning, TEXT("Found ambiguous file name for '%s'. Returning '%s', but could also be '%s'."), *PackageName, **OutFilename, *Filename); } else { *OutFilename = Filename; } } bResult = true; } } } } } float ThisTime = FPlatformTime::Seconds() - StartTime; if (bResult) { UE_LOG(LogPackageName, Log, TEXT("SearchForPackageOnDisk took %7.3fs to resolve %s."), ThisTime, *PackageName); } else { UE_LOG(LogPackageName, Log, TEXT("SearchForPackageOnDisk took %7.3fs, but failed to resolve %s."), ThisTime, *PackageName); } return bResult; } bool FPackageName::TryConvertShortPackagePathToLongInObjectPath(const FString& ObjectPath, FString& ConvertedObjectPath) { FString PackagePath; FString ObjectName; int32 DotPosition = ObjectPath.Find(TEXT("."), ESearchCase::CaseSensitive); if (DotPosition != INDEX_NONE) { PackagePath = ObjectPath.Mid(0, DotPosition); ObjectName = ObjectPath.Mid(DotPosition + 1); } else { PackagePath = ObjectPath; } FString LongPackagePath; if (!SearchForPackageOnDisk(PackagePath, &LongPackagePath)) { return false; } ConvertedObjectPath = FString::Printf(TEXT("%s.%s"), *LongPackagePath, *ObjectName); return true; } FString FPackageName::GetNormalizedObjectPath(const FString& ObjectPath) { if (!ObjectPath.IsEmpty() && FPackageName::IsShortPackageName(ObjectPath)) { FString LongPath; UE_LOG(LogPackageName, Warning, TEXT("Asset path \"%s\" is in short form, which is unsupported and -- even if valid -- resolving it will be really slow."), *ObjectPath); UE_LOG(LogPackageName, Warning, TEXT("Please consider resaving package in order to speed-up loading.")); if (!FPackageName::TryConvertShortPackagePathToLongInObjectPath(ObjectPath, LongPath)) { UE_LOG(LogPackageName, Warning, TEXT("Asset path \"%s\" could not be resolved."), *ObjectPath); } return LongPath; } else { return ObjectPath; } } FString FPackageName::GetDelegateResolvedPackagePath(const FString& InSourcePackagePath) { if (FCoreDelegates::PackageNameResolvers.Num() > 0) { bool WasResolved = false; // If the path is /Game/Path/Foo.Foo only worry about resolving the /Game/Path/Foo FString PathName = InSourcePackagePath; FString ObjectName; int32 DotIndex = INDEX_NONE; if (PathName.FindChar('.', DotIndex)) { ObjectName = PathName.Mid(DotIndex + 1); PathName.LeftInline(DotIndex, false); } for (auto Delegate: FCoreDelegates::PackageNameResolvers) { FString ResolvedPath; if (Delegate.Execute(PathName, ResolvedPath)) { UE_LOG(LogPackageName, Display, TEXT("Package '%s' was resolved to '%s'"), *PathName, *ResolvedPath); PathName = ResolvedPath; WasResolved = true; } } if (WasResolved) { // If package was passed in with an object, add that back on by deriving it from the package name if (ObjectName.Len()) { int32 LastSlashIndex = INDEX_NONE; if (PathName.FindLastChar('/', LastSlashIndex)) { ObjectName = PathName.Mid(LastSlashIndex + 1); } PathName += TEXT("."); PathName += ObjectName; } return PathName; } } return InSourcePackagePath; } FString FPackageName::GetSourcePackagePath(const FString& InLocalizedPackagePath) { // This function finds the start and end point of the "/L10N/" part of the path so that it can be removed auto GetL10NTrimRange = [](const FString& InPath, int32& OutL10NStart, int32& OutL10NLength) { const TCHAR* CurChar = *InPath; // Must start with a slash if (*CurChar++ != TEXT('/')) { return false; } // Find the end of the first part of the path, eg /Game/ while (*CurChar && *CurChar++ != TEXT('/')) { } if (!*CurChar) { // Found end-of-string return false; } if (FCString::Strnicmp(CurChar, TEXT("L10N/"), 5) == 0) // StartsWith "L10N/" { CurChar -= 1; // -1 because we need to eat the slash before L10N OutL10NStart = (CurChar - *InPath); OutL10NLength = 6; // "/L10N/" // Walk to the next slash as that will be the end of the culture code CurChar += OutL10NLength; while (*CurChar && *CurChar++ != TEXT('/')) { ++OutL10NLength; } return true; } else if (FCString::Stricmp(CurChar, TEXT("L10N")) == 0) // Is "L10N" { CurChar -= 1; // -1 because we need to eat the slash before L10N OutL10NStart = (CurChar - *InPath); OutL10NLength = 5; // "/L10N" return true; } return false; }; FString SourcePackagePath = InLocalizedPackagePath; int32 L10NStart = INDEX_NONE; int32 L10NLength = 0; if (GetL10NTrimRange(SourcePackagePath, L10NStart, L10NLength)) { SourcePackagePath.RemoveAt(L10NStart, L10NLength); } return SourcePackagePath; } FString FPackageName::GetLocalizedPackagePath(const FString& InSourcePackagePath) { const FName LocalizedPackageName = FPackageLocalizationManager::Get().FindLocalizedPackageName(*InSourcePackagePath); return (LocalizedPackageName.IsNone()) ? InSourcePackagePath : LocalizedPackageName.ToString(); } FString FPackageName::GetLocalizedPackagePath(const FString& InSourcePackagePath, const FString& InCultureName) { const FName LocalizedPackageName = FPackageLocalizationManager::Get().FindLocalizedPackageNameForCulture(*InSourcePackagePath, InCultureName); return (LocalizedPackageName.IsNone()) ? InSourcePackagePath : LocalizedPackageName.ToString(); } FString FPackageName::PackageFromPath(const TCHAR* InPathName) { FString PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(InPathName, PackageName)) { return PackageName; } else { // Not a valid package filename return InPathName; } } bool FPackageName::IsTextPackageExtension(const TCHAR* Ext) { return IsTextAssetPackageExtension(Ext) || IsTextMapPackageExtension(Ext); } bool FPackageName::IsTextAssetPackageExtension(const TCHAR* Ext) { if (*Ext != TEXT('.')) { return (TextAssetPackageExtension.EndsWith(Ext)); } else { return (TextAssetPackageExtension == Ext); } } bool FPackageName::IsTextMapPackageExtension(const TCHAR* Ext) { if (*Ext != TEXT('.')) { return (TextMapPackageExtension.EndsWith(Ext)); } else { return (TextMapPackageExtension == Ext); } } bool FPackageName::IsPackageExtension(const TCHAR* Ext) { return IsAssetPackageExtension(Ext) || IsMapPackageExtension(Ext); } bool FPackageName::IsAssetPackageExtension(const TCHAR* Ext) { if (*Ext != TEXT('.')) { return (AssetPackageExtension.EndsWith(Ext)); } else { return (AssetPackageExtension == Ext); } } bool FPackageName::IsMapPackageExtension(const TCHAR* Ext) { if (*Ext != TEXT('.')) { return (MapPackageExtension.EndsWith(Ext)); } else { return (MapPackageExtension == Ext); } } bool FPackageName::FindPackagesInDirectory(TArray& OutPackages, const FString& RootDir) { UE_CLOG(FIoDispatcher::IsInitialized(), LogPackageName, Error, TEXT("Can't search for packages using the filesystem when I/O dispatcher is enabled")); FString LocalPathToRootDir; if (!FPackageName::TryConvertLongPackageNameToFilename(RootDir / TEXT(""), LocalPathToRootDir)) { LocalPathToRootDir = RootDir; } LocalPathToRootDir = FPaths::ConvertRelativePathToFull(MoveTemp(LocalPathToRootDir)); // Find all files in RootDir, then filter by extension (we have two package extensions so they can't // be included in the search wildcard. TArray AllFiles; IFileManager::Get().FindFilesRecursive(AllFiles, *LocalPathToRootDir, TEXT("*.*"), true, false); // Keep track if any package has been found. Can't rely only on OutPackages.Num() > 0 as it may not be empty. const int32 PreviousPackagesCount = OutPackages.Num(); for (int32 FileIndex = 0; FileIndex < AllFiles.Num(); FileIndex++) { const FString& Filename = AllFiles[FileIndex]; if (IsPackageFilename(Filename)) { OutPackages.Add(Filename); } } return OutPackages.Num() > PreviousPackagesCount; } bool FPackageName::FindPackagesInDirectories(TArray& OutPackages, const TArrayView& RootDirs) { TSet Packages; TArray DirPackages; for (const FString& RootDir: RootDirs) { DirPackages.Reset(); FindPackagesInDirectory(DirPackages, RootDir); for (FString& DirPackage: DirPackages) { Packages.Add(MoveTemp(DirPackage)); } } OutPackages.Reserve(Packages.Num() + OutPackages.Num()); for (FString& Package: Packages) { OutPackages.Add(MoveTemp(Package)); } return Packages.Num() > 0; } void FPackageName::IteratePackagesInDirectory(const FString& RootDir, const FPackageNameVisitor& Callback) { class FPackageVisitor: public IPlatformFile::FDirectoryVisitor { public: const FPackageNameVisitor& Callback; explicit FPackageVisitor(const FPackageNameVisitor& InCallback) : Callback(InCallback) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { bool Result = true; if (!bIsDirectory && IsPackageFilename(FilenameOrDirectory)) { Result = Callback(FilenameOrDirectory); } return Result; } }; FPackageVisitor PackageVisitor(Callback); IFileManager::Get().IterateDirectoryRecursively(*RootDir, PackageVisitor); } void FPackageName::IteratePackagesInDirectory(const FString& RootDir, const FPackageNameStatVisitor& Callback) { class FPackageStatVisitor: public IPlatformFile::FDirectoryStatVisitor { public: const FPackageNameStatVisitor& Callback; explicit FPackageStatVisitor(const FPackageNameStatVisitor& InCallback) : Callback(InCallback) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, const FFileStatData& StatData) override { bool Result = true; if (!StatData.bIsDirectory && IsPackageFilename(FilenameOrDirectory)) { Result = Callback(FilenameOrDirectory, StatData); } return Result; } }; FPackageStatVisitor PackageVisitor(Callback); IFileManager::Get().IterateDirectoryStatRecursively(*RootDir, PackageVisitor); } void FPackageName::QueryRootContentPaths(TArray& OutRootContentPaths, bool bIncludeReadOnlyRoots, bool bWithoutLeadingSlashes, bool bWithoutTrailingSlashes) { const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get(); Paths.GetValidLongPackageRoots(OutRootContentPaths, bIncludeReadOnlyRoots); if (bWithoutTrailingSlashes || bWithoutLeadingSlashes) { for (FString& It: OutRootContentPaths) { if (bWithoutTrailingSlashes && It.Len() > 1 && It[It.Len() - 1] == TEXT('/')) { It.RemoveAt(It.Len() - 1, /*Count*/ 1, /*bAllowShrinking*/ false); } if (bWithoutLeadingSlashes && It.Len() > 1 && It[0] == TEXT('/')) { It.RemoveAt(0, /*Count*/ 1, /*bAllowShrinking*/ false); } } } } void FPackageName::EnsureContentPathsAreRegistered() { SCOPED_BOOT_TIMING("FPackageName::EnsureContentPathsAreRegistered"); FLongPackagePathsSingleton::Get(); } bool FPackageName::ParseExportTextPath(const FString& InExportTextPath, FString* OutClassName, FString* OutObjectPath) { if (InExportTextPath.Split(TEXT("'"), OutClassName, OutObjectPath, ESearchCase::CaseSensitive)) { if (OutObjectPath) { FString& OutObjectPathRef = *OutObjectPath; if (OutObjectPathRef.EndsWith(TEXT("'"), ESearchCase::CaseSensitive)) { OutObjectPathRef.LeftChopInline(1, false); } } return true; } return false; } template bool ParseExportTextPathImpl(const T& InExportTextPath, T* OutClassName, T* OutObjectPath) { int32 Index; if (InExportTextPath.FindChar('\'', Index)) { if (OutClassName) { *OutClassName = InExportTextPath.Left(Index); } if (OutObjectPath) { *OutObjectPath = InExportTextPath.Mid(Index + 1); OutObjectPath->RemoveSuffix(InExportTextPath.EndsWith('\'')); } return true; } return false; } bool FPackageName::ParseExportTextPath(FWideStringView InExportTextPath, FWideStringView* OutClassName, FWideStringView* OutObjectPath) { return ParseExportTextPathImpl(InExportTextPath, OutClassName, OutObjectPath); } bool FPackageName::ParseExportTextPath(FAnsiStringView InExportTextPath, FAnsiStringView* OutClassName, FAnsiStringView* OutObjectPath) { return ParseExportTextPathImpl(InExportTextPath, OutClassName, OutObjectPath); } bool FPackageName::ParseExportTextPath(const TCHAR* InExportTextPath, FStringView* OutClassName, FStringView* OutObjectPath) { return ParseExportTextPath(FStringView(InExportTextPath), OutClassName, OutObjectPath); } template T ExportTextPathToObjectPathImpl(const T& InExportTextPath) { T ObjectPath; if (FPackageName::ParseExportTextPath(InExportTextPath, nullptr, &ObjectPath)) { return ObjectPath; } // Could not parse the export text path. Could already be an object path, just return it back. return InExportTextPath; } FWideStringView FPackageName::ExportTextPathToObjectPath(FWideStringView InExportTextPath) { return ExportTextPathToObjectPathImpl(InExportTextPath); } FAnsiStringView FPackageName::ExportTextPathToObjectPath(FAnsiStringView InExportTextPath) { return ExportTextPathToObjectPathImpl(InExportTextPath); } FString FPackageName::ExportTextPathToObjectPath(const FString& InExportTextPath) { return ExportTextPathToObjectPathImpl(InExportTextPath); } FString FPackageName::ExportTextPathToObjectPath(const TCHAR* InExportTextPath) { return ExportTextPathToObjectPath(FString(InExportTextPath)); } template T ObjectPathToPackageNameImpl(const T& InObjectPath) { // Check for package delimiter int32 ObjectDelimiterIdx; if (InObjectPath.FindChar('.', ObjectDelimiterIdx)) { return InObjectPath.Mid(0, ObjectDelimiterIdx); } // No object delimiter. The path must refer to the package name directly. return InObjectPath; } FWideStringView FPackageName::ObjectPathToPackageName(FWideStringView InObjectPath) { return ObjectPathToPackageNameImpl(InObjectPath); } FAnsiStringView FPackageName::ObjectPathToPackageName(FAnsiStringView InObjectPath) { return ObjectPathToPackageNameImpl(InObjectPath); } FString FPackageName::ObjectPathToPackageName(const FString& InObjectPath) { return ObjectPathToPackageNameImpl(InObjectPath); } template T ObjectPathToObjectNameImpl(const T& InObjectPath) { // Check for a subobject int32 SubObjectDelimiterIdx; if (InObjectPath.FindChar(':', SubObjectDelimiterIdx)) { return InObjectPath.Mid(SubObjectDelimiterIdx + 1); } // Check for a top level object int32 ObjectDelimiterIdx; if (InObjectPath.FindChar('.', ObjectDelimiterIdx)) { return InObjectPath.Mid(ObjectDelimiterIdx + 1); } // No object or subobject delimiters. The path must refer to the object name directly (i.e. a package). return InObjectPath; } FString FPackageName::ObjectPathToObjectName(const FString& InObjectPath) { return ObjectPathToObjectNameImpl(InObjectPath); } FWideStringView FPackageName::ObjectPathToObjectName(FWideStringView InObjectPath) { return ObjectPathToObjectNameImpl(InObjectPath); } bool FPackageName::IsExtraPackage(FStringView InPackageName) { return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().ExtraRootPath); } bool FPackageName::IsScriptPackage(FStringView InPackageName) { return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().ScriptRootPath); } bool FPackageName::IsMemoryPackage(FStringView InPackageName) { return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().MemoryRootPath); } bool FPackageName::IsTempPackage(FStringView InPackageName) { return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().TempRootPath); } bool FPackageName::IsLocalizedPackage(FStringView InPackageName) { // Minimum valid package name length is "/A/L10N" if (InPackageName.Len() < 7) { return false; } const TCHAR* CurChar = InPackageName.GetData(); const TCHAR* EndChar = InPackageName.GetData() + InPackageName.Len(); // Must start with a slash if (CurChar == EndChar || *CurChar++ != TEXT('/')) { return false; } // Find the end of the first part of the path, eg /Game/ while (CurChar != EndChar && *CurChar++ != TEXT('/')) { } if (CurChar == EndChar) { // Found end-of-string return false; } // Are we part of the L10N folder? FStringView Remaining(CurChar, EndChar - CurChar); // Is "L10N" or StartsWith "L10N/" return Remaining.StartsWith(TEXT("L10N"_SV), ESearchCase::IgnoreCase) && (Remaining.Len() == 4 || Remaining[4] == '/'); } #if WITH_DEV_AUTOMATION_TESTS IMPLEMENT_SIMPLE_AUTOMATION_TEST(FPackageNameTests, "System.Core.Misc.PackageNames", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::SmokeFilter) bool FPackageNameTests::RunTest(const FString& Parameters) { // Localized paths tests { auto TestIsLocalizedPackage = [&](const FString& InPath, const bool InExpected) { const bool bResult = FPackageName::IsLocalizedPackage(InPath); if (bResult != InExpected) { AddError(FString::Printf(TEXT("Path '%s' failed FPackageName::IsLocalizedPackage (got '%d', expected '%d')."), *InPath, bResult, InExpected)); } }; TestIsLocalizedPackage(TEXT("/Game"), false); TestIsLocalizedPackage(TEXT("/Game/MyAsset"), false); TestIsLocalizedPackage(TEXT("/Game/L10N"), true); TestIsLocalizedPackage(TEXT("/Game/L10N/en"), true); TestIsLocalizedPackage(TEXT("/Game/L10N/en/MyAsset"), true); } // Source path tests { auto TestGetSourcePackagePath = [this](const FString& InPath, const FString& InExpected) { const FString Result = FPackageName::GetSourcePackagePath(InPath); if (Result != InExpected) { AddError(FString::Printf(TEXT("Path '%s' failed FPackageName::GetSourcePackagePath (got '%s', expected '%s')."), *InPath, *Result, *InExpected)); } }; TestGetSourcePackagePath(TEXT("/Game"), TEXT("/Game")); TestGetSourcePackagePath(TEXT("/Game/MyAsset"), TEXT("/Game/MyAsset")); TestGetSourcePackagePath(TEXT("/Game/L10N"), TEXT("/Game")); TestGetSourcePackagePath(TEXT("/Game/L10N/en"), TEXT("/Game")); TestGetSourcePackagePath(TEXT("/Game/L10N/en/MyAsset"), TEXT("/Game/MyAsset")); } return true; } #endif // WITH_DEV_AUTOMATION_TESTS