more consistant quote handling

This commit is contained in:
Robin Appelman 2023-03-11 15:49:51 +01:00
commit acf70c03d4
11 changed files with 131 additions and 109 deletions

View file

@ -1,7 +1,8 @@
use chrono::NaiveDateTime;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::fs::read_to_string;
use std::time::Duration;
use tf_log_parser::{parse, GameEvent, LineSplit, RawEvent};
use tf_log_parser::{parse, EventHandler, GameEvent, LineSplit, LogHandler, RawEvent, SubjectMap};
pub fn parse_benchmark(c: &mut Criterion) {
let input = read_to_string("test_data/log_2892242.log").unwrap();
@ -34,8 +35,32 @@ pub fn parse_raw(c: &mut Criterion) {
});
}
pub fn handle_event(c: &mut Criterion) {
let input = read_to_string("test_data/log_2892242.log").unwrap();
let events: Vec<_> = input
.split("L ")
.filter(|line| !line.is_empty())
.flat_map(RawEvent::parse)
.map(|raw| (GameEvent::parse(&raw).unwrap(), raw))
.collect();
c.bench_function("handle events 2892242", |b| {
let mut handler = LogHandler::default();
let mut subjects =
SubjectMap::<<LogHandler as EventHandler>::PerSubjectData>::with_capacity(32);
let mut start_time: Option<NaiveDateTime> = None;
b.iter(|| {
black_box(&events)
.iter()
.flat_map(|(event, raw_event)| {
black_box(handler.process(&raw_event, &event, &mut start_time, &mut subjects))
})
.count();
})
});
}
criterion_group!(
name = benches;
config = Criterion::default().measurement_time(Duration::from_secs(10));
targets = parse_benchmark, parse_raw, parse_event);
targets = parse_benchmark, parse_raw, parse_event, handle_event);
criterion_main!(benches);

View file

@ -53,14 +53,8 @@ impl Derivable for Event {
return err("optional fields can't be unnamed", &param.field_name);
};
let parser = if param.quoted {
quote_spanned!(field_name.span() => quoted(parse_field))
} else {
quote_spanned!(field_name.span() => parse_field)
};
Ok(quote_spanned!(
field_name.span() => #param_name => event.#field_name = #parser(value)?.1
field_name.span() => #param_name => event.#field_name = parse_field(value)?.1
))
})
.collect::<Result<Vec<_>>>()?;
@ -194,7 +188,8 @@ impl EventParam {
if optional && skip_after > 0 {
return err("skip_after can't be used with optional fields", input);
}
let quoted = contains_attribute(&input.attrs, &["event", "quoted"]);
let quoted =
get_attribute_value(&input.attrs, &["event", "quoted"]).unwrap_or(param_name.is_none());
Ok(EventParam {
span: input.span(),
@ -210,13 +205,16 @@ impl EventParam {
self.span
}
fn parser(&self) -> TokenStream {
let field_parser = if self.quoted {
fn field_parser(&self) -> TokenStream {
if self.quoted {
quote_spanned!(self.span() => quoted(parse_field))
} else {
quote_spanned!(self.span() => parse_field)
};
}
}
fn parser(&self) -> TokenStream {
let field_parser = self.field_parser();
if let Some(param_name) = &self.param_name {
quote_spanned!(self.span() => param_parse_with(#param_name, #field_parser))
} else {

View file

@ -9,7 +9,7 @@ fn main() -> Result<(), MainError> {
let events: Vec<_> = LineSplit::new(&input)
.flat_map(RawEvent::parse)
.flat_map(|raw| GameEvent::parse(&raw))
.map(|raw| GameEvent::parse(&raw).unwrap())
.collect();
println!("{} events parsed", events.len());

View file

@ -362,8 +362,10 @@ pub fn skip_matches(input: &str, char: u8) -> (&str, bool) {
pub fn find_between_end(input: &str, start: u8, end: u8) -> Option<&str> {
debug_assert!(start < 128 && end < 128); // only basic ascii
let end = memrchr(end, input.as_bytes())?;
let start = memrchr(start, &input.as_bytes()[0..end])?;
let bytes = input.as_bytes();
let end = memrchr(end, bytes)?;
// safety, memchr returns indices that are inside the input
let start = memrchr(start, unsafe { &bytes.get_unchecked(0..end) })?;
// safety, memchr returns indices that are inside the input length and we only split on ascii
Some(unsafe { input.get_unchecked((start + 1)..end) })
}

View file

@ -108,7 +108,6 @@ impl<'a> Event<'a> for PointCapturedEvent<'a> {
#[derive(Debug, Event)]
pub struct CurrentScoreEvent {
#[event(unnamed)]
pub score: u8,
#[event(name = "with")]
pub players: u8,
@ -121,7 +120,6 @@ pub struct GameOverEvent<'a> {
#[derive(Debug, Event)]
pub struct FinalScoreEvent {
#[event(unnamed)]
pub score: u8,
#[event(name = "with")]
pub players: u8,

View file

@ -1,4 +1,4 @@
use crate::event::{param_parse_with, parse_field, quoted, ParamIter};
use crate::event::{param_parse_with, parse_field, ParamIter};
use crate::raw_event::RawSubject;
use crate::{Event, IResult};
@ -7,7 +7,6 @@ pub struct HealedEvent<'a> {
#[event(name = "against")]
pub target: RawSubject<'a>,
#[event(name = "healing")]
#[event(quoted)]
pub amount: u32,
}

View file

@ -16,20 +16,25 @@ use std::str::FromStr;
#[derive(thiserror::Error, Debug)]
pub enum GameEventError {
#[error("malformed game event({ty:?}): {err}")]
Error { err: Box<Error>, ty: RawEventType },
Error {
err: Box<Error>,
ty: RawEventType,
params: String,
},
#[error("incomplete event body({0:?})")]
Incomplete(RawEventType),
}
trait GameEventErrTrait<T> {
fn with_type(self, ty: RawEventType) -> Result<T, GameEventError>;
fn with_raw(self, raw: &RawEvent) -> Result<T, GameEventError>;
}
impl<'a, T> GameEventErrTrait<T> for IResult<'a, T> {
fn with_type(self, ty: RawEventType) -> Result<T, GameEventError> {
fn with_raw(self, raw: &RawEvent) -> Result<T, GameEventError> {
self.map_err(|err| GameEventError::Error {
err: Box::new(err),
ty,
ty: raw.ty,
params: raw.params.to_string(),
})
.map(|(_rest, t)| t)
}
@ -99,100 +104,90 @@ pub enum GameEvent<'a> {
impl<'a> GameEvent<'a> {
pub fn parse(raw: &RawEvent<'a>) -> Result<GameEvent<'a>, GameEventError> {
Ok(match raw.ty {
RawEventType::ShotFired => {
GameEvent::ShotFired(parse_event(raw.params).with_type(raw.ty)?)
}
RawEventType::ShotHit => GameEvent::ShotHit(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::Damage => GameEvent::Damage(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::Killed => GameEvent::Kill(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::ShotFired => GameEvent::ShotFired(parse_event(raw.params).with_raw(raw)?),
RawEventType::ShotHit => GameEvent::ShotHit(parse_event(raw.params).with_raw(raw)?),
RawEventType::Damage => GameEvent::Damage(parse_event(raw.params).with_raw(raw)?),
RawEventType::Killed => GameEvent::Kill(parse_event(raw.params).with_raw(raw)?),
RawEventType::KillAssist => {
GameEvent::KillAssist(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::KillAssist(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::SayTeam => GameEvent::SayTeam(raw.params.trim_matches('"')),
RawEventType::Say => GameEvent::Say(raw.params.trim_matches('"')),
RawEventType::Healed => GameEvent::Healed(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::Healed => GameEvent::Healed(parse_event(raw.params).with_raw(raw)?),
RawEventType::ChargeDeployed => {
GameEvent::ChargeDeployed(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::ChargeDeployed(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::ChargeEnd => {
GameEvent::ChargeEnded(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::ChargeEnded(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::UberAdvantageLost => {
GameEvent::AdvantageLost(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::AdvantageLost(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::FirstHealAfterSpawn => {
GameEvent::FirstHeal(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::FirstHeal(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::ChargeReady => GameEvent::ChargeReady,
RawEventType::MedicDeath => {
GameEvent::MedicDeath(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::MedicDeath(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::MedicDeathEx => {
GameEvent::MedicDeathEx(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::MedicDeathEx(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::Spawned => GameEvent::Spawned(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::Spawned => GameEvent::Spawned(parse_event(raw.params).with_raw(raw)?),
RawEventType::ChangedRole => {
GameEvent::RoleChange(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::RoleChange(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::RoundStart => GameEvent::RoundStart,
RawEventType::RoundLength => {
GameEvent::RoundLength(parse_event(raw.params).with_type(raw.ty)?)
}
RawEventType::RoundWin => {
GameEvent::RoundWin(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::RoundLength(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::RoundWin => GameEvent::RoundWin(parse_event(raw.params).with_raw(raw)?),
RawEventType::RoundOvertime => GameEvent::RoundOverTime,
RawEventType::LogFileStarted => {
GameEvent::LogFileStarted(parse_event(raw.params).with_type(raw.ty)?)
}
RawEventType::Connected => {
GameEvent::Connected(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::LogFileStarted(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::Connected => GameEvent::Connected(parse_event(raw.params).with_raw(raw)?),
RawEventType::Disconnected => {
GameEvent::Disconnect(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::Disconnect(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::SteamIdValidated => GameEvent::SteamIdValidated,
RawEventType::Entered => GameEvent::Entered,
RawEventType::Joined => GameEvent::Joined(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::Suicide => GameEvent::Suicide(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::PickedUp => {
GameEvent::PickedUp(parse_event(raw.params).with_type(raw.ty)?)
}
RawEventType::Joined => GameEvent::Joined(parse_event(raw.params).with_raw(raw)?),
RawEventType::Suicide => GameEvent::Suicide(parse_event(raw.params).with_raw(raw)?),
RawEventType::PickedUp => GameEvent::PickedUp(parse_event(raw.params).with_raw(raw)?),
RawEventType::Domination => {
GameEvent::Domination(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::Domination(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::EmptyUber => GameEvent::EmptyUber,
RawEventType::Revenge => GameEvent::Revenge(parse_event(raw.params).with_type(raw.ty)?),
RawEventType::Revenge => GameEvent::Revenge(parse_event(raw.params).with_raw(raw)?),
RawEventType::TournamentStart => {
GameEvent::TournamentModeStarted(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::TournamentModeStarted(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::CaptureBlocked => {
GameEvent::CaptureBlocked(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::CaptureBlocked(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::PointCaptured => {
GameEvent::PointCaptured(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::PointCaptured(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::CurrentScore => {
GameEvent::CurrentScore(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::CurrentScore(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::PlayerBuiltObject => {
GameEvent::BuiltObject(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::BuiltObject(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::PlayerKilledObject => {
GameEvent::KilledObject(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::KilledObject(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::PlayerExtinguished => {
GameEvent::Extinguished(parse_event(raw.params).with_type(raw.ty)?)
}
RawEventType::GameOver => {
GameEvent::GameOver(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::Extinguished(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::GameOver => GameEvent::GameOver(parse_event(raw.params).with_raw(raw)?),
RawEventType::FinalScore => {
GameEvent::FinalScore(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::FinalScore(parse_event(raw.params).with_raw(raw)?)
}
RawEventType::LogFileClosed => GameEvent::LogFileClosed,
RawEventType::ObjectDetonated => {
GameEvent::ObjectDetonated(parse_event(raw.params).with_type(raw.ty)?)
GameEvent::ObjectDetonated(parse_event(raw.params).with_raw(raw)?)
}
_ => {
todo!("{:?} not parsed yet", raw.ty);
@ -224,8 +219,7 @@ impl<'a> Iterator for ParamIter<'a> {
fn param_pair_parse(input: &str) -> IResult<'_, (&str, &str)> {
let (input, open_tag) = skip_matches(input, b'(');
let (key, input) = split_once(input, b' ', 0)?;
let input = skip(input, 2)?;
let (key, input) = split_once(input, b' ', 2)?;
let (value, input) = split_once(input, b'"', 1)?;
let input = if open_tag { skip(input, 1)? } else { input };
@ -252,22 +246,16 @@ pub fn param_parse_with<'a, T, P: Fn(&'a str) -> IResult<'a, T>>(
parser: P,
) -> impl Fn(&'a str) -> IResult<'a, T> {
move |input: &str| {
debug_assert!(input.as_bytes()[0] != b' ');
let (input, has_open) = skip_matches(input, b'(');
let input = skip(input, key.len() + 1)?; // skip space + key
let input = skip(input, key.len() + 2)?; // skip space + key + quote
let (input, value) = parser(input)?;
let (value, input) = split_once(input, b'"', 1)?;
let (_, value) = parser(value)?;
let input = skip(input, has_open as usize)?;
debug_assert!(
input.is_empty() || input.as_bytes()[0] == b' ',
"\"{}\" starts with space",
input
);
let input = skip(input, 1).unwrap_or(input);
Ok((input, value))
}
@ -299,15 +287,9 @@ pub trait EventField<'a>: Sized + 'a {
impl<'a> EventField<'a> for &'a str {
fn parse_field(input: &'a str) -> IResult<Self> {
let (input, quoted) = skip_matches(input, b'"');
if quoted {
let (value, input) = split_once(input, b'"', 1)?;
Ok((input, value))
} else {
Ok(("", input))
}
}
}
impl<'a> EventField<'a> for i32 {
fn parse_field(input: &'a str) -> IResult<Self> {
@ -369,7 +351,7 @@ impl<'a> EventField<'a> for Option<NonZeroU32> {
impl<'a> EventField<'a> for RawSubject<'a> {
fn parse_field(input: &'a str) -> IResult<Self> {
against_subject_parser(input)
Ok(("", against_subject_parser(input)?))
}
}

View file

@ -1,5 +1,5 @@
use crate::common::{Class, Team};
use crate::event::{param_parse_with, parse_field, ParamIter};
use crate::event::{param_parse_with, parse_field, quoted, ParamIter};
use crate::raw_event::RawSubject;
use crate::{Error, Event, IResult};
use std::net::SocketAddr;
@ -38,6 +38,7 @@ pub struct KillEvent<'a> {
#[derive(Debug, Event)]
pub struct KillAssistEvent<'a> {
#[event(name = "against")]
pub target: RawSubject<'a>,
pub attacker_position: Option<(i32, i32, i32)>,
pub victim_position: Option<(i32, i32, i32)>,
@ -57,7 +58,7 @@ pub struct RoleChangeEvent {
#[derive(Debug, Event)]
pub struct ConnectedEvent {
#[event(name = "to")]
#[event(name = "address")]
pub address: SocketAddr,
}

View file

@ -10,7 +10,6 @@ pub use event::{Event, EventMeta, GameEvent};
use memchr::memmem::{find_iter, FindIter};
pub use raw_event::{RawEvent, RawEventType};
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::fmt::Debug;
use std::num::ParseIntError;
pub use tf_log_parser_derive::Event;
@ -78,22 +77,9 @@ pub fn parse_with_handler<Handler: EventHandler>(
let raw_event = event_res?;
let should_handle = Handler::does_handle(raw_event.ty);
if should_handle || start_time.is_none() {
let event_time: NaiveDateTime = raw_event.date.try_into().unwrap();
let match_time = match start_time {
Some(start_time) => (event_time - start_time).num_seconds() as u32,
None => {
start_time = Some(event_time);
0
}
};
if should_handle {
let event = GameEvent::parse(&raw_event)?;
let (subject, data) = subjects.insert(&raw_event.subject)?;
let meta = EventMeta {
time: match_time,
subject,
};
handler.handle(&meta, subject, data, &event);
handler.process(&raw_event, &event, &mut start_time, &mut subjects)?;
}
}
}

View file

@ -1,8 +1,9 @@
use crate::common::SubjectId;
use crate::event::{EventMeta, GameEvent};
use crate::raw_event::RawEventType;
use crate::{SubjectData, SubjectMap};
use crate::{Error, RawEvent, SubjectData, SubjectMap};
pub use chat::{ChatMessage, ChatMessages, ChatType};
use chrono::NaiveDateTime;
pub use classstats::{ClassStats, ClassStatsHandler};
pub use healspread::HealSpread;
pub use lobbysettings::{
@ -10,6 +11,7 @@ pub use lobbysettings::{
};
pub use medicstats::{MedicStats, MedicStatsBuilder};
use serde::Serialize;
use std::convert::TryInto;
use std::marker::PhantomData;
mod chat;
@ -25,6 +27,30 @@ pub trait EventHandler: Default {
fn does_handle(ty: RawEventType) -> bool;
fn process(
&mut self,
raw_event: &RawEvent,
event: &GameEvent,
start_time: &mut Option<NaiveDateTime>,
subjects: &mut SubjectMap<Self::PerSubjectData>,
) -> Result<(), Error> {
let event_time: NaiveDateTime = raw_event.date.try_into().unwrap();
let match_time = match start_time {
Some(start_time) => (event_time - *start_time).num_seconds() as u32,
None => {
*start_time = Some(event_time);
0
}
};
let (subject, data) = subjects.insert(&raw_event.subject)?;
let meta = EventMeta {
time: match_time,
subject,
};
self.handle(&meta, subject, data, &event);
Ok(())
}
fn handle(
&mut self,
meta: &EventMeta,

View file

@ -1,5 +1,5 @@
use crate::common::{split_once, Team};
use crate::{Error, IResult, Result};
use crate::{Error, Result};
use crate::{SubjectError, SubjectId};
use chrono::{NaiveDate, NaiveDateTime};
use logos::{Lexer, Logos};
@ -109,8 +109,13 @@ fn test_split_player_subject() {
)
}
pub fn against_subject_parser(input: &str) -> IResult<RawSubject> {
subject_parser(input).map_err(|_| Error::Incomplete)
pub fn against_subject_parser(input: &str) -> Result<RawSubject> {
// "against" fields are always players, and unquoted
if input.ends_with("e>") {
Ok(RawSubject::Console)
} else {
Ok(RawSubject::Player(input))
}
}
pub fn subject_parser(input: &str) -> Result<(&str, RawSubject)> {