Skip to content

Add rich error type #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Aug 5, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Install MSRV
uses: actions-rs/toolchain@v1
with:
toolchain: 1.60.0
toolchain: 1.65.0
override: true
- name: Run MSRV
run: cargo build
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ repos:
language: system
files: '[.]rs$'
pass_filenames: false
entry: rustup run --install 1.60 cargo build
entry: rustup run --install 1.65 cargo build
- id: docs
name: Check rustdoc compiles
language: system
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ documentation = "https://docs.rs/gamedig/latest/gamedig/"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
keywords = ["server", "query", "game", "check", "status"]
rust-version = "1.60.0"
rust-version = "1.65.0"

[features]
default = ["games", "services", "game_defs"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ within the library or want to request a feature, it's better to do so here rathe
on Discord.

## Usage
Minimum Supported Rust Version is `1.60.0` and the code is cross-platform.
Minimum Supported Rust Version is `1.65.0` and the code is cross-platform.

Pick a game/service/protocol (check the [GAMES](GAMES.md), [SERVICES](SERVICES.md) and [PROTOCOLS](PROTOCOLS.md) files to see the currently supported ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port can also be optional if the server is running the default ports) then query on it.

Expand Down
7 changes: 4 additions & 3 deletions VERSIONS.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# MSRV (Minimum Supported Rust Version)

Current: `1.60.0`
Current: `1.65.0`

Places to update:
- `Cargo.toml`
- `README.md`
- `.github/workflows/ci.yml`
- `.pre-commit-config.yaml`

# rustfmt version

Expand All @@ -22,5 +23,5 @@ The toolchain version used to run rustfmt in CI
Current: `nightly-2023-07-09`

Places to update:
- `./.github/workflows/ci.yml`
- `./.pre-commit-config.yaml`
- `.github/workflows/ci.yml`
- `.pre-commit-config.yaml`
27 changes: 17 additions & 10 deletions src/buffer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::GDError::{PacketBad, PacketUnderflow};
use crate::GDErrorKind::PacketBad;
use crate::GDErrorKind::PacketUnderflow;
use crate::GDResult;
use byteorder::{BigEndian, ByteOrder, LittleEndian};
use std::{convert::TryInto, marker::PhantomData};
Expand Down Expand Up @@ -69,12 +70,12 @@ impl<'a, B: ByteOrder> Buffer<'a, B> {
match new_cursor {
// If the addition was not successful (i.e., it resulted in an overflow or underflow),
// return an error indicating that the cursor is out of bounds.
None => Err(PacketBad),
None => Err(PacketBad.into()),

// If the new cursor position is either less than zero (i.e., before the start of the buffer)
// or greater than the remaining length of the buffer (i.e., past the end of the buffer),
// return an error indicating that the cursor is out of bounds.
Some(x) if x < 0 || x as usize > self.data_length() => Err(PacketBad),
Some(x) if x < 0 || x as usize > self.data_length() => Err(PacketBad.into()),

// If the new cursor position is within the bounds of the buffer, update the cursor
// position and return Ok.
Expand Down Expand Up @@ -107,7 +108,10 @@ impl<'a, B: ByteOrder> Buffer<'a, B> {
// If the size of `T` is larger than the remaining length, return an error
// because we don't have enough data left to read.
if size > remaining {
return Err(PacketUnderflow);
return Err(PacketUnderflow.context(format!(
"Size requested {} was larger than remaining bytes {}",
size, remaining
)));
}

// Slice the data array from the current cursor position for `size` amount of
Expand Down Expand Up @@ -242,7 +246,7 @@ macro_rules! impl_buffer_read_byte {
.map($map_func)
// If the data array is empty (and thus `first` returns None),
// `ok_or_else` will return a BufferError.
.ok_or_else(|| PacketBad)
.ok_or_else(|| PacketBad.into())
}
}
};
Expand All @@ -264,10 +268,10 @@ macro_rules! impl_buffer_read {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Convert the byte slice into an array of the appropriate type.
let array = data.try_into().map_err(|_| {
let array = data.try_into().map_err(|e| {
// If conversion fails, return an error indicating the required and provided
// lengths.
PacketBad
PacketBad.context(e)
})?;

// Use the provided function to read the data from the array into the given
Expand Down Expand Up @@ -345,7 +349,7 @@ impl StringDecoder for Utf8Decoder {
&data[.. position]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|_| PacketBad)?
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();

Expand Down Expand Up @@ -393,7 +397,7 @@ impl<B: ByteOrder> StringDecoder for Utf16Decoder<B> {
B::read_u16_into(&data[.. position], &mut paired_buf);

// Convert the buffer of u16 values into a String
let result = String::from_utf16(&paired_buf).map_err(|_| PacketBad)?;
let result = String::from_utf16(&paired_buf).map_err(|e| PacketBad.context(e))?;

// Update the cursor position
// The +2 accounts for the delimiter
Expand Down Expand Up @@ -543,6 +547,9 @@ mod tests {
let mut buffer = Buffer::<LittleEndian>::new(data);

let result: Result<u32, _> = buffer.read();
assert_eq!(result.unwrap_err(), PacketUnderflow);
assert_eq!(
result.unwrap_err(),
crate::GDErrorKind::PacketUnderflow.into()
);
}
}
169 changes: 154 additions & 15 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{
backtrace,
error::Error,
fmt::{self, Formatter},
};
Expand All @@ -8,7 +9,7 @@ pub type GDResult<T> = Result<T, GDError>;

/// GameDig Error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GDError {
pub enum GDErrorKind {
/// The received packet was bigger than the buffer size.
PacketOverflow,
/// The received packet was shorter than the expected one.
Expand All @@ -28,7 +29,7 @@ pub enum GDError {
/// Invalid input.
InvalidInput,
/// The server queried is not the queried game server.
BadGame(String),
BadGame,
/// Couldn't automatically query.
AutoQuery,
/// A protocol-defined expected format was not met.
Expand All @@ -41,12 +42,108 @@ pub enum GDError {
TypeParse,
}

impl Error for GDError {}
impl GDErrorKind {
/// Convert error kind into a full error with a source (and implicit
/// backtrace)
///
/// ```
/// use gamedig::{GDErrorKind, GDResult};
/// let _: GDResult<u32> = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e));
/// ```
pub fn context<E: Into<Box<dyn std::error::Error + 'static>>>(self, source: E) -> GDError {
GDError::from_error(self, source)
}
}

type ErrorSource = Box<dyn std::error::Error + 'static>;

/// Gamedig error type
///
/// Can be created in three ways (all of which will implicitly generate a
/// backtrace):
///
/// Directly from an [error kind](crate::errors::GDErrorKind) (without a source)
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.into();
/// ```
///
/// [From an error kind with a source](crate::errors::GDErrorKind::context) (any
/// type that implements `Into<Box<dyn std::error::Error + 'static>>)
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad");
/// ```
///
/// Using the [new helper](crate::errors::GDError::new)
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into()));
/// ```
pub struct GDError {
pub kind: GDErrorKind,
pub source: Option<ErrorSource>,
pub backtrace: Option<std::backtrace::Backtrace>,
}

impl From<GDErrorKind> for GDError {
fn from(value: GDErrorKind) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind: value,
source: None,
backtrace,
}
}
}

impl PartialEq for GDError {
fn eq(&self, other: &Self) -> bool { self.kind == other.kind }
}

impl Error for GDError {
fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(Box::as_ref) }
}

impl fmt::Debug for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "GDError{{ kind={:?}", self.kind)?;
if let Some(source) = &self.source {
writeln!(f, " source={:?}", source)?;
}
if let Some(backtrace) = &self.backtrace {
let bt = format!("{:#?}", backtrace);
writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?;
}
writeln!(f, "}}")?;
Ok(())
}
}

impl fmt::Display for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) }
}

impl GDError {
/// Create a new error (with automatic backtrace)
pub fn new(kind: GDErrorKind, source: Option<ErrorSource>) -> Self {
let backtrace = Some(std::backtrace::Backtrace::capture());
Self {
kind,
source,
backtrace,
}
}

/// Create a new error using any type that can be converted to an error
pub fn from_error<E: Into<Box<dyn std::error::Error + 'static>>>(kind: GDErrorKind, source: E) -> Self {
Self::new(kind, Some(source.into()))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -61,29 +158,71 @@ mod tests {
// Testing Err variant of the GDResult type
#[test]
fn test_gdresult_err() {
let result: GDResult<u32> = Err(GDError::InvalidInput);
let result: GDResult<u32> = Err(GDErrorKind::InvalidInput.into());
assert!(result.is_err());
}

// Testing the Display trait for the GDError type
// Testing cloning the GDErrorKind type
#[test]
fn test_cloning() {
let error = GDErrorKind::BadGame;
let cloned_error = error.clone();
assert_eq!(error, cloned_error);
}

// test display GDError
#[test]
fn test_display() {
let error = GDError::PacketOverflow;
assert_eq!(format!("{}", error), "PacketOverflow");
let err = GDErrorKind::BadGame.context("Rust is not a game");
let s = format!("{}", err);
println!("{}", s);
assert_eq!(
s,
"GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=<disabled>\n}\n"
);
}

// Testing the Error trait for the GDError type
// test error trait GDError
#[test]
fn test_error_trait() {
let error = GDError::PacketBad;
assert!(error.source().is_none());
let source: Result<u32, _> = "nan".parse();
let source_err = source.unwrap_err();

let error_with_context = GDErrorKind::TypeParse.context(source_err.clone());
assert!(error_with_context.source().is_some());
assert_eq!(
format!("{}", error_with_context.source().unwrap()),
format!("{}", source_err)
);

let error_without_context: GDError = GDErrorKind::TypeParse.into();
assert!(error_without_context.source().is_none());
}

// Testing cloning the GDError type
// Test creating GDError with GDError::new
#[test]
fn test_cloning() {
let error = GDError::BadGame(String::from("MyGame"));
let cloned_error = error.clone();
assert_eq!(error, cloned_error);
fn test_create_new() {
let error_from_new = GDError::new(GDErrorKind::InvalidInput, None);
assert!(error_from_new.backtrace.is_some());
assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput);
assert!(error_from_new.source.is_none());
}

// Test creating GDError with GDErrorKind::context
#[test]
fn test_create_context() {
let error_from_context = GDErrorKind::InvalidInput.context("test");
assert!(error_from_context.backtrace.is_some());
assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput);
assert!(error_from_context.source.is_some());
}

// Test creating GDError with From<GDErrorKind> for GDError
#[test]
fn test_create_into() {
let error_from_into: GDError = GDErrorKind::InvalidInput.into();
assert!(error_from_into.backtrace.is_some());
assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput);
assert!(error_from_into.source.is_none());
}
}
6 changes: 3 additions & 3 deletions src/games/bat1944.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
protocols::valve::{self, game, SteamApp},
GDError::TypeParse,
GDErrorKind::TypeParse,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
Expand All @@ -15,12 +15,12 @@ pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<game::Response> {

if let Some(rules) = &mut valve_response.rules {
if let Some(bat_max_players) = rules.get("bat_max_players_i") {
valve_response.info.players_maximum = bat_max_players.parse().map_err(|_| TypeParse)?;
valve_response.info.players_maximum = bat_max_players.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_max_players_i");
}

if let Some(bat_player_count) = rules.get("bat_player_count_s") {
valve_response.info.players_online = bat_player_count.parse().map_err(|_| TypeParse)?;
valve_response.info.players_online = bat_player_count.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_player_count_s");
}

Expand Down
Loading