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 56f0856441 Write protocol dependent lengths when encoding
CreateStringTable and TempEntities both read their payload length as a fixed
width field on protocol 23 and older and as a var int after that, but the
encoders always write the var int. Reencoding an older demo therefore hands
the game a length it can't read: string tables come back as "bogus substring
index", and any packet carrying temp entities is malformed from there on,
taking whatever messages follow it in that packet with it.

The threshold and both widths are taken from the read paths a few lines up in
these same two files rather than worked out separately, so the two sides can't
drift: > 23, with 20 bits for CreateStringTable and 17 for TempEntities. The
engine agrees, for what it's worth - netmessages.cpp reads both of these under
> PROTOCOL_VERSION_23 at NET_MAX_PAYLOAD_BITS_V23 + 3 and
NET_MAX_PAYLOAD_BITS_V23 respectively, and that constant is 17.

serverinfo and prefetch already branch on the protocol on both sides, these
two were the only ones that didn't.

On a protocol 15 demo that was 28909 of 92518 reencoded packets failing to
parse back; on test_data/protocol23.dem, 13220 of 43327. Both are zero now,
so protocol23.dem can join the reencode round trip test. Protocol 24 output
is unchanged, byte for byte.
2026-08-31 11:58:21 +01:00

187 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")]
#[test_case("test_data/protocol23.dem"; "protocol23.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);
}