diff --git a/SteamKit2/SteamKit2/Types/DepotManifest.cs b/SteamKit2/SteamKit2/Types/DepotManifest.cs
index 068be87df..d34ba9414 100644
--- a/SteamKit2/SteamKit2/Types/DepotManifest.cs
+++ b/SteamKit2/SteamKit2/Types/DepotManifest.cs
@@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
+using System.Linq;
namespace SteamKit2
{
@@ -81,6 +82,10 @@ public class FileData
///
public string FileName { get; internal set; }
///
+ /// Gets the name hash of the file.
+ ///
+ public byte[] FileNameHash { get; internal set; }
+ ///
/// Gets the chunks that this file is composed of.
///
public List Chunks { get; private set; }
@@ -98,9 +103,13 @@ public class FileData
/// Gets the hash of this file.
///
public byte[] FileHash { get; private set; }
+ ///
+ /// Gets symlink target of this file.
+ ///
+ public string LinkTarget { get; private set; }
- internal FileData(string filename, EDepotFileFlag flag, ulong size, byte[] hash, bool encrypted, int numChunks)
+ internal FileData(string filename, byte[] filenameHash, EDepotFileFlag flag, ulong size, byte[] hash, string linkTarget, bool encrypted, int numChunks)
{
if (encrypted)
{
@@ -111,10 +120,12 @@ internal FileData(string filename, EDepotFileFlag flag, ulong size, byte[] hash,
this.FileName = filename.Replace(altDirChar, Path.DirectorySeparatorChar);
}
+ this.FileNameHash = filenameHash;
this.Flags = flag;
this.TotalSize = size;
this.FileHash = hash;
this.Chunks = new List( numChunks );
+ this.LinkTarget = linkTarget;
}
}
@@ -149,6 +160,10 @@ internal FileData(string filename, EDepotFileFlag flag, ulong size, byte[] hash,
/// Gets the total compressed size of all files in this depot.
///
public ulong TotalCompressedSize { get; private set; }
+ ///
+ /// Gets CRC-32 checksum of encrypted manifest payload.
+ ///
+ public uint EncryptedCRC { get; private set; }
internal DepotManifest(byte[] data)
@@ -194,10 +209,53 @@ public bool DecryptFilenames(byte[] encryptionKey)
file.FileName = Encoding.UTF8.GetString( filename ).TrimEnd( new char[] { '\0' } ).Replace(altDirChar, Path.DirectorySeparatorChar);
}
+ // Sort file entries alphabetically because that's what Steam does
+ // TODO: Doesn't match Steam sorting if there are non-ASCII names present
+ Files.Sort( ( f1, f2 ) => StringComparer.OrdinalIgnoreCase.Compare( f1.FileName, f2.FileName ) );
+
FilenamesEncrypted = false;
return true;
}
+ ///
+ /// Serializes depot manifest and saves the output to a file.
+ ///
+ /// Output file name.
+ /// true if serialization was successful; otherwise, false.
+ public bool SaveToFile( string filename )
+ {
+ using ( var fs = File.Open( filename, FileMode.Create ) )
+ using ( var bw = new BinaryWriter( fs ) )
+ {
+ var data = Serialize();
+ if ( data != null )
+ {
+ bw.Write( data );
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Loads binary manifest from a file and deserializes it.
+ ///
+ /// Input file name.
+ /// DepotManifest object if deserialization was successful; otherwise, null.
+ public static DepotManifest? LoadFromFile( string filename )
+ {
+ if ( !File.Exists( filename ) )
+ return null;
+
+ using ( var fs = File.Open( filename, FileMode.Open ) )
+ using ( var ms = new MemoryStream() )
+ {
+ fs.CopyTo( ms );
+ return Deserialize( ms.ToArray() );
+ }
+ }
+
void InternalDeserialize(byte[] data)
{
ContentManifestPayload? payload = null;
@@ -276,7 +334,7 @@ void ParseBinaryManifest(Steam3Manifest manifest)
foreach (var file_mapping in manifest.Mapping)
{
- FileData filedata = new FileData(file_mapping.FileName!, file_mapping.Flags, file_mapping.TotalSize, file_mapping.HashContent!, FilenamesEncrypted, file_mapping.Chunks!.Length);
+ FileData filedata = new FileData(file_mapping.FileName!, file_mapping.HashFileName!, file_mapping.Flags, file_mapping.TotalSize, file_mapping.HashContent!, "", FilenamesEncrypted, file_mapping.Chunks!.Length);
foreach (var chunk in file_mapping.Chunks)
{
@@ -293,7 +351,7 @@ void ParseProtobufManifestPayload(ContentManifestPayload payload)
foreach (var file_mapping in payload.mappings)
{
- FileData filedata = new FileData(file_mapping.filename, (EDepotFileFlag)file_mapping.flags, file_mapping.size, file_mapping.sha_content, FilenamesEncrypted, file_mapping.chunks.Count);
+ FileData filedata = new FileData(file_mapping.filename, file_mapping.sha_filename, (EDepotFileFlag)file_mapping.flags, file_mapping.size, file_mapping.sha_content, file_mapping.linktarget, FilenamesEncrypted, file_mapping.chunks.Count);
foreach (var chunk in file_mapping.chunks)
{
@@ -312,6 +370,116 @@ void ParseProtobufManifestMetadata(ContentManifestMetadata metadata)
CreationTime = DateUtils.DateTimeFromUnixTime( metadata.creation_time );
TotalUncompressedSize = metadata.cb_disk_original;
TotalCompressedSize = metadata.cb_disk_compressed;
+ EncryptedCRC = metadata.crc_encrypted;
+ }
+
+ byte[]? Serialize()
+ {
+ DebugLog.Assert( Files != null, nameof( DepotManifest ), "Files was null when attempting to serialize manifest." );
+
+ var payload = new ContentManifestPayload();
+ var uniqueChunks = new List();
+
+ foreach ( var file in Files )
+ {
+ var protofile = new ContentManifestPayload.FileMapping();
+ protofile.filename = file.FileName.Replace( '/', '\\' );
+ protofile.size = file.TotalSize;
+ protofile.flags = (uint)file.Flags;
+ if ( FilenamesEncrypted )
+ {
+ // Assume the name is unmodified
+ protofile.sha_filename = file.FileNameHash;
+ }
+ else
+ {
+ protofile.sha_filename = CryptoHelper.SHAHash( Encoding.UTF8.GetBytes( file.FileName.Replace( '/', '\\' ).ToLower() ) );
+ }
+ protofile.sha_content = file.FileHash;
+ if ( !string.IsNullOrWhiteSpace( file.LinkTarget ) )
+ {
+ protofile.linktarget = file.LinkTarget;
+ }
+
+ foreach ( var chunk in file.Chunks )
+ {
+ var protochunk = new ContentManifestPayload.FileMapping.ChunkData();
+ protochunk.sha = chunk.ChunkID;
+ protochunk.crc = BitConverter.ToUInt32( chunk.Checksum!, 0 );
+ protochunk.offset = chunk.Offset;
+ protochunk.cb_original = chunk.UncompressedLength;
+ protochunk.cb_compressed = chunk.CompressedLength;
+
+ protofile.chunks.Add( protochunk );
+ if ( !uniqueChunks.Exists( x => x.SequenceEqual( chunk.ChunkID! ) ) )
+ {
+ uniqueChunks.Add( chunk.ChunkID! );
+ }
+ }
+
+ payload.mappings.Add( protofile );
+ }
+
+ var metadata = new ContentManifestMetadata();
+ metadata.depot_id = DepotID;
+ metadata.gid_manifest = ManifestGID;
+ metadata.creation_time = (uint)DateUtils.DateTimeToUnixTime( CreationTime );
+ metadata.filenames_encrypted = FilenamesEncrypted;
+ metadata.cb_disk_original = TotalUncompressedSize;
+ metadata.cb_disk_compressed = TotalCompressedSize;
+ metadata.unique_chunks = (uint)uniqueChunks.Count;
+
+ // Calculate payload CRC
+ using ( var ms_payload = new MemoryStream() )
+ {
+ Serializer.Serialize( ms_payload, payload );
+
+ int len = ( int )ms_payload.Length;
+ byte[] data = new byte[ 4 + len ];
+ Buffer.BlockCopy( BitConverter.GetBytes( len ), 0, data, 0, 4 );
+ Buffer.BlockCopy( ms_payload.ToArray(), 0, data, 4, len );
+ uint crc32 = Crc32.Compute( data );
+
+ if ( FilenamesEncrypted )
+ {
+ metadata.crc_encrypted = crc32;
+ }
+ else
+ {
+ metadata.crc_encrypted = EncryptedCRC;
+ metadata.crc_clear = crc32;
+ }
+ }
+
+ using var ms = new MemoryStream();
+ using var bw = new BinaryWriter( ms );
+
+ // Write Protobuf payload
+ using ( var ms_payload = new MemoryStream() )
+ {
+ Serializer.Serialize( ms_payload, payload );
+ bw.Write( DepotManifest.PROTOBUF_PAYLOAD_MAGIC );
+ bw.Write( ( int )ms_payload.Length );
+ bw.Write( ms_payload.ToArray() );
+ }
+
+ // Write Protobuf metadata
+ using ( var ms_metadata = new MemoryStream() )
+ {
+ Serializer.Serialize( ms_metadata, metadata );
+ bw.Write( DepotManifest.PROTOBUF_METADATA_MAGIC );
+ bw.Write( ( int )ms_metadata.Length );
+ bw.Write( ms_metadata.ToArray() );
+ }
+
+ // Write empty signature section
+ bw.Write( DepotManifest.PROTOBUF_SIGNATURE_MAGIC );
+ bw.Write( 0 );
+
+ // Write EOF marker
+ bw.Write( DepotManifest.PROTOBUF_ENDOFMANIFEST_MAGIC );
+
+ return ms.ToArray();
}
}
}