Skip to main content

steam_vdf_parser/
lib.rs

1//! Blazing fast VDF (Valve Data Format) parser.
2//!
3//! This library provides parsers for both text and binary VDF formats used by Steam and
4//! other Valve Software products.
5//!
6//! # Features
7//!
8//! - **`no_std` compatible** — works without the standard library, requires only `alloc`
9//! - **Zero-copy parsing** for text format when possible (no escape sequences)
10//! - **Binary format support** for Steam's appinfo.vdf, shortcuts.vdf, and packageinfo.vdf
11//! - **Winnow-powered** text parser for maximum performance
12//!
13//! # Example
14//!
15//! ```
16//! use steam_vdf_parser::parse_text;
17//!
18//! let input = r#""root"
19//! {
20//!     "key" "value"
21//!     "nested"
22//!     {
23//!         "subkey" "subvalue"
24//!     }
25//! }"#;
26//!
27//! let vdf = parse_text(input).unwrap();
28//! ```
29
30#![no_std]
31#![warn(missing_docs)]
32
33extern crate alloc;
34
35use alloc::borrow::Cow;
36use alloc::string::ToString;
37
38pub mod binary;
39pub mod error;
40pub mod text;
41pub mod value;
42
43pub use error::{Error, Result};
44pub use value::{Obj, Value, Vdf};
45
46// Re-export commonly used functions
47pub use binary::{InvalidUtf8, ParseOptions};
48pub use text::parse_text;
49
50/// Parse VDF from binary format (autodetects shortcuts or appinfo format).
51///
52/// This function returns zero-copy data where possible - strings are borrowed
53/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
54///
55/// Uses default [`ParseOptions`] (invalid UTF-8 handled lossily). Use
56/// [`parse_binary_with`] to control this.
57pub fn parse_binary(input: &[u8]) -> Result<Vdf<'_>> {
58    binary::parse(input)
59}
60
61/// Parse VDF from binary format (autodetects format) with explicit [`ParseOptions`].
62pub fn parse_binary_with(input: &[u8], options: ParseOptions) -> Result<Vdf<'_>> {
63    binary::parse_with(input, options)
64}
65
66/// Parse a shortcuts.vdf format binary file.
67///
68/// This function returns zero-copy data where possible - strings are borrowed
69/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
70///
71/// Uses default [`ParseOptions`] (invalid UTF-8 handled lossily). Use
72/// [`parse_shortcuts_with`] to control this.
73pub fn parse_shortcuts(input: &[u8]) -> Result<Vdf<'_>> {
74    binary::parse_shortcuts(input)
75}
76
77/// Parse a shortcuts.vdf format binary file with explicit [`ParseOptions`].
78pub fn parse_shortcuts_with(input: &[u8], options: ParseOptions) -> Result<Vdf<'_>> {
79    binary::parse_shortcuts_with(input, options)
80}
81
82/// Parse an appinfo.vdf format binary file.
83///
84/// This function returns zero-copy data where possible - strings are borrowed
85/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
86///
87/// Uses default [`ParseOptions`] (invalid UTF-8 handled lossily). Use
88/// [`parse_appinfo_with`] to control this.
89pub fn parse_appinfo(input: &[u8]) -> Result<Vdf<'_>> {
90    binary::parse_appinfo(input)
91}
92
93/// Parse an appinfo.vdf format binary file with explicit [`ParseOptions`].
94pub fn parse_appinfo_with(input: &[u8], options: ParseOptions) -> Result<Vdf<'_>> {
95    binary::parse_appinfo_with(input, options)
96}
97
98/// Parse a packageinfo.vdf format binary file.
99///
100/// This function returns zero-copy data where possible - strings are borrowed
101/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
102///
103/// Uses default [`ParseOptions`] (invalid UTF-8 handled lossily). Use
104/// [`parse_packageinfo_with`] to control this.
105pub fn parse_packageinfo(input: &[u8]) -> Result<Vdf<'_>> {
106    binary::parse_packageinfo(input)
107}
108
109/// Parse a packageinfo.vdf format binary file with explicit [`ParseOptions`].
110pub fn parse_packageinfo_with(input: &[u8], options: ParseOptions) -> Result<Vdf<'_>> {
111    binary::parse_packageinfo_with(input, options)
112}
113
114// Convert from borrowed to owned
115impl Vdf<'_> {
116    /// Convert to an owned version (with 'static lifetime).
117    ///
118    /// This creates a new `Vdf<'static>` with all strings owned, allowing the
119    /// data to outlive the original input.
120    pub fn into_owned(self) -> Vdf<'static> {
121        let (key, value) = self.into_parts();
122        let owned_key: Cow<'static, str> = match key {
123            Cow::Borrowed(s) => Cow::Owned(s.to_string()),
124            Cow::Owned(s) => Cow::Owned(s),
125        };
126        Vdf::new(owned_key, value.into_owned())
127    }
128}
129
130impl Value<'_> {
131    /// Convert to an owned version (with 'static lifetime).
132    pub fn into_owned(self) -> Value<'static> {
133        match self {
134            Value::Str(s) => Value::Str(match s {
135                Cow::Borrowed(b) => b.to_string().into(),
136                Cow::Owned(o) => o.into(),
137            }),
138            Value::Obj(obj) => Value::Obj(obj.into_owned()),
139            Value::I32(n) => Value::I32(n),
140            Value::U64(n) => Value::U64(n),
141            Value::Float(n) => Value::Float(n),
142            Value::Pointer(n) => Value::Pointer(n),
143            Value::Color(c) => Value::Color(c),
144        }
145    }
146}
147
148impl Obj<'_> {
149    /// Convert to an owned version (with 'static lifetime).
150    pub fn into_owned(self) -> Obj<'static> {
151        let mut new = Obj::new();
152        for (k, v) in self.iter() {
153            let owned_key: Cow<'static, str> = match k {
154                Cow::Borrowed(b) => Cow::Owned(b.to_string()),
155                Cow::Owned(o) => Cow::Owned(o.clone()),
156            };
157            // Clone the value since we're iterating
158            new.insert(owned_key, v.clone().into_owned());
159        }
160        new
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use alloc::vec;
168
169    // Simple shortcuts.vdf format test data (object start, key, string value, object end)
170    const SHORTCUTS_VDF: &[u8] = &[
171        0x00, // Object start
172        b't', b'e', b's', b't', 0x00, // Key "test"
173        0x01, // String type
174        b'k', b'e', b'y', 0x00, // Nested key "key"
175        b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
176        0x08, // Object end
177    ];
178
179    #[test]
180    fn test_parse_binary() {
181        let vdf = parse_binary(SHORTCUTS_VDF).unwrap();
182        assert!(vdf.as_obj().is_some());
183        assert_eq!(vdf.key(), "root");
184        let obj = vdf.as_obj().unwrap();
185        let test_obj = obj.get("test").and_then(|v| v.as_obj()).unwrap();
186        assert_eq!(test_obj.get("key").and_then(|v| v.as_str()), Some("value"));
187    }
188
189    #[test]
190    fn test_parse_shortcuts() {
191        let vdf = parse_shortcuts(SHORTCUTS_VDF).unwrap();
192        assert!(vdf.as_obj().is_some());
193        assert_eq!(vdf.key(), "root");
194        let obj = vdf.as_obj().unwrap();
195        let test_obj = obj.get("test").and_then(|v| v.as_obj()).unwrap();
196        assert_eq!(test_obj.get("key").and_then(|v| v.as_str()), Some("value"));
197    }
198
199    #[test]
200    fn test_parse_appinfo() {
201        // appinfo v40 magic + terminator (no apps)
202        // Need 8 bytes (magic + universe) + 68 bytes (APPINFO_ENTRY_HEADER_SIZE) = 76 bytes total
203        let mut input = vec![
204            0x28, 0x44, 0x56, 0x07, // magic: 0x07564428 (APPINFO_MAGIC_40)
205            0x20, 0x00, 0x00, 0x00, // universe: 32
206            0x00, 0x00, 0x00, 0x00, // app_id = 0 (terminator)
207        ];
208        // Pad to 76 bytes total (8 + APPINFO_ENTRY_HEADER_SIZE)
209        input.resize(76, 0);
210        let result = parse_appinfo(&input);
211        if let Err(e) = &result {
212            panic!("parse_appinfo failed: {:?}", e);
213        }
214        assert!(result.is_ok());
215        let vdf = result.unwrap();
216        assert!(vdf.key().starts_with("appinfo_universe_"));
217    }
218
219    #[test]
220    fn test_into_owned_vdf() {
221        let input = r#""root"
222        {
223            "key" "value"
224        }"#;
225        let borrowed = parse_text(input).unwrap();
226        let owned = borrowed.into_owned();
227        assert_eq!(owned.key(), "root");
228    }
229
230    #[test]
231    fn test_into_owned_value_str() {
232        let borrowed = Value::Str("test".into());
233        let owned = borrowed.into_owned();
234        assert!(matches!(owned, Value::Str(Cow::Owned(_))));
235    }
236
237    #[test]
238    fn test_into_owned_value_obj() {
239        let mut obj = Obj::new();
240        obj.insert("key", Value::Str("value".into()));
241        let borrowed = Value::Obj(obj);
242        let owned = borrowed.into_owned();
243        assert!(matches!(owned, Value::Obj(_)));
244    }
245
246    #[test]
247    fn test_into_owned_obj() {
248        let mut obj = Obj::new();
249        obj.insert("key", Value::Str("value".into()));
250        let owned = obj.into_owned();
251        assert!(owned.get("key").is_some());
252    }
253}