Files
UnrealEngineUWP/Engine/Source/Runtime/PakFile/Public/IPlatformFilePak.h
Dmitry Rekman 7a367ae37f Make OpenRead() aware about intended sharing mode of the file.
- Currently essential on Windows only; other platforms may make use of this when managing file handles.

#codereview Eric.Newman, Robert.Manuszewski, Josh.Adams, Michael.Trepka, Chris.Babcock, Marcus.Wassmer, Peter.Sauerbrei

[CL 2480709 by Dmitry Rekman in Main branch]
2015-03-16 17:14:06 -04:00

1338 lines
34 KiB
C++

// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#pragma once
PAKFILE_API DECLARE_LOG_CATEGORY_EXTERN(LogPakFile, Log, All);
/**
* Struct which holds pak file info (version, index offset, hash value).
*/
struct FPakInfo
{
enum
{
/** Magic number to use in header */
PakFile_Magic = 0x5A6F12E1,
/** Size of cached data. */
MaxChunkDataSize = 256*1024,
};
/** Version numbers. */
enum
{
PakFile_Version_Initial = 1,
PakFile_Version_NoTimestamps = 2,
PakFile_Version_CompressionEncryption = 3,
PakFile_Version_Latest = PakFile_Version_CompressionEncryption
};
/** Pak file magic value. */
uint32 Magic;
/** Pak file version. */
int32 Version;
/** Offset to pak file index. */
int64 IndexOffset;
/** Size (in bytes) of pak file index. */
int64 IndexSize;
/** Index SHA1 value. */
uint8 IndexHash[20];
/**
* Constructor.
*/
FPakInfo()
: Magic(PakFile_Magic)
, Version(PakFile_Version_Latest)
, IndexOffset(-1)
, IndexSize(0)
{
FMemory::Memset(IndexHash, 0, sizeof(IndexHash));
}
/**
* Gets the size of data serialized by this struct.
*
* @return Serialized data size.
*/
int64 GetSerializedSize() const
{
return sizeof(Magic) + sizeof(Version) + sizeof(IndexOffset) + sizeof(IndexSize) + sizeof(IndexHash);
}
/**
* Serializes this struct.
*
* @param Ar Archive to serialize data with.
*/
void Serialize(FArchive& Ar)
{
if (Ar.IsLoading() && Ar.TotalSize() < (Ar.Tell() + GetSerializedSize()))
{
Magic = 0;
return;
}
Ar << Magic;
Ar << Version;
Ar << IndexOffset;
Ar << IndexSize;
Ar.Serialize(IndexHash, sizeof(IndexHash));
}
};
/**
* Struct storing offsets and sizes of a compressed block
*/
struct FPakCompressedBlock
{
/** Offset of the start of a compression block. Offset is absolute */
int64 CompressedStart;
/** Offset of the end of a compression block. This may not align completely with the start of the next block. Offset is absolute */
int64 CompressedEnd;
bool operator == (const FPakCompressedBlock& B) const
{
return CompressedStart == B.CompressedStart && CompressedEnd == B.CompressedEnd;
}
bool operator != (const FPakCompressedBlock& B) const
{
return !(*this == B);
}
};
FORCEINLINE FArchive& operator<<(FArchive& Ar, FPakCompressedBlock& Block)
{
Ar << Block.CompressedStart;
Ar << Block.CompressedEnd;
return Ar;
}
/**
* Struct holding info about a single file stored in pak file.
*/
struct FPakEntry
{
/** Offset into pak file where the file is stored.*/
int64 Offset;
/** Serialized file size. */
int64 Size;
/** Uncompressed file size. */
int64 UncompressedSize;
/** Compression method. */
int32 CompressionMethod;
/** File SHA1 value. */
uint8 Hash[20];
/** Array of compression blocks that describe how to decompress this pak entry */
TArray<FPakCompressedBlock> CompressionBlocks;
/** Size of a compressed block in the file */
uint32 CompressionBlockSize;
/** True is file is encrypted */
uint8 bEncrypted;
/** Flag is set to true when FileHeader has been checked against PakHeader. It is not serialized */
mutable bool Verified;
/**
* Constructor.
*/
FPakEntry()
: Offset(-1)
, Size(0)
, UncompressedSize(0)
, CompressionMethod(0)
, CompressionBlockSize(0)
, bEncrypted(false)
, Verified(false)
{
FMemory::Memset(Hash, 0, sizeof(Hash));
}
/**
* Gets the size of data serialized by this struct.
*
* @return Serialized data size.
*/
int64 GetSerializedSize(int32 Version) const
{
int64 SerializedSize = sizeof(Offset) + sizeof(Size) + sizeof(UncompressedSize) + sizeof(CompressionMethod) + sizeof(Hash);
if (Version >= FPakInfo::PakFile_Version_CompressionEncryption)
{
SerializedSize += sizeof(bEncrypted) + sizeof(CompressionBlockSize);
if(CompressionMethod != COMPRESS_None)
{
SerializedSize += sizeof(FPakCompressedBlock) * CompressionBlocks.Num() + sizeof(int32);
}
}
if (Version < FPakInfo::PakFile_Version_NoTimestamps)
{
// Timestamp
SerializedSize += sizeof(int64);
}
return SerializedSize;
}
/**
* Compares two FPakEntry structs.
*/
bool operator == (const FPakEntry& B) const
{
// Offsets are not compared here because they're not
// serialized with file headers anyway.
return Size == B.Size &&
UncompressedSize == B.UncompressedSize &&
CompressionMethod == B.CompressionMethod &&
bEncrypted == B.bEncrypted &&
CompressionBlockSize == B.CompressionBlockSize &&
FMemory::Memcmp(Hash, B.Hash, sizeof(Hash)) == 0 &&
CompressionBlocks == B.CompressionBlocks;
}
/**
* Compares two FPakEntry structs.
*/
bool operator != (const FPakEntry& B) const
{
// Offsets are not compared here because they're not
// serialized with file headers anyway.
return Size != B.Size ||
UncompressedSize != B.UncompressedSize ||
CompressionMethod != B.CompressionMethod ||
bEncrypted != B.bEncrypted ||
CompressionBlockSize != B.CompressionBlockSize ||
FMemory::Memcmp(Hash, B.Hash, sizeof(Hash)) != 0 ||
CompressionBlocks != B.CompressionBlocks;
}
/**
* Serializes FPakEntry struct.
*
* @param Ar Archive to serialize data with.
* @param Entry Data to serialize.
*/
void Serialize(FArchive& Ar, int32 Version)
{
Ar << Offset;
Ar << Size;
Ar << UncompressedSize;
Ar << CompressionMethod;
if (Version <= FPakInfo::PakFile_Version_Initial)
{
FDateTime Timestamp;
Ar << Timestamp;
}
Ar.Serialize(Hash, sizeof(Hash));
if (Version >= FPakInfo::PakFile_Version_CompressionEncryption)
{
if(CompressionMethod != COMPRESS_None)
{
Ar << CompressionBlocks;
}
Ar << bEncrypted;
Ar << CompressionBlockSize;
}
}
/**
* Verifies two entries match to check for corruption.
*
* @param FileEntryA Entry 1.
* @param FileEntryB Entry 2.
*/
static bool VerifyPakEntriesMatch(const FPakEntry& FileEntryA, const FPakEntry& FileEntryB);
};
/** Pak directory type. */
typedef TMap<FString, FPakEntry*> FPakDirectory;
/**
* Pak file.
*/
class PAKFILE_API FPakFile : FNoncopyable
{
/** Pak filename. */
FString PakFilename;
/** Archive to serialize the pak file from. */
TAutoPtr<class FChunkCacheWorker> Decryptor;
/** Map of readers assigned to threads. */
TMap<uint32, TAutoPtr<FArchive>> ReaderMap;
/** Critical section for accessing ReaderMap. */
FCriticalSection CriticalSection;
/** Pak file info (trailer). */
FPakInfo Info;
/** Mount point. */
FString MountPoint;
/** Info on all files stored in pak. */
TArray<FPakEntry> Files;
/** Pak Index organized as a map of directories for faster Directory iteration. */
TMap<FString, FPakDirectory> Index;
/** Timestamp of this pak file. */
FDateTime Timestamp;
/** True if this is a signed pak file. */
bool bSigned;
/** True if this pak file is valid and usable */
bool bIsValid;
FArchive* CreatePakReader(const TCHAR* Filename);
FArchive* CreatePakReader(IFileHandle& InHandle, const TCHAR* Filename);
FArchive* SetupSignedPakReader(FArchive* Reader);
public:
/**
* Opens a pak file given its filename.
*
* @param Filename Pak filename.
* @param bIsSigned true if the pak is signed
*/
FPakFile(const TCHAR* Filename, bool bIsSigned);
/**
* Creates a pak file using the supplied file handle.
*
* @param LowerLevel Lower level platform file.
* @param Filename Filename.
* @param bIsSigned true if the pak is signed
*/
FPakFile(IPlatformFile* LowerLevel, const TCHAR* Filename, bool bIsSigned);
/**
* Creates a pak file using the supplied archive
*
* @param Archive Pointer to the archive which contains the pak file data
*/
FPakFile(FArchive* Archive);
~FPakFile();
/**
* Checks if the pak file is valid.
*
* @return true if this pak file is valid, false otherwise.
*/
bool IsValid() const
{
return bIsValid;
}
/**
* Gets pak filename.
*
* @return Pak filename.
*/
const FString& GetFilename() const
{
return PakFilename;
}
/**
* Gets pak file index.
*
* @return Pak index.
*/
const TMap<FString, FPakDirectory>& GetIndex() const
{
return Index;
}
/**
* Gets shared pak file archive for given thrad
*
* @return Pointer to pak file archive used to read data from pak.
*/
FArchive* GetSharedReader(IPlatformFile* LowerLevel);
/**
* Finds an entry in the pak file matching the given filename.
*
* @param Filename File to find.
* @return Pointer to pak file entry if the file was found, NULL otherwise.
*/
const FPakEntry* Find(const FString& Filename) const
{
const FPakEntry*const * FoundFile = NULL;
if (Filename.StartsWith(MountPoint))
{
FString Path(FPaths::GetPath(Filename));
const FPakDirectory* PakDirectory = FindDirectory(*Path);
if (PakDirectory != NULL)
{
FString RelativeFilename(Filename.Mid(MountPoint.Len()));
FoundFile = PakDirectory->Find(RelativeFilename);
}
}
return FoundFile ? *FoundFile : NULL;
}
/**
* Sets the pak file mount point.
*
* @param Path New mount point path.
*/
void SetMountPoint(const TCHAR* Path)
{
MountPoint = Path;
MakeDirectoryFromPath(MountPoint);
}
/**
* Gets pak file mount point.
*
* @return Mount point path.
*/
const FString& GetMountPoint() const
{
return MountPoint;
}
/**
* Looks for files or directories within the pak file.
*
* @param OutFiles List of files or folder matching search criteria.
* @param InPath Path to look for files or folder at.
* @param bIncludeFiles If true OutFiles will include matching files.
* @param bIncludeDirectories If true OutFiles will include matching folders.
* @param bRecursive If true, sub-folders will also be checked.
*/
template <class ContainerType>
void FindFilesAtPath(ContainerType& OutFiles, const TCHAR* InPath, bool bIncludeFiles = true, bool bIncludeDirectories = false, bool bRecursive = false) const
{
// Make sure all directory names end with '/'.
FString Directory(InPath);
MakeDirectoryFromPath(Directory);
// Check the specified path is under the mount point of this pak file.
// The reverse case (MountPoint StartsWith Directory) is needed to properly handle
// pak files that are a subdirectory of the actual directory
if ((Directory.StartsWith(MountPoint)) || (MountPoint.StartsWith(Directory)))
{
TArray<FString> DirectoriesInPak; // List of all unique directories at path
for (TMap<FString, FPakDirectory>::TConstIterator It(Index); It; ++It)
{
FString PakPath(MountPoint + It.Key());
// Check if the file is under the specified path.
if (PakPath.StartsWith(Directory))
{
if (bRecursive == true)
{
// Add everything
if (bIncludeFiles)
{
for (FPakDirectory::TConstIterator DirectoryIt(It.Value()); DirectoryIt; ++DirectoryIt)
{
OutFiles.Add(MountPoint + DirectoryIt.Key());
}
}
if (bIncludeDirectories)
{
if (Directory != PakPath)
{
DirectoriesInPak.Add(PakPath);
}
}
}
else
{
int32 SubDirIndex = PakPath.Len() > Directory.Len() ? PakPath.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, Directory.Len() + 1) : INDEX_NONE;
// Add files in the specified folder only.
if (bIncludeFiles && SubDirIndex == INDEX_NONE)
{
for (FPakDirectory::TConstIterator DirectoryIt(It.Value()); DirectoryIt; ++DirectoryIt)
{
OutFiles.Add(MountPoint + DirectoryIt.Key());
}
}
// Add sub-folders in the specified folder only
if (bIncludeDirectories && SubDirIndex >= 0)
{
DirectoriesInPak.AddUnique(PakPath.Left(SubDirIndex + 1));
}
}
}
}
OutFiles.Append(DirectoriesInPak);
}
}
/**
* Finds a directory in pak file.
*
* @param InPath Directory path.
* @return Pointer to a map with directory contents if the directory was found, NULL otherwise.
*/
const FPakDirectory* FindDirectory(const TCHAR* InPath) const
{
FString Directory(InPath);
MakeDirectoryFromPath(Directory);
const FPakDirectory* PakDirectory = NULL;
// Check the specified path is under the mount point of this pak file.
if (Directory.StartsWith(MountPoint))
{
PakDirectory = Index.Find(Directory.Mid(MountPoint.Len()));
}
return PakDirectory;
}
/**
* Checks if a directory exists in pak file.
*
* @param InPath Directory path.
* @return true if the given path exists in pak file, false otherwise.
*/
bool DirectoryExists(const TCHAR* InPath) const
{
return !!FindDirectory(InPath);
}
/** Iterator class used to iterate over all files in pak. */
class FFileIterator
{
/** Owner pak file. */
const FPakFile& PakFile;
/** Index iterator. */
TMap<FString, FPakDirectory>::TConstIterator IndexIt;
/** Directory iterator. */
FPakDirectory::TConstIterator DirectoryIt;
public:
/**
* Constructor.
*
* @param InPakFile Pak file to iterate.
*/
FFileIterator(const FPakFile& InPakFile)
: PakFile(InPakFile)
, IndexIt(PakFile.GetIndex())
, DirectoryIt((IndexIt ? FPakDirectory::TConstIterator(IndexIt.Value()): FPakDirectory()))
{
}
FFileIterator& operator++()
{
// Continue with the next file
++DirectoryIt;
while (!DirectoryIt && IndexIt)
{
// No more files in the current directory, jump to the next one.
++IndexIt;
if (IndexIt)
{
// No need to check if there's files in the current directory. If a directory
// exists in the index it is always non-empty.
DirectoryIt.~TConstIterator();
new(&DirectoryIt) FPakDirectory::TConstIterator(IndexIt.Value());
}
}
return *this;
}
SAFE_BOOL_OPERATORS(FFileIterator)
/** conversion to "bool" returning true if the iterator is valid. */
FORCEINLINE_EXPLICIT_OPERATOR_BOOL() const
{
return !!IndexIt;
}
/** inverse of the "bool" operator */
FORCEINLINE bool operator !() const
{
return !(bool)*this;
}
const FString& Filename() const { return DirectoryIt.Key(); }
const FPakEntry& Info() const { return *DirectoryIt.Value(); }
};
/**
* Gets this pak file info.
*
* @return Info about this pak file.
*/
const FPakInfo& GetInfo() const
{
return Info;
}
/**
* Gets this pak file's tiemstamp.
*
* @return Timestamp.
*/
const FDateTime& GetTimestamp() const
{
return Timestamp;
}
private:
/**
* Initializes the pak file.
*/
void Initialize(FArchive* Reader);
/**
* Loads and initializes pak file index.
*/
void LoadIndex(FArchive* Reader);
public:
/**
* Helper function to append '/' at the end of path.
*
* @param Path - path to convert in place to directory.
*/
static void MakeDirectoryFromPath(FString& Path)
{
if (Path.Len() > 0 && Path[Path.Len() - 1] != '/')
{
Path += TEXT("/");
}
}
};
/**
* Placeholder Class
*/
class PAKFILE_API FPakNoEncryption
{
public:
enum
{
Alignment = 1,
};
static FORCEINLINE int64 AlignReadRequest(int64 Size)
{
return Size;
}
static FORCEINLINE void DecryptBlock(void* Data, int64 Size)
{
// Nothing needs to be done here
}
};
template< typename EncryptionPolicy = FPakNoEncryption >
class PAKFILE_API FPakReaderPolicy
{
public:
/** Pak file that own this file data */
const FPakFile& PakFile;
/** Pak file entry for this file. */
const FPakEntry& PakEntry;
/** Pak file archive to read the data from. */
FArchive* PakReader;
/** Offset to the file in pak (including the file header). */
int64 OffsetToFile;
FPakReaderPolicy(const FPakFile& InPakFile,const FPakEntry& InPakEntry,FArchive* InPakReader)
: PakFile(InPakFile)
, PakEntry(InPakEntry)
, PakReader(InPakReader)
{
OffsetToFile = PakEntry.Offset + PakEntry.GetSerializedSize(PakFile.GetInfo().Version);
}
FORCEINLINE int64 FileSize() const
{
return PakEntry.Size;
}
void Serialize(int64 DesiredPosition, void* V, int64 Length)
{
uint8 TempBuffer[EncryptionPolicy::Alignment];
if (EncryptionPolicy::AlignReadRequest(DesiredPosition) != DesiredPosition)
{
int64 Start = DesiredPosition & ~(EncryptionPolicy::Alignment-1);
int64 Offset = DesiredPosition - Start;
int32 CopySize = EncryptionPolicy::Alignment-(DesiredPosition-Start);
PakReader->Seek(OffsetToFile + Start);
PakReader->Serialize(TempBuffer, EncryptionPolicy::Alignment);
EncryptionPolicy::DecryptBlock(TempBuffer, EncryptionPolicy::Alignment);
FMemory::Memcpy(V, TempBuffer+Offset, CopySize);
V = (void*)((uint8*)V + CopySize);
DesiredPosition += CopySize;
Length -= CopySize;
check(DesiredPosition % EncryptionPolicy::Alignment == 0);
}
else
{
PakReader->Seek(OffsetToFile + DesiredPosition);
}
int64 CopySize = Length & ~(EncryptionPolicy::Alignment-1);
PakReader->Serialize(V, CopySize);
EncryptionPolicy::DecryptBlock(V, CopySize);
Length -= CopySize;
V = (void*)((uint8*)V + CopySize);
if (Length > 0)
{
PakReader->Serialize(TempBuffer, EncryptionPolicy::Alignment);
EncryptionPolicy::DecryptBlock(TempBuffer, EncryptionPolicy::Alignment);
FMemory::Memcpy(V, TempBuffer, Length);
}
}
};
/**
* File handle to read from pak file.
*/
template< typename ReaderPolicy = FPakReaderPolicy<> >
class PAKFILE_API FPakFileHandle : public IFileHandle
{
/** True if PakReader is shared and should not be deleted by this handle. */
const bool bSharedReader;
/** Current read position. */
int64 ReadPos;
/** Class that controls reading from pak file */
ReaderPolicy Reader;
public:
/**
* Constructs pak file handle to read from pak.
*
* @param InFilename Filename
* @param InPakEntry Entry in the pak file.
* @param InPakFile Pak file.
*/
FPakFileHandle(const FPakFile& InPakFile, const FPakEntry& InPakEntry, FArchive* InPakReader, bool bIsSharedReader)
: bSharedReader(bIsSharedReader)
, ReadPos(0)
, Reader(InPakFile, InPakEntry, InPakReader)
{
}
/**
* Destructor. Cleans up the reader archive if necessary.
*/
virtual ~FPakFileHandle()
{
if (!bSharedReader)
{
delete Reader.PakReader;
}
}
// BEGIN IFileHandle Interface
virtual int64 Tell() override
{
return ReadPos;
}
virtual bool Seek(int64 NewPosition) override
{
if (NewPosition > Reader.FileSize() || NewPosition < 0)
{
return false;
}
ReadPos = NewPosition;
return true;
}
virtual bool SeekFromEnd(int64 NewPositionRelativeToEnd) override
{
return Seek(Reader.FileSize() - NewPositionRelativeToEnd);
}
virtual bool Read(uint8* Destination, int64 BytesToRead) override
{
// Check that the file header is OK
if (!Reader.PakEntry.Verified)
{
FPakEntry FileHeader;
Reader.PakReader->Seek(Reader.PakEntry.Offset);
FileHeader.Serialize(*Reader.PakReader, Reader.PakFile.GetInfo().Version);
if (FPakEntry::VerifyPakEntriesMatch(Reader.PakEntry, FileHeader))
{
Reader.PakEntry.Verified = true;
}
else
{
//Header is corrupt, fail the read
return false;
}
}
//
if (Reader.FileSize() >= (ReadPos + BytesToRead))
{
// Read directly from Pak.
Reader.Serialize(ReadPos, Destination, BytesToRead);
ReadPos += BytesToRead;
return true;
}
else
{
return false;
}
}
virtual bool Write(const uint8* Source, int64 BytesToWrite) override
{
// Writing in pak files is not allowed.
return false;
}
virtual int64 Size() override
{
return Reader.FileSize();
}
/// END IFileHandle Interface
};
/**
* Platform file wrapper to be able to use pak files.
**/
class PAKFILE_API FPakPlatformFile : public IPlatformFile
{
struct FPakListEntry
{
FPakListEntry()
: ReadOrder(0)
, PakFile(nullptr)
{}
uint32 ReadOrder;
FPakFile* PakFile;
FORCEINLINE bool operator < (const FPakListEntry& RHS) const
{
return ReadOrder > RHS.ReadOrder;
}
};
/** Wrapped file */
IPlatformFile* LowerLevel;
/** List of all available pak files. */
TArray<FPakListEntry> PakFiles;
/** True if this we're using signed content. */
bool bSigned;
/** Synchronization object for accessing the list of currently mounted pak files. */
FCriticalSection PakListCritical;
/**
* Gets mounted pak files
*/
FORCEINLINE void GetMountedPaks(TArray<FPakListEntry>& Paks)
{
FScopeLock ScopedLock(&PakListCritical);
Paks.Append(PakFiles);
}
/**
* Checks if a directory exists in one of the available pak files.
*
* @param Directory Directory to look for.
* @return true if the directory exists, false otherwise.
*/
bool DirectoryExistsInPakFiles(const TCHAR* Directory)
{
FString StandardPath = Directory;
FPaths::MakeStandardFilename(StandardPath);
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);
// Check all pak files.
for (int32 PakIndex = 0; PakIndex < Paks.Num(); PakIndex++)
{
if (Paks[PakIndex].PakFile->DirectoryExists(*StandardPath))
{
return true;
}
}
return false;
}
/**
* Helper function to copy a file from one handle to another usuing the supplied buffer.
*
* @param Dest Destination file handle.
* @param Source file handle.
* @param FileSize size of the source file.
* @param Buffer Pointer to the buffer used to copy data.
* @param BufferSize Sizeof of the buffer.
* @return true if the operation was successfull, false otherwise.
*/
bool BufferedCopyFile(IFileHandle& Dest, IFileHandle& Source, const int64 FileSize, uint8* Buffer, const int64 BufferSize) const;
/**
* Creates file handle to read from Pak file.
*
* @param Filename Filename to create the handle for.
* @param PakFile Pak file to read from.
* @param FileEntry File entry to create the handle for.
* @return Pointer to the new handle.
*/
IFileHandle* CreatePakFileHandle(const TCHAR* Filename, FPakFile* PakFile, const FPakEntry* FileEntry);
/**
* Handler for device delegate to prompt us to load a new pak.
*/
bool HandleMountPakDelegate(const FString& PakFilePath, uint32 PakOrder);
/**
* Handler for device delegate to prompt us to unload a pak.
*/
bool HandleUnmountPakDelegate(const FString& PakFilePath);
/**
* Finds all pak files in the given directory.
*
* @param Directory Directory to (recursively) look for pak files in
* @param OutPakFiles List of pak files
*/
static void FindPakFilesInDirectory(IPlatformFile* LowLevelFile, const TCHAR* Directory, TArray<FString>& OutPakFiles);
/**
* Finds all pak files in the known pak folders
*
* @param OutPakFiles List of all found pak files
*/
static void FindAllPakFiles(IPlatformFile* LowLevelFile, const TArray<FString>& PakFolders, TArray<FString>& OutPakFiles);
public:
static const TCHAR* GetTypeName()
{
return TEXT("PakFile");
}
/**
* Checks if pak files exist in any of the known pak file locations.
*/
static bool CheckIfPakFilesExist(IPlatformFile* LowLevelFile, const TArray<FString>& PakFolders);
/**
* Gets all pak file locations.
*/
static void GetPakFolders(const TCHAR* CmdLine, TArray<FString>& OutPakFolders);
/**
* Constructor.
*
* @param InLowerLevel Wrapper platform file.
*/
FPakPlatformFile();
/**
* Destructor.
*/
virtual ~FPakPlatformFile();
virtual bool ShouldBeUsed(IPlatformFile* Inner, const TCHAR* CmdLine) const override;
virtual bool Initialize(IPlatformFile* Inner, const TCHAR* CommandLineParam) override;
virtual IPlatformFile* GetLowerLevel() override
{
return LowerLevel;
}
virtual const TCHAR* GetName() const override
{
return FPakPlatformFile::GetTypeName();
}
/**
* Mounts a pak file at the specified path.
*
* @param InPakFilename Pak filename.
* @param InPath Path to mount the pak at.
*/
bool Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath = NULL);
bool Unmount(const TCHAR* InPakFilename);
/**
* Finds a file in the specified pak files.
*
* @param Paks Pak files to find the file in.
* @param Filename File to find in pak files.
* @param OutPakFile Optional pointer to a pak file where the filename was found.
* @return Pointer to pak entry if the file was found, NULL otherwise.
*/
FORCEINLINE static const FPakEntry* FindFileInPakFiles(TArray<FPakListEntry>& Paks,const TCHAR* Filename,FPakFile** OutPakFile)
{
FString StandardFilename(Filename);
FPaths::MakeStandardFilename(StandardFilename);
const FPakEntry* FoundEntry = NULL;
for (int32 PakIndex = 0; !FoundEntry && PakIndex < Paks.Num(); PakIndex++)
{
FoundEntry = Paks[PakIndex].PakFile->Find(*StandardFilename);
if (FoundEntry != NULL)
{
if (OutPakFile != NULL)
{
*OutPakFile = Paks[PakIndex].PakFile;
}
}
}
return FoundEntry;
}
/**
* Finds a file in all available pak files.
*
* @param Filename File to find in pak files.
* @param OutPakFile Optional pointer to a pak file where the filename was found.
* @return Pointer to pak entry if the file was found, NULL otherwise.
*/
const FPakEntry* FindFileInPakFiles(const TCHAR* Filename, FPakFile** OutPakFile = NULL)
{
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);
return FindFileInPakFiles(Paks, Filename, OutPakFile);
}
// BEGIN IPlatformFile Interface
virtual bool FileExists(const TCHAR* Filename) override
{
// Check pak files first.
if (FindFileInPakFiles(Filename) != NULL)
{
return true;
}
// File has not been found in any of the pak files, continue looking in inner platform file.
bool Result = LowerLevel->FileExists(Filename);
return Result;
}
virtual int64 FileSize(const TCHAR* Filename) override
{
// Check pak files first
const FPakEntry* FileEntry = FindFileInPakFiles(Filename);
if (FileEntry != NULL)
{
return FileEntry->CompressionMethod != COMPRESS_None ? FileEntry->UncompressedSize : FileEntry->Size;
}
// First look for the file in the user dir.
int64 Result = LowerLevel->FileSize(Filename);
return Result;
}
virtual bool DeleteFile(const TCHAR* Filename) override
{
// If file exists in pak file it will never get deleted.
if (FindFileInPakFiles(Filename) != NULL)
{
return false;
}
// The file does not exist in pak files, try LowerLevel->
bool Result = LowerLevel->DeleteFile(Filename);
return Result;
}
virtual bool IsReadOnly(const TCHAR* Filename) override
{
// Files in pak file are always read-only.
if (FindFileInPakFiles(Filename) != NULL)
{
return true;
}
// The file does not exist in pak files, try LowerLevel->
bool Result = LowerLevel->IsReadOnly(Filename);
return Result;
}
virtual bool MoveFile(const TCHAR* To, const TCHAR* From) override
{
// Files which exist in pak files can't be moved
if (FindFileInPakFiles(From) != NULL)
{
return false;
}
// Files not in pak are allowed to be moved.
bool Result = LowerLevel->MoveFile(To, From);
return Result;
}
virtual bool SetReadOnly(const TCHAR* Filename, bool bNewReadOnlyValue) override
{
// Files in pak file will never change their read-only flag.
if (FindFileInPakFiles(Filename) != NULL)
{
// This fails if soemone wants to make files from pak writable.
return bNewReadOnlyValue;
}
// Try lower level
bool Result = LowerLevel->SetReadOnly(Filename, bNewReadOnlyValue);
return Result;
}
virtual FDateTime GetTimeStamp(const TCHAR* Filename) override
{
// Check pak files first.
FPakFile* PakFile = NULL;
if (FindFileInPakFiles(Filename, &PakFile) != NULL)
{
return PakFile->GetTimestamp();
}
// Fall back to lower level.
FDateTime Result = LowerLevel->GetTimeStamp(Filename);
return Result;
}
virtual void SetTimeStamp(const TCHAR* Filename, FDateTime DateTime) override
{
// No modifications allowed on files from pak (although we could theoretically allow this one).
if (FindFileInPakFiles(Filename) == NULL)
{
LowerLevel->SetTimeStamp(Filename, DateTime);
}
}
virtual FDateTime GetAccessTimeStamp(const TCHAR* Filename) override
{
// AccessTimestamp not yet supported in pak files (although it is possible).
FPakFile* PakFile = NULL;
if (FindFileInPakFiles(Filename, &PakFile) != NULL)
{
return PakFile->GetTimestamp();
}
// Fall back to lower level.
FDateTime Result = LowerLevel->GetAccessTimeStamp(Filename);
return Result;
}
virtual FString GetFilenameOnDisk(const TCHAR* Filename) override
{
FPakFile* PakFile = NULL;
auto FileEntry = FindFileInPakFiles(Filename, &PakFile);
if (FileEntry)
{
const FString Path(FPaths::GetPath(Filename));
const FPakDirectory* PakDirectory = PakFile->FindDirectory(*Path);
if (PakDirectory != nullptr)
{
const FString* RealFilename = PakDirectory->FindKey(const_cast<FPakEntry*>(FileEntry));
if (RealFilename != nullptr)
{
return *RealFilename;
}
}
}
// Fall back to lower level.
return LowerLevel->GetFilenameOnDisk(Filename);
}
virtual IFileHandle* OpenRead(const TCHAR* Filename, bool bAllowWrite = false) override;
virtual IFileHandle* OpenWrite(const TCHAR* Filename, bool bAppend = false, bool bAllowRead = false) override
{
// No modifications allowed on pak files.
if (FindFileInPakFiles(Filename) != NULL)
{
return NULL;
}
// Use lower level to handle writing.
return LowerLevel->OpenWrite(Filename, bAppend, bAllowRead);
}
virtual bool DirectoryExists(const TCHAR* Directory) override
{
// Check pak files first.
if (DirectoryExistsInPakFiles(Directory))
{
return true;
}
// Directory does not exist in any of the pak files, continue searching using inner platform file.
bool Result = LowerLevel->DirectoryExists(Directory);
return Result;
}
virtual bool CreateDirectory(const TCHAR* Directory) override
{
// Directories can be created only under the normal path
return LowerLevel->CreateDirectory(Directory);
}
virtual bool DeleteDirectory(const TCHAR* Directory) override
{
// Even if the same directory exists outside of pak files it will never
// get truely deleted from pak and will still be reported by Iterate functions.
// Fail in cases like this.
if (DirectoryExistsInPakFiles(Directory))
{
return false;
}
// Directory does not exist in pak files so it's safe to delete.
return LowerLevel->DeleteDirectory(Directory);
}
/**
* Helper class to filter out files which have already been visited in one of the pak files.
*/
class FPakVisitor : public IPlatformFile::FDirectoryVisitor
{
public:
/** Wrapped visitor. */
FDirectoryVisitor& Visitor;
/** Visited pak files. */
TSet<FString>& VisitedPakFiles;
/** Cached list of pak files. */
TArray<FPakListEntry>& Paks;
/** Constructor. */
FPakVisitor(FDirectoryVisitor& InVisitor, TArray<FPakListEntry>& InPaks, TSet<FString>& InVisitedPakFiles)
: Visitor(InVisitor)
, VisitedPakFiles(InVisitedPakFiles)
, Paks(InPaks)
{}
virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory)
{
if (bIsDirectory == false)
{
FString StandardFilename(FilenameOrDirectory);
FPaths::MakeStandardFilename(StandardFilename);
if (VisitedPakFiles.Contains(StandardFilename))
{
// Already visited, continue iterating.
return true;
}
else if (FPakPlatformFile::FindFileInPakFiles(Paks, FilenameOrDirectory, NULL))
{
VisitedPakFiles.Add(StandardFilename);
}
}
return Visitor.Visit(FilenameOrDirectory, bIsDirectory);
}
};
virtual bool IterateDirectory(const TCHAR* Directory, IPlatformFile::FDirectoryVisitor& Visitor) override
{
bool Result = true;
TSet<FString> FilesVisitedInPak;
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);
// Iterate pak files first
for (int32 PakIndex = 0; PakIndex < Paks.Num(); PakIndex++)
{
FPakFile& PakFile = *Paks[PakIndex].PakFile;
const bool bIncludeFiles = true;
const bool bIncludeFolders = true;
TSet<FString> FilesVisitedInThisPak;
FString StandardDirectory = Directory;
FPaths::MakeStandardFilename(StandardDirectory);
PakFile.FindFilesAtPath(FilesVisitedInThisPak, *StandardDirectory, bIncludeFiles, bIncludeFolders);
for (TSet<FString>::TConstIterator SetIt(FilesVisitedInThisPak); SetIt && Result; ++SetIt)
{
const FString& Filename = *SetIt;
if (!FilesVisitedInPak.Contains(Filename))
{
bool bIsDir = Filename.Len() && Filename[Filename.Len() - 1] == '/';
if (bIsDir)
{
Result = Visitor.Visit(*Filename.LeftChop(1), true) && Result;
}
else
{
Result = Visitor.Visit(*Filename, false) && Result;
}
FilesVisitedInPak.Add(Filename);
}
}
}
if (Result && LowerLevel->DirectoryExists(Directory))
{
if (FilesVisitedInPak.Num())
{
// Iterate inner filesystem using FPakVisitor
FPakVisitor PakVisitor(Visitor, Paks, FilesVisitedInPak);
Result = LowerLevel->IterateDirectory(Directory, PakVisitor);
}
else
{
// No point in using FPakVisitor as it will only slow things down.
Result = LowerLevel->IterateDirectory(Directory, Visitor);
}
}
return Result;
}
virtual bool IterateDirectoryRecursively(const TCHAR* Directory, IPlatformFile::FDirectoryVisitor& Visitor) override
{
TSet<FString> FilesVisitedInPak;
TArray<FPakListEntry> Paks;
GetMountedPaks(Paks);
FPakVisitor PakVisitor(Visitor, Paks, FilesVisitedInPak);
return IPlatformFile::IterateDirectoryRecursively(Directory, PakVisitor);
}
virtual bool DeleteDirectoryRecursively(const TCHAR* Directory) override
{
// Can't delete directories existing in pak files. See DeleteDirectory(..) for more info.
if (DirectoryExistsInPakFiles(Directory))
{
return false;
}
// Directory does not exist in pak files so it's safe to delete.
return LowerLevel->DeleteDirectoryRecursively(Directory);
}
virtual bool CreateDirectoryTree(const TCHAR* Directory) override
{
// Directories can only be created only under the normal path
return LowerLevel->CreateDirectoryTree(Directory);
}
virtual bool CopyFile(const TCHAR* To, const TCHAR* From) override;
/**
* Converts a filename to a path inside pak file.
*
* @param Filename Filename to convert.
* @param Pak Pak to convert the filename realative to.
* @param Relative filename.
*/
FString ConvertToPakRelativePath(const TCHAR* Filename, const FPakFile* Pak)
{
FString RelativeFilename(Filename);
return RelativeFilename.Mid(Pak->GetMountPoint().Len());
}
FString ConvertToAbsolutePathForExternalAppForRead(const TCHAR* Filename) override
{
// Check in Pak file first
FPakFile* Pak = NULL;
if (FindFileInPakFiles(Filename, &Pak) != NULL)
{
return FString::Printf(TEXT("Pak: %s/%s"), *Pak->GetFilename(), *ConvertToPakRelativePath(Filename, Pak));
}
else
{
return LowerLevel->ConvertToAbsolutePathForExternalAppForRead(Filename);
}
}
FString ConvertToAbsolutePathForExternalAppForWrite(const TCHAR* Filename) override
{
// Check in Pak file first
FPakFile* Pak = NULL;
if (FindFileInPakFiles(Filename, &Pak) != NULL)
{
return FString::Printf(TEXT("Pak: %s/%s"), *Pak->GetFilename(), *ConvertToPakRelativePath(Filename, Pak));
}
else
{
return LowerLevel->ConvertToAbsolutePathForExternalAppForWrite(Filename);
}
}
// END IPlatformFile Interface
// BEGIN Console commands
#if !UE_BUILD_SHIPPING
void HandlePakListCommand(const TCHAR* Cmd, FOutputDevice& Ar);
void HandleMountCommand(const TCHAR* Cmd, FOutputDevice& Ar);
void HandleUnmountCommand(const TCHAR* Cmd, FOutputDevice& Ar);
#endif
// END Console commands
};