1use crate::assets::RgbaImage;
8use anyhow::{Context, Result, bail};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum G00Type {
13 Type24bit = 0,
14 Type8bit = 1,
15 TypeDir = 2,
16 TypeJpeg = 3,
17}
18
19#[derive(Debug, Clone)]
20pub struct DecodedG00 {
21 pub kind: G00Type,
22 pub width: u32,
23 pub height: u32,
24 pub frames: Vec<RgbaImage>,
26 pub original_sizes: Vec<(u32, u32)>,
29}
30
31fn read_u16le(buf: &[u8], off: usize) -> Result<u16> {
32 if off + 2 > buf.len() {
33 bail!("read u16le out of bounds at {off}");
34 }
35 Ok(u16::from_le_bytes([buf[off], buf[off + 1]]))
36}
37
38fn read_u32le(buf: &[u8], off: usize) -> Result<u32> {
39 if off + 4 > buf.len() {
40 bail!("read u32le out of bounds at {off}");
41 }
42 Ok(u32::from_le_bytes([
43 buf[off],
44 buf[off + 1],
45 buf[off + 2],
46 buf[off + 3],
47 ]))
48}
49
50fn read_i32le(buf: &[u8], off: usize) -> Result<i32> {
51 if off + 4 > buf.len() {
52 bail!("read i32le out of bounds at {off}");
53 }
54 Ok(i32::from_le_bytes([
55 buf[off],
56 buf[off + 1],
57 buf[off + 2],
58 buf[off + 3],
59 ]))
60}
61
62pub fn decode_g00(data: &[u8]) -> Result<DecodedG00> {
64 if data.len() < 1 + 2 + 2 {
65 bail!("g00 too small");
66 }
67 let ty = data[0];
68 let width = read_u16le(data, 1)? as u32;
69 let height = read_u16le(data, 3)? as u32;
70 let kind = match ty {
71 0 => G00Type::Type24bit,
72 1 => G00Type::Type8bit,
73 2 => G00Type::TypeDir,
74 3 => G00Type::TypeJpeg,
75 other => bail!("unknown g00 type: {other}"),
76 };
77
78 let mut off = 5;
79
80 match kind {
81 G00Type::Type24bit => {
82 let decompress_length = read_u32le(data, off + 4)? as usize;
84 off += 8;
85 if off > data.len() {
86 bail!("g00 type0 header out of bounds");
87 }
88 if decompress_length == 0 {
89 bail!("g00 type0 decompress_length=0");
90 }
91 let mut out = vec![0u8; decompress_length];
92 lzss_decompress_24bit(&data[off..], &mut out).context("lzss_decompress_24bit")?;
93
94 let rgba = out;
98 Ok(DecodedG00 {
99 kind,
100 width,
101 height,
102 original_sizes: vec![(width, height)],
103 frames: vec![RgbaImage {
104 width,
105 height,
106 center_x: 0,
107 center_y: 0,
108 rgba,
109 }],
110 })
111 }
112 G00Type::Type8bit => {
113 let (out, out_len) =
117 real_live_type1_uncompress(&data[off..]).context("type1 uncompress")?;
118 let raw = &out[..out_len.min(out.len())];
119 if raw.len() < 2 {
120 bail!("type1 palette header truncated");
121 }
122 let pal_cnt = read_u16le(raw, 0)? as usize;
123 let pal_bytes = pal_cnt
124 .checked_mul(4)
125 .context("type1 palette size overflow")?;
126 let indices_off = 2usize
127 .checked_add(pal_bytes)
128 .context("type1 palette offset overflow")?;
129 if indices_off > raw.len() {
130 bail!(
131 "type1 palette truncated: count={} bytes={} raw={}",
132 pal_cnt,
133 pal_bytes,
134 raw.len()
135 );
136 }
137 let pixel_count = (width as usize)
138 .checked_mul(height as usize)
139 .context("type1 pixel count overflow")?;
140 let indices_end = indices_off
141 .checked_add(pixel_count)
142 .context("type1 index range overflow")?;
143 if indices_end > raw.len() {
144 bail!(
145 "type1 indices truncated: need={} have={}",
146 pixel_count,
147 raw.len().saturating_sub(indices_off)
148 );
149 }
150
151 let palette = &raw[2..indices_off];
152 let indices = &raw[indices_off..indices_end];
153 let rgba_len = pixel_count
154 .checked_mul(4)
155 .context("type1 RGBA size overflow")?;
156 let mut rgba = Vec::with_capacity(rgba_len);
157 for &index in indices {
158 let index = index as usize;
159 if index >= pal_cnt {
160 bail!(
161 "type1 palette index out of range: index={} palette_count={}",
162 index,
163 pal_cnt
164 );
165 }
166 let base = index * 4;
167 let b = palette[base];
168 let g = palette[base + 1];
169 let r = palette[base + 2];
170 let a = palette[base + 3];
171 rgba.extend_from_slice(&[r, g, b, a]);
172 }
173
174 Ok(DecodedG00 {
175 kind,
176 width,
177 height,
178 original_sizes: vec![(width, height)],
179 frames: vec![RgbaImage {
180 width,
181 height,
182 center_x: 0,
183 center_y: 0,
184 rgba,
185 }],
186 })
187 }
188 G00Type::TypeDir => {
189 let index_entries = read_u32le(data, off)? as usize;
192 off += 4;
193 let g02_info_size = 24usize;
195 let skip = index_entries
196 .checked_mul(g02_info_size)
197 .context("index_entries overflow")?;
198 if off + skip > data.len() {
199 bail!("type2 g02_info_list out of bounds");
200 }
201 off += skip;
202
203 let decompress_length = read_u32le(data, off + 4)? as usize;
205 off += 8;
206 if decompress_length == 0 {
207 bail!("type2 decompress_length=0");
208 }
209 if off > data.len() {
210 bail!("type2 payload out of bounds");
211 }
212
213 let mut debuf = vec![0u8; decompress_length];
214 lzss_decompress(&data[off..], &mut debuf).context("lzss_decompress")?;
215
216 if debuf.len() < 4 {
218 bail!("type2 debuf too small");
219 }
220 let debuf_entries = read_u32le(&debuf, 0)? as usize;
221 let pairs_off = 4usize;
222 let pair_size = 8usize;
223 let pairs_bytes = debuf_entries
224 .checked_mul(pair_size)
225 .context("debuf_entries overflow")?;
226 if pairs_off + pairs_bytes > debuf.len() {
227 bail!("type2 pairs out of bounds");
228 }
229
230 let mut frames: Vec<RgbaImage> = Vec::with_capacity(index_entries);
234 let mut original_sizes = vec![(0, 0); index_entries];
235 for i in 0..index_entries {
236 if i >= debuf_entries {
237 frames.push(transparent_missing_g00_cut());
238 continue;
239 }
240 let p_off = pairs_off + i * pair_size;
241 let offset = read_u32le(&debuf, p_off)? as usize;
242 let length_raw = read_u32le(&debuf, p_off + 4)? as i32;
243 if offset == 0 || length_raw == 0 || offset >= debuf.len() {
244 frames.push(transparent_missing_g00_cut());
245 continue;
246 }
247
248 let part_bytes = &debuf[offset..];
254 let img = extract_g02_part(part_bytes)
255 .with_context(|| format!("extract g02 part idx={i}"))?;
256 let part = parse_g02_part_info_prefix(part_bytes)?;
257 original_sizes[i] = (part.width, part.height);
258 frames.push(img);
259 }
260
261 if frames.is_empty() {
262 bail!("type2 produced no frames");
263 }
264
265 Ok(DecodedG00 {
266 kind,
267 width,
268 height,
269 frames,
270 original_sizes,
271 })
272 }
273 G00Type::TypeJpeg => {
274 if off > data.len() {
275 bail!("g00 type3 header out of bounds");
276 }
277 let jpeg = siglus_assets::g00::decode_type3_jpeg_payload(&data[off..]);
278 if !jpeg.starts_with(&[0xFF, 0xD8]) {
279 bail!(
280 "g00 type3 XOR decode did not produce JPEG SOI: got={:02X?}",
281 &jpeg[..jpeg.len().min(2)]
282 );
283 }
284 let img = image::load_from_memory_with_format(&jpeg, image::ImageFormat::Jpeg)
285 .or_else(|_| image::load_from_memory(&jpeg))
286 .context("decode g00 type3 jpeg after XOR")?;
287 let rgba = img.to_rgba8();
288 let (w, h) = rgba.dimensions();
289 if w != width || h != height {
290 bail!("g00 jpeg size mismatch: got={w}x{h}, expect={width}x{height}");
291 }
292 Ok(DecodedG00 {
293 kind,
294 width,
295 height,
296 original_sizes: vec![(width, height)],
297 frames: vec![RgbaImage {
298 width,
299 height,
300 center_x: 0,
301 center_y: 0,
302 rgba: rgba.into_raw(),
303 }],
304 })
305 }
306 }
307}
308
309fn transparent_missing_g00_cut() -> RgbaImage {
310 RgbaImage {
311 width: 1,
312 height: 1,
313 center_x: 0,
314 center_y: 0,
315 rgba: vec![0, 0, 0, 0],
316 }
317}
318
319fn bgra_to_rgba_inplace(mut bgra: Vec<u8>) -> Vec<u8> {
320 for px in bgra.as_chunks_mut::<4>().0.iter_mut() {
321 let b = px[0];
322 let g = px[1];
323 let r = px[2];
324 let a = px[3];
325 px[0] = r;
326 px[1] = g;
327 px[2] = b;
328 px[3] = a;
329 }
330 bgra
331}
332
333fn real_live_type1_uncompress(compr: &[u8]) -> Result<(Vec<u8>, usize)> {
334 if compr.len() < 8 {
335 bail!("type1 data too small");
336 }
337 let _arc_size = read_u32le(compr, 0)? as usize;
341 let uncomprlen = read_u32le(compr, 4)? as usize;
342 if uncomprlen == 0 {
343 bail!("type1 org_size=0");
344 }
345
346 let mut out = vec![0u8; uncomprlen];
347 let mut curbyte = 8usize;
348 let mut act = 0usize;
349 let mut bit_count = 0u8;
350 let mut flag = 0u8;
351
352 while act < uncomprlen {
353 if bit_count == 0 {
354 if curbyte >= compr.len() {
355 bail!(
356 "type1 truncated before output complete: wrote={} expected={}",
357 act,
358 uncomprlen
359 );
360 }
361 flag = compr[curbyte];
362 curbyte += 1;
363 bit_count = 8;
364 }
365
366 if (flag & 1) != 0 {
367 if curbyte >= compr.len() {
368 bail!("type1 truncated literal");
369 }
370 out[act] = compr[curbyte];
371 act += 1;
372 curbyte += 1;
373 } else {
374 if curbyte + 2 > compr.len() {
375 bail!("type1 truncated backreference");
376 }
377 let count0 = compr[curbyte] as usize;
378 let b1 = compr[curbyte + 1] as usize;
379 curbyte += 2;
380
381 let offset = (b1 << 4) | (count0 >> 4);
382 let count = (count0 & 0xF) + 2;
383 if offset == 0 {
384 bail!("type1 invalid offset=0");
385 }
386 if act < offset {
387 bail!("type1 backref before start: act={act} offset={offset}");
388 }
389 copy_lzss_match(&mut out, &mut act, offset, count);
390 }
391
392 flag >>= 1;
393 bit_count -= 1;
394 }
395
396 Ok((out, uncomprlen))
397}
398
399fn copy_lzss_match(dst: &mut [u8], position: &mut usize, offset: usize, count: usize) {
403 debug_assert!(offset > 0 && *position >= offset && *position <= dst.len());
404 let source = *position - offset;
405 let end = *position + count.min(dst.len() - *position);
406 while *position < end {
407 let available = *position - source;
408 let size = available.min(end - *position);
409 dst.copy_within(source..source + size, *position);
410 *position += size;
411 }
412}
413
414fn lzss_decompress(src: &[u8], dst: &mut [u8]) -> Result<()> {
415 let mut s = 0usize;
416 let mut d = 0usize;
417 while d < dst.len() {
418 if s >= src.len() {
419 break;
420 }
421 let mut flags = src[s];
422 s += 1;
423 for _ in 0..8 {
424 if d >= dst.len() {
425 break;
426 }
427 if (flags & 1) != 0 {
428 if s >= src.len() {
429 break;
430 }
431 dst[d] = src[s];
432 d += 1;
433 s += 1;
434 } else {
435 if s + 2 > src.len() {
436 break;
437 }
438 let w = u16::from_le_bytes([src[s], src[s + 1]]) as usize;
439 s += 2;
440 let offset = w >> 4;
441 let count = (w & 0xF) + 2;
442 if offset == 0 {
443 bail!("lzss offset=0");
444 }
445 if d < offset {
446 bail!("lzss backref before start: d={d} offset={offset}");
447 }
448 copy_lzss_match(dst, &mut d, offset, count);
449 }
450 flags >>= 1;
451 }
452 }
453
454 if d != dst.len() {
455 bail!(
456 "lzss_decompress did not fill output: wrote {d} of {}",
457 dst.len()
458 );
459 }
460 Ok(())
461}
462
463fn lzss_decompress_24bit(src: &[u8], dst: &mut [u8]) -> Result<()> {
464 let mut s = 0usize;
468 let mut d = 0usize;
469 while d < dst.len() {
470 if s >= src.len() {
471 break;
472 }
473 let mut flags = src[s];
474 s += 1;
475 for _ in 0..8 {
476 if d >= dst.len() {
477 break;
478 }
479 if (flags & 1) != 0 {
480 if s + 3 > src.len() {
481 break;
482 }
483 if d + 4 > dst.len() {
484 bail!("lzss24 literal would overflow dst");
485 }
486 dst[d] = src[s + 2];
487 dst[d + 1] = src[s + 1];
488 dst[d + 2] = src[s];
489 dst[d + 3] = 0xFF;
490 d += 4;
491 s += 3;
492 } else {
493 if s + 2 > src.len() {
494 break;
495 }
496 let w = u16::from_le_bytes([src[s], src[s + 1]]) as usize;
497 s += 2;
498 let offset_bytes = (w >> 4) << 2; let dword_count = (w & 0xF) + 1;
500 let count_bytes = dword_count * 4;
501 if offset_bytes == 0 {
502 bail!("lzss24 offset_bytes=0");
503 }
504 if d < offset_bytes {
505 bail!("lzss24 backref before start: d={d} offset={offset_bytes}");
506 }
507 copy_lzss_match(dst, &mut d, offset_bytes, count_bytes);
508 }
509 flags >>= 1;
510 }
511 }
512
513 if d != dst.len() {
514 bail!(
515 "lzss_decompress_24bit did not fill output: wrote {d} of {}",
516 dst.len()
517 );
518 }
519 Ok(())
520}
521
522#[cfg(test)]
523mod lzss_fast_path_tests {
524 use super::*;
525
526 #[test]
527 fn overlapping_match_matches_original_forward_byte_copy() {
528 for offset in 1..=16 {
529 for count in 1..=32 {
530 let mut expected: Vec<u8> = (0..64).map(|i| (i * 37) as u8).collect();
531 let mut actual = expected.clone();
532 let mut expected_pos = 32usize;
533 for _ in 0..count {
534 if expected_pos >= expected.len() {
535 break;
536 }
537 expected[expected_pos] = expected[expected_pos - offset];
538 expected_pos += 1;
539 }
540 let mut actual_pos = 32usize;
541 copy_lzss_match(&mut actual, &mut actual_pos, offset, count);
542 assert_eq!(actual_pos, expected_pos);
543 assert_eq!(actual, expected, "offset={offset} count={count}");
544 }
545 }
546 }
547
548 #[test]
549 fn type0_literal_is_emitted_as_rgba_before_backreference_reuse() {
550 let mut dst = [0u8; 4];
553 lzss_decompress_24bit(&[1, 10, 20, 30], &mut dst).unwrap();
554 assert_eq!(dst, [30, 20, 10, 255]);
555 }
556}
557
558#[derive(Debug, Clone)]
559struct G02PartInfo {
560 _part_type: u8,
561 block_count: u16,
562 hs_orig_x: i32,
563 hs_orig_y: i32,
564 width: u32,
565 height: u32,
566 screen_show_x: i32,
567 screen_show_y: i32,
568 full_part_width: u32,
569 full_part_height: u32,
570}
571
572#[derive(Debug, Clone)]
573struct G02BlockInfo {
574 orig_x: u16,
575 orig_y: u16,
576 _sprite: bool,
577 width: u16,
578 height: u16,
579}
580
581const G02_PART_INFO_SIZE: usize = 0x74;
585const G02_BLOCK_INFO_SIZE: usize = 0x5c;
586
587fn parse_g02_block(buf: &[u8]) -> Result<G02BlockInfo> {
588 if buf.len() < G02_BLOCK_INFO_SIZE {
589 bail!("G00_CHIP_HEADER_STRUCT truncated");
590 }
591 let orig_x = read_u16le(buf, 0)?;
592 let orig_y = read_u16le(buf, 2)?;
593 let sprite = buf[4] == 1;
594 let width = read_u16le(buf, 6)?;
595 let height = read_u16le(buf, 8)?;
596 Ok(G02BlockInfo {
597 orig_x,
598 orig_y,
599 _sprite: sprite,
600 width,
601 height,
602 })
603}
604
605fn parse_g02_part_info_prefix(buf: &[u8]) -> Result<G02PartInfo> {
606 if buf.len() < G02_PART_INFO_SIZE {
611 bail!("G00_CUT_HEADER_STRUCT truncated");
612 }
613 let part_type = buf[0];
614 let block_count = read_u16le(buf, 2)?;
615 let disp_x = read_i32le(buf, 4)?;
616 let disp_y = read_i32le(buf, 8)?;
617 let disp_width = read_u32le(buf, 0x0C)?;
618 let disp_height = read_u32le(buf, 0x10)?;
619 let center_x = read_i32le(buf, 0x14)?;
620 let center_y = read_i32le(buf, 0x18)?;
621 let cut_width = read_u32le(buf, 0x1C)?;
622 let cut_height = read_u32le(buf, 0x20)?;
623 Ok(G02PartInfo {
624 _part_type: part_type,
625 block_count,
626 hs_orig_x: disp_x,
627 hs_orig_y: disp_y,
628 width: cut_width,
629 height: cut_height,
630 screen_show_x: center_x,
631 screen_show_y: center_y,
632 full_part_width: disp_width,
633 full_part_height: disp_height,
634 })
635}
636
637fn extract_g02_part(part_bytes: &[u8]) -> Result<RgbaImage> {
638 let part = parse_g02_part_info_prefix(part_bytes).context("parse part prefix")?;
643 if part.width == 0 || part.height == 0 {
644 bail!("g02 part has zero full-cut dimensions");
645 }
646 if part.full_part_width == 0 || part.full_part_height == 0 {
647 bail!("g02 part has zero display dimensions");
648 }
649
650 let header_size = G02_PART_INFO_SIZE;
653
654 let out_w = part.full_part_width;
655 let out_h = part.full_part_height;
656 let stride = (out_w as usize).checked_mul(4).context("stride overflow")?;
657 let mut dib = vec![0u8; stride * (out_h as usize)];
658
659 let mut off = header_size;
660 for _ in 0..part.block_count {
661 let block = parse_g02_block(&part_bytes[off..])?;
662 off += G02_BLOCK_INFO_SIZE;
663
664 let bw = block.width as usize;
665 let bh = block.height as usize;
666 if bw == 0 || bh == 0 {
667 bail!("g02 block zero size");
668 }
669 let px_len = bw
670 .checked_mul(bh)
671 .and_then(|v| v.checked_mul(4))
672 .context("pixel len overflow")?;
673 if off + px_len > part_bytes.len() {
674 bail!("g02 block pixel data out of bounds");
675 }
676 let src = &part_bytes[off..off + px_len];
677 off += px_len;
678
679 let dst_x_i = block.orig_x as i32 - part.hs_orig_x;
680 let dst_y_i = block.orig_y as i32 - part.hs_orig_y;
681 if dst_x_i < 0 || dst_y_i < 0 {
682 bail!(
683 "g02 block outside display rect: chip=({}, {}) disp=({}, {})",
684 block.orig_x,
685 block.orig_y,
686 part.hs_orig_x,
687 part.hs_orig_y
688 );
689 }
690 let dst_x = dst_x_i as usize;
691 let dst_y = dst_y_i as usize;
692 if dst_x.saturating_add(bw) > out_w as usize || dst_y.saturating_add(bh) > out_h as usize {
693 bail!(
694 "g02 block write outside display rect: dst=({}, {}) size={}x{} out={}x{}",
695 dst_x,
696 dst_y,
697 bw,
698 bh,
699 out_w,
700 out_h
701 );
702 }
703
704 for row in 0..bh {
705 let src_row_off = row * bw * 4;
706 let dst_row_off = (dst_y + row) * stride + dst_x * 4;
707 let dst_end = dst_row_off + bw * 4;
708 if dst_end > dib.len() {
709 bail!("g02 block write out of bounds");
710 }
711 dib[dst_row_off..dst_end].copy_from_slice(&src[src_row_off..src_row_off + bw * 4]);
712 }
713 }
714
715 let rgba = bgra_to_rgba_inplace(dib);
716
717 Ok(RgbaImage {
718 width: out_w,
719 height: out_h,
720 center_x: part.screen_show_x - part.hs_orig_x,
721 center_y: part.screen_show_y - part.hs_orig_y,
722 rgba,
723 })
724}
725
726#[cfg(test)]
727mod original_g00_layout_tests {
728 use super::*;
729
730 fn literal_type1_g00(width: u16, height: u16, raw: &[u8]) -> Vec<u8> {
731 let mut payload = Vec::new();
732 for chunk in raw.chunks(8) {
733 payload.push(if chunk.len() == 8 {
734 0xff
735 } else {
736 ((1u16 << chunk.len()) - 1) as u8
737 });
738 payload.extend_from_slice(chunk);
739 }
740 let mut file = Vec::new();
741 file.push(1); file.extend_from_slice(&width.to_le_bytes());
743 file.extend_from_slice(&height.to_le_bytes());
744 file.extend_from_slice(&(payload.len() as u32).to_le_bytes());
745 file.extend_from_slice(&(raw.len() as u32).to_le_bytes());
746 file.extend_from_slice(&payload);
747 file
748 }
749
750 #[test]
751 fn type1_expands_palette_indices_like_c_g00_chip_get_data() {
752 let mut raw = Vec::new();
754 raw.extend_from_slice(&2u16.to_le_bytes());
755 raw.extend_from_slice(&[10, 20, 30, 40]); raw.extend_from_slice(&[50, 60, 70, 80]); raw.extend_from_slice(&[1, 0]);
758
759 let decoded = decode_g00(&literal_type1_g00(2, 1, &raw)).unwrap();
760 assert_eq!(decoded.frames.len(), 1);
761 assert_eq!(decoded.frames[0].rgba, vec![70, 60, 50, 80, 30, 20, 10, 40]);
762 }
763
764 #[test]
765 fn type2_cut_uses_fixed_msvc_headers_and_display_rect_coordinates() {
766 let mut cut = vec![0u8; G02_PART_INFO_SIZE];
767 cut[0] = 2;
768 cut[2..4].copy_from_slice(&1u16.to_le_bytes()); cut[4..8].copy_from_slice(&10i32.to_le_bytes()); cut[8..12].copy_from_slice(&20i32.to_le_bytes()); cut[12..16].copy_from_slice(&2i32.to_le_bytes()); cut[16..20].copy_from_slice(&1i32.to_le_bytes()); cut[20..24].copy_from_slice(&11i32.to_le_bytes()); cut[24..28].copy_from_slice(&20i32.to_le_bytes()); cut[28..32].copy_from_slice(&32i32.to_le_bytes()); cut[32..36].copy_from_slice(&32i32.to_le_bytes()); let mut chip = vec![0u8; G02_BLOCK_INFO_SIZE];
779 chip[0..2].copy_from_slice(&10u16.to_le_bytes());
780 chip[2..4].copy_from_slice(&20u16.to_le_bytes());
781 chip[4] = 1; chip[6..8].copy_from_slice(&2u16.to_le_bytes());
783 chip[8..10].copy_from_slice(&1u16.to_le_bytes());
784 cut.extend_from_slice(&chip);
785 cut.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); let image = extract_g02_part(&cut).unwrap();
788 assert_eq!((image.width, image.height), (2, 1));
789 assert_eq!((image.center_x, image.center_y), (1, 0));
790 assert_eq!(image.rgba, vec![3, 2, 1, 4, 7, 6, 5, 8]);
791 }
792}