mirror of
https://codeberg.org/icewind/vdf-reader.git
synced 2026-06-03 10:04:08 +02:00
add support for deserializing a map with int keys as a sequence
This commit is contained in:
parent
1a37f7e866
commit
b928f85df6
14 changed files with 226 additions and 9 deletions
37
examples/steam_folders.rs
Normal file
37
examples/steam_folders.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::Entry;
|
use super::{Entry, Table};
|
||||||
use crate::entry::Value;
|
use crate::entry::Value;
|
||||||
|
use crate::error::{ParseStringError, UnknownError};
|
||||||
use crate::VdfError;
|
use crate::VdfError;
|
||||||
use serde::de::{DeserializeSeed, SeqAccess};
|
use serde::de::{DeserializeSeed, SeqAccess};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -80,3 +81,53 @@ impl<'de> SeqAccess<'de> for ArraySeq {
|
||||||
seed.deserialize(next).map(Some)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 crate::entry::table::TableSeq;
|
||||||
use serde::de::{DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor};
|
use serde::de::{DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
@ -614,7 +614,8 @@ impl<'de> Deserializer<'de> for Entry {
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
Entry::Array(arr) => visitor.visit_seq(ArraySeq::new(arr)),
|
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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use std::ops::{Deref, DerefMut};
|
||||||
/// A table of entries.
|
/// A table of entries.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Default)]
|
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Default)]
|
||||||
#[serde(transparent)]
|
#[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 {
|
impl From<HashMap<String, Entry>> for Table {
|
||||||
fn from(value: HashMap<String, Entry>) -> Self {
|
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 {
|
impl From<Table> for Entry {
|
||||||
fn from(table: Table) -> Self {
|
fn from(table: Table) -> Self {
|
||||||
Entry::Table(table)
|
Entry::Table(table)
|
||||||
|
|
|
||||||
10
src/error.rs
10
src/error.rs
|
|
@ -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
|
/// A token that wasn't expected was found while parsing
|
||||||
#[derive(Debug, Clone, Diagnostic)]
|
#[derive(Debug, Clone, Diagnostic)]
|
||||||
#[diagnostic(code(vmt_reader::unexpected_token))]
|
#[diagnostic(code(vmt_reader::unexpected_token))]
|
||||||
|
|
|
||||||
54
src/serde.rs
54
src/serde.rs
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::entry::{string_is_array, Entry, ParseItem};
|
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::tokenizer::{SpannedToken, Tokenizer};
|
||||||
use crate::{Token, VdfError};
|
use crate::{Token, VdfError};
|
||||||
use logos::Span;
|
use logos::Span;
|
||||||
|
|
@ -348,7 +348,17 @@ impl<'de> de::Deserializer<'de> for &'_ mut Deserializer<'de> {
|
||||||
where
|
where
|
||||||
V: Visitor<'de>,
|
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()];
|
let value_str = &self.source()[token.span.clone()];
|
||||||
if (value_str.starts_with("\"[") && value_str.ends_with("]\""))
|
if (value_str.starts_with("\"[") && value_str.ends_with("]\""))
|
||||||
|| (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> {
|
struct SeqWalker<'source, 'a> {
|
||||||
table: TableWalker<'source, 'a>,
|
table: TableWalker<'source, 'a>,
|
||||||
key: Cow<'source, str>,
|
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> {
|
struct Enum<'a, 'de: 'a> {
|
||||||
de: &'a mut Deserializer<'de>,
|
de: &'a mut Deserializer<'de>,
|
||||||
enclosed: bool,
|
enclosed: bool,
|
||||||
|
|
|
||||||
6
tests/data/maparray.vdf
Normal file
6
tests/data/maparray.vdf
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"MapArray"
|
||||||
|
{
|
||||||
|
"0" "foo"
|
||||||
|
"1" "bar"
|
||||||
|
"2" "asd"
|
||||||
|
}
|
||||||
6
tests/errors/invalidmaparraykeys.vdf
Normal file
6
tests/errors/invalidmaparraykeys.vdf
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"MapArray"
|
||||||
|
{
|
||||||
|
"0" "foo"
|
||||||
|
"1" "bar"
|
||||||
|
"3" "asd"
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,7 @@ enum Expected {
|
||||||
EnumInMap {
|
EnumInMap {
|
||||||
foo: EnumInMap,
|
foo: EnumInMap,
|
||||||
},
|
},
|
||||||
|
MapArray(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -211,6 +212,8 @@ struct GameList {
|
||||||
#[test_case("tests/errors/concrete.vmt")]
|
#[test_case("tests/errors/concrete.vmt")]
|
||||||
#[test_case("tests/errors/novalue.vdf")]
|
#[test_case("tests/errors/novalue.vdf")]
|
||||||
#[test_case("tests/errors/serde_array_type.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) {
|
fn test_serde(path: &str) {
|
||||||
let raw = read_to_string(path).unwrap();
|
let raw = read_to_string(path).unwrap();
|
||||||
match from_str::<Expected>(&raw) {
|
match from_str::<Expected>(&raw) {
|
||||||
|
|
@ -228,6 +231,7 @@ fn test_serde(path: &str) {
|
||||||
#[test_case("tests/data/concrete.vmt")]
|
#[test_case("tests/data/concrete.vmt")]
|
||||||
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
|
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
|
||||||
#[test_case("tests/data/serde_array_type.vdf")]
|
#[test_case("tests/data/serde_array_type.vdf")]
|
||||||
|
#[test_case("tests/data/maparray.vdf")]
|
||||||
fn test_serde_table(path: &str) {
|
fn test_serde_table(path: &str) {
|
||||||
let raw = read_to_string(path).unwrap();
|
let raw = read_to_string(path).unwrap();
|
||||||
match from_str::<Table>(&raw) {
|
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/messy.vdf")]
|
||||||
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
|
#[test_case("tests/data/DialogConfigOverlay_1280x720.vdf")]
|
||||||
#[test_case("tests/data/serde_array_type.vdf")]
|
#[test_case("tests/data/serde_array_type.vdf")]
|
||||||
|
#[test_case("tests/data/maparray.vdf")]
|
||||||
fn test_serde_from_table(path: &str) {
|
fn test_serde_from_table(path: &str) {
|
||||||
let raw = read_to_string(path).unwrap();
|
let raw = read_to_string(path).unwrap();
|
||||||
let result = Table::load_from_str(&raw).unwrap();
|
let result = Table::load_from_str(&raw).unwrap();
|
||||||
|
|
|
||||||
11
tests/snapshots/serde__table__tests__data__maparray.vdf.snap
Normal file
11
tests/snapshots/serde__table__tests__data__maparray.vdf.snap
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
source: tests/serde.rs
|
||||||
|
expression: result
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"MapArray": {
|
||||||
|
"0": "foo",
|
||||||
|
"1": "bar",
|
||||||
|
"2": "asd",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: tests/serde.rs
|
||||||
|
expression: material
|
||||||
|
---
|
||||||
|
MapArray([
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
"asd",
|
||||||
|
])
|
||||||
9
tests/snapshots/serde__tests__data__maparray.vdf.snap
Normal file
9
tests/snapshots/serde__tests__data__maparray.vdf.snap
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: tests/serde.rs
|
||||||
|
expression: result
|
||||||
|
---
|
||||||
|
MapArray([
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
"asd",
|
||||||
|
])
|
||||||
|
|
@ -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 │ }
|
||||||
|
╰────
|
||||||
|
|
@ -4,11 +4,10 @@ expression: out
|
||||||
---
|
---
|
||||||
vmt_parser::unknown_variant
|
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:1]
|
||||||
1 │ "Resource"
|
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 │ {
|
2 │ {
|
||||||
╰────
|
╰────
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue