From dc7f70e01b8eaf0bc7b88106019ae3c9881ed9fc Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Tue, 10 Mar 2026 19:23:38 -0700 Subject: [PATCH] Reorganize parse.rs, don't expose Nom in errors --- src/error.rs | 21 +- src/parse.rs | 323 +++++++++--------- .../zztff__parse__tests__decode_snapshot.snap | 2 +- 3 files changed, 174 insertions(+), 172 deletions(-) diff --git a/src/error.rs b/src/error.rs index b469f6c..5167a6c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,17 +12,23 @@ pub enum DecodeError { #[error("negative stat count")] NegativeStatCount, - #[error("nom error: {0}")] - NomError(String), + #[error("unexpected end of file")] + UnexpectedEof, + + #[error("unknown decode error")] + Unknown, } impl NomParseError for DecodeError { fn from_error_kind(_input: I, kind: ErrorKind) -> Self { - Self::NomError(kind.description().to_string()) + match kind { + ErrorKind::Eof => Self::UnexpectedEof, + _ => Self::Unknown, + } } - fn append(_input: I, kind: ErrorKind, other: Self) -> Self { - Self::NomError(format!("{}: {:?}", other, kind)) + fn append(_input: I, _kind: ErrorKind, other: Self) -> Self { + other } } @@ -30,16 +36,13 @@ 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)), + nom::Err::Incomplete(_) => Self::UnexpectedEof, } } } #[derive(Debug, Error)] pub enum EncodeError { - #[error("invalid tile count: {0}")] - InvalidTileCount(usize), - #[error("string too long")] StringTooLong { max: u8 }, diff --git a/src/parse.rs b/src/parse.rs index c4f2085..33cdb20 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -11,28 +11,85 @@ use super::elements::Element; use super::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline}; use super::error::{DecodeError, EncodeError}; -/// A single tile on a ZZT board. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct Tile { - pub element: u8, - pub color: u8, +/// A ZZT world file. +#[derive(Clone, Debug)] +pub struct World { + pub ammo: i16, + pub gems: i16, + pub keys: [bool; 7], + pub health: i16, + pub starting_board: i16, + pub torches: i16, + pub torch_cycles: i16, + pub energizer_cycles: i16, + pub score: i16, + pub name: String, + pub flags: [String; 10], + pub time: i16, + pub time_ticks: i16, + pub saved_game: bool, + pub boards: Vec, } -/// A stat's ZZT-OOP program. -/// -/// In ZZT, stats can either have their own code or bind to another stat's code. -/// This enum prevents invalid states where both are set. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Program { - /// The stat owns its own code. - Own(String), - /// The stat is bound to another stat (index into the stats list). - Bound(NonZero), -} - -impl Default for Program { +impl Default for World { fn default() -> Self { - Program::Own(String::new()) + World { + ammo: 0, + gems: 0, + keys: [false; 7], + health: 100, + starting_board: 0, + torches: 0, + torch_cycles: 0, + energizer_cycles: 0, + score: 0, + name: String::new(), + flags: Default::default(), + time: 0, + time_ticks: 0, + saved_game: false, + boards: Vec::new(), + } + } +} + +/// A ZZT board. +#[derive(Debug, Clone)] +pub struct Board { + pub name: String, + pub tiles: [Tile; 1500], + pub max_shots: u8, + pub is_dark: bool, + pub exit_north: Option>, + pub exit_south: Option>, + pub exit_west: Option>, + pub exit_east: Option>, + pub restart_on_zap: bool, + pub message: String, + pub enter_x: u8, + pub enter_y: u8, + pub time_limit: i16, + pub stats: Vec, +} + +impl Default for Board { + fn default() -> Self { + Board { + name: String::new(), + tiles: [Tile::default(); 1500], + max_shots: 255, + is_dark: false, + exit_north: None, + exit_south: None, + exit_west: None, + exit_east: None, + restart_on_zap: false, + message: String::new(), + enter_x: 1, + enter_y: 1, + time_limit: 0, + stats: Vec::new(), + } } } @@ -77,141 +134,28 @@ impl Default for Stat { } } -/// A ZZT board. -#[derive(Debug, Clone)] -pub struct Board { - pub name: String, - pub tiles: Vec, - pub max_shots: u8, - pub is_dark: bool, - pub exit_north: Option>, - pub exit_south: Option>, - pub exit_west: Option>, - pub exit_east: Option>, - pub restart_on_zap: bool, - pub message: String, - pub enter_x: u8, - pub enter_y: u8, - pub time_limit: i16, - pub stats: Vec, +/// A single tile on a ZZT board. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Tile { + pub element: u8, + pub color: u8, } -impl Default for Board { +/// A stat's ZZT-OOP program. +/// +/// In ZZT, stats can either have their own code or bind to another stat's code. +/// This enum prevents invalid states where both are set. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Program { + /// The stat owns its own code. + Own(String), + /// The stat is bound to another stat (index into the stats list). + Bound(NonZero), +} + +impl Default for Program { fn default() -> Self { - Board { - name: String::new(), - tiles: Vec::new(), - max_shots: 255, - is_dark: false, - exit_north: None, - exit_south: None, - exit_west: None, - exit_east: None, - restart_on_zap: false, - message: String::new(), - enter_x: 1, - enter_y: 1, - time_limit: 0, - stats: Vec::new(), - } - } -} - -/// A ZZT world file. -#[derive(Clone, Debug)] -pub struct World { - pub ammo: i16, - pub gems: i16, - pub keys: [bool; 7], - pub health: i16, - pub starting_board: i16, - pub torches: i16, - pub torch_cycles: i16, - pub energizer_cycles: i16, - pub score: i16, - pub name: String, - pub flags: [String; 10], - pub time: i16, - pub time_ticks: i16, - pub locked: bool, - pub boards: Vec, -} - -impl Default for World { - fn default() -> Self { - World { - ammo: 0, - gems: 0, - keys: [false; 7], - health: 100, - starting_board: 0, - torches: 0, - torch_cycles: 0, - energizer_cycles: 0, - score: 0, - name: String::new(), - flags: Default::default(), - time: 0, - time_ticks: 0, - locked: false, - boards: Vec::new(), - } - } -} - -// Parsing helpers - -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, 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)?; - let (input, _) = take(cap - actual_len)(input)?; - Ok((input, decode_oneline(data))) - } -} - -fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], DecodeError> { - let (_, size) = le_u16.parse(bytes)?; - take(size as usize + 2).parse(bytes) -} - -// Serialization helpers - -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<(), EncodeError>; - fn push_padding(&mut self, size: usize); -} - -impl SerializationHelpers for Vec { - fn push_bool(&mut self, value: bool) { - self.push(if value { 1 } else { 0 }); - } - - fn push_i16(&mut self, value: i16) { - self.extend(value.to_le_bytes()); - } - - fn push_string(&mut self, cap: u8, value: &str) -> Result<(), EncodeError> { - let bytes = encode_oneline(value)?; - if bytes.len() > cap as usize { - return Err(EncodeError::StringTooLong { max: cap }); - } - self.push(bytes.len() as u8); - self.extend_from_slice(&bytes); - self.push_padding(cap as usize - bytes.len()); - Ok(()) - } - - fn push_padding(&mut self, size: usize) { - self.resize(self.len() + size, 0); + Program::Own(String::new()) } } @@ -227,7 +171,7 @@ impl World { (le_i16, le_i16, le_i16, le_i16, le_i16).parse(input)?; let (input, (_, score, name)) = (take(2usize), le_i16, pstring(20)).parse(input)?; let (input, flags) = count(pstring(20), 10).parse(input)?; - let (_input, (time, time_ticks, locked)) = (le_i16, le_i16, bool_u8).parse(input)?; + let (_input, (time, time_ticks, saved_game)) = (le_i16, le_i16, bool_u8).parse(input)?; // Rest of header is padding; fast-forward starting from original input let (input, _) = take(512usize).parse(bytes)?; @@ -255,7 +199,7 @@ impl World { flags: flags.try_into().unwrap(), time, time_ticks, - locked, + saved_game, boards, }) } @@ -283,7 +227,7 @@ impl World { } result.push_i16(self.time); result.push_i16(self.time_ticks); - result.push_bool(self.locked); + result.push_bool(self.saved_game); result.push_padding(512 - result.len()); for board in &self.boards { @@ -305,16 +249,18 @@ impl Board { // Read terrain const NUM_TILES: usize = 60 * 25; let mut input = input; - let mut tiles = Vec::with_capacity(NUM_TILES); - while tiles.len() < NUM_TILES { + let mut tiles = [Tile::default(); NUM_TILES]; + let mut tile_index = 0; + while tile_index < NUM_TILES { let (next_input, (count, element, color)) = (le_u8, le_u8, le_u8).parse(input)?; input = next_input; let count: usize = if count == 0 { 256 } else { count.into() }; for _ in 0..count { - tiles.push(Tile { element, color }); - if tiles.len() > NUM_TILES { - return Err(DecodeError::InvalidTileCount(tiles.len())); + if tile_index >= NUM_TILES { + return Err(DecodeError::InvalidTileCount(tile_index + 1)); } + tiles[tile_index] = Tile { element, color }; + tile_index += 1; } } @@ -368,9 +314,6 @@ impl Board { result.push_string(50, &self.name)?; // Encode terrain - if self.tiles.len() != 1500 { - return Err(EncodeError::InvalidTileCount(self.tiles.len())); - } let mut iter = self.tiles.iter().peekable(); while let Some(tile) = iter.next() { let mut count: u8 = 1; @@ -493,6 +436,62 @@ impl Stat { } } +// Parsing helpers + +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, 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)?; + let (input, _) = take(cap - actual_len)(input)?; + Ok((input, decode_oneline(data))) + } +} + +fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], DecodeError> { + let (_, size) = le_u16.parse(bytes)?; + take(size as usize + 2).parse(bytes) +} + +// Serialization helpers + +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<(), EncodeError>; + fn push_padding(&mut self, size: usize); +} + +impl SerializationHelpers for Vec { + fn push_bool(&mut self, value: bool) { + self.push(if value { 1 } else { 0 }); + } + + fn push_i16(&mut self, value: i16) { + self.extend(value.to_le_bytes()); + } + + fn push_string(&mut self, cap: u8, value: &str) -> Result<(), EncodeError> { + let bytes = encode_oneline(value)?; + if bytes.len() > cap as usize { + return Err(EncodeError::StringTooLong { max: cap }); + } + self.push(bytes.len() as u8); + self.extend_from_slice(&bytes); + self.push_padding(cap as usize - bytes.len()); + Ok(()) + } + + fn push_padding(&mut self, size: usize) { + self.resize(self.len() + size, 0); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/snapshots/zztff__parse__tests__decode_snapshot.snap b/src/snapshots/zztff__parse__tests__decode_snapshot.snap index d140027..61180c3 100644 --- a/src/snapshots/zztff__parse__tests__decode_snapshot.snap +++ b/src/snapshots/zztff__parse__tests__decode_snapshot.snap @@ -35,7 +35,7 @@ World { ], time: 1007, time_ticks: 0, - locked: true, + saved_game: true, boards: [ Board { name: "Title screen",