A .NET library for reading and extracting VPK (Valve Pak) files, the uncompressed archive format used to package game content in Source and Source 2 engine games.
usingvarpackage=newPackage();// Open a vpk filepackage.Read("pak01_dir.vpk");// Can also pass in a streampackage.Read(File.OpenRead("pak01_dir.vpk"));// Optionally verify hashes and signatures of the file if there are anypackage.VerifyHashes();// Find a file, this returns a PackageEntryvarfile=package.FindEntry("path/to/file.txt");if(file!=null){// Read a file to a byte arraypackage.ReadEntry(file,outbyte[]fileContents);// Inspect entry metadataConsole.WriteLine(file.GetFullPath());// "path/to/file.txt"Console.WriteLine(file.GetFileName());// "file.txt"Console.WriteLine(file.TotalLength);// file size in bytesConsole.WriteLine(file.CRC32);// CRC32 checksum}Do note that files such as pak01_001.vpk are just data files, you have to open pak01_dir.vpk.
usingvarpackage=newPackage();package.Read("pak01_dir.vpk");foreach(vargroupinpackage.Entries){foreach(varentryingroup.Value){varfilePath=entry.GetFullPath();package.ReadEntry(entry,outbyte[]data);// Create the directory if needed, then write the fileDirectory.CreateDirectory(Path.GetDirectoryName(filePath));File.WriteAllBytes(filePath,data);}}usingvarpackage=newPackage();// Add files to the packagepackage.AddFile("path/to/file.txt",File.ReadAllBytes("file.txt"));package.AddFile("models/example.vmdl",File.ReadAllBytes("example.vmdl"));// Remove a file from the packagepackage.RemoveFile(package.FindEntry("path/to/file.txt"));// Write the package to diskpackage.Write("pak01_dir.vpk");Files added with multiChunk are written into numbered chunk files (pak01_000.vpk, pak01_001.vpk, ...) next to the directory file instead of into the directory file itself. A new chunk file is started once the current one reaches WriteChunkSize (200 MiB by default). The directory file contains MD5 hashes of the chunk files, which can be verified with VerifyChunkHashes.
usingvarpackage=newPackage();// Optionally lower the maximum chunk file size, in bytespackage.WriteChunkSize=100*1024*1024;package.AddFile("models/example.vmdl",File.ReadAllBytes("example.vmdl"),multiChunk:true);// Multi chunk packages must be written to a path so that the chunk files can be created,// and the filename should end with "_dir.vpk"package.Write("pak01_dir.vpk");By default, FindEntry performs a linear scan. If you need to look up many files, call OptimizeEntriesForBinarySearch() before Read() to sort entries and use binary search instead. You can also pass StringComparison.OrdinalIgnoreCase for case-insensitive lookups.
usingvarpackage=newPackage();// Call before Read() to enable binary search for FindEntrypackage.OptimizeEntriesForBinarySearch();package.Read("pak01_dir.vpk");// FindEntry calls are now significantly fastervarfile=package.FindEntry("path/to/file.txt");varentry=package.FindEntry("path/to/file.txt");// Allocate your own buffer (must be at least entry.TotalLength bytes)varbuffer=newbyte[entry.TotalLength];package.ReadEntry(entry,buffer,validateCrc:true);Using ArrayPool to avoid allocations when reading many files:
varentry=package.FindEntry("path/to/file.txt");varbuffer=ArrayPool<byte>.Shared.Rent((int)entry.TotalLength);try{package.ReadEntry(entry,buffer,validateCrc:true);// Use buffer[..entry.TotalLength] here}finally{ArrayPool<byte>.Shared.Return(buffer);}GetMemoryMappedStreamIfPossible returns a memory-mapped stream for large files (over 4 KiB) and a MemoryStream for smaller ones. This avoids reading the entire file into a byte array.
varentry=package.FindEntry("path/to/file.txt");usingvarstream=package.GetMemoryMappedStreamIfPossible(entry);usingvarpackage=newPackage();package.Read("pak01_dir.vpk");// Verify MD5 hashes of the directory tree and whole filepackage.VerifyHashes();// Verify MD5/Blake3 hashes of individual chunk files (pak01_000.vpk, pak01_001.vpk, ...)package.VerifyChunkHashes();// Verify CRC32 checksums of every file in the packagepackage.VerifyFileChecksums();// Verify the RSA signature if the package is signedboolvalid=package.IsSignatureValid();