use std::io::{BufRead, Cursor};
use std::result;
use std::time::Duration;
use anyhow::{anyhow as e, Result};
use quake_text::bytestr::to_unicode;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct MvdsvInfo {
pub settings: quake_serverinfo::Settings,
pub clients: Vec<MvdsvClient>,
pub qtv_stream: Option<QtvStream>,
}
impl MvdsvInfo {
pub fn try_from_status_response(response: &[u8]) -> Result<Self> {
let expected_header = vec![255, 255, 255, 255, 110];
if !response.starts_with(&expected_header) {
return Err(e!("Invalid header"));
}
let body = &response[expected_header.len()..];
let rows: Vec<Vec<u8>> = Cursor::new(body).split(10).filter_map(|l| l.ok()).collect();
const MIN_LENGTH: usize = "hostname\\x".len();
if rows.is_empty() || rows[0].len() < MIN_LENGTH {
return Err(e!("Invalid body"));
}
let settings = quake_serverinfo::Settings::from(rows[0].as_slice());
let mut qtv_stream = None;
let mut clients: Vec<MvdsvClient> = vec![];
for row in rows {
if row.starts_with(b"qtv ") {
qtv_stream = QtvStream::try_from(row.as_slice()).ok();
} else if let Ok(client) = MvdsvClient::try_from(row.as_slice()) {
clients.push(client);
}
}
Ok(Self {
settings,
clients,
qtv_stream,
})
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct MvdsvClient {
pub id: u32,
pub name: String,
pub team: String,
pub frags: i32,
pub ping: u32,
pub time: u32,
pub top_color: u8,
pub bottom_color: u8,
pub skin: String,
pub is_spectator: bool,
}
impl TryFrom<&[u8]> for MvdsvClient {
type Error = anyhow::Error;
fn try_from(bytes: &[u8]) -> result::Result<Self, Self::Error> {
let parts: Vec<String> = tokenize(to_unicode(bytes).as_str());
let id: u32 = parts[0].parse()?;
let mut frags: i32 = parts[1].parse()?;
let time: u32 = parts[2].parse()?;
let ping: i32 = parts[3].parse()?;
let mut name = parts[4].to_string();
let skin = parts[5].to_string();
let top_color: u8 = parts[6].parse()?;
let bottom_color: u8 = parts[7].parse()?;
let team = parts[8].to_string();
let is_spectator = ping < 1;
if is_spectator {
frags = 0;
name = name.trim_start_matches("\\s\\").to_string();
}
Ok(Self {
id,
frags,
ping: ping.unsigned_abs(),
time,
name,
team,
skin,
top_color,
bottom_color,
is_spectator,
})
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct QtvStream {
pub id: u32,
pub name: String,
pub url: String,
pub client_count: u32,
}
impl TryFrom<&[u8]> for QtvStream {
type Error = anyhow::Error;
fn try_from(bytes: &[u8]) -> result::Result<Self, Self::Error> {
let parts: Vec<String> = tokenize(to_unicode(bytes).as_str());
let id: u32 = parts[1].parse()?;
let name: String = parts[2].to_string();
let url: String = parts[3].to_string();
let client_count: u32 = parts[4].parse()?;
Ok(Self {
id,
name,
url,
client_count,
})
}
}
pub fn info(address: &str, timeout: Option<Duration>) -> Result<MvdsvInfo> {
let response = {
let message = b"\xff\xff\xff\xffstatus 55".to_vec();
let options = tinyudp::ReadOptions {
timeout,
..Default::default()
};
tinyudp::send_and_read(address, &message, &options)?
};
MvdsvInfo::try_from_status_response(response.as_slice())
}
fn tokenize(value: &str) -> Vec<String> {
let mut tokens: Vec<String> = vec![];
let mut in_quote = false;
let mut current_token = "".to_string();
for c in value.chars() {
match c {
'"' => in_quote = !in_quote,
' ' => {
if in_quote {
current_token.push(c);
} else {
tokens.push(current_token);
current_token = "".to_string();
}
}
_ => current_token.push(c),
}
}
if !current_token.is_empty() {
tokens.push(current_token);
}
tokens
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_tokenize() {
assert_eq!(
tokenize(r#"qtv 1 "zasadzka Qtv (2)" "2@zasadzka.pl:28000" 2"#),
vec![
"qtv".to_string(),
"1".to_string(),
"zasadzka Qtv (2)".to_string(),
"2@zasadzka.pl:28000".to_string(),
"2".to_string()
]
)
}
#[test]
fn test_server_info() -> Result<()> {
{
let info = info("quake.se:28501", None)?;
assert_eq!(
info.settings.hostname,
Some("QUAKE.SE KTX:28501".to_string())
);
}
{
let info = info("foo.bar:666", Some(Duration::from_millis(50)));
assert!(info.is_err());
}
Ok(())
}
#[test]
fn test_mvdsv_info() -> Result<()> {
{
let result = MvdsvInfo::try_from_status_response([0].as_slice());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid header".to_string()
);
}
{
let result =
MvdsvInfo::try_from_status_response([255, 255, 255, 255, 110, 0].as_slice());
assert_eq!(result.unwrap_err().to_string(), "Invalid body".to_string());
}
{
let response = [
255, 255, 255, 255, 110, 92, 109, 97, 120, 102, 112, 115, 92, 55, 55, 92, 112, 109,
95, 107, 116, 106, 117, 109, 112, 92, 49, 92, 42, 118, 101, 114, 115, 105, 111,
110, 92, 77, 86, 68, 83, 86, 32, 48, 46, 51, 54, 92, 42, 122, 95, 101, 120, 116,
92, 53, 49, 49, 92, 42, 97, 100, 109, 105, 110, 92, 108, 111, 108, 101, 107, 32,
60, 108, 111, 108, 101, 107, 64, 113, 117, 97, 107, 101, 49, 46, 112, 108, 62, 92,
107, 116, 120, 118, 101, 114, 92, 49, 46, 52, 50, 92, 115, 118, 95, 97, 110, 116,
105, 108, 97, 103, 92, 50, 92, 110, 101, 101, 100, 112, 97, 115, 115, 92, 52, 92,
109, 97, 120, 115, 112, 101, 99, 116, 97, 116, 111, 114, 115, 92, 49, 50, 92, 42,
103, 97, 109, 101, 100, 105, 114, 92, 113, 119, 92, 116, 101, 97, 109, 112, 108,
97, 121, 92, 50, 92, 109, 111, 100, 101, 92, 50, 111, 110, 50, 92, 116, 105, 109,
101, 108, 105, 109, 105, 116, 92, 49, 48, 92, 100, 101, 97, 116, 104, 109, 97, 116,
99, 104, 92, 51, 92, 42, 113, 118, 109, 92, 115, 111, 92, 42, 112, 114, 111, 103,
115, 92, 115, 111, 92, 109, 97, 120, 99, 108, 105, 101, 110, 116, 115, 92, 52, 92,
109, 97, 112, 92, 122, 116, 110, 100, 109, 51, 92, 115, 101, 114, 118, 101, 114,
100, 101, 109, 111, 92, 50, 111, 110, 50, 95, 114, 101, 100, 95, 118, 115, 95, 98,
108, 117, 101, 91, 122, 116, 110, 100, 109, 51, 93, 50, 48, 50, 52, 48, 55, 49, 54,
45, 49, 50, 52, 52, 46, 109, 118, 100, 92, 104, 111, 115, 116, 110, 97, 109, 101,
92, 122, 97, 115, 97, 100, 122, 107, 97, 58, 50, 55, 53, 48, 49, 32, 40, 114, 101,
100, 32, 118, 115, 46, 32, 98, 108, 117, 101, 41, 135, 92, 102, 112, 100, 92, 50,
48, 54, 92, 115, 116, 97, 116, 117, 115, 92, 57, 32, 109, 105, 110, 32, 108, 101,
102, 116, 10, 55, 53, 32, 49, 49, 32, 50, 32, 50, 53, 32, 34, 244, 105, 97, 108,
108, 34, 32, 34, 34, 32, 52, 32, 52, 32, 34, 114, 101, 100, 34, 10, 56, 48, 32, 50,
32, 50, 32, 49, 51, 32, 34, 114, 105, 107, 105, 34, 32, 34, 34, 32, 49, 51, 32, 49,
51, 32, 34, 98, 108, 117, 101, 34, 10, 56, 52, 32, 52, 32, 50, 32, 53, 49, 32, 34,
78, 76, 34, 32, 34, 34, 32, 52, 32, 52, 32, 34, 114, 101, 100, 34, 10, 55, 56, 32,
45, 57, 57, 57, 57, 32, 50, 32, 45, 53, 54, 32, 34, 92, 115, 92, 98, 97, 100, 97,
115, 115, 34, 32, 34, 98, 97, 100, 97, 115, 115, 34, 32, 49, 48, 32, 49, 49, 32,
34, 109, 97, 122, 34, 10, 55, 57, 32, 45, 57, 57, 57, 57, 32, 50, 32, 45, 51, 56,
32, 34, 92, 115, 92, 108, 111, 107, 101, 34, 32, 34, 34, 32, 52, 32, 52, 32, 34,
114, 101, 100, 34, 10, 56, 49, 32, 45, 57, 57, 57, 57, 32, 50, 32, 45, 51, 56, 32,
34, 92, 115, 92, 81, 117, 97, 107, 101, 34, 32, 34, 34, 32, 49, 51, 32, 49, 51, 32,
34, 98, 108, 117, 101, 34, 10, 56, 53, 32, 51, 32, 50, 32, 52, 53, 32, 34, 72, 108,
89, 34, 32, 34, 34, 32, 49, 51, 32, 49, 51, 32, 34, 98, 108, 117, 101, 34, 10, 56,
54, 32, 45, 57, 57, 57, 57, 32, 50, 32, 45, 54, 54, 54, 32, 34, 92, 115, 92, 91,
83, 101, 114, 118, 101, 77, 101, 93, 34, 32, 34, 34, 32, 49, 50, 32, 49, 49, 32,
34, 108, 113, 119, 99, 34, 10, 113, 116, 118, 32, 49, 32, 34, 122, 97, 115, 97,
100, 122, 107, 97, 32, 81, 116, 118, 32, 40, 50, 41, 34, 32, 34, 50, 64, 122, 97,
115, 97, 100, 122, 107, 97, 46, 112, 108, 58, 50, 56, 48, 48, 48, 34, 32, 50, 10,
0,
]
.as_slice();
let info = MvdsvInfo::try_from_status_response(response)?;
assert_eq!(
info.settings.hostname,
Some("zasadzka:27501 (red vs. blue)\u{87}".to_string())
);
assert_eq!(
info.qtv_stream,
Some(QtvStream {
id: 1,
name: "zasadzka Qtv (2)".to_string(),
url: "2@zasadzka.pl:28000".to_string(),
client_count: 2,
})
);
assert_eq!(
info.clients,
vec![
MvdsvClient {
id: 75,
frags: 11,
ping: 25,
time: 2,
name: "ôiall".to_string(),
team: "red".to_string(),
skin: "".to_string(),
top_color: 4,
bottom_color: 4,
is_spectator: false,
},
MvdsvClient {
id: 80,
frags: 2,
ping: 13,
time: 2,
name: "riki".to_string(),
team: "blue".to_string(),
skin: "".to_string(),
top_color: 13,
bottom_color: 13,
is_spectator: false,
},
MvdsvClient {
id: 84,
frags: 4,
ping: 51,
time: 2,
name: "NL".to_string(),
team: "red".to_string(),
skin: "".to_string(),
top_color: 4,
bottom_color: 4,
is_spectator: false,
},
MvdsvClient {
id: 78,
frags: 0,
ping: 56,
time: 2,
name: "badass".to_string(),
team: "maz".to_string(),
skin: "badass".to_string(),
top_color: 10,
bottom_color: 11,
is_spectator: true,
},
MvdsvClient {
id: 79,
frags: 0,
ping: 38,
time: 2,
name: "loke".to_string(),
team: "red".to_string(),
skin: "".to_string(),
top_color: 4,
bottom_color: 4,
is_spectator: true,
},
MvdsvClient {
id: 81,
frags: 0,
ping: 38,
time: 2,
name: "Quake".to_string(),
team: "blue".to_string(),
skin: "".to_string(),
top_color: 13,
bottom_color: 13,
is_spectator: true,
},
MvdsvClient {
id: 85,
frags: 3,
ping: 45,
time: 2,
name: "HlY".to_string(),
team: "blue".to_string(),
skin: "".to_string(),
top_color: 13,
bottom_color: 13,
is_spectator: false,
},
MvdsvClient {
id: 86,
frags: 0,
ping: 666,
time: 2,
name: "[ServeMe]".to_string(),
team: "lqwc".to_string(),
skin: "".to_string(),
top_color: 12,
bottom_color: 11,
is_spectator: true,
},
]
);
}
Ok(())
}
}