ssq 0.8.0

Rust implementation of Source Server Query (A2S)
Documentation
/// Arma 3 / DayZ server browser protocol parser. Requires the `arma3` feature.
#[cfg(feature = "arma3")]
pub mod arma3;

use std::io::Cursor;
use std::io::Read;
use std::io::Write;
use std::net::ToSocketAddrs;

use bstr::BString;
use bstr::ByteSlice;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;

#[cfg(feature = "serde")]
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;

use crate::Client;
use crate::HEADER_RULES;
use crate::ReadCString;
use crate::errors::Error;
use crate::errors::Result;

#[doc(hidden)]
pub const RULES_REQUEST: [u8; 5] = [0xFF, 0xFF, 0xFF, 0xFF, 0x56];

/// A single key-value rule from an A2S_RULES response.
///
/// ```no_run
/// use std::time::Duration;
/// use ssq::Client;
///
/// let client = Client::new(Duration::from_secs(5)).unwrap();
/// let rules = client.rules("127.0.0.1:27015").unwrap();
/// for r in &rules {
///     println!("{} = {}", r.name, r.value);
/// }
/// ```
#[derive(Debug, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub struct Rule {
    /// Name of the rule.
    #[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
    pub name: BString,

    /// Value of the rule.
    #[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary_bstring))]
    pub value: BString,
}

impl Rule {
    /// Returns the estimated byte size of this rule when serialized
    /// (name + NUL + value + NUL).
    pub fn size_hint(&self) -> usize {
        self.name.len() + 1 + self.value.len() + 1
    }

    /// Write this rule as a NUL-terminated name and value pair.
    pub fn write<W: Write>(&self, mut w: W) -> Result<()> {
        w.write_all(self.name.as_bytes())?;
        w.write_all(&[0])?;
        w.write_all(self.value.as_bytes())?;
        w.write_all(&[0])?;
        Ok(())
    }

    /// Serialize this rule to a byte vector. See [`Rule::write`].
    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
    }

    /// Returns the estimated total byte size for a full A2S_RULES response
    /// containing these rules (header + count + all rules).
    pub fn vec_size_hint(rules: &[Self]) -> usize {
        // header(5) + count(2) + sum of each rule
        5 + 2 + rules.iter().map(|r| r.size_hint()).sum::<usize>()
    }

    /// Serialize a list of rules into the full A2S_RULES wire format, including
    /// the 0xFFFFFFFF header, response byte, and rule count.
    pub fn write_vec<W: Write>(rules: &[Self], mut w: W) -> Result<()> {
        w.write_all(&[0xff, 0xff, 0xff, 0xff, HEADER_RULES])?;
        w.write_all(&(rules.len() as u16).to_le_bytes())?;
        for rule in rules {
            rule.write(&mut w)?;
        }
        Ok(())
    }

    /// Serialize a list of rules to a byte vector. See [`Rule::write_vec`].
    pub fn vec_to_bytes(rules: Vec<Self>) -> Vec<u8> {
        let mut bytes = Vec::with_capacity(Self::vec_size_hint(&rules));
        Self::write_vec(&rules, &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<Vec<Self>> {
        Self::from_reader(data)
    }

    /// Parse an A2S_RULES response from raw bytes.
    ///
    /// The reader should start at the response type byte (`0x45` / 'E'), after
    /// the 4-byte split/single packet header has been stripped.
    pub fn from_reader<R: Read>(mut data: R) -> Result<Vec<Self>> {
        let header = data.read_u8()?;
        if header != HEADER_RULES {
            return Err(Error::UnexpectedHeader {
                expected: HEADER_RULES,
                actual: header,
            });
        }

        let count = data.read_u16::<LittleEndian>()?;

        let mut rules: Vec<Rule> = Vec::with_capacity(count as usize);

        for _ in 0..count {
            rules.push(Rule {
                name: data.read_cstring()?,
                value: data.read_cstring()?,
            })
        }

        Ok(rules)
    }
}

impl Client {
    /// Send an A2S_RULES query and parse the response. Handles challenge
    /// negotiation automatically.
    pub fn rules<A: ToSocketAddrs>(&self, addr: A) -> Result<Vec<Rule>> {
        let data = self.do_challenge_request(addr, &RULES_REQUEST)?;
        Rule::from_reader(data.as_slice())
    }
}