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 text::parse_text;
48
49/// Parse VDF from binary format (autodetects shortcuts or appinfo format).
50///
51/// This function returns zero-copy data where possible - strings are borrowed
52/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
53pub fn parse_binary(input: &[u8]) -> Result<Vdf<'_>> {
54    binary::parse(input)
55}
56
57/// Parse a shortcuts.vdf format binary file.
58///
59/// This function returns zero-copy data where possible - strings are borrowed
60/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
61pub fn parse_shortcuts(input: &[u8]) -> Result<Vdf<'_>> {
62    binary::parse_shortcuts(input)
63}
64
65/// Parse an appinfo.vdf format binary file.
66///
67/// This function returns zero-copy data where possible - strings are borrowed
68/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
69pub fn parse_appinfo(input: &[u8]) -> Result<Vdf<'_>> {
70    binary::parse_appinfo(input)
71}
72
73/// Parse a packageinfo.vdf format binary file.
74///
75/// This function returns zero-copy data where possible - strings are borrowed
76/// from the input buffer. Use `.into_owned()` to convert to an owned `Vdf<'static>`.
77pub fn parse_packageinfo(input: &[u8]) -> Result<Vdf<'_>> {
78    binary::parse_packageinfo(input)
79}
80
81// Convert from borrowed to owned
82impl Vdf<'_> {
83    /// Convert to an owned version (with 'static lifetime).
84    ///
85    /// This creates a new `Vdf<'static>` with all strings owned, allowing the
86    /// data to outlive the original input.
87    pub fn into_owned(self) -> Vdf<'static> {
88        let (key, value) = self.into_parts();
89        let owned_key: Cow<'static, str> = match key {
90            Cow::Borrowed(s) => Cow::Owned(s.to_string()),
91            Cow::Owned(s) => Cow::Owned(s),
92        };
93        Vdf::new(owned_key, value.into_owned())
94    }
95}
96
97impl Value<'_> {
98    /// Convert to an owned version (with 'static lifetime).
99    pub fn into_owned(self) -> Value<'static> {
100        match self {
101            Value::Str(s) => Value::Str(match s {
102                Cow::Borrowed(b) => b.to_string().into(),
103                Cow::Owned(o) => o.into(),
104            }),
105            Value::Obj(obj) => Value::Obj(obj.into_owned()),
106            Value::I32(n) => Value::I32(n),
107            Value::U64(n) => Value::U64(n),
108            Value::Float(n) => Value::Float(n),
109            Value::Pointer(n) => Value::Pointer(n),
110            Value::Color(c) => Value::Color(c),
111        }
112    }
113}
114
115impl Obj<'_> {
116    /// Convert to an owned version (with 'static lifetime).
117    pub fn into_owned(self) -> Obj<'static> {
118        let mut new = Obj::new();
119        for (k, v) in self.iter() {
120            let owned_key: Cow<'static, str> = match k {
121                Cow::Borrowed(b) => Cow::Owned(b.to_string()),
122                Cow::Owned(o) => Cow::Owned(o.clone()),
123            };
124            // Clone the value since we're iterating
125            new.insert(owned_key, v.clone().into_owned());
126        }
127        new
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use alloc::vec;
135
136    // Simple shortcuts.vdf format test data (object start, key, string value, object end)
137    const SHORTCUTS_VDF: &[u8] = &[
138        0x00, // Object start
139        b't', b'e', b's', b't', 0x00, // Key "test"
140        0x01, // String type
141        b'k', b'e', b'y', 0x00, // Nested key "key"
142        b'v', b'a', b'l', b'u', b'e', 0x00, // Value "value"
143        0x08, // Object end
144    ];
145
146    #[test]
147    fn test_parse_binary() {
148        let vdf = parse_binary(SHORTCUTS_VDF).unwrap();
149        assert!(vdf.as_obj().is_some());
150        assert_eq!(vdf.key(), "root");
151        let obj = vdf.as_obj().unwrap();
152        let test_obj = obj.get("test").and_then(|v| v.as_obj()).unwrap();
153        assert_eq!(test_obj.get("key").and_then(|v| v.as_str()), Some("value"));
154    }
155
156    #[test]
157    fn test_parse_shortcuts() {
158        let vdf = parse_shortcuts(SHORTCUTS_VDF).unwrap();
159        assert!(vdf.as_obj().is_some());
160        assert_eq!(vdf.key(), "root");
161        let obj = vdf.as_obj().unwrap();
162        let test_obj = obj.get("test").and_then(|v| v.as_obj()).unwrap();
163        assert_eq!(test_obj.get("key").and_then(|v| v.as_str()), Some("value"));
164    }
165
166    #[test]
167    fn test_parse_appinfo() {
168        // appinfo v40 magic + terminator (no apps)
169        // Need 8 bytes (magic + universe) + 68 bytes (APPINFO_ENTRY_HEADER_SIZE) = 76 bytes total
170        let mut input = vec![
171            0x28, 0x44, 0x56, 0x07, // magic: 0x07564428 (APPINFO_MAGIC_40)
172            0x20, 0x00, 0x00, 0x00, // universe: 32
173            0x00, 0x00, 0x00, 0x00, // app_id = 0 (terminator)
174        ];
175        // Pad to 76 bytes total (8 + APPINFO_ENTRY_HEADER_SIZE)
176        input.resize(76, 0);
177        let result = parse_appinfo(&input);
178        if let Err(e) = &result {
179            panic!("parse_appinfo failed: {:?}", e);
180        }
181        assert!(result.is_ok());
182        let vdf = result.unwrap();
183        assert!(vdf.key().starts_with("appinfo_universe_"));
184    }
185
186    #[test]
187    fn test_into_owned_vdf() {
188        let input = r#""root"
189        {
190            "key" "value"
191        }"#;
192        let borrowed = parse_text(input).unwrap();
193        let owned = borrowed.into_owned();
194        assert_eq!(owned.key(), "root");
195    }
196
197    #[test]
198    fn test_into_owned_value_str() {
199        let borrowed = Value::Str("test".into());
200        let owned = borrowed.into_owned();
201        assert!(matches!(owned, Value::Str(Cow::Owned(_))));
202    }
203
204    #[test]
205    fn test_into_owned_value_obj() {
206        let mut obj = Obj::new();
207        obj.insert("key", Value::Str("value".into()));
208        let borrowed = Value::Obj(obj);
209        let owned = borrowed.into_owned();
210        assert!(matches!(owned, Value::Obj(_)));
211    }
212
213    #[test]
214    fn test_into_owned_obj() {
215        let mut obj = Obj::new();
216        obj.insert("key", Value::Str("value".into()));
217        let owned = obj.into_owned();
218        assert!(owned.get("key").is_some());
219    }
220}