//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//===========================================================================//

#include "vpklib/packedstore.h"
#include "packedstore_internal.h"
#include "tier1/utlintrusivelist.h"
#include "tier1/generichash.h"
#include "tier1/checksum_crc.h"
#include "tier1/checksum_md5.h"
#include "tier1/utldict.h"
#include "tier2/fileutils.h"
#include "tier1/utlbuffer.h"

#ifdef VPK_ENABLE_SIGNING
	#include "crypto.h"
#endif


#ifdef IS_WINDOWS_PC
#include <windows.h>
#endif

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"


typedef uint16 PackFileIndex_t;
#define PACKFILEINDEX_END 0xffff
const uint16 packedfileindex_end = 0xffff;

#pragma pack(1)
struct CFilePartDescr
{
	PackFileIndex_t m_nFileNumber;
	uint32 m_nFileDataOffset;
	uint32 m_nFileDataSize;
};


struct CFileHeaderFixedData
{
	uint32 m_nFileCRC;
	uint16 m_nMetaDataSize;
	CFilePartDescr m_PartDescriptors[1];					// variable length

	FORCEINLINE const void *MetaData( void ) const;
	
	FORCEINLINE const CFilePartDescr *FileData( int nPart = 0 ) const;

	uint32 TotalDataSize( void ) const
	{
		return m_nMetaDataSize + m_PartDescriptors[0].m_nFileDataSize;
	}

	size_t HeaderSizeIncludingMetaData( void ) const
	{
		size_t nRet = sizeof( *this ) - sizeof( m_PartDescriptors ) + m_nMetaDataSize;
		// see how many parts we have and count the size of their descriptors
		CFilePartDescr const *pPart = m_PartDescriptors;
		while( pPart->m_nFileNumber != PACKFILEINDEX_END )
		{
			nRet += sizeof( CFilePartDescr );
			pPart++;
		}
		nRet += sizeof( PackFileIndex_t );					// count terminator
		return nRet;
	}

};
#pragma pack()

#define PACKEDFILE_DIR_HASH_SIZE 43

static int s_FileHeaderSize( char const *pName, int nNumDataParts, int nNumMetaDataBytes )
{
	return 1 + strlen( pName ) + 							// name plus nul
		sizeof( uint32 ) +									// file crc
		sizeof( uint16 ) + 									// meta data size
		nNumMetaDataBytes +									// metadata
		nNumDataParts * sizeof( CFilePartDescr ) +			// part data
		sizeof( PackFileIndex_t );							// part data 0xff end marker
}

class CFileDirectoryData
{
public:
	CFileDirectoryData *m_pNext;
	char const *m_Name;

};



// hash chain for accelerating file lookups. We can find an extension by hash, and find the
// directories containing files with this extension by another hash
class CFileExtensionData
{
public:
	CFileExtensionData *m_pNext;							// next one that has the same hash
	char const *m_Name;						// points at extension string within the directory data
	// nodes for each directory containing a file of this type
	CUtlIntrusiveList<CFileDirectoryData> m_pDirectoryHashTable[PACKEDFILE_DIR_HASH_SIZE];

	~CFileExtensionData( void )
	{
		for( int i = 0; i < ARRAYSIZE( m_pDirectoryHashTable ); i++ )
		{
			m_pDirectoryHashTable[i].Purge();
		}
	}

};


static int SkipFile( char const * &pData )					// returns highest file index
{
	int nHighestChunkIndex = -1;
	pData += 1 + V_strlen( pData );
	pData += sizeof( uint32 );

	uint16 nMetaDataSize;
	Q_memcpy( &nMetaDataSize, pData, sizeof( uint16 ) );

	pData += sizeof( uint16 );
	while ( Q_memcmp( pData, &packedfileindex_end, sizeof( packedfileindex_end ) ) != 0 )
	{
		int nIdx = reinterpret_cast<CFilePartDescr const *>(pData)->m_nFileNumber;

		if ( nIdx != VPKFILENUMBER_EMBEDDED_IN_DIR_FILE )
			nHighestChunkIndex = MAX( nHighestChunkIndex, nIdx  );
		pData += sizeof( CFilePartDescr );
	}
	pData += sizeof( PackFileIndex_t );
	pData += nMetaDataSize;
	return nHighestChunkIndex;
}

static inline int SkipAllFilesInDir( char const * & pData )
{
	int nHighestChunkIndex = -1;
	pData += 1 + strlen( pData );					// skip dir name
	// now, march through all the files
	while( *pData )									// until we're out of files to look at
	{
		int nSkipIndex = SkipFile( pData );
		nHighestChunkIndex = MAX( nHighestChunkIndex, nSkipIndex );
	}
	pData++;												// skip end marker
	return nHighestChunkIndex;
}


CFileHeaderFixedData *CPackedStore::FindFileEntry( char const *pDirname, char const *pBaseName, char const *pExtension, uint8 **pExtBaseOut , uint8 **pNameBaseOut )
{
	if ( pExtBaseOut )
		*pExtBaseOut = NULL;
	if ( pNameBaseOut )
		*pNameBaseOut = NULL;

	int nExtensionHash = HashString( pExtension ) % PACKEDFILE_EXT_HASH_SIZE;
	CFileExtensionData const *pExt = m_pExtensionData[nExtensionHash].FindNamedNodeCaseSensitive( pExtension );
	if ( pExt )
	{
		int nDirHash = HashString( pDirname ) % PACKEDFILE_DIR_HASH_SIZE;
		CFileDirectoryData const *pDir = pExt->m_pDirectoryHashTable[nDirHash].FindNamedNodeCaseSensitive( pDirname );
		if ( pDir )
		{
			if ( pExtBaseOut )
				*pExtBaseOut = (uint8 *) pDir;
			// we found the right directory. now, sequential search. data is heavily packed, so
			// this is a little awkward. See fileformat.txt
			char const *pData = pDir->m_Name;
			pData += 1 + strlen( pData );					// skip dir name
			// now, march through all the files
			while( *pData )									// until we're out of files to look at
			{
				if ( !V_strcmp( pData, pBaseName ) )		// found it?
				{
					if ( pNameBaseOut )
						*pNameBaseOut = (uint8 *) pData;

					return ( CFileHeaderFixedData * )( pData + 1 + V_strlen( pData ) ); // return header
				}
				// this isn't it - skip over it
				SkipFile( pData );
			}
		}
	}
	return NULL;
}


const void *CFileHeaderFixedData::MetaData( void ) const
{
	if ( ! m_nMetaDataSize )
		return NULL;
	const CFilePartDescr *ret = &( m_PartDescriptors[0] );
	while( ret->m_nFileNumber != PACKFILEINDEX_END )
		ret++;
	return reinterpret_cast<uint8 const *>( ret ) + sizeof( PackFileIndex_t );
}
	
CFilePartDescr const *CFileHeaderFixedData::FileData( int nPart ) const
{
	return m_PartDescriptors + nPart;
}

void CPackedStore::Init( void )
{
	m_nHighestChunkFileIndex = -1;
	m_bUseDirFile = false;
	m_pszFileBaseName[0] = 0;
	m_pszFullPathName[0] = 0;
	memset( m_pExtensionData, 0, sizeof( m_pExtensionData ) );
	m_nDirectoryDataSize = 0;
	m_nWriteChunkSize = k_nVPKDefaultChunkSize;

	m_nSizeOfSignedData = 0;
	m_Signature.Purge();
	m_SignaturePrivateKey.Purge();
	m_SignaturePublicKey.Purge();
}
   
void CPackedStore::BuildHashTables( void )
{
	m_nHighestChunkFileIndex = -1;
	for( int i = 0; i < ARRAYSIZE( m_pExtensionData ) ; i++ )
	{
		m_pExtensionData[i].Purge();
	}
	char const *pData = reinterpret_cast< char const *>( DirectoryData() );
	while( *pData )
	{
		// for each extension
		int nExtensionHash = HashString( pData ) % PACKEDFILE_EXT_HASH_SIZE;
		CFileExtensionData *pNewExt = new CFileExtensionData;
		pNewExt->m_Name = pData;
		m_pExtensionData[nExtensionHash].AddToHead( pNewExt );
		// now, iterate over all directories associated with this extension
		pData += 1 + strlen( pData );
		while( *pData )
		{
			int nDirHash = HashString( pData ) % PACKEDFILE_DIR_HASH_SIZE;
			CFileDirectoryData *pNewDir = new CFileDirectoryData;
			pNewDir->m_Name = pData;
			pNewExt->m_pDirectoryHashTable[nDirHash].AddToHead( pNewDir );
			int nDirChunk = SkipAllFilesInDir( pData );
			m_nHighestChunkFileIndex = MAX( m_nHighestChunkFileIndex, nDirChunk );
		}
		// step past \0
		pData++;
	}
}


bool CPackedStore::IsEmpty( void ) const
{
	return ( m_DirectoryData.Count() <= 1 );
}

static void StripTrailingString( char *pszBuf, const char *pszStrip )
{
	int lBuf = V_strlen( pszBuf );
	int lStrip = V_strlen( pszStrip );
	if ( lBuf < lStrip )
		return;
	char *pExpectedPos = pszBuf + lBuf - lStrip;
	if ( V_stricmp( pExpectedPos, pszStrip ) == 0 )
		*pExpectedPos = '\0';
}

CPackedStore::CPackedStore( char const *pFileBasename, char *pszFName, IBaseFileSystem *pFS, bool bOpenForWrite ):m_PackedStoreReadCache( pFS )
{
	Init();
	m_pFileSystem = pFS;
	m_PackedStoreReadCache.m_pPackedStore = this;
	m_DirectoryData.AddToTail( 0 );

	if ( pFileBasename )
	{
		V_strcpy( m_pszFileBaseName, pFileBasename );
		StripTrailingString( m_pszFileBaseName, ".vpk" );
		StripTrailingString( m_pszFileBaseName, "_dir" );
		sprintf( pszFName, "%s_dir.vpk", m_pszFileBaseName );
#ifdef _WIN32
		Q_strlower( pszFName );
#endif
		CInputFile dirFile( pszFName );

		// Try to load the VPK as a standalone (probably an addon) even if the standard _dir name is not present
		if ( dirFile.IsOk() )
		{
			m_bUseDirFile = true;
		}
		else
		{
			m_bUseDirFile = false;
			sprintf( pszFName, "%s.vpk", m_pszFileBaseName );
			dirFile.Open( pszFName );
		}

		bool bNewFileFormat = false;
		if ( dirFile.IsOk() )
		{
			// first, check if it is the new versioned variant
			VPKDirHeader_t dirHeader;
			// try to read the header.
			if ( 
				( dirFile.Read( &dirHeader, sizeof( dirHeader ) ) == sizeof( dirHeader ) ) &&
				( dirHeader.m_nHeaderMarker == VPK_HEADER_MARKER ) )
			{
				if ( dirHeader.m_nVersion == VPK_PREVIOUS_VERSION ) 
				{
					// fill in the fields of the new header.
					dirHeader.m_nEmbeddedChunkSize = dirFile.Size() - dirHeader.m_nDirectorySize - sizeof( VPKDirHeaderOld_t );
					dirHeader.m_nChunkHashesSize = 0;
					dirHeader.m_nSelfHashesSize = 0;
					dirHeader.m_nSignatureSize = 0;
					// pretend we didnt read the extra header
					dirFile.Seek( sizeof( VPKDirHeaderOld_t ) );
				}
				else if ( dirHeader.m_nVersion != VPK_CURRENT_VERSION )
				{
					Error( "Unknown version %d for vpk %s", dirHeader.m_nVersion, pFileBasename );
				}
				bNewFileFormat = true;
			}
			else											// its an old file
			{
				dirFile.Seek( 0 );
				// fill in a fake header, zero out garbage we read
				dirHeader.m_nDirectorySize = dirFile.Size();
				dirHeader.m_nEmbeddedChunkSize = 0;
				dirHeader.m_nChunkHashesSize = 0;
				dirHeader.m_nSelfHashesSize = 0;
				dirHeader.m_nSignatureSize = 0;
			}
			uint32 nSizeOfHeader = dirFile.Tell();
			int nSize = dirHeader.m_nDirectorySize;
			m_nDirectoryDataSize = dirHeader.m_nDirectorySize;
			m_DirectoryData.SetCount( nSize );
			dirFile.MustRead( DirectoryData(), nSize );
			// now, if we are opening for write, read the entire contents of the embedded data chunk in the dir into ram
			if ( bOpenForWrite && bNewFileFormat )
			{
				if ( dirHeader.m_nEmbeddedChunkSize )
				{
					CUtlVector<uint8> readBuffer;
					int nRemainingSize = dirHeader.m_nEmbeddedChunkSize;

					m_EmbeddedChunkData.EnsureCapacity( dirHeader.m_nEmbeddedChunkSize );

					// We'll allocate around half a meg of contiguous memory for the read. Any more and the SDK's VPK 
					// utility has a higher chance of choking on low-end machines.
					readBuffer.SetCount( 524288 );

					while ( nRemainingSize > 0 ) 
					{
						int nReadSize = MIN( nRemainingSize , 524288 );

						dirFile.MustRead( readBuffer.Base(), nReadSize );

						for ( int i = 0; i < nReadSize; i++ )
						{
							m_EmbeddedChunkData.AddToTail( readBuffer[i] );
						}
						nRemainingSize -= nReadSize;
					}
				}
			}
			int cbVecHashes = dirHeader.m_nChunkHashesSize;
			int ctHashes = cbVecHashes/sizeof(m_vecChunkHashFraction[0]);
			m_vecChunkHashFraction.EnsureCount( ctHashes );
			dirFile.MustRead( m_vecChunkHashFraction.Base(), cbVecHashes );
			FOR_EACH_VEC( m_vecChunkHashFraction, i )
			{
				int idxFound = m_vecChunkHashFraction.Find( m_vecChunkHashFraction[i] );
				Assert ( idxFound == i ); idxFound;
			}

			// now read the self hashes
			V_memset( m_DirectoryMD5.bits, 0, sizeof(m_DirectoryMD5.bits) );
			V_memset( m_ChunkHashesMD5.bits, 0, sizeof(m_ChunkHashesMD5.bits) );
			V_memset( m_TotalFileMD5.bits, 0, sizeof(m_TotalFileMD5.bits) );
			if ( dirHeader.m_nSelfHashesSize == 3*sizeof(m_DirectoryMD5.bits) )
			{
				// first is an MD5 of directory data
				dirFile.MustRead( m_DirectoryMD5.bits, sizeof(m_DirectoryMD5.bits) );
				// next is an MD5 of 
				dirFile.MustRead( m_ChunkHashesMD5.bits, sizeof(m_ChunkHashesMD5.bits) );
				// at this point the filesystem has calculated an MD5 of everything in the file up to this point.
				// we could ask it for a snapshot of that MD5 value and then be able to compare it to m_TotalFileMD5
				// but we would have to do it *before* we read it
				dirFile.MustRead( m_TotalFileMD5.bits, sizeof(m_TotalFileMD5.bits) );
			}

			// Is there a signature?
			m_nSizeOfSignedData = 0;
			if ( dirHeader.m_nSignatureSize != 0 )
			{

				// Everything immediately proceeding it should have been signed.
				m_nSizeOfSignedData = dirFile.Tell();
				uint32 nExpectedSignedSize = nSizeOfHeader + dirHeader.ComputeSizeofSignedDataAfterHeader();
				if ( m_nSizeOfSignedData != nExpectedSignedSize )
				{
					Error( "Size mismatch determining size of signed data block (%d vs %d)", m_nSizeOfSignedData, nExpectedSignedSize );
				}

				// Read the public key
				uint32 cubPublicKey = 0;
				dirFile.MustRead( &cubPublicKey, sizeof(cubPublicKey) );
				m_SignaturePublicKey.SetCount( cubPublicKey );
				dirFile.MustRead( m_SignaturePublicKey.Base(), cubPublicKey );

				// Read the private key
				uint32 cubSignature = 0;
				dirFile.MustRead( &cubSignature, sizeof(cubSignature) );
				m_Signature.SetCount( cubSignature );
				dirFile.MustRead( m_Signature.Base(), cubSignature );
			}
		}
		Q_MakeAbsolutePath( m_pszFullPathName, sizeof( m_pszFullPathName ), m_pszFileBaseName );
		V_strcat_safe( m_pszFullPathName, ".vpk" );
		//Q_strlower( m_pszFullPathName ); // NO!  this screws up linux.
		Q_FixSlashes( m_pszFullPathName );
	}
	BuildHashTables();
}


void CPackedStore::GetDataFileName( char *pchFileNameOut, int cchFileNameOut, int nFileNumber ) const
{
	if ( nFileNumber == VPKFILENUMBER_EMBEDDED_IN_DIR_FILE )
	{
		if ( m_bUseDirFile )
		{
			V_snprintf( pchFileNameOut, cchFileNameOut, "%s_dir.vpk", m_pszFileBaseName );
		}
		else
		{
			V_snprintf( pchFileNameOut, cchFileNameOut, "%s.vpk", m_pszFileBaseName );
		}
	}
	else
	{
		V_snprintf( pchFileNameOut, cchFileNameOut, "%s_%03d.vpk", m_pszFileBaseName, nFileNumber );
	}

}

CPackedStore::~CPackedStore( void )
{
	for( int i = 0; i < ARRAYSIZE( m_pExtensionData ) ; i++ )
	{
		m_pExtensionData[i].Purge();
	}

	for (int i = 0; i < ARRAYSIZE( m_FileHandles ); i++ )
	{
		if ( m_FileHandles[i].m_nFileNumber != -1 )
		{
#ifdef IS_WINDOWS_PC
			CloseHandle( m_FileHandles[i].m_hFileHandle );
#else
			m_pFileSystem->Close( m_FileHandles[i].m_hFileHandle );
#endif

		}
	}

	// Free the FindFirst cache data
	m_directoryList.PurgeAndDeleteElementsArray();

	FOR_EACH_MAP( m_dirContents, i )
	{
		m_dirContents[i]->PurgeAndDeleteElementsArray();
		delete m_dirContents[i];
	}
}

void SplitFileComponents( char const *pFileName, char *pDirOut, char *pBaseOut, char *pExtOut )
{
	char pTmpDirOut[MAX_PATH];
	V_ExtractFilePath( pFileName, pTmpDirOut, MAX_PATH );
	// now, pTmpDirOut to pDirOut, except when we find more then one '\' in a row, only output one
	char *pOutDirPtr = pDirOut;
	for( char *pDirInPtr = pTmpDirOut; *pDirInPtr; pDirInPtr++ )
	{
		char c = *( pDirInPtr );
		*( pOutDirPtr++ ) = c;
		// if we copied a \, skip all subsequent slashes
		while( ( c == '\\'  ) && ( pDirInPtr[1] == c ) )
		{
			pDirInPtr++;
		}
	}
	*( pOutDirPtr ) = 0;									// null terminate

	if ( !pDirOut[0] )
		strcpy( pDirOut, " " );								// blank dir name
	V_strncpy( pBaseOut, V_UnqualifiedFileName( pFileName ), MAX_PATH );
	char *pDot = strrchr( pBaseOut, '.' );
	if ( pDot )
	{
		*pDot = 0;
		V_strncpy( pExtOut, pDot+1, MAX_PATH );
	}
	else
	{
		pExtOut[0]=' ';
		pExtOut[1]=0;
	}

	V_FixSlashes( pDirOut, '/' );
	V_strlower( pDirOut );

	// the game sometimes asks for paths like dir1/../dir2/ we will replace this with dir2/. This
	// one line of perl code sucks in c++.
	for(;;)
	{
		char *pDotDot = V_strstr( pDirOut + 1, "/../" );		// start at second char. we don't want a beginning /
		if (! pDotDot )
		{
			break;
		}
		// search backwards from the /.. for the previous directory part
		char *pPrevSlash = pDotDot - 1;
		while( ( pPrevSlash > pDirOut ) && ( pPrevSlash[0] != '/' ) )
		{
			pPrevSlash--;
		}
		// if our path was dir0/dir1/../dir2, we are now pointing at "/dir1".
		// is strmove in all compilers? that would be better than this loop
		char *pStrIn = pDotDot + 3;
		for(;;)
		{
			*pPrevSlash = *pStrIn;
			if ( pStrIn[0] )
			{
				++pPrevSlash;
				++pStrIn;
			}
			else
			{
				break;
			}
		}
	}

	
	char *pLastDirChar = pDirOut + strlen( pDirOut ) - 1;
	if ( ( pLastDirChar[0] == '/' ) || ( pLastDirChar[0] == '\\' ) )
		*pLastDirChar = 0;									// kill trailing slash
	V_strlower( pBaseOut );
	V_strlower( pExtOut );
}

CPackedStoreFileHandle CPackedStore::OpenFile( char const *pFileName )
{
	char dirName[MAX_PATH];
	char baseName[MAX_PATH];
	char extName[MAX_PATH];

	// Fix up the filename first
	char tempFileName[MAX_PATH];

	V_strncpy( tempFileName, pFileName, sizeof( tempFileName ) );
	V_FixSlashes( tempFileName, CORRECT_PATH_SEPARATOR );
//	V_RemoveDotSlashes( tempFileName, CORRECT_PATH_SEPARATOR, true );
	V_FixDoubleSlashes( tempFileName );
	if ( !V_IsAbsolutePath( tempFileName ) )
	{
		V_strlower( tempFileName );
	}
	
	SplitFileComponents( tempFileName, dirName, baseName, extName );

	CPackedStoreFileHandle ret;

	CFileHeaderFixedData *pHeader = FindFileEntry( dirName, baseName, extName, NULL, &( ret.m_pDirFileNamePtr ) );
	
	if ( pHeader )
	{
		ret.m_nFileNumber = pHeader->m_PartDescriptors[0].m_nFileNumber;
		ret.m_nFileOffset = pHeader->m_PartDescriptors[0].m_nFileDataOffset;
		ret.m_nFileSize = pHeader->m_PartDescriptors[0].m_nFileDataSize + pHeader->m_nMetaDataSize;
		ret.m_nCurrentFileOffset = 0;
		ret.m_pMetaData = pHeader->MetaData();
		ret.m_nMetaDataSize = pHeader->m_nMetaDataSize;
  		ret.m_pHeaderData = pHeader;
		ret.m_pOwner = this;
	}
	else
	{
		ret.m_nFileNumber = -1;
		ret.m_pOwner = NULL;
	}
	return ret;

}

CPackedStoreFileHandle CPackedStore::GetHandleForHashingFiles()
{
	CPackedStoreFileHandle ret;
	ret.m_nFileNumber = 0;
	ret.m_nFileOffset = 0;
	ret.m_nFileSize = 0;
	ret.m_nMetaDataSize = 0;
	ret.m_nCurrentFileOffset = 0;
	ret.m_pDirFileNamePtr = NULL;
	ret.m_pHeaderData = NULL;
	ret.m_pMetaData = NULL;
	ret.m_pOwner = this;
	return ret;
}

void CPackedStore::Write( void )
{
	// !KLUDGE!
	// Write the whole header into a buffer in memory.
	// We do this so we can easily sign it.
	CUtlBuffer bufDirFile;

	VPKDirHeader_t headerOut;
	headerOut.m_nDirectorySize = m_DirectoryData.Count();
	headerOut.m_nEmbeddedChunkSize = m_EmbeddedChunkData.Count();
	headerOut.m_nChunkHashesSize = m_vecChunkHashFraction.Count()*sizeof(m_vecChunkHashFraction[0]);
	headerOut.m_nSelfHashesSize = 3*sizeof(m_DirectoryMD5.bits);
	headerOut.m_nSignatureSize = 0;

	// Do we plan on signing this thing and writing a signature?
	m_Signature.Purge();
	if ( m_SignaturePrivateKey.Count() > 0 && m_SignaturePublicKey.Count() > 0 )
	{
		#ifdef VPK_ENABLE_SIGNING
			uint32 nExpectedSignatureSize = k_cubRSASignature;
			headerOut.m_nSignatureSize = sizeof(uint32) + m_SignaturePublicKey.Count() + sizeof(uint32) + nExpectedSignatureSize;
		#else
			Error( "VPK signing not implemented" );
		#endif
	}

	bufDirFile.Put( &headerOut, sizeof( headerOut ) );
	bufDirFile.Put( DirectoryData(), m_DirectoryData.Count() );

	if ( m_EmbeddedChunkData.Count() )
	{
		int nRemainingSize = m_EmbeddedChunkData.Count();
		CUtlVector<uint8> writeBuffer;
		
		writeBuffer.SetCount( 524288 );
		int nChunkOffset = 0;

		while ( nRemainingSize > 0 )
		{
			// We'll write around half a meg of contiguous memory at once. Any more and the SDK's VPK 
			// utility has a higher chance of choking on low-end machines.
			int nWriteSize = MIN( nRemainingSize, 524288 );
			
			for ( int i = 0; i < nWriteSize; i++ )
			{
				writeBuffer[i] = m_EmbeddedChunkData[nChunkOffset++];
			}

			bufDirFile.Put( writeBuffer.Base(), nWriteSize );
			nRemainingSize -= nWriteSize;
		}
	}

	// write the chunk hashes out
	bufDirFile.Put( m_vecChunkHashFraction.Base(), m_vecChunkHashFraction.Count()*sizeof(m_vecChunkHashFraction[0]) );

	// write out the MD5s of the 2 main pieces of data
	bufDirFile.Put( m_DirectoryMD5.bits, sizeof( m_DirectoryMD5.bits ) );
	bufDirFile.Put( m_ChunkHashesMD5.bits, sizeof( m_ChunkHashesMD5.bits ) );

	// compute the final MD5 ( of everything in the file up to this point )
	MD5_ProcessSingleBuffer( bufDirFile.Base(), bufDirFile.TellPut(), m_TotalFileMD5 );
	bufDirFile.Put( m_TotalFileMD5.bits, sizeof( m_TotalFileMD5.bits ) );

	// Should we sign all this stuff?
	m_nSizeOfSignedData = 0;
	#ifdef VPK_ENABLE_SIGNING
		if ( headerOut.m_nSignatureSize > 0 )
		{

			m_nSizeOfSignedData = bufDirFile.TellPut();
			uint32 nExpectedSignedSize = sizeof(headerOut) + headerOut.ComputeSizeofSignedDataAfterHeader();
			if ( m_nSizeOfSignedData != nExpectedSignedSize )
			{
				Error( "Size mismatch determining size of signed data block (%d vs %d)", m_nSizeOfSignedData, nExpectedSignedSize );
			}

			// Allocate more than enough space to hold the signature
			m_Signature.SetCount( nExpectedSignatureSize + 1024 );

			// Calcuate the signature
			uint32 cubSignature = m_Signature.Count();
			if ( !CCrypto::RSASignSHA256( (const uint8 *)bufDirFile.Base(), bufDirFile.TellPut(), 
				(uint8 *)m_Signature.Base(), &cubSignature, 
				(const uint8 *)m_SignaturePrivateKey.Base(), m_SignaturePrivateKey.Count() ) )
			{
				Error( "VPK signing failed.  Private key may be corrupt or invalid" );
			}

			// Confirm that the size was what we expected
			if ( cubSignature != nExpectedSignatureSize )
			{
				Error( "VPK signing produced %d byte signature.  Expected size was %d bytes", cubSignature, nExpectedSignatureSize );
			}

			// Shrink signature to fit
			m_Signature.SetCountNonDestructively( cubSignature );

			// Now re-check the signature, using the public key that we are about
			// to burn into the file, to make sure there's no mismatch.
			if ( !CCrypto::RSAVerifySignatureSHA256( (const uint8 *)bufDirFile.Base(), bufDirFile.TellPut(), 
				(const uint8 *)m_Signature.Base(), cubSignature, 
				(const uint8 *)m_SignaturePublicKey.Base(), m_SignaturePublicKey.Count() ) )
			{
				Error( "VPK signature verification failed immediately after signing.  The public key might be invalid, or might not match the private key used to generate the signature." );
			}

			// Write public key which should be used
			uint32 cubPublicKey = m_SignaturePublicKey.Count();
			bufDirFile.Put( &cubPublicKey, sizeof(cubPublicKey) );
			bufDirFile.Put( m_SignaturePublicKey.Base(), cubPublicKey );

			// Write signature
			bufDirFile.Put( &cubSignature, sizeof(cubSignature) );
			bufDirFile.Put( m_Signature.Base(), cubSignature );
		}
	#endif

	char szOutFileName[MAX_PATH];

	// Delete any existing header file, either the standalone kind,
	// or the _dir kind.
	V_sprintf_safe( szOutFileName, "%s.vpk", m_pszFileBaseName );
	if ( g_pFullFileSystem->FileExists( szOutFileName ) )
		g_pFullFileSystem->RemoveFile( szOutFileName );
	V_sprintf_safe( szOutFileName, "%s_dir.vpk", m_pszFileBaseName );
	if ( g_pFullFileSystem->FileExists( szOutFileName ) )
		g_pFullFileSystem->RemoveFile( szOutFileName );

	// Force on multi-chunk mode if we have any files in a chunk
	if ( m_nHighestChunkFileIndex >= 0 )
		m_bUseDirFile = true;


	// Fetch actual name to write
	GetDataFileName( szOutFileName, sizeof(szOutFileName), VPKFILENUMBER_EMBEDDED_IN_DIR_FILE );

	// Now actually write the data to disk
	COutputFile dirFile( szOutFileName );
	dirFile.Write( bufDirFile.Base(), bufDirFile.TellPut() );
	dirFile.Close();
}

#ifdef VPK_ENABLE_SIGNING

void CPackedStore::SetKeysForSigning( int nPrivateKeySize, const void *pPrivateKeyData, int nPublicKeySize, const void *pPublicKeyData )
{
	m_SignaturePrivateKey.SetSize( nPrivateKeySize );
	V_memcpy( m_SignaturePrivateKey.Base(), pPrivateKeyData, nPrivateKeySize );

	m_SignaturePublicKey.SetSize( nPublicKeySize );
	V_memcpy( m_SignaturePublicKey.Base(), pPublicKeyData, nPublicKeySize );

	// Discard any existing signature
	m_Signature.Purge();
}

CPackedStore::ESignatureCheckResult CPackedStore::CheckSignature( int nSignatureSize, const void *pSignature ) const
{
	if ( m_Signature.Count() == 0 )
		return eSignatureCheckResult_NotSigned;

	Assert( m_nSizeOfSignedData > 0 );

	// Confirm correct public key, if they specified one.
	if ( nSignatureSize > 0 && pSignature != NULL )
	{
		if ( m_SignaturePublicKey.Count() != nSignatureSize || V_memcmp( pSignature, m_SignaturePublicKey.Base(), nSignatureSize ) != 0 )
		{
			return eSignatureCheckResult_WrongKey;
		}
	}

	char szFilename[ MAX_PATH ];
	GetDataFileName( szFilename, sizeof( szFilename ), VPKFILENUMBER_EMBEDDED_IN_DIR_FILE );

	// Read the data
	CUtlBuffer bufSignedData;
	if ( !g_pFullFileSystem->ReadFile( szFilename, NULL, bufSignedData, m_nSizeOfSignedData ) )
		return eSignatureCheckResult_Failed;
	if ( bufSignedData.TellPut() < (int)m_nSizeOfSignedData )
	{
		Assert( false ); // ?
		return eSignatureCheckResult_Failed;
	}

	// Check the signature
	if ( !CCrypto::RSAVerifySignatureSHA256( (const uint8 *)bufSignedData.Base(), m_nSizeOfSignedData, 
		(const uint8 *)m_Signature.Base(), m_Signature.Count(), 
		(const uint8 *)m_SignaturePublicKey.Base(), m_SignaturePublicKey.Count() ) )
	{
		return eSignatureCheckResult_InvalidSignature;
	}

	return eSignatureCheckResult_ValidSignature;
}

#endif

CPackedStoreReadCache::CPackedStoreReadCache( IBaseFileSystem *pFS ):m_treeCachedVPKRead( CachedVPKRead_t::Less )
{
	m_pPackedStore = NULL;
	m_cItemsInCache = 0;
	m_pFileSystem = pFS;
	m_cubReadFromCache = 0;
	m_cReadFromCache = 0;
	m_cDiscardsFromCache = 0;
	m_cAddedToCache = 0;
	m_cCacheMiss = 0;
	m_cubCacheMiss = 0;
	m_cFileErrors = 0;
	m_cFileErrorsCorrected = 0;
	m_cFileResultsDifferent = 0;
	m_pFileTracker = NULL;
}

// check if the read request can be satisfied from the read cache we have in 1MB chunks
bool CPackedStoreReadCache::BCanSatisfyFromReadCache( uint8 *pOutData, CPackedStoreFileHandle &handle, FileHandleTracker_t &fHandle, int nDesiredPos, int nNumBytes, int &nRead )
{
	nRead = 0;
	int nFileFraction = nDesiredPos & k_nCacheBufferMask;
	int nOffset = nDesiredPos - nFileFraction;
	int cubReadChunk = nOffset + nNumBytes;
	if ( cubReadChunk > k_cubCacheBufferSize )
		cubReadChunk = ( k_nCacheBufferMask - nOffset ) & (k_cubCacheBufferSize-1);
	else
		cubReadChunk = nNumBytes;
	// the request might straddle multiple chunks - we make sure we have all of the data, if we are missing any, we fail
	while ( nNumBytes )
	{
		int nReadChunk = 0;
		if ( !BCanSatisfyFromReadCacheInternal( pOutData, handle, fHandle, nDesiredPos, cubReadChunk, nReadChunk ) )
		{
			return false;
		}
		nNumBytes -= cubReadChunk;
		pOutData += cubReadChunk;
		nDesiredPos += cubReadChunk;
		nRead += nReadChunk;
		nFileFraction += k_cubCacheBufferSize;
		cubReadChunk = nNumBytes;
		if ( cubReadChunk > k_cubCacheBufferSize )
			cubReadChunk = k_cubCacheBufferSize;
	}
	return true;
}


// read a single line into the cache
bool CPackedStoreReadCache::ReadCacheLine( FileHandleTracker_t &fHandle, CachedVPKRead_t &cachedVPKRead )
{
	cachedVPKRead.m_cubBuffer = 0;
#ifdef IS_WINDOWS_PC
	if ( cachedVPKRead.m_nFileFraction != fHandle.m_nCurOfs )
		SetFilePointer ( fHandle.m_hFileHandle, cachedVPKRead.m_nFileFraction, NULL,  FILE_BEGIN); 
	ReadFile( fHandle.m_hFileHandle, cachedVPKRead.m_pubBuffer, k_cubCacheBufferSize, (LPDWORD) &cachedVPKRead.m_cubBuffer, NULL );
	SetFilePointer ( fHandle.m_hFileHandle, fHandle.m_nCurOfs, NULL,  FILE_BEGIN); 
#else
	m_pFileSystem->Seek( fHandle.m_hFileHandle, cachedVPKRead.m_nFileFraction, FILESYSTEM_SEEK_HEAD );
	cachedVPKRead.m_cubBuffer = m_pFileSystem->Read( cachedVPKRead.m_pubBuffer, k_cubCacheBufferSize, fHandle.m_hFileHandle );
	m_pFileSystem->Seek( fHandle.m_hFileHandle, fHandle.m_nCurOfs, FILESYSTEM_SEEK_HEAD );
#endif
	Assert( cachedVPKRead.m_hMD5RequestHandle == 0 );
	if ( m_pFileTracker ) // file tracker doesn't exist in the VPK command line tool
	{
		cachedVPKRead.m_hMD5RequestHandle = m_pFileTracker->SubmitThreadedMD5Request( cachedVPKRead.m_pubBuffer, cachedVPKRead.m_cubBuffer, m_pPackedStore->m_PackFileID, cachedVPKRead.m_nPackFileNumber, cachedVPKRead.m_nFileFraction );
	}
	return cachedVPKRead.m_cubBuffer > 0;
}


// check if the MD5 matches
bool CPackedStoreReadCache::CheckMd5Result( CachedVPKRead_t &cachedVPKRead )
{
	ChunkHashFraction_t chunkHashFraction;
	if ( !m_pPackedStore->FindFileHashFraction( cachedVPKRead.m_nPackFileNumber, cachedVPKRead.m_nFileFraction, chunkHashFraction ) )
		return true;

	if ( Q_memcmp( &cachedVPKRead.m_md5Value, &chunkHashFraction.m_md5contents, sizeof( MD5Value_t ) ) != 0	)
	{
		char szFilename[ 512 ];
		m_pPackedStore->GetDataFileName( szFilename, sizeof(szFilename), cachedVPKRead.m_nPackFileNumber );

		char szCalculated[ MD5_DIGEST_LENGTH*2 + 4 ];
		char szExpected[ MD5_DIGEST_LENGTH*2 + 4 ];
		V_binarytohex( cachedVPKRead.m_md5Value.bits, MD5_DIGEST_LENGTH, szCalculated, sizeof(szCalculated) );
		V_binarytohex( chunkHashFraction.m_md5contents.bits, MD5_DIGEST_LENGTH, szExpected, sizeof(szExpected) );

		Warning(
			"Corruption detected in %s\n"
			"\n"
			"Try verifying the integrity of your game cache.\n"
			"https://support.steampowered.com/kb_article.php?ref=2037-QEUH-3335"
			"\n"
			"Offset %d, expected %s, got %s\n",
			szFilename,
			cachedVPKRead.m_nFileFraction, szExpected, szCalculated
		);

		// we got an error reading this chunk, record the error
		m_cFileErrors++;
		cachedVPKRead.m_cFailedHashes++;
		// give a copy to the fail whale
		//m_queueCachedVPKReadsRetry.PushItem( cachedVPKRead );
		return false;
	}
	if ( cachedVPKRead.m_cFailedHashes > 0 )
	{
		m_cFileErrorsCorrected++;
	}
	return true;
}


int CPackedStoreReadCache::FindBufferToUse()
{
	int idxLRU = 0;
	int idxToRemove = m_treeCachedVPKRead.InvalidIndex();
	uint32 uTimeLowest = (uint32)~0; // MAXINT
	// find the oldest item, reuse its buffer
	for ( int i = 0; i < m_cItemsInCache; i++ )
	{
		if ( m_rgLastUsedTime[i] < uTimeLowest )
		{
			uTimeLowest = m_rgLastUsedTime[i];
			idxToRemove = m_rgCurrentCacheIndex[i];
			idxLRU = i;
		}
		int idxCurrent = m_rgCurrentCacheIndex[i];
		// while we are here check if the MD5 is done
		if ( m_treeCachedVPKRead[idxCurrent].m_hMD5RequestHandle )
		{
			CachedVPKRead_t &cachedVPKRead = m_treeCachedVPKRead[idxCurrent];
			if ( m_pFileTracker->IsMD5RequestComplete( cachedVPKRead.m_hMD5RequestHandle, &cachedVPKRead.m_md5Value ) )
			{
				// if it is done, check the results
				cachedVPKRead.m_hMD5RequestHandle = 0;
				// if we got bad data - stop looking, just use this one
				if ( !CheckMd5Result( cachedVPKRead ) )
					return i;
			}
		}
	}

	// if we submitted its MD5 for processing, then wait until that is done
	if ( m_treeCachedVPKRead[idxToRemove].m_hMD5RequestHandle )
	{
		CachedVPKRead_t &cachedVPKRead = m_treeCachedVPKRead[idxToRemove];
		m_pFileTracker->BlockUntilMD5RequestComplete( cachedVPKRead.m_hMD5RequestHandle, &cachedVPKRead.m_md5Value );
		m_treeCachedVPKRead[idxToRemove].m_hMD5RequestHandle = 0;
		// make sure it matches what it is supposed to match
		CheckMd5Result( cachedVPKRead );
	}
	return idxLRU;
}


// manage the cache
bool CPackedStoreReadCache::BCanSatisfyFromReadCacheInternal( uint8 *pOutData, CPackedStoreFileHandle &handle, FileHandleTracker_t &fHandle, int nDesiredPos, int nNumBytes, int &nRead )
{
	m_rwlock.LockForRead();
	bool bLockedForWrite = false;

	CachedVPKRead_t key;
	key.m_nPackFileNumber = handle.m_nFileNumber;
	key.m_nFileFraction = nDesiredPos & k_nCacheBufferMask;
	int idxTrackedVPKFile = m_treeCachedVPKRead.Find( key );
	if ( idxTrackedVPKFile == m_treeCachedVPKRead.InvalidIndex() || m_treeCachedVPKRead[idxTrackedVPKFile].m_pubBuffer == NULL )
	{
		m_rwlock.UnlockRead();
		m_rwlock.LockForWrite();
		bLockedForWrite = true;
		// if we didnt find it, we had to grab the write lock, it may have been added while we waited
		idxTrackedVPKFile = m_treeCachedVPKRead.Find( key );
	}
	if ( idxTrackedVPKFile == m_treeCachedVPKRead.InvalidIndex() )
	{
		idxTrackedVPKFile = m_treeCachedVPKRead.Insert( key );
	}

	CachedVPKRead_t &cachedVPKRead = m_treeCachedVPKRead[idxTrackedVPKFile];

	// Cache hit?
	if ( cachedVPKRead.m_pubBuffer == NULL )
	{

		// We need to have it locked for write, because we're about to muck with these structures.
		if ( !bLockedForWrite )
		{
			Assert( bLockedForWrite );
			return false;
		}

		Assert( cachedVPKRead.m_idxLRU < 0 );

		// Can we add another line to the cache, or should we reuse an existing one?
		int idxLRU = -1;
		if ( m_cItemsInCache >= k_nCacheBuffersToKeep )
		{
			// Need to kick out the LRU.
			idxLRU = FindBufferToUse();
			int idxToRemove = m_rgCurrentCacheIndex[idxLRU];
			Assert( m_treeCachedVPKRead[idxToRemove].m_idxLRU == idxLRU );
			Assert( m_treeCachedVPKRead[idxToRemove].m_pubBuffer != NULL );

			// Transfer ownership of the buffer
			cachedVPKRead.m_pubBuffer = m_treeCachedVPKRead[idxToRemove].m_pubBuffer;
			m_treeCachedVPKRead[idxToRemove].m_pubBuffer = NULL;
			m_treeCachedVPKRead[idxToRemove].m_cubBuffer = 0;
			m_treeCachedVPKRead[idxToRemove].m_idxLRU = -1;
			m_cDiscardsFromCache++;
		}
		else
		{

			// We can add a new one
			idxLRU = m_cItemsInCache;
			m_cItemsInCache++;
			Assert( cachedVPKRead.m_pubBuffer == NULL );
		}
		m_rgCurrentCacheIndex[idxLRU] = idxTrackedVPKFile;
		cachedVPKRead.m_idxLRU = idxLRU;

		if ( cachedVPKRead.m_pubBuffer == NULL )
		{
			cachedVPKRead.m_pubBuffer = (uint8 *)malloc( k_cubCacheBufferSize );
			if ( cachedVPKRead.m_pubBuffer == NULL )
				Error( "Out of memory" );
		}
		ReadCacheLine( fHandle, cachedVPKRead );
		m_cAddedToCache++;
	}
	else
	{
		Assert( cachedVPKRead.m_idxLRU >= 0 );
		Assert( m_rgCurrentCacheIndex[cachedVPKRead.m_idxLRU] == idxTrackedVPKFile );
	}

	// Assume no bytes will be satisfied from cache
	bool bSuccess = false;
	nRead = 0;

	// Can we read at least one byte?
	Assert( cachedVPKRead.m_nFileFraction <= nDesiredPos );
	int nBufferEnd = cachedVPKRead.m_cubBuffer + cachedVPKRead.m_nFileFraction;
	if ( cachedVPKRead.m_pubBuffer != NULL && nBufferEnd > nDesiredPos )
	{
		nRead = Min( nBufferEnd - nDesiredPos, nNumBytes );
		int nOffset = nDesiredPos - cachedVPKRead.m_nFileFraction;
		Assert( nOffset >= 0 );
		memcpy( pOutData, (uint8 *)&cachedVPKRead.m_pubBuffer[nOffset], nRead );
		m_cubReadFromCache += nRead;
		m_cReadFromCache ++;
		bSuccess = true;
		m_rgLastUsedTime[m_treeCachedVPKRead[idxTrackedVPKFile].m_idxLRU] = Plat_MSTime();
	}
	if ( bLockedForWrite )
		m_rwlock.UnlockWrite();
	else
		m_rwlock.UnlockRead();

	return bSuccess;
}


void CPackedStoreReadCache::RetryBadCacheLine( CachedVPKRead_t &cachedVPKRead )
{
	ChunkHashFraction_t chunkHashFraction;
	m_pPackedStore->FindFileHashFraction( cachedVPKRead.m_nPackFileNumber, cachedVPKRead.m_nFileFraction, chunkHashFraction );

	cachedVPKRead.m_pubBuffer = (uint8 *)malloc( k_cubCacheBufferSize );
	FileHandleTracker_t &fHandle = m_pPackedStore->GetFileHandle( cachedVPKRead.m_nPackFileNumber );
	fHandle.m_Mutex.Lock();
	ReadCacheLine( fHandle, cachedVPKRead );
	fHandle.m_Mutex.Unlock();
	m_pFileTracker->BlockUntilMD5RequestComplete( cachedVPKRead.m_hMD5RequestHandle, &cachedVPKRead.m_md5Value );
	cachedVPKRead.m_hMD5RequestHandle = 0;
	CheckMd5Result( cachedVPKRead );
	cachedVPKRead.m_pubBuffer = NULL;
}


// try reloading anything that failed its md5 check
// this is currently only for gathering information, doesnt do anything to repair the cache
void CPackedStoreReadCache::RetryAllBadCacheLines()
{
//	while( m_queueCachedVPKReadsRetry.Count() )
//	{
//		CachedVPKRead_t cachedVPKRead;
//		m_rwlock.LockForWrite();
//		if ( m_queueCachedVPKReadsRetry.PopItem( &cachedVPKRead ) )
//		{
//			// retry anything that didnt match one time
//			RetryBadCacheLine( cachedVPKRead );
//			m_listCachedVPKReadsFailed.AddToTail( cachedVPKRead );
//			// m_listCachedVPKReadsFailed contains all the data about failed reads - for error or OGS reporting
//		}
//		m_rwlock.UnlockWrite();
//	}
}

void CPackedStore::GetPackFileLoadErrorSummary( CUtlString &sErrors )
{
	FOR_EACH_LL( m_PackedStoreReadCache.m_listCachedVPKReadsFailed, i )
	{
		char szDataFileName[MAX_PATH];
		CPackedStoreFileHandle fhandle = GetHandleForHashingFiles();
		fhandle.m_nFileNumber = m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_nPackFileNumber;
		fhandle.GetPackFileName( szDataFileName, sizeof(szDataFileName) );
		const char *pszFileName = V_GetFileName( szDataFileName );

		CUtlString sTemp;
		sTemp.Format( "Pack File %s at offset %x length %x errorcount = %d \n", 
			pszFileName,
			m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_nFileFraction, 
			m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_cubBuffer,
			m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_cFailedHashes );
		sErrors += sTemp ;
		
		char hex[sizeof(MD5Value_t)*2 + 1 ];
		Q_binarytohex( m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_md5Value.bits, 
					sizeof(MD5Value_t),	hex, sizeof( hex ) );

		ChunkHashFraction_t chunkHashFraction;
		FindFileHashFraction( m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_nPackFileNumber, m_PackedStoreReadCache.m_listCachedVPKReadsFailed[i].m_nFileFraction, chunkHashFraction );

		char hex2[sizeof(MD5Value_t)*2 + 1 ];
		Q_binarytohex( chunkHashFraction.m_md5contents.bits, 
			sizeof(MD5Value_t),	hex2, sizeof( hex2 ) );
		sTemp.Format( "Last Md5 Value %s Should be %s \n", hex, hex2 );

		sErrors += sTemp ;
	}

}

int CPackedStore::ReadData( CPackedStoreFileHandle &handle, void *pOutData, int nNumBytes )
{
	int nRet = 0;

	// clamp read size to file size
	nNumBytes = MIN( nNumBytes, handle.m_nFileSize - handle.m_nCurrentFileOffset );
	if ( nNumBytes > 0 )
	{
		// first satisfy from the metadata, if we can
		int nNumMetaDataBytes = MIN( nNumBytes, handle.m_nMetaDataSize - handle.m_nCurrentFileOffset );
		if ( nNumMetaDataBytes > 0 )
		{
			memcpy( pOutData, reinterpret_cast<uint8 const *>( handle.m_pMetaData )
					+ handle.m_nCurrentFileOffset, nNumMetaDataBytes );
			nRet += nNumMetaDataBytes;
			pOutData = reinterpret_cast<uint8 *>( pOutData ) + nNumMetaDataBytes;
			handle.m_nCurrentFileOffset += nNumMetaDataBytes;
			nNumBytes -= nNumMetaDataBytes;
		}
		// satisfy remaining bytes from file
		if ( nNumBytes > 0 )
		{
			FileHandleTracker_t &fHandle = GetFileHandle( handle.m_nFileNumber );
			int nDesiredPos = handle.m_nFileOffset + handle.m_nCurrentFileOffset - handle.m_nMetaDataSize;
			int nRead;
			fHandle.m_Mutex.Lock();
			if ( handle.m_nFileNumber == VPKFILENUMBER_EMBEDDED_IN_DIR_FILE )
			{
				// for file data in the directory header, all offsets are relative to the size of the dir header.
				nDesiredPos += m_nDirectoryDataSize + sizeof( VPKDirHeader_t );
			}

			if ( m_PackedStoreReadCache.BCanSatisfyFromReadCache( (uint8 *)pOutData, handle, fHandle, nDesiredPos, nNumBytes, nRead ) )
			{
				handle.m_nCurrentFileOffset += nRead;
			}
			else
			{
#ifdef IS_WINDOWS_PC
				if ( nDesiredPos != fHandle.m_nCurOfs )
					SetFilePointer ( fHandle.m_hFileHandle, nDesiredPos, NULL,  FILE_BEGIN); 
				ReadFile( fHandle.m_hFileHandle, pOutData, nNumBytes, (LPDWORD) &nRead, NULL );
#else
				m_pFileSystem->Seek( fHandle.m_hFileHandle, nDesiredPos, FILESYSTEM_SEEK_HEAD );
				nRead = m_pFileSystem->Read( pOutData, nNumBytes, fHandle.m_hFileHandle );
#endif
				handle.m_nCurrentFileOffset += nRead;
				fHandle.m_nCurOfs = nRead + nDesiredPos;
			}
			Assert( nRead == nNumBytes );
			nRet += nRead;
			fHandle.m_Mutex.Unlock();
		}
	}
	m_PackedStoreReadCache.RetryAllBadCacheLines();
	return nRet;
}

bool CPackedStore::HashEntirePackFile( CPackedStoreFileHandle &handle, int64 &nFileSize, int nFileFraction, int nFractionSize, FileHash_t &fileHash )
{
#define	CRC_CHUNK_SIZE	(32*1024)
	unsigned char tempBuf[CRC_CHUNK_SIZE];

#ifdef COMPUTE_HASH_TIMES
	CFastTimer timer;
	timer.Start();
#endif

	FileHandleTracker_t &fHandle = GetFileHandle( handle.m_nFileNumber );
	fHandle.m_Mutex.Lock();
	
#ifdef IS_WINDOWS_PC
	unsigned int fileSizeHigh;
	unsigned int fileLength = GetFileSize( fHandle.m_hFileHandle, (LPDWORD) &fileSizeHigh );
#else
	unsigned int fileLength = m_pFileSystem->Size( fHandle.m_hFileHandle );
#endif
	nFileSize = fileLength;
	MD5Context_t ctx;
	memset(&ctx, 0, sizeof(MD5Context_t));
	MD5Init(&ctx);

	int nDesiredPos = nFileFraction;
#ifdef IS_WINDOWS_PC
	if ( nDesiredPos != fHandle.m_nCurOfs )
		SetFilePointer ( fHandle.m_hFileHandle, nDesiredPos, NULL,  FILE_BEGIN); 
#else
	m_pFileSystem->Seek( fHandle.m_hFileHandle, nDesiredPos, FILESYSTEM_SEEK_HEAD );
#endif

	int nFractionLength = ( fileLength - nFileFraction );
	if ( nFractionLength > nFractionSize )
		nFractionLength = nFractionSize;
	int nChunks = nFractionLength / CRC_CHUNK_SIZE + 1;
	unsigned int curStartByte = 0;
	for ( int iChunk=0; iChunk < nChunks; iChunk++ )
	{
		int curEndByte = MIN( curStartByte + CRC_CHUNK_SIZE, (uint)nFractionLength );
		int chunkLen = curEndByte - curStartByte;
		if ( chunkLen == 0 )
			break;

		int nRead;
#ifdef IS_WINDOWS_PC
		ReadFile( fHandle.m_hFileHandle, tempBuf, chunkLen, (LPDWORD) &nRead, NULL );
#else
		nRead = m_pFileSystem->Read( tempBuf, chunkLen, fHandle.m_hFileHandle );
#endif
		MD5Update(&ctx, tempBuf, nRead);

		curStartByte += CRC_CHUNK_SIZE;
	}
	MD5Final( fileHash.m_md5contents.bits, &ctx);
	fileHash.m_crcIOSequence = nFractionLength;
	fileHash.m_cbFileLen = nFractionLength;
	fileHash.m_eFileHashType = FileHash_t::k_EFileHashTypeEntireFile;
	fileHash.m_nPackFileNumber = handle.m_nFileNumber;
	fileHash.m_PackFileID = handle.m_pOwner->m_PackFileID;

	// seek back to where it was
#ifdef IS_WINDOWS_PC
	SetFilePointer ( fHandle.m_hFileHandle, fHandle.m_nCurOfs, NULL,  FILE_BEGIN); 
#else
	m_pFileSystem->Seek( fHandle.m_hFileHandle, fHandle.m_nCurOfs, FILESYSTEM_SEEK_HEAD );
#endif
	fHandle.m_Mutex.Unlock();

#ifdef COMPUTE_HASH_TIMES
	timer.End();
	int nMicroSec = timer.GetDuration().GetMicroseconds();
	char rgch[256];
	Q_snprintf( rgch, 256, "MD5 Pack File %d %d \n", handle.m_nFileNumber, nMicroSec );
	Plat_DebugString( rgch );
#endif
	return true;
}

void CPackedStore::DiscardChunkHashes( int iChunkFileIndex )
{
	// Wow, this could be a LOT faster because the list is
	// sorted.  Probably not worth optimizing
	FOR_EACH_VEC_BACK( m_vecChunkHashFraction, i )
	{
		if ( m_vecChunkHashFraction[i].m_nPackFileNumber == iChunkFileIndex )
			m_vecChunkHashFraction.Remove( i );
	}
}

void CPackedStore::HashChunkFile( int iChunkFileIndex )
{
	AUTO_LOCK( m_Mutex );
	static const int k_nFileFractionSize = 0x00100000; // 1 MB

	// Purge any hashes we already have for this chunk.
	DiscardChunkHashes( iChunkFileIndex );

	CPackedStoreFileHandle VPKHandle = GetHandleForHashingFiles();
	VPKHandle.m_nFileNumber = iChunkFileIndex;

	int nFileFraction = 0;
	while ( 1 )
	{
		FileHash_t filehash;

		// VPKHandle.m_nFileNumber;
		// nFileFraction;
		int64 fileSize = 0;
		// if we have never hashed this before - do it now
		HashEntirePackFile( VPKHandle, fileSize, nFileFraction, k_nFileFractionSize, filehash );
		ChunkHashFraction_t fileHashFraction;
		fileHashFraction.m_cbChunkLen = filehash.m_cbFileLen;
		fileHashFraction.m_nPackFileNumber = VPKHandle.m_nFileNumber;
		fileHashFraction.m_nFileFraction = nFileFraction;
		Q_memcpy( fileHashFraction.m_md5contents.bits, filehash.m_md5contents.bits, sizeof(fileHashFraction.m_md5contents) );
		m_vecChunkHashFraction.Insert( fileHashFraction );
		// move to next section
		nFileFraction += k_nFileFractionSize;
		// if we are at EOF we are done
		if ( nFileFraction > fileSize )
			break;
	}
}


void CPackedStore::HashAllChunkFiles()
{
	// Rebuild the directory hash tables.  The main reason to do this is
	// so that the highest chunk number is correct, in case chunks have
	// been removed.
	BuildHashTables();

	// make brand new hashes
	m_vecChunkHashFraction.Purge();
	for ( int iChunkFileIndex = 0 ; iChunkFileIndex <= GetHighestChunkFileIndex() ; ++iChunkFileIndex )
		HashChunkFile( iChunkFileIndex );
}

void CPackedStore::ComputeDirectoryHash( MD5Value_t &md5Directory )
{
	MD5Context_t ctx;
	memset(&ctx, 0, sizeof(MD5Context_t));
	MD5Init(&ctx);
	MD5Update(&ctx, m_DirectoryData.Base(), m_DirectoryData.Count() );
	MD5Final( md5Directory.bits, &ctx);
}


void CPackedStore::ComputeChunkHash( MD5Value_t &md5ChunkHashes )
{
	MD5Context_t ctx;
	memset(&ctx, 0, sizeof(MD5Context_t));
	MD5Init(&ctx);
	MD5Update(&ctx, (uint8 *)m_vecChunkHashFraction.Base(), m_vecChunkHashFraction.Count()*sizeof(m_vecChunkHashFraction[0]) );
	MD5Final( md5ChunkHashes.bits, &ctx);
}


bool CPackedStore::BTestDirectoryHash()
{
	if ( !BFileContainedHashes() )
		return true;
	MD5Value_t md5Directory;
	ComputeDirectoryHash( md5Directory );
	return Q_memcmp( m_DirectoryMD5.bits, md5Directory.bits, sizeof( md5Directory.bits ) ) == 0;
}


bool CPackedStore::BTestMasterChunkHash()
{
	if ( !BFileContainedHashes() )
		return true;
	MD5Value_t md5ChunkHashes;
	ComputeChunkHash( md5ChunkHashes );
	return Q_memcmp( m_ChunkHashesMD5.bits, md5ChunkHashes.bits, sizeof( md5ChunkHashes.bits ) ) == 0;
}


void CPackedStore::HashEverything()
{
	HashAllChunkFiles();
	HashMetadata();
}

void CPackedStore::HashMetadata()
{
	ComputeDirectoryHash( m_DirectoryMD5 );
	ComputeChunkHash( m_ChunkHashesMD5 );
}


bool CPackedStore::FindFileHashFraction( int nPackFileNumber, int nFileFraction, ChunkHashFraction_t &fileHashFraction )
{
	ChunkHashFraction_t fileHashFractionFind;
	fileHashFractionFind.m_nFileFraction = nFileFraction;
	fileHashFractionFind.m_nPackFileNumber = nPackFileNumber;

	int idx = m_vecChunkHashFraction.Find( fileHashFractionFind );
	if ( idx == m_vecChunkHashFraction.InvalidIndex() )
	{
		Assert( false );
		return false;
	}
	fileHashFraction = m_vecChunkHashFraction[idx];
	return true;
}

void CPackedStore::GetPackFileName( CPackedStoreFileHandle &handle, char *pchFileNameOut, int cchFileNameOut ) const
{
	GetDataFileName( pchFileNameOut, cchFileNameOut, handle.m_nFileNumber );
}

FileHandleTracker_t & CPackedStore::GetFileHandle( int nFileNumber )
{
	AUTO_LOCK( m_Mutex );
	int nFileHandleIdx = nFileNumber % ARRAYSIZE( m_FileHandles );

	if ( m_FileHandles[nFileHandleIdx].m_nFileNumber == nFileNumber )
	{
		return m_FileHandles[nFileHandleIdx];
	}
	else if ( m_FileHandles[nFileHandleIdx].m_nFileNumber == -1 )
	{
		// no luck finding the handle - need a new one
		char pszDataFileName[MAX_PATH];
		GetDataFileName( pszDataFileName, sizeof(pszDataFileName), nFileNumber );
		m_FileHandles[nFileHandleIdx].m_nCurOfs = 0;
#ifdef IS_WINDOWS_PC
		m_FileHandles[nFileHandleIdx].m_hFileHandle = 
			CreateFile( pszDataFileName,               // file to open
						GENERIC_READ,          // open for reading
						FILE_SHARE_READ,       // share for reading
						NULL,                  // default security
						OPEN_EXISTING,         // existing file only
						FILE_ATTRIBUTE_NORMAL, // normal file
						NULL);                 // no attr. template
			
		if ( m_FileHandles[nFileHandleIdx].m_hFileHandle != INVALID_HANDLE_VALUE )
		{
			m_FileHandles[nFileHandleIdx].m_nFileNumber = nFileNumber;
		}
#else
		m_FileHandles[nFileHandleIdx].m_hFileHandle = m_pFileSystem->Open( pszDataFileName, "rb" );
		if ( m_FileHandles[nFileHandleIdx].m_hFileHandle != FILESYSTEM_INVALID_HANDLE )
		{
			m_FileHandles[nFileHandleIdx].m_nFileNumber = nFileNumber;
		}
#endif
		return m_FileHandles[nFileHandleIdx];
	}
	Error( "Exceeded limit of number of vpk files supported (%d)!\n", MAX_ARCHIVE_FILES_TO_KEEP_OPEN_AT_ONCE );
	static FileHandleTracker_t invalid;
#ifdef IS_WINDOWS_PC
	invalid.m_hFileHandle = INVALID_HANDLE_VALUE;
#else
	invalid.m_hFileHandle = FILESYSTEM_INVALID_HANDLE;
#endif
	return invalid;
}

bool CPackedStore::RemoveFileFromDirectory( const char *pszName )
{
	// Remove it without building hash tables
	if ( !InternalRemoveFileFromDirectory( pszName ) )
		return false;

	// We removed it, we need to rebuild hash tables
	BuildHashTables();
	return true;
}

bool CPackedStore::InternalRemoveFileFromDirectory( const char *pszName )
{
	CPackedStoreFileHandle pData = OpenFile( pszName );
	if ( !pData )
		return false;

	CFileHeaderFixedData *pHeader = pData.m_pHeaderData;
	// delete the old header so we can insert a new one with updated contents
	int nBytesToRemove = ( int )( V_strlen( ( char * ) pData.m_pDirFileNamePtr ) + 1 + pHeader->HeaderSizeIncludingMetaData() );
	m_DirectoryData.RemoveMultiple( pData.m_pDirFileNamePtr - m_DirectoryData.Base(), nBytesToRemove );
	return true;
}

void CPackedStore::AddFileToDirectory( const VPKContentFileInfo_t &info )
{
	// this method is fairly complicated because it has to do inserts into the packed directory
	// data Our strategy is to build out the whole ext _ dir _ file record. if none of this is
	// already present, we will just insert it in the head of the file. If the extension is
	// present, we'll insert the dir+file part. If the extension + dir is present, we just insert
	// the file part at the right place. If everything is present, we just need to return the
	// current record

	// First, remove it if it's already there,
	// without rebuilding the hash tables
	InternalRemoveFileFromDirectory( info.m_sName );

	// let's build out a header
	char pszExt[MAX_PATH];
	char pszBase[MAX_PATH];
	char pszDir[MAX_PATH];
	SplitFileComponents( info.m_sName, pszDir, pszBase, pszExt );
	int nNumDataParts = 1;
	int nFileDataSize = s_FileHeaderSize( pszBase, nNumDataParts, info.m_iPreloadSize );
	int nTotalHeaderSize = ( int )( nFileDataSize + ( 2 + strlen( pszExt ) ) + ( 2 + strlen( pszDir ) ) );
	char *pBuf = ( char * ) stackalloc( nTotalHeaderSize );
	char *pOut = pBuf;
	strcpy( pOut, pszExt );
	pOut += strlen( pszExt );
	*( pOut++ ) = 0;										// null on ext name
	strcpy( pOut, pszDir );
	pOut += strlen( pszDir );
	*( pOut++ ) = 0;									// null at end of dir name
	strcpy( pOut, pszBase );
	pOut += strlen( pszBase );
	*( pOut++ ) = 0;
	uint32 nCRC = info.m_crc;
	memcpy( pOut, &nCRC, sizeof( nCRC ) );
	pOut += sizeof( int );
	if ( info.m_iPreloadSize > 0xffff )
		Error( "Preload size for '%s' is too big", info.m_sName.String() );
	uint16 nMetaDataSize = (uint16)info.m_iPreloadSize;
	memcpy( pOut, &nMetaDataSize, sizeof( uint16 ) );
	pOut += sizeof( uint16 );

	// now, build file parts.
	CFilePartDescr newPart;
	newPart.m_nFileDataSize = info.GetSizeInChunkFile();
	newPart.m_nFileNumber = ( info.m_idxChunk < 0 ) ? VPKFILENUMBER_EMBEDDED_IN_DIR_FILE : info.m_idxChunk;
	newPart.m_nFileDataOffset = info.m_iOffsetInChunk;

	memcpy( pOut, &newPart, sizeof( newPart ) );
	pOut += sizeof( newPart );

	PackFileIndex_t endOfPartMarker = PACKFILEINDEX_END;
	memcpy( pOut, &endOfPartMarker, sizeof( endOfPartMarker ) );
	pOut += sizeof( PackFileIndex_t );
	if ( nMetaDataSize )
	{
		Assert( info.m_pPreloadData );
		memcpy( pOut, info.m_pPreloadData, nMetaDataSize );
		pOut += nMetaDataSize;
	}
	*( pOut++ ) = 0;									// mark no more files in dir
	*( pOut++ ) = 0;									// mark no more dirs in extension
	Assert( pOut - pBuf == nTotalHeaderSize );

	// now, we need to insert our header, figuring out how many of the fields are already there
	int nExtensionHash = HashString( pszExt ) % PACKEDFILE_EXT_HASH_SIZE;
	int nInsertOffset = 0;
	CFileExtensionData const *pExt = m_pExtensionData[nExtensionHash].FindNamedNodeCaseSensitive( pszExt );
	char *pHeaderInsertPtr = pBuf;

	if ( pExt )
	{
		// this is not a new extension. we should not insert the extension record
		nTotalHeaderSize -= 2 + strlen( pszExt );		// null + end of dir list marker
		pHeaderInsertPtr += 1 + strlen( pszExt );					// don't insert the name + null
		// now, look for the directory
		int nDirHash = HashString( pszDir ) % PACKEDFILE_DIR_HASH_SIZE;
		CFileDirectoryData const *pDir = pExt->m_pDirectoryHashTable[nDirHash].FindNamedNodeCaseSensitive( pszDir );
		if ( pDir )
		{
			// dir and extension found. all we need to do is insert the file data itself
			nTotalHeaderSize -= 2 + strlen( pszDir );	// null + end of file list marker
			pHeaderInsertPtr += 1 + strlen( pszDir );
			char const *pStartOfDirFileData = pDir->m_Name + 1 + strlen( pDir->m_Name );
			nInsertOffset = pStartOfDirFileData - ( char const * ) ( m_DirectoryData.Base() );
		}
		else
		{
			char const *pStartOfExtFileData = pExt->m_Name + 1 + strlen( pExt->m_Name );
			nInsertOffset = pStartOfExtFileData - ( char const * ) ( m_DirectoryData.Base() );
		}
	}
	m_DirectoryData.InsertMultipleBefore( nInsertOffset, nTotalHeaderSize );
	memcpy( &m_DirectoryData[nInsertOffset], pHeaderInsertPtr, nTotalHeaderSize );
	BuildHashTables();
}

ePackedStoreAddResultCode CPackedStore::AddFile( char const *pFile, uint16 nMetaDataSize, const void *pFileData, uint32 nFileTotalSize, bool bMultiChunk, uint32 const *pCrcValue )
{

	// Calculate CRC if they didn't provide one
	uint32 nCRC;
	if ( pCrcValue )
	{
		nCRC = *pCrcValue;
	}
	else
	{
		nCRC = CRC32_ProcessSingleBuffer( pFileData, nFileTotalSize );
	}

	// Check if it is already here with the same contents
	CPackedStoreFileHandle pData = OpenFile( pFile );
	ePackedStoreAddResultCode nRslt = EPADD_NEWFILE;
	if ( pData )											// already in pack
	{
		CFileHeaderFixedData *pHeader = pData.m_pHeaderData;
		if ( ( nFileTotalSize == pHeader->TotalDataSize() ) && ( pHeader->m_nFileCRC == nCRC ) && ( nMetaDataSize == pHeader->m_nMetaDataSize  ) ) // file unchanged?
		{
			return EPADD_ADDSAMEFILE;
		}
		nRslt = EPADD_UPDATEFILE;
	}

	// Build up the directory info into an interface structure
	VPKContentFileInfo_t dirEntry;
	dirEntry.m_sName = pFile;
	dirEntry.m_iTotalSize = nFileTotalSize;
	dirEntry.m_iPreloadSize = Min( (uint32)nMetaDataSize, (uint32)nFileTotalSize ) ;
	dirEntry.m_pPreloadData = ( dirEntry.m_iPreloadSize > 0 ) ? pFileData : NULL;
	dirEntry.m_crc = nCRC;
	uint32 nBytesInChunk = dirEntry.GetSizeInChunkFile();
	const unsigned char *pDataStart = (const unsigned char *)pFileData + dirEntry.m_iPreloadSize;

	if ( bMultiChunk && nBytesInChunk > 0 )
	{

		// Check if we need to start a new chunk
		char szDataFileName[MAX_PATH];
		if ( m_nHighestChunkFileIndex < 0 )
		{
			dirEntry.m_idxChunk = 0;
			dirEntry.m_iOffsetInChunk = 0;
		}
		else
		{
			dirEntry.m_idxChunk = m_nHighestChunkFileIndex;

			// Append to most recent chunk
			GetDataFileName( szDataFileName, sizeof(szDataFileName), m_nHighestChunkFileIndex );
			dirEntry.m_iOffsetInChunk = g_pFullFileSystem->Size( szDataFileName );
			if ( (int)dirEntry.m_iOffsetInChunk <= 0 ) // technical wrong, but we shouldn't have 2GB chunks.  (Sort of defeats the whole purpose.)
			{
				// Note, there is one possible failure case.  if we have a file whose data
				// is actually all in the preload section, but it is marked as being
				// in a chunk, then we might have a zero byte "chunk."  We really should
				// not be assigning any files to "chunks" if they are entirely in the preload
				// area.
				Error( "Error querying %s for file size\n", szDataFileName );
			}

			// Check if we need to start a new chunk
			if ( (int)dirEntry.m_iOffsetInChunk >= m_nWriteChunkSize )
			{
				++dirEntry.m_idxChunk;
				dirEntry.m_iOffsetInChunk = 0;
			}
		}

		m_nHighestChunkFileIndex = MAX( m_nHighestChunkFileIndex, dirEntry.m_idxChunk );

		// write the actual data
		GetDataFileName( szDataFileName, sizeof(szDataFileName), dirEntry.m_idxChunk );
		FileHandle_t fHandle = m_pFileSystem->Open( szDataFileName, "rb+" );
		if ( !fHandle && dirEntry.m_iOffsetInChunk == 0 )
			fHandle = m_pFileSystem->Open( szDataFileName, "wb" );
		if ( !fHandle )
			Error( "Cannot open %s for writing", szDataFileName );

		m_pFileSystem->Seek( fHandle, dirEntry.m_iOffsetInChunk, FILESYSTEM_SEEK_HEAD );
		m_pFileSystem->Write( pDataStart, nBytesInChunk, fHandle );
		m_pFileSystem->Close( fHandle );

		// Force on the use of the "dir" file
		m_bUseDirFile = true;
	}
	else
	{
		// append to the dir data.
		dirEntry.m_idxChunk = VPKFILENUMBER_EMBEDDED_IN_DIR_FILE;
		dirEntry.m_iOffsetInChunk = m_EmbeddedChunkData.Count();
		m_EmbeddedChunkData.AddMultipleToTail( nBytesInChunk, pDataStart );
	}

	// Update the directory
	AddFileToDirectory( dirEntry );

	return nRslt;
}

int CPackedStore::GetFileList( CUtlStringList &outFilenames, bool bFormattedOutput, bool bSortedOutput )
{
	return GetFileList( NULL, outFilenames, bFormattedOutput, bSortedOutput );
}

int CPackedStore::GetFileList( const char *pWildCard, CUtlStringList &outFilenames, bool bFormattedOutput, bool bSortedOutput )
{
	// Separate the wildcard base from the extension
	char szWildCardPath[MAX_PATH];
	char szWildCardBase[64];
	char szWildCardExt[20];
	bool bNoBaseWildcard = false;
	bool bNoExtWildcard = false;

	szWildCardPath[0] = szWildCardExt[0] = szWildCardBase[0] = NULL;

	// Parse the wildcard string into a base and extension used for string comparisons
	if ( pWildCard )
	{
		V_ExtractFilePath( pWildCard, szWildCardPath, sizeof( szWildCardPath ) );
		V_FixSlashes( szWildCardPath, '/' );

		V_FileBase( pWildCard, szWildCardBase, sizeof( szWildCardBase ) );
		V_ExtractFileExtension( pWildCard, szWildCardExt, sizeof( szWildCardExt ) );

		// Remove '*' from the base and extension strings so that the string comparison calls will match
		char *pcStar = strchr( szWildCardBase, '*' );
		pcStar ? *pcStar = NULL : bNoBaseWildcard = true;
		pcStar = strchr( szWildCardExt, '*' );
		pcStar ? *pcStar = NULL : bNoExtWildcard = true;
	}

	char const *pData = reinterpret_cast< char const *>( DirectoryData() );
	while( *pData )
	{
		// for each extension
		char pszCurExtension[MAX_PATH];
		if ( pData[0] != ' ' )
			sprintf( pszCurExtension, ".%s", pData );
		else
			pszCurExtension[0] = 0;
		// now, iterate over all directories associated with this extension
		pData += 1 + strlen( pData );
		while( *pData )
		{
			char pszCurDir[MAX_PATH];
			if ( pData[0] != ' ' )
				sprintf( pszCurDir, "%s/", pData );
			else
				pszCurDir[0] = 0;
			pData += 1 + strlen( pData );					// skip dir name
			// now, march through all the files
			while( *pData )									// until we're out of files to look at
			{
				char pszFNameOut[MAX_PATH*2];
				if ( bFormattedOutput )
				{
					CFileHeaderFixedData const *pHeader = reinterpret_cast< CFileHeaderFixedData const *>( pData + 1 + strlen( pData ) );
					sprintf( pszFNameOut, "%s%s%s crc=0x%x metadatasz=%d", pszCurDir, pData, pszCurExtension, pHeader->m_nFileCRC, pHeader->m_nMetaDataSize );
					CFilePartDescr const *pPart = &( pHeader->m_PartDescriptors[0] );
					while( pPart->m_nFileNumber != PACKFILEINDEX_END )
					{
						sprintf( pszFNameOut + strlen( pszFNameOut )," fnumber=%d ofs=0x%x sz=%d",
								 pPart->m_nFileNumber, pPart->m_nFileDataOffset, pPart->m_nFileDataSize );
						pPart++;
					}
				}
				else
				{
 					V_strncpy( pszFNameOut, pszCurDir, sizeof( pszFNameOut ) );
 					V_strncat( pszFNameOut, pData, sizeof( pszFNameOut ) );
 					V_strncat( pszFNameOut, pszCurExtension, sizeof( pszFNameOut ) );
				}
				SkipFile( pData );

				bool matches = true;

				if ( pWildCard )
				{
					// See if the filename matches the wildcards
					char szFNameOutPath[MAX_PATH];
					char szFNameOutBase[64];
					char szFNameOutExt[20];

					V_ExtractFilePath( pszFNameOut, szFNameOutPath, sizeof( szFNameOutPath ) );
					V_FileBase( pszFNameOut, szFNameOutBase, sizeof( szFNameOutBase ) );
					V_ExtractFileExtension( pszFNameOut, szFNameOutExt, sizeof( szFNameOutExt ) );

					matches =  !V_strnicmp( szFNameOutPath, szWildCardPath, sizeof( szWildCardPath ) );
					matches = matches && ( !V_strlen( szWildCardExt ) || bNoExtWildcard ? 0 == V_strnicmp( szFNameOutExt, szWildCardExt, strlen( szWildCardExt ) ) : 0 != V_stristr(szFNameOutExt, szWildCardExt ) );
					matches = matches && ( !V_strlen( szWildCardBase ) || bNoBaseWildcard ? 0 == V_strnicmp( szFNameOutBase, szWildCardBase, strlen( szWildCardBase ) ) : 0 != V_stristr(szFNameOutBase, szWildCardBase ) );
				}

				// Add the file to the output list
				if ( matches )
				{
					char *pFName = new char[1 + strlen( pszFNameOut ) ];
					strcpy( pFName, pszFNameOut );
					outFilenames.AddToTail( pFName );
				}
			}
			pData++;												// skip end marker
		}
		pData++;												// skip end marker
	}

	if ( bSortedOutput )
	{
		outFilenames.Sort( &CUtlStringList::SortFunc );
	}
	
	return outFilenames.Count();
}

void CPackedStore::GetFileList( const char *pWildcard, CUtlVector<VPKContentFileInfo_t> &outVecResults )
{

	// !KLUDGE! Get the filenames first, and then "find" them again.
	CUtlStringList vecFilenames;
	GetFileList( vecFilenames, false, false );

	FOR_EACH_VEC( vecFilenames, i )
	{
		// Locate where it is in the existing file
		CPackedStoreFileHandle h = OpenFile( vecFilenames[i] );
		if ( !h )
			Error( "File '%s' was returned by GetFileList, but OpenFile() fails?!", vecFilenames[i] );

		// Convert to output structure
		VPKContentFileInfo_t &f = outVecResults[ outVecResults.AddToTail() ];
		f.m_sName = vecFilenames[i];
		f.m_idxChunk = ( h.m_nFileNumber == VPKFILENUMBER_EMBEDDED_IN_DIR_FILE ) ? -1 : h.m_nFileNumber;
		f.m_iTotalSize = h.m_nFileSize;
		f.m_iOffsetInChunk = h.m_nFileOffset;
		f.m_iPreloadSize = h.m_nMetaDataSize;
		f.m_crc = h.m_pHeaderData->m_nFileCRC;
		f.m_pPreloadData = h.m_pHeaderData->MetaData();
	}
}

int CPackedStore::GetFileAndDirLists( CUtlStringList &outDirnames, CUtlStringList &outFilenames, bool bSortedOutput )
{
	return GetFileAndDirLists( NULL, outDirnames, outFilenames, bSortedOutput );
}

void CPackedStore::BuildFindFirstCache()
{
	CUtlStringList allVPKFiles;
	char szLastDirFound[MAX_PATH];

	// Init
	V_strncpy( szLastDirFound, "$$$$$$$HighlyUnlikelyPathForInitializationPurposes#######", sizeof( szLastDirFound ) );
	m_dirContents.SetLessFunc( DefLessFunc( int ) );

	// Get all files in the VPK
	GetFileList( allVPKFiles, false, true );

	// Add directories to directory list and files into map
	FOR_EACH_VEC( allVPKFiles, i )
	{
		char szFilePath[MAX_PATH];

		V_ExtractFilePath( allVPKFiles[i], szFilePath, sizeof( szFilePath ) );
		Q_StripTrailingSlash( szFilePath );
		
		// New directory
		if ( V_strnicmp( szFilePath, szLastDirFound, sizeof( szLastDirFound ) ) )
		{
			// Mark the new one as the last one encountered
			V_strncpy( szLastDirFound, szFilePath, sizeof( szFilePath ) );

			// Add it
			m_directoryList.CopyAndAddToTail( szFilePath );
			m_dirContents.Insert( m_directoryList.Count(), new CUtlStringList() ); // Freed in destructor
		}
		
		unsigned short nIndex = m_dirContents.Find( m_directoryList.Count() );
		CUtlStringList *pList = m_dirContents.Element( nIndex );

		pList->CopyAndAddToTail( V_UnqualifiedFileName( allVPKFiles[i] ) );
	}
}

int CPackedStore::GetFileAndDirLists( const char *pWildCard, CUtlStringList &outDirnames, CUtlStringList &outFilenames, bool bSortedOutput )
{


	// If this is the first time we've called FindFirst on this CPackedStore then let's build the caches
	if ( !m_directoryList.Count() )
	{
		BuildFindFirstCache();
#ifdef NEVER
		printf("CPackedStore::GetFileAndDirLists - list of directories in VPK files\n");
		FOR_EACH_VEC( m_directoryList, i )
		{
			printf("\t%d : %s\n", i, m_directoryList[i] );
		}
#endif // NEVER
	}

	// printf("CPackedStore::GetFileAndDirLists - Searching for %s\n", pWildCard? pWildCard: "NULL");

	if ( pWildCard )
	{
		CUtlDict<int,int> AddedDirectories; // Used to remove duplicate paths

		char szWildCardPath[MAX_PATH];
		char szWildCardBase[64];
		char szWildCardExt[20];
		
		int nLenWildcardPath = 0;
		int nLenWildcardBase = 0;
		int nLenWildcardExt = 0;

		bool bBaseWildcard = true;
		bool bExtWildcard = true;

		szWildCardPath[0] = szWildCardExt[0] = szWildCardBase[0] = '\0';
		//
		// Parse the wildcard string into a base and extension used for string comparisons
		//
		V_ExtractFilePath( pWildCard, szWildCardPath, sizeof( szWildCardPath ) );
		V_FixSlashes( szWildCardPath, '/' );

		V_FileBase( pWildCard, szWildCardBase, sizeof( szWildCardBase ) );
		V_ExtractFileExtension( pWildCard, szWildCardExt, sizeof( szWildCardExt ) );

		// From the pattern, we now have the directory path up to the file pattern, the filename base, and the filename 
		// extension.

		// Remove '*' from the base and extension strings so that the string comparison calls will match
		char *pcStar = strchr( szWildCardBase, '*' );
		pcStar ? *pcStar = NULL : bBaseWildcard = false;

		pcStar = strchr( szWildCardExt, '*' );
		pcStar ? *pcStar = NULL : bExtWildcard = false;

		nLenWildcardPath = V_strlen( szWildCardPath );
		nLenWildcardBase = V_strlen( szWildCardBase );
		nLenWildcardExt = V_strlen( szWildCardExt );

		// Generate the list of directories and files that match the wildcard
		//

		//
		// Directories first
		//
		FOR_EACH_VEC( m_directoryList, i )
		{
			// Does this file's path match the wildcard path?
			if  (  (  nLenWildcardPath && ( 0 == V_strnicmp( m_directoryList[i], szWildCardPath, nLenWildcardPath ) ) ) 
				|| ( !nLenWildcardPath && ( 0 == V_strlen( m_directoryList[i] ) ) ) )
			{
				// Extract the sub-directory name if there is one
				char szSubDir[64];
				char *szSubDirExtension = NULL; // this is anything after a '.' in szSubDir
				bool bBaseMatch = false;
				bool bExtMatch = false;

				// Copy everything to the right of the root directory
				V_strncpy( szSubDir, &m_directoryList[i][nLenWildcardPath], sizeof( szSubDir ) );

				// Set the next / to NULL and we have our subdirectory
				char *pSlash = strchr( szSubDir, '/' );
				pSlash ? *pSlash = NULL : NULL;

				szSubDirExtension = strchr( szSubDir, '.' );
				if ( szSubDirExtension )
				{
					// Null out the . and move the szSubDirExtension to point to the extension
					*szSubDirExtension = '\0';
					szSubDirExtension++;
				}

				// 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_strnicmp( szSubDir, szWildCardBase, nLenWildcardBase ) );

				// If we have an extension and we have a szWildCardExtension to mach against
				if ( bExtWildcard )
					bExtMatch = true; // The extension is the wildcard ("*"), so whatever we have as the extension matches
				else 
					bExtMatch = ( NULL == szSubDirExtension && '\0' == *szWildCardExt ) || (( NULL != szSubDirExtension ) && ( 0 == V_strnicmp( szSubDirExtension, szWildCardExt, nLenWildcardExt ) ));

				// If both parts match, then add it to the list of directories that match
				if ( bBaseMatch && bExtMatch )
				{
					char szFullPathToDir[ MAX_PATH ];

					V_strncpy( szFullPathToDir, szWildCardPath, nLenWildcardPath );
					V_strcat_safe( szFullPathToDir, "/" );
					V_strcat_safe( szFullPathToDir, szSubDir );
		
					// Add the subdirectory to the list if it isn't already there
					if ( -1 == AddedDirectories.Find( szFullPathToDir ) )
					{
						char *pDName = new char[1 + strlen( szFullPathToDir )];
						V_strncpy( pDName, szFullPathToDir, 1 + strlen( szFullPathToDir ) );
						outDirnames.AddToTail( pDName );
						AddedDirectories.Insert( pDName, 0 );
					}
				}
			}
		}

		//
		// Files
		//
		FOR_EACH_VEC( m_directoryList, i )
		{
			// We no longer want the trailing slash
			Q_StripTrailingSlash( szWildCardPath );

			// Find the directory that matches the wildcard path
			if ( !V_strnicmp( szWildCardPath, m_directoryList[i], sizeof( szWildCardPath ) ) )
			{
				CUtlStringList &filesInDirectory = *(m_dirContents.Element( i ));

				// Use the cached list of files in this directory
				FOR_EACH_VEC( filesInDirectory, iFile )
				{
					bool matches = true;


					// See if the filename matches the wildcards
					char szFNameOutBase[64];
					char szFNameOutExt[20];

					V_FileBase( filesInDirectory[iFile], szFNameOutBase, sizeof( szFNameOutBase ) );
					V_ExtractFileExtension( filesInDirectory[iFile], szFNameOutExt, sizeof( szFNameOutExt ) );

					// Since we have a sorted list we can optimize using the return code of the compare
					int c = V_strnicmp( szWildCardBase, szFNameOutBase, nLenWildcardBase );
					if ( c < 0 )
						break;
					if ( c > 0 )
						continue;
					matches = ( (nLenWildcardExt <= 0) || bBaseWildcard ? 0 == V_strnicmp( szFNameOutExt, szWildCardExt, nLenWildcardExt ) : V_stristr( szFNameOutExt, szWildCardExt ) != NULL );

					// Add the file to the output list
					if ( matches )
					{	
						bool bFound = false;

						FOR_EACH_VEC( outFilenames, j )
						{
							if ( !V_strncmp( outFilenames[j], filesInDirectory[iFile], V_strlen( filesInDirectory[iFile] ) ) )
							{
								bFound = true;
								break;
							}
						}

						if ( !bFound )
						{
							outFilenames.CopyAndAddToTail( filesInDirectory[iFile] );
						}
					}
				}
			}
		}
	}
	else // Otherwise, simply return the base data
	{
		// Add all the files as well
		FOR_EACH_VEC( m_directoryList, i )
		{
			// Add all directories
			outDirnames.CopyAndAddToTail( m_directoryList[i] );

			// Now add all files
			CUtlStringList &filesInDirectory = *(m_dirContents.Element( i ));
			FOR_EACH_VEC( filesInDirectory, j )
			{
				outFilenames.CopyAndAddToTail( filesInDirectory[j] );
			}
		}
	}

	// Sort the output if requested
	if ( bSortedOutput )
	{
		outDirnames.Sort( &CUtlStringList::SortFunc );
		outFilenames.Sort( &CUtlStringList::SortFunc );
	}

	return outDirnames.Count();
}