Skip to main content

steamkit/util/
v_zip_util.rs

1//! Valve's "VZip" container, used for Steam client updates and depot chunks.
2
3use crate::error::SteamError;
4use crate::util::lzma::compress::lzma::lzma_encoder::Encoder;
5use crate::util::lzma::i_coder::{
6    CoderPropID, CoderProperty, ICoder, ISetCoderProperties, IWriteCoderProperties,
7};
8use crate::util::lzma_decoder::LzmaDecoder;
9
10/// The magic that starts a VZip stream.
11const VZIP_HEADER: u16 = 0x5A56;
12
13/// The magic that ends a VZip stream.
14const VZIP_FOOTER: u16 = 0x767A;
15
16/// Magic, version, timestamp/CRC.
17const HEADER_LENGTH: usize = 7;
18
19/// CRC, decompressed size, magic.
20const FOOTER_LENGTH: usize = 10;
21
22/// The version byte of the VZip files we write.
23const VERSION: u8 = b'a';
24
25/// The parts of a VZip buffer: the LZMA properties and stream, and the CRC and
26/// size of the decompressed data from the footer.
27#[derive(Debug)]
28pub struct Frame<'a> {
29    /// The LZMA properties of the stream.
30    pub properties: &'a [u8],
31    /// The compressed stream.
32    pub compressed: &'a [u8],
33    /// The CRC of the decompressed data.
34    pub output_crc: u32,
35    /// The size of the decompressed data.
36    pub decompressed_size: i32,
37}
38
39/// The VZip container format.
40#[derive(Debug)]
41pub struct VZipUtil;
42
43impl VZipUtil {
44    /// Splits a VZip buffer into its parts. The four bytes after the header are
45    /// a timestamp in some files (Steam client VZips) and a CRC32 in others
46    /// (depot chunks), so they're not interpreted.
47    pub fn read_frame(buffer: &[u8], version: u8) -> Result<Frame<'_>, SteamError> {
48        if buffer.len() < HEADER_LENGTH + LzmaDecoder::PROPERTIES_SIZE + FOOTER_LENGTH {
49            return Err(SteamError::Decompression(format!(
50                "VZip data is too short (was {} bytes).",
51                buffer.len()
52            )));
53        }
54
55        let header = u16::from_le_bytes([buffer[0], buffer[1]]);
56
57        if header != VZIP_HEADER {
58            return Err(SteamError::Decompression(
59                "Expecting VZipHeader at start of stream".to_string(),
60            ));
61        }
62
63        if buffer[2] != version {
64            return Err(SteamError::Decompression(format!(
65                "Expecting VZip version '{}'",
66                version as char
67            )));
68        }
69
70        let footer = &buffer[buffer.len() - FOOTER_LENGTH..];
71        let output_crc = u32::from_le_bytes([footer[0], footer[1], footer[2], footer[3]]);
72        let size_decompressed = i32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]]);
73
74        let footer_magic = u16::from_le_bytes([footer[8], footer[9]]);
75
76        if footer_magic != VZIP_FOOTER {
77            return Err(SteamError::Decompression(
78                "Expecting VZipFooter at end of stream".to_string(),
79            ));
80        }
81
82        if size_decompressed < 0 {
83            return Err(SteamError::Decompression(format!(
84                "VZip decompressed size is negative (was {size_decompressed})."
85            )));
86        }
87
88        let properties_start = HEADER_LENGTH;
89        let compressed_start = HEADER_LENGTH + LzmaDecoder::PROPERTIES_SIZE;
90        let compressed_end = buffer.len() - FOOTER_LENGTH;
91
92        Ok(Frame {
93            properties: &buffer[properties_start..compressed_start],
94            compressed: &buffer[compressed_start..compressed_end],
95            output_crc,
96            decompressed_size: size_decompressed,
97        })
98    }
99
100    /// Decompresses a VZip buffer into `destination` and returns how many bytes
101    /// were written.
102    pub fn decompress(
103        buffer: &[u8],
104        destination: &mut [u8],
105        verify_checksum: bool,
106    ) -> Result<i32, SteamError> {
107        let frame = VZipUtil::read_frame(buffer, VERSION)?;
108
109        if destination.len() < frame.decompressed_size as usize {
110            return Err(SteamError::InvalidArgument(
111                "The destination buffer is smaller than the decompressed data size.".to_string(),
112            ));
113        }
114
115        let output = &mut destination[..frame.decompressed_size as usize];
116
117        LzmaDecoder::decode(frame.properties, frame.compressed, output, 0)?;
118
119        if verify_checksum && crc32fast::hash(output) != frame.output_crc {
120            return Err(SteamError::Decompression(
121                "CRC does not match decompressed data. VZip data may be corrupted.".to_string(),
122            ));
123        }
124
125        Ok(frame.decompressed_size)
126    }
127
128    /// Compresses `buffer` into a VZip buffer.
129    pub fn compress(buffer: &[u8]) -> Result<Vec<u8>, SteamError> {
130        let crc = crc32fast::hash(buffer);
131        let mut out: Vec<u8> = Vec::with_capacity(buffer.len() + 64);
132
133        out.extend_from_slice(&VZIP_HEADER.to_le_bytes());
134        out.push(VERSION);
135        out.extend_from_slice(&crc.to_le_bytes());
136
137        let mut encoder = Encoder::new();
138
139        encoder
140            .set_coder_properties(
141                &[
142                    CoderPropID::DictionarySize,
143                    CoderPropID::PosStateBits,
144                    CoderPropID::LitContextBits,
145                    CoderPropID::LitPosBits,
146                    CoderPropID::Algorithm,
147                    CoderPropID::NumFastBytes,
148                    CoderPropID::MatchFinder,
149                    CoderPropID::EndMarker,
150                ],
151                &[
152                    CoderProperty::Int32(1 << 23),
153                    CoderProperty::Int32(2),
154                    CoderProperty::Int32(3),
155                    CoderProperty::Int32(0),
156                    CoderProperty::Int32(2),
157                    CoderProperty::Int32(128),
158                    CoderProperty::String("bt4".to_string()),
159                    CoderProperty::Boolean(false),
160                ],
161            )?;
162
163        encoder.write_coder_properties(&mut out);
164        encoder.code(buffer.to_vec(), &mut out, -1, -1, None)?;
165
166        out.extend_from_slice(&crc.to_le_bytes());
167        out.extend_from_slice(&(buffer.len() as u32).to_le_bytes());
168        out.extend_from_slice(&VZIP_FOOTER.to_le_bytes());
169
170        Ok(out)
171    }
172}