//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //===========================================================================// // Let's make sure aserts, etc are enabled for this tool #define RELEASEASSERTS #include "tier0/platform.h" #include "tier0/progressbar.h" #include "vpklib/packedstore.h" #include "mathlib/mathlib.h" #include "tier1/KeyValues.h" #include "tier2/tier2.h" #include "tier0/memdbgon.h" #include "tier2/fileutils.h" #include "tier1/utldict.h" #include "tier1/utlbuffer.h" #ifdef VPK_ENABLE_SIGNING #include "crypto.h" #endif static bool s_bBeVerbose = false; static bool s_bMakeMultiChunk = false; static bool s_bUseSteamPipeFriendlyBuilder = false; static int s_iMultichunkSize = k_nVPKDefaultChunkSize / ( 1024 * 1024 ); const int k_nVPKDefaultChunkAlign = 1; static int s_iChunkAlign = k_nVPKDefaultChunkAlign; static CUtlString s_sPrivateKeyFile; static CUtlString s_sPublicKeyFile; static void PrintArgSummaryAndExit( int iReturnCode = 1 ) { fflush(stderr); printf( "Usage: vpk [options] \n" " vpk [options] \n" " vpk [options] \n" "\n" "CREATE VPK / ADD FILES:\n" " vpk \n" " Creates a pack file named .vpk located\n" " in the parent of the specified directory.\n" " vpk a ...\n" " Add file(s).\n" " vpk a @\n" " Add files listed in a response file.\n" " vpk k \n" " Add files listed in a keyvalues control file.\n" " vpk \n" " Create VPK from directory structure. (This is invoked when\n" " a directory is dragged onto the VPK tool.)\n" "\n" "EXTRACT FILES:\n" " vpk x ...\n" " Extract file(s).\n" " vpk \n" " Extract all files from VPK. (This is invoked when\n" " a .VPK file is dragged onto the VPK tool.)\n" "\n" "DISPLAY VPK INFO:\n" " vpk l \n" " List contents of VPK.\n" " vpk L \n" " List contents (detailed) of VPK.\n" #ifdef VPK_ENABLE_SIGNING " vpk dumpsig \n" " Display signature information of VPK file\n" "\n" "VPK INTEGRITY / SECURITY:\n" " vpk checkhash \n" " Check all VPK chunk MD5's and file CRC's.\n" " vpk checksig \n" " Verify signature of specified VPK file.\n" " Requires -k to specify key file to use.\n" // " vpk rehash \n" // " Recalculate chunk MD5's. (Does not recalculate file CRC's)\n" // " Can be used with -k to sign an existing unsigned VPK.\n" "\n" "MISC:\n" " vpk generate_keypair \n" " Generate public/private key file. Output files\n" " will be named .publickey.vdf\n" " and .privatekey.vdf\n" " Remember: your private key should be kept private.\n" #endif "\n" "\n" "Options:\n" " -v Verbose.\n" " -M Produce a multi-chunk pack file\n" " -P Use SteamPipe-friendly incremental build algorithm.\n" " Use with 'k' command.\n" " For optimal incremental build performance, the control file used\n" " for the previous build should exist and be named the same as the\n" " input control file, with '.bak' appended, and each file entry\n" " should have an 'md5' value. The 'md5' field need not be the\n" " actual MD5 of the file contents, it is just a unique identifier\n" " that will be compared to determine if the file contents has changed\n" " between builds.\n" " This option implies -M\n" ); printf( " -c \n" " Use specified chunk size (in MB). Default is %d.\n", k_nVPKDefaultChunkSize / ( 1024 * 1024 ) ); printf( " -a \n" " Align files within chunk on n-byte boundary. Default is %d.\n", k_nVPKDefaultChunkAlign ); #ifdef VPK_ENABLE_SIGNING printf( " -K \n" " With commands 'a' or 'k': Sign VPK with specified private key.\n" " -k \n" " With commands 'a' or 'k': Public key that will be distributed\n" " and used by third parties to verify signatures.\n" " With command 'checksig': Check signature using specified key file.\n" ); #endif exit( iReturnCode ); } bool IsRestrictedFileType( const char *pcFileName ) { return ( V_stristr( pcFileName, ".bat" ) || V_stristr( pcFileName, ".cmd" ) || V_stristr( pcFileName, ".com" ) || V_stristr( pcFileName, ".dll" ) || V_stristr( pcFileName, ".exe" ) || V_stristr( pcFileName, ".msi" ) || V_stristr( pcFileName, ".rar" ) || V_stristr( pcFileName, ".reg" ) || V_stristr( pcFileName, ".zip" ) ); } void ReadFile( char const *pName ) { FileHandle_t f = g_pFullFileSystem->Open( pName, "rb" ); if ( f ) { int fileSize = g_pFullFileSystem->Size( f ); unsigned bufSize = ((IFileSystem *)g_pFullFileSystem)->GetOptimalReadSize( f, fileSize ); void *buffer = ((IFileSystem *)g_pFullFileSystem)->AllocOptimalReadBuffer( f, bufSize ); // read into local buffer ( ((IFileSystem *)g_pFullFileSystem)->ReadEx( buffer, bufSize, fileSize, f ) != 0 ); g_pFullFileSystem->Close( f ); // close file after reading ((IFileSystem *)g_pFullFileSystem)->FreeOptimalReadBuffer( buffer ); } } void BenchMark( CUtlVector &names ) { for( int i = 0; i < names.Count(); i++ ) ReadFile( names[i] ); } static void AddFileToPack( CPackedStore &mypack, char const *pSrcName, int nPreloadSize = 0, char const *pDestName = NULL ) { // Check to make sure that no restricted file types are being added to the VPK if ( IsRestrictedFileType( pSrcName ) ) { printf( "Ignoring %s: unsupported file type.\n", pSrcName ); return; } // !FIXME! Make sure they didn't request alignment, because we aren't doing it. if ( s_iChunkAlign != 1 ) Error( "-a is only supported with -P" ); if ( (! pDestName ) || ( pDestName[0] == 0 ) ) { pDestName = pSrcName; } CRequiredInputFile f( pSrcName ); int fileSize = f.Size(); uint8 *pData = new uint8[fileSize]; f.MustRead( pData, fileSize ); ePackedStoreAddResultCode rslt = mypack.AddFile( pDestName, Min( fileSize, nPreloadSize ), pData, fileSize, s_bMakeMultiChunk ); if ( rslt == EPADD_ERROR ) { Error( "Error adding %s\n", pSrcName ); } if ( s_bBeVerbose ) { switch( rslt ) { case EPADD_ADDSAMEFILE: { if ( s_bBeVerbose ) { printf( "File %s is already in the archive with the same contents\n", pSrcName ); } } break; case EPADD_UPDATEFILE: { if ( s_bBeVerbose ) { printf( "File %s is already in the archive and has been updated\n", pSrcName ); } } break; case EPADD_NEWFILE: { if ( s_bBeVerbose ) { printf( "Add new file %s\n", pSrcName ); } } break; } } delete[] pData; } #ifdef VPK_ENABLE_SIGNING static void LoadKeyFile( const char *pszFilename, const char *pszTag, CUtlVector &outBytes ) { KeyValuesAD kv("key"); if ( !kv->LoadFromFile( g_pFullFileSystem, pszFilename ) ) Error( "Failed to load key file %s", pszFilename ); const char *pszType = kv->GetString( "type", NULL ); if ( pszType == NULL ) Error( "Key file %s is missing 'type'", pszFilename ); if ( V_stricmp( pszType, "rsa" ) != 0 ) Error( "Key type '%s' is not supported", pszType ); const char *pszEncodedBytes = kv->GetString( pszTag, NULL ); if ( pszEncodedBytes == NULL ) Error( "Key file is missing '%s'", pszTag ); uint8 rgubDecodedData[k_nRSAKeyLenMax*2]; uint cubDecodedData = Q_ARRAYSIZE( rgubDecodedData ); if( !CCrypto::HexDecode( pszEncodedBytes, rgubDecodedData, &cubDecodedData ) || cubDecodedData <= 0 ) Error( "Key file contains invalid '%s' value", pszTag ); outBytes.SetSize( cubDecodedData ); V_memcpy( outBytes.Base(), rgubDecodedData, cubDecodedData ); } #endif static void CheckLoadKeyFilesForSigning( CPackedStore &mypack ) { // Not signing? if ( s_sPrivateKeyFile.IsEmpty() && s_sPublicKeyFile.IsEmpty() ) return; // Signatures only supported if creating multi-chunk file if ( !s_bMakeMultiChunk ) { Error( "Multichunk not specified. Only multi-chunk VPK's support signatures.\n" ); } // If they specified one, they must specify both if ( s_sPrivateKeyFile.IsEmpty() || s_sPublicKeyFile.IsEmpty() ) Error( "Must specify both public and private key files in order to sign VPK" ); #ifdef VPK_ENABLE_SIGNING CUtlVector bytesPrivateKey; LoadKeyFile( s_sPrivateKeyFile, "rsa_private_key", bytesPrivateKey ); printf( "Loaded private key file %s\n", s_sPrivateKeyFile.String() ); CUtlVector bytesPublicKey; LoadKeyFile( s_sPublicKeyFile, "rsa_public_key", bytesPublicKey ); printf( "Loaded public key file %s\n", s_sPublicKeyFile.String() ); mypack.SetKeysForSigning( bytesPrivateKey.Count(), bytesPrivateKey.Base(), bytesPublicKey.Count(), bytesPublicKey.Base() ); #else Error( "VPK signing not implemented" ); #endif } class VPKBuilder { public: VPKBuilder( CPackedStore &packfile ); ~VPKBuilder(); void BuildFromInputKeys() { if ( s_bUseSteamPipeFriendlyBuilder ) BuildSteamPipeFriendlyFromInputKeys(); else BuildOldSchoolFromInputKeys(); } void SetInputKeys( KeyValues *pInputKeys, const char *pszControlFilename ); void LoadInputKeys( const char *pszControlFilename ); private: CPackedStore &m_packfile; struct VPKBuildFile_t { VPKBuildFile_t() { m_pOld = NULL; m_pNew = NULL; m_iOldSortIndex = -1; m_iNewSortIndex = -1; m_md5Old.Zero(); m_md5New.Zero(); m_pOldKey = NULL; m_pNewKey = NULL; } VPKContentFileInfo_t *m_pOld; VPKContentFileInfo_t *m_pNew; int m_iOldSortIndex; int m_iNewSortIndex; KeyValues *m_pOldKey; KeyValues *m_pNewKey; MD5Value_t m_md5Old; MD5Value_t m_md5New; CUtlString m_sNameOnDisk; }; static int CompareBuildFileByOldPhysicalPosition( VPKBuildFile_t* const *pa, VPKBuildFile_t* const *pb ) { const VPKContentFileInfo_t *a = (*pa)->m_pOld; const VPKContentFileInfo_t *b = (*pb)->m_pOld; if ( a->m_idxChunk < b->m_idxChunk ) return -1; if ( a->m_idxChunk > b->m_idxChunk ) return +1; if ( a->m_iOffsetInChunk < b->m_iOffsetInChunk ) return -1; if ( a->m_iOffsetInChunk > b->m_iOffsetInChunk ) return +1; return 0; } CUtlString m_sControlFilename; /// List of all files, past and present, keyed by the name in the VPK. CUtlDict m_dictFiles; /// All files as they existed in the old VPK. (Empty if we are building from scratch.) CUtlVector m_vecOldFiles; /// List of all new files. CUtlVector m_vecNewFiles; /// List of new files, in the requested order, only counting those /// that will actually go into a chunk CUtlVector m_vecNewFilesInChunkOrder; /// List of old files that have some content in a chunk file, /// in the order they currently appear CUtlVector m_vecOldFilesInChunkOrder; int64 m_iNewTotalFileSize; int64 m_iNewTotalFileSizeInChunkFiles; /// A group of files that are contiguous in the logical linear /// file list. struct VPKInputFileRange_t { int m_iFirstInputFile; // index of first input file in the chunk int m_iLastInputFile; // index of last input file in the chunk int m_iChunkFilenameIndex; bool m_bKeepExistingFile; int64 m_nTotalSizeInChunkFile; int FileCount() const { int iResult = m_iLastInputFile - m_iFirstInputFile + 1; Assert( iResult > 0 ); return iResult; } }; KeyValues *m_pInputKeys; KeyValues *m_pOldInputKeys; CUtlLinkedList m_llFileRanges; CUtlVector m_vecRangeForChunk; CUtlString m_sReasonToForceWriteDirFile; void BuildOldSchoolFromInputKeys(); void BuildSteamPipeFriendlyFromInputKeys(); void SanityCheckRanges(); void SplitRangeAt( int iFirstInputFile ); void AddRange( VPKInputFileRange_t range ); void MapRangeToChunk( int idxRange, int iChunkFilenameIndex, bool bKeepExistingFile ); void CalculateRangeTotalSizeInChunkFile( VPKInputFileRange_t &range ) const; void UnmapAllRangesForChangedChunks(); void CoaleseAllUnmappedRanges(); void PrintRangeDebug(); void MapAllRangesToChunks(); }; VPKBuilder::VPKBuilder( CPackedStore &packfile ) : m_packfile( packfile ) { CUtlVector savePublicKey; savePublicKey = m_packfile.GetSignaturePublicKey(); CheckLoadKeyFilesForSigning( m_packfile ); if ( savePublicKey.Count() != m_packfile.GetSignaturePublicKey().Count() || V_memcmp( savePublicKey.Base(), m_packfile.GetSignaturePublicKey().Base(), savePublicKey.Count() ) != 0 ) { if ( m_packfile.GetSignaturePublicKey().Count() == 0 ) { m_sReasonToForceWriteDirFile = "Signature removed."; } else if ( savePublicKey.Count() == 0 ) { m_sReasonToForceWriteDirFile = "Signature added."; } else { m_sReasonToForceWriteDirFile = "Public key used for signing changed."; } } m_pInputKeys = NULL; m_pOldInputKeys = NULL; // !FIXME! Check if public key is changing so we know if we need to re-sign! } VPKBuilder::~VPKBuilder() { if ( m_pInputKeys ) m_pInputKeys->deleteThis(); if ( m_pOldInputKeys ) m_pOldInputKeys->deleteThis(); } void VPKBuilder::BuildOldSchoolFromInputKeys() { // Just add them in order FOR_EACH_VEC( m_vecNewFiles, i ) { VPKContentFileInfo_t *f = m_vecNewFiles[ i ]; int idxInDict = m_dictFiles.Find( f->m_sName.String() ); Assert( idxInDict >= 0 ); VPKBuildFile_t *bf = &m_dictFiles[ idxInDict ]; Assert( bf->m_pNew == f ); AddFileToPack( m_packfile, bf->m_sNameOnDisk, f->m_iPreloadSize, f->m_sName ); } if ( s_bBeVerbose ) printf( "Hashing metadata.\n" ); m_packfile.HashMetadata(); if ( s_bBeVerbose ) printf( "Writing directory file.\n" ); m_packfile.Write(); } void VPKBuilder::BuildSteamPipeFriendlyFromInputKeys() { // Get list of all files already in the VPK m_packfile.GetFileList( NULL, m_vecOldFiles ); FOR_EACH_VEC( m_vecOldFiles, i ) { VPKContentFileInfo_t *f = &m_vecOldFiles[i]; char szNameInVPK[ MAX_PATH ]; V_strcpy_safe( szNameInVPK, f->m_sName ); V_FixSlashes( szNameInVPK, '\\' ); // always use Windows slashes in VPK f->m_sName = szNameInVPK; // Add it to the dictionary int idxInDict = m_dictFiles.Find( szNameInVPK ); if ( idxInDict == m_dictFiles.InvalidIndex() ) idxInDict = m_dictFiles.Insert( szNameInVPK ); // Each logical file should only be in a VPK file once if ( m_dictFiles[ idxInDict ].m_pOld ) Error( "File '%s' is listed in VPK directory multiple times?! Cannot build incrementally.\n", szNameInVPK ); m_dictFiles[ idxInDict ].m_pOld = f; } // See if we should build incrementally bool bIncremental = ( m_vecOldFiles.Count() > 0 ) && !m_sControlFilename.IsEmpty(); if ( bIncremental ) { printf( "Building incrementally in SteamPipe-friendly manner.\n" ); printf( "Existing pack file contains %d files\n", m_vecOldFiles.Count() ); CUtlString sControlFilenameBak = m_sControlFilename; sControlFilenameBak += ".bak"; m_pOldInputKeys = new KeyValues( "oldkeys" ); if ( m_pOldInputKeys->LoadFromFile( g_pFullFileSystem, sControlFilenameBak ) ) { printf( "Loaded %s OK\n", sControlFilenameBak.String() ); printf( "Fetching MD5's and checking that it matches the pack file\n" ); for ( KeyValues *i = m_pOldInputKeys; i; i = i->GetNextKey() ) { const char *pszNameOnDisk = i->GetString( "srcpath", i->GetName() ); char szNameInVPK[ MAX_PATH ]; V_strcpy_safe( szNameInVPK, i->GetString( "destpath", "" ) ); if ( szNameInVPK[0] == '\0' ) Error( "File '%s' is missing 'destpath' in old KeyValues control file", pszNameOnDisk ); V_FixSlashes( szNameInVPK, '\\' ); // always use Windows slashes in VPK // Locate file build entry. We should have one in the VPK int idxInDict = m_dictFiles.Find( szNameInVPK ); if ( idxInDict == m_dictFiles.InvalidIndex() || m_dictFiles[ idxInDict ].m_pOld == NULL ) Error( "File '%s' in old KeyValues control file not found in pack file.\nThat control file was probably not used to build the pack file\n", szNameInVPK ); VPKBuildFile_t &bf = m_dictFiles[ idxInDict ]; if ( bf.m_pOldKey ) Error( "File '%s' appears multiple times in old KeyValues control file.\nThat control file was probably not used to build the pack file\n", szNameInVPK ); bf.m_pOldKey = i; // Fetch preload size from old KV, clamp to actual file size. int iPreloadSizeFromControlFile = i->GetInt( "preloadsize", 0 ); iPreloadSizeFromControlFile = Min( iPreloadSizeFromControlFile, (int)bf.m_pOld->m_iTotalSize ); if ( iPreloadSizeFromControlFile != (int)bf.m_pOld->m_iPreloadSize ) Error( "File '%s' preload size mismatch in old KeyValues control file and pack file.\nThat control file was probably not used to build the pack file\n", szNameInVPK ); const char *pszMD5 = i->GetString( "md5", "" ); if ( *pszMD5 ) { if ( V_strlen( pszMD5 ) != MD5_DIGEST_LENGTH*2 ) Error( "File '%s' has invalid MD5 '%s'", pszNameOnDisk, pszMD5 ); V_hextobinary( pszMD5, MD5_DIGEST_LENGTH*2, bf.m_md5Old.bits, MD5_DIGEST_LENGTH ); } else { printf( "WARNING: Old control file entry '%s' does not have an MD5; we will have to compare file contents for this file.\n", pszNameOnDisk ); } } // Now many sure every file in the pack was found in the control file. If not, then // they probably don't match and we should not trust the MD5's. FOR_EACH_DICT_FAST( m_dictFiles, idxInDict ) { VPKBuildFile_t &bf = m_dictFiles[ idxInDict ]; if ( bf.m_pOld && bf.m_pOldKey == NULL ) Error( "File '%s' is in pack but not in old control file %s.\n" "That control file was probably not used to build the pack file", bf.m_pOld->m_sName.String(), sControlFilenameBak.String() ); } printf( "%s appears to match VPK file.\nUsing MD5s for incremental building\n", sControlFilenameBak.String() ); } else { printf( "WARNING: %s not present; incremental building will be slow.\n", sControlFilenameBak.String() ); printf( " For best results, provide the control file previously used for building.\n" ); m_pOldInputKeys->deleteThis(); m_pOldInputKeys = NULL; } } else { printf( "Building pack file from scratch.\n" ); } // Dictionary is now complete. Gather up list of files in order // sorted by where they were in the old pack set FOR_EACH_DICT_FAST( m_dictFiles, i ) { VPKBuildFile_t *f = &m_dictFiles[i]; if ( f->m_pOld && f->m_pOld->GetSizeInChunkFile() > 0 ) m_vecOldFilesInChunkOrder.AddToTail( f ); } m_vecOldFilesInChunkOrder.Sort( CompareBuildFileByOldPhysicalPosition ); FOR_EACH_VEC( m_vecOldFilesInChunkOrder, i ) { m_vecOldFilesInChunkOrder[i]->m_iOldSortIndex = i; } // How many chunks are currently in the VPK. (Might be zero) int nOldChunkCount = 0; if ( m_vecOldFilesInChunkOrder.Count() > 0 ) nOldChunkCount = m_vecOldFilesInChunkOrder[ m_vecOldFilesInChunkOrder.Count()-1 ]->m_pOld->m_idxChunk + 1; // For each chunk filename (_nnn.vpk), remember which block // of files maps will be used to create it. // None of the chunks have been assigned a block of files yet for ( int i = 0 ; i < nOldChunkCount ; ++i ) m_vecRangeForChunk.AddToTail( m_llFileRanges.InvalidIndex() ); // Start by putting all the files into a single range // with no corresponding chunk VPKInputFileRange_t rangeAllFiles; rangeAllFiles.m_iChunkFilenameIndex = -1; rangeAllFiles.m_iFirstInputFile = 0; rangeAllFiles.m_iLastInputFile = m_vecNewFilesInChunkOrder.Count()-1; rangeAllFiles.m_bKeepExistingFile = false; CalculateRangeTotalSizeInChunkFile( rangeAllFiles ); m_llFileRanges.AddToTail( rangeAllFiles ); SanityCheckRanges(); // Building incrementally? if ( bIncremental && nOldChunkCount > 0 ) { printf( "Scanning for unchanged chunk files...\n" ); // For each existing chunk, see if it's totally modified or not. // In our case, since SteamPipe rewrites an entire file from scratch // anytime a single byte changes, we don't care how much a chunk // file changes, we only need to detect if we can carry it forward // exactly as is or not. int idxOldFile = 0; while ( idxOldFile < m_vecOldFilesInChunkOrder.Count() ) { // What chunk are we in? VPKBuildFile_t const &firstFile = *m_vecOldFilesInChunkOrder[ idxOldFile ]; int idxChunk = firstFile.m_pOld->m_idxChunk; char szDataFilename[ MAX_PATH ]; m_packfile.GetDataFileName( szDataFilename, sizeof(szDataFilename), idxChunk ); const char *pszShortDataFilename = V_GetFileName( szDataFilename ); int idxInChunk = 0; CUtlVector vecFilesToCompareContents; // Scan to the end of files in this chunk. CUtlString sReasonCannotReuse; while ( idxOldFile < m_vecOldFilesInChunkOrder.Count() ) { VPKBuildFile_t const &f = *m_vecOldFilesInChunkOrder[ idxOldFile ]; Assert( f.m_iOldSortIndex == idxOldFile ); // End of this old chunk? VPKContentFileInfo_t const *pOld = f.m_pOld; Assert( pOld ); if ( idxChunk != pOld->m_idxChunk ) break; Assert( f.m_iOldSortIndex == firstFile.m_iOldSortIndex + idxInChunk ); if ( sReasonCannotReuse.IsEmpty() ) { VPKContentFileInfo_t const *pNew = f.m_pNew; int iExpectedSortIndex = firstFile.m_iNewSortIndex + idxInChunk; const char *pszFilename = pOld->m_sName.String(); if ( pNew == NULL ) { sReasonCannotReuse.Format( "File '%s' was removed.", pszFilename ); } else if ( pOld->m_iTotalSize != pNew->m_iTotalSize ) { sReasonCannotReuse.Format( "File '%s' changed size.", pszFilename ); } else if ( pOld->m_iPreloadSize != pNew->m_iPreloadSize ) { sReasonCannotReuse.Format( "File '%s' changed preload size.", pszFilename ); } else if ( f.m_iNewSortIndex != iExpectedSortIndex ) { // Files reordered in some way. Try to give an appropriate message if ( f.m_iNewSortIndex > iExpectedSortIndex && iExpectedSortIndex < m_vecNewFilesInChunkOrder.Count() ) { VPKContentFileInfo_t const *pInsertedFile = m_vecNewFilesInChunkOrder[ iExpectedSortIndex ]; const char *pszInsertedFilename = pInsertedFile->m_sName.String(); int idxDictInserted = m_dictFiles.Find( pszInsertedFilename ); Assert( idxDictInserted != m_dictFiles.InvalidIndex() ); if ( m_dictFiles[idxDictInserted].m_pOld == NULL ) sReasonCannotReuse.Format( "File '%s' was inserted\n", pszInsertedFilename ); else sReasonCannotReuse.Format( "Chunk reordered. '%s' listed where '%s' used to be.", pszInsertedFilename, pszFilename ); } else { sReasonCannotReuse.Format( "Chunk was reordered. File '%s' was moved.", pszFilename ); } } else if ( f.m_md5Old.IsZero() || f.m_md5New.IsZero() ) { vecFilesToCompareContents.AddToTail( idxOldFile ); } else if ( f.m_md5Old != f.m_md5New ) { sReasonCannotReuse.Format( "File '%s' changed. (Based on MD5s in control file.)", pszFilename ); } } ++idxOldFile; ++idxInChunk; } // Check if we need to actually compare any file contents if ( sReasonCannotReuse.IsEmpty() && vecFilesToCompareContents.Count() > 0 ) { // We'll have to actually load the source file // and compare the CRC printf( "%s: Checking for differences using file CRCs...\n", pszShortDataFilename ); FOR_EACH_VEC( vecFilesToCompareContents, i ) { VPKBuildFile_t const &f = *m_vecOldFilesInChunkOrder[ vecFilesToCompareContents[i] ]; Assert( f.m_pOld ); // Load the input file CUtlBuffer buf; if ( !g_pFullFileSystem->ReadFile( f.m_sNameOnDisk, NULL, buf ) || buf.TellPut() != (int)f.m_pOld->m_iTotalSize ) { Error( "Error reading %s", f.m_sNameOnDisk.String() ); } // Calculate the CRC uint32 crc = CRC32_ProcessSingleBuffer( buf.Base(), f.m_pOld->m_iTotalSize ); // Mismatch? if ( crc != f.m_pOld->m_crc ) { sReasonCannotReuse.Format( "File '%s' changed. (CRCs differs from %s.)", f.m_pOld->m_sName.String(), f.m_sNameOnDisk.String() ); break; } } } // Can we take this file as is? if ( sReasonCannotReuse.IsEmpty() ) { printf( "%s could be reused.\n", pszShortDataFilename ); // Map the chunk VPKInputFileRange_t chunkRange; chunkRange.m_iChunkFilenameIndex = idxChunk; chunkRange.m_iFirstInputFile = firstFile.m_iNewSortIndex; chunkRange.m_iLastInputFile = firstFile.m_iNewSortIndex + idxInChunk - 1; chunkRange.m_bKeepExistingFile = true; AddRange( chunkRange ); } else { printf( "%s cannot be reused. %s\n", pszShortDataFilename, sReasonCannotReuse.String() ); } } } // Take file ranges that are not mapped to a chunk, and map them. MapAllRangesToChunks(); int nNewChunkCount = m_llFileRanges.Count(); printf( "Pack file will contain %d chunk files\n", nNewChunkCount ); // Remove files from directory that have been deleted int iFilesRemoved = 0; FOR_EACH_DICT_FAST( m_dictFiles, i ) { const VPKBuildFile_t &bf = m_dictFiles[i]; if ( bf.m_pOld && !bf.m_pNew ) m_packfile.RemoveFileFromDirectory( bf.m_pOld->m_sName.String() ); } printf( "Removing %d files from the directory\n", iFilesRemoved ); // Make sure ranges are cool SanityCheckRanges(); // Grow chunk -> range table as necessary while ( m_vecRangeForChunk.Count() < nNewChunkCount ) m_vecRangeForChunk.AddToTail( m_llFileRanges.InvalidIndex() ); // OK, at this point, we're ready to assign any ranges that have // not yet been assigned a range an appropriate range index int idxChunk = 0; int iChunksToKeep = 0; int iFilesToKeep = 0; int64 iChunkSizeToKeep = 0; int iChunksToWrite = 0; int iFilesToWrite = 0; int64 iChunkSizeToWrite = 0; FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t &r = m_llFileRanges[ idxRange ]; if ( r.m_iChunkFilenameIndex >= 0 ) { Assert( r.m_bKeepExistingFile ); iChunksToKeep += 1; iChunkSizeToKeep += r.m_nTotalSizeInChunkFile; iFilesToKeep += r.FileCount(); continue; } // Range has not been assigned a chunk. // Locate the next chunk index // that has not been assigned to a range while ( m_vecRangeForChunk[idxChunk] != m_llFileRanges.InvalidIndex() ) { ++idxChunk; Assert( idxChunk < nNewChunkCount ); } // Map the range MapRangeToChunk( idxRange, idxChunk, false ); ++idxChunk; Assert( idxChunk <= nNewChunkCount ); iChunksToWrite += 1; iChunkSizeToWrite += r.m_nTotalSizeInChunkFile; iFilesToWrite += r.FileCount(); } // Now scan chunks in order, and write and chunks that changed. bool bNeedToWriteDir = false; for ( int idxChunk = 0 ; idxChunk < nNewChunkCount ; ++idxChunk ) { int idxRange = m_vecRangeForChunk[ idxChunk ]; VPKInputFileRange_t &r = m_llFileRanges[ idxRange ]; char szDataFilename[ MAX_PATH ]; m_packfile.GetDataFileName( szDataFilename, sizeof(szDataFilename), idxChunk ); const char *pszShortDataFilename = V_GetFileName( szDataFilename ); // Dump info about the chunk and what we're doing with it printf( "%s %s (%d files, %lld bytes)\n", r.m_bKeepExistingFile ? "Keeping" : "Writing", pszShortDataFilename, r.FileCount(), (long long)r.m_nTotalSizeInChunkFile ); if ( s_bBeVerbose ) { printf( " First file: %s\n", m_vecNewFilesInChunkOrder[ r.m_iFirstInputFile ]->m_sName.String() ); printf( " Last file : %s\n", m_vecNewFilesInChunkOrder[ r.m_iLastInputFile ]->m_sName.String() ); } // Retaining the existing file? if ( r.m_bKeepExistingFile ) { // Mark the input files in this chunk as having been assigned to this chunk. for ( int idxFile = r.m_iFirstInputFile ; idxFile <= r.m_iLastInputFile ; ++idxFile ) { VPKContentFileInfo_t *f = m_vecNewFilesInChunkOrder[ idxFile ]; f->m_idxChunk = idxChunk; } continue; } // Create the output file. FileHandle_t fChunkWrite = g_pFullFileSystem->Open( szDataFilename, "wb" ); if ( !fChunkWrite ) Error( "Can't create %s\n", szDataFilename ); // Scan input files in order. uint32 iOffsetInChunk = 0; for ( int idxFile = r.m_iFirstInputFile ; idxFile <= r.m_iLastInputFile ; ++idxFile ) { VPKContentFileInfo_t *f = m_vecNewFilesInChunkOrder[ idxFile ]; int idxInDict = m_dictFiles.Find( f->m_sName.String() ); Assert( idxInDict >= 0 ); VPKBuildFile_t *bf = &m_dictFiles[ idxInDict ]; Assert( bf->m_pNew == f ); // Load the input file CUtlBuffer buf; if ( !g_pFullFileSystem->ReadFile( bf->m_sNameOnDisk, NULL, buf ) || buf.TellPut() != (int)f->m_iTotalSize ) { Error( "Error reading %s", bf->m_sNameOnDisk.String() ); } Assert( iOffsetInChunk == g_pFullFileSystem->Tell( fChunkWrite ) ); // Calculate the CRC f->m_crc = CRC32_ProcessSingleBuffer( buf.Base(), f->m_iTotalSize ); // Finish filling in all of the header f->m_iOffsetInChunk = iOffsetInChunk; f->m_idxChunk = idxChunk; f->m_pPreloadData = buf.Base(); // Update the directory m_packfile.AddFileToDirectory( *f ); // Write the data int nBytesToWrite = f->GetSizeInChunkFile(); int nBytesWritten = g_pFullFileSystem->Write( (byte*)buf.Base() + f->m_iPreloadSize, nBytesToWrite, fChunkWrite ); if ( nBytesWritten != nBytesToWrite ) Error( "Error writing %s", szDataFilename ); iOffsetInChunk += nBytesToWrite; Assert( iOffsetInChunk == g_pFullFileSystem->Tell( fChunkWrite ) ); // Align Assert( s_iChunkAlign > 0 ); while ( iOffsetInChunk % s_iChunkAlign ) { unsigned char zero = 0; g_pFullFileSystem->Write( &zero, 1, fChunkWrite ); ++iOffsetInChunk; } // Let's clear this pointer just for grins f->m_pPreloadData = NULL; } g_pFullFileSystem->Close( fChunkWrite ); // While we know the data is sitting in the OS file cache, // let's immediately re-calc the chunk hashes m_packfile.HashChunkFile( idxChunk ); // We'll need to re-save the directory bNeedToWriteDir = true; } // Delete any extra chunks that aren't needed anymore for ( int iChunkToDelete = nNewChunkCount ; iChunkToDelete < nOldChunkCount ; ++iChunkToDelete ) { char szDataFilename[ MAX_PATH ]; m_packfile.GetDataFileName( szDataFilename, sizeof(szDataFilename), iChunkToDelete ); printf( "Deleting %s.\n", szDataFilename ); g_pFullFileSystem->RemoveFile( szDataFilename ); if ( g_pFullFileSystem->FileExists( szDataFilename ) ) Error( "Failed to delete %s\n", szDataFilename ); m_packfile.DiscardChunkHashes( iChunkToDelete ); // We'll need to re-save the directory bNeedToWriteDir = true; } if ( s_bBeVerbose ) { printf( "Chunk files: %12s%12s\n", "Retained", "Written" ); printf( " Pack file chunks: %12d%12d\n", iChunksToKeep, iChunksToWrite ); printf( " Data files: %12d%12d\n", iFilesToKeep, iFilesToWrite ); printf( " Bytes in chunk: %12lld%12lld\n", (long long)iChunkSizeToKeep, (long long)iChunkSizeToWrite ); } // Finally, scan for any files that need to go in the directory, // but don't have any data in a chunk. (Zero byte files, or all // data is in the preload area.) FOR_EACH_DICT( m_dictFiles, idxInDict ) { VPKBuildFile_t *bf = &m_dictFiles[ idxInDict ]; VPKContentFileInfo_t *pNew = bf->m_pNew; if ( pNew == NULL || pNew->m_idxChunk >= 0 ) continue; Assert( pNew->GetSizeInChunkFile() == 0 ); // Check if the file has changed and we need to update the directory VPKContentFileInfo_t *pOld = bf->m_pOld; int iNeedToUpdateFile = 1; if ( pOld ) { if ( pOld->m_iTotalSize != pNew->m_iTotalSize || pOld->m_iPreloadSize != pNew->m_iPreloadSize ) { iNeedToUpdateFile = 1; } else if ( !bf->m_md5Old.IsZero() && !bf->m_md5New.IsZero() ) { // We have hashes and can make the determination purely from the hashes if ( bf->m_md5Old == bf->m_md5New ) iNeedToUpdateFile = 0; else iNeedToUpdateFile = 1; } else { // Not able to make a determination without loading the file iNeedToUpdateFile = -1; } } // Might we need to update the file? if ( iNeedToUpdateFile == 0 ) { // We were able to determine that the files match, and // we know that there's no need to load the input file or // check CRC's continue; } // If we get here, we might need to update the header. // Load the file CUtlBuffer buf; if ( !g_pFullFileSystem->ReadFile( bf->m_sNameOnDisk, NULL, buf ) || buf.TellPut() != (int)pNew->m_iTotalSize ) { Error( "Error reading %s", bf->m_sNameOnDisk.String() ); } // Calculate the CRC pNew->m_crc = CRC32_ProcessSingleBuffer( buf.Base(), pNew->m_iTotalSize ); // Compare CRC's if ( iNeedToUpdateFile < 0 ) { Assert( pOld ); if ( pOld->m_crc == pNew->m_crc ) continue; } // We need to add the file to the header if ( pNew->m_iPreloadSize > 0 ) pNew->m_pPreloadData = buf.Base(); else Assert( pNew->m_iTotalSize == 0 ); // Write the directory entry. This will make a copy of any preload data m_packfile.AddFileToDirectory( *pNew ); // Let's clear this pointer just for grins pNew->m_pPreloadData = NULL; // We'll need to re-save the directory bNeedToWriteDir = true; } // Nothing changed? if ( !bNeedToWriteDir ) { if ( m_sReasonToForceWriteDirFile.IsEmpty() ) { printf( "Nothing changed; not writing directory file.\n" ); return; } printf( "VPK contents not changed, but directory needs to be resaved. %s.\n", m_sReasonToForceWriteDirFile.String() ); } if ( s_bBeVerbose ) printf( "Hashing metadata.\n" ); m_packfile.HashMetadata(); if ( s_bBeVerbose ) printf( "Writing directory file.\n" ); m_packfile.Write(); } void VPKBuilder::LoadInputKeys( const char *pszControlFilename ) { KeyValues *pInputKeys = new KeyValues( "packkeys" ); if ( !pInputKeys->LoadFromFile( g_pFullFileSystem, pszControlFilename ) ) Error( "Failed to load %s", pszControlFilename ); SetInputKeys( pInputKeys, pszControlFilename ); } void VPKBuilder::SetInputKeys( KeyValues *pInputKeys, const char *pszControlFilename ) { m_pInputKeys = pInputKeys; m_sControlFilename = pszControlFilename; m_iNewTotalFileSize = 0; m_iNewTotalFileSizeInChunkFiles = 0; int iSortIndex = 0; for ( KeyValues *i = m_pInputKeys; i; i = i->GetNextKey() ) { const char *pszNameOnDisk = i->GetString( "srcpath", i->GetName() ); char szNameInVPK[ MAX_PATH ]; V_strcpy_safe( szNameInVPK, i->GetString( "destpath", "" ) ); if ( szNameInVPK[0] == '\0' ) Error( "File '%s' is missing 'destpath' in KeyValues control file", pszNameOnDisk ); V_FixSlashes( szNameInVPK, '\\' ); // always use Windows slashes in VPK // Fail if passed an absolute path. if ( szNameInVPK[0] == '\\' ) Error( "destpath '%s' is an absolute path; only relative paths should be used", szNameInVPK ); // Check to make sure that no restricted file types are being added to the VPK. if ( IsRestrictedFileType( szNameInVPK ) ) { printf( "WARNING: Control file lists '%s'. We cannot put that type of file in the pack.\n", szNameInVPK ); continue; } // Make sure we have a dictionary entry int idxInDict = m_dictFiles.Find( szNameInVPK ); if ( idxInDict == m_dictFiles.InvalidIndex() ) idxInDict = m_dictFiles.Insert( szNameInVPK ); VPKBuildFile_t &bf = m_dictFiles[ idxInDict ]; if ( !bf.m_sNameOnDisk.IsEmpty() || bf.m_pNew || bf.m_iNewSortIndex >= 0 ) Error( "destpath '%s' in VPK appears multiple times in the KV control file.\n (Source files '%s' and '%s')", szNameInVPK, bf.m_sNameOnDisk.String(), pszNameOnDisk ); bf.m_sNameOnDisk = pszNameOnDisk; VPKContentFileInfo_t *f = new VPKContentFileInfo_t; f->m_sName = szNameInVPK; f->m_iTotalSize = g_pFullFileSystem->Size( pszNameOnDisk ); f->m_iPreloadSize = Min( (uint32)i->GetInt( "preloadsize", 0 ), f->m_iTotalSize ); const char *pszMD5 = i->GetString( "md5", "" ); if ( *pszMD5 ) { if ( V_strlen( pszMD5 ) != MD5_DIGEST_LENGTH*2 ) Error( "File '%s' has invalid MD5 '%s'", pszNameOnDisk, pszMD5 ); V_hextobinary( pszMD5, MD5_DIGEST_LENGTH*2, bf.m_md5New.bits, MD5_DIGEST_LENGTH ); } m_vecNewFiles.AddToTail( f ); bf.m_pNew = f; if ( f->GetSizeInChunkFile() > 0 ) { bf.m_iNewSortIndex = iSortIndex++; m_vecNewFilesInChunkOrder.AddToTail( f ); } m_iNewTotalFileSize += f->m_iTotalSize; m_iNewTotalFileSizeInChunkFiles += f->GetSizeInChunkFile(); } printf( "Control file lists %d files\n", m_vecNewFiles.Count() ); printf( " Total file size . . . . : %12lld bytes\n", (long long)m_iNewTotalFileSize ); printf( " Size in preload area . : %12lld bytes\n", (long long)(m_iNewTotalFileSize - m_iNewTotalFileSizeInChunkFiles ) ); printf( " Size in data area . . . : %12lld bytes\n", (long long)m_iNewTotalFileSizeInChunkFiles ); } void VPKBuilder::MapAllRangesToChunks() { //PrintRangeDebug(); // If a range is NOT at least as big as one chunk file, then we will have to merge it // with an adjacent range --- that is, we will need to unmap an adjacent range. // So the first step will be to identify which of the currently mapped ranges // to unmap in order to get rid of any ranges that cannot get mapped to a chunk. // We might have a choice in the matter, and each range that we unmap means // another file that will have to be rewritten. So the goal here is to minimize // the number/size of chunks that we unmap and force to rewrite. // // The current state of affairs should be that all mapped regions correspond to chunk // files that do not need to be rewritten, and there are no two unmapped chunk files in a row. int64 iSizeTooSmallForAChunk = (int64)m_packfile.GetWriteChunkSize() * 95 / 100; for (;;) { for (;;) { // Make sure the problem of small chunks can be solved by unmapping // a mapped chunk UnmapAllRangesForChangedChunks(); CoaleseAllUnmappedRanges(); // Find a mapped region next to a region that's too // small to get its own chunk. If there are multiple, // we'll choose the "best" one to coalesce according to // a greedy algorithm. int idxBestRangeToUnmap = m_llFileRanges.InvalidIndex(); int iBestScore = -1; int64 iBestSize = -1; FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t &r = m_llFileRanges[ idxRange ]; if ( r.m_iChunkFilenameIndex < 0 ) continue; // Check if neighbors exist and are too small // for their own chunk. Calculate score heuristic // based on how good of a candidate we are to // be the one to get combined with our neighbors int iScore = 0; int idxPrev = m_llFileRanges.Previous( idxRange ); if ( idxPrev != m_llFileRanges.InvalidIndex() ) { VPKInputFileRange_t &p = m_llFileRanges[ idxPrev ]; if ( p.m_iChunkFilenameIndex < 0 && p.m_nTotalSizeInChunkFile < iSizeTooSmallForAChunk ) { ++iScore; if ( idxPrev == m_llFileRanges.Head() ) iScore += 3; // Nobody else could fix this, so we need to do it } } int idxNext = m_llFileRanges.Next( idxRange ); if ( idxNext != m_llFileRanges.InvalidIndex() ) { VPKInputFileRange_t &n = m_llFileRanges[ idxNext ]; if ( n.m_iChunkFilenameIndex < 0 && n.m_nTotalSizeInChunkFile < iSizeTooSmallForAChunk ) { ++iScore; if ( idxNext == m_llFileRanges.Tail() ) iScore += 3; // Nobody else could fix this, so we need to do it } } // Do we have any reason at all to absorb our neighbors? if ( iScore == 0 ) continue; // Check if we're the best one so far to absorb our neighbor if ( iScore < iBestScore ) continue; // When choosing which of two neighbors should absorb a new gap, add it to the smaller one. // (That will be less to rewrite and also keep the chunk size at a more desirable level.) if ( iScore == iBestScore && r.m_nTotalSizeInChunkFile > iBestSize ) continue; // We're the new best iBestScore = iScore; idxBestRangeToUnmap = idxRange; iBestSize = r.m_nTotalSizeInChunkFile; } // Did we find a range that needed to absorb its neighbor? if ( idxBestRangeToUnmap == m_llFileRanges.InvalidIndex() ) break; // Unmap it MapRangeToChunk( idxBestRangeToUnmap, -1, false ); // We'll coalesce the unmapped region with its neighbor(s) and // start the whole process over } // OK, at this point, if there were any ranges that were too small to hold their // own chunks, then we should have merged them. (Unless there is exactly one range.) // The next step is to split up ranges that are too large for a single chunk. SanityCheckRanges(); FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t *r = &m_llFileRanges[ idxRange ]; // Check how many chunks this int iChunks = r->m_nTotalSizeInChunkFile / m_packfile.GetWriteChunkSize(); if ( iChunks <= 1 ) continue; // If they consistently build with the same chunk size, then // we should only hit this for ranges that are going to be rewritten. // However, if this chunk is already fine as it, let's leave it alone. // There's no reason to split it. if ( r->m_iChunkFilenameIndex >= 0 ) { Assert( r->m_bKeepExistingFile ); printf( "Chunk %d is currently bigger than desired chunk size of %d bytes, but we're not splitting it because the contents have not changed.\n", r->m_iChunkFilenameIndex, m_packfile.GetWriteChunkSize() ); continue; } // Try to split off approximately 1 N/th of the data into this chunk. // Note that if we have big files inside, we might not have enough granularity to // do exactly what they desire and could get caught in a bad state int64 iDesiredSize = r->m_nTotalSizeInChunkFile / iChunks; Assert( iDesiredSize >= m_packfile.GetWriteChunkSize() ); int iNewLastInputFile = r->m_iFirstInputFile; int64 iNewSize = m_vecNewFilesInChunkOrder[ iNewLastInputFile ]->GetSizeInChunkFile(); while ( iNewSize < iDesiredSize && iNewLastInputFile < r->m_iLastInputFile ) { ++iNewLastInputFile; iNewSize += m_vecNewFilesInChunkOrder[ iNewLastInputFile ]->GetSizeInChunkFile(); } // Do the split int iSaveFirstInputFile = r->m_iFirstInputFile; SplitRangeAt( iNewLastInputFile+1 ); r = &m_llFileRanges[ idxRange ]; // ranges may have moved in memory! // Here we make an assumption that SplitRangeAt will keep range idxRange // modified and link the new range AFTER this range. Verify that assumption. Assert( r->m_iFirstInputFile == iSaveFirstInputFile ); Assert( r->m_iLastInputFile == iNewLastInputFile ); Assert( r->m_nTotalSizeInChunkFile == iNewSize ); // We've got this range approximately to the desired size. // The next range should be approximately (N-1)/N as big as the original // size of this range, and if N>2, then it wil need to be split, too } // OK, all ranges should now be the appropriate size, and should // map to exactly one chunk. We just haven't assigned the chunk // numbers yet. The important thing to realize is that the numbers // are essentially arbitrary, and if we're going to rewrite a file, // it doesn't matter if data moves from one chunk to another with // a totally different number. However....leaving a gap is probably // a bad idea. We don't know what assumptions existing tools make, // and this could be confusing and look like a missing file. So // if we have N chunks, we will always number them 0...N-1. int nNewChunkCount = m_llFileRanges.Count(); // Check if the number of chunks has been reduced, and a chunk file // that we previously thought we would be able to retain has // a file index that won't exist any more, then let's unmap those ranges // and start over. bool bNeedToStartOver = false; for ( int i = m_vecRangeForChunk.Count()-1 ; i >= nNewChunkCount ; --i ) { int idxRange = m_vecRangeForChunk[i]; if ( idxRange == m_llFileRanges.InvalidIndex() ) continue; Assert( m_llFileRanges[ idxRange ].m_iChunkFilenameIndex == i ); Assert( m_llFileRanges[ idxRange ].m_bKeepExistingFile ); MapRangeToChunk( idxRange, -1, false ); bNeedToStartOver = true; } if ( !bNeedToStartOver ) break; // We unmapped a chunk because the chunk file is going to // get deleted. Start all over! } } void VPKBuilder::UnmapAllRangesForChangedChunks() { SanityCheckRanges(); FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t &r = m_llFileRanges[ idxRange ]; // If range was assigned a chunk, but the chunk file will have to be rewitten, // then unmap it if ( r.m_iChunkFilenameIndex >= 0 && !r.m_bKeepExistingFile ) MapRangeToChunk( idxRange, -1, false ); } SanityCheckRanges(); } void VPKBuilder::CoaleseAllUnmappedRanges() { SanityCheckRanges(); int idxRange = m_llFileRanges.Head(); for (;;) { int idxNext = m_llFileRanges.Next( idxRange ); if ( idxNext == m_llFileRanges.InvalidIndex() ) break; // Grab shortcuts VPKInputFileRange_t &ri = m_llFileRanges[ idxRange ]; VPKInputFileRange_t &rn = m_llFileRanges[ idxNext ]; // Both chunks unassigned? if ( ri.m_iChunkFilenameIndex < 0 && rn.m_iChunkFilenameIndex < 0 ) { // Merge current with next ri.m_iLastInputFile = rn.m_iLastInputFile; CalculateRangeTotalSizeInChunkFile( ri ); m_llFileRanges.Remove( idxNext ); // List should be valid at this point SanityCheckRanges(); } else { // Keep it, advance to the next one idxRange = idxNext; } } } void VPKBuilder::CalculateRangeTotalSizeInChunkFile( VPKInputFileRange_t &range ) const { range.m_nTotalSizeInChunkFile = 0; for ( int i = range.m_iFirstInputFile ; i <= range.m_iLastInputFile ; ++i ) { range.m_nTotalSizeInChunkFile += m_vecNewFilesInChunkOrder[ i ]->GetSizeInChunkFile(); } } void VPKBuilder::SanityCheckRanges() { int iFileIndex = 0; int64 iTotalSizeInChunks = 0; FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t &r = m_llFileRanges[ idxRange ]; Assert( r.m_iFirstInputFile == iFileIndex ); Assert( r.m_iLastInputFile >= r.m_iFirstInputFile ); iFileIndex = r.m_iLastInputFile + 1; iTotalSizeInChunks += r.m_nTotalSizeInChunkFile; } Assert( iFileIndex == m_vecNewFilesInChunkOrder.Count() ); Assert( iTotalSizeInChunks == m_iNewTotalFileSizeInChunkFiles ); } void VPKBuilder::PrintRangeDebug() { FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t &r = m_llFileRanges[ idxRange ]; printf( "Range handle %d:\n", idxRange ); printf( " File range %d .. %d\n", r.m_iFirstInputFile, r.m_iLastInputFile ); printf( " Chunk %d%s\n", r.m_iChunkFilenameIndex, r.m_bKeepExistingFile ? " (keep existing file)" : "" ); printf( " Size %lld\n", (long long)r.m_nTotalSizeInChunkFile ); } } void VPKBuilder::AddRange( VPKInputFileRange_t range ) { // Sanity check that ranges are in a valid order SanityCheckRanges(); // Split up the range(s) we overlap so that we will match exactly one range SplitRangeAt( range.m_iFirstInputFile ); SplitRangeAt( range.m_iLastInputFile+1 ); // Locate the range FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t *p = &m_llFileRanges[ idxRange ]; if ( p->m_iLastInputFile < range.m_iFirstInputFile ) continue; // Range should now match exactly Assert( p->m_iFirstInputFile == range.m_iFirstInputFile ); Assert( p->m_iLastInputFile == range.m_iLastInputFile ); // Assign it to the proper chunk MapRangeToChunk( idxRange, range.m_iChunkFilenameIndex, range.m_bKeepExistingFile ); return; } // We should have found it Assert( false ); } void VPKBuilder::SplitRangeAt( int iFirstInputFile ) { // Sanity check that ranges are in a valid order SanityCheckRanges(); // Now Locate any ranges that we overlap, and split them as appropriate FOR_EACH_LL( m_llFileRanges, idxRange ) { VPKInputFileRange_t *p = &m_llFileRanges[ idxRange ]; // No need to make any changes if split already exists at requested location if ( p->m_iFirstInputFile == iFirstInputFile || p->m_iLastInputFile+1 == iFirstInputFile) return; // Found the range to split? Assert( p->m_iFirstInputFile < iFirstInputFile ); if ( p->m_iLastInputFile >= iFirstInputFile ) { // We should only be spliting up unallocated space Assert( p->m_iChunkFilenameIndex < 0 ); VPKInputFileRange_t newRange = *p; p->m_iLastInputFile = iFirstInputFile-1; newRange.m_iFirstInputFile = iFirstInputFile; CalculateRangeTotalSizeInChunkFile( newRange ); CalculateRangeTotalSizeInChunkFile( *p ); m_llFileRanges.InsertAfter( idxRange, newRange ); // Make sure we didn't screw anything up SanityCheckRanges(); return; } } // We should have found something Assert( false ); } void VPKBuilder::MapRangeToChunk( int idxRange, int iChunkFilenameIndex, bool bKeepExistingFile ) { VPKInputFileRange_t *p = &m_llFileRanges[ idxRange ]; // If range was already mapped to a chunk, unmap it. if ( p->m_iChunkFilenameIndex >= 0 ) { Assert( m_vecRangeForChunk[ p->m_iChunkFilenameIndex ] == idxRange ); m_vecRangeForChunk[ p->m_iChunkFilenameIndex ] = m_llFileRanges.InvalidIndex(); p->m_iChunkFilenameIndex = -1; p->m_bKeepExistingFile = false; } // Map range to a chunk? if ( iChunkFilenameIndex >= 0 ) { Assert( m_vecRangeForChunk[ iChunkFilenameIndex ] == m_llFileRanges.InvalidIndex() ); p->m_iChunkFilenameIndex = iChunkFilenameIndex; p->m_bKeepExistingFile = bKeepExistingFile; m_vecRangeForChunk[ iChunkFilenameIndex ] = idxRange; } else { Assert( !bKeepExistingFile ); } } #ifdef VPK_ENABLE_SIGNING void GenerateKeyPair( const char *pszBaseKeyName ) { printf( "Generating RSA public/private keypair...\n" ); // // This code pretty much copied from vsign.cpp // uint8 rgubPublicKey[k_nRSAKeyLenMax]={0}; uint cubPublicKey = Q_ARRAYSIZE( rgubPublicKey ); uint8 rgubPrivateKey[k_nRSAKeyLenMax]={0}; uint cubPrivateKey = Q_ARRAYSIZE( rgubPrivateKey ); if( !CCrypto::RSAGenerateKeys( rgubPublicKey, &cubPublicKey, rgubPrivateKey, &cubPrivateKey ) ) { Error( "Failed to generate RSA keypair.\n" ); } char rgchEncodedPublicKey[k_nRSAKeyLenMax*4]; uint cubEncodedPublicKey = Q_ARRAYSIZE( rgchEncodedPublicKey ); if( !CCrypto::HexEncode( rgubPublicKey, cubPublicKey, rgchEncodedPublicKey, cubEncodedPublicKey ) ) { Error( "Failed to encode public key.\n" ); } // Don't encrypt // uint8 rgubEncryptedPrivateKey[Q_ARRAYSIZE( rgubPrivateKey )*2]; // uint cubEncryptedPrivateKey = Q_ARRAYSIZE( rgubEncryptedPrivateKey ); // // if( !CCrypto::SymmetricEncrypt( rgubPrivateKey, cubPrivateKey, rgubEncryptedPrivateKey, &cubEncryptedPrivateKey, (uint8 *)rgchPassphrase, k_nSymmetricKeyLen ) ) // { // printf( "ERROR! Failed to encrypt private key.\n" ); // return false; // } char rgchEncodedEncryptedPrivateKey[Q_ARRAYSIZE( rgubPrivateKey )*8]; if( !CCrypto::HexEncode( rgubPrivateKey, cubPrivateKey, rgchEncodedEncryptedPrivateKey, Q_ARRAYSIZE(rgchEncodedEncryptedPrivateKey) ) ) { Error( "Failed to encode private key.\n" ); } // Good Lord. Use fopen, because it will work without any surprising crap or hidden limitations. // I just wasted an hour trying to get CUtlBuffer and our filesystem to print a block of text to a file. // Save public keyfile { CUtlString sPubFilename( pszBaseKeyName ); sPubFilename += ".publickey.vdf"; FILE *f = fopen( sPubFilename, "wt" ); if ( f == NULL ) Error( "Cannot create %s.", sPubFilename.String() ); // Write public keyfile fprintf( f, "// Public key file. You can publish this key file and share it with the world.\n" "// It can be used by third parties to verify any signatures made with the corresponding private key.\n" "public_key\n" "{\n" "\ttype \"rsa\"\n" "\trsa_public_key \"%s\"\n" "}\n", rgchEncodedPublicKey ); fclose(f); printf( " Saved %s\n", sPubFilename.String() ); } // Save private keyfile { CUtlString sPrivFilename( pszBaseKeyName ); sPrivFilename += ".privatekey.vdf"; FILE *f = fopen( sPrivFilename, "wt" ); if ( f == NULL ) Error( "Cannot create %s.", sPrivFilename.String() ); fprintf( f, "// Private key file.\n" "// This key can be used to sign files. Third parties can verify your signature by using your public key.\n" "//\n" "// THIS KEY SHOULD BE KEPT SECRET\n" "//\n" "// You should share your public key freely, but anyone who has your private key will be able to impersonate you.\n" "private_key\n" "{\n" "\ttype \"rsa\"\n" "\trsa_private_key \"%s\"\n" "\n" "\t// Note: the private key is stored in plaintext. It is not encrypted or protected by a password.\n" "\t// Anyone who obtains this key can use it to sign files.\n" "\tprivate_key_encrypted 0\n" "\n" "\t// The public key that corresponds to this private key. The public keyfile you can share with others is\n" "\t// saved in another file, but the key data is duplicated here to help you confirm which public key matches\n" "\t// with this private key.\n" "\tpublic_key\n" "\t{\n" "\t\ttype \"rsa\"\n" "\t\trsa_public_key \"%s\"\n" "\t}\n" "}\n", rgchEncodedEncryptedPrivateKey, rgchEncodedPublicKey ); fclose(f); printf( " Saved %s\n", sPrivFilename.String() ); } printf( "\n" ); printf( "REMEMBER: Your private key should be kept secret. Don't share it!\n" ); } static void CheckSignature( const char *pszFilename ) { char szActualFileName[MAX_PATH]; CPackedStore pack( pszFilename, szActualFileName, g_pFullFileSystem ); // Make sure they didn't make a mistake CUtlVector bytesPublicKey; if ( s_sPublicKeyFile.IsEmpty() ) { if ( !s_sPrivateKeyFile.IsEmpty() ) Error( "Private keys are not used to verify signatures. Did you mean to use -k instead?" ); printf( "Checking signature using public key in VPK.\n" "\n" "NOTE: This just confirms that the VPK has a valid signature,\n" " not that signature was made by any particular party. Use -k\n" " and provide a public key in order to verify that a file was\n" " signed by a particular trusted party.\n" ); } else { LoadKeyFile( s_sPublicKeyFile, "rsa_public_key", bytesPublicKey ); printf( "Loaded public key file %s\n", s_sPublicKeyFile.String() ); } printf( "\n" ); fflush( stdout ); CPackedStore::ESignatureCheckResult result = pack.CheckSignature( bytesPublicKey.Count(), bytesPublicKey.Base() ); switch (result ) { default: case CPackedStore::eSignatureCheckResult_Failed: fprintf( stderr, "ERROR: FAILED\n" ); fflush( stderr ); printf( "IO error or other generic failure." ); exit(-1); case CPackedStore::eSignatureCheckResult_NotSigned: fprintf( stderr, "ERROR: NOT SIGNED\n" ); fflush( stderr ); printf( "The VPK does not contain a signature." ); exit(1); case CPackedStore::eSignatureCheckResult_WrongKey: fprintf( stderr, "ERROR: KEY MISMATCH\n" ); fflush( stderr ); printf( "The public key provided does not match the public\n" "key contained in the VPK file. The VPK was not\n" "signed using the private key corresponding to your\n" "public key.\n" ); exit(2); case CPackedStore::eSignatureCheckResult_InvalidSignature: fprintf( stderr, "ERROR: INVALID SIGNATURE\n" ); fflush( stderr ); printf( "The VPK contains a signature, but it isn't valid." ); exit(3); case CPackedStore::eSignatureCheckResult_ValidSignature: printf( "SUCCESS\n" ); if ( s_sPublicKeyFile.IsEmpty() ) { printf( "VPK contains a valid signature." ); } else { printf( "VPK signature validated using the specified public key." ); } exit(0); } } static void CheckHashes( const char *pszFilename ) { char szActualFileName[MAX_PATH]; CPackedStore pack( pszFilename, szActualFileName, g_pFullFileSystem ); char szChunkFilename[ 256 ]; printf( "Checking cache line hashes:\n" ); CUtlSortVector &vecHashes = pack.AccessPackFileHashes(); CPackedStoreFileHandle handle = pack.GetHandleForHashingFiles(); handle.m_nFileNumber = -1; int nCheckedFractionsOK = 0; int nTotalCheckedCacheLines = 0; int nTotalErrorCacheLines = 0; FOR_EACH_VEC( vecHashes, idx ) { ChunkHashFraction_t frac = vecHashes[idx]; if ( idx == 0 || frac.m_nPackFileNumber != handle.m_nFileNumber ) { if ( nCheckedFractionsOK > 0 ) printf( "OK. (%d caches lines)\n", nCheckedFractionsOK ); handle.m_nFileNumber = frac.m_nPackFileNumber; pack.GetPackFileName( handle, szChunkFilename, sizeof(szChunkFilename) ); printf(" %s: ", szChunkFilename ); fflush( stdout ); nCheckedFractionsOK = 0; } FileHash_t filehash; // VPKHandle.m_nFileNumber; // nFileFraction; int64 fileSize = 0; // if we have never hashed this before - do it now pack.HashEntirePackFile( handle, fileSize, frac.m_nFileFraction, frac.m_cbChunkLen, filehash ); ++nTotalCheckedCacheLines; if ( filehash.m_cbFileLen != frac.m_cbChunkLen ) { if ( nCheckedFractionsOK >= 0 ) { printf( "\n" ); fflush( stdout ); nCheckedFractionsOK = -1; } fprintf( stderr, " @%d: size mismatch. Stored: %d Computed: %d\n", frac.m_nFileFraction, frac.m_cbChunkLen, filehash.m_cbFileLen ); fflush( stderr ); ++nTotalErrorCacheLines; } else if ( filehash.m_md5contents != frac.m_md5contents ) { if ( nCheckedFractionsOK >= 0 ) { printf( "\n" ); fflush( stdout ); nCheckedFractionsOK = -1; } char szCalculated[ MD5_DIGEST_LENGTH*2 + 4 ]; char szExpected[ MD5_DIGEST_LENGTH*2 + 4 ]; V_binarytohex( filehash.m_md5contents.bits, MD5_DIGEST_LENGTH, szCalculated, sizeof(szCalculated) ); V_binarytohex( frac.m_md5contents.bits, MD5_DIGEST_LENGTH, szExpected, sizeof(szExpected) ); fprintf( stderr, " @%d: hash mismatch: Got %s, expected %s.\n", frac.m_nFileFraction, szCalculated, szExpected ); fflush( stderr ); ++nTotalErrorCacheLines; } else { if ( nCheckedFractionsOK >= 0 ) ++nCheckedFractionsOK; } } if ( nCheckedFractionsOK > 0 ) printf( "OK. (%d caches lines)\n", nCheckedFractionsOK ); if ( nTotalErrorCacheLines == 0 ) { printf( "All %d cache lines hashes matched OK\n", nTotalCheckedCacheLines ); exit(0); } fprintf( stderr, "%d cache lines failed validation out of %d checked \n", nTotalErrorCacheLines, nTotalCheckedCacheLines ); exit(1); } static void PrintBinaryBlob( const CUtlVector &blob ) { const int kRowLen = 32; for ( int i = 0 ; i < blob.Count() ; i += kRowLen ) { int iEnd = Min( i+kRowLen, blob.Count() ); const char *pszSep = " "; for ( int j = i ; j < iEnd ; ++j ) { printf( "%s%02X", pszSep, blob[j] ); pszSep = ""; } printf( "\n" ); } } static void DumpSignatureInfo( const char *pszFilename ) { char szActualFileName[MAX_PATH]; CPackedStore pack( pszFilename, szActualFileName, g_pFullFileSystem ); if ( pack.GetSignature().Count() == 0 ) { printf( "VPK is not signed\n" ); return; } printf( "Public key:\n" ); PrintBinaryBlob( pack.GetSignaturePublicKey() ); printf( "Signature:\n" ); PrintBinaryBlob( pack.GetSignature() ); } #endif void BuildRecursiveFileList( const char *pcDirName, CUtlStringList &fileList ) { char szDirWildcard[MAX_PATH]; FileFindHandle_t findHandle; V_snprintf( szDirWildcard, sizeof( szDirWildcard ), "%s%c%s", pcDirName, CORRECT_PATH_SEPARATOR, "*.*" ); char const *pcResult = g_pFullFileSystem->FindFirst( szDirWildcard, &findHandle ); if ( pcResult ) { do { char szFullResultPath[MAX_PATH]; if ( '.' == pcResult[0] ) { pcResult = g_pFullFileSystem->FindNext( findHandle ); continue; } // Make a full path to the result V_snprintf( szFullResultPath, sizeof( szFullResultPath ), "%s%c%s", pcDirName, CORRECT_PATH_SEPARATOR, pcResult ); if ( g_pFullFileSystem->IsDirectory( szFullResultPath ) ) { // Recurse BuildRecursiveFileList( szFullResultPath, fileList ); } else { // Add file to the file list fileList.CopyAndAddToTail( szFullResultPath ); } pcResult = g_pFullFileSystem->FindNext( findHandle ); } while ( pcResult ); g_pFullFileSystem->FindClose( findHandle ); } } static void DroppedVpk( const char *pszVpkFilename ) { char szActualFileName[MAX_PATH]; CPackedStore mypack( pszVpkFilename, szActualFileName, g_pFullFileSystem ); CUtlStringList fileNames; char szVPKParentDir[MAX_PATH]; V_strncpy( szVPKParentDir, pszVpkFilename, sizeof( szVPKParentDir ) ); V_SetExtension( szVPKParentDir, "", sizeof( szVPKParentDir ) ); mypack.GetFileList( fileNames, false, true ); for( int i = 0 ; i < fileNames.Count(); i++ ) { char szDestFilePath[MAX_PATH]; CPackedStoreFileHandle pData = mypack.OpenFile( fileNames[i] ); V_snprintf( szDestFilePath, sizeof( szDestFilePath ), "%s%c%s", szVPKParentDir, CORRECT_PATH_SEPARATOR, fileNames[i] ); if ( pData ) { char szParentDirectory[MAX_PATH]; V_ExtractFilePath( szDestFilePath, szParentDirectory, sizeof( szParentDirectory ) ); V_FixSlashes( szParentDirectory ); if ( !g_pFullFileSystem->IsDirectory( szParentDirectory ) ) { g_pFullFileSystem->CreateDirHierarchy( szParentDirectory ); } printf( "extracting %s\n", fileNames[i] ); COutputFile outF( szDestFilePath ); if ( outF.IsOk() ) { int nBytes = pData.m_nFileSize; while( nBytes ) { char cpBuf[65535]; int nReadSize = MIN( sizeof( cpBuf ), nBytes ); mypack.ReadData( pData, cpBuf, nReadSize ); outF.Write( cpBuf, nReadSize ); nBytes -= nReadSize; } outF.Close(); } } } } static void DroppedDirectory( const char *pszDirectoryArg ) { // Strip trailing slash, if any char szDirectory[MAX_PATH]; V_strcpy_safe( szDirectory, pszDirectoryArg ); V_StripTrailingSlash( szDirectory ); char szVPKPath[MAX_PATH]; // Construct path to VPK V_snprintf( szVPKPath, sizeof( szVPKPath ), "%s.vpk", szDirectory ); // Delete any existing one at that location if ( g_pFullFileSystem->FileExists( szVPKPath ) ) { if ( g_pFullFileSystem->IsFileWritable( szVPKPath ) ) { g_pFullFileSystem->RemoveFile( szVPKPath ); } else { fprintf( stderr, "Cannot delete file: %s\n", szVPKPath ); exit(1); } } // Make the VPK char szActualFileName[MAX_PATH]; CPackedStore mypack( szVPKPath, szActualFileName, g_pFullFileSystem, true ); mypack.SetWriteChunkSize( s_iMultichunkSize * 1024*1024 ); // !KLUDGE! Create keyvalues object, since that's what the builder uses printf( "Finding files and creating temporary control file...\n" ); CUtlStringList fileList; BuildRecursiveFileList( szDirectory, fileList ); KeyValues *pInputKeys = new KeyValues("packkeys"); const int nBaseDirLength = V_strlen( szDirectory ); for( int i = 0 ; i < fileList.Count(); i++ ) { // .... Ug O(n^2) KeyValues *pFileKey = pInputKeys->CreateNewKey(); const char *pszFilename = fileList[i]; pFileKey->SetString( "srcpath", pszFilename ); const char *pszDestPath = pszFilename + nBaseDirLength; if ( *pszDestPath == '/' || *pszDestPath == '\\' ) ++pszDestPath; pFileKey->SetString( "destpath", pszDestPath ); } VPKBuilder builder( mypack ); builder.SetInputKeys( pInputKeys->GetFirstSubKey(), "" ); builder.BuildFromInputKeys(); } int main(int argc, char **argv) { InitCommandLineProgram( argc, argv ); int nCurArg = 1; // // Check for standard usage syntax // while ( ( nCurArg < argc ) && ( argv[nCurArg][0] == '-' ) ) { switch( argv[nCurArg][1] ) { case '?': // args { PrintArgSummaryAndExit( 0 ); // return success in this case. } break; case 'M': { s_bMakeMultiChunk = true; } break; case 'P': { s_bUseSteamPipeFriendlyBuilder = true; s_bMakeMultiChunk = true; } break; case 'v': // verbose { s_bBeVerbose = true; } break; case 'a': { nCurArg++; if ( nCurArg >= argc ) { fprintf( stderr, "Expected argument after %s\n", argv[nCurArg-1] ); exit( 1 ); } s_iChunkAlign = V_atoi( argv[nCurArg] ); if ( s_iChunkAlign <= 0 || s_iChunkAlign > 32*1024 ) { fprintf( stderr, "Invalid alignment value %s\n", argv[nCurArg] ); exit( 1 ); } } break; case 'c': { nCurArg++; if ( nCurArg >= argc ) { fprintf( stderr, "Expected argument after %s\n", argv[nCurArg-1] ); exit( 1 ); } s_iMultichunkSize = V_atoi( argv[nCurArg] ); if ( s_iMultichunkSize <= 0 || s_iMultichunkSize > 1*1024 ) { fprintf( stderr, "Invalid chunk size %s\n", argv[nCurArg] ); exit( 1 ); } } break; case 'K': nCurArg++; if ( nCurArg >= argc ) { fprintf( stderr, "Expected argument after %s\n", argv[nCurArg-1] ); exit( 1 ); } s_sPrivateKeyFile = argv[nCurArg]; break; case 'k': nCurArg++; if ( nCurArg >= argc ) { fprintf( stderr, "Expected argument after %s\n", argv[nCurArg-1] ); exit( 1 ); } s_sPublicKeyFile = argv[nCurArg]; break; default: Error( "Unrecognized option '%s'\n", argv[nCurArg] ); } nCurArg++; } argc -= ( nCurArg - 1 ); argv += ( nCurArg - 1 ); if ( argc < 2 ) { Error( "No command specified. Try 'vpk -?' for info.\n" ); } const char *pszCommand = argv[1]; if ( V_stricmp( pszCommand, "l" ) == 0 ) { if ( argc != 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } // list a file char szActualFileName[MAX_PATH]; CPackedStore mypack( argv[2], szActualFileName, g_pFullFileSystem ); CUtlStringList fileNames; mypack.GetFileList( fileNames, pszCommand[0] == 'L', true ); for( int i = 0 ; i < fileNames.Count(); i++ ) { printf( "%s\n", fileNames[i] ); } } else if ( V_strcmp( pszCommand, "a" ) == 0 ) { if ( argc < 3 ) { fprintf( stderr, "Not enough arguments for '%s' command.\n", pszCommand ); exit(1); } char szActualFileName[MAX_PATH]; CPackedStore mypack( argv[2], szActualFileName, g_pFullFileSystem, true ); CheckLoadKeyFilesForSigning( mypack ); for( int i = 3; i < argc; i++ ) { if ( argv[i][0] == '@' ) { // response file? CRequiredInputTextFile hResponseFile( argv[i] + 1 ); CUtlStringList fileList; hResponseFile.ReadLines( fileList ); for( int i = 0 ; i < fileList.Count(); i++ ) { AddFileToPack( mypack, fileList[i] ); } } else { AddFileToPack( mypack, argv[i] ); } } mypack.HashEverything(); mypack.Write(); } else if ( V_strcmp( pszCommand, "k" ) == 0 ) { if ( argc != 4 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } char szActualFileName[MAX_PATH]; CPackedStore mypack( argv[2], szActualFileName, g_pFullFileSystem, true ); mypack.SetWriteChunkSize( s_iMultichunkSize * 1024*1024 ); VPKBuilder builder( mypack ); builder.LoadInputKeys( argv[3] ); builder.BuildFromInputKeys(); } else if ( V_strcmp( pszCommand, "x" ) == 0 ) { if ( argc < 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } // extract a file char szActualFileName[MAX_PATH]; CPackedStore mypack( argv[2], szActualFileName, g_pFullFileSystem ); for( int i = 3; i < argc; i++ ) { CPackedStoreFileHandle pData = mypack.OpenFile( argv[i] ); if ( pData ) { printf( "extracting %s\n", argv[i] ); COutputFile outF( argv[i] ); if ( !outF.IsOk() ) { fprintf( stderr, "Unable to create '%s'.\n", argv[i] ); exit(1); } int nBytes = pData.m_nFileSize; while( nBytes ) { char cpBuf[65535]; int nReadSize = MIN( sizeof( cpBuf ), nBytes ); mypack.ReadData( pData, cpBuf, nReadSize ); outF.Write( cpBuf, nReadSize ); nBytes -= nReadSize; } outF.Close(); } else { printf( "couldn't find file %s\n", argv[i] ); break; } } } else if ( V_strcmp( pszCommand, "B" ) == 0 ) { if ( argc != 4 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } // benchmark CRequiredInputTextFile hResponseFile( argv[3] ); CUtlStringList files; hResponseFile.ReadLines( files ); printf("%d files\n", files.Count() ); float stime = Plat_FloatTime(); BenchMark( files ); printf( " time no pack = %f\n", Plat_FloatTime() - stime ); //g_pFullFileSystem->AddVPKFile( argv[2] ); //stime = Plat_FloatTime(); //BenchMark( files ); //printf( " time pack = %f\n", Plat_FloatTime() - stime ); } else if ( V_strcmp( pszCommand, "rehash" ) == 0 ) { if ( argc != 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } char szActualFileName[MAX_PATH]; CPackedStore mypack( argv[2], szActualFileName, g_pFullFileSystem, true ); CheckLoadKeyFilesForSigning( mypack ); mypack.HashEverything(); mypack.Write(); } else if ( V_strcmp( pszCommand, "checkhash" ) == 0 ) { if ( argc != 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } CheckHashes( argv[2] ); } #ifdef VPK_ENABLE_SIGNING else if ( V_strcmp( pszCommand, "generate_keypair" ) == 0 ) { if ( argc != 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } GenerateKeyPair( argv[2] ); } else if ( V_strcmp( pszCommand, "checksig" ) == 0 ) { if ( argc != 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } CheckSignature( argv[2] ); } else if ( V_strcmp( pszCommand, "dumpsig" ) == 0 ) { if ( argc != 3 ) { fprintf( stderr, "Incorrect number of arguments for '%s' command.\n", pszCommand ); exit(1); } DumpSignatureInfo( argv[2] ); } #endif else if ( argc == 2 && g_pFullFileSystem->IsDirectory( argv[1] ) ) { DroppedDirectory( argv[1] ); } else if ( argc == 2 && V_GetFileExtension( argv[1] ) && V_stristr( V_GetFileExtension( argv[1] ), "vpk") ) { DroppedVpk( argv[1] ); } else { Error( "Unknown command '%s'. Try 'vpk -?' for info.\n", pszCommand ); } return 0; }