diff --git a/examples/announce.rs b/examples/announce.rs index 3269b83..216a40e 100644 --- a/examples/announce.rs +++ b/examples/announce.rs @@ -40,8 +40,8 @@ fn main() { let text = cli.text.join("\n"); let command = CharGridCommand { - origin: Origin::ZERO, grid: CharGrid::wrap_str(TILE_WIDTH, &text), + origin: Origin::ZERO, }; connection.send(command).expect("sending text failed"); } diff --git a/examples/brightness_tester.rs b/examples/brightness_tester.rs index 665f533..ee39209 100644 --- a/examples/brightness_tester.rs +++ b/examples/brightness_tester.rs @@ -18,8 +18,8 @@ fn main() { bitmap.fill(true); let command = BitmapCommand { - origin: Origin::ZERO, bitmap, + origin: Origin::ZERO, compression: CompressionCode::default(), }; connection.send(command).expect("send failed"); diff --git a/examples/game_of_life.rs b/examples/game_of_life.rs index b06f312..73ce6da 100644 --- a/examples/game_of_life.rs +++ b/examples/game_of_life.rs @@ -23,8 +23,8 @@ fn main() { loop { let command = BitmapCommand { - origin: Origin::ZERO, bitmap: field.clone(), + origin: Origin::ZERO, compression: CompressionCode::default(), }; connection.send(command).expect("could not send"); diff --git a/examples/random_brightness.rs b/examples/random_brightness.rs index d3b9cc3..4799b2b 100644 --- a/examples/random_brightness.rs +++ b/examples/random_brightness.rs @@ -29,8 +29,8 @@ fn main() { filled_grid.fill(true); let command = BitmapCommand { - origin: Origin::ZERO, bitmap: filled_grid, + origin: Origin::ZERO, compression: CompressionCode::default(), }; connection.send(command).expect("send failed"); diff --git a/examples/websocket.rs b/examples/websocket.rs index cf578c9..0100317 100644 --- a/examples/websocket.rs +++ b/examples/websocket.rs @@ -12,8 +12,8 @@ fn main() { pixels.fill(true); let command = BitmapCommand { - origin: Origin::ZERO, bitmap: pixels, + origin: Origin::ZERO, compression: CompressionCode::default(), }; connection.send(command).unwrap(); diff --git a/examples/wiping_clear.rs b/examples/wiping_clear.rs index ace3a54..76a2c33 100644 --- a/examples/wiping_clear.rs +++ b/examples/wiping_clear.rs @@ -33,8 +33,8 @@ fn main() { } let command = BitmapCommand { - origin: Origin::ZERO, bitmap: enabled_pixels.clone(), + origin: Origin::ZERO, compression: CompressionCode::default(), }; connection diff --git a/src/brightness.rs b/src/brightness.rs index 6911243..d7e2972 100644 --- a/src/brightness.rs +++ b/src/brightness.rs @@ -18,6 +18,7 @@ use rand::{ /// let result = connection.send(BrightnessCommand::from(b)); /// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[repr(transparent)] pub struct Brightness(u8); impl From for u8 { diff --git a/src/commands/bitmap.rs b/src/commands/bitmap.rs index 68b9eee..b7eafb4 100644 --- a/src/commands/bitmap.rs +++ b/src/commands/bitmap.rs @@ -1,8 +1,10 @@ use crate::{ - command_code::CommandCode, commands::TryFromPacketError, - compression::into_compressed, compression::into_decompressed, Bitmap, - CompressionCode, Grid, Header, Origin, Packet, Pixels, TypedCommand, - TILE_SIZE, + command_code::CommandCode, + commands::errors::{TryFromPacketError, TryIntoPacketError}, + compression::into_compressed, + compression::into_decompressed, + Bitmap, CompressionCode, Grid, Header, Origin, Packet, Pixels, + TypedCommand, TILE_SIZE, }; /// Overwrites a rectangular region of pixels. @@ -28,25 +30,28 @@ use crate::{ /// /// connection.send(command).expect("send failed"); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BitmapCommand { - /// where to start drawing the pixels - pub origin: Origin, /// the pixels to send pub bitmap: Bitmap, + /// where to start drawing the pixels + pub origin: Origin, /// how to compress the command when converting to packet pub compression: CompressionCode, } -impl From for Packet { - fn from(value: BitmapCommand) -> Self { +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: BitmapCommand) -> Result { assert_eq!(value.origin.x % 8, 0); assert_eq!(value.bitmap.width() % 8, 0); - let tile_x = (value.origin.x / TILE_SIZE) as u16; - let tile_w = (value.bitmap.width() / TILE_SIZE) as u16; - let pixel_h = value.bitmap.height() as u16; - let payload = into_compressed(value.compression, value.bitmap.into()); + let tile_x = (value.origin.x / TILE_SIZE).try_into()?; + let tile_w = (value.bitmap.width() / TILE_SIZE).try_into()?; + let pixel_h = value.bitmap.height().try_into()?; + let payload = into_compressed(value.compression, value.bitmap.into()) + .ok_or(TryIntoPacketError::CompressionFailed)?; let command = match value.compression { CompressionCode::Uncompressed => { CommandCode::BitmapLinearWinUncompressed @@ -61,16 +66,16 @@ impl From for Packet { CompressionCode::Zstd => CommandCode::BitmapLinearWinZstd, }; - Packet { + Ok(Packet { header: Header { command_code: command.into(), a: tile_x, - b: value.origin.y as u16, + b: value.origin.y.try_into()?, c: tile_w, d: pixel_h, }, payload, - } + }) } } @@ -162,8 +167,11 @@ impl BitmapCommand { mod tests { use super::*; use crate::command_code::CommandCode; + use crate::commands::tests::TestImplementsCommand; use crate::*; + impl TestImplementsCommand for BitmapCommand {} + #[test] fn command_code() { assert_eq!( @@ -188,7 +196,8 @@ mod tests { bitmap: Bitmap::new(8, 8).unwrap(), compression: *compression, } - .into(); + .try_into() + .unwrap(); let Packet { header, diff --git a/src/commands/bitmap_legacy.rs b/src/commands/bitmap_legacy.rs index 82df258..a19c22b 100644 --- a/src/commands/bitmap_legacy.rs +++ b/src/commands/bitmap_legacy.rs @@ -1,6 +1,6 @@ use crate::{ command_code::CommandCode, commands::check_command_code_only, - commands::TryFromPacketError, Packet, TypedCommand, + commands::errors::TryFromPacketError, Packet, TypedCommand, }; use std::fmt::Debug; @@ -17,7 +17,7 @@ use std::fmt::Debug; /// # #[allow(deprecated)] /// connection.send(BitmapLegacyCommand).unwrap(); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[deprecated] pub struct BitmapLegacyCommand; @@ -54,8 +54,12 @@ impl From for TypedCommand { #[allow(deprecated)] mod tests { use super::*; - use crate::commands::tests::round_trip; - use crate::Header; + use crate::{ + commands::tests::{round_trip, TestImplementsCommand}, + Header + }; + + impl TestImplementsCommand for BitmapLegacyCommand {} #[test] fn invalid_fields() { diff --git a/src/commands/bitvec.rs b/src/commands/bitvec.rs index 679b717..9ea6361 100644 --- a/src/commands/bitvec.rs +++ b/src/commands/bitvec.rs @@ -1,8 +1,7 @@ -use crate::compression::into_compressed; use crate::{ - command_code::CommandCode, commands::TryFromPacketError, - compression::into_decompressed, BitVec, CompressionCode, Header, Offset, - Packet, TypedCommand, + command_code::CommandCode, commands::errors::TryFromPacketError, + compression::into_compressed, compression::into_decompressed, BitVec, + CompressionCode, Header, Offset, Packet, TryIntoPacketError, TypedCommand, }; /// Binary operations for use with the [BitVecCommand] command. @@ -31,40 +30,43 @@ pub enum BinaryOperation { /// For example, [BinaryOperation::Or] can be used to turn on some pixels without affecting other pixels. /// /// The contained [BitVec] is always uncompressed. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Eq)] pub struct BitVecCommand { - /// where to start overwriting pixel data - pub offset: Offset, /// the pixels to send to the display as one long row pub bitvec: BitVec, + /// where to start overwriting pixel data + pub offset: Offset, /// The operation to apply on the display per bit comparing old and new state. pub operation: BinaryOperation, /// how to compress the command when converting to packet pub compression: CompressionCode, } -impl From for Packet { - fn from(command: BitVecCommand) -> Self { - let command_code = match command.operation { +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: BitVecCommand) -> Result { + let command_code = match value.operation { BinaryOperation::Overwrite => CommandCode::BitmapLinear, BinaryOperation::And => CommandCode::BitmapLinearAnd, BinaryOperation::Or => CommandCode::BitmapLinearOr, BinaryOperation::Xor => CommandCode::BitmapLinearXor, }; - let payload: Vec<_> = command.bitvec.into(); - let length = payload.len() as u16; - let payload = into_compressed(command.compression, payload); - Packet { + let payload: Vec<_> = value.bitvec.into(); + let length = payload.len().try_into()?; + let payload = into_compressed(value.compression, payload) + .ok_or(TryIntoPacketError::CompressionFailed)?; + Ok(Packet { header: Header { command_code: command_code.into(), - a: command.offset as u16, + a: value.offset.try_into()?, b: length, - c: command.compression.into(), + c: value.compression.into(), d: 0, }, payload, - } + }) } } @@ -135,9 +137,11 @@ impl From for TypedCommand { #[cfg(test)] mod tests { use super::*; - use crate::commands::tests::round_trip; + use crate::commands::tests::{round_trip, TestImplementsCommand}; use crate::{commands, Bitmap, BitmapCommand, Origin}; + impl TestImplementsCommand for BitVecCommand {} + #[test] fn command_code() { assert_eq!( @@ -193,7 +197,8 @@ mod tests { compression: *compression, operation: BinaryOperation::Overwrite, } - .into(); + .try_into() + .unwrap(); let Packet { header, mut payload, @@ -223,7 +228,8 @@ mod tests { compression: CompressionCode::Uncompressed, operation: BinaryOperation::Or, } - .into(); + .try_into() + .unwrap(); let Header { command_code: command, a: offset, @@ -255,7 +261,8 @@ mod tests { compression: CompressionCode::Uncompressed, operation: BinaryOperation::And, } - .into(); + .try_into() + .unwrap(); let Header { command_code: command, a: offset, @@ -287,7 +294,8 @@ mod tests { compression: CompressionCode::Uncompressed, operation: BinaryOperation::Xor, } - .into(); + .try_into() + .unwrap(); let Header { command_code: command, a: offset, diff --git a/src/commands/brightness.rs b/src/commands/brightness.rs index 5a0484f..876a28a 100644 --- a/src/commands/brightness.rs +++ b/src/commands/brightness.rs @@ -1,6 +1,7 @@ use crate::{ command_code::CommandCode, commands::check_command_code, - commands::TryFromPacketError, Brightness, Header, Packet, TypedCommand, + commands::errors::TryFromPacketError, Brightness, Header, Packet, + TypedCommand, }; /// Set the brightness of all tiles to the same value. @@ -13,7 +14,7 @@ use crate::{ /// let command = BrightnessCommand { brightness: Brightness::MAX }; /// connection.send(command).unwrap(); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BrightnessCommand { /// the brightness to set all pixels to pub brightness: Brightness, @@ -85,12 +86,14 @@ impl From for BrightnessCommand { #[cfg(test)] mod tests { use crate::command_code::CommandCode; - use crate::commands::tests::round_trip; + use crate::commands::errors::TryFromPacketError; + use crate::commands::tests::{round_trip, TestImplementsCommand}; use crate::{ - commands, Brightness, BrightnessCommand, Header, Packet, - TryFromPacketError, TypedCommand, + commands, Brightness, BrightnessCommand, Header, Packet, TypedCommand, }; + impl TestImplementsCommand for BrightnessCommand {} + #[test] fn brightness_as_command() { assert_eq!( diff --git a/src/commands/brightness_grid.rs b/src/commands/brightness_grid.rs index 08d68b7..7feec52 100644 --- a/src/commands/brightness_grid.rs +++ b/src/commands/brightness_grid.rs @@ -1,25 +1,27 @@ use crate::{ command_code::CommandCode, commands::check_command_code, - commands::TryFromPacketError, BrightnessGrid, ByteGrid, Header, Origin, - Packet, Tiles, TypedCommand, + commands::errors::TryFromPacketError, BrightnessGrid, ByteGrid, Header, + Origin, Packet, Tiles, TryIntoPacketError, TypedCommand, }; /// Set the brightness of individual tiles in a rectangular area of the display. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Eq)] pub struct BrightnessGridCommand { - /// which tile the brightness rectangle should start - pub origin: Origin, /// the brightness values per tile pub grid: BrightnessGrid, + /// which tile the brightness rectangle should start + pub origin: Origin, } -impl From for Packet { - fn from(value: BrightnessGridCommand) -> Self { - Packet::origin_grid_to_packet( +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: BrightnessGridCommand) -> Result { + Ok(Packet::origin_grid_to_packet( value.origin, value.grid, CommandCode::CharBrightness, - ) + )?) } } @@ -74,12 +76,15 @@ impl From for TypedCommand { #[cfg(test)] mod tests { - use crate::commands::tests::round_trip; + use crate::commands::errors::TryFromPacketError; + use crate::commands::tests::{round_trip, TestImplementsCommand}; use crate::{ commands, BrightnessGrid, BrightnessGridCommand, Origin, Packet, - TryFromPacketError, TypedCommand, + TypedCommand, }; + impl TestImplementsCommand for BrightnessGridCommand {} + #[test] fn round_trip_char_brightness() { round_trip( @@ -98,7 +103,7 @@ mod tests { origin: Origin::ZERO, grid, }; - let mut packet: Packet = command.into(); + let mut packet: Packet = command.try_into().unwrap(); let slot = packet.payload.get_mut(1).unwrap(); *slot = 23; assert_eq!( diff --git a/src/commands/char_grid.rs b/src/commands/char_grid.rs index a2e4585..8eac84a 100644 --- a/src/commands/char_grid.rs +++ b/src/commands/char_grid.rs @@ -1,7 +1,7 @@ use crate::{ command_code::CommandCode, commands::check_command_code, - commands::TryFromPacketError, CharGrid, Header, Origin, Packet, Tiles, - TypedCommand, + commands::errors::TryFromPacketError, CharGrid, Header, Origin, Packet, + Tiles, TryIntoPacketError, TypedCommand, }; /// Show text on the screen. @@ -16,21 +16,23 @@ use crate::{ /// let grid = CharGrid::from("Hello,\nWorld!"); /// connection.send(CharGridCommand { origin: Origin::ZERO, grid }).expect("send failed"); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CharGridCommand { - /// which tile the text should start - pub origin: Origin, /// the text to send to the display pub grid: CharGrid, + /// which tile the text should start on + pub origin: Origin, } -impl From for Packet { - fn from(value: CharGridCommand) -> Self { - Packet::origin_grid_to_packet( +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: CharGridCommand) -> Result { + Ok(Packet::origin_grid_to_packet( value.origin, value.grid, CommandCode::Utf8Data, - ) + )?) } } @@ -82,9 +84,11 @@ impl From for TypedCommand { #[cfg(test)] mod tests { - use crate::commands::tests::round_trip; + use crate::commands::tests::{round_trip, TestImplementsCommand}; use crate::{CharGrid, CharGridCommand, Origin}; + impl TestImplementsCommand for CharGridCommand {} + #[test] fn round_trip_utf8_data() { round_trip( diff --git a/src/commands/clear.rs b/src/commands/clear.rs index 538d261..b7f1b5b 100644 --- a/src/commands/clear.rs +++ b/src/commands/clear.rs @@ -1,6 +1,6 @@ use crate::{ command_code::CommandCode, commands::check_command_code_only, - commands::TryFromPacketError, Packet, TypedCommand, + commands::errors::TryFromPacketError, Packet, TypedCommand, }; use std::fmt::Debug; @@ -12,7 +12,7 @@ use std::fmt::Debug; /// # use servicepoint::*; /// # let connection = FakeConnection; /// connection.send(ClearCommand).unwrap(); -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] /// ``` pub struct ClearCommand; @@ -43,8 +43,11 @@ impl From for TypedCommand { #[cfg(test)] mod tests { use super::*; + use crate::commands::tests::TestImplementsCommand; use crate::Header; + impl TestImplementsCommand for ClearCommand {} + #[test] fn round_trip() { crate::commands::tests::round_trip(ClearCommand.into()); diff --git a/src/commands/cp437_grid.rs b/src/commands/cp437_grid.rs index 7ac1987..dbab68c 100644 --- a/src/commands/cp437_grid.rs +++ b/src/commands/cp437_grid.rs @@ -1,7 +1,7 @@ use crate::{ command_code::CommandCode, commands::check_command_code, - commands::TryFromPacketError, Cp437Grid, Header, Origin, Packet, Tiles, - TypedCommand, + commands::errors::TryFromPacketError, Cp437Grid, Header, Origin, Packet, + Tiles, TryIntoPacketError, TypedCommand, }; /// Show text on the screen. @@ -27,21 +27,23 @@ use crate::{ /// connection.send(Cp437GridCommand{ origin: Origin::new(2, 2), grid }).unwrap(); /// ``` /// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437 -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Cp437GridCommand { - /// which tile the text should start - pub origin: Origin, /// the text to send to the display pub grid: Cp437Grid, + /// which tile the text should start + pub origin: Origin, } -impl From for Packet { - fn from(value: Cp437GridCommand) -> Self { - Packet::origin_grid_to_packet( +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: Cp437GridCommand) -> Result { + Ok(Packet::origin_grid_to_packet( value.origin, value.grid, CommandCode::Cp437Data, - ) + )?) } } @@ -91,7 +93,9 @@ impl From for TypedCommand { #[cfg(test)] mod tests { use super::*; - use crate::commands::tests::round_trip; + use crate::commands::tests::{round_trip, TestImplementsCommand}; + + impl TestImplementsCommand for Cp437GridCommand {} #[test] fn round_trip_cp437_data() { diff --git a/src/commands/errors.rs b/src/commands/errors.rs new file mode 100644 index 0000000..531eac6 --- /dev/null +++ b/src/commands/errors.rs @@ -0,0 +1,44 @@ +use crate::LoadBitmapError; +use std::num::TryFromIntError; + +/// Err values for [crate::TypedCommand::try_from]. +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum TryFromPacketError { + /// the contained command code does not correspond to a known command + #[error("The command code {0:?} does not correspond to a known command")] + InvalidCommand(u16), + /// the expected payload size was n, but size m was found + #[error("the expected payload size was {0}, but size {1} was found")] + UnexpectedPayloadSize(usize, usize), + /// Header fields not needed for the command have been used. + /// + /// Note that these commands would usually still work on the actual display. + #[error("Header fields not needed for the command have been used")] + ExtraneousHeaderValues, + /// The contained compression code is not known. This could be of disabled features. + #[error("The compression code {0:?} does not correspond to a known compression algorithm.")] + InvalidCompressionCode(u16), + /// Decompression of the payload failed. This can be caused by corrupted packets. + #[error("The decompression of the payload failed")] + DecompressionFailed, + /// The given brightness value is out of bounds + #[error("The given brightness value {0} is out of bounds.")] + InvalidBrightness(u8), + /// Some provided text was not valid UTF-8. + #[error(transparent)] + InvalidUtf8(#[from] std::string::FromUtf8Error), + /// The bitmap contained in the payload could not be loaded + #[error(transparent)] + LoadBitmapFailed(#[from] LoadBitmapError), +} + +/// An error that can occur when parsing a raw packet as a command +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum TryIntoPacketError { + /// Compression of the payload failed. + #[error("The compression of the payload failed")] + CompressionFailed, + /// Conversion (probably to u16) failed + #[error(transparent)] + ConversionError(#[from] TryFromIntError), +} diff --git a/src/commands/fade_out.rs b/src/commands/fade_out.rs index 42dbb94..fd03aa6 100644 --- a/src/commands/fade_out.rs +++ b/src/commands/fade_out.rs @@ -1,6 +1,6 @@ use crate::{ command_code::CommandCode, commands::check_command_code_only, - commands::TryFromPacketError, Packet, TypedCommand, + commands::errors::TryFromPacketError, Packet, TypedCommand, }; use std::fmt::Debug; @@ -15,7 +15,7 @@ use std::fmt::Debug; /// # let connection = FakeConnection; /// connection.send(FadeOutCommand).unwrap(); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FadeOutCommand; impl TryFrom for FadeOutCommand { @@ -45,10 +45,11 @@ impl From for TypedCommand { #[cfg(test)] mod tests { use crate::command_code::CommandCode; - use crate::commands::tests::round_trip; - use crate::{ - FadeOutCommand, Header, Packet, TryFromPacketError, TypedCommand, - }; + use crate::commands::errors::TryFromPacketError; + use crate::commands::tests::{round_trip, TestImplementsCommand}; + use crate::{ClearCommand, FadeOutCommand, Header, Packet, TypedCommand}; + + impl TestImplementsCommand for FadeOutCommand {} #[test] fn round_trip_fade_out() { diff --git a/src/commands/hard_reset.rs b/src/commands/hard_reset.rs index 0459ec4..ba1d855 100644 --- a/src/commands/hard_reset.rs +++ b/src/commands/hard_reset.rs @@ -1,6 +1,6 @@ use crate::{ command_code::CommandCode, commands::check_command_code_only, - commands::TryFromPacketError, Packet, TypedCommand, + commands::errors::TryFromPacketError, Packet, TypedCommand, }; use std::fmt::Debug; @@ -15,7 +15,7 @@ use std::fmt::Debug; /// # let connection = FakeConnection; /// connection.send(HardResetCommand).unwrap(); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct HardResetCommand; impl TryFrom for HardResetCommand { @@ -46,9 +46,11 @@ impl From for TypedCommand { #[cfg(test)] mod test { use super::*; - use crate::commands::tests::round_trip; + use crate::commands::tests::{round_trip, TestImplementsCommand}; use crate::Header; + impl TestImplementsCommand for HardResetCommand {} + #[test] fn round_trip_hard_reset() { round_trip(HardResetCommand.into()); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 80daac9..091c8dc 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ mod brightness_grid; mod char_grid; mod clear; mod cp437_grid; +mod errors; mod fade_out; mod hard_reset; mod typed; @@ -22,6 +23,7 @@ pub use brightness_grid::*; pub use char_grid::*; pub use clear::*; pub use cp437_grid::*; +pub use errors::*; pub use fade_out::*; pub use hard_reset::*; pub use typed::*; @@ -74,9 +76,12 @@ pub use typed::*; /// # let connection = FakeConnection; /// connection.send(command).unwrap(); /// ``` -pub trait Command: Debug + Clone + PartialEq + Into {} +pub trait Command: + Debug + Clone + Eq + TryInto + TryFrom +{ +} -impl> Command for T {} +impl + TryFrom> Command for T {} fn check_command_code_only( packet: Packet, @@ -121,8 +126,10 @@ fn check_command_code( mod tests { use crate::*; + pub(crate) trait TestImplementsCommand: Command {} + pub(crate) fn round_trip(original: TypedCommand) { - let packet: Packet = original.clone().into(); + let packet: Packet = original.clone().try_into().unwrap(); let copy: TypedCommand = match TypedCommand::try_from(packet) { Ok(command) => command, Err(err) => panic!("could not reload {original:?}: {err:?}"), diff --git a/src/commands/typed.rs b/src/commands/typed.rs index 9e263c3..434915a 100644 --- a/src/commands/typed.rs +++ b/src/commands/typed.rs @@ -1,70 +1,31 @@ use crate::{ - command_code::CommandCode, BitVecCommand, BitmapCommand, BrightnessCommand, - BrightnessGridCommand, CharGridCommand, ClearCommand, Cp437GridCommand, - FadeOutCommand, HardResetCommand, Header, LoadBitmapError, Packet, + command_code::CommandCode, commands::errors::TryFromPacketError, + BitVecCommand, BitmapCommand, BrightnessCommand, BrightnessGridCommand, + CharGridCommand, ClearCommand, Cp437GridCommand, FadeOutCommand, + HardResetCommand, Header, Packet, TryIntoPacketError, }; /// This enum contains all commands provided by the library. /// This is useful in case you want one data type for all kinds of commands without using `dyn`. /// /// Please look at the contained structs for documentation per command. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[allow(missing_docs)] +#[allow(deprecated)] pub enum TypedCommand { Clear(ClearCommand), - CharGrid(CharGridCommand), - Cp437Grid(Cp437GridCommand), - Bitmap(BitmapCommand), - Brightness(BrightnessCommand), - BrightnessGrid(BrightnessGridCommand), - BitVec(BitVecCommand), - HardReset(HardResetCommand), - FadeOut(FadeOutCommand), - - #[allow(deprecated)] #[deprecated] BitmapLegacy(crate::BitmapLegacyCommand), } -/// Err values for [TypedCommand::try_from]. -#[derive(Debug, PartialEq, thiserror::Error)] -pub enum TryFromPacketError { - /// the contained command code does not correspond to a known command - #[error("The command code {0:?} does not correspond to a known command")] - InvalidCommand(u16), - /// the expected payload size was n, but size m was found - #[error("the expected payload size was {0}, but size {1} was found")] - UnexpectedPayloadSize(usize, usize), - /// Header fields not needed for the command have been used. - /// - /// Note that these commands would usually still work on the actual display. - #[error("Header fields not needed for the command have been used")] - ExtraneousHeaderValues, - /// The contained compression code is not known. This could be of disabled features. - #[error("The compression code {0:?} does not correspond to a known compression algorithm.")] - InvalidCompressionCode(u16), - /// Decompression of the payload failed. This can be caused by corrupted packets. - #[error("The decompression of the payload failed")] - DecompressionFailed, - /// The given brightness value is out of bounds - #[error("The given brightness value {0} is out of bounds.")] - InvalidBrightness(u8), - /// Some provided text was not valid UTF-8. - #[error(transparent)] - InvalidUtf8(#[from] std::string::FromUtf8Error), - /// The bitmap contained in the payload could not be loaded - #[error(transparent)] - LoadBitmapFailed(#[from] LoadBitmapError), -} - impl TryFrom for TypedCommand { type Error = TryFromPacketError; @@ -136,27 +97,33 @@ impl TryFrom for TypedCommand { } } -impl From for Packet { - fn from(command: TypedCommand) -> Self { - match command { +impl TryFrom for Packet { + type Error = TryIntoPacketError; + + fn try_from(value: TypedCommand) -> Result { + Ok(match value { TypedCommand::Clear(c) => c.into(), - TypedCommand::CharGrid(c) => c.into(), - TypedCommand::Cp437Grid(c) => c.into(), - TypedCommand::Bitmap(c) => c.into(), + TypedCommand::CharGrid(c) => c.try_into()?, + TypedCommand::Cp437Grid(c) => c.try_into()?, + TypedCommand::Bitmap(c) => c.try_into()?, TypedCommand::Brightness(c) => c.into(), - TypedCommand::BrightnessGrid(c) => c.into(), - TypedCommand::BitVec(c) => c.into(), + TypedCommand::BrightnessGrid(c) => c.try_into()?, + TypedCommand::BitVec(c) => c.try_into()?, TypedCommand::HardReset(c) => c.into(), TypedCommand::FadeOut(c) => c.into(), #[allow(deprecated)] TypedCommand::BitmapLegacy(c) => c.into(), - } + }) } } #[cfg(test)] mod tests { - use crate::{Header, Packet, TryFromPacketError, TypedCommand}; + use crate::commands::errors::TryFromPacketError; + use crate::commands::tests::TestImplementsCommand; + use crate::{Header, Packet, TypedCommand}; + + impl TestImplementsCommand for TypedCommand {} #[test] fn error_invalid_command() { diff --git a/src/compression.rs b/src/compression.rs index 4686f90..e02930c 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -5,6 +5,7 @@ use std::io::{Read, Write}; use bzip2::read::{BzDecoder, BzEncoder}; #[cfg(feature = "compression_zlib")] use flate2::{FlushCompress, FlushDecompress, Status}; +use log::error; #[cfg(feature = "compression_zstd")] use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; @@ -67,28 +68,39 @@ pub(crate) fn into_decompressed( } } -#[allow(clippy::unwrap_used)] pub(crate) fn into_compressed( kind: CompressionCode, payload: Payload, -) -> Payload { +) -> Option { match kind { - CompressionCode::Uncompressed => payload, + CompressionCode::Uncompressed => Some(payload), #[cfg(feature = "compression_zlib")] CompressionCode::Zlib => { let mut compress = flate2::Compress::new(flate2::Compression::fast(), true); let mut buffer = [0u8; 10000]; - match compress - .compress(&payload, &mut buffer, FlushCompress::Finish) - .expect("compress failed") - { - Status::Ok => panic!("buffer should be big enough"), - Status::BufError => panic!("BufError"), - Status::StreamEnd => {} - }; - buffer[..compress.total_out() as usize].to_owned() + match compress.compress( + &payload, + &mut buffer, + FlushCompress::Finish, + ) { + Ok(Status::Ok) => { + error!("buffer not big enough"); + None + } + Ok(Status::BufError) => { + error!("Could not compress: {:?}", Status::BufError); + None + } + Ok(Status::StreamEnd) => { + Some(buffer[..compress.total_out() as usize].to_owned()) + } + Err(_) => { + error!("compress returned err"); + None + } + } } #[cfg(feature = "compression_bzip2")] CompressionCode::Bzip2 => { @@ -96,21 +108,39 @@ pub(crate) fn into_compressed( BzEncoder::new(&*payload, bzip2::Compression::fast()); let mut compressed = vec![]; match encoder.read_to_end(&mut compressed) { - Err(err) => panic!("could not compress payload: {}", err), - Ok(_) => compressed, + Err(err) => { + error!("Could not compress: {:?}", err); + None + } + Ok(_) => Some(compressed), } } #[cfg(feature = "compression_lzma")] - CompressionCode::Lzma => lzma::compress(&payload, 6).unwrap(), + CompressionCode::Lzma => match lzma::compress(&payload, 6) { + Ok(payload) => Some(payload), + Err(e) => { + error!("Could not compress: {e:?}"); + None + } + }, #[cfg(feature = "compression_zstd")] CompressionCode::Zstd => { + let buf = Vec::with_capacity(payload.len()); let mut encoder = - ZstdEncoder::new(vec![], zstd::DEFAULT_COMPRESSION_LEVEL) - .expect("could not create encoder"); - encoder - .write_all(&payload) - .expect("could not compress payload"); - encoder.finish().expect("could not finish encoding") + match ZstdEncoder::new(buf, zstd::DEFAULT_COMPRESSION_LEVEL) { + Err(e) => { + error!("failed to create decoder: {e:?}"); + return None; + } + Ok(encoder) => encoder, + }; + + if let Err(e) = encoder.write_all(&payload) { + error!("failed to decompress payload: {e:?}"); + return None; + } + + encoder.finish().ok() } } } diff --git a/src/compression_code.rs b/src/compression_code.rs index 8dadc5d..4091f28 100644 --- a/src/compression_code.rs +++ b/src/compression_code.rs @@ -21,7 +21,7 @@ /// }; /// ``` #[repr(u16)] -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CompressionCode { /// no compression Uncompressed = 0x0, diff --git a/src/connections/fake.rs b/src/connections/fake.rs index d84303d..dac14f5 100644 --- a/src/connections/fake.rs +++ b/src/connections/fake.rs @@ -1,16 +1,28 @@ -use crate::{Connection, Packet}; +use crate::{Connection, Packet, SendError}; use log::debug; +use std::{convert::Infallible, error::Error, fmt::Debug}; #[derive(Debug)] /// A fake connection for testing that does not actually send anything. pub struct FakeConnection; impl Connection for FakeConnection { - // TODO: () does not implement Error+Debug, some placeholder is needed - type Error = std::io::Error; + type TransportError = Infallible; - fn send(&self, packet: impl Into) -> Result<(), Self::Error> { - let data: Vec = packet.into().into(); + fn send>( + &self, + packet: P, + ) -> Result< + (), + SendError<

>::Error, Self::TransportError>, + > + where +

>::Error: Error + Debug, + { + let data: Vec = packet + .try_into() + .map(Into::>::into) + .map_err(SendError::IntoPacket)?; debug!("Sending fake packet: {data:?}"); Ok(()) } diff --git a/src/connections/mod.rs b/src/connections/mod.rs index e71887c..4837105 100644 --- a/src/connections/mod.rs +++ b/src/connections/mod.rs @@ -1,8 +1,7 @@ //! This module contains the [Connection] trait and all implementations provided in this library. use crate::Packet; -use std::error::Error; -use std::fmt::Debug; +use std::{error::Error, fmt::Debug}; mod fake; #[cfg(feature = "protocol_udp")] @@ -16,6 +15,20 @@ pub use udp::*; #[cfg(feature = "protocol_websocket")] pub use websocket::*; +/// An error that can happen when sending a command +#[derive(Debug, thiserror::Error)] +pub enum SendError< + IntoPacketError: Error + Debug, + TransportError: Error + Debug, +> { + /// An error occurred while sending the bytes via the underlying transport + #[error("An error occurred while sending the bytes via the underlying transport: {0:?}")] + Transport(TransportError), + /// An error occurred while preparing the data to send + #[error("An error occurred while preparing the data to send: {0:?}")] + IntoPacket(IntoPacketError), +} + /// A connection to the display. /// /// Used to send [Packets][Packet] or [Commands][crate::Command]. @@ -30,7 +43,7 @@ pub use websocket::*; /// ``` pub trait Connection: Debug { /// The error that can occur when sending a packet - type Error: Error + Debug; + type TransportError: Error + Debug; /// Send something packet-like to the display. Usually this is in the form of a Command. /// @@ -49,5 +62,13 @@ pub trait Connection: Debug { /// connection.send(servicepoint::ClearCommand) /// .expect("send failed"); /// ``` - fn send(&self, packet: impl Into) -> Result<(), Self::Error>; + fn send>( + &self, + packet: P, + ) -> Result< + (), + SendError<

>::Error, Self::TransportError>, + > + where +

>::Error: Error + Debug; } diff --git a/src/connections/udp.rs b/src/connections/udp.rs index 900d159..cd3239f 100644 --- a/src/connections/udp.rs +++ b/src/connections/udp.rs @@ -1,6 +1,5 @@ -use crate::{Connection, Packet}; -use std::fmt::Debug; -use std::net::UdpSocket; +use crate::{Connection, Packet, SendError}; +use std::{error::Error, fmt::Debug, net::UdpSocket}; /// A connection using the UDP protocol. /// @@ -39,11 +38,26 @@ impl UdpConnection { } impl Connection for UdpConnection { - type Error = std::io::Error; + type TransportError = std::io::Error; - fn send(&self, packet: impl Into) -> Result<(), Self::Error> { - let data: Vec = packet.into().into(); - self.socket.send(&data).map(move |_| ()) // ignore Ok value + fn send>( + &self, + packet: P, + ) -> Result< + (), + SendError<

>::Error, Self::TransportError>, + > + where +

>::Error: Error + Debug, + { + let data: Vec = packet + .try_into() + .map(Into::>::into) + .map_err(SendError::IntoPacket)?; + self.socket + .send(&data) + .map(move |_| ()) + .map_err(SendError::Transport) // ignore Ok value } } diff --git a/src/connections/websocket.rs b/src/connections/websocket.rs index 2d4b6b7..118dd5f 100644 --- a/src/connections/websocket.rs +++ b/src/connections/websocket.rs @@ -1,4 +1,5 @@ -use crate::{Connection, Packet}; +use crate::{Connection, Packet, SendError}; +use std::{error::Error, fmt::Debug}; /// A connection using the WebSocket protocol. /// @@ -20,12 +21,27 @@ pub struct WebsocketConnection( ); impl Connection for WebsocketConnection { - type Error = tungstenite::Error; + type TransportError = tungstenite::Error; - fn send(&self, packet: impl Into) -> Result<(), Self::Error> { - let data: Vec = packet.into().into(); + fn send>( + &self, + packet: P, + ) -> Result< + (), + SendError<

>::Error, Self::TransportError>, + > + where +

>::Error: Error + Debug, + { + let data: Vec = packet + .try_into() + .map(Into::>::into) + .map_err(SendError::IntoPacket)? + .into(); let mut socket = self.0.lock().unwrap(); - socket.send(tungstenite::Message::Binary(data.into())) + socket + .send(tungstenite::Message::Binary(data.into())) + .map_err(SendError::Transport) } } @@ -63,6 +79,6 @@ impl WebsocketConnection { impl Drop for WebsocketConnection { fn drop(&mut self) { - _ = self.0.try_lock().map(move |mut sock| sock.close(None)); + drop(self.0.try_lock().map(move |mut sock| sock.close(None))); } } diff --git a/src/containers/bitmap.rs b/src/containers/bitmap.rs index e4af09a..ed1bd7a 100644 --- a/src/containers/bitmap.rs +++ b/src/containers/bitmap.rs @@ -153,6 +153,7 @@ impl Bitmap { /// pixel.set(index % 2 == 0) /// } /// ``` + #[must_use] pub fn iter_mut(&mut self) -> IterMut { self.bit_vec.iter_mut() } @@ -258,6 +259,7 @@ impl From<&Bitmap> for ValueGrid { } } +#[must_use] struct IterRows<'t> { bitmap: &'t Bitmap, row: usize, diff --git a/src/containers/char_grid.rs b/src/containers/char_grid.rs index f8d5403..a7d722b 100644 --- a/src/containers/char_grid.rs +++ b/src/containers/char_grid.rs @@ -33,6 +33,7 @@ impl CharGrid { /// # use servicepoint::CharGrid; /// let grid = CharGrid::wrap_str(2, "abc\ndef"); /// ``` + #[must_use] pub fn wrap_str(width: usize, text: &str) -> Self { let lines = text .split('\n') diff --git a/src/containers/value_grid.rs b/src/containers/value_grid.rs index 56bd321..ef85939 100644 --- a/src/containers/value_grid.rs +++ b/src/containers/value_grid.rs @@ -13,7 +13,7 @@ impl Value for T {} /// /// This structure can be used with any type that implements the [Value] trait. /// You can also use the concrete type aliases provided in this crate, e.g. [CharGrid] and [ByteGrid]. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ValueGrid { width: usize, height: usize, @@ -50,6 +50,7 @@ impl ValueGrid { /// - height: size in y-direction /// /// returns: [ValueGrid] initialized to default value. + #[must_use] pub fn new(width: usize, height: usize) -> Self { Self { data: vec![Default::default(); width * height], @@ -193,6 +194,7 @@ impl ValueGrid { /// ``` /// [Brightness]: [crate::Brightness] /// [Command]: [crate::Command] + #[must_use] pub fn map(&self, f: F) -> ValueGrid where TConverted: Value, @@ -358,11 +360,13 @@ impl Grid for ValueGrid { impl DataRef for ValueGrid { /// Get the underlying byte rows mutable + #[must_use] fn data_ref_mut(&mut self) -> &mut [T] { self.data.as_mut_slice() } /// Get the underlying byte rows read only + #[must_use] fn data_ref(&self) -> &[T] { self.data.as_slice() } @@ -376,6 +380,7 @@ impl From> for Vec { } /// An iterator iver the rows in a [ValueGrid] +#[must_use] pub struct IterGridRows<'t, T: Value> { grid: &'t ValueGrid, row: usize, diff --git a/src/origin.rs b/src/origin.rs index 28d93e7..0f0950e 100644 --- a/src/origin.rs +++ b/src/origin.rs @@ -2,7 +2,7 @@ use crate::TILE_SIZE; use std::marker::PhantomData; /// An origin marks the top left position of a window sent to the display. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Origin { /// position in the width direction pub x: usize, @@ -44,11 +44,11 @@ impl std::ops::Add> for Origin { pub trait DisplayUnit {} /// Marks something to be measured in number of pixels. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Pixels(); /// Marks something to be measured in number of iles. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Tiles(); impl DisplayUnit for Pixels {} @@ -86,6 +86,12 @@ impl TryFrom<&Origin> for Origin { } } +impl Default for Origin { + fn default() -> Self { + Self::ZERO + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/packet.rs b/src/packet.rs index 14c35d7..8c325b5 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -23,9 +23,8 @@ //! let packet = Packet::try_from(bytes).expect("could not read packet from bytes"); //! ``` -use crate::command_code::CommandCode; -use crate::{Grid, Origin, Tiles}; -use std::mem::size_of; +use crate::{command_code::CommandCode, Grid, Origin, Tiles}; +use std::{mem::size_of, num::TryFromIntError}; /// A raw header. /// @@ -144,17 +143,17 @@ impl Packet { origin: Origin, grid: impl Grid + Into, command_code: CommandCode, - ) -> Packet { - Packet { + ) -> Result { + Ok(Packet { header: Header { command_code: command_code.into(), - a: origin.x as u16, - b: origin.y as u16, - c: grid.width() as u16, - d: grid.height() as u16, + a: origin.x.try_into()?, + b: origin.y.try_into()?, + c: grid.width().try_into()?, + d: grid.height().try_into()?, }, payload: grid.into(), - } + }) } pub(crate) fn command_code_only(c: CommandCode) -> Self {