diff --git a/rust/src/replay.rs b/rust/src/replay.rs index 06dafe7..1a476c5 100644 --- a/rust/src/replay.rs +++ b/rust/src/replay.rs @@ -135,6 +135,10 @@ impl StandaloneReplayManager { thread::spawn(move || match fs::read(path) { Ok(mut bytes) => { + if let Err(e) = read_header(&mut bytes) { + return send.send(ReplayReadEvent::Error(e.to_string())); + } + let cursor = Cursor::new(&mut bytes); let decoder = GzDecoder::new(cursor); @@ -665,6 +669,7 @@ impl ReplayRecorder { thread::spawn(move || { let mut bytes = Vec::new(); + bytes.extend(produce_header()); let encoder = GzEncoder::new(&mut bytes, Compression::default()); match ciborium::into_writer(&replay, encoder) { Ok(_) => match fs::write(path, bytes) { @@ -928,3 +933,102 @@ pub struct ReplayTriggerableState { active: Option, active_for: f64, } + +const GAME_NAME: &str = "skullball"; +const MAJOR: u8 = match u8::from_str_radix(env!("CARGO_PKG_VERSION_MAJOR"), 10) { + Ok(res) => res, + Err(_) => 0, +}; +const MINOR: u8 = match u8::from_str_radix(env!("CARGO_PKG_VERSION_MINOR"), 10) { + Ok(res) => res, + Err(_) => 0, +}; +const PATCH: u8 = match u8::from_str_radix(env!("CARGO_PKG_VERSION_PATCH"), 10) { + Ok(res) => res, + Err(_) => 0, +}; +const VERSION: Version = Version { + major: MAJOR, + minor: MINOR, + patch: PATCH, +}; + +fn produce_header() -> Vec { + let mut bytes: Vec = Vec::new(); + bytes.extend(GAME_NAME.as_bytes()); + bytes.push(MAJOR); + bytes.push(MINOR); + bytes.push(PATCH); + bytes +} + +fn read_header(bytes: &mut Vec) -> Result<(), HeaderReadError> { + if bytes.len() < (GAME_NAME.len() + 3) { + return Err(HeaderReadError::ReadIncomplete); + } + + let mut name_bytes = Vec::with_capacity(GAME_NAME.len()); + for _ in 0..GAME_NAME.len() { + name_bytes.push(bytes.remove(0)); + } + let name = String::from_utf8(name_bytes).map_err(|_| HeaderReadError::InvalidTitle)?; + if name != GAME_NAME { + return Err(HeaderReadError::InvalidTitle); + } + + let major = bytes.remove(0); + let minor = bytes.remove(0); + let patch = bytes.remove(0); + let version = Version { + major, + minor, + patch, + }; + if version != VERSION { + return Err(HeaderReadError::InvalidVersion(version)); + } + + Ok(()) +} + +pub enum HeaderReadError { + ReadIncomplete, + InvalidTitle, + InvalidVersion(Version), +} + +impl std::fmt::Display for HeaderReadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HeaderReadError::ReadIncomplete => { + write!(f, "Header read incomplete, invalid replay file") + } + HeaderReadError::InvalidTitle => { + write!(f, "Invalid title in header, invalid replay file") + } + HeaderReadError::InvalidVersion(version) => write!( + f, + "Replay game version mismatch: {} != {}", + version, VERSION + ), + } + } +} + +#[derive(Debug, PartialEq, PartialOrd, Eq)] +pub struct Version { + pub major: u8, + pub minor: u8, + pub patch: u8, +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.major, f)?; + write!(f, ".")?; + std::fmt::Display::fmt(&self.minor, f)?; + write!(f, ".")?; + std::fmt::Display::fmt(&self.patch, f)?; + Ok(()) + } +}