1
0
Fork 0
mirror of https://codeberg.org/demostf/parser.git synced 2026-10-01 08:44:05 +02:00
parser/tests/reencode.rs
Peter 7c5c596d17 Use the demo's protocol version when reencoding
DemoHandler::default() starts out at protocol 24 and relies on handle_header
to replace that with whatever the demo actually is. Parsing through DemoParser
does this for you, but reencode drives the handler itself and never did, so
every demo got read as protocol 24.

Older protocols put a few fields at different widths, so the message stream
desyncs partway in. RawPacketStream reports that as a truncated demo rather
than an error, which is why nothing complains about it: reencode_demo on
test_data/protocol23.dem writes 1076 bytes, prints a truncation warning and
still exits 0.

The reencode test missed this because protocol23.dem was never in its list,
and because nothing checked the demo had been read to the end. Add a test for
that and the same assert to the existing round trip.

Output for protocol 24 demos is unchanged, byte for byte.
2026-08-31 11:14:15 +01:00

186 lines
8 KiB
Rust

#[cfg(feature = "write")]
use std::fs;
#[cfg(feature = "write")]
use test_case::test_case;
#[cfg(feature = "write")]
use bitbuffer::{BitRead, BitReadBuffer, BitReadStream, BitWrite, BitWriteStream, LittleEndian};
#[cfg(feature = "write")]
use std::collections::HashMap;
#[cfg(feature = "write")]
use tf_demo_parser::demo::header::Header;
#[cfg(feature = "write")]
use tf_demo_parser::demo::message::Message;
#[cfg(feature = "write")]
use tf_demo_parser::demo::packet::datatable::SendTableName;
#[cfg(feature = "write")]
use tf_demo_parser::demo::packet::Packet;
#[cfg(feature = "write")]
use tf_demo_parser::demo::parser::{DemoHandler, Encode, NullHandler, RawPacketStream};
#[cfg(feature = "write")]
use tf_demo_parser::demo::sendprop::{RawSendPropDefinition, SendPropIdentifier, SendPropName};
#[cfg(feature = "write")]
use tf_demo_parser::{Demo, Parse};
#[cfg(feature = "write")]
#[test_case("test_data/small.dem"; "small.dem")]
#[test_case("test_data/gully.dem"; "gully.dem")]
#[test_case("test_data/comp.dem"; "comp.dem")]
#[test_case("test_data/malformed_cvar.dem"; "malformed_cvar.dem")]
#[test_case("test_data/unicode-saytext.dem"; "unicode-saytext.dem")]
#[test_case("test_data/nousers.dem"; "nousers.dem")]
#[test_case("test_data/decal.dem"; "decal.dem")]
#[test_case("test_data/saytext2.dem"; "saytext2.dem")]
#[test_case("test_data/emptysaytext.dem"; "emptysaytext.dem")]
fn re_encode_test(input_file: &str) {
let file = fs::read(input_file).expect("Unable to read file");
let demo = Demo::new(&file);
let mut out_buffer = Vec::with_capacity(file.len());
let mut stream = demo.get_stream();
let header = Header::read(&mut stream).unwrap();
let header_size = stream.pos() / 8;
assert_eq!(1072, header_size);
let mut packets = RawPacketStream::new(stream);
let mut handler = DemoHandler::parse_all_with_analyser(NullHandler);
handler.handle_header(&header);
{
let mut out_stream = BitWriteStream::new(&mut out_buffer, LittleEndian);
header.write(&mut out_stream).unwrap();
}
assert_eq!(file[0..header_size], out_buffer);
let mut prop_names: HashMap<
SendPropIdentifier,
(SendTableName, SendPropName, RawSendPropDefinition),
> = HashMap::new();
while let Some(packet) = packets.next(&handler.state_handler).unwrap() {
if let Packet::DataTables(data_table) = &packet {
for table in data_table.tables.iter() {
for prop_def in &table.props {
prop_names.insert(
prop_def.identifier(),
(table.name.clone(), prop_def.name.clone(), prop_def.clone()),
);
}
}
}
out_buffer.clear();
{
let mut out_stream = BitWriteStream::new(&mut out_buffer, LittleEndian);
packet
.encode(&mut out_stream, &handler.state_handler)
.unwrap();
}
let mut re_read = BitReadStream::new(BitReadBuffer::new(&out_buffer, LittleEndian));
let re_decoded = Packet::parse(&mut re_read, &handler.state_handler)
.unwrap_or_else(|_| panic!("while parsing {:?}", packet.packet_type()));
assert_eq!(packet.packet_type(), re_decoded.packet_type());
match (&packet, &re_decoded) {
(
Packet::Message(msg) | Packet::Signon(msg),
Packet::Message(re_msg) | Packet::Signon(re_msg),
) => {
assert_eq!(msg.tick, re_msg.tick);
assert_eq!(msg.meta, re_msg.meta);
assert_eq!(
msg.messages
.iter()
.map(|msg| msg.get_message_type())
.collect::<Vec<_>>(),
re_msg
.messages
.iter()
.map(|msg| msg.get_message_type())
.collect::<Vec<_>>()
);
assert_eq!(msg.messages.len(), re_msg.messages.len());
for (msg, re_msg) in msg.messages.iter().zip(re_msg.messages.iter()) {
assert_eq!(msg.get_message_type(), re_msg.get_message_type());
match (msg, re_msg) {
(Message::PacketEntities(msg), Message::PacketEntities(re_msg)) => {
assert_eq!(msg.updated_base_line, re_msg.updated_base_line);
assert_eq!(msg.base_line, re_msg.base_line);
assert_eq!(msg.delta, re_msg.delta);
assert_eq!(msg.max_entries, re_msg.max_entries);
assert_eq!(msg.removed_entities, re_msg.removed_entities);
assert_eq!(msg.entities.len(), re_msg.entities.len());
for (ent, re_ent) in msg.entities.iter().zip(re_msg.entities.iter()) {
let props = ent
.props
.iter()
.map(|prop| {
(
prop_names.get(&prop.identifier).unwrap(),
prop.value.clone(),
)
})
.collect::<Vec<_>>();
let re_props = re_ent
.props
.iter()
.map(|prop| {
(
prop_names.get(&prop.identifier).unwrap(),
prop.value.clone(),
)
})
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(props, re_props);
}
}
(msg, re_msg) => assert_eq!(msg, re_msg),
}
}
}
(Packet::StringTables(packet), Packet::StringTables(re_packet)) => {
assert_eq!(packet.tick, re_packet.tick);
assert_eq!(packet.tables.len(), re_packet.tables.len());
for (table, re_table) in packet.tables.iter().zip(re_packet.tables.iter()) {
assert_eq!(table, re_table);
}
}
(packet, re_decoded) => {
assert_eq!(packet, re_decoded);
}
}
handler.handle_packet(packet).unwrap();
}
// a desync shows up as the packet loop quietly stopping early, so the
// round trip above passes while most of the demo is never looked at
assert!(!packets.incomplete, "{} was not read to the end", input_file);
}
/// A demo recorded on an older protocol must be parsed with that protocol, not
/// the protocol 24 that `ParserState` starts out with. Getting it wrong shifts
/// the width of a handful of fields (`ServerInfo`'s map hash, `PreFetch`), the
/// message stream desyncs, and `RawPacketStream` quietly stops early rather
/// than returning an error - so this checks the whole demo was actually read.
#[cfg(feature = "write")]
#[test]
fn parse_with_header_protocol() {
let file = fs::read("test_data/protocol23.dem").expect("Unable to read file");
let demo = Demo::new(&file);
let mut stream = demo.get_stream();
let header = Header::read(&mut stream).unwrap();
assert_eq!(23, header.protocol);
let mut packets = RawPacketStream::new(stream);
let mut handler = DemoHandler::parse_all_with_analyser(NullHandler);
handler.handle_header(&header);
let mut count = 0;
while let Some(packet) = packets.next(&handler.state_handler).unwrap() {
handler.handle_packet(packet).unwrap();
count += 1;
}
assert!(!packets.incomplete, "demo was not read to the end");
assert_eq!(128235, count);
}