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, 1, 0, 1, 0, 16, 0, 0, 48, 126, 19, 99, 111, 109, 46, 114, 97, 121, 98, 114, 105, 116, 116, 111, 110, 46, 116, 101, 115, 116, 4, 84, 101, 115, 116, 5, 49, 46, 49, 46, 48, 11, 82, 97, 121, 32, 66, 114, 105, 116, 116, 111, 110, 1, 0, 4, ]
213 );
214 }
215
216 #[test]
217 #[rustfmt::skip]
218 fn test_read() {
219 let bytes = vec![
220 253, 161, 1, 1, 0, 2, 0, 0, 1, 5, 2, 6, 66, 89, 100, 65, 53, 70, 5, 84, 101, 115, 116, 33, 2, 118, 49, 3, 82, 97, 121, 2, 1, 88, ];
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}