Skip to main content

steam_client/utils/
binary_kv.rs

1//! Binary KeyValue parser.
2//!
3//! Parses binary KV data used by Steam for package info.
4//! This format is different from the text-based VDF format.
5
6use std::{
7    collections::HashMap,
8    fmt,
9    io::{self, BufRead, Cursor},
10};
11
12use byteorder::{LittleEndian, ReadBytesExt};
13
14/// Error during binary KV parsing.
15#[derive(Debug)]
16pub enum BinaryKvError {
17    /// IO error.
18    Io(io::Error),
19    /// Invalid type byte.
20    InvalidType(u8),
21    /// Invalid string encoding.
22    InvalidString,
23    /// Unexpected end of data.
24    UnexpectedEof,
25}
26
27impl fmt::Display for BinaryKvError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            BinaryKvError::Io(e) => write!(f, "IO error: {}", e),
31            BinaryKvError::InvalidType(t) => write!(f, "Invalid type byte: {}", t),
32            BinaryKvError::InvalidString => write!(f, "Invalid string encoding"),
33            BinaryKvError::UnexpectedEof => write!(f, "Unexpected end of data"),
34        }
35    }
36}
37
38impl std::error::Error for BinaryKvError {}
39
40impl From<io::Error> for BinaryKvError {
41    fn from(e: io::Error) -> Self {
42        BinaryKvError::Io(e)
43    }
44}
45
46/// Binary KV type markers.
47#[repr(u8)]
48#[derive(Debug, Clone, Copy, PartialEq)]
49enum BinaryKvType {
50    None = 0,
51    String = 1,
52    Int32 = 2,
53    Float32 = 3,
54    Pointer = 4,
55    WideString = 5,
56    Color = 6,
57    UInt64 = 7,
58    End = 8,
59    Int64 = 10,
60    AlternateEnd = 11,
61}
62
63impl TryFrom<u8> for BinaryKvType {
64    type Error = BinaryKvError;
65
66    fn try_from(value: u8) -> Result<Self, Self::Error> {
67        match value {
68            0 => Ok(BinaryKvType::None),
69            1 => Ok(BinaryKvType::String),
70            2 => Ok(BinaryKvType::Int32),
71            3 => Ok(BinaryKvType::Float32),
72            4 => Ok(BinaryKvType::Pointer),
73            5 => Ok(BinaryKvType::WideString),
74            6 => Ok(BinaryKvType::Color),
75            7 => Ok(BinaryKvType::UInt64),
76            8 => Ok(BinaryKvType::End),
77            10 => Ok(BinaryKvType::Int64),
78            11 => Ok(BinaryKvType::AlternateEnd),
79            _ => Err(BinaryKvError::InvalidType(value)),
80        }
81    }
82}
83
84/// A binary KV value.
85#[derive(Debug, Clone, PartialEq)]
86pub enum BinaryKvValue {
87    /// No value / null.
88    None,
89    /// String value.
90    String(String),
91    /// 32-bit integer.
92    Int32(i32),
93    /// 32-bit float.
94    Float32(f32),
95    /// Pointer (32-bit).
96    Pointer(u32),
97    /// Wide string (UTF-16).
98    WideString(String),
99    /// Color (RGBA).
100    Color(u32),
101    /// 64-bit unsigned integer.
102    UInt64(u64),
103    /// 64-bit signed integer.
104    Int64(i64),
105    /// Nested object.
106    Object(HashMap<String, BinaryKvValue>),
107}
108
109impl BinaryKvValue {
110    /// Get as string if this is a string value.
111    pub fn as_str(&self) -> Option<&str> {
112        match self {
113            BinaryKvValue::String(s) | BinaryKvValue::WideString(s) => Some(s),
114            _ => None,
115        }
116    }
117
118    /// Get as i32 if this is an Int32 value.
119    pub fn as_i32(&self) -> Option<i32> {
120        match self {
121            BinaryKvValue::Int32(v) => Some(*v),
122            _ => None,
123        }
124    }
125
126    /// Get as u64 if this is a UInt64 value.
127    pub fn as_u64(&self) -> Option<u64> {
128        match self {
129            BinaryKvValue::UInt64(v) => Some(*v),
130            BinaryKvValue::Int64(v) => Some(*v as u64),
131            BinaryKvValue::Int32(v) => Some(*v as u64),
132            _ => None,
133        }
134    }
135
136    /// Get as object if this is an object value.
137    pub fn as_object(&self) -> Option<&HashMap<String, BinaryKvValue>> {
138        match self {
139            BinaryKvValue::Object(obj) => Some(obj),
140            _ => None,
141        }
142    }
143
144    /// Get a nested value by key.
145    pub fn get(&self, key: &str) -> Option<&BinaryKvValue> {
146        self.as_object().and_then(|obj| obj.get(key))
147    }
148
149    /// Get a nested string value by key.
150    pub fn get_str(&self, key: &str) -> Option<&str> {
151        self.get(key).and_then(|v| v.as_str())
152    }
153
154    /// Get a nested i32 value by key.
155    pub fn get_i32(&self, key: &str) -> Option<i32> {
156        self.get(key).and_then(|v| v.as_i32())
157    }
158}
159
160/// Binary KV parser.
161struct BinaryKvParser<R> {
162    reader: R,
163}
164
165impl<R: BufRead> BinaryKvParser<R> {
166    fn new(reader: R) -> Self {
167        Self { reader }
168    }
169
170    fn read_cstring(&mut self) -> Result<String, BinaryKvError> {
171        let mut bytes = Vec::new();
172        self.reader.read_until(0, &mut bytes)?;
173        if bytes.last() == Some(&0) {
174            bytes.pop();
175        }
176        String::from_utf8(bytes).map_err(|_| BinaryKvError::InvalidString)
177    }
178
179    fn read_value(&mut self, value_type: BinaryKvType) -> Result<BinaryKvValue, BinaryKvError> {
180        match value_type {
181            BinaryKvType::None => {
182                // None type is followed by an object
183                self.read_object()
184            }
185            BinaryKvType::String => {
186                let s = self.read_cstring()?;
187                Ok(BinaryKvValue::String(s))
188            }
189            BinaryKvType::Int32 => {
190                let v = self.reader.read_i32::<LittleEndian>()?;
191                Ok(BinaryKvValue::Int32(v))
192            }
193            BinaryKvType::Float32 => {
194                let v = self.reader.read_f32::<LittleEndian>()?;
195                Ok(BinaryKvValue::Float32(v))
196            }
197            BinaryKvType::Pointer => {
198                let v = self.reader.read_u32::<LittleEndian>()?;
199                Ok(BinaryKvValue::Pointer(v))
200            }
201            BinaryKvType::WideString => {
202                // Read UTF-16 LE string
203                let mut chars = Vec::new();
204                loop {
205                    let word = self.reader.read_u16::<LittleEndian>()?;
206                    if word == 0 {
207                        break;
208                    }
209                    chars.push(word);
210                }
211                let s = String::from_utf16(&chars).map_err(|_| BinaryKvError::InvalidString)?;
212                Ok(BinaryKvValue::WideString(s))
213            }
214            BinaryKvType::Color => {
215                let v = self.reader.read_u32::<LittleEndian>()?;
216                Ok(BinaryKvValue::Color(v))
217            }
218            BinaryKvType::UInt64 => {
219                let v = self.reader.read_u64::<LittleEndian>()?;
220                Ok(BinaryKvValue::UInt64(v))
221            }
222            BinaryKvType::Int64 => {
223                let v = self.reader.read_i64::<LittleEndian>()?;
224                Ok(BinaryKvValue::Int64(v))
225            }
226            BinaryKvType::End | BinaryKvType::AlternateEnd => {
227                // Should not be called with End type
228                Ok(BinaryKvValue::None)
229            }
230        }
231    }
232
233    fn read_object(&mut self) -> Result<BinaryKvValue, BinaryKvError> {
234        self.read_object_internal(true)
235    }
236
237    fn read_object_internal(&mut self, is_root: bool) -> Result<BinaryKvValue, BinaryKvError> {
238        let mut map = HashMap::new();
239        let mut is_first = true;
240
241        loop {
242            let type_byte = self.reader.read_u8()?;
243            let value_type = BinaryKvType::try_from(type_byte)?;
244
245            if value_type == BinaryKvType::End || value_type == BinaryKvType::AlternateEnd {
246                break;
247            }
248
249            let mut key = self.read_cstring()?;
250
251            // Root node special case: if type is None, key is empty, and this is the first
252            // entry, read another cstring as the actual key (matches JS
253            // behavior)
254            if value_type == BinaryKvType::None && key.is_empty() && is_root && is_first {
255                key = self.read_cstring()?;
256            }
257
258            is_first = false;
259
260            let value = self.read_value_internal(value_type)?;
261
262            // Drop empty keys (matches JS behavior)
263            if !key.is_empty() {
264                map.insert(key, value);
265            }
266        }
267
268        Ok(BinaryKvValue::Object(map))
269    }
270
271    fn read_value_internal(&mut self, value_type: BinaryKvType) -> Result<BinaryKvValue, BinaryKvError> {
272        match value_type {
273            BinaryKvType::None => {
274                // None type is followed by a nested object
275                self.read_object_internal(false)
276            }
277            _ => self.read_value(value_type),
278        }
279    }
280
281    fn parse(&mut self) -> Result<BinaryKvValue, BinaryKvError> {
282        // Read the root object
283        self.read_object()
284    }
285}
286
287/// Parse binary KV data into a BinaryKvValue.
288///
289/// # Example
290/// ```rust,ignore
291/// use steam_client::binary_kv::parse_binary_kv;
292///
293/// let data: &[u8] = &[/* binary kv data */];
294/// let value = parse_binary_kv(data).unwrap();
295/// ```
296pub fn parse_binary_kv(data: &[u8]) -> Result<BinaryKvValue, BinaryKvError> {
297    let cursor = Cursor::new(data);
298    let mut parser = BinaryKvParser::new(cursor);
299    parser.parse()
300}
301
302/// Calculate the byte length of binary KV data starting at the given position.
303/// This is useful for parsing multiple KV structures from a stream.
304pub fn get_binary_kv_length(data: &[u8]) -> Result<usize, BinaryKvError> {
305    let mut pos = 0;
306
307    fn skip_object(data: &[u8], pos: &mut usize) -> Result<(), BinaryKvError> {
308        loop {
309            if *pos >= data.len() {
310                return Err(BinaryKvError::UnexpectedEof);
311            }
312
313            let type_byte = data[*pos];
314            *pos += 1;
315
316            if type_byte == 8 || type_byte == 11 {
317                // End or AlternateEnd
318                return Ok(());
319            }
320
321            let value_type = BinaryKvType::try_from(type_byte)?;
322
323            // Skip key string
324            while *pos < data.len() && data[*pos] != 0 {
325                *pos += 1;
326            }
327            if *pos >= data.len() {
328                return Err(BinaryKvError::UnexpectedEof);
329            }
330            *pos += 1; // Skip null terminator
331
332            // Skip value
333            match value_type {
334                BinaryKvType::None => skip_object(data, pos)?,
335                BinaryKvType::String => {
336                    while *pos < data.len() && data[*pos] != 0 {
337                        *pos += 1;
338                    }
339                    if *pos >= data.len() {
340                        return Err(BinaryKvError::UnexpectedEof);
341                    }
342                    *pos += 1;
343                }
344                BinaryKvType::Int32 | BinaryKvType::Float32 | BinaryKvType::Pointer | BinaryKvType::Color => {
345                    if *pos + 4 > data.len() {
346                        return Err(BinaryKvError::UnexpectedEof);
347                    }
348                    *pos += 4;
349                }
350                BinaryKvType::UInt64 | BinaryKvType::Int64 => {
351                    if *pos + 8 > data.len() {
352                        return Err(BinaryKvError::UnexpectedEof);
353                    }
354                    *pos += 8;
355                }
356                BinaryKvType::WideString => loop {
357                    if *pos + 1 >= data.len() {
358                        return Err(BinaryKvError::UnexpectedEof);
359                    }
360                    let word = u16::from_le_bytes([data[*pos], data[*pos + 1]]);
361                    *pos += 2;
362                    if word == 0 {
363                        break;
364                    }
365                },
366                BinaryKvType::End | BinaryKvType::AlternateEnd => return Ok(()),
367            }
368        }
369    }
370
371    skip_object(data, &mut pos)?;
372    Ok(pos)
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_simple_object() {
381        // Build a simple binary KV: { "key" : "value" }
382        let mut data = Vec::new();
383        data.push(1); // String type
384        data.extend_from_slice(b"key\0"); // Key
385        data.extend_from_slice(b"value\0"); // Value
386        data.push(8); // End
387
388        let result = parse_binary_kv(&data).unwrap();
389        assert_eq!(result.get_str("key"), Some("value"));
390    }
391
392    #[test]
393    fn test_int32_value() {
394        // Build: { "count" : 42 }
395        let mut data = Vec::new();
396        data.push(2); // Int32 type
397        data.extend_from_slice(b"count\0"); // Key
398        data.extend_from_slice(&42i32.to_le_bytes()); // Value
399        data.push(8); // End
400
401        let result = parse_binary_kv(&data).unwrap();
402        assert_eq!(result.get_i32("count"), Some(42));
403    }
404
405    #[test]
406    fn test_nested_object() {
407        // Build: { "parent" : { "child" : "value" } }
408        let mut data = Vec::new();
409        data.push(0); // None/Object type
410        data.extend_from_slice(b"parent\0"); // Key
411                                             // Nested object
412        data.push(1); // String type
413        data.extend_from_slice(b"child\0"); // Key
414        data.extend_from_slice(b"value\0"); // Value
415        data.push(8); // End of nested
416        data.push(8); // End of root
417
418        let result = parse_binary_kv(&data).unwrap();
419        let parent = result.get("parent").unwrap();
420        assert_eq!(parent.get_str("child"), Some("value"));
421    }
422}