diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /filesystem/packfile.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'filesystem/packfile.cpp')
| -rw-r--r-- | filesystem/packfile.cpp | 1127 |
1 files changed, 1127 insertions, 0 deletions
diff --git a/filesystem/packfile.cpp b/filesystem/packfile.cpp new file mode 100644 index 0000000..5190959 --- /dev/null +++ b/filesystem/packfile.cpp @@ -0,0 +1,1127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//===========================================================================// + +#include "packfile.h" +#include "zip_utils.h" +#include "tier0/basetypes.h" +#include "tier1/convar.h" +#include "tier1/lzmaDecoder.h" +#include "tier1/utlbuffer.h" +#include "tier1/generichash.h" + +ConVar fs_monitor_read_from_pack( "fs_monitor_read_from_pack", "0", 0, "0:Off, 1:Any, 2:Sync only" ); + +// How many bytes we should decode at a time when doing pseudo-reads to seek forward in a compressed file handle, +// (affects maximum stack allocation by a forward seek) +#define COMPRESSED_SEEK_READ_CHUNK 1024 + +CPackFile::CPackFile() +{ + m_FileLength = 0; + m_hPackFileHandleFS = NULL; + m_fs = NULL; + m_nBaseOffset = 0; + m_bIsMapPath = false; + m_lPackFileTime = 0L; + m_refCount = 0; + m_nOpenFiles = 0; + m_PackFileID = 0; +} + +CPackFile::~CPackFile() +{ + if ( m_nOpenFiles ) + { + Error( "Closing pack file with %d open files!\n", m_nOpenFiles ); + } + + if ( m_hPackFileHandleFS ) + { + m_fs->FS_fclose( m_hPackFileHandleFS ); + m_hPackFileHandleFS = NULL; + } + + m_fs->m_ZipFiles.FindAndRemove( this ); +} + +int CPackFile::GetSectorSize() +{ + if ( m_hPackFileHandleFS ) + { + return m_fs->FS_GetSectorSize( m_hPackFileHandleFS ); + } +#if defined( SUPPORT_PACKED_STORE ) + else if ( m_hPackFileHandleVPK ) + { + return 2048; + } +#endif + else + { + return -1; + } +} + +// Read a bit of the file from the pack file: +int CZipPackFileHandle::Read( void* pBuffer, int nDestSize, int nBytes ) +{ + // Clamp nBytes to not go past the end of the file (async is still possible due to nDestSize) + if ( nBytes + m_nFilePointer > m_nLength ) + { + nBytes = m_nLength - m_nFilePointer; + } + + // Seek to the given file pointer and read + int nBytesRead = m_pOwner->ReadFromPack( m_nIndex, pBuffer, nDestSize, nBytes, m_nBase + m_nFilePointer ); + + m_nFilePointer += nBytesRead; + + return nBytesRead; +} + +// Seek around inside the pack: +int CZipPackFileHandle::Seek( int nOffset, int nWhence ) +{ + if ( nWhence == SEEK_SET ) + { + m_nFilePointer = nOffset; + } + else if ( nWhence == SEEK_CUR ) + { + m_nFilePointer += nOffset; + } + else if ( nWhence == SEEK_END ) + { + m_nFilePointer = m_nLength + nOffset; + } + + // Clamp the file pointer to the actual bounds of the file: + if ( m_nFilePointer > m_nLength ) + { + m_nFilePointer = m_nLength; + } + + return m_nFilePointer; +} + +//----------------------------------------------------------------------------- +// Open a file inside of a pack file. +//----------------------------------------------------------------------------- +CFileHandle *CZipPackFile::OpenFile( const char *pFileName, const char *pOptions ) +{ + int nIndex, nOriginalSize, nCompressedSize; + int64 nPosition; + unsigned short nCompressionMethod; + + // find the file's location in the pack + if ( GetFileInfo( pFileName, nIndex, nPosition, nOriginalSize, nCompressedSize, nCompressionMethod ) ) + { + m_mutex.Lock(); +#if defined( SUPPORT_PACKED_STORE ) + if ( m_nOpenFiles == 0 && m_hPackFileHandleFS == NULL && !m_hPackFileHandleVPK ) +#else + if ( m_nOpenFiles == 0 && m_hPackFileHandleFS == NULL ) +#endif + { + // Try to open it as a regular file first + m_hPackFileHandleFS = m_fs->Trace_FOpen( m_ZipName, "rb", 0, NULL ); + + // !NOTE! Pack files inside of VPK not supported + } + m_nOpenFiles++; + m_mutex.Unlock(); + CPackFileHandle* ph = NULL; + if ( nCompressionMethod == ZIP_COMPRESSION_LZMA ) + { + ph = new CLZMAZipPackFileHandle( this, nPosition, nOriginalSize, nCompressedSize, nIndex ); + } + else + { + AssertMsg( nCompressionMethod == ZIP_COMPRESSION_NONE, "Unsupported compression type in zip pack file" ); + ph = new CZipPackFileHandle( this, nPosition, nOriginalSize, nIndex ); + } + CFileHandle *fh = new CFileHandle( m_fs ); + fh->m_pPackFileHandle = ph; + fh->m_nLength = nOriginalSize; + + // The default mode for fopen is text, so require 'b' for binary + if ( strstr( pOptions, "b" ) == NULL ) + { + fh->m_type = FT_PACK_TEXT; + } + else + { + fh->m_type = FT_PACK_BINARY; + } + +#if !defined( _RETAIL ) + fh->SetName( pFileName ); +#endif + return fh; + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Get a directory entry from a pack's preload section +//----------------------------------------------------------------------------- +ZIP_PreloadDirectoryEntry* CZipPackFile::GetPreloadEntry( int nEntryIndex ) +{ + if ( !m_pPreloadHeader ) + { + return NULL; + } + + // If this entry doesn't have a corresponding preload entry, fail. + if ( m_PackFiles[nEntryIndex].m_nPreloadIdx == INVALID_PRELOAD_ENTRY ) + { + return NULL; + } + + return &m_pPreloadDirectory[m_PackFiles[nEntryIndex].m_nPreloadIdx]; +} + +//----------------------------------------------------------------------------- +// Read a file from the pack +//----------------------------------------------------------------------------- +int CZipPackFile::ReadFromPack( int nEntryIndex, void* pBuffer, int nDestBytes, int nBytes, int64 nOffset ) +{ + if ( nEntryIndex >= 0 ) + { + if ( nBytes <= 0 ) + { + return 0; + } + + // X360TBD: This is screwy, it works because m_nBaseOffset is 0 for preload capable zips + // It comes into play for files out of the embedded bsp zip, + // this hackery is a pre-bias expecting ReadFromPack() do a symmetric post bias, yuck. + + // Attempt to satisfy request from possible preload section, otherwise fall through + // A preload entry may be compressed + ZIP_PreloadDirectoryEntry *pPreloadEntry = GetPreloadEntry( nEntryIndex ); + if ( pPreloadEntry ) + { + // convert the absolute pack file position to a local file position + int nLocalOffset = nOffset - m_PackFiles[nEntryIndex].m_nPosition; + byte *pPreloadData = (byte*)m_pPreloadData + pPreloadEntry->DataOffset; + + if ( CLZMA::IsCompressed( pPreloadData ) ) + { + unsigned int actualSize = CLZMA::GetActualSize( pPreloadData ); + if ( nLocalOffset + nBytes <= (int)actualSize ) + { + // satisfy from compressed preload + if ( fs_monitor_read_from_pack.GetInt() == 1 ) + { + char szName[MAX_PATH]; + IndexToFilename( nEntryIndex, szName, sizeof( szName ) ); + Msg( "Read From Pack: [Preload] Requested:%d, Compressed:%d, %s\n", nBytes, pPreloadEntry->Length, szName ); + } + + if ( nLocalOffset == 0 && nDestBytes >= (int)actualSize && nBytes == (int)actualSize ) + { + // uncompress directly into caller's buffer + CLZMA::Uncompress( (unsigned char *)pPreloadData, (unsigned char *)pBuffer ); + return nBytes; + } + + // uncompress into temporary memory + CUtlMemory< byte > tempMemory; + tempMemory.EnsureCapacity( actualSize ); + CLZMA::Uncompress( pPreloadData, tempMemory.Base() ); + // copy only what caller expects + V_memcpy( pBuffer, (byte*)tempMemory.Base() + nLocalOffset, nBytes ); + return nBytes; + } + } + else if ( nLocalOffset + nBytes <= (int)pPreloadEntry->Length ) + { + // satisfy from uncompressed preload + if ( fs_monitor_read_from_pack.GetInt() == 1 ) + { + char szName[MAX_PATH]; + IndexToFilename( nEntryIndex, szName, sizeof( szName ) ); + Msg( "Read From Pack: [Preload] Requested:%d, Total:%d, %s\n", nBytes, pPreloadEntry->Length, szName ); + } + + V_memcpy( pBuffer, pPreloadData + nLocalOffset, nBytes ); + return nBytes; + } + } + } + +#if defined ( _X360 ) + // fell through as a direct request from within the pack + // intercept to possible embedded section + if ( m_pSection ) + { + // a section is a special update zip that has no files, only preload + // it has to be in the section + V_memcpy( pBuffer, (byte*)m_pSection + nOffset, nBytes ); + return nBytes; + } +#endif + + // Otherwise, do the read from the pack + m_mutex.Lock(); + + if ( fs_monitor_read_from_pack.GetInt() == 1 || ( fs_monitor_read_from_pack.GetInt() == 2 && ThreadInMainThread() ) ) + { + // spew info about real i/o request + char szName[MAX_PATH]; + IndexToFilename( nEntryIndex, szName, sizeof( szName ) ); + Msg( "Read From Pack: Sync I/O: Requested:%7d, Offset:0x%16.16llx, %s\n", nBytes, m_nBaseOffset + nOffset, szName ); + } + + int nBytesRead = 0; + // Seek to the start of the read area and perform the read: TODO: CHANGE THIS INTO A CFileHandle + if ( m_hPackFileHandleFS ) + { + m_fs->FS_fseek( m_hPackFileHandleFS, m_nBaseOffset + nOffset, SEEK_SET ); + nBytesRead = m_fs->FS_fread( pBuffer, nDestBytes, nBytes, m_hPackFileHandleFS ); + } +#if defined( SUPPORT_PACKED_STORE ) + else + { + // We're a packfile embedded in a VPK + m_hPackFileHandleVPK.Seek( m_nBaseOffset + nOffset, FILESYSTEM_SEEK_HEAD ); + nBytesRead = m_hPackFileHandleVPK.Read( pBuffer, nBytes ); + } +#endif + m_mutex.Unlock(); + + return nBytesRead; +} + +//----------------------------------------------------------------------------- +// Gets size, position, and index for a file in the pack. +//----------------------------------------------------------------------------- +bool CZipPackFile::GetFileInfo( const char *pFileName, int &nBaseIndex, int64 &nFileOffset, int &nOriginalSize, int &nCompressedSize, unsigned short &nCompressionMethod ) +{ + char szCleanName[MAX_FILEPATH]; + Q_strncpy( szCleanName, pFileName, sizeof( szCleanName ) ); +#ifdef _WIN32 + Q_strlower( szCleanName ); +#endif + Q_FixSlashes( szCleanName ); + + if ( !Q_RemoveDotSlashes( szCleanName, CORRECT_PATH_SEPARATOR, false ) ) + { + return false; + } + + CZipPackFile::CPackFileEntry lookup; + + // We may get passed non-canonicalized filenames, so we need to remove the ../ from the path + char szFixedName[MAX_PATH] = {0}; + V_strcpy_safe( szFixedName, pFileName ); + V_RemoveDotSlashes( szFixedName ); + + lookup.m_HashName = HashStringCaselessConventional( szFixedName ); + + int idx = m_PackFiles.Find( lookup ); + if ( -1 != idx ) + { + nFileOffset = m_PackFiles[idx].m_nPosition; + nOriginalSize = m_PackFiles[idx].m_nOriginalSize; + nCompressedSize = m_PackFiles[idx].m_nCompressedSize; + nBaseIndex = idx; + nCompressionMethod = m_PackFiles[idx].m_nCompressionMethod; + return true; + } + + return false; +} + +bool CZipPackFile::IndexToFilename( int nIndex, char *pBuffer, int nBufferSize ) +{ + AssertMsg( nIndex >= 0 && nIndex < m_PackFiles.Count(), "Out of bounds vector access in IndexToFilename" ); + if ( nIndex >= 0 ) + { + m_fs->String( m_PackFiles[nIndex].m_hFileName, pBuffer, nBufferSize ); + return true; + } + + Q_strncpy( pBuffer, "unknown", nBufferSize ); + + return false; +} + +//----------------------------------------------------------------------------- +// Find a file in the pack. +//----------------------------------------------------------------------------- +bool CZipPackFile::ContainsFile( const char *pFileName ) +{ + int nIndex, nOriginalSize, nCompressedSize; + int64 nOffset; + unsigned short nCompressionMethod; + bool bFound = GetFileInfo( pFileName, nIndex, nOffset, nOriginalSize, nCompressedSize, nCompressionMethod ); + return bFound; +} + +//----------------------------------------------------------------------------- +// Build a list of matching files and directories given a FindFirst() style wildcard +//----------------------------------------------------------------------------- +void CZipPackFile::GetFileAndDirLists( const char *pRawWildCard, CUtlStringList &outDirnames, CUtlStringList &outFilenames, bool bSortedOutput ) +{ + // See also: VPKlib function with same name. + + CUtlDict<int,int> AddedDirectories; // Used to remove duplicate paths + + char szWildCard[MAX_PATH] = { 0 }; + char szWildCardPath[MAX_PATH] = { 0 }; + char szWildCardBase[MAX_PATH] = { 0 }; + char szWildCardExt[MAX_PATH] = { 0 }; + + size_t nLenWildcardPath = 0; + size_t nLenWildcardBase = 0; + + bool bBaseWildcard = true; + bool bExtWildcard = true; + + // + // Parse the wildcard string into a base and extension used for string comparisons + // + V_strncpy( szWildCard, pRawWildCard, sizeof( szWildCard ) ); + V_FixSlashes( szWildCard, '/' ); + V_RemoveDotSlashes( szWildCard, '/', /* bRemoveDoubleSlashes */ true ); + + // Workaround edge case in crappy path code. ExtractFilePath extracts a/b/ from a/b/c/ but FileBase would return the empty string. + size_t nLenWildCard = V_strlen( szWildCard ); + if ( nLenWildCard && szWildCard[ nLenWildCard - 1 ] == '/' ) + { + V_strncpy( szWildCardPath, szWildCard, sizeof( szWildCardPath ) ); + } + else + { + V_ExtractFilePath( szWildCard, szWildCardPath, sizeof( szWildCardPath ) ); + } + + V_FileBase( szWildCard, szWildCardBase, sizeof( szWildCardBase ) ); + bool bWildcardHasExt = !!V_strrchr( szWildCard, '.' ); + V_ExtractFileExtension( szWildCard, szWildCardExt, sizeof( szWildCardExt ) ); + + // From the pattern, we now have the directory path up to the file pattern, the filename base, and the filename + // extension. + + // We don't support partial wildcards here (foo*bar.*). This support is massively inconsistent in our codebase and + // there's no one point where we implement it, so rather than trying to match one of our broken implementations + // (windows stdio is the only one I could find that was actually right), I'm going with "you shouldn't use this API + // for that". + bBaseWildcard = ( V_strcmp( szWildCardBase, "*" ) == 0 ); + bExtWildcard = ( V_strcmp( szWildCardExt, "*" ) == 0 ); + + if ( !bWildcardHasExt && bBaseWildcard ) + { + // For the special case of just '*' (and not, e.g., '*.') match '*.*' + bExtWildcard = true; + } + + nLenWildcardPath = V_strlen( szWildCardPath ); + nLenWildcardBase = V_strlen( szWildCardBase ); + + // Generate the list of directories and files that match the wildcard + // + + // For each candidate we attempt to walk up its path and consider the directories it represents as well (the + // directories in a zip only exist in that files contain them, there are no empty directories) + FOR_EACH_VEC( m_PackFiles, filesIdx ) + { + char szCandidateName[MAX_PATH] = { 0 }; + IndexToFilename( filesIdx, szCandidateName, sizeof( szCandidateName )); + + if ( !szCandidateName[0] ) + { + continue; + } + + // Check if this file starts with the wildcard selector's path. + // Note that we only ensure the prefix is the same. There are no specific entries for directories in a zip, they + // only exist in that files in the zip reference them, so handle subdirectory matches from filenames as well. + CUtlDict<int,int> ConsideredDirectories; // Will have duplicate directory matches when multiple files reside in them + if ( ( nLenWildcardPath && ( 0 == V_strnicmp( szCandidateName, szWildCardPath, nLenWildcardPath ) ) ) + || ( !nLenWildcardPath && strchr( szCandidateName, '/' ) ) ) + { + // Check if we matched because of a sub-directory, e.g. a/b/*.* would match /a/b/c/d/foo (in which case we + // want to add /a/b/c to the matched directories list, ignoring the actual specific file) + char szCandidateBaseName[MAX_PATH] = { 0 }; + bool bIsDir = false; + size_t nSubDirLen = 0; + char *pSubDirSlash = strchr( szCandidateName + nLenWildcardPath, '/' ); + if ( pSubDirSlash ) + { + // This is a subdirectory match, drop everything after it and continue with it as the filename + nSubDirLen = (size_t)( (ptrdiff_t)pSubDirSlash - (ptrdiff_t)( szCandidateName + nLenWildcardPath ) ); + V_strncpy( szCandidateBaseName, szCandidateName + nLenWildcardPath, nSubDirLen + 1 ); + bIsDir = true; + + // Early out if we already considered this exact directory from another file + if ( ConsideredDirectories.Find( szCandidateBaseName ) != ConsideredDirectories.InvalidIndex() ) + { + continue; + } + + ConsideredDirectories.Insert( szCandidateBaseName, 0 ); + } + else + { + V_strncpy( szCandidateBaseName, szCandidateName + nLenWildcardPath, sizeof( szCandidateBaseName ) ); + } + + char *pExt = strchr( szCandidateBaseName, '.' ); + if ( pExt ) + { + // Null out the . and move to point to the extension + *pExt = '\0'; + pExt++; + } + + // Determine if this file matches the wildcart (*.*, *.ext, ext.*) + bool bBaseMatch = false; + bool bExtMatch = false; + + // If we have a base dir name, and we have a szWildCardBase to match against + if ( bBaseWildcard ) + bBaseMatch = true; // The base is the wildCard ("*"), so whatever we have as the base matches + else + bBaseMatch = ( 0 == V_stricmp( szCandidateBaseName, szWildCardBase ) ); + + // If we have an extension and we have a szWildCardExtension to mach against + if ( ( bExtWildcard && pExt ) || ( !pExt && !bWildcardHasExt ) ) + bExtMatch = true; + else + bExtMatch = bWildcardHasExt && pExt && ( 0 == V_stricmp( pExt, szWildCardExt ) ); + + // If both parts match, then add it to the list + if ( bBaseMatch && bExtMatch ) + { + if ( bIsDir ) + { + // Pull up to the subdir we considered out of szCandidateName + size_t nMatchSize = nLenWildcardPath + nSubDirLen + 1; + char *pszFullMatch = new char[ nMatchSize ]; + V_strncpy( pszFullMatch, szCandidateName, nMatchSize ); + outDirnames.AddToTail( pszFullMatch ); + } + else + { + size_t nMatchSize = V_strlen( szCandidateName ) + 1; + char *pszFullMatch = new char[ nMatchSize ]; + V_strncpy( pszFullMatch, szCandidateName, nMatchSize ); + outFilenames.AddToTail( pszFullMatch ); + } + } + } + } + + // Sort the output if requested + if ( bSortedOutput ) + { + outDirnames.Sort( &CUtlStringList::SortFunc ); + outFilenames.Sort( &CUtlStringList::SortFunc ); + } +} + + +//----------------------------------------------------------------------------- +// Set up the preload section +//----------------------------------------------------------------------------- +void CZipPackFile::SetupPreloadData() +{ + if ( m_pPreloadHeader || !m_nPreloadSectionSize ) + { + // already loaded or not available + return; + } + + MEM_ALLOC_CREDIT_( "xZip" ); + + void *pPreload; +#if defined ( _X360 ) + if ( m_pSection ) + { + pPreload = (byte*)m_pSection + m_nPreloadSectionOffset; + } + else +#endif + { + pPreload = malloc( m_nPreloadSectionSize ); + if ( !pPreload ) + { + return; + } + + if ( IsX360() ) + { + // 360 XZips are always dvd aligned + Assert( ( m_nPreloadSectionSize % XBOX_DVD_SECTORSIZE ) == 0 ); + Assert( ( m_nPreloadSectionOffset % XBOX_DVD_SECTORSIZE ) == 0 ); + } + + // preload data is loaded as a single unbuffered i/o operation + ReadFromPack( -1, pPreload, -1, m_nPreloadSectionSize, m_nPreloadSectionOffset ); + } + + // setup the header + m_pPreloadHeader = (ZIP_PreloadHeader *)pPreload; + + // setup the preload directory + m_pPreloadDirectory = (ZIP_PreloadDirectoryEntry *)((byte *)m_pPreloadHeader + sizeof( ZIP_PreloadHeader ) ); + + // setup the remap table + m_pPreloadRemapTable = (unsigned short *)((byte *)m_pPreloadDirectory + m_pPreloadHeader->PreloadDirectoryEntries * sizeof( ZIP_PreloadDirectoryEntry ) ); + + // set the preload data base + m_pPreloadData = (byte *)m_pPreloadRemapTable + m_pPreloadHeader->DirectoryEntries * sizeof( unsigned short ); +} + +void CZipPackFile::DiscardPreloadData() +{ + if ( !m_pPreloadHeader ) + { + // already discarded + return; + } + +#if defined ( _X360 ) + // a section is an alias, the header becomes an alias, not owned memory + if ( !m_pSection ) + { + free( m_pPreloadHeader ); + } +#else + free( m_pPreloadHeader ); +#endif + m_pPreloadHeader = NULL; +} + +//----------------------------------------------------------------------------- +// Parse the zip file to build the file directory and preload section +//----------------------------------------------------------------------------- +bool CZipPackFile::Prepare( int64 fileLen, int64 nFileOfs ) +{ + if ( !fileLen || fileLen < sizeof( ZIP_EndOfCentralDirRecord ) ) + { + // nonsense zip + return false; + } + + // Pack files are always little-endian + m_swap.ActivateByteSwapping( IsX360() ); + + m_FileLength = fileLen; + m_nBaseOffset = nFileOfs; + + ZIP_EndOfCentralDirRecord rec = { 0 }; + + // Find and read the central header directory from its expected position at end of the file + bool bCentralDirRecord = false; + int64 offset = fileLen - sizeof( ZIP_EndOfCentralDirRecord ); + + // 360 can have an incompatible format + bool bCompatibleFormat = true; + if ( IsX360() ) + { + // 360 has dependable exact zips, backup to handle possible xzip format + if ( offset - XZIP_COMMENT_LENGTH >= 0 ) + { + offset -= XZIP_COMMENT_LENGTH; + } + + // single i/o operation, scanning forward + char *pTemp = (char *)_alloca( fileLen - offset ); + ReadFromPack( -1, pTemp, -1, fileLen - offset, offset ); + while ( offset <= (int64)(fileLen - sizeof( ZIP_EndOfCentralDirRecord )) ) + { + memcpy( &rec, pTemp, sizeof( ZIP_EndOfCentralDirRecord ) ); + m_swap.SwapFieldsToTargetEndian( &rec ); + if ( rec.signature == PKID( 5, 6 ) ) + { + bCentralDirRecord = true; + if ( rec.commentLength >= 4 ) + { + char *pComment = pTemp + sizeof( ZIP_EndOfCentralDirRecord ); + if ( !V_strnicmp( pComment, "XZP2", 4 ) ) + { + bCompatibleFormat = false; + } + } + break; + } + offset++; + pTemp++; + } + } + else + { + // scan entire file from expected location for central dir + for ( ; offset >= 0; offset-- ) + { + ReadFromPack( -1, (void*)&rec, -1, sizeof( rec ), offset ); + m_swap.SwapFieldsToTargetEndian( &rec ); + if ( rec.signature == PKID( 5, 6 ) ) + { + bCentralDirRecord = true; + break; + } + } + } + Assert( bCentralDirRecord ); + if ( !bCentralDirRecord ) + { + // no zip directory, bad zip + return false; + } + + int numFilesInZip = rec.nCentralDirectoryEntries_Total; + if ( numFilesInZip <= 0 ) + { + // empty valid zip + return true; + } + + int firstFileIdx = 0; + + MEM_ALLOC_CREDIT(); + + // read central directory into memory and parse + CUtlBuffer zipDirBuff( 0, rec.centralDirectorySize, 0 ); + zipDirBuff.EnsureCapacity( rec.centralDirectorySize ); + zipDirBuff.ActivateByteSwapping( IsX360() ); + ReadFromPack( -1, zipDirBuff.Base(), -1, rec.centralDirectorySize, rec.startOfCentralDirOffset ); + zipDirBuff.SeekPut( CUtlBuffer::SEEK_HEAD, rec.centralDirectorySize ); + + ZIP_FileHeader zipFileHeader; + char filename[MAX_PATH] = { 0 }; + + // Check for a preload section, expected to be the first file in the zip + zipDirBuff.GetObjects( &zipFileHeader ); + zipDirBuff.Get( filename, Min( (size_t)zipFileHeader.fileNameLength, sizeof(filename) - 1 ) ); + if ( !V_stricmp( filename, PRELOAD_SECTION_NAME ) ) + { + m_nPreloadSectionSize = zipFileHeader.uncompressedSize; + m_nPreloadSectionOffset = zipFileHeader.relativeOffsetOfLocalHeader + + sizeof( ZIP_LocalFileHeader ) + + zipFileHeader.fileNameLength + + zipFileHeader.extraFieldLength; + SetupPreloadData(); + + // Set up to extract the remaining files + int nextOffset = bCompatibleFormat ? zipFileHeader.extraFieldLength + zipFileHeader.fileCommentLength : 0; + zipDirBuff.SeekGet( CUtlBuffer::SEEK_CURRENT, nextOffset ); + firstFileIdx = 1; + } + else + { + if ( IsX360() ) + { + // all 360 zip files are expected to have preload sections + // only during development, maps are allowed to lack them, due to auto-conversion + if ( !m_bIsMapPath || g_pFullFileSystem->GetDVDMode() == DVDMODE_STRICT ) + { + Warning( "ZipFile '%s' missing preload section\n", m_ZipName.String() ); + } + } + + // No preload section, reset buffer pointer + zipDirBuff.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); + } + + // Parse out central directory and determine absolute file positions of data. + // Supports uncompressed zip files, with or without preload sections + bool bSuccess = true; + char tmpString[MAX_PATH] = { 0 }; + + m_PackFiles.EnsureCapacity( numFilesInZip ); + + for ( int i = firstFileIdx; i < numFilesInZip; ++i ) + { + CZipPackFile::CPackFileEntry lookup; + zipDirBuff.GetObjects( &zipFileHeader ); + + if ( zipFileHeader.signature != PKID( 1, 2 ) ) + { + Warning( "Invalid pack file signature\n" ); + bSuccess = false; + break; + } + + if ( zipFileHeader.compressionMethod != ZIP_COMPRESSION_NONE && zipFileHeader.compressionMethod != ZIP_COMPRESSION_LZMA ) + { + Warning( "Pack file uses unsupported compression method: %hi\n", zipFileHeader.compressionMethod ); + bSuccess = false; + break; + } + + Assert( zipFileHeader.fileNameLength < sizeof( tmpString ) ); + unsigned int fileNameLen = Min( (size_t)zipFileHeader.fileNameLength, sizeof( tmpString ) - 1 ); + zipDirBuff.Get( (void *)tmpString, fileNameLen ); + tmpString[fileNameLen] = '\0'; + Q_FixSlashes( tmpString ); + + lookup.m_hFileName = m_fs->FindOrAddFileName( tmpString ); + lookup.m_HashName = HashStringCaselessConventional( tmpString ); + lookup.m_nOriginalSize = zipFileHeader.uncompressedSize; + lookup.m_nCompressedSize = zipFileHeader.compressedSize; + lookup.m_nPosition = zipFileHeader.relativeOffsetOfLocalHeader + + sizeof( ZIP_LocalFileHeader ) + + zipFileHeader.fileNameLength + + zipFileHeader.extraFieldLength; + lookup.m_nCompressionMethod = zipFileHeader.compressionMethod; + + // track the index to this file's possible preload directory entry + if ( m_pPreloadRemapTable ) + { + lookup.m_nPreloadIdx = m_pPreloadRemapTable[i]; + } + else + { + lookup.m_nPreloadIdx = INVALID_PRELOAD_ENTRY; + } + m_PackFiles.InsertNoSort( lookup ); + + int nextOffset = bCompatibleFormat ? zipFileHeader.extraFieldLength + zipFileHeader.fileCommentLength : 0; + zipDirBuff.SeekGet( CUtlBuffer::SEEK_CURRENT, nextOffset ); + } + + m_PackFiles.RedoSort(); + + return bSuccess; +} + + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +CZipPackFile::CZipPackFile( CBaseFileSystem* fs, void *pSection ) + : m_PackFiles() +{ + m_fs = fs; + m_pPreloadDirectory = NULL; + m_pPreloadData = NULL; + m_pPreloadHeader = NULL; + m_pPreloadRemapTable = NULL; + m_nPreloadSectionOffset = 0; + m_nPreloadSectionSize = 0; + +#if defined( _X360 ) + m_pSection = pSection; +#endif +} + +CZipPackFile::~CZipPackFile() +{ + DiscardPreloadData(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : src1 - +// src2 - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CZipPackFile::CPackFileLessFunc::Less( CZipPackFile::CPackFileEntry const& src1, CZipPackFile::CPackFileEntry const& src2, void *pCtx ) +{ + return ( src1.m_HashName < src2.m_HashName ); +} + +//----------------------------------------------------------------------------- +// Purpose: Zip Pack file handle implementation +//----------------------------------------------------------------------------- +CZipPackFileHandle::CZipPackFileHandle( CZipPackFile* pOwner, int64 nBase, unsigned int nLength, unsigned int nIndex, unsigned int nFilePointer ) +{ + m_pOwner = pOwner; + m_nBase = nBase; + m_nLength = nLength; + m_nIndex = nIndex; + m_nFilePointer = nFilePointer; + pOwner->AddRef(); +} + +CZipPackFileHandle::~CZipPackFileHandle() +{ + m_pOwner->m_mutex.Lock(); + --m_pOwner->m_nOpenFiles; + // XXX(johns) this doesn't go here, the hell + if ( m_pOwner->m_nOpenFiles == 0 && m_pOwner->m_bIsMapPath ) + { + if ( m_pOwner->m_hPackFileHandleFS ) + { + m_pOwner->FileSystem()->Trace_FClose( m_pOwner->m_hPackFileHandleFS ); + m_pOwner->m_hPackFileHandleFS = NULL; + } + } + m_pOwner->Release(); + m_pOwner->m_mutex.Unlock(); +} + +void CZipPackFileHandle::SetBufferSize( int nBytes ) +{ + if ( m_pOwner->m_hPackFileHandleFS ) + { + m_pOwner->FileSystem()->FS_setbufsize( m_pOwner->m_hPackFileHandleFS, nBytes ); + } +} + +int CZipPackFileHandle::GetSectorSize() +{ + return m_pOwner->GetSectorSize(); +} + +int64 CZipPackFileHandle::AbsoluteBaseOffset() +{ + return m_pOwner->GetPackFileBaseOffset() + m_nBase; +} + +#if defined( _DEBUG ) && !defined( OSX ) +#include <atomic> +static std::atomic<int> sLZMAPackFileHandles( 0 ); +#endif // defined( _DEBUG ) && !defined( OSX ) + +CLZMAZipPackFileHandle::CLZMAZipPackFileHandle( CZipPackFile* pOwner, int64 nBase, unsigned int nOriginalSize, unsigned int nCompressedSize, + unsigned int nIndex, unsigned int nFilePointer ) + : CZipPackFileHandle( pOwner, nBase, nCompressedSize, nIndex, nFilePointer ), + m_BackSeekBuffer( 0, PACKFILE_COMPRESSED_FILEHANDLE_SEEK_BUFFER ), + m_ReadBuffer( 0, PACKFILE_COMPRESSED_FILEHANDLE_READ_BUFFER ), + m_pLZMAStream( NULL ), m_nSeekPosition( 0 ), m_nOriginalSize( nOriginalSize ) +{ + Reset(); +#if defined( _DEBUG ) && !defined( OSX ) + if ( ++sLZMAPackFileHandles == PACKFILE_COMPRESSED_FILE_HANDLES_WARNING ) + { + // By my count a live filehandle is currently around 270k, mostly due to the LZMA dictionary (256k) with the + // rest being the read/seek buffers. + Warning( "More than %u compressed file handles in use. " + "These carry large buffers around, and can cause high memory usage\n", + PACKFILE_COMPRESSED_FILE_HANDLES_WARNING ); + } +#endif // defined( _DEBUG ) && !defined( OSX ) +} + +CLZMAZipPackFileHandle::~CLZMAZipPackFileHandle() +{ + delete m_pLZMAStream; + m_pLZMAStream = NULL; +#if defined( _DEBUG ) && !defined( OSX ) + sLZMAPackFileHandles--; + Assert( sLZMAPackFileHandles >= 0 ); +#endif // defined( _DEBUG ) && !defined( OSX ) +} + +int CLZMAZipPackFileHandle::Read( void* pBuffer, int nDestSize, int nBytes ) +{ + int nMaxRead = Min( Min( nDestSize, nBytes ), Size() - Tell() ); + int nBytesRead = 0; + + // If we have seeked backwards into our buffer, read from there first + int nBackSeek = m_BackSeekBuffer.TellPut() - m_BackSeekBuffer.TellGet(); + Assert( nBackSeek >= 0 ); + if ( nBackSeek > 0 ) + { + int nBackSeekRead = Min( nBackSeek, nMaxRead ); + m_BackSeekBuffer.Get( pBuffer, nBackSeekRead ); + nBytesRead += nBackSeekRead; + } + + // Done if nothing to read + if ( nMaxRead - nBytesRead <= 0 ) + { + m_nSeekPosition += nBytesRead; + return nBytesRead; + } + + // Read bytes not fulfilled by backbuffer + Assert( m_BackSeekBuffer.TellPut() == m_BackSeekBuffer.TellGet() ); + while ( nBytesRead < nMaxRead ) + { + // refill read buffer if empty + int nRemainingReadBuffer = FillReadBuffer(); + + // Consume from read buffer + unsigned int nCompressedBytesRead = 0; + unsigned int nOutputBytesWritten = 0; + bool bSuccess = m_pLZMAStream->Read( (unsigned char *)m_ReadBuffer.PeekGet(), nRemainingReadBuffer, + (unsigned char *)pBuffer + nBytesRead, nMaxRead - nBytesRead, + nCompressedBytesRead, nOutputBytesWritten ); + + if ( bSuccess ) + { + // fixup get position + m_ReadBuffer.SeekGet( CUtlBuffer::SEEK_CURRENT, nCompressedBytesRead ); + + nBytesRead += nOutputBytesWritten; + + AssertMsg( nCompressedBytesRead == (unsigned int)nRemainingReadBuffer || nBytesRead == nMaxRead, + "Should have consumed the readbuffer or reached nMaxRead" ); + + if ( nCompressedBytesRead == 0 && nOutputBytesWritten == 0 ) + { + AssertMsg( nCompressedBytesRead > 0 || nOutputBytesWritten > 0, + "Stuck progress in read loop, aborting. Stream may be defunct." ); + break; + } + } + else + { + Warning( "Pack file: reading from LZMA stream failed\n" ); + break; + } + } + + // Finally, store last bytes output to the backseek buffer + + // If we read less than BackSeekBuffer.Size() bytes, shift the end of the old backseek buffer up + int nOldBackSeek = m_BackSeekBuffer.TellPut(); + int nReuseBackSeek = Max( Min( m_BackSeekBuffer.Size() - nBytesRead, nOldBackSeek ), 0 ); + if ( nReuseBackSeek ) + { + // Shift the reused chunk to the front + V_memmove( m_BackSeekBuffer.Base(), + (unsigned char *)m_BackSeekBuffer.Base() + m_BackSeekBuffer.TellPut() - nReuseBackSeek, + nReuseBackSeek ); + } + + // Update get/put position + m_BackSeekBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, nReuseBackSeek ); + m_BackSeekBuffer.SeekGet( CUtlBuffer::SEEK_HEAD, nReuseBackSeek ); + + // Fill in remainder from what we just read + int nReadIntoBackSeek = Min( m_BackSeekBuffer.Size() - nReuseBackSeek, nBytesRead ); + m_BackSeekBuffer.Put( (unsigned char *)pBuffer + nBytesRead - nReadIntoBackSeek, nReadIntoBackSeek ); + m_BackSeekBuffer.SeekGet( CUtlBuffer::SEEK_CURRENT, nReadIntoBackSeek ); + + m_nSeekPosition += nBytesRead; + return nBytesRead; +} + +int CLZMAZipPackFileHandle::Seek( int nOffset, int nWhence ) +{ + int nNewPosition = m_nSeekPosition; + + if ( nWhence == SEEK_CUR ) + { + nNewPosition = m_nSeekPosition + nOffset; + } + else if ( nWhence == SEEK_END ) + { + nNewPosition = Size() + nOffset; + } + else if ( nWhence == SEEK_SET ) + { + nNewPosition = nOffset; + } + else + { + AssertMsg( false, "Unknown seek type" ); + } + + nNewPosition = Min( Size(), nNewPosition ); + nNewPosition = Max( 0, nNewPosition ); + + if ( nNewPosition == m_nSeekPosition ) + { + return nNewPosition; + } + + // Backwards seek + if ( nNewPosition < m_nSeekPosition ) + { + int nBackSeekAvailable = m_BackSeekBuffer.TellGet(); + int nDesiredBackSeek = m_nSeekPosition - nNewPosition; + + if ( nBackSeekAvailable >= nDesiredBackSeek ) + { + // Move get backwards into backseek buffer to account for seek + m_BackSeekBuffer.SeekGet( CUtlBuffer::SEEK_CURRENT, -nDesiredBackSeek ); + m_nSeekPosition = nNewPosition; + } + else + { + // Seeking backwards beyond our backseek buffer. Have to restart stream. This kills the performance. + Warning( "LZMA file handle: seeking backwards beyond backseek buffer size ( %u ), " + "replaying read & decompression of %u bytes. Should avoid large back seeks in compressed files or " + "increase backseek buffer sizing.", + m_BackSeekBuffer.Size(), nNewPosition ); + + // Reset to beginning of underlying stream + Reset(); + + // Fall through to performing a forward seek + } + } + + // Forward seek + if ( nNewPosition > m_nSeekPosition ) + { + // Can't actually seek forward without making decode progress. Issue fake reads until we've reached our target. + unsigned char dummyBuffer[COMPRESSED_SEEK_READ_CHUNK]; + while ( nNewPosition > m_nSeekPosition ) + { + int nReadSize = Min( nNewPosition - m_nSeekPosition, COMPRESSED_SEEK_READ_CHUNK ); + unsigned int nBytesRead = Read( &dummyBuffer, sizeof(dummyBuffer), nReadSize ); + m_nSeekPosition += nBytesRead; + if ( !nBytesRead ) + { + Warning( "LZMA file handle: failed reading forward to desired seek position\n" ); + break; + } + } + } + + return m_nSeekPosition; +} + +int CLZMAZipPackFileHandle::Tell() +{ + return m_nSeekPosition; +} + +int CLZMAZipPackFileHandle::Size() +{ + return m_nOriginalSize; +} + +int CLZMAZipPackFileHandle::FillReadBuffer() +{ + int nRemainingReadBuffer = m_ReadBuffer.TellPut() - m_ReadBuffer.TellGet(); + int nRemainingCompressedBytes = CZipPackFileHandle::Size() - CZipPackFileHandle::Tell(); + + if ( nRemainingReadBuffer > 0 || nRemainingCompressedBytes <= 0 ) + { + // No action if read buffer isn't empty + return nRemainingReadBuffer; + } + + // Reset empty read buffer + m_ReadBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, 0 ); + m_ReadBuffer.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); + int nRefillSize = Min( nRemainingCompressedBytes, m_ReadBuffer.Size() ); + int nRefillResult = CZipPackFileHandle::Read( m_ReadBuffer.PeekPut(), m_ReadBuffer.Size(), nRefillSize ); + AssertMsg( nRefillSize == nRefillResult, "Don't expect to fail to read here" ); + + // Fixup put pointer after writing into buffer's memory + m_ReadBuffer.SeekPut( CUtlBuffer::SEEK_CURRENT, nRefillResult ); + + return nRefillResult; +} + +void CLZMAZipPackFileHandle::Reset() +{ + // Seek underlying stream back to start + CZipPackFileHandle::Seek( SEEK_SET, 0 ); + + delete m_pLZMAStream; + m_pLZMAStream = new CLZMAStream(); + m_pLZMAStream->InitZIPHeader( CZipPackFileHandle::Size(), m_nOriginalSize ); + m_nSeekPosition = 0; + m_BackSeekBuffer.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); + m_BackSeekBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, 0 ); + m_ReadBuffer.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); + m_ReadBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, 0 ); +} |