1use std::cmp::Ordering;
4use std::collections::HashSet;
5use std::io::{Read, Write};
6use std::path::{Path, MAIN_SEPARATOR};
7use std::time::UNIX_EPOCH;
8
9use aes::cipher::block_padding::Pkcs7;
10use aes::cipher::BlockDecryptMut;
11use aes::cipher::KeyInit;
12use aes::Aes256;
13use base64::Engine;
14use cbc::cipher::KeyIvInit;
15use prost::Message;
16use sha1::{Digest, Sha1};
17use steamkit_protos::steam::content_manifest_payload::file_mapping::ChunkData as ProtoChunkData;
18use steamkit_protos::steam::content_manifest_payload::FileMapping as ProtoFileMapping;
19use steamkit_protos::steam::{ContentManifestMetadata, ContentManifestPayload, ContentManifestSignature};
20
21use crate::error::SteamError;
22use crate::types::manifest::{
23 date_time_from_unix_time, date_time_to_unix_time, ByteReader, Steam3Manifest,
24};
25
26const DIRECTORY_SEPARATOR: char = MAIN_SEPARATOR;
28
29const DIRECTORY_SEPARATOR_STR: &str = if MAIN_SEPARATOR == '\\' { "\\" } else { "/" };
31
32const ALT_DIR_CHAR: char = if MAIN_SEPARATOR == '\\' { '/' } else { '\\' };
34
35const ALT_DIR_CHAR_STR: &str = if MAIN_SEPARATOR == '\\' { "/" } else { "\\" };
37
38const PROTOBUF_PAYLOAD_MAGIC: u32 = 0x71F6_17D0;
40const PROTOBUF_METADATA_MAGIC: u32 = 0x1F48_12BE;
42const PROTOBUF_SIGNATURE_MAGIC: u32 = 0x1B81_B817;
44const PROTOBUF_ENDOFMANIFEST_MAGIC: u32 = 0x32C4_15AB;
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
49pub struct ChunkData {
50 pub chunk_id: Option<Vec<u8>>,
52 pub checksum: u32,
54 pub offset: u64,
56 pub compressed_length: u32,
58 pub uncompressed_length: u32,
60}
61
62impl ChunkData {
63 pub fn new() -> ChunkData {
65 ChunkData::default()
66 }
67
68 pub fn with_values(
70 id: Vec<u8>,
71 checksum: u32,
72 offset: u64,
73 comp_length: u32,
74 uncomp_length: u32,
75 ) -> ChunkData {
76 ChunkData {
77 chunk_id: Some(id),
78 checksum,
79 offset,
80 compressed_length: comp_length,
81 uncompressed_length: uncomp_length,
82 }
83 }
84}
85
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
88pub struct FileData {
89 pub file_name: String,
91 pub file_name_hash: Vec<u8>,
93 pub chunks: Vec<ChunkData>,
95 pub flags: u32,
97 pub total_size: u64,
99 pub file_hash: Vec<u8>,
101 pub link_target: Option<String>,
103}
104
105impl FileData {
106 pub fn new() -> FileData {
108 FileData::default()
109 }
110
111 pub fn with_values(
113 filename: &str,
114 filename_hash: Vec<u8>,
115 flags: u32,
116 size: u64,
117 hash: Vec<u8>,
118 link_target: &str,
119 encrypted: bool,
120 num_chunks: usize,
121 ) -> FileData {
122 let file_name = if encrypted {
123 filename.to_string()
124 } else {
125 filename.replace(ALT_DIR_CHAR_STR, DIRECTORY_SEPARATOR_STR)
126 };
127
128 FileData {
129 file_name,
130 file_name_hash: filename_hash,
131 flags,
132 total_size: size,
133 file_hash: hash,
134 chunks: Vec::with_capacity(num_chunks),
135 link_target: Some(link_target.to_string()),
136 }
137 }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct DepotManifest {
143 files: Option<Vec<FileData>>,
144 filenames_encrypted: bool,
145 depot_id: u32,
146 manifest_gid: u64,
147 creation_time: std::time::SystemTime,
148 total_uncompressed_size: u64,
149 total_compressed_size: u64,
150 encrypted_crc: u32,
151}
152
153impl Default for DepotManifest {
154 fn default() -> DepotManifest {
155 DepotManifest {
156 files: None,
157 filenames_encrypted: false,
158 depot_id: 0,
159 manifest_gid: 0,
160 creation_time: UNIX_EPOCH,
161 total_uncompressed_size: 0,
162 total_compressed_size: 0,
163 encrypted_crc: 0,
164 }
165 }
166}
167
168impl DepotManifest {
169 pub fn new() -> DepotManifest {
171 DepotManifest::default()
172 }
173
174 pub fn files(&self) -> Option<&[FileData]> {
176 self.files.as_deref()
177 }
178
179 pub fn set_files(&mut self, files: Option<Vec<FileData>>) {
181 self.files = files;
182 }
183
184 pub fn filenames_encrypted(&self) -> bool {
186 self.filenames_encrypted
187 }
188
189 pub fn set_filenames_encrypted(&mut self, filenames_encrypted: bool) {
191 self.filenames_encrypted = filenames_encrypted;
192 }
193
194 pub fn depot_id(&self) -> u32 {
196 self.depot_id
197 }
198
199 pub fn set_depot_id(&mut self, depot_id: u32) {
201 self.depot_id = depot_id;
202 }
203
204 pub fn manifest_gid(&self) -> u64 {
206 self.manifest_gid
207 }
208
209 pub fn set_manifest_gid(&mut self, manifest_gid: u64) {
211 self.manifest_gid = manifest_gid;
212 }
213
214 pub fn creation_time(&self) -> std::time::SystemTime {
216 self.creation_time
217 }
218
219 pub fn set_creation_time(&mut self, creation_time: std::time::SystemTime) {
221 self.creation_time = creation_time;
222 }
223
224 pub fn total_uncompressed_size(&self) -> u64 {
226 self.total_uncompressed_size
227 }
228
229 pub fn set_total_uncompressed_size(&mut self, total_uncompressed_size: u64) {
231 self.total_uncompressed_size = total_uncompressed_size;
232 }
233
234 pub fn total_compressed_size(&self) -> u64 {
236 self.total_compressed_size
237 }
238
239 pub fn set_total_compressed_size(&mut self, total_compressed_size: u64) {
241 self.total_compressed_size = total_compressed_size;
242 }
243
244 pub fn encrypted_crc(&self) -> u32 {
246 self.encrypted_crc
247 }
248
249 pub fn set_encrypted_crc(&mut self, encrypted_crc: u32) {
251 self.encrypted_crc = encrypted_crc;
252 }
253
254 pub fn deserialize(data: &[u8]) -> Result<DepotManifest, SteamError> {
256 let mut reader = ByteReader::new(data);
257 DepotManifest::internal_deserialize(&mut reader)
258 }
259
260 pub fn deserialize_from_stream(stream: &mut dyn Read) -> Result<DepotManifest, SteamError> {
262 let mut data = Vec::new();
263 stream.read_to_end(&mut data)?;
264 DepotManifest::deserialize(&data)
265 }
266
267 pub async fn load_from_file(
269 filename: impl AsRef<Path>,
270 ) -> Result<Option<DepotManifest>, SteamError> {
271 match tokio::fs::read(filename).await {
272 Ok(data) => Ok(Some(DepotManifest::deserialize(&data)?)),
273 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
274 Err(err) => Err(err.into()),
275 }
276 }
277
278 pub async fn save_to_file(&self, filename: impl AsRef<Path>) -> Result<(), SteamError> {
280 let mut buffer = Vec::new();
281 self.serialize(&mut buffer)?;
282 tokio::fs::write(filename, buffer).await?;
283 Ok(())
284 }
285
286 pub fn serialize(&self, output: &mut dyn Write) -> Result<(), SteamError> {
288 let mut payload = ContentManifestPayload::default();
289 let mut unique_chunks: HashSet<Vec<u8>> = HashSet::new();
290
291 for file in self.files.iter().flatten() {
292 let mut protofile = ProtoFileMapping::default();
293 protofile.size = Some(file.total_size);
294 protofile.flags = Some(file.flags);
295
296 if self.filenames_encrypted {
297 protofile.filename = Some(file.file_name.clone());
299 protofile.sha_filename = Some(file.file_name_hash.clone());
300 } else {
301 let filename = file.file_name.replace('/', "\\");
302 protofile.filename = Some(filename.clone());
303 protofile.sha_filename = Some(
304 Sha1::digest(filename.to_lowercase().as_bytes())
305 .as_slice()
306 .to_vec(),
307 );
308 }
309
310 protofile.sha_content = Some(file.file_hash.clone());
311
312 if let Some(link_target) = file.link_target.as_ref() {
313 if !link_target.trim().is_empty() {
314 protofile.linktarget = Some(link_target.clone());
315 }
316 }
317
318 for chunk in &file.chunks {
319 let mut protochunk = ProtoChunkData::default();
320 protochunk.sha = chunk.chunk_id.clone();
321 protochunk.crc = Some(chunk.checksum);
322 protochunk.offset = Some(chunk.offset);
323 protochunk.cb_original = Some(chunk.uncompressed_length);
324 protochunk.cb_compressed = Some(chunk.compressed_length);
325
326 protofile.chunks.push(protochunk);
327
328 if let Some(chunk_id) = chunk.chunk_id.as_ref() {
329 unique_chunks.insert(chunk_id.clone());
330 }
331 }
332
333 payload.mappings.push(protofile);
334 }
335
336 let mut metadata = ContentManifestMetadata::default();
337 metadata.depot_id = Some(self.depot_id);
338 metadata.gid_manifest = Some(self.manifest_gid);
339 metadata.creation_time = Some(date_time_to_unix_time(self.creation_time) as u32);
340 metadata.filenames_encrypted = Some(self.filenames_encrypted);
341 metadata.cb_disk_original = Some(self.total_uncompressed_size);
342 metadata.cb_disk_compressed = Some(self.total_compressed_size);
343 metadata.unique_chunks = Some(unique_chunks.len() as u32);
344
345 let payload_bytes = payload.encode_to_vec();
346
347 let mut crc_data = Vec::with_capacity(4 + payload_bytes.len());
349 crc_data.extend_from_slice(&(payload_bytes.len() as i32).to_le_bytes());
350 crc_data.extend_from_slice(&payload_bytes);
351 let crc32 = crc32fast::hash(&crc_data);
352
353 if self.filenames_encrypted {
354 metadata.crc_encrypted = Some(crc32);
355 metadata.crc_clear = Some(0);
356 } else {
357 metadata.crc_encrypted = Some(self.encrypted_crc);
358 metadata.crc_clear = Some(crc32);
359 }
360
361 let metadata_bytes = metadata.encode_to_vec();
362
363 output.write_all(&PROTOBUF_PAYLOAD_MAGIC.to_le_bytes())?;
365 output.write_all(&(payload_bytes.len() as i32).to_le_bytes())?;
366 output.write_all(&payload_bytes)?;
367
368 output.write_all(&PROTOBUF_METADATA_MAGIC.to_le_bytes())?;
370 output.write_all(&(metadata_bytes.len() as i32).to_le_bytes())?;
371 output.write_all(&metadata_bytes)?;
372
373 output.write_all(&PROTOBUF_SIGNATURE_MAGIC.to_le_bytes())?;
375 output.write_all(&0i32.to_le_bytes())?;
376
377 output.write_all(&PROTOBUF_ENDOFMANIFEST_MAGIC.to_le_bytes())?;
379
380 Ok(())
381 }
382
383 pub fn decrypt_filenames(&mut self, encryption_key: &[u8]) -> bool {
386 if !self.filenames_encrypted {
387 return true;
388 }
389
390 if encryption_key.len() != 32 {
391 tracing::error!("Decrypt filenames used with non 32 byte key!");
392 return false;
393 }
394
395 {
396 let Some(files) = self.files.as_mut() else {
397 tracing::error!("Files was null when attempting to decrypt filenames.");
398 return false;
399 };
400
401 for file in files.iter_mut() {
402 let Some(name) = decrypt_name(&file.file_name, encryption_key) else {
403 tracing::error!("Failed to decrypt the filename.");
404 return false;
405 };
406
407 file.file_name = name;
408
409 if let Some(link_target) = file.link_target.as_ref() {
410 if !link_target.is_empty() {
411 let Some(link_name) = decrypt_name(link_target, encryption_key) else {
412 tracing::error!("Failed to decrypt the filename.");
413 return false;
414 };
415
416 file.link_target = Some(link_name);
417 }
418 }
419 }
420
421 files.sort_by(|left, right| {
423 cmp_ordinal_ignore_case(&left.file_name, &right.file_name)
424 });
425 }
426
427 self.filenames_encrypted = false;
428 true
429 }
430
431 fn internal_deserialize(reader: &mut ByteReader<'_>) -> Result<DepotManifest, SteamError> {
433 let mut payload: Option<ContentManifestPayload> = None;
434 let mut metadata: Option<ContentManifestMetadata> = None;
435 let mut signature: Option<ContentManifestSignature> = None;
436
437 loop {
438 let magic = reader.read_u32()?;
439
440 if magic == PROTOBUF_ENDOFMANIFEST_MAGIC {
441 break;
442 }
443
444 match magic {
445 Steam3Manifest::MAGIC => {
446 let mut binary_manifest = Steam3Manifest::default();
447 binary_manifest.deserialize(reader)?;
448
449 let mut manifest = DepotManifest::new();
450 manifest.parse_binary_manifest(&binary_manifest);
451
452 let marker = reader.read_u32()?;
453 if marker != magic {
454 return Err(SteamError::Malformed(
455 "Unable to find end of message marker for depot manifest".to_string(),
456 ));
457 }
458
459 return Ok(manifest);
462 }
463 PROTOBUF_PAYLOAD_MAGIC => {
464 let length = reader.read_u32()? as usize;
465 let bytes = reader.read_bytes(length);
466 payload = Some(ContentManifestPayload::decode(bytes.as_slice())?);
467 }
468 PROTOBUF_METADATA_MAGIC => {
469 let length = reader.read_u32()? as usize;
470 let bytes = reader.read_bytes(length);
471 metadata = Some(ContentManifestMetadata::decode(bytes.as_slice())?);
472 }
473 PROTOBUF_SIGNATURE_MAGIC => {
474 let length = reader.read_u32()? as usize;
475 let bytes = reader.read_bytes(length);
476 signature = Some(ContentManifestSignature::decode(bytes.as_slice())?);
477 }
478 other => {
479 return Err(SteamError::Malformed(format!(
480 "Unrecognized magic value {other:X} in depot manifest."
481 )))
482 }
483 }
484 }
485
486 match (payload, metadata, signature) {
487 (Some(payload), Some(metadata), Some(_)) => {
488 let mut manifest = DepotManifest::new();
489 manifest.parse_protobuf_manifest_metadata(&metadata);
490 manifest.parse_protobuf_manifest_payload(&payload);
491 Ok(manifest)
492 }
493 _ => Err(SteamError::Malformed(
494 "Missing ContentManifest sections required for parsing depot manifest".to_string(),
495 )),
496 }
497 }
498
499 fn parse_binary_manifest(&mut self, manifest: &Steam3Manifest) {
501 let filenames_encrypted = manifest.are_file_names_encrypted;
502 let mut files = Vec::with_capacity(manifest.mapping.len());
503
504 for file_mapping in &manifest.mapping {
505 let mut file_data = FileData::with_values(
506 file_mapping.file_name.as_deref().unwrap_or(""),
507 file_mapping.hash_file_name.clone().unwrap_or_default(),
508 file_mapping.flags,
509 file_mapping.total_size,
510 file_mapping.hash_content.clone().unwrap_or_default(),
511 "",
512 filenames_encrypted,
513 file_mapping.chunks.len(),
514 );
515
516 for chunk in &file_mapping.chunks {
517 file_data.chunks.push(ChunkData::with_values(
518 chunk.chunk_gid.clone().unwrap_or_default(),
519 chunk.checksum,
520 chunk.offset,
521 chunk.compressed_size,
522 chunk.decompressed_size,
523 ));
524 }
525
526 files.push(file_data);
527 }
528
529 self.files = Some(files);
530 self.filenames_encrypted = filenames_encrypted;
531 self.depot_id = manifest.depot_id;
532 self.manifest_gid = manifest.manifest_gid;
533 self.creation_time = manifest.creation_time;
534 self.total_uncompressed_size = manifest.total_uncompressed_size;
535 self.total_compressed_size = manifest.total_compressed_size;
536 self.encrypted_crc = manifest.encrypted_crc;
537 }
538
539 fn parse_protobuf_manifest_payload(&mut self, payload: &ContentManifestPayload) {
541 let mut files = Vec::with_capacity(payload.mappings.len());
542
543 for file_mapping in &payload.mappings {
544 let mut file_data = FileData::with_values(
545 file_mapping.filename.as_deref().unwrap_or(""),
546 file_mapping.sha_filename.clone().unwrap_or_default(),
547 file_mapping.flags.unwrap_or(0),
548 file_mapping.size.unwrap_or(0),
549 file_mapping.sha_content.clone().unwrap_or_default(),
550 file_mapping.linktarget.as_deref().unwrap_or(""),
551 self.filenames_encrypted,
552 file_mapping.chunks.len(),
553 );
554
555 for chunk in &file_mapping.chunks {
556 file_data.chunks.push(ChunkData::with_values(
557 chunk.sha.clone().unwrap_or_default(),
558 chunk.crc.unwrap_or(0),
559 chunk.offset.unwrap_or(0),
560 chunk.cb_compressed.unwrap_or(0),
561 chunk.cb_original.unwrap_or(0),
562 ));
563 }
564
565 files.push(file_data);
566 }
567
568 self.files = Some(files);
569 }
570
571 fn parse_protobuf_manifest_metadata(&mut self, metadata: &ContentManifestMetadata) {
573 self.filenames_encrypted = metadata.filenames_encrypted.unwrap_or(false);
574 self.depot_id = metadata.depot_id.unwrap_or(0);
575 self.manifest_gid = metadata.gid_manifest.unwrap_or(0);
576 self.creation_time = date_time_from_unix_time(metadata.creation_time.unwrap_or(0) as u64);
577 self.total_uncompressed_size = metadata.cb_disk_original.unwrap_or(0);
578 self.total_compressed_size = metadata.cb_disk_compressed.unwrap_or(0);
579 self.encrypted_crc = metadata.crc_encrypted.unwrap_or(0);
580 }
581}
582
583fn decrypt_name(name: &str, encryption_key: &[u8]) -> Option<String> {
585 use base64::prelude::BASE64_STANDARD;
586
587 let encoded: String = name.chars().filter(|c| !c.is_whitespace()).collect();
590 let decoded = BASE64_STANDARD.decode(encoded.as_bytes()).ok()?;
591
592 if decoded.len() < 16 {
594 return None;
595 }
596
597 let cipher = Aes256::new_from_slice(encryption_key).ok()?;
598
599 let mut iv = aes::cipher::Block::<Aes256>::clone_from_slice(&decoded[..16]);
600 <Aes256 as aes::cipher::BlockDecrypt>::decrypt_block(&cipher, &mut iv);
601
602 let decryptor = cbc::Decryptor::<Aes256>::new_from_slices(encryption_key, &iv).ok()?;
603
604 let mut buffer = decoded[16..].to_vec();
605 let plaintext_len = decryptor.decrypt_padded_mut::<Pkcs7>(&mut buffer).ok()?.len();
606
607 let mut filename_length = plaintext_len;
609 if filename_length > 0 && buffer[filename_length - 1] == 0 {
610 filename_length -= 1;
611 }
612
613 for byte in buffer.iter_mut() {
615 if *byte == ALT_DIR_CHAR as u8 {
616 *byte = DIRECTORY_SEPARATOR as u8;
617 }
618 }
619
620 Some(String::from_utf8_lossy(&buffer[..filename_length]).into_owned())
621}
622
623fn cmp_ordinal_ignore_case(left: &str, right: &str) -> Ordering {
625 let mut left_chars = left
626 .chars()
627 .map(|ch| ch.to_uppercase().next().unwrap_or(ch));
628 let mut right_chars = right
629 .chars()
630 .map(|ch| ch.to_uppercase().next().unwrap_or(ch));
631
632 loop {
633 match (left_chars.next(), right_chars.next()) {
634 (None, None) => return Ordering::Equal,
635 (None, Some(_)) => return Ordering::Less,
636 (Some(_), None) => return Ordering::Greater,
637 (Some(left_char), Some(right_char)) => match left_char.cmp(&right_char) {
638 Ordering::Equal => continue,
639 other => return other,
640 },
641 }
642 }
643}