use crate::types::{UnitRecord, UnitType};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ParseError {
#[error("File too small to contain header (need at least 4 bytes, got {actual})")]
FileTooSmall { actual: usize },
#[error(
"Unexpected end of file at record {record_index} (need {needed} bytes, have {available})"
)]
UnexpectedEndOfFile {
record_index: usize,
needed: usize,
available: usize,
},
#[error("Invalid unit data at record {record_index}: {message}")]
InvalidUnitData {
record_index: usize,
message: String,
},
#[error("Formation contains too many units: {count} (maximum allowed: {max_allowed})")]
FormationTooLarge { count: usize, max_allowed: usize },
#[error("Failed to read file: {0}")]
IoError(#[from] std::io::Error),
}
pub type ParseResult<T> = Result<T, ParseError>;
const MAX_UNITS: usize = 500;
pub struct AttackSetupParser;
impl AttackSetupParser {
pub fn parse(data: &[u8]) -> ParseResult<Vec<UnitRecord>> {
if data.len() < 4 {
return Err(ParseError::FileTooSmall { actual: data.len() });
}
let count_bytes: [u8; 4] = [data[0], data[1], data[2], data[3]];
let count = u32::from_le_bytes(count_bytes) as usize;
if count > MAX_UNITS {
return Err(ParseError::FormationTooLarge {
count,
max_allowed: MAX_UNITS,
});
}
let mut records = Vec::with_capacity(count);
let mut offset = 4;
for i in 0..count {
if offset + 3 > data.len() {
return Err(ParseError::UnexpectedEndOfFile {
record_index: i,
needed: 3,
available: data.len().saturating_sub(offset),
});
}
let unit_type_raw = data[offset + 2];
let record_size = UnitType::record_size_for_raw_type(unit_type_raw);
if offset + record_size > data.len() {
return Err(ParseError::UnexpectedEndOfFile {
record_index: i,
needed: record_size,
available: data.len().saturating_sub(offset),
});
}
let x = data[offset];
let y = data[offset + 1];
let extra_data = if record_size > 3 {
&data[offset + 3..offset + record_size]
} else {
&[]
};
let unit_type = UnitType::from_bytes(unit_type_raw, extra_data).map_err(|message| {
ParseError::InvalidUnitData {
record_index: i,
message,
}
})?;
records.push(UnitRecord::new(x, y, unit_type));
offset += record_size;
}
Ok(records)
}
pub fn parse_file<P: AsRef<std::path::Path>>(path: P) -> ParseResult<Vec<UnitRecord>> {
let data = std::fs::read(path)?;
Self::parse(&data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_file() {
let result = AttackSetupParser::parse(&[]);
assert!(matches!(result, Err(ParseError::FileTooSmall { .. })));
}
#[test]
fn test_file_too_small() {
let result = AttackSetupParser::parse(&[1, 2]);
assert!(matches!(result, Err(ParseError::FileTooSmall { .. })));
}
#[test]
fn test_empty_formation() {
let data = [0, 0, 0, 0]; let result = AttackSetupParser::parse(&data).unwrap();
assert_eq!(result.len(), 0);
}
}