table statement handling

This commit is contained in:
Robin Appelman 2023-12-15 18:07:47 +01:00
commit 28469fde0f
7 changed files with 379 additions and 24 deletions

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# vdf-reader
A parser for Valve's Data Format v1 (VDF) also known as [KeyValues](https://developer.valvesoftware.com/wiki/KeyValues).
The parser focuses on being able to deal with all the various weird forms vdf takes in the wild and providing access to the data stream instead of always requiring parsing the file in full.

View file

@ -16,6 +16,15 @@ pub enum Entry {
Value(Value), Value(Value),
} }
impl From<Item<'_>> for Entry {
fn from(item: Item) -> Self {
match item {
Item::Item { content, .. } => Entry::Value(content.into()),
Item::Statement { content, .. } => Entry::Statement(content.into()),
}
}
}
impl Entry { impl Entry {
/// Lookup an entry with a path. /// Lookup an entry with a path.
pub fn lookup<S: AsRef<str>>(&self, path: S) -> Option<&Entry> { pub fn lookup<S: AsRef<str>>(&self, path: S) -> Option<&Entry> {
@ -152,4 +161,5 @@ mod statement;
pub use statement::Statement; pub use statement::Statement;
mod value; mod value;
use crate::Item;
pub use value::Value; pub use value::Value;

View file

@ -1,4 +1,5 @@
use super::{Array, Entry, Statement, Value}; use super::{Array, Entry};
use crate::entry::{Statement, Value};
use crate::{Event, Item, Reader, Result}; use crate::{Event, Item, Reader, Result};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use std::collections::HashMap; use std::collections::HashMap;
@ -20,7 +21,9 @@ where
ordered.serialize(serializer) ordered.serialize(serializer)
} }
fn insert(map: &mut HashMap<String, Entry>, key: String, value: Entry) { fn insert<K: Into<String>, V: Into<Entry>>(map: &mut HashMap<String, Entry>, key: K, value: V) {
let key = key.into();
let value = value.into();
if !map.contains_key(&key) { if !map.contains_key(&key) {
map.insert(key, value); map.insert(key, value);
return; return;
@ -44,21 +47,19 @@ impl Table {
while let Some(event) = reader.event() { while let Some(event) = reader.event() {
match event? { match event? {
Event::Entry {
key: Item::Item { content: key, .. },
value,
..
} => insert(&mut map, key, Value::from(value.into_content())),
Event::Entry { Event::Entry {
key: Item::Statement { content: key, .. }, key: Item::Statement { content: key, .. },
value, value,
.. ..
} => insert(&mut map, key.into(), Statement::from(value.content).into()), } => insert(&mut map, key, Statement::from(value.into_content())),
Event::Entry { Event::GroupStart { name, .. } => insert(&mut map, name, Table::load(reader)?),
key: Item::Key { content: key, .. },
value,
..
} => insert(&mut map, key.into(), Value::from(value.content).into()),
Event::GroupStart { name, .. } => {
insert(&mut map, name.into(), Table::load(reader)?.into())
}
Event::GroupEnd { .. } => break, Event::GroupEnd { .. } => break,
} }

View file

@ -10,31 +10,25 @@ pub enum Item<'a> {
Statement { content: Cow<'a, str>, span: Span }, Statement { content: Cow<'a, str>, span: Span },
/// A value. /// A value.
Key { content: Cow<'a, str>, span: Span }, Item { content: Cow<'a, str>, span: Span },
} }
impl<'a> Item<'a> { impl<'a> Item<'a> {
pub fn span(&self) -> Span { pub fn span(&self) -> Span {
match self { match self {
Item::Statement { span, .. } => span.clone(), Item::Statement { span, .. } => span.clone(),
Item::Key { span, .. } => span.clone(), Item::Item { span, .. } => span.clone(),
} }
} }
pub fn into_content(self) -> Cow<'a, str> { pub fn into_content(self) -> Cow<'a, str> {
match self { match self {
Item::Statement { content, .. } => content, Item::Statement { content, .. } => content,
Item::Key { content, .. } => content, Item::Item { content, .. } => content,
} }
} }
} }
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Value<'a> {
pub content: Cow<'a, str>,
pub span: Span,
}
/// Reader event. /// Reader event.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Event<'a> { pub enum Event<'a> {
@ -47,7 +41,7 @@ pub enum Event<'a> {
/// An entry. /// An entry.
Entry { Entry {
key: Item<'a>, key: Item<'a>,
value: Value<'a>, value: Item<'a>,
span: Span, span: Span,
}, },
} }
@ -134,12 +128,12 @@ impl<'a> Reader<'a> {
} }
Some((Ok(Token::GroupEnd), span)) => return Some(Ok(Event::GroupEnd { span })), Some((Ok(Token::GroupEnd), span)) => return Some(Ok(Event::GroupEnd { span })),
Some((Ok(Token::Item), span)) => Item::Key { Some((Ok(Token::Item), span)) => Item::Item {
content: string(self.lexer.slice()), content: string(self.lexer.slice()),
span, span,
}, },
Some((Ok(Token::QuotedItem), span)) => Item::Key { Some((Ok(Token::QuotedItem), span)) => Item::Item {
content: quoted_string(self.lexer.slice()), content: quoted_string(self.lexer.slice()),
span, span,
}, },
@ -233,12 +227,22 @@ impl<'a> Reader<'a> {
})) }))
} }
Some((Ok(Token::QuotedItem), span)) => Value { Some((Ok(Token::QuotedItem), span)) => Item::Item {
content: quoted_string(self.lexer.slice()), content: quoted_string(self.lexer.slice()),
span, span,
}, },
Some((Ok(Token::Item), span)) => Value { Some((Ok(Token::Item), span)) => Item::Item {
content: string(self.lexer.slice()),
span,
},
Some((Ok(Token::QuotedStatement), span)) => Item::Statement {
content: quoted_string(self.lexer.slice()),
span,
},
Some((Ok(Token::Statement), span)) => Item::Statement {
content: string(self.lexer.slice()), content: string(self.lexer.slice()),
span, span,
}, },
@ -254,7 +258,7 @@ impl<'a> Reader<'a> {
} }
}; };
let span = key.span().start..value.span.end; let span = key.span().start..value.span().end;
Some(Ok(Event::Entry { key, value, span })) Some(Ok(Event::Entry { key, value, span }))
} }
} }

View file

@ -0,0 +1,179 @@
"UserConfigData"
{
"Steam"
{
"cached"
{
"OverlaySplash.res"
{
}
}
}
"FriendsMainDialog"
{
"xpos" "1028"
"ypos" "280"
"wide" "252"
"tall" "440"
"FriendPanelSelf"
{
}
"FriendsDialogSheet"
{
"FriendsFriendsPage"
{
"BuddyList"
{
"0_collapsed" "0"
"1_collapsed" "0"
"2_collapsed" "0"
"3_collapsed" "0"
"4_collapsed" "0"
"5_collapsed" "0"
"allfriends_collapsed" "0"
}
}
"FriendsClansPage"
{
"BuddyList"
{
"0_collapsed" "0"
"1_collapsed" "0"
"2_collapsed" "0"
"3_collapsed" "0"
}
}
}
"FriendsState"
{
}
}
"Servers"
{
"DialogServerBrowser.res"
{
"xpos" "0"
"ypos" "0"
"wide" "1280"
"tall" "720"
"GameTabs"
{
"InternetGames"
{
"gamelist"
{
"#ServerBrowser_Password_hidden" "0"
"#ServerBrowser_Bots_hidden" "1"
"#ServerBrowser_Secure_hidden" "0"
"#ServerBrowser_Servers_hidden" "0"
"#ServerBrowser_IPAddress_hidden" "1"
"#ServerBrowser_Game_hidden" "0"
"#ServerBrowser_Players_hidden" "0"
"#ServerBrowser_Map_hidden" "0"
"#ServerBrowser_Latency_hidden" "0"
"sort_column" "#ServerBrowser_Latency"
"sort_column_secondary" ""
"sort_column_asc" "1"
"sort_column_secondary_asc" "1"
}
}
"FavoriteGames"
{
"gamelist"
{
"#ServerBrowser_Password_hidden" "0"
"#ServerBrowser_Bots_hidden" "1"
"#ServerBrowser_Secure_hidden" "0"
"#ServerBrowser_Servers_hidden" "0"
"#ServerBrowser_IPAddress_hidden" "1"
"#ServerBrowser_Game_hidden" "0"
"#ServerBrowser_Players_hidden" "0"
"#ServerBrowser_Map_hidden" "0"
"#ServerBrowser_Latency_hidden" "0"
"sort_column" "#ServerBrowser_Latency"
"sort_column_secondary" ""
"sort_column_asc" "1"
"sort_column_secondary_asc" "1"
}
}
"HistoryGames"
{
"gamelist"
{
"#ServerBrowser_Password_hidden" "0"
"#ServerBrowser_Bots_hidden" "1"
"#ServerBrowser_Secure_hidden" "0"
"#ServerBrowser_Servers_hidden" "0"
"#ServerBrowser_IPAddress_hidden" "1"
"#ServerBrowser_Game_hidden" "0"
"#ServerBrowser_Players_hidden" "0"
"#ServerBrowser_Map_hidden" "0"
"#ServerBrowser_Latency_hidden" "0"
"#ServerBrowser_LastPlayed_hidden" "0"
"sort_column" "#ServerBrowser_LastPlayed"
"sort_column_secondary" "#ServerBrowser_Latency"
"sort_column_asc" "1"
"sort_column_secondary_asc" "1"
}
}
"SpectateGames"
{
"gamelist"
{
"#ServerBrowser_Password_hidden" "0"
"#ServerBrowser_Bots_hidden" "1"
"#ServerBrowser_Secure_hidden" "0"
"#ServerBrowser_Servers_hidden" "0"
"#ServerBrowser_IPAddress_hidden" "1"
"#ServerBrowser_Game_hidden" "0"
"#ServerBrowser_Players_hidden" "0"
"#ServerBrowser_Map_hidden" "0"
"#ServerBrowser_Latency_hidden" "0"
"sort_column" "#ServerBrowser_Latency"
"sort_column_secondary" ""
"sort_column_asc" "1"
"sort_column_secondary_asc" "1"
}
}
"LanGames"
{
"gamelist"
{
"#ServerBrowser_Password_hidden" "0"
"#ServerBrowser_Bots_hidden" "1"
"#ServerBrowser_Secure_hidden" "0"
"#ServerBrowser_Servers_hidden" "0"
"#ServerBrowser_IPAddress_hidden" "1"
"#ServerBrowser_Game_hidden" "0"
"#ServerBrowser_Players_hidden" "0"
"#ServerBrowser_Map_hidden" "0"
"#ServerBrowser_Latency_hidden" "0"
"sort_column" "#ServerBrowser_Latency"
"sort_column_secondary" ""
"sort_column_asc" "1"
"sort_column_secondary_asc" "1"
}
}
"FriendsGames"
{
"gamelist"
{
"#ServerBrowser_Password_hidden" "0"
"#ServerBrowser_Bots_hidden" "1"
"#ServerBrowser_Secure_hidden" "0"
"#ServerBrowser_Servers_hidden" "0"
"#ServerBrowser_IPAddress_hidden" "1"
"#ServerBrowser_Game_hidden" "0"
"#ServerBrowser_Players_hidden" "0"
"#ServerBrowser_Map_hidden" "0"
"#ServerBrowser_Latency_hidden" "0"
"sort_column" "#ServerBrowser_Latency"
"sort_column_secondary" ""
"sort_column_asc" "1"
"sort_column_secondary_asc" "1"
}
}
}
}
}
}

View file

@ -5,6 +5,7 @@ use vdf_reader::Reader;
#[test_case("tests/data/concrete.vmt")] #[test_case("tests/data/concrete.vmt")]
#[test_case("tests/data/messy.vdf")] #[test_case("tests/data/messy.vdf")]
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
fn test_parse(path: &str) { fn test_parse(path: &str) {
let raw = read_to_string(path).unwrap(); let raw = read_to_string(path).unwrap();
let mut reader = Reader::from(raw.as_str()); let mut reader = Reader::from(raw.as_str());

View file

@ -0,0 +1,155 @@
---
source: tests/parse.rs
expression: parsed
---
Table({
"UserConfigData": Table(Table({
"FriendsMainDialog": Table(Table({
"FriendPanelSelf": Table(Table({})),
"FriendsDialogSheet": Table(Table({
"FriendsClansPage": Table(Table({
"BuddyList": Table(Table({
"0_collapsed": Value(Value("0")),
"1_collapsed": Value(Value("0")),
"2_collapsed": Value(Value("0")),
"3_collapsed": Value(Value("0")),
})),
})),
"FriendsFriendsPage": Table(Table({
"BuddyList": Table(Table({
"0_collapsed": Value(Value("0")),
"1_collapsed": Value(Value("0")),
"2_collapsed": Value(Value("0")),
"3_collapsed": Value(Value("0")),
"4_collapsed": Value(Value("0")),
"5_collapsed": Value(Value("0")),
"allfriends_collapsed": Value(Value("0")),
})),
})),
})),
"FriendsState": Table(Table({})),
"tall": Value(Value("440")),
"wide": Value(Value("252")),
"xpos": Value(Value("1028")),
"ypos": Value(Value("280")),
})),
"Servers": Table(Table({
"DialogServerBrowser.res": Table(Table({
"GameTabs": Table(Table({
"FavoriteGames": Table(Table({
"gamelist": Table(Table({
"#ServerBrowser_Bots_hidden": Statement(Statement("1")),
"#ServerBrowser_Game_hidden": Statement(Statement("0")),
"#ServerBrowser_IPAddress_hidden": Statement(Statement("1")),
"#ServerBrowser_Latency_hidden": Statement(Statement("0")),
"#ServerBrowser_Map_hidden": Statement(Statement("0")),
"#ServerBrowser_Password_hidden": Statement(Statement("0")),
"#ServerBrowser_Players_hidden": Statement(Statement("0")),
"#ServerBrowser_Secure_hidden": Statement(Statement("0")),
"#ServerBrowser_Servers_hidden": Statement(Statement("0")),
"sort_column": Value(Value("#ServerBrowser_Latency")),
"sort_column_asc": Value(Value("1")),
"sort_column_secondary": Value(Value("")),
"sort_column_secondary_asc": Value(Value("1")),
})),
})),
"FriendsGames": Table(Table({
"gamelist": Table(Table({
"#ServerBrowser_Bots_hidden": Statement(Statement("1")),
"#ServerBrowser_Game_hidden": Statement(Statement("0")),
"#ServerBrowser_IPAddress_hidden": Statement(Statement("1")),
"#ServerBrowser_Latency_hidden": Statement(Statement("0")),
"#ServerBrowser_Map_hidden": Statement(Statement("0")),
"#ServerBrowser_Password_hidden": Statement(Statement("0")),
"#ServerBrowser_Players_hidden": Statement(Statement("0")),
"#ServerBrowser_Secure_hidden": Statement(Statement("0")),
"#ServerBrowser_Servers_hidden": Statement(Statement("0")),
"sort_column": Value(Value("#ServerBrowser_Latency")),
"sort_column_asc": Value(Value("1")),
"sort_column_secondary": Value(Value("")),
"sort_column_secondary_asc": Value(Value("1")),
})),
})),
"HistoryGames": Table(Table({
"gamelist": Table(Table({
"#ServerBrowser_Bots_hidden": Statement(Statement("1")),
"#ServerBrowser_Game_hidden": Statement(Statement("0")),
"#ServerBrowser_IPAddress_hidden": Statement(Statement("1")),
"#ServerBrowser_LastPlayed_hidden": Statement(Statement("0")),
"#ServerBrowser_Latency_hidden": Statement(Statement("0")),
"#ServerBrowser_Map_hidden": Statement(Statement("0")),
"#ServerBrowser_Password_hidden": Statement(Statement("0")),
"#ServerBrowser_Players_hidden": Statement(Statement("0")),
"#ServerBrowser_Secure_hidden": Statement(Statement("0")),
"#ServerBrowser_Servers_hidden": Statement(Statement("0")),
"sort_column": Value(Value("#ServerBrowser_LastPlayed")),
"sort_column_asc": Value(Value("1")),
"sort_column_secondary": Value(Value("#ServerBrowser_Latency")),
"sort_column_secondary_asc": Value(Value("1")),
})),
})),
"InternetGames": Table(Table({
"gamelist": Table(Table({
"#ServerBrowser_Bots_hidden": Statement(Statement("1")),
"#ServerBrowser_Game_hidden": Statement(Statement("0")),
"#ServerBrowser_IPAddress_hidden": Statement(Statement("1")),
"#ServerBrowser_Latency_hidden": Statement(Statement("0")),
"#ServerBrowser_Map_hidden": Statement(Statement("0")),
"#ServerBrowser_Password_hidden": Statement(Statement("0")),
"#ServerBrowser_Players_hidden": Statement(Statement("0")),
"#ServerBrowser_Secure_hidden": Statement(Statement("0")),
"#ServerBrowser_Servers_hidden": Statement(Statement("0")),
"sort_column": Value(Value("#ServerBrowser_Latency")),
"sort_column_asc": Value(Value("1")),
"sort_column_secondary": Value(Value("")),
"sort_column_secondary_asc": Value(Value("1")),
})),
})),
"LanGames": Table(Table({
"gamelist": Table(Table({
"#ServerBrowser_Bots_hidden": Statement(Statement("1")),
"#ServerBrowser_Game_hidden": Statement(Statement("0")),
"#ServerBrowser_IPAddress_hidden": Statement(Statement("1")),
"#ServerBrowser_Latency_hidden": Statement(Statement("0")),
"#ServerBrowser_Map_hidden": Statement(Statement("0")),
"#ServerBrowser_Password_hidden": Statement(Statement("0")),
"#ServerBrowser_Players_hidden": Statement(Statement("0")),
"#ServerBrowser_Secure_hidden": Statement(Statement("0")),
"#ServerBrowser_Servers_hidden": Statement(Statement("0")),
"sort_column": Value(Value("#ServerBrowser_Latency")),
"sort_column_asc": Value(Value("1")),
"sort_column_secondary": Value(Value("")),
"sort_column_secondary_asc": Value(Value("1")),
})),
})),
"SpectateGames": Table(Table({
"gamelist": Table(Table({
"#ServerBrowser_Bots_hidden": Statement(Statement("1")),
"#ServerBrowser_Game_hidden": Statement(Statement("0")),
"#ServerBrowser_IPAddress_hidden": Statement(Statement("1")),
"#ServerBrowser_Latency_hidden": Statement(Statement("0")),
"#ServerBrowser_Map_hidden": Statement(Statement("0")),
"#ServerBrowser_Password_hidden": Statement(Statement("0")),
"#ServerBrowser_Players_hidden": Statement(Statement("0")),
"#ServerBrowser_Secure_hidden": Statement(Statement("0")),
"#ServerBrowser_Servers_hidden": Statement(Statement("0")),
"sort_column": Value(Value("#ServerBrowser_Latency")),
"sort_column_asc": Value(Value("1")),
"sort_column_secondary": Value(Value("")),
"sort_column_secondary_asc": Value(Value("1")),
})),
})),
})),
"tall": Value(Value("720")),
"wide": Value(Value("1280")),
"xpos": Value(Value("0")),
"ypos": Value(Value("0")),
})),
})),
"Steam": Table(Table({
"cached": Table(Table({
"OverlaySplash.res": Table(Table({})),
})),
})),
})),
})