add support for deserializing a map with int keys as a sequence

This commit is contained in:
Robin Appelman 2025-06-05 20:42:13 +02:00
commit b928f85df6
14 changed files with 226 additions and 9 deletions

37
examples/steam_folders.rs Normal file
View file

@ -0,0 +1,37 @@
use miette::{Context, IntoDiagnostic, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::env::args;
use std::fs::read_to_string;
use vdf_reader::from_str;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct LibraryFolders {
#[serde(rename = "libraryfolders")]
folders: Vec<LibraryFolder>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct LibraryFolder {
path: String,
label: String,
#[serde(rename = "contentid")]
content_id: i64,
#[serde(rename = "totalsize")]
total_size: u64,
update_clean_bytes_tally: u64,
time_last_update_verified: u64,
apps: HashMap<u64, u64>,
}
fn main() -> Result<()> {
let path = args().nth(1).expect("no path provided");
let raw = read_to_string(path)
.into_diagnostic()
.wrap_err("failed to read input")?;
let folders: LibraryFolders = from_str(&raw)?;
dbg!(folders);
Ok(())
}

View file

@ -1,5 +1,6 @@
use super::Entry;
use super::{Entry, Table};
use crate::entry::Value;
use crate::error::{ParseStringError, UnknownError};
use crate::VdfError;
use serde::de::{DeserializeSeed, SeqAccess};
use serde::{Deserialize, Serialize};
@ -80,3 +81,53 @@ impl<'de> SeqAccess<'de> for ArraySeq {
seed.deserialize(next).map(Some)
}
}
pub(crate) struct TableArraySeq {
iter: std::vec::IntoIter<(String, Entry)>,
last_key: Option<u64>,
}
impl TableArraySeq {
pub(crate) fn new(table: Table) -> Self {
// since the tables map doesn't have a stable order, we need to re-sort them to have the keys in order
let mut items: Vec<_> = table.into_iter().collect();
items.sort_by(|(key_a, _), (key_b, _)| key_a.cmp(key_b));
TableArraySeq {
iter: items.into_iter(),
last_key: None,
}
}
}
impl<'de> SeqAccess<'de> for TableArraySeq {
type Error = VdfError;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: DeserializeSeed<'de>,
{
let (key, next) = match self.iter.next() {
Some(next) => next,
None => return Ok(None),
};
let key: u64 = key.parse().map_err(|_| {
VdfError::ParseString(ParseStringError {
ty: "u64",
value: key,
})
})?;
if let Some(last_key) = self.last_key {
let expected_key = last_key + 1;
if expected_key != key {
return Err(VdfError::Other(UnknownError::from(format!(
"Invalid array key {key}, expected {expected_key}"
))));
}
}
self.last_key = Some(key);
seed.deserialize(next).map(Some)
}
}

View file

@ -216,7 +216,7 @@ macro_rules! from_str {
);
}
use crate::entry::array::ArraySeq;
use crate::entry::array::{ArraySeq, TableArraySeq};
use crate::entry::table::TableSeq;
use serde::de::{DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
@ -614,7 +614,8 @@ impl<'de> Deserializer<'de> for Entry {
{
match self {
Entry::Array(arr) => visitor.visit_seq(ArraySeq::new(arr)),
_ => Err(UnknownError::from("array2").into()),
Entry::Table(table) => visitor.visit_seq(TableArraySeq::new(table)),
_ => Err(UnknownError::from("array").into()),
}
}

View file

@ -12,7 +12,7 @@ use std::ops::{Deref, DerefMut};
/// A table of entries.
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Default)]
#[serde(transparent)]
pub struct Table(#[serde(serialize_with = "ordered_map")] HashMap<String, Entry>);
pub struct Table(#[serde(serialize_with = "ordered_map")] HashMap<String, Entry>); // todo: switch to a map that maintains item order
impl From<HashMap<String, Entry>> for Table {
fn from(value: HashMap<String, Entry>) -> Self {
@ -118,6 +118,15 @@ impl Table {
}
}
impl IntoIterator for Table {
type Item = (String, Entry);
type IntoIter = hash_map::IntoIter<String, Entry>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl From<Table> for Entry {
fn from(table: Table) -> Self {
Entry::Table(table)

View file

@ -181,6 +181,16 @@ impl From<&str> for UnknownError {
}
}
impl From<String> for UnknownError {
fn from(value: String) -> Self {
UnknownError {
error: value,
err_span: (0..0).into(),
src: String::new(),
}
}
}
/// A token that wasn't expected was found while parsing
#[derive(Debug, Clone, Diagnostic)]
#[diagnostic(code(vmt_reader::unexpected_token))]

View file

@ -1,5 +1,5 @@
use crate::entry::{string_is_array, Entry, ParseItem};
use crate::error::{ExpectToken, NoValidTokenError, ResultExt, SerdeParseError};
use crate::error::{ExpectToken, NoValidTokenError, ResultExt, SerdeParseError, UnknownError};
use crate::tokenizer::{SpannedToken, Tokenizer};
use crate::{Token, VdfError};
use logos::Span;
@ -348,7 +348,17 @@ impl<'de> de::Deserializer<'de> for &'_ mut Deserializer<'de> {
where
V: Visitor<'de>,
{
let token = self.peek().expect_token(STRING_ITEMS, self.source())?;
let token = self.peek().expect_token(SEQ_START_ITEMS, self.source())?;
if token.token == Token::GroupStart {
// we allow deserializing a map of consecutive int keys as a seq
let _ = self.next();
return visitor.visit_seq(MapSeqWalker {
table: TableWalker::new(self, false),
last_key: None,
});
}
let value_str = &self.source()[token.span.clone()];
if (value_str.starts_with("\"[") && value_str.ends_with("]\""))
|| (value_str.starts_with("\"{") && value_str.ends_with("}\""))
@ -555,6 +565,15 @@ impl<'de> MapAccess<'de> for TableWalker<'de, '_> {
}
}
/// STRING_ITEMS and GroupStart
const SEQ_START_ITEMS: &[Token] = &[
Token::Item,
Token::QuotedItem,
Token::Statement,
Token::QuotedStatement,
Token::GroupStart,
];
struct SeqWalker<'source, 'a> {
table: TableWalker<'source, 'a>,
key: Cow<'source, str>,
@ -674,6 +693,37 @@ where
}
}
struct MapSeqWalker<'source, 'a> {
table: TableWalker<'source, 'a>,
last_key: Option<u64>,
}
impl<'de> SeqAccess<'de> for MapSeqWalker<'de, '_> {
type Error = VdfError;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: DeserializeSeed<'de>,
{
let Some(key) = self.table.next_key()? else {
return Ok(None);
};
if let Some(last_key) = self.last_key {
let expected_key = last_key + 1;
if expected_key != key {
return Err(VdfError::Other(UnknownError::from(format!(
"Invalid array key {key}, expected {expected_key}"
)))
.with_source_span(self.table.de.last_span.clone(), self.table.de.source()));
}
}
self.last_key = Some(key);
Ok(Some(self.table.next_value_seed(seed)?))
}
}
struct Enum<'a, 'de: 'a> {
de: &'a mut Deserializer<'de>,
enclosed: bool,

6
tests/data/maparray.vdf Normal file
View file

@ -0,0 +1,6 @@
"MapArray"
{
"0" "foo"
"1" "bar"
"2" "asd"
}

View file

@ -0,0 +1,6 @@
"MapArray"
{
"0" "foo"
"1" "bar"
"3" "asd"
}

View file

@ -69,6 +69,7 @@ enum Expected {
EnumInMap {
foo: EnumInMap,
},
MapArray(Vec<String>),
}
#[derive(Debug, Serialize, Deserialize)]
@ -211,6 +212,8 @@ struct GameList {
#[test_case("tests/errors/concrete.vmt")]
#[test_case("tests/errors/novalue.vdf")]
#[test_case("tests/errors/serde_array_type.vdf")]
#[test_case("tests/data/maparray.vdf")]
#[test_case("tests/errors/invalidmaparraykeys.vdf")]
fn test_serde(path: &str) {
let raw = read_to_string(path).unwrap();
match from_str::<Expected>(&raw) {
@ -228,6 +231,7 @@ fn test_serde(path: &str) {
#[test_case("tests/data/concrete.vmt")]
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
#[test_case("tests/data/serde_array_type.vdf")]
#[test_case("tests/data/maparray.vdf")]
fn test_serde_table(path: &str) {
let raw = read_to_string(path).unwrap();
match from_str::<Table>(&raw) {
@ -247,6 +251,7 @@ fn test_serde_table(path: &str) {
#[test_case("tests/data/messy.vdf")]
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
#[test_case("tests/data/serde_array_type.vdf")]
#[test_case("tests/data/maparray.vdf")]
fn test_serde_from_table(path: &str) {
let raw = read_to_string(path).unwrap();
let result = Table::load_from_str(&raw).unwrap();

View file

@ -0,0 +1,11 @@
---
source: tests/serde.rs
expression: result
---
{
"MapArray": {
"0": "foo",
"1": "bar",
"2": "asd",
},
}

View file

@ -0,0 +1,9 @@
---
source: tests/serde.rs
expression: material
---
MapArray([
"foo",
"bar",
"asd",
])

View file

@ -0,0 +1,9 @@
---
source: tests/serde.rs
expression: result
---
MapArray([
"foo",
"bar",
"asd",
])

View file

@ -0,0 +1,14 @@
---
source: tests/serde.rs
expression: out
---
vmt_reader::unexpected_token
× Invalid array key 3, expected 2
╭─[5:3]
4 │ "1" "bar"
5 │ "3" "asd"
· ─┬─
· ╰── Invalid array key 3, expected 2
6 │ }
╰────

View file

@ -4,11 +4,10 @@ expression: out
---
vmt_parser::unknown_variant
× Unknown variant "Resource" expected on of expected on of Types, LightmappedGeneric, Resource/specificPanel.res, UserConfigData, Sprite, EnumInMap
× Unknown variant "Resource" expected on of expected on of Types, LightmappedGeneric, Resource/specificPanel.res, UserConfigData, Sprite, EnumInMap, MapArray
╭─[1:1]
1 │ "Resource"
· ─────┬────
· ╰── expected on of Types, LightmappedGeneric, Resource/specificPanel.res, UserConfigData, Sprite, EnumInMap
· ╰── expected on of Types, LightmappedGeneric, Resource/specificPanel.res, UserConfigData, Sprite, EnumInMap, MapArray
2 │ {
╰────