Skip to main content

rgpre/import/
gxa.rs

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 = &section[..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                // BPAL stores 6-bit VGA values (0–63); shift left by 2 for 8-bit RGB
209                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(&section[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(&section[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}