rust/hedgewars-engine-messages/src/parser.rs
author unC0Rr
Fri, 06 Oct 2023 22:37:44 +0200
changeset 16011 cf580d9ff7ef
parent 15831 c3971b38bbfa
permissions -rw-r--r--
Modernize engine protocol parser

use std::str;

use nom::branch::alt;
use nom::bytes::streaming::*;
use nom::combinator::*;
use nom::error::{ErrorKind, ParseError};
use nom::multi::*;
use nom::number::streaming::*;
use nom::sequence::{pair, preceded, terminated, tuple};
use nom::{Err, IResult, Parser};

use crate::messages::{
    ConfigEngineMessage::*, EngineMessage::*, KeystrokeAction::*, SyncedEngineMessage::*,
    UnorderedEngineMessage::*, *,
};

fn eof_slice<I>(i: I) -> IResult<I, I>
where
    I: nom::InputLength + Clone,
{
    if i.input_len() == 0 {
        Ok((i.clone(), i))
    } else {
        Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)))
    }
}
fn unrecognized_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
    map(rest, |i: &[u8]| Unknown(i.to_owned()))(input)
}

fn string_tail(input: &[u8]) -> IResult<&[u8], String> {
    map_res(rest, str::from_utf8)(input).map(|(i, s)| (i, s.to_owned()))
}

fn length_without_timestamp(input: &[u8]) -> IResult<&[u8], usize> {
    map_opt(rest_len, |l| if l > 2 { Some(l - 2) } else { None })(input)
}

fn synced_message(input: &[u8]) -> IResult<&[u8], SyncedEngineMessage> {
    alt((
        alt((
            map(tag(b"L"), |_| Left(Press)),
            map(tag(b"l"), |_| Left(Release)),
            map(tag(b"R"), |_| Right(Press)),
            map(tag(b"r"), |_| Right(Release)),
            map(tag(b"U"), |_| Up(Press)),
            map(tag(b"u"), |_| Up(Release)),
            map(tag(b"D"), |_| Down(Press)),
            map(tag(b"d"), |_| Down(Release)),
            map(tag(b"Z"), |_| Precise(Press)),
            map(tag(b"z"), |_| Precise(Release)),
            map(tag(b"A"), |_| Attack(Press)),
            map(tag(b"a"), |_| Attack(Release)),
            map(tag(b"N"), |_| NextTurn),
            map(tag(b"j"), |_| LongJump),
            map(tag(b"J"), |_| HighJump),
            map(tag(b"S"), |_| Switch),
        )),
        alt((
            map(tag(b","), |_| Skip),
            map(tag(b"1"), |_| Timer(1)),
            map(tag(b"2"), |_| Timer(2)),
            map(tag(b"3"), |_| Timer(3)),
            map(tag(b"4"), |_| Timer(4)),
            map(tag(b"5"), |_| Timer(5)),
            map(tuple((tag(b"p"), be_i24, be_i24)), |(_, x, y)| Put(x, y)),
            map(tuple((tag(b"P"), be_i24, be_i24)), |(_, x, y)| {
                CursorMove(x, y)
            }),
            map(preceded(tag(b"f"), string_tail), TeamControlLost),
            map(preceded(tag(b"g"), string_tail), TeamControlGained),
            map(preceded(tag(b"t"), be_u8), Taunt),
            map(preceded(tag(b"w"), be_u8), SetWeapon),
            map(preceded(tag(b"~"), be_u8), Slot),
            map(tag(b"+"), |_| Heartbeat),
        )),
    ))(input)
}

fn unsynced_message(input: &[u8]) -> IResult<&[u8], UnsyncedEngineMessage> {
    alt((
        map(
            preceded(tag(b"F"), string_tail),
            UnsyncedEngineMessage::TeamControlLost,
        ),
        map(
            preceded(tag(b"G"), string_tail),
            UnsyncedEngineMessage::TeamControlGained,
        ),
        map(
            preceded(tag(b"h"), string_tail),
            UnsyncedEngineMessage::HogSay,
        ),
        map(
            preceded(tag(b"s"), string_tail),
            UnsyncedEngineMessage::ChatMessage,
        ),
        map(
            preceded(tag(b"b"), string_tail),
            UnsyncedEngineMessage::TeamMessage,
        ),
    ))(input)
}

fn unordered_message(input: &[u8]) -> IResult<&[u8], UnorderedEngineMessage> {
    alt((
        map(tag(b"?"), |_| Ping),
        map(tag(b"!"), |_| Pong),
        map(preceded(tag(b"E"), string_tail), Error),
        map(preceded(tag(b"W"), string_tail), Warning),
        map(preceded(tag(b"M"), string_tail), GameSetupChecksum),
        map(tag(b"o"), |_| StopSyncing),
        map(tag(b"I"), |_| PauseToggled),
    ))(input)
}

fn config_message(input: &[u8]) -> IResult<&[u8], ConfigEngineMessage> {
    alt((
        map(tag(b"C"), |_| ConfigRequest),
        map(preceded(tag(b"eseed "), string_tail), SetSeed),
        map(preceded(tag(b"e$feature_size "), string_tail), |s| {
            SetFeatureSize(s.parse().unwrap_or_default())
        }),
    ))(input)
}

fn timestamped_message(input: &[u8]) -> IResult<&[u8], (SyncedEngineMessage, u16)> {
    terminated(pair(synced_message, be_u16), eof_slice)(input)
}
fn unwrapped_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
    alt((
        map(timestamped_message, |(m, t)| {
            EngineMessage::Synced(m, t as u32)
        }),
        map(tag(b"#"), |_| Synced(TimeWrap, 65535u32)),
        map(unordered_message, Unordered),
        map(unsynced_message, Unsynced),
        map(config_message, Config),
        unrecognized_message,
    ))(input)
}

fn length_specifier(input: &[u8]) -> IResult<&[u8], u16> {
    alt((
        verify(map(take(1usize), |a: &[u8]| a[0] as u16), |&l| l < 64),
        map(take(2usize), |a: &[u8]| {
            (a[0] as u16 - 64) * 256 + a[1] as u16 + 64
        }),
    ))(input)
}

fn empty_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
    map(tag(b"\0"), |_| Empty)(input)
}

fn non_empty_message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
    map_parser(length_data(length_specifier), unwrapped_message)(input)
}

fn message(input: &[u8]) -> IResult<&[u8], EngineMessage> {
    alt((empty_message, non_empty_message))(input)
}

pub fn extract_messages(input: &[u8]) -> IResult<&[u8], Vec<EngineMessage>> {
    many0(complete(message))(input)
}

pub fn extract_message(buf: &[u8]) -> Option<(usize, EngineMessage)> {
    let parse_result = message(buf);
    match parse_result {
        Ok((tail, msg)) => {
            let consumed = buf.len() - tail.len();

            Some((consumed, msg))
        }
        Err(Err::Incomplete(_)) => None,
        Err(Err::Error(_)) | Err(Err::Failure(_)) => unreachable!(),
    }
}

#[cfg(test)]
mod tests {
    use crate::messages::UnsyncedEngineMessage::*;
    use crate::parser::*;

    #[test]
    fn parse_length() {
        assert_eq!(length_specifier(b"\x01"), Ok((&b""[..], 1)));
        assert_eq!(length_specifier(b"\x00"), Ok((&b""[..], 0)));
        assert_eq!(length_specifier(b"\x3f"), Ok((&b""[..], 63)));
        assert_eq!(length_specifier(b"\x40\x00"), Ok((&b""[..], 64)));
        assert_eq!(
            length_specifier(b"\xff\xff"),
            Ok((&b""[..], EngineMessage::MAX_LEN))
        );
    }

    #[test]
    fn parse_synced_messages() {
        assert_eq!(
            message(b"\x03L\x01\x02"),
            Ok((&b""[..], Synced(Left(Press), 258)))
        );

        assert_eq!(message(b"\x01#"), Ok((&b""[..], Synced(TimeWrap, 65535))));

        assert_eq!(
            message(&vec![9, b'p', 255, 133, 151, 1, 0, 2, 0, 0]),
            Ok((&b""[..], Synced(Put(-31337, 65538), 0)))
        );
    }

    #[test]
    fn parse_unsynced_messages() {
        assert_eq!(
            message(b"\x06shello"),
            Ok((&b""[..], Unsynced(ChatMessage(String::from("hello")))))
        );
    }

    #[test]
    fn parse_incorrect_messages() {
        assert_eq!(message(b"\x00"), Ok((&b""[..], Empty)));
        assert_eq!(message(b"\x01\x00"), Ok((&b""[..], Unknown(vec![0]))));

        // garbage after correct message
        assert_eq!(
            message(b"\x04La\x01\x02"),
            Ok((&b""[..], Unknown(vec![76, 97, 1, 2])))
        );
    }

    #[test]
    fn parse_config_messages() {
        assert_eq!(message(b"\x01C"), Ok((&b""[..], Config(ConfigRequest))));
    }

    #[test]
    fn parse_test_general() {
        assert_eq!(string_tail(b"abc"), Ok((&b""[..], String::from("abc"))));

        assert_eq!(extract_message(b"\x02#"), None);

        assert_eq!(synced_message(b"L"), Ok((&b""[..], Left(Press))));

        assert_eq!(
            extract_message(b"\x01#"),
            Some((2, Synced(TimeWrap, 65535)))
        );
    }
}