1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
|
//========= 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
#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 );
int nMetaDataSize = *(reinterpret_cast<uint16 const *>( pData ) );
pData += sizeof( uint16 );
while ( *( ( PackFileIndex_t const *) pData ) != PACKFILEINDEX_END )
{
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.PurgeAndDeleteElements();
FOR_EACH_MAP( m_dirContents, i )
{
m_dirContents[i]->PurgeAndDeleteElements();
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_strcpy( pBaseOut, V_UnqualifiedFileName( pFileName ) );
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();
uint32 nExpectedSignatureSize = 0;
if ( m_SignaturePrivateKey.Count() > 0 && m_SignaturePublicKey.Count() > 0 )
{
#ifdef VPK_ENABLE_SIGNING
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();
}
|