// Copyright Epic Games, Inc. All Rights Reserved. #include "PakFileUtilities.h" #include "IPlatformFilePak.h" #include "Misc/SecureHash.h" #include "Math/BigInt.h" #include "SignedArchiveWriter.h" #include "Misc/AES.h" #include "Templates/UniquePtr.h" #include "Serialization/LargeMemoryWriter.h" #include "ProfilingDebugging/DiagnosticTable.h" #include "Serialization/JsonSerializer.h" #include "Misc/Base64.h" #include "Misc/Compression.h" #include "Misc/Fnv.h" #include "Features/IModularFeatures.h" #include "Misc/CoreDelegates.h" #include "Misc/FileHelper.h" #include "Misc/ConfigCacheIni.h" #include "Misc/ConfigContext.h" #include "HAL/PlatformFileManager.h" #include "Async/ParallelFor.h" #include "Async/AsyncWork.h" #include "Modules/ModuleManager.h" #include "DerivedDataCacheInterface.h" #include "Serialization/MemoryReader.h" #include "Serialization/MemoryWriter.h" #include "Serialization/FileRegions.h" #include "Misc/ICompressionFormat.h" #include "Misc/KeyChainUtilities.h" #include "IoStoreUtilities.h" #include "Interfaces/IPluginManager.h" #include "Containers/SpscQueue.h" #include "Async/Async.h" #include "Async/Future.h" #include "Virtualization/VirtualizationSystem.h" #include "CookedPackageStore.h" #include "ZenStoreHttpClient.h" IMPLEMENT_MODULE(FDefaultModuleImpl, PakFileUtilities); #define GUARANTEE_UASSET_AND_UEXP_IN_SAME_PAK 0 #define USE_DDC_FOR_COMPRESSED_FILES 0 #define PAKCOMPRESS_DERIVEDDATA_VER TEXT("9493D2AB515048658AF7BE1342EC21FC") DEFINE_LOG_CATEGORY_STATIC(LogMakeBinaryConfig, Log, All); #define SEEK_OPT_VERBOSITY Display #define DETAILED_UNREALPAK_TIMING 0 #if DETAILED_UNREALPAK_TIMING struct FUnrealPakScopeCycleCounter { volatile int64& Counter; uint32 StartTime; FUnrealPakScopeCycleCounter(volatile int64& InCounter) : Counter(InCounter) { StartTime = FPlatformTime::Cycles(); } ~FUnrealPakScopeCycleCounter() { uint32 EndTime = FPlatformTime::Cycles(); volatile int64 DeltaTime = EndTime; if (EndTime > StartTime) { DeltaTime = EndTime - StartTime; } FPlatformAtomics::InterlockedAdd(&Counter, DeltaTime); } }; volatile int64 GCompressionTime = 0; volatile int64 GDDCSyncReadTime = 0; volatile int64 GDDCSyncWriteTime = 0; int64 GDDCHits = 0; int64 GDDCMisses = 0; #endif bool ListFilesInPak(const TCHAR * InPakFilename, int64 SizeFilter, bool bIncludeDeleted, const FString& CSVFilename, bool bExtractToMountPoint, const FKeyChain& InKeyChain, bool bAppendFile, bool bIncludeFooter); bool SignPakFile(const FString& InPakFilename, const FRSAKeyHandle InSigningKey) { FString SignatureFilename(FPaths::ChangeExtension(InPakFilename, TEXT(".sig"))); TUniquePtr PakFile(IFileManager::Get().CreateFileReader(*InPakFilename)); const int64 TotalSize = PakFile->TotalSize(); bool bFoundMagic = false; FPakInfo PakInfo; const int64 FileInfoSize = PakInfo.GetSerializedSize(FPakInfo::PakFile_Version_Latest); if (TotalSize >= FileInfoSize) { const int64 FileInfoPos = TotalSize - FileInfoSize; PakFile->Seek(FileInfoPos); PakInfo.Serialize(*PakFile.Get(), FPakInfo::PakFile_Version_Latest); bFoundMagic = (PakInfo.Magic == FPakInfo::PakFile_Magic); } // TODO: This should probably mimic the logic of FPakFile::Initialize which iterates back through pak versions until it find a compatible one if (!bFoundMagic) { return false; } TArray ComputedSignatureData; ComputedSignatureData.Append(PakInfo.IndexHash.Hash, UE_ARRAY_COUNT(FSHAHash::Hash)); const uint64 BlockSize = FPakInfo::MaxChunkDataSize; uint64 Remaining = PakFile->TotalSize(); PakFile->Seek(0); TArray Buffer; Buffer.SetNum(BlockSize); TArray Hashes; Hashes.Empty(IntCastChecked(PakFile->TotalSize() / BlockSize)); while (Remaining > 0) { const uint64 CurrentBlockSize = FMath::Min(BlockSize, Remaining); Remaining -= CurrentBlockSize; PakFile->Serialize(Buffer.GetData(), CurrentBlockSize); Hashes.Add(ComputePakChunkHash(Buffer.GetData(), CurrentBlockSize)); } FPakSignatureFile Signatures; Signatures.SetChunkHashesAndSign(Hashes, ComputedSignatureData, InSigningKey); TUniquePtr SignatureFile(IFileManager::Get().CreateFileWriter(*SignatureFilename)); Signatures.Serialize(*SignatureFile.Get()); const bool bSuccess = !PakFile->IsError() && !SignatureFile->IsError(); return bSuccess; } bool SignIOStoreContainer(FArchive& ContainerAr) { return true; } class FMemoryCompressor; /** * AsyncTask for FMemoryCompressor * Compress a memory block asynchronously */ class FBlockCompressTask : public FNonAbandonableTask { public: friend class FAsyncTask; friend class FMemoryCompressor; FBlockCompressTask(void* InUncompressedBuffer, int32 InUncompressedSize, FName InFormat, int32 InBlockSize, std::atomic& InRemainingTasksCounter, FGraphEventRef InAllTasksFinishedEvent) : RemainingTasksCounter(InRemainingTasksCounter), AllTasksFinishedEvent(InAllTasksFinishedEvent), UncompressedBuffer(InUncompressedBuffer), UncompressedSize(InUncompressedSize), Format(InFormat), BlockSize(InBlockSize), Result(false) { // Store buffer size. CompressedSize = FCompression::CompressMemoryBound(Format, BlockSize); CompressedBuffer = FMemory::Malloc(CompressedSize); } ~FBlockCompressTask() { FMemory::Free(CompressedBuffer); } /** Do compress */ void DoWork() { #if DETAILED_UNREALPAK_TIMING FUnrealPakScopeCycleCounter Scope(GCompressionTime); #endif // Compress memory block. // Actual size will be stored to CompressedSize. Result = FCompression::CompressMemory(Format, CompressedBuffer, CompressedSize, UncompressedBuffer, UncompressedSize, COMPRESS_ForPackaging); if (--RemainingTasksCounter == 0) { AllTasksFinishedEvent->DispatchSubsequents(); } } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } private: std::atomic& RemainingTasksCounter; FGraphEventRef AllTasksFinishedEvent; // Source buffer void* UncompressedBuffer; int32 UncompressedSize; // Compress parameters FName Format; int32 BlockSize; int32 BitWindow; // Compressed result void* CompressedBuffer; int32 CompressedSize; bool Result; }; /** * asynchronous memory compressor */ class FMemoryCompressor { public: /** Divide into blocks and start compress asynchronously */ FMemoryCompressor(uint8* UncompressedBuffer, int64 UncompressedSize, FName Format, int32 CompressionBlockSize, FGraphEventRef InCompressionFinishedEvent) : CompressionFinishedEvent(InCompressionFinishedEvent) , Index(0) { // Divide into blocks and start compression async tasks. // These blocks must be as same as followed CompressMemory callings. int64 UncompressedBytes = 0; while (UncompressedSize) { int32 BlockSize = (int32)FMath::Min(UncompressedSize, CompressionBlockSize); auto* AsyncTask = new FAsyncTask(UncompressedBuffer + UncompressedBytes, BlockSize, Format, BlockSize, RemainingTasksCounter, CompressionFinishedEvent); BlockCompressAsyncTasks.Add(AsyncTask); UncompressedSize -= BlockSize; UncompressedBytes += BlockSize; } } ~FMemoryCompressor() { for (FAsyncTask* AsyncTask : BlockCompressAsyncTasks) { check(AsyncTask->IsDone()); delete AsyncTask; } } void StartWork() { if (BlockCompressAsyncTasks.IsEmpty()) { CompressionFinishedEvent->DispatchSubsequents(); } else { int32 AsyncTasksCount = BlockCompressAsyncTasks.Num(); RemainingTasksCounter = AsyncTasksCount; for (int32 TaskIndex = 0; TaskIndex < AsyncTasksCount; ++TaskIndex) { BlockCompressAsyncTasks[TaskIndex]->StartBackgroundTask(); } } } /** Fetch compressed result. Returns true and store CompressedSize if succeeded */ bool CopyNextCompressedBuffer(FName Format, void* CompressedBuffer, int32& CompressedSize, const void* UncompressedBuffer, int32 UncompressedSize) { // Fetch compressed result from task. // We assume this is called only once, same order, same parameters for // each task. FAsyncTask* AsyncTask = BlockCompressAsyncTasks[Index++]; while (!AsyncTask->IsDone()) { // Compression is done but we also need to wait for the async task to be marked as completed before we call GetTask() below // DON'T call EnsureCompletion here, we're already running in a task so we don't want to pull in other tasks while waiting FPlatformProcess::Sleep(0); } FBlockCompressTask& Task = AsyncTask->GetTask(); check(Task.Format == Format); check(Task.UncompressedBuffer == UncompressedBuffer); check(Task.UncompressedSize == UncompressedSize); check(CompressedSize >= Task.CompressedSize); if (!Task.Result) { return false; } FMemory::Memcpy(CompressedBuffer, Task.CompressedBuffer, Task.CompressedSize); CompressedSize = Task.CompressedSize; return true; } private: FGraphEventRef CompressionFinishedEvent; TArray*> BlockCompressAsyncTasks; std::atomic RemainingTasksCounter = 0; // Fetched task index int32 Index; }; bool FPakOrderMap::ProcessOrderFile(const TCHAR* ResponseFile, bool bSecondaryOrderFile, bool bMergeOrder, TOptional InOffset) { uint64 OrderOffset = 0; int32 OpenOrderNumber = 0; if (InOffset.IsSet()) { OrderOffset = InOffset.GetValue(); } else if (bSecondaryOrderFile || bMergeOrder) { OrderOffset = MaxIndex + 1; } if (bSecondaryOrderFile) { MaxPrimaryOrderIndex = OrderOffset; } // List of all items to add to pak file FString Text; UE_LOG(LogPakFile, Display, TEXT("Loading pak order file %s..."), ResponseFile); if (FFileHelper::LoadFileToString(Text, ResponseFile)) { // Read all lines TArray Lines; Text.ParseIntoArray(Lines, TEXT("\n"), true); for (int32 EntryIndex = 0; EntryIndex < Lines.Num(); EntryIndex++) { FString Path; Lines[EntryIndex].ReplaceInline(TEXT("\r"), TEXT("")); Lines[EntryIndex].ReplaceInline(TEXT("\n"), TEXT("")); const TCHAR* OrderLinePtr = *(Lines[EntryIndex]); // Skip comments if (FCString::Strncmp(OrderLinePtr, TEXT("#"), 1) == 0 || FCString::Strncmp(OrderLinePtr, TEXT("//"), 2) == 0) { continue; } if (!FParse::Token(OrderLinePtr, Path, false)) { UE_LOG(LogPakFile, Error, TEXT("Invalid entry in the response file %s."), *Lines[EntryIndex]); return false; } if(Lines[EntryIndex].FindLastChar('"', OpenOrderNumber)) { FString ReadNum = Lines[EntryIndex].RightChop(OpenOrderNumber + 1); Lines[EntryIndex].LeftInline(OpenOrderNumber + 1, EAllowShrinking::No); ReadNum.TrimStartInline(); if(ReadNum.Len() == 0) { // If order files don't have explicit numbers just use the line number OpenOrderNumber = EntryIndex; } else if (ReadNum.IsNumeric()) { OpenOrderNumber = FCString::Atoi(*ReadNum); } else { UE_LOG(LogPakFile, Error, TEXT("Invalid entry in the response file %s, couldn't parse an order number after the path."), *Lines[EntryIndex]); return false; } } else { // If order files don't have explicit numbers just use the line number OpenOrderNumber = EntryIndex; } FPaths::NormalizeFilename(Path); Path = Path.ToLower(); if ((bSecondaryOrderFile || bMergeOrder) && OrderMap.Contains(Path)) { continue; } OrderMap.Add(Path, OpenOrderNumber + OrderOffset); MaxIndex = FMath::Max(MaxIndex, OpenOrderNumber + OrderOffset); } UE_LOG(LogPakFile, Display, TEXT("Finished loading pak order file %s."), ResponseFile); return true; } else { UE_LOG(LogPakFile, Error, TEXT("Unable to load pak order file %s."), ResponseFile); return false; } } void FPakOrderMap::MergeOrderMap(FPakOrderMap&& Other) { for (TPair& OrderedFile : Other.OrderMap) { if (OrderMap.Contains(OrderedFile.Key) == false) { OrderMap.Add(MoveTemp(OrderedFile.Key), OrderedFile.Value); } } Other.OrderMap.Empty(); // We moved strings out of this so empty it so nobody tries to use the keys if (MaxIndex == MAX_uint64) { MaxIndex = Other.MaxIndex; } else { MaxIndex = FMath::Max(MaxIndex, Other.MaxIndex); } } uint64 FPakOrderMap::GetFileOrder(const FString& Path, bool bAllowUexpUBulkFallback, bool* OutIsPrimary) const { FString RegionStr; FString NewPath = RemapLocalizationPathIfNeeded(Path.ToLower(), RegionStr); const uint64* FoundOrder = OrderMap.Find(NewPath); uint64 ReturnOrder = MAX_uint64; if (FoundOrder != nullptr) { ReturnOrder = *FoundOrder; if (OutIsPrimary) { *OutIsPrimary = (ReturnOrder < MaxPrimaryOrderIndex); } } else if (bAllowUexpUBulkFallback) { // if this is a cook order or an old order it will not have uexp files in it, so we put those in the same relative order after all of the normal files, but before any ubulk files if (Path.EndsWith(TEXT("uexp")) || Path.EndsWith(TEXT("ubulk"))) { uint64 CounterpartOrder = GetFileOrder(FPaths::GetBaseFilename(Path, false) + TEXT(".uasset"), false); if (CounterpartOrder == MAX_uint64) { CounterpartOrder = GetFileOrder(FPaths::GetBaseFilename(Path, false) + TEXT(".umap"), false); } if (CounterpartOrder != MAX_uint64) { if (Path.EndsWith(TEXT("uexp"))) { ReturnOrder = CounterpartOrder | (1 << 29); } else { ReturnOrder = CounterpartOrder | (1 << 30); } } } } // Optionally offset based on region, so multiple files in different regions don't get the same order. // I/O profiling suggests this is slightly worse, so leaving this disabled for now #if 0 if (ReturnOrder != MAX_uint64) { if (RegionStr.Len() > 0) { uint64 RegionOffset = 0; for (int i = 0; i < RegionStr.Len(); i++) { int8 Letter = (int8)(RegionStr[i] - TEXT('a')); RegionOffset |= (uint64(Letter) << (i * 5)); } return ReturnOrder + (RegionOffset << 16); } } #endif return ReturnOrder; } void FPakOrderMap::WriteOpenOrder(FArchive* Ar) { OrderMap.ValueSort([](const uint64& A, const uint64& B) { return A < B; }); for (const auto& It : OrderMap) { Ar->Logf(TEXT("\"%s\" %d"), *It.Key, It.Value); } } FString FPakOrderMap::RemapLocalizationPathIfNeeded(const FString& PathLower, FString& OutRegion) const { static const TCHAR* L10NPrefix = (const TCHAR*)TEXT("/content/l10n/"); static const int32 L10NPrefixLength = FCString::Strlen(L10NPrefix); int32 FoundIndex = PathLower.Find(L10NPrefix, ESearchCase::CaseSensitive); if (FoundIndex > 0) { // Validate the content index is the first one int32 ContentIndex = PathLower.Find(TEXT("/content/"), ESearchCase::CaseSensitive); if (ContentIndex == FoundIndex) { int32 EndL10NOffset = ContentIndex + L10NPrefixLength; int32 NextSlashIndex = PathLower.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, EndL10NOffset); int32 RegionLength = NextSlashIndex - EndL10NOffset; if (RegionLength >= 2) { FString NonLocalizedPath = PathLower.Mid(0, ContentIndex) + TEXT("/content") + PathLower.Mid(NextSlashIndex); OutRegion = PathLower.Mid(EndL10NOffset, RegionLength); return NonLocalizedPath; } } } return PathLower; } enum class ESeekOptMode : uint8 { None = 0, OnePass = 1, Incremental = 2, Incremental_OnlyPrimaryOrder = 3, Incremental_PrimaryThenSecondary = 4, COUNT }; struct FPatchSeekOptParams { FPatchSeekOptParams() : MaxGapSize(0) , MaxInflationPercent(0.0f) , Mode(ESeekOptMode::None) , MaxAdjacentOrderDiff(128) {} int64 MaxGapSize; float MaxInflationPercent; // For Incremental_ modes only ESeekOptMode Mode; int32 MaxAdjacentOrderDiff; }; struct FPakCommandLineParameters { FPakCommandLineParameters() : CompressionBlockSize(64 * 1024) , FileSystemBlockSize(0) , PatchFilePadAlign(0) , AlignForMemoryMapping(0) , GeneratePatch(false) , EncryptIndex(false) , UseCustomCompressor(false) , bSign(false) , bPatchCompatibilityMode421(false) , bFallbackOrderForNonUassetFiles(false) , bAlignFilesLargerThanBlock(false) , bForceCompress(false) , bFileRegions(false) , bRequiresRehydration(false) { } TArray CompressionFormats; FPatchSeekOptParams SeekOptParams; int32 CompressionBlockSize; int64 FileSystemBlockSize; int64 PatchFilePadAlign; int64 AlignForMemoryMapping; bool GeneratePatch; FString SourcePatchPakFilename; FString SourcePatchDiffDirectory; FString InputFinalPakFilename; // This is the resulting pak file we want to end up with after we generate the pak patch. This is used instead of passing in the raw content. FString ChangedFilesOutputFilename; FString CsvPath; FString ProjectStoreFilename; bool EncryptIndex; bool UseCustomCompressor; FGuid EncryptionKeyGuid; bool bSign; bool bPatchCompatibilityMode421; bool bFallbackOrderForNonUassetFiles; bool bAlignFilesLargerThanBlock; // Align files that are larger than block size bool bForceCompress; // Force all files that request compression to be compressed, even if that results in a larger file size DEPRECATED bool bFileRegions; // Enables the processing and output of cook file region metadata, used during packaging on some platforms. bool bRequiresRehydration; // One or more files to be added to the pak file are virtualized and require rehydration. }; struct FPakInputPair { FString Source; FString Dest; uint64 SuggestedOrder; bool bNeedsCompression; bool bNeedEncryption; bool bIsDeleteRecord; // This is used for patch PAKs when a file is deleted from one patch to the next bool bIsInPrimaryOrder; bool bNeedRehydration; FPakInputPair() : SuggestedOrder(MAX_uint64) , bNeedsCompression(false) , bNeedEncryption(false) , bIsDeleteRecord(false) , bIsInPrimaryOrder(false) , bNeedRehydration(false) {} FPakInputPair(const FString& InSource, const FString& InDest) : Source(InSource) , Dest(InDest) , bNeedsCompression(false) , bNeedEncryption(false) , bIsDeleteRecord(false) , bNeedRehydration(false) {} FORCEINLINE bool operator==(const FPakInputPair& Other) const { return Source == Other.Source; } }; struct FPakEntryOrder { FPakEntryOrder() : Order(MAX_uint64) {} FString Filename; uint64 Order; }; struct FFileInfo { uint64 FileSize; int32 PatchIndex; bool bIsDeleteRecord; bool bForceInclude; uint8 Hash[16]; }; bool ExtractFilesFromPak(const TCHAR* InPakFilename, TMap& InFileHashes, const TCHAR* InDestPath, bool bUseMountPoint, const FKeyChain& InKeyChain, const FString* InFilter, TArray* OutEntries = nullptr, TArray* OutDeletedEntries = nullptr, FPakOrderMap* OutOrderMap = nullptr, TArray* OutUsedEncryptionKeys = nullptr, bool* OutAnyPakSigned = nullptr); struct FCompressedFileBuffer { FCompressedFileBuffer() : OriginalSize(0) , TotalCompressedSize(0) , FileCompressionBlockSize(0) , CompressedBufferSize(0) , RehydrationCount(0) , RehydrationBytes(0) { } void Reinitialize(FName CompressionMethod, int64 CompressionBlockSize) { TotalCompressedSize = 0; FileCompressionBlockSize = 0; FileCompressionMethod = CompressionMethod; CompressedBlocks.Reset(); CompressedBlocks.AddUninitialized(IntCastChecked((OriginalSize+CompressionBlockSize-1)/CompressionBlockSize)); } void Empty() { OriginalSize = 0; TotalCompressedSize = 0; FileCompressionBlockSize = 0; FileCompressionMethod = NAME_None; CompressedBuffer = nullptr; CompressedBufferSize = 0; CompressedBlocks.Empty(); RehydrationCount = 0; RehydrationBytes = 0; } void EnsureBufferSpace(int64 RequiredSpace) { if(RequiredSpace > CompressedBufferSize) { TUniquePtr NewCompressedBuffer = MakeUnique(RequiredSpace); FMemory::Memcpy(NewCompressedBuffer.Get(), CompressedBuffer.Get(), CompressedBufferSize); CompressedBuffer = MoveTemp(NewCompressedBuffer); CompressedBufferSize = RequiredSpace; } } void ResetSource(); bool ReadSource(const FPakInputPair& InFile, FCookedPackageStore* PackageStore); void SetSourceAsWorkingBuffer(); TUniquePtr BeginCompressFileToWorkingBuffer(const FPakInputPair& InFile, FName CompressionMethod, const int32 CompressionBlockSize, FGraphEventRef EndCompressionBarrier); bool EndCompressFileToWorkingBuffer(const FPakInputPair& InFile, FName CompressionMethod, const int32 CompressionBlockSize, FMemoryCompressor& MemoryCompressor); void SerializeDDCData(FArchive &Ar) { Ar << OriginalSize; Ar << TotalCompressedSize; Ar << FileCompressionBlockSize; Ar << FileCompressionMethod; Ar << CompressedBlocks; if (Ar.IsLoading()) { EnsureBufferSpace(TotalCompressedSize); } Ar.Serialize(CompressedBuffer.Get(), TotalCompressedSize); } int64 GetSerializedSizeEstimate() const { int64 Size = 0; Size += sizeof(*this); Size += CompressedBlocks.Num() * sizeof(FPakCompressedBlock); Size += CompressedBufferSize; return Size; } FString GetDDCKeyString(const uint8* UncompressedFile, const int64& UncompressedFileSize, FName CompressionFormat, const int64& BlockSize); /** Returns the size of the file, excluding any padding that might have been applied */ int64 GetFileSize() const { return OriginalSize; } TArray64 UncompressedBuffer; int64 OriginalSize; int64 TotalCompressedSize; int32 FileCompressionBlockSize; FName FileCompressionMethod; TArray CompressedBlocks; int64 CompressedBufferSize; TUniquePtr CompressedBuffer; int32 RehydrationCount; int64 RehydrationBytes; }; template bool ReadSizeParam(const TCHAR* CmdLine, const TCHAR* ParamStr, T& SizeOut) { FString ParamValueStr; if (FParse::Value(CmdLine, ParamStr, ParamValueStr) && FParse::Value(CmdLine, ParamStr, SizeOut)) { if (ParamValueStr.EndsWith(TEXT("GB"))) { SizeOut *= 1024 * 1024 * 1024; } else if (ParamValueStr.EndsWith(TEXT("MB"))) { SizeOut *= 1024 * 1024; } else if (ParamValueStr.EndsWith(TEXT("KB"))) { SizeOut *= 1024; } return true; } return false; } FString GetLongestPath(const TArray& FilesToAdd) { FString LongestPath; int32 MaxNumDirectories = 0; for (int32 FileIndex = 0; FileIndex < FilesToAdd.Num(); FileIndex++) { const FString& Filename = FilesToAdd[FileIndex].Dest; int32 NumDirectories = 0; for (int32 Index = 0; Index < Filename.Len(); Index++) { if (Filename[Index] == '/') { NumDirectories++; } } if (NumDirectories > MaxNumDirectories) { LongestPath = Filename; MaxNumDirectories = NumDirectories; } } return FPaths::GetPath(LongestPath) + TEXT("/"); } FString GetCommonRootPath(const TArray& FilesToAdd) { FString Root = GetLongestPath(FilesToAdd); for (int32 FileIndex = 0; FileIndex < FilesToAdd.Num() && Root.Len(); FileIndex++) { FString Filename(FilesToAdd[FileIndex].Dest); FString Path = FPaths::GetPath(Filename) + TEXT("/"); int32 CommonSeparatorIndex = -1; int32 SeparatorIndex = Path.Find(TEXT("/"), ESearchCase::CaseSensitive); while (SeparatorIndex >= 0) { if (FCString::Strnicmp(*Root, *Path, SeparatorIndex + 1) != 0) { break; } CommonSeparatorIndex = SeparatorIndex; if (CommonSeparatorIndex + 1 < Path.Len()) { SeparatorIndex = Path.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, CommonSeparatorIndex + 1); } else { break; } } if ((CommonSeparatorIndex + 1) < Root.Len()) { Root.MidInline(0, CommonSeparatorIndex + 1, EAllowShrinking::No); } } return Root; } FString FCompressedFileBuffer::GetDDCKeyString(const uint8* UncompressedFile, const int64& UncompressedFileSize, FName CompressionFormat, const int64& BlockSize) { FString KeyString; KeyString += FString::Printf(TEXT("_F:%s_C:%s_B:%" INT64_FMT "_"), *CompressionFormat.ToString(), *FCompression::GetCompressorDDCSuffix(CompressionFormat), BlockSize); FSHA1 HashState; HashState.Update(UncompressedFile, UncompressedFileSize); HashState.Final(); FSHAHash FinalHash; HashState.GetHash(FinalHash.Hash); KeyString += FinalHash.ToString();; return FDerivedDataCacheInterface::BuildCacheKey(TEXT("PAKCOMPRESS_"), PAKCOMPRESS_DERIVEDDATA_VER, *KeyString); } void FCompressedFileBuffer::ResetSource() { UncompressedBuffer.Empty(); } bool FCompressedFileBuffer::ReadSource(const FPakInputPair& InputInfo, FCookedPackageStore* PackageStore) { if (!InputInfo.bNeedRehydration) { TUniquePtr FileHandle(IFileManager::Get().CreateFileReader(*InputInfo.Source)); if (FileHandle) { OriginalSize = FileHandle->TotalSize(); const int64 PaddedEncryptedFileSize = Align(OriginalSize, FAES::AESBlockSize); UncompressedBuffer.SetNumUninitialized(PaddedEncryptedFileSize); FileHandle->Serialize(UncompressedBuffer.GetData(), OriginalSize); if (!FileHandle->IsError()) { return true; } } else if (PackageStore && PackageStore->HasZenStoreClient()) { FString FullFilename = FPaths::ConvertRelativePathToFull(InputInfo.Source); FIoChunkId ChunkId = PackageStore->GetChunkIdFromFileName(FullFilename); if (ChunkId.IsValid()) { TIoStatusOr ChunkReadStatus = PackageStore->ReadChunk(ChunkId); if (ChunkReadStatus.IsOk()) { FIoBuffer Buffer = ChunkReadStatus.ConsumeValueOrDie(); OriginalSize = Buffer.GetSize(); const int64 PaddedEncryptedFileSize = Align(OriginalSize, FAES::AESBlockSize); UncompressedBuffer.SetNumUninitialized(PaddedEncryptedFileSize); FMemory::Memcpy(UncompressedBuffer.GetData(), Buffer.GetData(), Buffer.GetSize()); return true; } } } UncompressedBuffer.Empty(); OriginalSize = -1; return false; } else { using namespace UE::Virtualization; IVirtualizationSystem& System = IVirtualizationSystem::Get(); TArray PackagePath; PackagePath.Add(InputInfo.Source); TArray RehydratedPackage; TArray RehydrationInfo; TArray Errors; if (System.TryRehydratePackages(PackagePath, FAES::AESBlockSize, Errors, RehydratedPackage, &RehydrationInfo) == ERehydrationResult::Success) { OriginalSize = RehydrationInfo[0].RehydratedSize; const int64 PaddedEncryptedFileSize = Align(OriginalSize, FAES::AESBlockSize); // Confirm padding worked check(RehydratedPackage[0].GetSize() == PaddedEncryptedFileSize); //TODO: Keep data in FSharedBuffer form, rather than TArray UncompressedBuffer.SetNumUninitialized(RehydratedPackage[0].GetSize()); FMemory::Memcpy(UncompressedBuffer.GetData(), RehydratedPackage[0].GetData(), RehydratedPackage[0].GetSize()); RehydrationCount = RehydrationInfo[0].NumPayloadsRehydrated; RehydrationBytes = RehydrationInfo[0].RehydratedSize - RehydrationInfo[0].OriginalSize; if (RehydrationCount) { UE_LOG(LogPakFile, Verbose, TEXT("Rehydrated (+%lld bytes) %s"), RehydrationBytes, *PackagePath[0]); } return true; } else { UncompressedBuffer.Empty(); OriginalSize = -1; return false; } } } void FCompressedFileBuffer::SetSourceAsWorkingBuffer() { // TODO: Do not submit with this assert check(!UncompressedBuffer.IsEmpty() || OriginalSize == 0); // TODO: Better way to move the buffers ownership, but can't safely steal the memory in a // TArray CompressedBufferSize = UncompressedBuffer.Num(); CompressedBuffer = MakeUnique(CompressedBufferSize); FMemory::Memcpy(CompressedBuffer.Get(), UncompressedBuffer.GetData(), CompressedBufferSize); UncompressedBuffer.Empty(); } TUniquePtr FCompressedFileBuffer::BeginCompressFileToWorkingBuffer(const FPakInputPair& InFile, FName CompressionMethod, const int32 CompressionBlockSize, FGraphEventRef EndCompressionBarrier) { check(!UncompressedBuffer.IsEmpty() || OriginalSize == 0); Reinitialize(CompressionMethod, CompressionBlockSize); #if USE_DDC_FOR_COMPRESSED_FILES const bool bShouldUseDDC = true; // && (FileSize > 20 * 1024 ? true : false); FString DDCKey; TArray> GetData; if (bShouldUseDDC) { #if DETAILED_UNREALPAK_TIMING FUnrealPakScopeCycleCounter Scope(GDDCSyncReadTime); #endif DDCKey = GetDDCKeyString(UncompressedBuffer.GetData(), FileSize, CompressionMethod, CompressionBlockSize); if (GetDerivedDataCacheRef().CachedDataProbablyExists(*DDCKey)) { int32 AsyncHandle = GetDerivedDataCacheRef().GetAsynchronous(*DDCKey, InFile.Dest); GetDerivedDataCacheRef().WaitAsynchronousCompletion(AsyncHandle); GetData.Empty(GetData.Max()); bool Result = false; GetDerivedDataCacheRef().GetAsynchronousResults(AsyncHandle, GetData, &Result); if (Result) { FMemoryReader Ar(GetData, true); SerializeDDCData(Ar); UncompressedBuffer.Empty(); EndCompressionBarrier->DispatchSubsequents(); return nullptr; } } } #endif // Start parallel compress return MakeUnique(UncompressedBuffer.GetData(), OriginalSize, CompressionMethod, CompressionBlockSize, EndCompressionBarrier); } bool FCompressedFileBuffer::EndCompressFileToWorkingBuffer(const FPakInputPair& InFile, FName CompressionMethod, const int32 CompressionBlockSize, FMemoryCompressor& MemoryCompressor) { { // Build buffers for working int64 UncompressedSize = OriginalSize; // CompressMemoryBound truncates its size argument to 32bits, so we can not use (possibly > 32-bit) FileSize directly to calculate required buffer space int32 MaxCompressedBufferSize = Align(FCompression::CompressMemoryBound(CompressionMethod, CompressionBlockSize, COMPRESS_NoFlags), FAES::AESBlockSize); int32 CompressionBufferRemainder = Align(FCompression::CompressMemoryBound(CompressionMethod, int32(UncompressedSize % CompressionBlockSize), COMPRESS_NoFlags), FAES::AESBlockSize); EnsureBufferSpace(MaxCompressedBufferSize * (UncompressedSize / CompressionBlockSize) + CompressionBufferRemainder); TotalCompressedSize = 0; int64 UncompressedBytes = 0; int32 CurrentBlock = 0; while (UncompressedSize) { int32 BlockSize = (int32)FMath::Min(UncompressedSize, CompressionBlockSize); int32 MaxCompressedBlockSize = FCompression::CompressMemoryBound(CompressionMethod, BlockSize, COMPRESS_NoFlags); int32 CompressedBlockSize = FMath::Max(MaxCompressedBufferSize, MaxCompressedBlockSize); FileCompressionBlockSize = FMath::Max(BlockSize, FileCompressionBlockSize); EnsureBufferSpace(Align(TotalCompressedSize + CompressedBlockSize, FAES::AESBlockSize)); if (!MemoryCompressor.CopyNextCompressedBuffer(CompressionMethod, CompressedBuffer.Get() + TotalCompressedSize, CompressedBlockSize, UncompressedBuffer.GetData() + UncompressedBytes, BlockSize)) { return false; } UncompressedSize -= BlockSize; UncompressedBytes += BlockSize; CompressedBlocks[CurrentBlock].CompressedStart = TotalCompressedSize; CompressedBlocks[CurrentBlock].CompressedEnd = TotalCompressedSize + CompressedBlockSize; ++CurrentBlock; TotalCompressedSize += CompressedBlockSize; if (InFile.bNeedEncryption) { int64 EncryptionBlockPadding = Align(TotalCompressedSize, FAES::AESBlockSize); for (int64 FillIndex = TotalCompressedSize; FillIndex < EncryptionBlockPadding; ++FillIndex) { // Fill the trailing buffer with bytes from file. Note that this is now from a fixed location // rather than a random one so that we produce deterministic results CompressedBuffer.Get()[FillIndex] = CompressedBuffer.Get()[FillIndex % TotalCompressedSize]; } TotalCompressedSize += EncryptionBlockPadding - TotalCompressedSize; } } } #if USE_DDC_FOR_COMPRESSED_FILES if (bShouldUseDDC) { #if DETAILED_UNREALPAK_TIMING FUnrealPakScopeCycleCounter Scope(GDDCSyncWriteTime); ++GDDCMisses; #endif GetData.Empty(GetData.Max()); FMemoryWriter Ar(GetData, true); SerializeDDCData(Ar); GetDerivedDataCacheRef().Put(*DDCKey, GetData, InFile.Dest); } #endif return true; } bool PrepareCopyFileToPak(const FString& InMountPoint, const FPakInputPair& InFile, const FCompressedFileBuffer& UncompressedFile, FPakEntryPair& OutNewEntry, uint8*& OutDataToWrite, int64& OutSizeToWrite, const FKeyChain& InKeyChain, TArray* OutFileRegions) { const int64 FileSize = UncompressedFile.OriginalSize; const int64 PaddedEncryptedFileSize = Align(FileSize, FAES::AESBlockSize); if (FileSize < 0) { return false; } check(UncompressedFile.CompressedBufferSize == PaddedEncryptedFileSize); OutNewEntry.Filename = InFile.Dest.Mid(InMountPoint.Len()); OutNewEntry.Info.Offset = 0; // Don't serialize offsets here. OutNewEntry.Info.Size = FileSize; OutNewEntry.Info.UncompressedSize = FileSize; OutNewEntry.Info.CompressionMethodIndex = 0; OutNewEntry.Info.SetEncrypted( InFile.bNeedEncryption ); OutNewEntry.Info.SetDeleteRecord(false); { OutSizeToWrite = FileSize; uint8* UncompressedBuffer = UncompressedFile.CompressedBuffer.Get(); if (InFile.bNeedEncryption) { for(int64 FillIndex = FileSize; FillIndex < PaddedEncryptedFileSize; ++FillIndex) { // Fill the trailing buffer with bytes from file. Note that this is now from a fixed location // rather than a random one so that we produce deterministic results UncompressedBuffer[FillIndex] = UncompressedBuffer[(FillIndex - FileSize)%FileSize]; } //Encrypt the buffer before writing it to disk check(InKeyChain.GetPrincipalEncryptionKey()); FAES::EncryptData(UncompressedBuffer, PaddedEncryptedFileSize, InKeyChain.GetPrincipalEncryptionKey()->Key); // Update the size to be written OutSizeToWrite = PaddedEncryptedFileSize; OutNewEntry.Info.SetEncrypted( true ); } // Calculate the buffer hash value FSHA1::HashBuffer(UncompressedBuffer, FileSize, OutNewEntry.Info.Hash); OutDataToWrite = UncompressedBuffer; } if (OutFileRegions) { // Read the matching regions file, if it exists. TUniquePtr RegionsFile(IFileManager::Get().CreateFileReader(*(InFile.Source + FFileRegion::RegionsFileExtension))); if (RegionsFile.IsValid()) { FFileRegion::SerializeFileRegions(*RegionsFile.Get(), *OutFileRegions); } } return true; } void FinalizeCopyCompressedFileToPak(FPakInfo& InPakInfo, const FCompressedFileBuffer& CompressedFile, FPakEntryPair& OutNewEntry) { check(CompressedFile.TotalCompressedSize != 0); check(OutNewEntry.Info.CompressionBlocks.Num() == CompressedFile.CompressedBlocks.Num()); check(OutNewEntry.Info.CompressionMethodIndex == InPakInfo.GetCompressionMethodIndex(CompressedFile.FileCompressionMethod)); int64 TellPos = OutNewEntry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest); const TArray& Blocks = CompressedFile.CompressedBlocks; for (int32 BlockIndex = 0, BlockCount = Blocks.Num(); BlockIndex < BlockCount; ++BlockIndex) { OutNewEntry.Info.CompressionBlocks[BlockIndex].CompressedStart = Blocks[BlockIndex].CompressedStart + TellPos; OutNewEntry.Info.CompressionBlocks[BlockIndex].CompressedEnd = Blocks[BlockIndex].CompressedEnd + TellPos; } } bool PrepareCopyCompressedFileToPak(const FString& InMountPoint, FPakInfo& Info, const FPakInputPair& InFile, const FCompressedFileBuffer& CompressedFile, FPakEntryPair& OutNewEntry, uint8*& OutDataToWrite, int64& OutSizeToWrite, const FKeyChain& InKeyChain) { if (CompressedFile.TotalCompressedSize == 0) { return false; } OutNewEntry.Info.CompressionMethodIndex = Info.GetCompressionMethodIndex(CompressedFile.FileCompressionMethod); OutNewEntry.Info.CompressionBlocks.AddZeroed(CompressedFile.CompressedBlocks.Num()); if (InFile.bNeedEncryption) { check(InKeyChain.GetPrincipalEncryptionKey()); FAES::EncryptData(CompressedFile.CompressedBuffer.Get(), CompressedFile.TotalCompressedSize, InKeyChain.GetPrincipalEncryptionKey()->Key); } //Hash the final buffer thats written FSHA1 Hash; Hash.Update(CompressedFile.CompressedBuffer.Get(), CompressedFile.TotalCompressedSize); Hash.Final(); // Update file size & Hash OutNewEntry.Info.CompressionBlockSize = CompressedFile.FileCompressionBlockSize; OutNewEntry.Info.UncompressedSize = CompressedFile.OriginalSize; OutNewEntry.Info.Size = CompressedFile.TotalCompressedSize; Hash.GetHash(OutNewEntry.Info.Hash); // Write the header, then the data OutNewEntry.Filename = InFile.Dest.Mid(InMountPoint.Len()); OutNewEntry.Info.Offset = 0; // Don't serialize offsets here. OutNewEntry.Info.SetEncrypted( InFile.bNeedEncryption ); OutNewEntry.Info.SetDeleteRecord(false); OutSizeToWrite = CompressedFile.TotalCompressedSize; OutDataToWrite = CompressedFile.CompressedBuffer.Get(); //OutNewEntry.Info.Serialize(InPak,FPakInfo::PakFile_Version_Latest); //InPak.Serialize(CompressedFile.CompressedBuffer.Get(), CompressedFile.TotalCompressedSize); return true; } void PrepareDeleteRecordForPak(const FString& InMountPoint, const FPakInputPair InDeletedFile, FPakEntryPair& OutNewEntry) { OutNewEntry.Filename = InDeletedFile.Dest.Mid(InMountPoint.Len()); OutNewEntry.Info.SetDeleteRecord(true); } void ProcessCommonCommandLine(const TCHAR* CmdLine, FPakCommandLineParameters& CmdLineParameters) { // List of all items to add to pak file FString ClusterSizeString; if (FParse::Param(CmdLine, TEXT("patchcompatibilitymode421"))) { CmdLineParameters.bPatchCompatibilityMode421 = true; } if (FParse::Param(CmdLine, TEXT("fallbackOrderForNonUassetFiles"))) { CmdLineParameters.bFallbackOrderForNonUassetFiles = true; } if (FParse::Value(CmdLine, TEXT("-blocksize="), ClusterSizeString) && FParse::Value(CmdLine, TEXT("-blocksize="), CmdLineParameters.FileSystemBlockSize)) { if (ClusterSizeString.EndsWith(TEXT("MB"))) { CmdLineParameters.FileSystemBlockSize *= 1024*1024; } else if (ClusterSizeString.EndsWith(TEXT("KB"))) { CmdLineParameters.FileSystemBlockSize *= 1024; } } else { CmdLineParameters.FileSystemBlockSize = 0; } FString CompBlockSizeString; if (FParse::Value(CmdLine, TEXT("-compressionblocksize="), CompBlockSizeString) && FParse::Value(CmdLine, TEXT("-compressionblocksize="), CmdLineParameters.CompressionBlockSize)) { if (CompBlockSizeString.EndsWith(TEXT("MB"))) { CmdLineParameters.CompressionBlockSize *= 1024 * 1024; } else if (CompBlockSizeString.EndsWith(TEXT("KB"))) { CmdLineParameters.CompressionBlockSize *= 1024; } } if (!FParse::Value(CmdLine, TEXT("-patchpaddingalign="), CmdLineParameters.PatchFilePadAlign)) { CmdLineParameters.PatchFilePadAlign = 0; } if (!FParse::Value(CmdLine, TEXT("-AlignForMemoryMapping="), CmdLineParameters.AlignForMemoryMapping)) { CmdLineParameters.AlignForMemoryMapping = 0; } if (FParse::Param(CmdLine, TEXT("encryptindex"))) { CmdLineParameters.EncryptIndex = true; } if (FParse::Param(CmdLine, TEXT("sign"))) { CmdLineParameters.bSign = true; } if (FParse::Param(CmdLine, TEXT("AlignFilesLargerThanBlock"))) { CmdLineParameters.bAlignFilesLargerThanBlock = true; } if (FParse::Param(CmdLine, TEXT("ForceCompress"))) { CmdLineParameters.bForceCompress = true; UE_LOG(LogPakFile, Warning, TEXT("-ForceCompress is deprecated. It will be removed in a future release.")); } if (FParse::Param(CmdLine, TEXT("FileRegions"))) { CmdLineParameters.bFileRegions = true; } FString DesiredCompressionFormats; // look for -compressionformats or -compressionformat on the commandline if (FParse::Value(CmdLine, TEXT("-compressionformats="), DesiredCompressionFormats) || FParse::Value(CmdLine, TEXT("-compressionformat="), DesiredCompressionFormats)) { TArray Formats; DesiredCompressionFormats.ParseIntoArray(Formats, TEXT(",")); for (FString& Format : Formats) { // look until we have a valid format FName FormatName = *Format; if (FCompression::IsFormatValid(FormatName)) { CmdLineParameters.CompressionFormats.Add(FormatName); break; } else { UE_LOG(LogPakFile, Warning, TEXT("Compression format %s is not recognized"), *Format); } } } // disable zlib as fallback if requested if (FParse::Param(CmdLine, TEXT("disablezlib"))) { UE_LOG(LogPakFile, Display, TEXT("Disabling ZLib as a compression option.")); } else { CmdLineParameters.CompressionFormats.AddUnique(NAME_Zlib); } FParse::Value(CmdLine, TEXT("-patchSeekOptMaxInflationPercent="), CmdLineParameters.SeekOptParams.MaxInflationPercent); ReadSizeParam(CmdLine, TEXT("-patchSeekOptMaxGapSize="), CmdLineParameters.SeekOptParams.MaxGapSize); FParse::Value(CmdLine, TEXT("-patchSeekOptMaxAdjacentOrderDiff="), CmdLineParameters.SeekOptParams.MaxAdjacentOrderDiff); // For legacy reasons, if we specify a max gap size without a mode, we default to OnePass if (CmdLineParameters.SeekOptParams.MaxGapSize > 0) { CmdLineParameters.SeekOptParams.Mode = ESeekOptMode::OnePass; } FParse::Value(CmdLine, TEXT("-patchSeekOptMode="), (int32&)CmdLineParameters.SeekOptParams.Mode); FParse::Value(CmdLine, TEXT("csv="), CmdLineParameters.CsvPath); FParse::Value(FCommandLine::Get(), TEXT("ProjectStore="), CmdLineParameters.ProjectStoreFilename); } void ProcessPakFileSpecificCommandLine(const TCHAR* CmdLine, const TArray& NonOptionArguments, TArray& Entries, FPakCommandLineParameters& CmdLineParameters) { FString ResponseFile; if (FParse::Value(CmdLine, TEXT("-create="), ResponseFile)) { CmdLineParameters.GeneratePatch = FParse::Value(CmdLine, TEXT("-generatepatch="), CmdLineParameters.SourcePatchPakFilename); FParse::Value(CmdLine, TEXT("-outputchangedfiles="), CmdLineParameters.ChangedFilesOutputFilename); bool bCompress = FParse::Param(CmdLine, TEXT("compress")); if ( bCompress ) { // the correct way to enable compression is via bCompressed in UProjectPackagingSettings // which passes -compressed to CopyBuildToStaging and writes the response file UE_LOG(LogPakFile, Warning, TEXT("-compress is deprecated, use -compressed with UAT instead")); } bool bEncrypt = FParse::Param(CmdLine, TEXT("encrypt")); // if the response file is a pak file, then this is the pak file we want to use as the source if (ResponseFile.EndsWith(TEXT(".pak"), ESearchCase::IgnoreCase) && CmdLineParameters.GeneratePatch) { FString OutputPath; if (FParse::Value(CmdLine, TEXT("extractedpaktemp="), OutputPath) == false) { UE_LOG(LogPakFile, Error, TEXT("-extractedpaktemp= not specified. Required when specifying pak file as the response file."), *ResponseFile); } FString ExtractedPakKeysFile; FKeyChain ExtractedPakKeys; if ( FParse::Value(CmdLine, TEXT("extractedpakcryptokeys="), ExtractedPakKeysFile) ) { KeyChainUtilities::LoadKeyChainFromFile(ExtractedPakKeysFile, ExtractedPakKeys); KeyChainUtilities::ApplyEncryptionKeys(ExtractedPakKeys); } TMap FileHashes; ExtractFilesFromPak(*ResponseFile, FileHashes, *OutputPath, true, ExtractedPakKeys, nullptr, &Entries, nullptr, nullptr, nullptr, nullptr); } else { TArray Lines; bool bParseLines = true; if (IFileManager::Get().DirectoryExists(*ResponseFile)) { IFileManager::Get().FindFilesRecursive(Lines, *ResponseFile, TEXT("*"), true, false); bParseLines = false; } else { TRACE_CPUPROFILER_EVENT_SCOPE(LoadResponseFile); FString Text; UE_LOG(LogPakFile, Display, TEXT("Loading response file %s"), *ResponseFile); if (FFileHelper::LoadFileToString(Text, *ResponseFile)) { // Remove all carriage return characters. Text.ReplaceInline(TEXT("\r"), TEXT("")); // Read all lines Text.ParseIntoArray(Lines, TEXT("\n"), true); } else { UE_LOG(LogPakFile, Error, TEXT("Failed to load %s"), *ResponseFile); } } TRACE_CPUPROFILER_EVENT_SCOPE(AddEntries); Entries.Reserve(Lines.Num()); FString NextToken; for (int32 EntryIndex = 0; EntryIndex < Lines.Num(); EntryIndex++) { FPakInputPair& Input = Entries.AddDefaulted_GetRef(); if (bParseLines) { TRACE_CPUPROFILER_EVENT_SCOPE(CommandLineParseHelper); bool bHasParsedSource = false; bool bHasParsedDest = false; const TCHAR* LinePtr = *Lines[EntryIndex]; while (FParse::Token(LinePtr, NextToken, false)) { FStringView TokenView(NextToken); if (TokenView[0] == TCHAR('-')) { FStringView Switch = TokenView.Mid(1); if (Switch == TEXT("compress")) { Input.bNeedsCompression = true; } else if (Switch == TEXT("encrypt")) { Input.bNeedEncryption = true; } else if (Switch == TEXT("delete")) { Input.bIsDeleteRecord = true; } else if (Switch == TEXT("rehydrate")) { Input.bNeedRehydration = true; CmdLineParameters.bRequiresRehydration = true; } } else { if (!bHasParsedSource) { Input.Source = MoveTemp(NextToken); bHasParsedSource = true; } else if (!bHasParsedDest) { Input.Dest = MoveTemp(NextToken); bHasParsedDest = true; } } } if (!bHasParsedSource) { Entries.Pop(EAllowShrinking::No); continue; } if (!bHasParsedDest || Input.bIsDeleteRecord) { Input.Dest = Input.Source; } } else { Input.Source = MoveTemp(Lines[EntryIndex]); Input.Dest = Input.Source; } { TRACE_CPUPROFILER_EVENT_SCOPE(OtherPathStuff); FPaths::NormalizeFilename(Input.Source); FPaths::NormalizeFilename(Input.Dest); } Input.bNeedsCompression |= bCompress; Input.bNeedEncryption |= bEncrypt; UE_LOG(LogPakFile, Verbose, TEXT("Added file Source: %s Dest: %s"), *Input.Source, *Input.Dest); bool bIsMappedBulk = Input.Source.EndsWith(TEXT(".m.ubulk")); if (bIsMappedBulk && CmdLineParameters.AlignForMemoryMapping > 0 && Input.bNeedsCompression && !Input.bNeedEncryption) // if it is encrypted, we will compress it anyway since it won't be mapped at runtime { // no compression for bulk aligned files because they are memory mapped Input.bNeedsCompression = false; UE_LOG(LogPakFile, Verbose, TEXT("Stripped compression from %s for memory mapping."), *Input.Dest); } } } UE_LOG(LogPakFile, Display, TEXT("Added %d entries to add to pak file."), Entries.Num()); } else { // Override destination path. FString MountPoint; FParse::Value(CmdLine, TEXT("-dest="), MountPoint); FPaths::NormalizeFilename(MountPoint); FPakFile::MakeDirectoryFromPath(MountPoint); // Parse command line params. The first param after the program name is the created pak name for (int32 Index = 1; Index < NonOptionArguments.Num(); Index++) { // Skip switches and add everything else to the Entries array FPakInputPair Input; Input.Source = *NonOptionArguments[Index]; FPaths::NormalizeFilename(Input.Source); if (MountPoint.Len() > 0) { FString SourceDirectory( FPaths::GetPath(Input.Source) ); FPakFile::MakeDirectoryFromPath(SourceDirectory); Input.Dest = Input.Source.Replace(*SourceDirectory, *MountPoint, ESearchCase::IgnoreCase); } else { Input.Dest = FPaths::GetPath(Input.Source); FPakFile::MakeDirectoryFromPath(Input.Dest); } FPaths::NormalizeFilename(Input.Dest); Entries.Add(Input); } } } void ProcessCommandLine(const TCHAR* CmdLine, const TArray& NonOptionArguments, TArray& Entries, FPakCommandLineParameters& CmdLineParameters) { ProcessCommonCommandLine(CmdLine, CmdLineParameters); ProcessPakFileSpecificCommandLine(CmdLine, NonOptionArguments, Entries, CmdLineParameters); } bool InitializeVirtualizationSystem() { if (UE::Virtualization::IVirtualizationSystem::IsInitialized()) { return true; } UE_LOG(LogPakFile, Display, TEXT("Initializing the virtualization system...")); if (FModuleManager::Get().LoadModule(TEXT("Virtualization"), ELoadModuleFlags::LogFailures) == nullptr) { UE_LOG(LogPakFile, Error, TEXT("Failed to load the virtualization module")); return false; } IPluginManager& PluginMgr = IPluginManager::Get(); const FString PerforcePluginPath = FPaths::EnginePluginsDir() / TEXT("Developer/PerforceSourceControl/PerforceSourceControl.uplugin"); FText ErrorMsg; if (!PluginMgr.AddToPluginsList(PerforcePluginPath, &ErrorMsg)) { UE_LOG(LogPakFile, Error, TEXT("Failed to find 'PerforceSourceControl' plugin due to: %s"), *ErrorMsg.ToString()); return false; } PluginMgr.MountNewlyCreatedPlugin(TEXT("PerforceSourceControl")); TSharedPtr Plugin = PluginMgr.FindPlugin(TEXT("PerforceSourceControl")); if (Plugin == nullptr || !Plugin->IsEnabled()) { UE_LOG(LogPakFile, Error, TEXT("The 'PerforceSourceControl' plugin is disabled.")); return false; } FConfigFile Config; const FString ProjectPath = FPaths::GetPath(FPaths::GetProjectFilePath()); const FString EngineConfigPath = FPaths::Combine(FPaths::EngineDir(), TEXT("Config/")); const FString ProjectConfigPath = FPaths::Combine(ProjectPath, TEXT("Config/")); if (!FConfigCacheIni::LoadExternalIniFile(Config, TEXT("Engine"), *EngineConfigPath, *ProjectConfigPath, true)) { UE_LOG(LogPakFile, Error, TEXT("Failed to load config files for the project '%s"), *ProjectPath); return false; } // We might need to make sure that the DDC is initialized too #if !USE_DDC_FOR_COMPRESSED_FILES GetDerivedDataCacheRef(); #endif //!USE_DDC_FOR_COMPRESSED_FILES UE::Virtualization::FInitParams InitParams(FApp::GetProjectName(), Config); UE::Virtualization::Initialize(InitParams, UE::Virtualization::EInitializationFlags::ForceInitialize); UE_LOG(LogPakFile, Display, TEXT("Virtualization system initialized")); return true; } void CollectFilesToAdd(TArray& OutFilesToAdd, const TArray& InEntries, const FPakOrderMap& OrderMap, const FPakCommandLineParameters& CmdLineParameters) { TRACE_CPUPROFILER_EVENT_SCOPE(CollectFilesToAdd); UE_LOG(LogPakFile, Display, TEXT("Collecting files to add to pak file...")); const double StartTime = FPlatformTime::Seconds(); // Start collecting files TSet AddedFiles; for (int32 Index = 0; Index < InEntries.Num(); Index++) { const FPakInputPair& Input = InEntries[Index]; const FString& Source = Input.Source; bool bCompression = Input.bNeedsCompression; bool bEncryption = Input.bNeedEncryption; bool bRehydrate = Input.bNeedRehydration; if (Input.bIsDeleteRecord) { // just pass through any delete records found in the input OutFilesToAdd.Add(Input); continue; } FString Filename = FPaths::GetCleanFilename(Source); FString Directory = FPaths::GetPath(Source); FPaths::MakeStandardFilename(Directory); FPakFile::MakeDirectoryFromPath(Directory); if (Filename.IsEmpty()) { Filename = TEXT("*.*"); } if ( Filename.Contains(TEXT("*")) ) { // Add multiple files TArray FoundFiles; IFileManager::Get().FindFilesRecursive(FoundFiles, *Directory, *Filename, true, false); FString DestDirectory = FPaths::GetPath(Input.Dest); for (int32 FileIndex = 0; FileIndex < FoundFiles.Num(); FileIndex++) { FPakInputPair FileInput; FileInput.Source = FoundFiles[FileIndex]; FPaths::MakeStandardFilename(FileInput.Source); FileInput.Dest = FPaths::Combine(DestDirectory, FPaths::GetCleanFilename(FileInput.Source)); uint64 FileOrder = OrderMap.GetFileOrder(FileInput.Dest, false, &FileInput.bIsInPrimaryOrder); if(FileOrder != MAX_uint64) { FileInput.SuggestedOrder = FileOrder; } else { // we will put all unordered files at 1 << 28 so that they are before any uexp or ubulk files we assign orders to here FileInput.SuggestedOrder = (1 << 28); // if this is a cook order or an old order it will not have uexp files in it, so we put those in the same relative order after all of the normal files, but before any ubulk files if (FileInput.Dest.EndsWith(TEXT("uexp")) || FileInput.Dest.EndsWith(TEXT("ubulk"))) { FileOrder = OrderMap.GetFileOrder(FPaths::GetBaseFilename(FileInput.Dest, false) + TEXT(".uasset"), false, &FileInput.bIsInPrimaryOrder); if (FileOrder == MAX_uint64) { FileOrder = OrderMap.GetFileOrder(FPaths::GetBaseFilename(FileInput.Dest, false) + TEXT(".umap"), false, &FileInput.bIsInPrimaryOrder); } if (FileInput.Dest.EndsWith(TEXT("uexp"))) { FileInput.SuggestedOrder = ((FileOrder != MAX_uint64) ? FileOrder : 0) + (1 << 29); } else { FileInput.SuggestedOrder = ((FileOrder != MAX_uint64) ? FileOrder : 0) + (1 << 30); } } } FileInput.bNeedsCompression = bCompression; FileInput.bNeedEncryption = bEncryption; FileInput.bNeedRehydration = bRehydrate; if (!AddedFiles.Contains(FileInput.Source)) { OutFilesToAdd.Add(FileInput); AddedFiles.Add(FileInput.Source); } else { int32 FoundIndex; OutFilesToAdd.Find(FileInput,FoundIndex); OutFilesToAdd[FoundIndex].bNeedEncryption |= bEncryption; OutFilesToAdd[FoundIndex].bNeedsCompression |= bCompression; OutFilesToAdd[FoundIndex].bNeedRehydration |= bRehydrate; OutFilesToAdd[FoundIndex].SuggestedOrder = FMath::Min(OutFilesToAdd[FoundIndex].SuggestedOrder, FileInput.SuggestedOrder); } } } else { // Add single file FPakInputPair FileInput; FileInput.Source = Input.Source; FPaths::MakeStandardFilename(FileInput.Source); FileInput.Dest = Input.Dest.EndsWith(TEXT("/")) ? FileInput.Source.Replace(*Directory, *Input.Dest, ESearchCase::IgnoreCase) : Input.Dest; uint64 FileOrder = OrderMap.GetFileOrder(FileInput.Dest, CmdLineParameters.bFallbackOrderForNonUassetFiles, &FileInput.bIsInPrimaryOrder); if (FileOrder != MAX_uint64) { FileInput.SuggestedOrder = FileOrder; } FileInput.bNeedEncryption = bEncryption; FileInput.bNeedsCompression = bCompression; FileInput.bNeedRehydration = bRehydrate; if (AddedFiles.Contains(FileInput.Source)) { int32 FoundIndex; OutFilesToAdd.Find(FileInput, FoundIndex); OutFilesToAdd[FoundIndex].bNeedEncryption |= bEncryption; OutFilesToAdd[FoundIndex].bNeedsCompression |= bCompression; OutFilesToAdd[FoundIndex].bNeedRehydration |= bRehydrate; OutFilesToAdd[FoundIndex].SuggestedOrder = FMath::Min(OutFilesToAdd[FoundIndex].SuggestedOrder, FileInput.SuggestedOrder); } else { OutFilesToAdd.Add(FileInput); AddedFiles.Add(FileInput.Source); } } } // Sort by suggested order then alphabetically struct FInputPairSort { FORCEINLINE bool operator()(const FPakInputPair& A, const FPakInputPair& B) const { return A.bIsDeleteRecord == B.bIsDeleteRecord ? (A.SuggestedOrder == B.SuggestedOrder ? A.Dest < B.Dest : A.SuggestedOrder < B.SuggestedOrder) : A.bIsDeleteRecord < B.bIsDeleteRecord; } }; OutFilesToAdd.Sort(FInputPairSort()); UE_LOG(LogPakFile, Display, TEXT("Collected %d files in %.2lfs."), OutFilesToAdd.Num(), FPlatformTime::Seconds() - StartTime); } bool BufferedCopyFile(FArchive& Dest, FArchive& Source, const FPakFile& PakFile, const FPakEntry& Entry, void* Buffer, int64 BufferSize, const FKeyChain& InKeyChain) { // Align down BufferSize = BufferSize & ~(FAES::AESBlockSize-1); int64 RemainingSizeToCopy = Entry.Size; while (RemainingSizeToCopy > 0) { const int64 SizeToCopy = FMath::Min(BufferSize, RemainingSizeToCopy); // If file is encrypted so we need to account for padding int64 SizeToRead = Entry.IsEncrypted() ? Align(SizeToCopy,FAES::AESBlockSize) : SizeToCopy; Source.Serialize(Buffer,SizeToRead); if (Entry.IsEncrypted()) { const FNamedAESKey* Key = InKeyChain.GetPrincipalEncryptionKey(); check(Key); FAES::DecryptData((uint8*)Buffer, SizeToRead, Key->Key); } Dest.Serialize(Buffer, SizeToCopy); RemainingSizeToCopy -= SizeToRead; } return true; } bool UncompressCopyFile(FArchive& Dest, FArchive& Source, const FPakEntry& Entry, uint8*& PersistentBuffer, int64& BufferSize, const FKeyChain& InKeyChain, const FPakFile& PakFile) { if (Entry.UncompressedSize == 0) { return false; } // The compression block size depends on the bit window that the PAK file was originally created with. Since this isn't stored in the PAK file itself, // we can use FCompression::CompressMemoryBound as a guideline for the max expected size to avoid unncessary reallocations, but we need to make sure // that we check if the actual size is not actually greater (eg. UE-59278). FName EntryCompressionMethod = PakFile.GetInfo().GetCompressionMethod(Entry.CompressionMethodIndex); int32 MaxCompressionBlockSize = FCompression::CompressMemoryBound(EntryCompressionMethod, Entry.CompressionBlockSize); for (const FPakCompressedBlock& Block : Entry.CompressionBlocks) { MaxCompressionBlockSize = FMath::Max(MaxCompressionBlockSize, IntCastChecked(Block.CompressedEnd - Block.CompressedStart)); } int64 WorkingSize = Entry.CompressionBlockSize + MaxCompressionBlockSize; if (BufferSize < WorkingSize) { PersistentBuffer = (uint8*)FMemory::Realloc(PersistentBuffer, WorkingSize); BufferSize = WorkingSize; } uint8* UncompressedBuffer = PersistentBuffer+MaxCompressionBlockSize; for (uint32 BlockIndex=0, BlockIndexNum=Entry.CompressionBlocks.Num(); BlockIndex < BlockIndexNum; ++BlockIndex) { int64 CompressedBlockSize = Entry.CompressionBlocks[BlockIndex].CompressedEnd - Entry.CompressionBlocks[BlockIndex].CompressedStart; int64 UncompressedBlockSize = FMath::Min(Entry.UncompressedSize - Entry.CompressionBlockSize*BlockIndex, Entry.CompressionBlockSize); Source.Seek(Entry.CompressionBlocks[BlockIndex].CompressedStart + (PakFile.GetInfo().HasRelativeCompressedChunkOffsets() ? Entry.Offset : 0)); int64 SizeToRead = Entry.IsEncrypted() ? Align(CompressedBlockSize, FAES::AESBlockSize) : CompressedBlockSize; Source.Serialize(PersistentBuffer, SizeToRead); if (Entry.IsEncrypted()) { const FNamedAESKey* Key = InKeyChain.GetEncryptionKeys().Find(PakFile.GetInfo().EncryptionKeyGuid); if (Key == nullptr) { Key = InKeyChain.GetPrincipalEncryptionKey(); } check(Key); FAES::DecryptData(PersistentBuffer, SizeToRead, Key->Key); } if (!FCompression::UncompressMemory(EntryCompressionMethod, UncompressedBuffer, IntCastChecked(UncompressedBlockSize), PersistentBuffer, IntCastChecked(CompressedBlockSize))) { return false; } Dest.Serialize(UncompressedBuffer,UncompressedBlockSize); } return true; } TEncryptionInt ParseEncryptionIntFromJson(TSharedPtr InObj, const TCHAR* InName) { FString Base64; if (InObj->TryGetStringField(InName, Base64)) { TArray Bytes; FBase64::Decode(Base64, Bytes); check(Bytes.Num() == sizeof(TEncryptionInt)); return TEncryptionInt((uint32*)&Bytes[0]); } else { return TEncryptionInt(); } } void LoadKeyChain(const TCHAR* CmdLine, FKeyChain& OutCryptoSettings) { OutCryptoSettings.SetSigningKey( InvalidRSAKeyHandle ); OutCryptoSettings.GetEncryptionKeys().Empty(); // First, try and parse the keys from a supplied crypto key cache file FString CryptoKeysCacheFilename; if (FParse::Value(CmdLine, TEXT("cryptokeys="), CryptoKeysCacheFilename)) { UE_LOG(LogPakFile, Display, TEXT("Parsing crypto keys from a crypto key cache file")); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, OutCryptoSettings); } else if (FParse::Param(CmdLine, TEXT("encryptionini"))) { FString ProjectDir, EngineDir, Platform; if (FParse::Value(CmdLine, TEXT("projectdir="), ProjectDir, false) && FParse::Value(CmdLine, TEXT("enginedir="), EngineDir, false) && FParse::Value(CmdLine, TEXT("platform="), Platform, false)) { UE_LOG(LogPakFile, Warning, TEXT("A legacy command line syntax is being used for crypto config. Please update to using the -cryptokey parameter as soon as possible as this mode is deprecated")); FConfigFile EngineConfig; FConfigCacheIni::LoadExternalIniFile(EngineConfig, TEXT("Engine"), *FPaths::Combine(EngineDir, TEXT("Config\\")), *FPaths::Combine(ProjectDir, TEXT("Config/")), true, *Platform); bool bDataCryptoRequired = false; EngineConfig.GetBool(TEXT("PlatformCrypto"), TEXT("PlatformRequiresDataCrypto"), bDataCryptoRequired); if (!bDataCryptoRequired) { return; } FConfigFile ConfigFile; FConfigCacheIni::LoadExternalIniFile(ConfigFile, TEXT("Crypto"), *FPaths::Combine(EngineDir, TEXT("Config\\")), *FPaths::Combine(ProjectDir, TEXT("Config/")), true, *Platform); bool bSignPak = false; bool bEncryptPakIniFiles = false; bool bEncryptPakIndex = false; bool bEncryptAssets = false; bool bEncryptPak = false; if (ConfigFile.Num()) { UE_LOG(LogPakFile, Display, TEXT("Using new format crypto.ini files for crypto configuration")); static const TCHAR* SectionName = TEXT("/Script/CryptoKeys.CryptoKeysSettings"); ConfigFile.GetBool(SectionName, TEXT("bEnablePakSigning"), bSignPak); ConfigFile.GetBool(SectionName, TEXT("bEncryptPakIniFiles"), bEncryptPakIniFiles); ConfigFile.GetBool(SectionName, TEXT("bEncryptPakIndex"), bEncryptPakIndex); ConfigFile.GetBool(SectionName, TEXT("bEncryptAssets"), bEncryptAssets); bEncryptPak = bEncryptPakIniFiles || bEncryptPakIndex || bEncryptAssets; if (bSignPak) { FString PublicExpBase64, PrivateExpBase64, ModulusBase64; ConfigFile.GetString(SectionName, TEXT("SigningPublicExponent"), PublicExpBase64); ConfigFile.GetString(SectionName, TEXT("SigningPrivateExponent"), PrivateExpBase64); ConfigFile.GetString(SectionName, TEXT("SigningModulus"), ModulusBase64); TArray PublicExp, PrivateExp, Modulus; FBase64::Decode(PublicExpBase64, PublicExp); FBase64::Decode(PrivateExpBase64, PrivateExp); FBase64::Decode(ModulusBase64, Modulus); OutCryptoSettings.SetSigningKey(FRSA::CreateKey(PublicExp, PrivateExp, Modulus)); UE_LOG(LogPakFile, Display, TEXT("Parsed signature keys from config files.")); } if (bEncryptPak) { FString EncryptionKeyString; ConfigFile.GetString(SectionName, TEXT("EncryptionKey"), EncryptionKeyString); if (EncryptionKeyString.Len() > 0) { TArray Key; FBase64::Decode(EncryptionKeyString, Key); check(Key.Num() == sizeof(FAES::FAESKey::Key)); FNamedAESKey NewKey; NewKey.Name = TEXT("Default"); NewKey.Guid = FGuid(); FMemory::Memcpy(NewKey.Key.Key, &Key[0], sizeof(FAES::FAESKey::Key)); OutCryptoSettings.GetEncryptionKeys().Add(NewKey.Guid, NewKey); UE_LOG(LogPakFile, Display, TEXT("Parsed AES encryption key from config files.")); } } } else { static const TCHAR* SectionName = TEXT("Core.Encryption"); UE_LOG(LogPakFile, Display, TEXT("Using old format encryption.ini files for crypto configuration")); FConfigCacheIni::LoadExternalIniFile(ConfigFile, TEXT("Encryption"), *FPaths::Combine(EngineDir, TEXT("Config\\")), *FPaths::Combine(ProjectDir, TEXT("Config/")), true, *Platform); ConfigFile.GetBool(SectionName, TEXT("SignPak"), bSignPak); ConfigFile.GetBool(SectionName, TEXT("EncryptPak"), bEncryptPak); if (bSignPak) { FString RSAPublicExp, RSAPrivateExp, RSAModulus; ConfigFile.GetString(SectionName, TEXT("rsa.publicexp"), RSAPublicExp); ConfigFile.GetString(SectionName, TEXT("rsa.privateexp"), RSAPrivateExp); ConfigFile.GetString(SectionName, TEXT("rsa.modulus"), RSAModulus); //TODO: Fix me! //OutSigningKey.PrivateKey.Exponent.Parse(RSAPrivateExp); //OutSigningKey.PrivateKey.Modulus.Parse(RSAModulus); //OutSigningKey.PublicKey.Exponent.Parse(RSAPublicExp); //OutSigningKey.PublicKey.Modulus = OutSigningKey.PrivateKey.Modulus; UE_LOG(LogPakFile, Display, TEXT("Parsed signature keys from config files.")); } if (bEncryptPak) { FString EncryptionKeyString; ConfigFile.GetString(SectionName, TEXT("aes.key"), EncryptionKeyString); FNamedAESKey NewKey; NewKey.Name = TEXT("Default"); NewKey.Guid = FGuid(); if (EncryptionKeyString.Len() == 32 && TCString::IsPureAnsi(*EncryptionKeyString)) { for (int32 Index = 0; Index < 32; ++Index) { NewKey.Key.Key[Index] = (uint8)EncryptionKeyString[Index]; } OutCryptoSettings.GetEncryptionKeys().Add(NewKey.Guid, NewKey); UE_LOG(LogPakFile, Display, TEXT("Parsed AES encryption key from config files.")); } } } } } else { UE_LOG(LogPakFile, Display, TEXT("Using command line for crypto configuration")); FString EncryptionKeyString; FParse::Value(CmdLine, TEXT("aes="), EncryptionKeyString, false); if (EncryptionKeyString.Len() > 0) { UE_LOG(LogPakFile, Warning, TEXT("A legacy command line syntax is being used for crypto config. Please update to using the -cryptokey parameter as soon as possible as this mode is deprecated")); FNamedAESKey NewKey; NewKey.Name = TEXT("Default"); NewKey.Guid = FGuid(); const uint32 RequiredKeyLength = sizeof(NewKey.Key); // Error checking if (EncryptionKeyString.Len() < RequiredKeyLength) { UE_LOG(LogPakFile, Fatal, TEXT("AES encryption key must be %d characters long"), RequiredKeyLength); } if (EncryptionKeyString.Len() > RequiredKeyLength) { UE_LOG(LogPakFile, Warning, TEXT("AES encryption key is more than %d characters long, so will be truncated!"), RequiredKeyLength); EncryptionKeyString.LeftInline(RequiredKeyLength); } if (!FCString::IsPureAnsi(*EncryptionKeyString)) { UE_LOG(LogPakFile, Fatal, TEXT("AES encryption key must be a pure ANSI string!")); } const auto AsAnsi = StringCast(*EncryptionKeyString); check(AsAnsi.Length() == RequiredKeyLength); FMemory::Memcpy(NewKey.Key.Key, AsAnsi.Get(), RequiredKeyLength); OutCryptoSettings.GetEncryptionKeys().Add(NewKey.Guid, NewKey); UE_LOG(LogPakFile, Display, TEXT("Parsed AES encryption key from command line.")); } } FString EncryptionKeyOverrideGuidString; FGuid EncryptionKeyOverrideGuid; if (FParse::Value(CmdLine, TEXT("EncryptionKeyOverrideGuid="), EncryptionKeyOverrideGuidString)) { FGuid::Parse(EncryptionKeyOverrideGuidString, EncryptionKeyOverrideGuid); } OutCryptoSettings.SetPrincipalEncryptionKey(OutCryptoSettings.GetEncryptionKeys().Find(EncryptionKeyOverrideGuid)); } /** * Creates a pak file writer. This can be a signed writer if the encryption keys are specified in the command line */ FArchive* CreatePakWriter(const TCHAR* Filename, const FKeyChain& InKeyChain, bool bSign) { if (IFileManager::Get().FileExists(Filename)) { UE_LOG(LogPakFile, Error, TEXT("Failed to create pak at %s. File already exists at that location."), Filename); return nullptr; } FArchive* Writer = IFileManager::Get().CreateFileWriter(Filename); if (Writer) { if (bSign) { UE_LOG(LogPakFile, Display, TEXT("Creating signed pak %s."), Filename); Writer = new FSignedArchiveWriter(*Writer, Filename, InKeyChain.GetSigningKey()); } else { UE_LOG(LogPakFile, Display, TEXT("Creating pak %s."), Filename); } } return Writer; } /* Helper for index creation in CreatePakFile. Used to (conditionally) write Bytes into a SecondaryIndex and serialize into the PrimaryIndex the offset of the SecondaryIndex */ struct FSecondaryIndexWriter { private: TArray& SecondaryIndexData; FMemoryWriter SecondaryWriter; TArray& PrimaryIndexData; FMemoryWriter& PrimaryIndexWriter; FSHAHash SecondaryHash; int64 OffsetToDataInPrimaryIndex = INDEX_NONE; int64 OffsetToSecondaryInPakFile = INDEX_NONE; int64 SecondarySize = 0; bool bShouldWrite; public: FSecondaryIndexWriter(TArray& InSecondaryIndexData, bool bInShouldWrite, TArray& InPrimaryIndexData, FMemoryWriter& InPrimaryIndexWriter) : SecondaryIndexData(InSecondaryIndexData) , SecondaryWriter(SecondaryIndexData) , PrimaryIndexData(InPrimaryIndexData) , PrimaryIndexWriter(InPrimaryIndexWriter) , bShouldWrite(bInShouldWrite) { if (bShouldWrite) { SecondaryWriter.SetByteSwapping(PrimaryIndexWriter.ForceByteSwapping()); } } /** * Write the condition flag and the Offset,Size,Hash into the primary index. Offset of the Secondary index cannot be calculated until the PrimaryIndex is done writing later, * so placeholders are left instead, with a marker to rewrite them later. */ void WritePlaceholderToPrimary() { PrimaryIndexWriter << bShouldWrite; if (bShouldWrite) { OffsetToDataInPrimaryIndex = PrimaryIndexData.Num(); PrimaryIndexWriter << OffsetToSecondaryInPakFile; PrimaryIndexWriter << SecondarySize; PrimaryIndexWriter << SecondaryHash; } } FMemoryWriter& GetSecondaryWriter() { return SecondaryWriter; } /** The caller has populated this->SecondaryIndexData using GetSecondaryWriter(). We now calculate the size, encrypt, and hash, and store the Offset,Size,Hash in the PrimaryIndex */ void FinalizeAndRecordOffset(int64 OffsetInPakFile, const TFunction & IndexData, FSHAHash & OutHash)>& FinalizeIndexBlock) { if (!bShouldWrite) { return; } FinalizeIndexBlock(SecondaryIndexData, SecondaryHash); SecondarySize = SecondaryIndexData.Num(); OffsetToSecondaryInPakFile = OffsetInPakFile; PrimaryIndexWriter.Seek(OffsetToDataInPrimaryIndex); PrimaryIndexWriter << OffsetToSecondaryInPakFile; PrimaryIndexWriter << SecondarySize; PrimaryIndexWriter << SecondaryHash; } }; /* Verify that Indexes constructed for serialization into the PakFile match the originally collected list of FPakEntryPairs */ void VerifyIndexesMatch(TArray& EntryList, FPakFile::FDirectoryIndex& DirectoryIndex, FPakFile::FPathHashIndex& PathHashIndex, uint64 PathHashSeed, const FString& MountPoint, const TArray& EncodedPakEntries, const TArray& NonEncodableEntries, int32 NumEncodedEntries, int32 NumDeletedEntries, FPakInfo& Info) { check(NumEncodedEntries + NonEncodableEntries.Num() + NumDeletedEntries == EntryList.Num()); FPakEntry EncodedEntry; for (FPakEntryPair& Pair : EntryList) { FString FullPath = FPaths::Combine(MountPoint, Pair.Filename); const FPakEntryLocation* PakEntryLocation = FPakFile::FindLocationFromIndex(FullPath, MountPoint, DirectoryIndex); if (!PakEntryLocation) { check(false); continue; } const FPakEntryLocation* PathHashLocation = FPakFile::FindLocationFromIndex(FullPath, MountPoint, PathHashIndex, PathHashSeed, Info.Version); if (!PathHashLocation) { check(false); continue; } check(*PakEntryLocation == *PathHashLocation); check(FPakFile::GetPakEntry(*PakEntryLocation, &EncodedEntry, EncodedPakEntries, NonEncodableEntries, Info) != FPakFile::EFindResult::NotFound); check(Pair.Info.IsDeleteRecord() == EncodedEntry.IsDeleteRecord()); check(Pair.Info.IsDeleteRecord() || Pair.Info.IndexDataEquals(EncodedEntry)); } }; class FPakWriterContext { public: bool Initialize(const FPakCommandLineParameters& InCmdLineParameters); bool AddPakFile(const TCHAR* Filename, const TArray& FilesToAdd, const FKeyChain& InKeyChain); bool Flush(); private: struct FOutputPakFile; struct FOutputPakFileEntry { FOutputPakFile* PakFile = nullptr; FPakInputPair InputPair; int64 OriginalFileSize = 0; FOutputPakFileEntry* UExpFileInPair = nullptr; bool bIsUAssetUExpPairUAsset = false; bool bIsUAssetUExpPairUExp = false; bool bIsMappedBulk = false; bool bIsLastFileInPak = false; bool bSomeCompressionSucceeded = false; bool bCopiedToPak = false; FPakEntryPair Entry; FName CompressionMethod = NAME_None; int64 RealFileSize = 0; TUniquePtr MemoryCompressor; FGraphEventRef BeginCompressionBarrier; FGraphEventRef BeginCompressionTask; FGraphEventRef EndCompressionBarrier; FGraphEventRef EndCompressionTask; FCompressedFileBuffer& AccessCompressedBuffer() { return CompressedFileBuffer; } const FCompressedFileBuffer& GetCompressedBuffer() const { return CompressedFileBuffer; } private: FCompressedFileBuffer CompressedFileBuffer; }; struct FOutputPakFile { FString Filename; FKeyChain KeyChain; TUniquePtr PakFileHandle; TUniquePtr PakFileRegionsHandle; TArray Entries; FPakInfo Info; FString MountPoint; TArray Index; // Some platforms provide patch download size reduction by diffing the patch files. However, they often operate on specific block // sizes when dealing with new data within the file. Pad files out to the given alignment to work with these systems more nicely. // We also want to combine smaller files into the same padding size block so we don't waste as much space. i.e. grouping 64 1k files together // rather than padding each out to 64k. uint64 ContiguousTotalSizeSmallerThanBlockSize = 0; uint64 ContiguousFilesSmallerThanBlockSize = 0; TArray AllFileRegions; // Stats uint64 TotalUncompressedSize = 0; uint64 TotalCompressedSize = 0; TArray Compressor_Stat_Count; TArray Compressor_Stat_RawBytes; TArray Compressor_Stat_CompBytes; uint64 TotalRequestedEncryptedFiles = 0; uint64 TotalEncryptedFiles = 0; uint64 TotalEncryptedDataSize = 0; FEventRef AllEntriesRetiredEvent; int32 RetiredEntriesCount = 0; uint64 RehydratedCount = 0; uint64 RehydratedBytes = 0; }; void BeginCompress(FOutputPakFileEntry* Entry); void EndCompress(FOutputPakFileEntry* Entry); void Retire(FOutputPakFileEntry* Entry); void CompressionThreadFunc(); void WriterThreadFunc(); FPakCommandLineParameters CmdLineParameters; TSet NoPluginCompressionFileNames; TSet NoPluginCompressionExtensions; TArray64 PaddingBuffer; TArray> OutputPakFiles; TArray CompressionFormatsAndNone; TAtomic TotalFilesWithPoorForcedCompression{ 0 }; TAtomic TotalExtraMemoryForPoorForcedCompression{ 0 }; TSpscQueue CompressionQueue; TSpscQueue WriteQueue; TFuture CompressionThread; TFuture WriterThread; TUniquePtr PackageStore; FEventRef CompressionQueueEntryAddedEvent; FEventRef WriteQueueEntryAddedEvent; FEventRef EntryRetiredEvent; TAtomic ScheduledFileSize{ 0 }; TAtomic bFlushed{ false }; uint64 TotalEntriesCount = 0; TAtomic RetiredEntriesCount{ 0 }; }; bool FPakWriterContext::Initialize(const FPakCommandLineParameters& InCmdLineParameters) { #if USE_DDC_FOR_COMPRESSED_FILES GetDerivedDataCacheRef(); #endif CmdLineParameters = InCmdLineParameters; int64 PaddingBufferSize = 64 * 1024; PaddingBufferSize = FMath::Max(PaddingBufferSize, CmdLineParameters.AlignForMemoryMapping); PaddingBufferSize = FMath::Max(PaddingBufferSize, CmdLineParameters.PatchFilePadAlign); PaddingBuffer.SetNumZeroed(PaddingBufferSize); // track compression stats per format : CompressionFormatsAndNone = CmdLineParameters.CompressionFormats; CompressionFormatsAndNone.AddUnique(NAME_None); { // log the methods and indexes // we're going to prefer to use only index [0] so we want that to be our favorite compressor FString FormatLogLine(TEXT("CompressionFormats in priority order: ")); for (int32 MethodIndex = 0; MethodIndex < CmdLineParameters.CompressionFormats.Num(); MethodIndex++) { FName CompressionMethod = CompressionFormatsAndNone[MethodIndex]; if ( MethodIndex > 0 ) { FormatLogLine += TEXT(", "); } FormatLogLine += CompressionMethod.ToString(); } UE_LOG(LogPakFile, Display, TEXT("%s"), *FormatLogLine); } if (!CmdLineParameters.ProjectStoreFilename.IsEmpty()) { TUniquePtr NewPackageStore = MakeUnique(FPaths::GetPath(CmdLineParameters.ProjectStoreFilename)); FIoStatus Status = NewPackageStore->LoadProjectStore(*CmdLineParameters.ProjectStoreFilename); if (Status.IsOk()) { PackageStore = MoveTemp(NewPackageStore); } else { UE_LOG(LogPakFile, Fatal, TEXT("Failed loading project store '%s'"), *CmdLineParameters.ProjectStoreFilename); } } // Oodle is built into the Engine now and can be used to decode startup phase files (ini,res,uplugin) // which used to be forced to Zlib // set bDoUseOodleDespiteNoPluginCompression = true to let Oodle compress those files bool bDoUseOodleDespiteNoPluginCompression = false; GConfig->GetBool(TEXT("Pak"), TEXT("bDoUseOodleDespiteNoPluginCompression"), bDoUseOodleDespiteNoPluginCompression, GEngineIni); if ( bDoUseOodleDespiteNoPluginCompression && CompressionFormatsAndNone[0] == NAME_Oodle ) { // NoPluginCompression sets are empty UE_LOG(LogPakFile, Display, TEXT("Oodle enabled on 'NoPluginCompression' files")); } else { TArray ExtensionsToNotUsePluginCompression; GConfig->GetArray(TEXT("Pak"), TEXT("ExtensionsToNotUsePluginCompression"), ExtensionsToNotUsePluginCompression, GEngineIni); for (const FString& Ext : ExtensionsToNotUsePluginCompression) { NoPluginCompressionExtensions.Add(Ext); } TArray FileNamesToNotUsePluginCompression; GConfig->GetArray(TEXT("Pak"), TEXT("FileNamesToNotUsePluginCompression"), FileNamesToNotUsePluginCompression, GEngineIni); for (const FString& FileName : FileNamesToNotUsePluginCompression) { NoPluginCompressionFileNames.Add(FileName); } } CompressionThread = Async(EAsyncExecution::Thread, [this]() { CompressionThreadFunc(); }); WriterThread = Async(EAsyncExecution::Thread, [this]() { WriterThreadFunc(); }); return true; } bool FPakWriterContext::AddPakFile(const TCHAR* Filename, const TArray& FilesToAdd, const FKeyChain& InKeyChain) { TRACE_CPUPROFILER_EVENT_SCOPE(AddPakFile); check(!bFlushed); TUniquePtr OutputPakFile = MakeUnique(); OutputPakFile->Filename = Filename; OutputPakFile->KeyChain = InKeyChain; { TRACE_CPUPROFILER_EVENT_SCOPE(CreateFileHandle); OutputPakFile->PakFileHandle.Reset(CreatePakWriter(Filename, InKeyChain, CmdLineParameters.bSign)); } if (!OutputPakFile->PakFileHandle) { UE_LOG(LogPakFile, Error, TEXT("Unable to create pak file \"%s\"."), Filename); return false; } if (CmdLineParameters.bFileRegions) { FString RegionsFilename = FString(Filename) + FFileRegion::RegionsFileExtension; { TRACE_CPUPROFILER_EVENT_SCOPE(CreateRegionsFileHandle); OutputPakFile->PakFileRegionsHandle.Reset(IFileManager::Get().CreateFileWriter(*RegionsFilename)); } if (!OutputPakFile->PakFileRegionsHandle) { UE_LOG(LogPakFile, Error, TEXT("Unable to create pak regions file \"%s\"."), *RegionsFilename); return false; } } if (InKeyChain.GetPrincipalEncryptionKey()) { UE_LOG(LogPakFile, Display, TEXT("Using encryption key '%s' [%s]"), *InKeyChain.GetPrincipalEncryptionKey()->Name, *InKeyChain.GetPrincipalEncryptionKey()->Guid.ToString()); } OutputPakFile->Info.bEncryptedIndex = (InKeyChain.GetPrincipalEncryptionKey() && CmdLineParameters.EncryptIndex); OutputPakFile->Info.EncryptionKeyGuid = InKeyChain.GetPrincipalEncryptionKey() ? InKeyChain.GetPrincipalEncryptionKey()->Guid : FGuid(); if (CmdLineParameters.bPatchCompatibilityMode421) { // for old versions, put in some known names that we may have used OutputPakFile->Info.GetCompressionMethodIndex(NAME_None); OutputPakFile->Info.GetCompressionMethodIndex(NAME_Zlib); OutputPakFile->Info.GetCompressionMethodIndex(NAME_Gzip); OutputPakFile->Info.GetCompressionMethodIndex(TEXT("Bogus")); OutputPakFile->Info.GetCompressionMethodIndex(TEXT("Oodle")); } OutputPakFile->Compressor_Stat_Count.SetNumZeroed(CompressionFormatsAndNone.Num()); OutputPakFile->Compressor_Stat_RawBytes.SetNumZeroed(CompressionFormatsAndNone.Num()); OutputPakFile->Compressor_Stat_CompBytes.SetNumZeroed(CompressionFormatsAndNone.Num()); OutputPakFile->MountPoint = GetCommonRootPath(FilesToAdd); OutputPakFile->Entries.SetNum(FilesToAdd.Num()); for (int32 FileIndex = 0; FileIndex < FilesToAdd.Num(); FileIndex++) { FOutputPakFileEntry& OutputEntry = OutputPakFile->Entries[FileIndex]; OutputEntry.PakFile = OutputPakFile.Get(); OutputEntry.InputPair = FilesToAdd[FileIndex]; OutputEntry.bIsMappedBulk = FilesToAdd[FileIndex].Source.EndsWith(TEXT(".m.ubulk")); OutputEntry.bIsLastFileInPak = FileIndex + 1 == FilesToAdd.Num(); if (FileIndex > 0) { if (FPaths::GetBaseFilename(FilesToAdd[FileIndex - 1].Dest, false) == FPaths::GetBaseFilename(FilesToAdd[FileIndex].Dest, false) && FPaths::GetExtension(FilesToAdd[FileIndex - 1].Dest, true) == TEXT(".uasset") && FPaths::GetExtension(FilesToAdd[FileIndex].Dest, true) == TEXT(".uexp")) { OutputEntry.bIsUAssetUExpPairUExp = true; } } if (!OutputEntry.bIsUAssetUExpPairUExp && FileIndex + 1 < FilesToAdd.Num()) { if (FPaths::GetBaseFilename(FilesToAdd[FileIndex].Dest, false) == FPaths::GetBaseFilename(FilesToAdd[FileIndex + 1].Dest, false) && FPaths::GetExtension(FilesToAdd[FileIndex].Dest, true) == TEXT(".uasset") && FPaths::GetExtension(FilesToAdd[FileIndex + 1].Dest, true) == TEXT(".uexp")) { OutputEntry.bIsUAssetUExpPairUAsset = true; OutputEntry.UExpFileInPair = &OutputPakFile->Entries[FileIndex + 1]; } } OutputEntry.BeginCompressionBarrier = FGraphEvent::CreateGraphEvent(); OutputEntry.BeginCompressionTask = FFunctionGraphTask::CreateAndDispatchWhenReady([this, &OutputEntry]() { BeginCompress(&OutputEntry); }, TStatId(), OutputEntry.BeginCompressionBarrier, ENamedThreads::AnyHiPriThreadHiPriTask); OutputEntry.EndCompressionBarrier = FGraphEvent::CreateGraphEvent(); OutputEntry.EndCompressionTask = FFunctionGraphTask::CreateAndDispatchWhenReady([this, &OutputEntry]() { EndCompress(&OutputEntry); }, TStatId(), OutputEntry.EndCompressionBarrier, ENamedThreads::AnyHiPriThreadHiPriTask); CompressionQueue.Enqueue(&OutputEntry); WriteQueue.Enqueue(&OutputEntry); } CompressionQueueEntryAddedEvent->Trigger(); WriteQueueEntryAddedEvent->Trigger(); TotalEntriesCount += OutputPakFile->Entries.Num(); if (OutputPakFile->Entries.IsEmpty()) { OutputPakFile->AllEntriesRetiredEvent->Trigger(); } OutputPakFiles.Add(MoveTemp(OutputPakFile)); return true; } void FPakWriterContext::BeginCompress(FOutputPakFileEntry* Entry) { TRACE_CPUPROFILER_EVENT_SCOPE(BeginCompress); if (!Entry->AccessCompressedBuffer().ReadSource(Entry->InputPair, PackageStore.Get())) { // TODO: Should we give an error? Entry->bSomeCompressionSucceeded = true; // Prevent loading in EndCompress Entry->EndCompressionBarrier->DispatchSubsequents(); return; } // don't try to compress tiny files // even if they do compress, it is a bad use of decoder time if (Entry->AccessCompressedBuffer().GetFileSize() < 1024 || !Entry->InputPair.bNeedsCompression) { Entry->AccessCompressedBuffer().SetSourceAsWorkingBuffer(); Entry->bSomeCompressionSucceeded = true; // Only used for logging purposes Entry->EndCompressionBarrier->DispatchSubsequents(); // TODO: Set Entry->RealFileSize or change to accessor? return; } // if first method (oodle) doesn't compress, don't try other methods (zlib) // if Oodle refused to compress it was not an error, it was a choice because Oodle didn't think // compressing that file was worth the decode time // we do NOT want Zlib to then get enabled for that file! const int32 MethodIndex = 0; { Entry->CompressionMethod = CmdLineParameters.CompressionFormats[MethodIndex]; // because compression is a plugin, certain files need to be loadable out of pak files before plugins are loadable // (like .uplugin files). for these, we enforce a non-plugin compression - zlib // note that those file types are also excluded from iostore, so still go through this pak system if (NoPluginCompressionExtensions.Find(FPaths::GetExtension(Entry->InputPair.Source)) != nullptr || NoPluginCompressionFileNames.Find(FPaths::GetCleanFilename(Entry->InputPair.Source)) != nullptr) { Entry->CompressionMethod = NAME_Zlib; } } // attempt to compress the data Entry->MemoryCompressor = Entry->AccessCompressedBuffer().BeginCompressFileToWorkingBuffer(Entry->InputPair, Entry->CompressionMethod, CmdLineParameters.CompressionBlockSize, Entry->EndCompressionBarrier); if (Entry->MemoryCompressor != nullptr) { Entry->MemoryCompressor->StartWork(); } } void FPakWriterContext::EndCompress(FOutputPakFileEntry* Entry) { TRACE_CPUPROFILER_EVENT_SCOPE(EndCompress); const int32 MethodIndex = 0; { if (Entry->MemoryCompressor && Entry->AccessCompressedBuffer().EndCompressFileToWorkingBuffer(Entry->InputPair, Entry->CompressionMethod, CmdLineParameters.CompressionBlockSize, *Entry->MemoryCompressor.Get())) { // for modern compressors we don't want any funny heuristics turning compression on/off // let the compressor decide; it has an introspective measure of whether compression is worth doing or not bool bNotEnoughCompression = Entry->GetCompressedBuffer().TotalCompressedSize >= Entry->OriginalFileSize; if (Entry->CompressionMethod == NAME_Zlib) { // for forced-Zlib files still use the old heuristic : // Zlib must save at least 1K regardless of percentage (for small files) bNotEnoughCompression = (Entry->OriginalFileSize - Entry->GetCompressedBuffer().TotalCompressedSize) < 1024; if (!bNotEnoughCompression) { // Check the compression ratio, if it's too low just store uncompressed. Also take into account read size // if we still save 64KB it's probably worthwhile compressing, as that saves a file read operation in the runtime. // TODO: drive this threshold from the command line float PercentLess = ((float)Entry->GetCompressedBuffer().TotalCompressedSize / ((float)Entry->OriginalFileSize / 100.f)); bNotEnoughCompression = (PercentLess > 90.f) && ((Entry->OriginalFileSize - Entry->GetCompressedBuffer().TotalCompressedSize) < 65536); } } const bool bIsLastCompressionFormat = MethodIndex == CmdLineParameters.CompressionFormats.Num() - 1; if (bNotEnoughCompression && (!CmdLineParameters.bForceCompress || !bIsLastCompressionFormat)) { // compression did not succeed, we can try the next format, so do nothing here } else { Entry->Entry.Info.CompressionBlocks.AddUninitialized(Entry->GetCompressedBuffer().CompressedBlocks.Num()); Entry->RealFileSize = Entry->GetCompressedBuffer().TotalCompressedSize + Entry->Entry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest); Entry->Entry.Info.CompressionBlocks.Reset(); // at this point, we have successfully compressed the file, no need to continue Entry->bSomeCompressionSucceeded = true; if (bNotEnoughCompression) { // None of the compression formats were good enough, but we were under instructions to use some form of compression // This was likely to aid with runtime error checking on mobile devices. Record how many cases of this we encountered and // How much the size difference was TotalFilesWithPoorForcedCompression++; TotalExtraMemoryForPoorForcedCompression += FMath::Max(Entry->GetCompressedBuffer().TotalCompressedSize - Entry->OriginalFileSize, (int64)0); } } } } Entry->MemoryCompressor.Reset(); // If no compression was able to make it small enough, or compress at all, don't compress it if (Entry->bSomeCompressionSucceeded) { Entry->AccessCompressedBuffer().ResetSource(); } else { Entry->CompressionMethod = NAME_None; Entry->AccessCompressedBuffer().SetSourceAsWorkingBuffer(); } } void FPakWriterContext::Retire(FOutputPakFileEntry* Entry) { ++RetiredEntriesCount; Entry->AccessCompressedBuffer().Empty(); ScheduledFileSize -= Entry->OriginalFileSize; EntryRetiredEvent->Trigger(); ++Entry->PakFile->RetiredEntriesCount; if (Entry->PakFile->RetiredEntriesCount == Entry->PakFile->Entries.Num()) { Entry->PakFile->AllEntriesRetiredEvent->Trigger(); } } void FPakWriterContext::CompressionThreadFunc() { const int64 MaxScheduledFileSize = 2ll << 30; for (;;) { TOptional FromQueue = CompressionQueue.Dequeue(); if (FromQueue.IsSet()) { FOutputPakFileEntry* Entry = FromQueue.GetValue(); if (!Entry->OriginalFileSize) { Entry->OriginalFileSize = IFileManager::Get().FileSize(*Entry->InputPair.Source); } Entry->RealFileSize = Entry->OriginalFileSize + Entry->Entry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest); if (Entry->UExpFileInPair) { // Need the size of the uexp as well before we can proceed with this entry Entry->UExpFileInPair->OriginalFileSize = IFileManager::Get().FileSize(*Entry->UExpFileInPair->InputPair.Source); } int64 LocalScheduledFileSize = ScheduledFileSize; if (LocalScheduledFileSize + Entry->OriginalFileSize > MaxScheduledFileSize) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForMemory); while (LocalScheduledFileSize > 0 && LocalScheduledFileSize + Entry->OriginalFileSize > MaxScheduledFileSize) { EntryRetiredEvent->Wait(); LocalScheduledFileSize = ScheduledFileSize; } } ScheduledFileSize += Entry->OriginalFileSize; Entry->BeginCompressionBarrier->DispatchSubsequents(); } else { if (bFlushed) { return; } TRACE_CPUPROFILER_EVENT_SCOPE(WaitingForWork); CompressionQueueEntryAddedEvent->Wait(); } } } void FPakWriterContext::WriterThreadFunc() { const int64 RequiredPatchPadding = CmdLineParameters.PatchFilePadAlign; for (;;) { TOptional FromQueue = WriteQueue.Dequeue(); if (!FromQueue.IsSet()) { if (bFlushed) { return; } TRACE_CPUPROFILER_EVENT_SCOPE(WaitingForWork); WriteQueueEntryAddedEvent->Wait(); continue; } TRACE_CPUPROFILER_EVENT_SCOPE(ProcessFile); FOutputPakFileEntry* OutputEntry = FromQueue.GetValue(); bool bDeleted = OutputEntry->InputPair.bIsDeleteRecord; // Remember the offset but don't serialize it with the entry header. FArchive* PakFileHandle = OutputEntry->PakFile->PakFileHandle.Get(); int64 NewEntryOffset = PakFileHandle->Tell(); FPakEntryPair& NewEntry = OutputEntry->Entry; if (!OutputEntry->EndCompressionTask->IsComplete()) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForCompression); OutputEntry->EndCompressionTask->Wait(); } const FName& CompressionMethod = OutputEntry->CompressionMethod; const FCompressedFileBuffer& CompressedFileBuffer = OutputEntry->GetCompressedBuffer(); if (!bDeleted) { const int64& RealFileSize = OutputEntry->RealFileSize; const int64& OriginalFileSize = OutputEntry->OriginalFileSize; const int32 CompressionBlockSize = CmdLineParameters.CompressionBlockSize; NewEntry.Info.CompressionMethodIndex = OutputEntry->PakFile->Info.GetCompressionMethodIndex(CompressionMethod); // Account for file system block size, which is a boundary we want to avoid crossing. if (!OutputEntry->bIsUAssetUExpPairUExp && // don't split uexp / uasset pairs CmdLineParameters.FileSystemBlockSize > 0 && OriginalFileSize != INDEX_NONE && (CmdLineParameters.bAlignFilesLargerThanBlock || RealFileSize <= CmdLineParameters.FileSystemBlockSize) && (NewEntryOffset / CmdLineParameters.FileSystemBlockSize) != ((NewEntryOffset + RealFileSize - 1) / CmdLineParameters.FileSystemBlockSize)) // File crosses a block boundary { int64 OldOffset = NewEntryOffset; NewEntryOffset = AlignArbitrary(NewEntryOffset, CmdLineParameters.FileSystemBlockSize); int64 PaddingRequired = NewEntryOffset - OldOffset; check(PaddingRequired >= 0); check(PaddingRequired < RealFileSize); if (PaddingRequired > 0) { UE_LOG(LogPakFile, Verbose, TEXT("%14llu - %14llu : %14llu padding."), PakFileHandle->Tell(), PakFileHandle->Tell() + PaddingRequired, PaddingRequired); while (PaddingRequired > 0) { int64 AmountToWrite = FMath::Min(PaddingRequired, PaddingBuffer.Num()); PakFileHandle->Serialize(PaddingBuffer.GetData(), AmountToWrite); PaddingRequired -= AmountToWrite; } check(PakFileHandle->Tell() == NewEntryOffset); } } // Align bulk data if (OutputEntry->bIsMappedBulk && CmdLineParameters.AlignForMemoryMapping > 0 && OriginalFileSize != INDEX_NONE && !bDeleted) { if (!IsAligned(NewEntryOffset + NewEntry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest), CmdLineParameters.AlignForMemoryMapping)) { int64 OldOffset = NewEntryOffset; NewEntryOffset = AlignArbitrary(NewEntryOffset + NewEntry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest), CmdLineParameters.AlignForMemoryMapping) - NewEntry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest); int64 PaddingRequired = NewEntryOffset - OldOffset; check(PaddingRequired > 0); check(PaddingBuffer.Num() >= PaddingRequired); { UE_LOG(LogPakFile, Verbose, TEXT("%14llu - %14llu : %14llu bulk padding."), PakFileHandle->Tell(), PakFileHandle->Tell() + PaddingRequired, PaddingRequired); PakFileHandle->Serialize(PaddingBuffer.GetData(), PaddingRequired); check(PakFileHandle->Tell() == NewEntryOffset); } } } } TArray CurrentFileRegions; int64 SizeToWrite = 0; uint8* DataToWrite = nullptr; if (bDeleted) { PrepareDeleteRecordForPak(OutputEntry->PakFile->MountPoint, OutputEntry->InputPair, NewEntry); OutputEntry->bCopiedToPak = false; // Directly add the new entry to the index, no more work to do OutputEntry->PakFile->Index.Add(NewEntry); } else if (OutputEntry->InputPair.bNeedsCompression && CompressionMethod != NAME_None) { OutputEntry->bCopiedToPak = PrepareCopyCompressedFileToPak(OutputEntry->PakFile->MountPoint, OutputEntry->PakFile->Info, OutputEntry->InputPair, CompressedFileBuffer, NewEntry, DataToWrite, SizeToWrite, OutputEntry->PakFile->KeyChain); } else { OutputEntry->bCopiedToPak = PrepareCopyFileToPak(OutputEntry->PakFile->MountPoint, OutputEntry->InputPair, CompressedFileBuffer, NewEntry, DataToWrite, SizeToWrite, OutputEntry->PakFile->KeyChain, CmdLineParameters.bFileRegions ? &CurrentFileRegions : nullptr); } int64 TotalSizeToWrite = SizeToWrite + NewEntry.Info.GetSerializedSize(FPakInfo::PakFile_Version_Latest); if (OutputEntry->bCopiedToPak) { if (RequiredPatchPadding > 0 && !(OutputEntry->bIsMappedBulk && CmdLineParameters.AlignForMemoryMapping > 0) // don't wreck the bulk padding with patch padding ) { //if the next file is going to cross a patch-block boundary then pad out the current set of files with 0's //and align the next file up. bool bCrossesBoundary = (NewEntryOffset / RequiredPatchPadding) != ((NewEntryOffset + TotalSizeToWrite - 1) / RequiredPatchPadding); bool bPatchPadded = false; if (!OutputEntry->bIsUAssetUExpPairUExp) // never patch-pad the uexp of a uasset/uexp pair { bool bPairProbablyCrossesBoundary = false; // we don't consider compression because we have not compressed the uexp yet. if (OutputEntry->bIsUAssetUExpPairUAsset) { int64 UExpFileSize = OutputEntry->UExpFileInPair->OriginalFileSize / 2; // assume 50% compression bPairProbablyCrossesBoundary = (NewEntryOffset / RequiredPatchPadding) != ((NewEntryOffset + TotalSizeToWrite + UExpFileSize - 1) / RequiredPatchPadding); } if (TotalSizeToWrite >= RequiredPatchPadding || // if it exactly the padding size and by luck does not cross a boundary, we still consider it "over" because it can't be packed with anything else bCrossesBoundary || bPairProbablyCrossesBoundary) { NewEntryOffset = AlignArbitrary(NewEntryOffset, RequiredPatchPadding); int64 CurrentLoc = PakFileHandle->Tell(); int64 PaddingSize = NewEntryOffset - CurrentLoc; check(PaddingSize >= 0); if (PaddingSize) { UE_LOG(LogPakFile, Verbose, TEXT("%14llu - %14llu : %14llu patch padding."), PakFileHandle->Tell(), PakFileHandle->Tell() + PaddingSize, PaddingSize); check(PaddingSize <= PaddingBuffer.Num()); //have to pad manually with 0's. File locations skipped by Seek and never written are uninitialized which would defeat the whole purpose //of padding for certain platforms patch diffing systems. PakFileHandle->Serialize(PaddingBuffer.GetData(), PaddingSize); } check(PakFileHandle->Tell() == NewEntryOffset); bPatchPadded = true; } } //if the current file is bigger than a patch block then we will always have to pad out the previous files. //if there were a large set of contiguous small files behind us then this will be the natural stopping point for a possible pathalogical patching case where growth in the small files causes a cascade //to dirty up all the blocks prior to this one. If this could happen let's warn about it. if (bPatchPadded || OutputEntry->bIsLastFileInPak) // also check the last file, this won't work perfectly if we don't end up adding the last file for some reason { const uint64 ContiguousGroupedFilePatchWarningThreshhold = 50 * 1024 * 1024; if (OutputEntry->PakFile->ContiguousTotalSizeSmallerThanBlockSize > ContiguousGroupedFilePatchWarningThreshhold) { UE_LOG(LogPakFile, Display, TEXT("%" UINT64_FMT " small files (%" INT64_FMT ") totaling %" UINT64_FMT " contiguous bytes found before first 'large' file. Changes to any of these files could cause the whole group to be 'dirty' in a per-file binary diff based patching system."), OutputEntry->PakFile->ContiguousFilesSmallerThanBlockSize, RequiredPatchPadding, OutputEntry->PakFile->ContiguousTotalSizeSmallerThanBlockSize); } OutputEntry->PakFile->ContiguousTotalSizeSmallerThanBlockSize = 0; OutputEntry->PakFile->ContiguousFilesSmallerThanBlockSize = 0; } else { OutputEntry->PakFile->ContiguousTotalSizeSmallerThanBlockSize += TotalSizeToWrite; OutputEntry->PakFile->ContiguousFilesSmallerThanBlockSize++; } } if (OutputEntry->InputPair.bNeedsCompression && CompressionMethod != NAME_None) { FinalizeCopyCompressedFileToPak(OutputEntry->PakFile->Info, CompressedFileBuffer, NewEntry); } { // track per-compressor stats : // note GetCompressionMethodIndex in the pak entry is the index in the Pak file list of compressors // not the same as the index in the command line list int32 CompressionMethodIndex; if (CompressionFormatsAndNone.Find(CompressionMethod, CompressionMethodIndex)) { OutputEntry->PakFile->Compressor_Stat_Count[CompressionMethodIndex] += 1; OutputEntry->PakFile->Compressor_Stat_RawBytes[CompressionMethodIndex] += NewEntry.Info.UncompressedSize; OutputEntry->PakFile->Compressor_Stat_CompBytes[CompressionMethodIndex] += NewEntry.Info.Size; } } // Write to file { TRACE_CPUPROFILER_EVENT_SCOPE(WriteToFile); int64 Offset = PakFileHandle->Tell(); NewEntry.Info.Serialize(*PakFileHandle, FPakInfo::PakFile_Version_Latest); int64 PayloadOffset = PakFileHandle->Tell(); PakFileHandle->Serialize(DataToWrite, SizeToWrite); int64 EndOffset = PakFileHandle->Tell(); if (CmdLineParameters.bFileRegions) { FFileRegion::AccumulateFileRegions(OutputEntry->PakFile->AllFileRegions, Offset, PayloadOffset, EndOffset, CurrentFileRegions); } UE_LOG(LogPakFile, Verbose, TEXT("%14llu [header] - %14llu - %14llu : %14llu header+file %s."), Offset, PayloadOffset, EndOffset, EndOffset - Offset, *NewEntry.Filename); } // Update offset now and store it in the index (and only in index) NewEntry.Info.Offset = NewEntryOffset; OutputEntry->PakFile->Index.Add(NewEntry); const TCHAR* EncryptedString = TEXT(""); if (OutputEntry->InputPair.bNeedEncryption) { OutputEntry->PakFile->TotalRequestedEncryptedFiles++; if (OutputEntry->PakFile->KeyChain.GetPrincipalEncryptionKey()) { OutputEntry->PakFile->TotalEncryptedFiles++; OutputEntry->PakFile->TotalEncryptedDataSize += SizeToWrite; EncryptedString = TEXT("encrypted "); } } if (OutputEntry->InputPair.bNeedsCompression && CompressionMethod != NAME_None) { OutputEntry->PakFile->TotalCompressedSize += NewEntry.Info.Size; OutputEntry->PakFile->TotalUncompressedSize += NewEntry.Info.UncompressedSize; } if (OutputEntry->InputPair.bNeedRehydration) { OutputEntry->PakFile->RehydratedCount += OutputEntry->AccessCompressedBuffer().RehydrationCount; OutputEntry->PakFile->RehydratedBytes += OutputEntry->AccessCompressedBuffer().RehydrationBytes; } } Retire(OutputEntry); } } bool FPakWriterContext::Flush() { TRACE_CPUPROFILER_EVENT_SCOPE(Flush); check(!bFlushed); bFlushed = true; CompressionQueueEntryAddedEvent->Trigger(); WriteQueueEntryAddedEvent->Trigger(); const int64 RequiredPatchPadding = CmdLineParameters.PatchFilePadAlign; for (TUniquePtr& OutputPakFile : OutputPakFiles) { { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForWritesToComplete); for (;;) { bool bCompleted = OutputPakFile->AllEntriesRetiredEvent->Wait(2000); if (bCompleted) { break; } UE_LOG(LogPakFile, Display, TEXT("Writing entries (%llu/%llu)..."), RetiredEntriesCount.Load(), TotalEntriesCount); } } TRACE_CPUPROFILER_EVENT_SCOPE(FinalizePakFile); bool bPakFileIsEmpty = OutputPakFile->Index.Num() == 0; if (bPakFileIsEmpty) { UE_LOG(LogPakFile, Display, TEXT("Created empty pak file: %s"), *OutputPakFile->Filename); } else { UE_LOG(LogPakFile, Display, TEXT("Created pak file: %s"), *OutputPakFile->Filename); } for (FOutputPakFileEntry& OutputEntry : OutputPakFile->Entries) { if (!OutputEntry.bSomeCompressionSucceeded) { UE_LOG(LogPakFile, Verbose, TEXT("File \"%s\" did not get small enough from compression, or compression failed."), *OutputEntry.InputPair.Source); } if (OutputEntry.bCopiedToPak) { const TCHAR* EncryptedString = TEXT(""); if (OutputEntry.InputPair.bNeedEncryption) { if (OutputEntry.PakFile->KeyChain.GetPrincipalEncryptionKey()) { EncryptedString = TEXT("encrypted "); } } if (OutputEntry.InputPair.bNeedsCompression && OutputEntry.CompressionMethod != NAME_None) { float PercentLess = ((float)OutputEntry.Entry.Info.Size / ((float)OutputEntry.Entry.Info.UncompressedSize / 100.f)); if (OutputEntry.InputPair.SuggestedOrder < MAX_uint64) { UE_LOG(LogPakFile, Verbose, TEXT("Added compressed %sfile \"%s\", %.2f%% of original size. Compressed with %s, Size %lld bytes, Original Size %lld bytes (order %llu)."), EncryptedString, *OutputEntry.Entry.Filename, PercentLess, *OutputEntry.CompressionMethod.ToString(), OutputEntry.Entry.Info.Size, OutputEntry.Entry.Info.UncompressedSize, OutputEntry.InputPair.SuggestedOrder); } else { UE_LOG(LogPakFile, Verbose, TEXT("Added compressed %sfile \"%s\", %.2f%% of original size. Compressed with %s, Size %lld bytes, Original Size %lld bytes (no order given)."), EncryptedString, *OutputEntry.Entry.Filename, PercentLess, *OutputEntry.CompressionMethod.ToString(), OutputEntry.Entry.Info.Size, OutputEntry.Entry.Info.UncompressedSize); } } else { if (OutputEntry.InputPair.SuggestedOrder < MAX_uint64) { UE_LOG(LogPakFile, Verbose, TEXT("Added %sfile \"%s\", %lld bytes (order %llu)."), EncryptedString, *OutputEntry.Entry.Filename, OutputEntry.Entry.Info.Size, OutputEntry.InputPair.SuggestedOrder); } else { UE_LOG(LogPakFile, Verbose, TEXT("Added %sfile \"%s\", %lld bytes (no order given)."), EncryptedString, *OutputEntry.Entry.Filename, OutputEntry.Entry.Info.Size); } } } else { if (OutputEntry.InputPair.bIsDeleteRecord) { UE_LOG(LogPakFile, Verbose, TEXT("Created delete record for file \"%s\"."), *OutputEntry.InputPair.Source); } else { UE_LOG(LogPakFile, Warning, TEXT("Missing file \"%s\" will not be added to PAK file."), *OutputEntry.InputPair.Source); } } } if (RequiredPatchPadding > 0) { for (const FPakEntryPair& Pair : OutputPakFile->Index) { const FString& EntryFilename = Pair.Filename; const FPakEntry& PakEntry = Pair.Info; int64 EntrySize = PakEntry.GetSerializedSize(FPakInfo::PakFile_Version_Latest); int64 TotalSizeToWrite = PakEntry.Size + EntrySize; if (TotalSizeToWrite >= RequiredPatchPadding) { int64 RealStart = PakEntry.Offset; if ((RealStart % RequiredPatchPadding) != 0 && !EntryFilename.EndsWith(TEXT("uexp")) && // these are export sections of larger files and may be packed with uasset/umap and so we don't need a warning here !(EntryFilename.EndsWith(TEXT(".m.ubulk")) && CmdLineParameters.AlignForMemoryMapping > 0)) // Bulk padding unaligns patch padding and so we don't need a warning here { UE_LOG(LogPakFile, Warning, TEXT("File at offset %" INT64_FMT " of size %" INT64_FMT " not aligned to patch size %" INT64_FMT), RealStart, PakEntry.Size, RequiredPatchPadding); } } } } FPakFooterInfo Footer(*OutputPakFile->Filename, OutputPakFile->MountPoint, OutputPakFile->Info, OutputPakFile->Index); Footer.SetEncryptionInfo(OutputPakFile->KeyChain, &OutputPakFile->TotalEncryptedDataSize); Footer.SetFileRegionInfo(CmdLineParameters.bFileRegions, OutputPakFile->AllFileRegions); WritePakFooter(*OutputPakFile->PakFileHandle, Footer); if (CmdLineParameters.bSign) { TArray SignatureData; SignatureData.Append(OutputPakFile->Info.IndexHash.Hash, UE_ARRAY_COUNT(FSHAHash::Hash)); ((FSignedArchiveWriter*)OutputPakFile->PakFileHandle.Get())->SetSignatureData(SignatureData); } uint64 TotalPakFileSize = OutputPakFile->PakFileHandle->TotalSize(); OutputPakFile->PakFileHandle->Close(); OutputPakFile->PakFileHandle.Reset(); if (CmdLineParameters.bFileRegions) { FFileRegion::SerializeFileRegions(*OutputPakFile->PakFileRegionsHandle.Get(), OutputPakFile->AllFileRegions); OutputPakFile->PakFileRegionsHandle->Close(); OutputPakFile->PakFileRegionsHandle.Reset(); } if (bPakFileIsEmpty == false) { // log per-compressor stats : for (int32 MethodIndex = 0; MethodIndex < CompressionFormatsAndNone.Num(); MethodIndex++) { if (OutputPakFile->Compressor_Stat_Count[MethodIndex]) { FName CompressionMethod = CompressionFormatsAndNone[MethodIndex]; UE_LOG(LogPakFile, Display, TEXT("CompressionFormat %d [%s] : %d files, %lld -> %lld bytes"), MethodIndex, *(CompressionMethod.ToString()), OutputPakFile->Compressor_Stat_Count[MethodIndex], OutputPakFile->Compressor_Stat_RawBytes[MethodIndex], OutputPakFile->Compressor_Stat_CompBytes[MethodIndex] ); } } UE_LOG(LogPakFile, Display, TEXT("Added %d files, %lld bytes total"), OutputPakFile->Index.Num(), TotalPakFileSize); UE_LOG(LogPakFile, Display, TEXT("PrimaryIndex size: %" INT64_FMT " bytes"), Footer.PrimaryIndexSize); UE_LOG(LogPakFile, Display, TEXT("PathHashIndex size: %" INT64_FMT " bytes"), Footer.PathHashIndexSize); UE_LOG(LogPakFile, Display, TEXT("FullDirectoryIndex size: %" INT64_FMT " bytes"), Footer.FullDirectoryIndexSize); if (OutputPakFile->TotalUncompressedSize) { float PercentLess = ((float)OutputPakFile->TotalCompressedSize / ((float)OutputPakFile->TotalUncompressedSize / 100.f)); UE_LOG(LogPakFile, Display, TEXT("Compression summary: %.2f%% of original size. Compressed Size %" UINT64_FMT " bytes, Original Size %" UINT64_FMT " bytes. "), PercentLess, OutputPakFile->TotalCompressedSize, OutputPakFile->TotalUncompressedSize); } if (OutputPakFile->RehydratedCount > 0) { UE_LOG(LogPakFile, Display, TEXT("Asset Rehydration")); UE_LOG(LogPakFile, Display, TEXT(" Rehydrated: %" UINT64_FMT " payloads"), OutputPakFile->RehydratedCount); UE_LOG(LogPakFile, Display, TEXT(" Rehydrated: %" UINT64_FMT " bytes (%.2fMB)"), OutputPakFile->RehydratedBytes, (float)OutputPakFile->RehydratedBytes / 1024.0f / 1024.0f); } if (OutputPakFile->TotalEncryptedDataSize) { UE_LOG(LogPakFile, Display, TEXT("Encryption - ENABLED")); UE_LOG(LogPakFile, Display, TEXT(" Files: %" UINT64_FMT), OutputPakFile->TotalEncryptedFiles); if (OutputPakFile->Info.bEncryptedIndex) { UE_LOG(LogPakFile, Display, TEXT(" Index: Encrypted (%" INT64_FMT " bytes, %.2fMB)"), OutputPakFile->Info.IndexSize, (float)OutputPakFile->Info.IndexSize / 1024.0f / 1024.0f); } else { UE_LOG(LogPakFile, Display, TEXT(" Index: Unencrypted")); } UE_LOG(LogPakFile, Display, TEXT(" Total: %" UINT64_FMT " bytes (%.2fMB)"), OutputPakFile->TotalEncryptedDataSize, (float)OutputPakFile->TotalEncryptedDataSize / 1024.0f / 1024.0f); } else { UE_LOG(LogPakFile, Display, TEXT("Encryption - DISABLED")); } if (OutputPakFile->TotalEncryptedFiles < OutputPakFile->TotalRequestedEncryptedFiles) { UE_LOG(LogPakFile, Display, TEXT("%" UINT64_FMT " files requested encryption, but no AES key was supplied! Encryption was skipped for these files"), OutputPakFile->TotalRequestedEncryptedFiles); } UE_LOG(LogPakFile, Display, TEXT("")); } } WriterThread.Wait(); CompressionThread.Wait(); UE_LOG(LogPakFile, Display, TEXT("Writer and Compression Threads exited.")); if (TotalFilesWithPoorForcedCompression > 0) { UE_LOG(LogPakFile, Display, TEXT("Num files forcibly compressed due to -forcecompress option: %" INT64_FMT ", using %" INT64_FMT "bytes extra"), (int64)TotalFilesWithPoorForcedCompression, (int64)TotalExtraMemoryForPoorForcedCompression); } #if DETAILED_UNREALPAK_TIMING UE_LOG(LogPakFile, Display, TEXT("Detailed timing stats")); UE_LOG(LogPakFile, Display, TEXT("Compression time: %lf"), ((double)GCompressionTime) * FPlatformTime::GetSecondsPerCycle()); UE_LOG(LogPakFile, Display, TEXT("DDC Hits: %d"), GDDCHits); UE_LOG(LogPakFile, Display, TEXT("DDC Misses: %d"), GDDCMisses); UE_LOG(LogPakFile, Display, TEXT("DDC Sync Read Time: %lf"), ((double)GDDCSyncReadTime) * FPlatformTime::GetSecondsPerCycle()); UE_LOG(LogPakFile, Display, TEXT("DDC Sync Write Time: %lf"), ((double)GDDCSyncWriteTime) * FPlatformTime::GetSecondsPerCycle()); #endif if (CmdLineParameters.CsvPath.Len() > 0) { int64 SizeFilter = 0; bool bIncludeDeleted = true; bool bExtractToMountPoint = true; bool bPerPakCsvFiles = FPaths::DirectoryExists(CmdLineParameters.CsvPath); FString CsvFilename; if (!bPerPakCsvFiles) { // When CsvPath is a filename append .pak.csv to create a unique single csv for all pak files, // different from the unique single .utoc.csv for all container files. CsvFilename = CmdLineParameters.CsvPath + TEXT(".pak.csv"); } bool bAppendFile = false; for (TUniquePtr& OutputPakFile : OutputPakFiles) { if (bPerPakCsvFiles) { // When CsvPath is a dir, then create one unique .pak.csv per pak file CsvFilename = CmdLineParameters.CsvPath / FPaths::GetCleanFilename(OutputPakFile->Filename) + TEXT(".csv"); } ListFilesInPak( *OutputPakFile->Filename, SizeFilter, bIncludeDeleted, CsvFilename, bExtractToMountPoint, OutputPakFile->KeyChain, bAppendFile, false); if (!bPerPakCsvFiles) { bAppendFile = true; } } } return true; } bool CreatePakFile(const TCHAR* Filename, const TArray& FilesToAdd, const FPakCommandLineParameters& CmdLineParameters, const FKeyChain& InKeyChain) { const double StartTime = FPlatformTime::Seconds(); // Create Pak FPakWriterContext PakWriterContext; if (!PakWriterContext.Initialize(CmdLineParameters)) { return false; } if (!PakWriterContext.AddPakFile(Filename, FilesToAdd, InKeyChain)) { PakWriterContext.Flush(); return false; } return PakWriterContext.Flush(); } void WritePakFooter(FArchive& PakHandle, FPakFooterInfo& Footer) { // Create the second copy of the FPakEntries stored in the Index at the end of the PakFile // FPakEntries in the Index are stored as compacted bytes in EncodedPakEntries if possible, or in uncompacted NonEncodableEntries if not. // At the same time, create the two Indexes that map to the encoded-or-not given FPakEntry. The runtime will only load one of these, depending // on whether it needs to be able to do DirectorySearches. auto FinalizeIndexBlockSize = [&Footer](TArray& IndexData) { if (Footer.Info.bEncryptedIndex) { int32 OriginalSize = IndexData.Num(); int32 AlignedSize = Align(OriginalSize, FAES::AESBlockSize); for (int32 PaddingIndex = IndexData.Num(); PaddingIndex < AlignedSize; ++PaddingIndex) { uint8 Byte = IndexData[PaddingIndex % OriginalSize]; IndexData.Add(Byte); } } }; auto FinalizeIndexBlock = [&PakHandle, &Footer, &FinalizeIndexBlockSize](TArray& IndexData, FSHAHash& OutHash) { FinalizeIndexBlockSize(IndexData); FSHA1::HashBuffer(IndexData.GetData(), IndexData.Num(), OutHash.Hash); if (Footer.Info.bEncryptedIndex) { check(Footer.KeyChain && Footer.KeyChain->GetPrincipalEncryptionKey() && Footer.TotalEncryptedDataSize); FAES::EncryptData(IndexData.GetData(), IndexData.Num(), Footer.KeyChain->GetPrincipalEncryptionKey()->Key); *Footer.TotalEncryptedDataSize += IndexData.Num(); } }; TArray EncodedPakEntries; int32 NumEncodedEntries = 0; int32 NumDeletedEntries = 0; TArray NonEncodableEntries; FPakFile::FDirectoryIndex DirectoryIndex; FPakFile::FPathHashIndex PathHashIndex; TMap CollisionDetection; // Currently detecting Collisions only within the files stored into a single Pak. TODO: Create separate job to detect collisions over all files in the export. uint64 PathHashSeed; int32 NextIndex = 0; auto ReadNextEntry = [&Footer, &NextIndex]() -> FPakEntryPair& { return Footer.Index[NextIndex++]; }; FPakFile::EncodePakEntriesIntoIndex(Footer.Index.Num(), ReadNextEntry, Footer.Filename, Footer.Info, Footer.MountPoint, NumEncodedEntries, NumDeletedEntries, &PathHashSeed, &DirectoryIndex, &PathHashIndex, EncodedPakEntries, NonEncodableEntries, &CollisionDetection, FPakInfo::PakFile_Version_Latest); VerifyIndexesMatch(Footer.Index, DirectoryIndex, PathHashIndex, PathHashSeed, Footer.MountPoint, EncodedPakEntries, NonEncodableEntries, NumEncodedEntries, NumDeletedEntries, Footer.Info); // We write one PrimaryIndex and two SecondaryIndexes to the Pak File // PrimaryIndex // Common scalar data such as MountPoint // PresenceBit and Offset,Size,Hash for the SecondaryIndexes // PakEntries (Encoded and NonEncoded) // SecondaryIndex PathHashIndex: used by default in shipped versions of games. Uses less memory, but does not provide access to all filenames. // TMap from hash of FilePath to FPakEntryLocation // Pruned DirectoryIndex, containing only the FilePaths that were requested kept by allow list config variables // SecondaryIndex FullDirectoryIndex: used for developer tools and for titles that opt out of PathHashIndex because they need access to all filenames. // TMap from DirectoryPath to FDirectory, which itself is a TMap from CleanFileName to FPakEntryLocation // Each Index is separately encrypted and hashed. Runtime consumer such as the tools or the client game will only load one of these off of disk (unless runtime verification is turned on). // Create the pruned DirectoryIndex for use in the Primary Index TMap PrunedDirectoryIndex; FPakFile::PruneDirectoryIndex(DirectoryIndex, &PrunedDirectoryIndex, Footer.MountPoint); bool bWritePathHashIndex = FPakFile::IsPakWritePathHashIndex(); bool bWriteFullDirectoryIndex = FPakFile::IsPakWriteFullDirectoryIndex(); checkf(bWritePathHashIndex || bWriteFullDirectoryIndex, TEXT("At least one of Engine:[Pak]:WritePathHashIndex and Engine:[Pak]:WriteFullDirectoryIndex must be true")); TArray PrimaryIndexData; TArray PathHashIndexData; TArray FullDirectoryIndexData; Footer.Info.IndexOffset = PakHandle.Tell(); // Write PrimaryIndex bytes { FMemoryWriter PrimaryIndexWriter(PrimaryIndexData); PrimaryIndexWriter.SetByteSwapping(PakHandle.ForceByteSwapping()); PrimaryIndexWriter << const_cast(Footer.MountPoint); int32 NumEntries = Footer.Index.Num(); PrimaryIndexWriter << NumEntries; PrimaryIndexWriter << PathHashSeed; FSecondaryIndexWriter PathHashIndexWriter(PathHashIndexData, bWritePathHashIndex, PrimaryIndexData, PrimaryIndexWriter); PathHashIndexWriter.WritePlaceholderToPrimary(); FSecondaryIndexWriter FullDirectoryIndexWriter(FullDirectoryIndexData, bWriteFullDirectoryIndex, PrimaryIndexData, PrimaryIndexWriter); FullDirectoryIndexWriter.WritePlaceholderToPrimary(); PrimaryIndexWriter << EncodedPakEntries; int32 NonEncodableEntriesNum = NonEncodableEntries.Num(); PrimaryIndexWriter << NonEncodableEntriesNum; for (FPakEntry& PakEntry : NonEncodableEntries) { PakEntry.Serialize(PrimaryIndexWriter, FPakInfo::PakFile_Version_Latest); } // Finalize the size of the PrimaryIndex (it may change due to alignment padding) because we need the size to know the offset of the SecondaryIndexes which come after it in the PakFile. // Do not encrypt and hash it yet, because we still need to replace placeholder data in it for the Offset,Size,Hash of each SecondaryIndex FinalizeIndexBlockSize(PrimaryIndexData); // Write PathHashIndex bytes if (bWritePathHashIndex) { { FMemoryWriter& SecondaryWriter = PathHashIndexWriter.GetSecondaryWriter(); SecondaryWriter << PathHashIndex; FPakFile::SaveIndexInternal_DirectoryIndex(SecondaryWriter, PrunedDirectoryIndex); } PathHashIndexWriter.FinalizeAndRecordOffset(Footer.Info.IndexOffset + PrimaryIndexData.Num(), FinalizeIndexBlock); } // Write FullDirectoryIndex bytes if (bWriteFullDirectoryIndex) { { FMemoryWriter& SecondaryWriter = FullDirectoryIndexWriter.GetSecondaryWriter(); FPakFile::SaveIndexInternal_DirectoryIndex(SecondaryWriter, DirectoryIndex); } FullDirectoryIndexWriter.FinalizeAndRecordOffset(Footer.Info.IndexOffset + PrimaryIndexData.Num() + PathHashIndexData.Num(), FinalizeIndexBlock); } // Encrypt and Hash the PrimaryIndex now that we have filled in the SecondaryIndex information FinalizeIndexBlock(PrimaryIndexData, Footer.Info.IndexHash); } // Write the bytes for each Index into the PakFile Footer.Info.IndexSize = PrimaryIndexData.Num(); PakHandle.Serialize(PrimaryIndexData.GetData(), PrimaryIndexData.Num()); if (bWritePathHashIndex) { PakHandle.Serialize(PathHashIndexData.GetData(), PathHashIndexData.Num()); } if (bWriteFullDirectoryIndex) { PakHandle.Serialize(FullDirectoryIndexData.GetData(), FullDirectoryIndexData.Num()); } // Save the FPakInfo, which has offset, size, and hash value for the PrimaryIndex, at the end of the PakFile Footer.Info.Serialize(PakHandle, FPakInfo::PakFile_Version_Latest); if (Footer.bFileRegions) { check(Footer.AllFileRegions); // Add a final region to include the headers / data at the end of the .pak, after the last file payload. FFileRegion::AccumulateFileRegions(*Footer.AllFileRegions, Footer.Info.IndexOffset, Footer.Info.IndexOffset, PakHandle.Tell(), {}); } Footer.PrimaryIndexSize = PrimaryIndexData.Num(); Footer.PathHashIndexSize = PathHashIndexData.Num(); Footer.FullDirectoryIndexSize = FullDirectoryIndexData.Num(); } bool TestPakFile(const TCHAR* Filename, bool TestHashes) { TRefCountPtr PakFile = MakeRefCount(&FPlatformFileManager::Get().GetPlatformFile(), Filename, /*bIsSigned=*/ false); if (PakFile->IsValid()) { return TestHashes ? PakFile->Check() : true; } else { UE_LOG(LogPakFile, Error, TEXT("Unable to open pak file \"%s\"."), Filename); return false; } } bool ListFilesInPak(const TCHAR * InPakFilename, int64 SizeFilter, bool bIncludeDeleted, const FString& CSVFilename, bool bExtractToMountPoint, const FKeyChain& InKeyChain, bool bAppendFile, bool bIncludeFooter) { TRACE_CPUPROFILER_EVENT_SCOPE(ListFilesInPak); IPlatformFile* LowerLevelPlatformFile = &FPlatformFileManager::Get().GetPlatformFile(); TRefCountPtr PakFilePtr = MakeRefCount(LowerLevelPlatformFile, InPakFilename, /*bIsSigned=*/ false); FPakFile& PakFile = *PakFilePtr; int32 FileCount = 0; int64 FileSize = 0; int64 FilteredSize = 0; if (PakFile.IsValid()) { UE_LOG(LogPakFile, Display, TEXT("Listing %s with mount point \"%s\""), *FPaths::GetCleanFilename(InPakFilename), *PakFile.GetMountPoint()); TArray, FPakEntry>> Records; for (FPakFile::FPakEntryIterator It(PakFile,bIncludeDeleted); It; ++It) { const FString* Filename = It.TryGetFilename(); Records.Add({ Filename ? TOptional(*Filename) : TOptional(), It.Info()}); } struct FOffsetSort { FORCEINLINE bool operator()(const TPair, FPakEntry>& A, const TPair, FPakEntry>& B) const { return A.Value.Offset < B.Value.Offset; } }; Records.Sort(FOffsetSort()); const FString MountPoint = bExtractToMountPoint ? PakFile.GetMountPoint() : TEXT(""); FileCount = Records.Num(); // The Hashes are not stored in the FPakEntries stored in the index, but they are stored for the FPakEntries stored before each payload. // Read the hashes out of the payload TArray HashesBuffer; HashesBuffer.SetNum(FileCount * sizeof(FPakEntry::Hash)); uint8* Hashes = HashesBuffer.GetData(); int32 EntryIndex = 0; for (const TPair, FPakEntry>& It : Records) { PakFile.ReadHashFromPayload(It.Value, Hashes + (EntryIndex++)*sizeof(FPakEntry::Hash)); } if (CSVFilename.Len() > 0) { TRACE_CPUPROFILER_EVENT_SCOPE(WriteCsv); uint32 WriteFlags = bAppendFile ? FILEWRITE_Append : 0; TUniquePtr OutputArchive(IFileManager::Get().CreateFileWriter(*CSVFilename, WriteFlags)); if (!OutputArchive.IsValid()) { UE_LOG(LogPakFile, Display, TEXT("Failed to save CSV file %s"), *CSVFilename); return false; } if (!bAppendFile) { OutputArchive->Logf(TEXT("Filename, Offset, Size, Hash, Deleted, Compressed, CompressionMethod")); } EntryIndex = 0; for (const TPair, FPakEntry>& It : Records) { const FPakEntry& Entry = It.Value; uint8* Hash = Hashes + (EntryIndex++)*sizeof(FPakEntry::Hash); bool bWasCompressed = Entry.CompressionMethodIndex != 0; OutputArchive->Logf( TEXT("%s%s, %lld, %lld, %s, %s, %s, %d"), *MountPoint, It.Key ? **It.Key : TEXT(""), Entry.Offset, Entry.Size, *BytesToHex(Hash, sizeof(FPakEntry::Hash)), Entry.IsDeleteRecord() ? TEXT("true") : TEXT("false"), bWasCompressed ? TEXT("true") : TEXT("false"), Entry.CompressionMethodIndex); } if (bIncludeFooter) { // If desired, emit a pseudoentry for our metadata. This is all stored at the end of the file // so rather than ask the pak file anything we just assign it to everything past the end of the // records. int64 RecordEndOffset = 0; if (Records.Num()) { const FPakEntry& LastEntry = Records[Records.Num()-1].Value; RecordEndOffset = LastEntry.Offset + LastEntry.Size; } OutputArchive->Logf( TEXT("