Наш первый netchan-пакет был на 22 байта короче эталонного — ровно на cl_playermodel\0male07\0. Без него сервер не знает, какую модель ставить нашему энтити-игроку. Набор конваров вынесен в client::userinfo_pairs() и теперь сверяется с эталоном настоящего GMod (новая фикстура fixtures/classicbox_client_hello.bin, 511 байт из пакета #24) попарно, включая порядок. При том же имени наш пакет теперь совпадает с эталонным побайтово: тело идентично в 513 из 514 байт, различаются только checksum/challenge из-за другой сессии и один остаточный бит добивки.
145 lines
5.8 KiB
Rust
145 lines
5.8 KiB
Rust
//! Набор userinfo-конваров должен совпадать с настоящим клиентом GMod.
|
||
//!
|
||
//! Фикстур `fixtures/classicbox_client_hello.bin` — 511 байт, которые реальный
|
||
//! клиент GMod положил в первый netchan-пакет при подключении к ClassicBox
|
||
//! (пакет #24 в `real_client.pcap`). Это эталон: сервер формирует userinfo
|
||
//! игрока именно из этих пар, и по полноте набора видно, чего клиенту не
|
||
//! хватает.
|
||
//!
|
||
//! Тест ловит именно ту ошибку, которая была: мы не отправляли `cl_playermodel`,
|
||
//! и наш пакет был на 22 байта короче эталонного.
|
||
|
||
use net::bitbuf::{BitReader, BitWriter};
|
||
use net::client::userinfo_pairs;
|
||
use net::netchan::{ClientMessage, Message, parse_messages};
|
||
|
||
/// Полезная нагрузка первого netchan-пакета настоящего GMod.
|
||
const REFERENCE: &[u8] = include_bytes!("fixtures/classicbox_client_hello.bin");
|
||
|
||
/// Раскладывает `NET_SetConVar` из блока сообщений в список пар.
|
||
fn convars_of(block: &[u8]) -> Vec<(String, String)> {
|
||
let msgs = parse_messages(block, 0);
|
||
for m in &msgs {
|
||
if let Message::SetConVar(pairs) = m {
|
||
return pairs.clone();
|
||
}
|
||
}
|
||
panic!("в блоке нет NET_SetConVar");
|
||
}
|
||
|
||
/// Собирает наш собственный hello так же, как это делает клиент при подключении.
|
||
fn our_hello_convars() -> Vec<(String, String)> {
|
||
let mut w = BitWriter::new();
|
||
ClientMessage::SetConVar(userinfo_pairs("rpopre_apovle")).write_for_test(&mut w);
|
||
let bytes = w.bytes().to_vec();
|
||
convars_of(&bytes)
|
||
}
|
||
|
||
#[test]
|
||
fn reference_hello_parses() {
|
||
// Фикстура должна читаться: ровно SetConVar и SignonState.
|
||
let msgs = parse_messages(REFERENCE, 0);
|
||
let kinds: Vec<&str> = msgs
|
||
.iter()
|
||
.filter(|m| !matches!(m, Message::Nop))
|
||
.map(|m| match m {
|
||
Message::SetConVar(_) => "SetConVar",
|
||
Message::SignOnState { .. } => "SignonState",
|
||
other => panic!("неожиданное сообщение {other:?}"),
|
||
})
|
||
.collect();
|
||
assert_eq!(
|
||
kinds,
|
||
vec!["SetConVar", "SignonState"],
|
||
"эталонный hello должен состоять из SetConVar и SignonState"
|
||
);
|
||
|
||
let pairs = convars_of(REFERENCE);
|
||
assert_eq!(pairs.len(), 28, "эталон содержит 28 пар");
|
||
assert_eq!(pairs[0].0, "tv_nochat");
|
||
assert_eq!(
|
||
pairs.last().unwrap().0,
|
||
"cl_autowepswitch",
|
||
"последняя пара эталонного набора"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn reference_hello_signon_state_is_connected() {
|
||
let msgs = parse_messages(REFERENCE, 0);
|
||
let state = msgs
|
||
.iter()
|
||
.find_map(|m| match m {
|
||
Message::SignOnState { state, spawn_count } => Some((*state, *spawn_count)),
|
||
_ => None,
|
||
})
|
||
.expect("SignonState");
|
||
assert_eq!(state.0, 2, "CONNECTED");
|
||
assert_eq!(state.1, u32::MAX, "spawn_count = -1");
|
||
}
|
||
|
||
#[test]
|
||
fn our_convars_match_reference_exactly() {
|
||
let reference = convars_of(REFERENCE);
|
||
let ours = our_hello_convars();
|
||
|
||
assert_eq!(
|
||
ours.len(),
|
||
reference.len(),
|
||
"число userinfo-конваров: у нас {}, в эталоне {}\nнабор: {:?}",
|
||
ours.len(),
|
||
reference.len(),
|
||
ours
|
||
);
|
||
|
||
for (i, (ours, refpair)) in ours.iter().zip(reference.iter()).enumerate() {
|
||
// `name` зависит от того, под кем мы заходим, — сверяем отдельно.
|
||
if ours.0 == "name" {
|
||
assert_eq!(refpair.0, "name", "позиция {i}: в эталоне должен быть name");
|
||
assert_eq!(ours.1, "rpopre_apovle");
|
||
continue;
|
||
}
|
||
assert_eq!(
|
||
(ours.0.as_str(), ours.1.as_str()),
|
||
(refpair.0.as_str(), refpair.1.as_str()),
|
||
"конвар №{i} разошёлся с эталоном"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn no_duplicate_convars() {
|
||
// Сервер принимает дубликаты молча, но лишние userinfo-пары ломают
|
||
// отображение в браузере сервера — проверяем уникальность имён.
|
||
let ours = our_hello_convars();
|
||
let mut names: Vec<&str> = ours.iter().map(|(k, _)| k.as_str()).collect();
|
||
let count = names.len();
|
||
names.sort_unstable();
|
||
names.dedup();
|
||
assert_eq!(names.len(), count, "в наборе есть повторяющиеся имена");
|
||
}
|
||
|
||
#[test]
|
||
fn convar_values_fit_protocol_limits() {
|
||
// Имя и значение ограничены 260 байтами (см. NET_SetConVar в Source.NET),
|
||
// а их не больше 255 в одном сообщении.
|
||
let ours = our_hello_convars();
|
||
assert!(ours.len() <= 255, "конваров больше 255");
|
||
for (k, v) in &ours {
|
||
assert!(k.len() < 260, "имя конвара длиннее 260 байт: {k}");
|
||
assert!(v.len() < 260, "значение длиннее 260 байт: {k}");
|
||
assert!(
|
||
k.bytes().all(|b| b != 0),
|
||
"в имени конвара нуль-байт: {k:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn reader_stays_inside_block() {
|
||
// Sanity: разбор эталонного блока не выходит за границы буфера.
|
||
let mut r = BitReader::new(REFERENCE);
|
||
assert!(r.bits_left() == REFERENCE.len() * 8);
|
||
let _ = r.read_ubit_long(6);
|
||
assert!(r.bits_left() < REFERENCE.len() * 8);
|
||
}
|