botstadia_interface/
archive.rs1use serde::{Deserialize, Serialize};
11
12const MAGIC: &[u8; 4] = b"BSRA";
13pub const ARCHIVE_FORMAT: u32 = 1;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct ArchiveHeader {
19 pub arena_id: String,
20 pub arena_version: String,
21 pub arena_digest: String,
23 pub payload_schema_version: u32,
24 pub seats: Vec<String>,
26 pub control_rate: u32,
27 pub turns: u32,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct Archive {
33 pub header: ArchiveHeader,
34 pub frames: Vec<Vec<u8>>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum ArchiveError {
41 BadMagic,
42 UnsupportedFormat(u32),
43 Corrupt(String),
44}
45
46impl std::fmt::Display for ArchiveError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::BadMagic => write!(f, "not a BotStadia replay archive"),
50 Self::UnsupportedFormat(format) => {
51 write!(f, "archive format {format} isn't one this reader knows")
52 }
53 Self::Corrupt(text) => write!(f, "archive is corrupt: {text}"),
54 }
55 }
56}
57
58impl std::error::Error for ArchiveError {}
59
60impl Archive {
61 pub fn encode(&self) -> Vec<u8> {
62 let mut bytes = Vec::new();
63 bytes.extend_from_slice(MAGIC);
64 bytes.extend_from_slice(&ARCHIVE_FORMAT.to_le_bytes());
65 bytes
66 .extend_from_slice(&postcard::to_allocvec(self).expect("an archive always serialises"));
67 bytes
68 }
69
70 pub fn decode(bytes: &[u8]) -> Result<Self, ArchiveError> {
71 if bytes.len() < 8 || &bytes[..4] != MAGIC {
72 return Err(ArchiveError::BadMagic);
73 }
74 let format = u32::from_le_bytes(bytes[4..8].try_into().expect("four bytes"));
75 if format != ARCHIVE_FORMAT {
76 return Err(ArchiveError::UnsupportedFormat(format));
77 }
78 postcard::from_bytes(&bytes[8..]).map_err(|error| ArchiveError::Corrupt(error.to_string()))
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn round_trips_and_refuses_strangers() {
88 let archive = Archive {
89 header: ArchiveHeader {
90 arena_id: "sample".into(),
91 arena_version: "0.1.0".into(),
92 arena_digest: String::new(),
93 payload_schema_version: 1,
94 seats: vec!["a".into(), "b".into()],
95 control_rate: 50,
96 turns: 2,
97 },
98 frames: vec![vec![1, 2], vec![3]],
99 };
100 let bytes = archive.encode();
101 assert_eq!(Archive::decode(&bytes).unwrap(), archive);
102 assert_eq!(Archive::decode(b"nope"), Err(ArchiveError::BadMagic));
103 let mut future = bytes.clone();
104 future[4] = 9;
105 assert_eq!(
106 Archive::decode(&future),
107 Err(ArchiveError::UnsupportedFormat(9))
108 );
109 }
110}