use std::convert::TryFrom;
use std::io::Cursor;
use std::io::ErrorKind;
use std::io::Read;
use std::io::Write;
use std::net::ToSocketAddrs;
#[cfg(feature = "serde")]
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;
use bstr::BString;
use bstr::ByteSlice;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use byteorder::WriteBytesExt;
use crate::AppId;
use crate::Client;
use crate::GameId;
use crate::HEADER_CHALLENGE;
use crate::HEADER_INFO;
use crate::ReadCString;
use crate::SteamId;
use crate::errors::Error;
use crate::errors::Result;
#[doc(hidden)]
pub const INFO_REQUEST: [u8; 25] = [
0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x53, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x20, 0x45, 0x6E, 0x67, 0x69,
0x6E, 0x65, 0x20, 0x51, 0x75, 0x65, 0x72, 0x79, 0x00,
];
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct TheShip {
pub mode: TheShipMode,
pub witnesses: u8,
pub duration: u8,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum TheShipMode {
Hunt = 0,
Elimination = 1,
Duel = 2,
Deathmatch = 3,
VIPTeam = 4,
TeamElimination = 5,
Unknown = 255,
}
impl From<u8> for TheShipMode {
fn from(v: u8) -> Self {
match v {
0 => Self::Hunt,
1 => Self::Elimination,
2 => Self::Duel,
3 => Self::Deathmatch,
4 => Self::VIPTeam,
5 => Self::TeamElimination,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct ExtendedServerInfo {
pub port: Option<u16>,
pub steam_id: Option<SteamId>,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_option_bstring))]
pub keywords: Option<BString>,
pub game_id: Option<GameId>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct SourceTVInfo {
pub port: u16,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
pub name: BString,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum ServerType {
Dedicated = b'd',
NonDedicated = b'i',
SourceTV = b'p',
}
impl TryFrom<u8> for ServerType {
type Error = Error;
fn try_from(val: u8) -> Result<Self> {
match val {
b'd' => Ok(Self::Dedicated),
b'i' => Ok(Self::NonDedicated),
b'p' => Ok(Self::SourceTV),
other => Err(Self::Error::UnexpectedHeader {
expected: b'd',
actual: other,
}),
}
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum ServerOS {
Linux = b'l',
Windows = b'w',
Mac = b'm',
}
impl TryFrom<u8> for ServerOS {
type Error = Error;
fn try_from(val: u8) -> Result<Self> {
match val {
b'l' => Ok(Self::Linux),
b'w' => Ok(Self::Windows),
b'm' | b'o' => Ok(Self::Mac),
other => Err(Self::Error::UnexpectedHeader {
expected: b'l',
actual: other,
}),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct Info {
pub protocol: u8,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
pub name: BString,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
pub map: BString,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
pub folder: BString,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
pub game: BString,
pub app_id: AppId,
pub players: u8,
pub max_players: u8,
pub bots: u8,
pub server_type: ServerType,
pub server_os: ServerOS,
pub visibility: bool,
pub vac: bool,
pub the_ship: Option<TheShip>,
#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
pub version: BString,
pub edf: u8,
pub extended_server_info: ExtendedServerInfo,
pub source_tv: Option<SourceTVInfo>,
}
impl Info {
pub fn size_hint(&self) -> usize {
let mut size = 5
+ 1
+ self.name.len()
+ 1
+ self.map.len()
+ 1
+ self.folder.len()
+ 1
+ self.game.len()
+ 1
+ 2
+ 1
+ 1
+ 1
+ 1
+ 1
+ 1
+ 1
+ self.version.len()
+ 1;
if self.the_ship.is_some() {
size += 3;
}
if self.edf != 0 {
size += 1; }
if self.extended_server_info.port.is_some() {
size += 2;
}
if self.extended_server_info.steam_id.is_some() {
size += 8;
}
if let Some(keywords) = &self.extended_server_info.keywords {
size += keywords.len() + 1;
}
if self.extended_server_info.game_id.is_some() {
size += 8;
}
if let Some(source_tv) = &self.source_tv {
size += 2 + source_tv.name.len() + 1;
}
size
}
pub fn write<W: Write>(&self, mut w: W) -> Result<()> {
w.write_all(&[0xff, 0xff, 0xff, 0xff, HEADER_INFO])?;
w.write_all(&[self.protocol])?;
w.write_all(self.name.as_bytes())?;
w.write_all(&[0])?;
w.write_all(self.map.as_bytes())?;
w.write_all(&[0])?;
w.write_all(self.folder.as_bytes())?;
w.write_all(&[0])?;
w.write_all(self.game.as_bytes())?;
w.write_all(&[0])?;
w.write_all(&self.app_id.0.to_le_bytes())?;
w.write_all(&[self.players, self.max_players, self.bots])?;
w.write_all(&[self.server_type as u8])?;
w.write_all(&[self.server_os as u8])?;
w.write_all(&[if self.visibility { 1 } else { 0 }])?;
w.write_all(&[if self.vac { 1 } else { 0 }])?;
if let Some(the_ship) = &self.the_ship {
w.write_all(&[the_ship.mode as u8, the_ship.witnesses, the_ship.duration])?;
}
w.write_all(self.version.as_bytes())?;
w.write_all(&[0])?;
if self.edf != 0 {
w.write_all(&[self.edf])?;
}
if let Some(port) = &self.extended_server_info.port {
w.write_all(&port.to_le_bytes())?;
}
if let Some(steam_id) = &self.extended_server_info.steam_id {
w.write_all(&steam_id.0.to_le_bytes())?;
}
if let Some(source_tv) = &self.source_tv {
w.write_all(&source_tv.port.to_le_bytes())?;
w.write_all(source_tv.name.as_bytes())?;
w.write_all(&[0])?;
}
if let Some(keywords) = &self.extended_server_info.keywords {
w.write_all(keywords.as_bytes())?;
w.write_all(&[0])?;
}
if let Some(game_id) = &self.extended_server_info.game_id {
w.write_all(&game_id.0.to_le_bytes())?;
}
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.size_hint());
self.write(&mut bytes)
.expect("writing to Vec should not fail");
bytes
}
#[deprecated(since = "0.6.2", note = "use from_reader")]
pub fn from_cursor(data: Cursor<Vec<u8>>) -> Result<Self> {
Self::from_reader(data)
}
pub fn from_reader<R: Read>(mut data: R) -> Result<Self> {
let header = data.read_u8()?;
if header != HEADER_INFO {
return Err(Error::UnexpectedHeader {
expected: HEADER_INFO,
actual: header,
});
}
let protocol = data.read_u8()?;
let name = data.read_cstring()?;
let map = data.read_cstring()?;
let folder = data.read_cstring()?;
let game = data.read_cstring()?;
let app_id = AppId(data.read_u16::<LittleEndian>()?);
let players = data.read_u8()?;
let max_players = data.read_u8()?;
let bots = data.read_u8()?;
let server_type = ServerType::try_from(data.read_u8()?)?;
let server_os = ServerOS::try_from(data.read_u8()?)?;
let visibility = read_bool(&mut data, "visibility")?;
let vac = read_bool(&mut data, "vac")?;
let the_ship = if app_id == AppId::THE_SHIP {
Some(TheShip {
mode: TheShipMode::from(data.read_u8()?),
witnesses: data.read_u8()?,
duration: data.read_u8()?,
})
} else {
None
};
let version = data.read_cstring()?;
let edf = match data.read_u8() {
Ok(val) => val,
Err(err) => {
if err.kind() != ErrorKind::UnexpectedEof {
return Err(Error::Io(err));
} else {
0
}
}
};
let port = if edf & 0x80 != 0 {
Some(data.read_u16::<LittleEndian>()?)
} else {
None
};
let steam_id = if edf & 0x10 != 0 {
Some(SteamId(data.read_u64::<LittleEndian>()?))
} else {
None
};
let source_tv = if edf & 0x40 != 0 {
Some(SourceTVInfo {
port: data.read_u16::<LittleEndian>()?,
name: data.read_cstring()?,
})
} else {
None
};
let keywords = if edf & 0x20 != 0 {
Some(data.read_cstring()?)
} else {
None
};
let game_id = if edf & 0x01 != 0 {
Some(GameId(data.read_u64::<LittleEndian>()?))
} else {
None
};
let extended_server_info = ExtendedServerInfo {
port,
steam_id,
keywords,
game_id,
};
Ok(Info {
protocol,
name,
map,
folder,
game,
app_id,
players,
max_players,
bots,
server_type,
server_os,
visibility,
vac,
the_ship,
version,
edf,
extended_server_info,
source_tv,
})
}
}
fn read_bool<R: Read>(data: &mut R, field: &'static str) -> Result<bool> {
let value = data.read_u8()?;
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(Error::InvalidBool { field, value }),
}
}
impl Client {
pub fn info<A: ToSocketAddrs>(&self, addr: A) -> Result<Info> {
let response = self.send(&INFO_REQUEST, &addr)?;
let mut packet = Cursor::new(&response);
let header = packet.read_u8()?;
if header == HEADER_CHALLENGE {
let challenge = packet.read_i32::<LittleEndian>()?;
let mut query = Vec::with_capacity(29);
query.write_all(&INFO_REQUEST)?;
query.write_i32::<LittleEndian>(challenge)?;
let data = self.send(&query, addr)?;
Info::from_reader(data.as_slice())
} else {
Info::from_reader(response.as_slice())
}
}
}