maikor_vm_file/
game_header.rs

1use crate::file_utils::ReaderExt;
2use crate::read_write_impl::{Readable, Writeable};
3use crate::GameFileError::{FieldTooLong, FileAccessError};
4use crate::{
5    FileFormatInvalid, GameFileError, GameFileHeader, InvalidFileVersion, FILE_FORMAT_VER,
6    ID_HEADER, MAX_STRING_LEN,
7};
8
9impl GameFileHeader {
10    #[allow(clippy::too_many_arguments)]
11    pub fn new(
12        id: String,
13        build: u32,
14        compiled_for_maikor_version: u16,
15        min_maikor_version: u16,
16        ram_bank_count: u8,
17        name: String,
18        version: String,
19        author: String,
20        code_bank_count: u8,
21        atlas_bank_count: u8,
22    ) -> Self {
23        Self {
24            id,
25            build,
26            compiled_for_maikor_version,
27            min_maikor_version,
28            ram_bank_count,
29            name,
30            version,
31            author,
32            code_bank_count,
33            atlas_bank_count,
34        }
35    }
36}
37
38impl GameFileHeader {
39    pub fn validate(&self) -> Result<(), String> {
40        let mut error = String::new();
41
42        if self.build == 0 {
43            error.push_str("Build version must be at least 1\n");
44        }
45        if self.compiled_for_maikor_version < self.min_maikor_version {
46            error.push_str("Minimum maikor version must <= compile version\n");
47        }
48        if self.author.trim().is_empty() {
49            error.push_str("Author must have at least one character\n");
50        } else if self.author.trim().len() > MAX_STRING_LEN {
51            error.push_str("Author is too long, max of 255 characters\n");
52        }
53        if self.name.trim().is_empty() {
54            error.push_str("Name must have at least one character\n");
55        } else if self.name.trim().len() > MAX_STRING_LEN {
56            error.push_str("Name is too long, max of 255 characters\n");
57        }
58        if self.version.trim().is_empty() {
59            error.push_str("Version must have at least one character\n");
60        } else if self.version.trim().len() > MAX_STRING_LEN {
61            error.push_str("Version is too long, max of 255 characters\n");
62        }
63        if self.id.trim().is_empty() {
64            error.push_str("ID must have at least one character\n");
65        } else if self.id.trim().len() > MAX_STRING_LEN {
66            error.push_str("ID is too long, max of 255 characters\n");
67        }
68        if self.atlas_bank_count == 0 {
69            error.push_str("Must have at least one atlas bank\n");
70        }
71
72        if error.is_empty() {
73            Ok(())
74        } else {
75            Err(error)
76        }
77    }
78}
79
80impl Readable for GameFileHeader {
81    fn from_reader<R: ReaderExt>(reader: &mut R) -> Result<GameFileHeader, GameFileError> {
82        let file_header = reader
83            .read_u16()
84            .map_err(|e| FileAccessError(e, "reading file header"))?;
85        let file_ver = reader
86            .read_u8()
87            .map_err(|e| FileAccessError(e, "reading file ver"))?;
88        if file_header != u16::from_be_bytes([ID_HEADER[0], ID_HEADER[1]]) {
89            return Err(FileFormatInvalid());
90        }
91        if file_ver != FILE_FORMAT_VER {
92            return Err(InvalidFileVersion(file_ver));
93        }
94
95        let min_maikor_version = reader
96            .read_u16()
97            .map_err(|e| FileAccessError(e, "reading min ver"))?;
98        let compiled_for_maikor_version = reader
99            .read_u16()
100            .map_err(|e| FileAccessError(e, "reading compiled ver"))?;
101        let build = reader
102            .read_u32()
103            .map_err(|e| FileAccessError(e, "reading build"))?;
104        let id = reader
105            .read_len_string()
106            .map_err(|e| FileAccessError(e, "reading id"))?;
107        let name = reader
108            .read_len_string()
109            .map_err(|e| FileAccessError(e, "reading name"))?;
110        let version = reader
111            .read_len_string()
112            .map_err(|e| FileAccessError(e, "reading version"))?;
113        let author = reader
114            .read_len_string()
115            .map_err(|e| FileAccessError(e, "reading author"))?;
116        let code_bank_count = reader
117            .read_u8()
118            .map_err(|e| FileAccessError(e, "reading code bank count"))?;
119        let ram_bank_count = reader
120            .read_u8()
121            .map_err(|e| FileAccessError(e, "reading ram bank count"))?;
122        let atlas_bank_count = reader
123            .read_u8()
124            .map_err(|e| FileAccessError(e, "reading atlas bank count"))?;
125
126        Ok(GameFileHeader::new(
127            id,
128            build,
129            compiled_for_maikor_version,
130            min_maikor_version,
131            ram_bank_count,
132            name,
133            version,
134            author,
135            code_bank_count,
136            atlas_bank_count,
137        ))
138    }
139}
140
141impl Writeable for GameFileHeader {
142    fn as_bytes(&self) -> Result<Vec<u8>, GameFileError> {
143        let mut output = vec![];
144        output.extend_from_slice(&ID_HEADER);
145        output.push(FILE_FORMAT_VER);
146        output.extend_from_slice(&self.min_maikor_version.to_be_bytes());
147        output.extend_from_slice(&self.compiled_for_maikor_version.to_be_bytes());
148        output.extend_from_slice(&self.build.to_be_bytes());
149        output.extend_from_slice(&convert_string("ID", &self.id)?);
150        output.extend_from_slice(&convert_string("Name", &self.name)?);
151        output.extend_from_slice(&convert_string("Version", &self.version)?);
152        output.extend_from_slice(&convert_string("Author", &self.author)?);
153        output.push(self.code_bank_count);
154        output.push(self.ram_bank_count);
155        output.push(self.atlas_bank_count);
156
157        Ok(output)
158    }
159}
160
161fn convert_string(field_name: &'static str, str: &str) -> Result<Vec<u8>, GameFileError> {
162    let len = str.trim().len();
163    if len > MAX_STRING_LEN {
164        return Err(FieldTooLong(field_name, MAX_STRING_LEN, len));
165    }
166    let mut bytes = str.as_bytes().to_vec();
167    bytes.insert(0, len as u8);
168    Ok(bytes)
169}
170
171#[cfg(test)]
172mod test {
173    use crate::read_write_impl::{Readable, Writeable};
174    use crate::GameFileHeader;
175    use std::io::BufReader;
176
177    #[test]
178    #[rustfmt::skip]
179    fn test_write() {
180        let header = GameFileHeader::new(
181            String::from("com.raybritton.test"),
182            12414,
183            16,
184            1,
185            0,
186            String::from("Test"),
187            String::from("1.1.0"),
188            String::from("Ray Britton"),
189            1,
190            4,
191        );
192
193        assert_eq!(
194            header.as_bytes().unwrap(),
195            [
196                253, 161,       //header
197                1,              //file ver
198                0, 1,           //min ver
199                0, 16,          //target ver
200                0, 0, 48, 126,  //build
201                19,             //id len
202                99, 111, 109, 46, 114, 97, 121, 98, 114, 105, 116, 116, 111, 110, 46, 116, 101, 115, 116, //id 
203                4,              //name len
204                84, 101, 115, 116, //name
205                5,              //ver len
206                49, 46, 49, 46, 48,  //ver
207                11,             //author len
208                82, 97, 121, 32, 66, 114, 105, 116, 116, 111, 110, //author 
209                1,              //code banks
210                0,              //ram banks
211                4,              //atlas banks
212            ]
213        );
214    }
215
216    #[test]
217    #[rustfmt::skip]
218    fn test_read() {
219        let bytes = vec![
220            253, 161, //header
221            1,   //file ver
222            1, 0, //min ver
223            2, 0, //target ver
224            0, 1, 5, 2, //build
225            6, //id len
226            66, 89, 100, 65, 53, 70, //id
227            5,  //name len
228            84, 101, 115, 116, 33, //name
229            2,  //ver len
230            118, 49, //ver
231            3,  //author len
232            82, 97, 121, //author
233            2,   //code banks
234            1,   //ram banks
235            88,  //atlas banks
236        ];
237        let mut reader = BufReader::new(&*bytes);
238
239        let header = GameFileHeader::from_reader(&mut reader).unwrap();
240        
241        assert_eq!(header.min_maikor_version, 256);
242        assert_eq!(header.compiled_for_maikor_version, 512);
243        assert_eq!(header.build, 66818);
244        assert_eq!(header.id, String::from("BYdA5F"));
245        assert_eq!(header.name, String::from("Test!"));
246        assert_eq!(header.code_bank_count, 2);
247        assert_eq!(header.ram_bank_count, 1);
248        assert_eq!(header.atlas_bank_count, 88);
249        assert_eq!(header.version, String::from("v1"));
250        assert_eq!(header.author, String::from("Ray"));
251    }
252
253    #[test]
254    fn test_read_write() {
255        let header = GameFileHeader::new(
256            String::from("TEST TEST TEST TEST TEST"),
257            12511,
258            12,
259            3341,
260            10,
261            String::from("TE ST APP TEST APP TESPT"),
262            String::from("v1.1.2351b"),
263            String::from("Ray Britton testing"),
264            12,
265            100,
266        );
267
268        let bytes = header.as_bytes().unwrap();
269
270        let parsed_header = GameFileHeader::from_reader(&mut BufReader::new(&*bytes)).unwrap();
271
272        assert_eq!(header, parsed_header);
273    }
274}