1use crate::{Result, error::Error};
2use delharc::decode::{Decoder, Lh1Decoder};
3
4#[derive(Debug, Clone)]
5pub struct GxaFrame {
6 pub width: u16,
7 pub height: u16,
8 pub rgba: Vec<u8>,
9}
10
11#[derive(Debug, Clone)]
12pub struct GxaFile {
13 pub title: String,
14 pub frame_count: u16,
15 pub frames: Vec<GxaFrame>,
16}
17
18fn read_i16_le(data: &[u8], offset: usize) -> Result<i16> {
19 if offset + 2 > data.len() {
20 return Err(Error::Parse(format!(
21 "GXA read_i16 out of bounds at offset {offset}"
22 )));
23 }
24 Ok(i16::from_le_bytes([data[offset], data[offset + 1]]))
25}
26
27fn read_u32_le(data: &[u8], offset: usize) -> Result<u32> {
28 if offset + 4 > data.len() {
29 return Err(Error::Parse(format!(
30 "GXA read_u32 out of bounds at offset {offset}"
31 )));
32 }
33 Ok(u32::from_le_bytes([
34 data[offset],
35 data[offset + 1],
36 data[offset + 2],
37 data[offset + 3],
38 ]))
39}
40
41fn read_u32_be(data: &[u8], offset: usize) -> Result<u32> {
42 if offset + 4 > data.len() {
43 return Err(Error::Parse(format!(
44 "GXA read_u32 out of bounds at offset {offset}"
45 )));
46 }
47 Ok(u32::from_be_bytes([
48 data[offset],
49 data[offset + 1],
50 data[offset + 2],
51 data[offset + 3],
52 ]))
53}
54
55fn decode_frame_rgba(
56 width: usize,
57 height: usize,
58 pixels: &[u8],
59 palette: &[[u8; 3]; 256],
60) -> Result<Vec<u8>> {
61 let pixel_count = width
62 .checked_mul(height)
63 .ok_or_else(|| Error::Parse("GXA width*height overflow".to_string()))?;
64 if pixels.len() < pixel_count {
65 return Err(Error::Parse(format!(
66 "GXA frame pixel data too small: got {}, need {pixel_count}",
67 pixels.len()
68 )));
69 }
70
71 let mut rgba = vec![0u8; pixel_count * 4];
72 for (i, &px) in pixels[..pixel_count].iter().enumerate() {
73 let idx = usize::from(px);
74 let o = i * 4;
75 if idx == 0 {
76 rgba[o..o + 4].copy_from_slice(&[0, 0, 0, 0]);
77 } else {
78 let c = palette[idx];
79 rgba[o..o + 4].copy_from_slice(&[c[0], c[1], c[2], 255]);
80 }
81 }
82
83 Ok(rgba)
84}
85
86fn decompress_rle(compressed: &[u8], pixel_count: usize) -> Result<Vec<u8>> {
87 let mut out = Vec::with_capacity(pixel_count);
88 let mut cur = 0usize;
89
90 while out.len() < pixel_count {
91 if cur >= compressed.len() {
92 return Err(Error::Parse(
93 "GXA RLE compressed stream truncated".to_string(),
94 ));
95 }
96 let ctrl = compressed[cur];
97 cur += 1;
98
99 if ctrl & 0x80 != 0 {
100 if cur >= compressed.len() {
101 return Err(Error::Parse(
102 "GXA RLE repeat run missing value byte".to_string(),
103 ));
104 }
105 let value = compressed[cur];
106 cur += 1;
107 let run_len = usize::from(ctrl & 0x7F) + 1;
108 for _ in 0..run_len {
109 if out.len() == pixel_count {
110 break;
111 }
112 out.push(value);
113 }
114 } else {
115 let run_len = usize::from(ctrl) + 1;
116 if cur + run_len > compressed.len() {
117 return Err(Error::Parse(
118 "GXA RLE literal run exceeds compressed stream".to_string(),
119 ));
120 }
121 let remaining = pixel_count - out.len();
122 let copy_len = run_len.min(remaining);
123 out.extend_from_slice(&compressed[cur..cur + copy_len]);
124 cur += run_len;
125 }
126 }
127
128 Ok(out)
129}
130
131fn decompress_lzhuf(compressed: &[u8], pixel_count: usize) -> Result<Vec<u8>> {
132 let mut decoder = Lh1Decoder::new(compressed);
133 let mut output = vec![0u8; pixel_count];
134 decoder
135 .fill_buffer(&mut output)
136 .map_err(|e| Error::Parse(format!("GXA LZHUF decompression failed: {e:?}")))?;
137 Ok(output)
138}
139
140pub fn parse_gxa_file(data: &[u8]) -> Result<GxaFile> {
141 let mut cursor = 0usize;
142
143 let mut title = String::new();
144 let mut frame_count = 0u16;
145 let mut palette = [[0u8; 3]; 256];
146 let mut has_palette = false;
147 let mut decoded_frames: Vec<GxaFrame> = Vec::new();
148
149 while cursor + 4 <= data.len() {
150 let tag_bytes: [u8; 4] = data[cursor..cursor + 4]
151 .try_into()
152 .map_err(|_| Error::Parse("GXA section tag read failed".to_string()))?;
153 cursor += 4;
154
155 if tag_bytes == *b"END " {
156 break;
157 }
158
159 let section_len = usize::try_from(read_u32_be(data, cursor)?)
160 .map_err(|_| Error::Parse("GXA section length does not fit usize".to_string()))?;
161 cursor += 4;
162
163 let section_end = if cursor + section_len > data.len() {
164 if tag_bytes == *b"BBMP" {
165 data.len()
166 } else {
167 return Err(Error::Parse(format!(
168 "GXA section out of bounds at offset {cursor} with length {section_len}"
169 )));
170 }
171 } else {
172 cursor + section_len
173 };
174
175 let section = &data[cursor..section_end];
176 cursor = section_end;
177
178 match &tag_bytes {
179 b"BMHD" => {
180 if section.len() < 34 {
181 return Err(Error::Parse(format!(
182 "GXA BMHD too small: {} bytes (need >= 34)",
183 section.len()
184 )));
185 }
186 let raw_title = §ion[..22];
187 let end = raw_title
188 .iter()
189 .position(|&b| b == 0)
190 .unwrap_or(raw_title.len());
191 title = String::from_utf8_lossy(&raw_title[..end]).to_string();
192 let count = read_i16_le(section, 32)?;
193 if count < 0 {
194 return Err(Error::Parse(format!(
195 "GXA BMHD negative frame count: {count}"
196 )));
197 }
198 frame_count = u16::try_from(count)
199 .map_err(|_| Error::Parse("GXA frame count conversion failed".to_string()))?;
200 }
201 b"BPAL" => {
202 if section.len() < 256 * 3 {
203 return Err(Error::Parse(format!(
204 "GXA BPAL too small: {} bytes (need >= 768)",
205 section.len()
206 )));
207 }
208 for (i, color) in palette.iter_mut().enumerate() {
210 let off = i * 3;
211 *color = [
212 section[off] << 2,
213 section[off + 1] << 2,
214 section[off + 2] << 2,
215 ];
216 }
217 has_palette = true;
218 }
219 b"BBMP" => {
220 if frame_count == 0 {
221 return Err(Error::Parse(
222 "GXA BBMP encountered before BMHD with valid frame count".to_string(),
223 ));
224 }
225 if !has_palette {
226 return Err(Error::Parse(
227 "GXA BBMP encountered before BPAL palette".to_string(),
228 ));
229 }
230
231 let mut sec_cur = 0usize;
232 decoded_frames.clear();
233 decoded_frames.reserve(usize::from(frame_count));
234
235 for frame_idx in 0..usize::from(frame_count) {
236 if sec_cur + 18 > section.len() {
237 return Err(Error::Parse(format!(
238 "GXA BBMP frame {frame_idx} header out of bounds"
239 )));
240 }
241
242 let width_i16 = read_i16_le(section, sec_cur + 2)?;
243 let height_i16 = read_i16_le(section, sec_cur + 4)?;
244 if width_i16 <= 0 || height_i16 <= 0 {
245 return Err(Error::Parse(format!(
246 "GXA BBMP invalid dimensions for frame {frame_idx}: {width_i16}x{height_i16}"
247 )));
248 }
249 let width = usize::try_from(width_i16)
250 .map_err(|_| Error::Parse("GXA width conversion failed".to_string()))?;
251 let height = usize::try_from(height_i16)
252 .map_err(|_| Error::Parse("GXA height conversion failed".to_string()))?;
253
254 let pixel_count = width.checked_mul(height).ok_or_else(|| {
255 Error::Parse(format!(
256 "GXA BBMP frame {frame_idx} dimensions overflow: {width}x{height}"
257 ))
258 })?;
259
260 let compression = read_i16_le(section, sec_cur + 10)?;
261
262 let pixels = match compression {
263 0 => {
264 let data_start = sec_cur + 18;
265 let data_end = data_start + pixel_count;
266 if data_end > section.len() {
267 return Err(Error::Parse(format!(
268 "GXA BBMP frame {frame_idx} raw payload out of bounds"
269 )));
270 }
271 sec_cur = data_end;
272 section[data_start..data_end].to_vec()
273 }
274 1 => {
275 if sec_cur + 22 > section.len() {
276 return Err(Error::Parse(format!(
277 "GXA BBMP frame {frame_idx} RLE header out of bounds"
278 )));
279 }
280 let comp_size = usize::try_from(read_u32_le(section, sec_cur + 18)?)
281 .map_err(|_| {
282 Error::Parse(
283 "GXA RLE compressed size conversion failed".to_string(),
284 )
285 })?;
286 let data_start = sec_cur + 22;
287 let data_end = data_start + comp_size;
288 if data_end > section.len() {
289 return Err(Error::Parse(format!(
290 "GXA BBMP frame {frame_idx} RLE payload out of bounds"
291 )));
292 }
293 let decompressed =
294 decompress_rle(§ion[data_start..data_end], pixel_count)?;
295 sec_cur = data_end;
296 decompressed
297 }
298 2 => {
299 if sec_cur + 26 > section.len() {
300 return Err(Error::Parse(format!(
301 "GXA BBMP frame {frame_idx} LZHUF header out of bounds"
302 )));
303 }
304 let comp_size = usize::try_from(read_u32_le(section, sec_cur + 18)?)
305 .map_err(|_| {
306 Error::Parse(
307 "GXA LZHUF compressed size conversion failed".to_string(),
308 )
309 })?;
310 let _uncomp_size = usize::try_from(read_u32_le(section, sec_cur + 22)?)
311 .map_err(|_| {
312 Error::Parse(
313 "GXA LZHUF uncompressed size conversion failed".to_string(),
314 )
315 })?;
316 let data_start = sec_cur + 26;
317 let data_end = data_start + comp_size;
318 if data_end > section.len() {
319 return Err(Error::Parse(format!(
320 "GXA BBMP frame {frame_idx} LZHUF payload out of bounds"
321 )));
322 }
323 let decompressed =
324 decompress_lzhuf(§ion[data_start..data_end], pixel_count)?;
325 sec_cur = data_end;
326 decompressed
327 }
328 _ => {
329 return Err(Error::Parse(format!(
330 "GXA BBMP frame {frame_idx} unsupported compression type {compression}"
331 )));
332 }
333 };
334
335 let rgba = decode_frame_rgba(width, height, &pixels, &palette)?;
336 decoded_frames.push(GxaFrame {
337 width: u16::try_from(width)
338 .map_err(|_| Error::Parse("GXA width > u16::MAX".to_string()))?,
339 height: u16::try_from(height)
340 .map_err(|_| Error::Parse("GXA height > u16::MAX".to_string()))?,
341 rgba,
342 });
343 }
344 }
345 _ => {}
346 }
347 }
348
349 if decoded_frames.is_empty() {
350 return Err(Error::Parse("GXA contains no decoded frames".to_string()));
351 }
352
353 Ok(GxaFile {
354 title,
355 frame_count,
356 frames: decoded_frames,
357 })
358}