From d73faf64c7d42451e00af77870507ad7c5004bab Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Tue, 10 Mar 2026 00:08:51 -0700 Subject: [PATCH] Split error enum into Encode vs. Decode errors --- src/encoding.rs | 10 +++++----- src/error.rs | 32 +++++++++++++++++++------------- src/lib.rs | 2 +- src/parse.rs | 42 +++++++++++++++++++++--------------------- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/encoding.rs b/src/encoding.rs index 6060631..90a4f1a 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,10 +1,10 @@ use codepage_437::CP437_WINGDINGS; -use super::error::ParseError; +use super::error::EncodeError; /// Serialize a multi-line code string for an object or a scroll. /// Uses ZZT's convention of CR-terminated lines. -pub fn encode_multiline(input: &str) -> Result, ParseError> { +pub fn encode_multiline(input: &str) -> Result, EncodeError> { input .chars() .map(|c| { @@ -13,7 +13,7 @@ pub fn encode_multiline(input: &str) -> Result, ParseError> { } else { CP437_WINGDINGS .encode(c) - .ok_or_else(|| ParseError::EncodingError(c)) + .ok_or_else(|| EncodeError::EncodingError(c)) } }) .collect() @@ -35,13 +35,13 @@ pub fn decode_multiline(input: &[u8]) -> String { /// Serialize a single-line string for a board title. /// Newlines cannot be encoded because ZZT interprets them as dingbats (♪). -pub fn encode_oneline(input: &str) -> Result, ParseError> { +pub fn encode_oneline(input: &str) -> Result, EncodeError> { input .chars() .map(|c| { CP437_WINGDINGS .encode(c) - .ok_or_else(|| ParseError::EncodingError(c)) + .ok_or_else(|| EncodeError::EncodingError(c)) }) .collect() } diff --git a/src/error.rs b/src/error.rs index 50559a8..b469f6c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,7 @@ use nom::error::{ErrorKind, ParseError as NomParseError}; use thiserror::Error; #[derive(Debug, Error)] -pub enum ParseError { +pub enum DecodeError { #[error("invalid ZZT file magic")] InvalidMagic, @@ -12,20 +12,11 @@ pub enum ParseError { #[error("negative stat count")] NegativeStatCount, - #[error("string too long")] - StringTooLong { max: u8 }, - - #[error("cannot encode character: {0}")] - EncodingError(char), - - #[error("board data too large")] - BoardTooLarge, - #[error("nom error: {0}")] NomError(String), } -impl NomParseError for ParseError { +impl NomParseError for DecodeError { fn from_error_kind(_input: I, kind: ErrorKind) -> Self { Self::NomError(kind.description().to_string()) } @@ -35,11 +26,26 @@ impl NomParseError for ParseError { } } -impl From> for ParseError { - fn from(value: nom::Err) -> Self { +impl From> for DecodeError { + fn from(value: nom::Err) -> Self { match value { nom::Err::Error(e) | nom::Err::Failure(e) => e, nom::Err::Incomplete(needed) => Self::NomError(format!("incomplete: {:?}", needed)), } } } + +#[derive(Debug, Error)] +pub enum EncodeError { + #[error("invalid tile count: {0}")] + InvalidTileCount(usize), + + #[error("string too long")] + StringTooLong { max: u8 }, + + #[error("cannot encode character: {0}")] + EncodingError(char), + + #[error("board data too large")] + BoardTooLarge, +} diff --git a/src/lib.rs b/src/lib.rs index 4df7c74..75a8503 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,5 @@ mod parse; pub use elements::Element; pub use encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline}; -pub use error::ParseError; +pub use error::{DecodeError, EncodeError}; pub use parse::{Board, Program, Stat, Tile, World}; diff --git a/src/parse.rs b/src/parse.rs index 086dc76..52d3e80 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -9,7 +9,7 @@ use nom::{ use super::elements::Element; use super::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline}; -use super::error::ParseError; +use super::error::{DecodeError, EncodeError}; /// A single tile on a ZZT board. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -161,13 +161,13 @@ impl Default for World { // Parsing helpers -fn bool_u8(input: &[u8]) -> IResult<&[u8], bool, ParseError> { +fn bool_u8(input: &[u8]) -> IResult<&[u8], bool, DecodeError> { let (input, byte) = le_u8(input)?; Ok((input, byte != 0)) } -fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], String, ParseError> { - move |input: &[u8]| -> IResult<&[u8], String, ParseError> { +fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], String, DecodeError> { + move |input: &[u8]| -> IResult<&[u8], String, DecodeError> { let (input, len) = le_u8(input)?; let actual_len = len.min(cap); let (input, data) = take(actual_len)(input)?; @@ -176,7 +176,7 @@ fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], String, ParseError> { } } -fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], ParseError> { +fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], DecodeError> { let (_, size) = le_u16.parse(bytes)?; take(size as usize + 2).parse(bytes) } @@ -186,7 +186,7 @@ fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], ParseError> { trait SerializationHelpers { fn push_bool(&mut self, value: bool); fn push_i16(&mut self, value: i16); - fn push_string(&mut self, cap: u8, value: &str) -> Result<(), ParseError>; + fn push_string(&mut self, cap: u8, value: &str) -> Result<(), EncodeError>; fn push_padding(&mut self, size: usize); } @@ -199,10 +199,10 @@ impl SerializationHelpers for Vec { self.extend(value.to_le_bytes()); } - fn push_string(&mut self, cap: u8, value: &str) -> Result<(), ParseError> { + fn push_string(&mut self, cap: u8, value: &str) -> Result<(), EncodeError> { let bytes = encode_oneline(value)?; if bytes.len() > cap as usize { - return Err(ParseError::StringTooLong { max: cap }); + return Err(EncodeError::StringTooLong { max: cap }); } self.push(bytes.len() as u8); self.extend_from_slice(&bytes); @@ -217,10 +217,10 @@ impl SerializationHelpers for Vec { impl World { /// Parse a ZZT world from bytes. - pub fn from_bytes(bytes: &[u8]) -> Result { + pub fn from_bytes(bytes: &[u8]) -> Result { let (input, _) = tag(&[0xff, 0xff][..]) .parse(bytes) - .map_err(|_: nom::Err| ParseError::InvalidMagic)?; + .map_err(|_: nom::Err| DecodeError::InvalidMagic)?; let (input, num_boards) = le_i16.parse(input)?; let (input, (ammo, gems, keys)) = (le_i16, le_i16, count(bool_u8, 7)).parse(input)?; let (input, (health, starting_board, torches, torch_cycles, energizer_cycles)) = @@ -235,7 +235,7 @@ impl World { // Load boards let num_boards = num_boards as usize + 1; let (_input, chunks) = count(board_slice, num_boards).parse(input)?; - let boards: Result, ParseError> = chunks + let boards: Result, DecodeError> = chunks .iter() .map(|bytes: &&[u8]| Board::from_bytes(bytes)) .collect(); @@ -261,7 +261,7 @@ impl World { } /// Serialize this world to bytes. - pub fn to_bytes(&self) -> Result, ParseError> { + pub fn to_bytes(&self) -> Result, EncodeError> { let mut result = Vec::with_capacity(512); result.push_i16(-1); // file magic: ZZT world result.push_i16(self.boards.len() as i16 - 1); @@ -295,7 +295,7 @@ impl World { impl Board { /// Parse a board from bytes (including the 2-byte size header). - pub fn from_bytes(bytes: &[u8]) -> Result { + pub fn from_bytes(bytes: &[u8]) -> Result { // Ignore length bytes let (input, _) = le_u16.parse(bytes)?; @@ -313,7 +313,7 @@ impl Board { for _ in 0..count { tiles.push(Tile { element, color }); if tiles.len() > NUM_TILES { - return Err(ParseError::InvalidTileCount(tiles.len())); + return Err(DecodeError::InvalidTileCount(tiles.len())); } } } @@ -334,7 +334,7 @@ impl Board { let (input, num_stats) = le_i16(input)?; let num_stats = num_stats + 1; if num_stats < 0 { - return Err(ParseError::NegativeStatCount); + return Err(DecodeError::NegativeStatCount); } let (_input, stats) = count(Stat::parse, num_stats as usize).parse(input)?; @@ -357,19 +357,19 @@ impl Board { } /// Parse a standalone .brd file (same format, no world header). - pub fn from_brd_bytes(bytes: &[u8]) -> Result { + pub fn from_brd_bytes(bytes: &[u8]) -> Result { Board::from_bytes(bytes) } /// Serialize this board to bytes. - pub fn to_bytes(&self) -> Result, ParseError> { + pub fn to_bytes(&self) -> Result, EncodeError> { let mut result = vec![]; result.push_padding(2); // reserve space for board size result.push_string(50, &self.name)?; // Encode terrain if self.tiles.len() != 1500 { - return Err(ParseError::InvalidTileCount(self.tiles.len())); + return Err(EncodeError::InvalidTileCount(self.tiles.len())); } let mut iter = self.tiles.iter().peekable(); while let Some(tile) = iter.next() { @@ -407,7 +407,7 @@ impl Board { // Fix up board size let size: u16 = (result.len() - 2) .try_into() - .map_err(|_| ParseError::BoardTooLarge)?; + .map_err(|_| EncodeError::BoardTooLarge)?; result.splice(0..2, size.to_le_bytes()); Ok(result) @@ -415,7 +415,7 @@ impl Board { } impl Stat { - fn parse(input: &[u8]) -> IResult<&[u8], Self, ParseError> { + fn parse(input: &[u8]) -> IResult<&[u8], Self, DecodeError> { let (input, (x, y, x_step, y_step)) = (le_u8, le_u8, le_i16, le_i16).parse(input)?; let (input, (cycle, p1, p2, p3)) = (le_i16, le_u8, le_u8, le_u8).parse(input)?; let (input, (follower, leader)) = (le_i16, le_i16).parse(input)?; @@ -459,7 +459,7 @@ impl Stat { )) } - fn to_bytes(&self) -> Result, ParseError> { + fn to_bytes(&self) -> Result, EncodeError> { let mut result = vec![]; result.push(self.x); result.push(self.y);