better errors for keys without values

This commit is contained in:
Robin Appelman 2023-12-15 17:48:56 +01:00
commit 9c06896b34
8 changed files with 191 additions and 6 deletions

29
Cargo.lock generated
View file

@ -406,6 +406,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.193"
@ -603,6 +612,17 @@ dependencies = [
"serde",
"test-case",
"thiserror",
"walkdir",
]
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
@ -621,6 +641,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View file

@ -14,3 +14,4 @@ serde = { version = "1.0.193", features = ["derive"] }
test-case = "3.3.1"
insta = { version = "1.34.0", features = ["ron"] }
miette = { version = "5.10.0", features = ["fancy"] }
walkdir = "2.4.0"

View file

@ -0,0 +1,46 @@
use miette::{Context, IntoDiagnostic, Result};
use std::env::args;
use std::fs::read_to_string;
use std::path::Path;
use vdf_reader::entry::Table;
use vdf_reader::Reader;
use walkdir::WalkDir;
fn main() -> Result<()> {
let mut success = 0;
let mut err = Vec::new();
let dir = args().nth(1).expect("no path provided");
for entry in WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name().to_str().unwrap_or_default();
name.ends_with(".vmt") || name.ends_with(".vdf")
})
{
if let Err(e) = try_parse(entry.path()) {
err.push(e);
let e = try_parse(entry.path()).unwrap_err();
println!("{:?}", e);
} else {
success += 1;
println!("{}", entry.path().display());
}
}
println!("successfully parsed {success} files");
println!("found errors in {} files", err.len());
for e in err {
println!("{:?}", e);
}
Ok(())
}
fn try_parse(path: &Path) -> Result<Table> {
let raw = read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", path.display()))?;
let mut reader = Reader::from(raw.as_str());
Table::load(&mut reader).wrap_err_with(|| format!("failed to parse {}", path.display()))
}

16
examples/tokens.rs Normal file
View file

@ -0,0 +1,16 @@
use miette::{Context, IntoDiagnostic, Result};
use std::env::args;
use std::fs::read_to_string;
use vdf_reader::Reader;
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 reader = Reader::from(raw.as_str());
for event in reader {
println!("{:?}", event?);
}
Ok(())
}

View file

@ -4,9 +4,13 @@ use std::str;
/// Parser token.
#[derive(PartialEq, Debug, Logos, Display, Clone)]
#[logos(skip r"[ \t\n\f\r]+")] // whitespace
#[logos(skip r"[ \t\f\r]+")] // whitespace
#[logos(skip r"//[^\n]*")] // comments
pub enum Token {
/// Newline
#[regex("\n([ \t\r]\n)*")]
#[display("newline")]
NewLine,
/// A group is starting.
#[token("{")]
#[display("start of group")]
@ -16,7 +20,7 @@ pub enum Token {
#[display("end of group")]
GroupEnd,
/// An enclosed or bare item.
#[regex("[^# \t\n{}\"][^ \"\t\n]*", priority = 0)]
#[regex("[^# \t\n{}\"][^ \t\n]*", priority = 0)]
#[display("item")]
Item,
/// An enclosed or bare item.
@ -92,17 +96,26 @@ mod tests {
// a comment
#include other
empty ""
\\"broken" comment
}"#
),
Ok(vec![
(Token::Item, "foo"),
(Token::GroupStart, "{"),
(Token::NewLine, "\n"),
(Token::QuotedItem, r#""asd""#),
(Token::QuotedItem, r#""bar""#),
(Token::NewLine, "\n"),
(Token::NewLine, "\n"),
(Token::Statement, r#"#include"#),
(Token::Item, r#"other"#),
(Token::NewLine, "\n"),
(Token::Item, r#"empty"#),
(Token::QuotedItem, r#""""#),
(Token::NewLine, "\n"),
(Token::Item, r#"\\"broken""#),
(Token::Item, r#"comment"#),
(Token::NewLine, "\n"),
(Token::GroupEnd, "}")
])
)

View file

@ -1,6 +1,6 @@
use super::{Result, Token};
use crate::error::{NoValidTokenError, UnexpectedTokenError};
use logos::{Lexer, Span, SpannedIter};
use logos::{Lexer, Logos, Span, SpannedIter};
use std::borrow::Cow;
/// Kinds of item.
@ -67,6 +67,7 @@ impl Event<'_> {
pub struct Reader<'a> {
pub(crate) content: &'a str,
lexer: SpannedIter<'a, Token>,
peeked: Option<(Result<Token, <Token as Logos<'a>>::Error>, Span)>,
}
impl<'a> From<&'a str> for Reader<'a> {
@ -74,14 +75,43 @@ impl<'a> From<&'a str> for Reader<'a> {
Reader {
content,
lexer: Lexer::new(content).spanned(),
peeked: None,
}
}
}
impl<'a> Reader<'a> {
fn token(&mut self) -> Option<(Result<Token, <Token as Logos>::Error>, Span)> {
if let Some((token, span)) = self.peeked.take() {
Some((token, span))
} else {
self.lexer.next()
}
}
fn peek(&mut self) -> Option<(Result<Token, <Token as Logos>::Error>, Span)> {
if self.peeked.is_none() {
self.peeked = self.lexer.next();
}
self.peeked.clone()
}
fn token_eat_newlines(&mut self) -> Option<(Result<Token, <Token as Logos>::Error>, Span)> {
loop {
let (token, span) = self.token()?;
match token {
Err(e) => return Some((Err(e), span)),
Ok(Token::NewLine) => {
continue;
}
Ok(token) => return Some((Ok(token), span)),
}
}
}
/// Get the next event, this does copies.
#[allow(dead_code)]
pub fn event(&mut self) -> Option<Result<Event>> {
pub fn event(&mut self) -> Option<Result<Event<'a>>> {
const VALID_KEY: &[Token] = &[
Token::Item,
Token::QuotedItem,
@ -90,7 +120,7 @@ impl<'a> Reader<'a> {
Token::QuotedStatement,
];
let key = match self.lexer.next() {
let key = match self.token_eat_newlines() {
None => {
return None;
}
@ -137,7 +167,46 @@ impl<'a> Reader<'a> {
const VALID_VALUE: &[Token] = &[Token::Item, Token::QuotedItem, Token::GroupStart];
let value = match self.lexer.next() {
// only a group start is allowed to have newlines between the key and value
while matches!(self.peek(), Some((Ok(Token::NewLine), _))) {
let _newline = self.token();
if !matches!(
self.peek(),
Some((Ok(Token::GroupStart | Token::NewLine), _))
) {
let span = key.span().end..key.span().end;
match self.peeked.clone() {
Some((Ok(token), _)) => {
return Some(Err(UnexpectedTokenError::new(
&[Token::GroupStart],
Some(token),
span.into(),
self.content.into(),
)
.into()))
}
Some((Err(_), _)) => {
return Some(Err(NoValidTokenError::new(
VALID_VALUE,
span.into(),
self.content.into(),
)
.into()));
}
None => {
return Some(Err(UnexpectedTokenError::new(
VALID_VALUE,
None,
span.into(),
self.content.into(),
)
.into()))
}
}
}
}
let value = match self.token() {
None => {
return Some(Err(UnexpectedTokenError::new(
VALID_VALUE,
@ -190,6 +259,14 @@ impl<'a> Reader<'a> {
}
}
impl<'a> Iterator for Reader<'a> {
type Item = Result<Event<'a>>;
fn next(&mut self) -> Option<Self::Item> {
self.event()
}
}
fn quoted_string(source: &str) -> Cow<str> {
string(&source[1..source.len() - 1])
}

View file

@ -9,4 +9,6 @@
array 2
array "3"
windows_path "C:\test\no newline"
\\"$translucent" 1 // this is read vdf written by real valve developers
}

View file

@ -5,6 +5,7 @@ expression: parsed
Table({
"#base": Statement(Statement("panelBase.res")),
"Resource/specificPanel.res": Table(Table({
"\\\"$translucent\"": Value(Value("1")),
"array": Array(Array([
Value(Value("1")),
Value(Value("2")),