Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[package]
name = "masterstat"
description = "Get server addresses from QuakeWorld master servers."
keywords = ["quake", "quakeworld", "servers"]
keywords = ["masters", "quake", "quakeworld", "servers"]
repository = "https://github.com/vikpe/masterstat-rust"
authors = ["Viktor Persson <viktor.persson@arcsin.se>"]
version = "0.1.3"
version = "0.2.0"
edition = "2021"
license = "MIT"
include = [
Expand All @@ -19,11 +19,10 @@ include = [

[dependencies]
anyhow = "1.0.82"
binrw = "0.14.1"
futures = "0.3.30"
tinyudp = "0.2.1"
tinyudp = "0.4.0"
tokio = { version = "1.37.0", features = ["rt", "sync"] }
zerocopy = "0.7.32"
zerocopy-derive = "0.7.32"

[dev-dependencies]
pretty_assertions = "1.4.0"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# masterstat [![Test](https://github.com/vikpe/masterstat-rust/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/vikpe/masterstat-rust/actions/workflows/test.yml) [![crates](https://img.shields.io/crates/v/masterstat)](https://crates.io/crates/masterstat) [![docs.rs](https://img.shields.io/docsrs/masterstat)](https://docs.rs/masterstat/)
# masterstat [![Test](https://github.com/quakeworld/masterstat/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/quakeworld/masterstat/actions/workflows/test.yml) [![crates](https://img.shields.io/crates/v/masterstat)](https://crates.io/crates/masterstat) [![docs.rs](https://img.shields.io/docsrs/masterstat)](https://docs.rs/masterstat/)

> Get server addresses from QuakeWorld master servers.
> Get server addresses from QuakeWorld master servers

## Installation

Expand Down
31 changes: 15 additions & 16 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use std::io::Cursor;
use std::sync::Arc;
use std::time::Duration;

use anyhow::{anyhow as e, Result};
use binrw::BinRead;
use tokio::sync::Mutex;
use zerocopy::FromBytes;

use crate::server_address::{RawServerAddress, ServerAddress, RAW_ADDRESS_SIZE};

const SERVERS_COMMAND: [u8; 3] = [0x63, 0x0a, 0x00];
const SERVERS_RESPONSE_HEADER: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0x64, 0x0a];
use crate::server_address::{RawServerAddress, ServerAddress};

/// Get server addresses from a single master server
///
Expand All @@ -28,11 +26,12 @@ pub fn server_addresses(
master_address: &str,
timeout: Option<Duration>,
) -> Result<Vec<ServerAddress>> {
const MESSAGE: [u8; 3] = [99, 10, 0];
let options = tinyudp::ReadOptions {
timeout,
buffer_size: 16 * 1024, // 16 kb
};
let response = tinyudp::send_and_read(master_address, &SERVERS_COMMAND, &options)?;
let response = tinyudp::send_and_read(master_address, &MESSAGE, &options)?;
let server_addresses = parse_servers_response(&response)?;
Ok(sorted_and_unique(&server_addresses))
}
Expand All @@ -59,7 +58,6 @@ pub async fn server_addresses_from_many(

for master_address in master_addresses.iter().map(|a| a.as_ref().to_string()) {
let result_mux = result_mux.clone();

let task = tokio::spawn(async move {
if let Ok(servers) = server_addresses(&master_address, timeout) {
let mut result = result_mux.lock().await;
Expand All @@ -76,22 +74,23 @@ pub async fn server_addresses_from_many(
}

fn parse_servers_response(response: &[u8]) -> Result<Vec<ServerAddress>> {
if !response.starts_with(&SERVERS_RESPONSE_HEADER) {
const RESPONSE_HEADER: [u8; 6] = [255, 255, 255, 255, 100, 10];

if !response.starts_with(&RESPONSE_HEADER) {
return Err(e!("Invalid response"));
}

let body = &response[SERVERS_RESPONSE_HEADER.len()..];
let server_addresses = body
.chunks(RAW_ADDRESS_SIZE)
.filter(|b| b.len() == RAW_ADDRESS_SIZE)
.filter_map(RawServerAddress::read_from)
.map(ServerAddress::from)
.collect::<Vec<ServerAddress>>();
let body = &mut Cursor::new(&response[RESPONSE_HEADER.len()..]);
let mut server_addresses = vec![];

while let Ok(raw_address) = RawServerAddress::read(body) {
server_addresses.push(ServerAddress::from(raw_address));
}

Ok(server_addresses)
}

pub fn sorted_and_unique(server_addresses: &[ServerAddress]) -> Vec<ServerAddress> {
fn sorted_and_unique(server_addresses: &[ServerAddress]) -> Vec<ServerAddress> {
let mut servers = server_addresses.to_vec();
servers.sort();
servers.dedup();
Expand Down
29 changes: 14 additions & 15 deletions src/server_address.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
use binrw::BinRead;
use std::fmt::Display;

use zerocopy::{BigEndian, U16};
use zerocopy_derive::{FromBytes, FromZeroes};

pub const RAW_ADDRESS_SIZE: usize = 6;

#[derive(FromZeroes, FromBytes)]
#[derive(BinRead)]
#[br(big)]
pub struct RawServerAddress {
pub ip: [u8; 4],
pub port: U16<BigEndian>,
pub port: u16,
}

#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
Expand All @@ -27,30 +24,32 @@ impl From<RawServerAddress> for ServerAddress {
fn from(raw: RawServerAddress) -> Self {
ServerAddress {
ip: raw.ip.map(|b| b.to_string()).join("."),
port: raw.port.into(),
port: raw.port,
}
}
}

#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use zerocopy::{FromBytes, U16};

use crate::server_address::{RawServerAddress, ServerAddress};
use anyhow::Result;
use binrw::BinRead;
use pretty_assertions::assert_eq;
use std::io::Cursor;

#[test]
fn test_raw_server_address() {
let raw_address = RawServerAddress::read_from(&[192, 168, 1, 1, 0x75, 0x30]).unwrap();
fn test_raw_server_address() -> Result<()> {
let raw_address = RawServerAddress::read(&mut Cursor::new(&[192, 168, 1, 1, 117, 48]))?;
assert_eq!(raw_address.ip, [192, 168, 1, 1]);
assert_eq!(raw_address.port, U16::from(30000));
assert_eq!(raw_address.port, 30000);
Ok(())
}

#[test]
fn test_server_address_from_raw_server_address() {
let raw_address = RawServerAddress {
ip: [192, 168, 1, 1],
port: U16::from(30000),
port: 30000,
};
let address = ServerAddress::from(raw_address);
assert_eq!(address.ip, "192.168.1.1");
Expand Down
Loading