Skip to main content

steamkit/util/
v_zstd_util.rs

1//! Valve's "VZstd" container, used for zstd-compressed depot chunks.
2
3use crate::error::SteamError;
4
5/// The magic that starts a VZstd stream.
6const VZSTD_HEADER: u32 = 0x615A_5356;
7
8/// Magic, CRC.
9const HEADER_LENGTH: usize = 8;
10
11/// CRC, decompressed size, unknown, magic.
12const FOOTER_LENGTH: usize = 15;
13
14/// The VZstd container format.
15#[derive(Debug)]
16pub struct VZstdUtil;
17
18impl VZstdUtil {
19    /// Decompresses a VZstd buffer into `destination` and returns how many
20    /// bytes were written.
21    pub fn decompress(
22        buffer: &[u8],
23        destination: &mut [u8],
24        verify_checksum: bool,
25    ) -> Result<i32, SteamError> {
26        if buffer.len() < HEADER_LENGTH + FOOTER_LENGTH {
27            return Err(SteamError::Decompression(format!(
28                "VZstd data is too short (was {} bytes).",
29                buffer.len()
30            )));
31        }
32
33        // Native byte order, like the reference implementation.
34        if u32::from_ne_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) != VZSTD_HEADER {
35            return Err(SteamError::Decompression(
36                "Expecting VZstdHeader at start of stream".to_string(),
37            ));
38        }
39
40        let crc32 = i32::from_ne_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]);
41        let footer = buffer.len() - FOOTER_LENGTH;
42        let crc32_footer =
43            i32::from_ne_bytes([buffer[footer], buffer[footer + 1], buffer[footer + 2], buffer[footer + 3]]);
44        let size_decompressed = i32::from_ne_bytes([
45            buffer[footer + 4],
46            buffer[footer + 5],
47            buffer[footer + 6],
48            buffer[footer + 7],
49        ]);
50
51        // The CRC is stored twice, in the header and in the footer.
52        debug_assert_eq!(crc32, crc32_footer);
53
54        if buffer[buffer.len() - 3] != b'z'
55            || buffer[buffer.len() - 2] != b's'
56            || buffer[buffer.len() - 1] != b'v'
57        {
58            return Err(SteamError::Decompression(
59                "Expecting VZstdFooter at end of stream".to_string(),
60            ));
61        }
62
63        if destination.len() < size_decompressed as usize {
64            return Err(SteamError::InvalidArgument(
65                "The destination buffer is smaller than the decompressed data size.".to_string(),
66            ));
67        }
68
69        let input = &buffer[HEADER_LENGTH..footer];
70
71        let decompressed = zstd::stream::decode_all(input)
72            .map_err(|_| SteamError::Decompression("Failed to decompress Zstd.".to_string()))?;
73
74        if decompressed.len() != size_decompressed as usize {
75            return Err(SteamError::Decompression(format!(
76                "Failed to decompress Zstd (expected {size_decompressed} bytes, got {}).",
77                decompressed.len()
78            )));
79        }
80
81        destination[..decompressed.len()].copy_from_slice(&decompressed);
82
83        if verify_checksum
84            && crc32fast::hash(&destination[..decompressed.len()]) != crc32_footer as u32
85        {
86            return Err(SteamError::Decompression(
87                "CRC does not match decompressed data. VZstd data may be corrupted.".to_string(),
88            ));
89        }
90
91        Ok(size_decompressed)
92    }
93}