1use lzfse_rust::LzfseRingDecoder;
2
3use crate::apple_compression::{self, AppleCompressionError};
4use crate::color::{output_bytes_to_rgba, palette_entry_to_rgba, row_to_rgba};
5use crate::model::{DecodeType, Deepmap2Header, PixelFormat};
6use crate::predictor::apply_predictor;
7use crate::{Deepmap2Error, Deepmap2Result};
8
9const LZVN_THRESHOLD: usize = 0x1000;
12const APPLE_STREAM_SCAN_LIMIT: usize = 8;
13const APPLE_STREAM_EOS_MAGIC: &[u8; 4] = b"bvx$";
14
15fn apply_pixel_format_override(
16 header: &Deepmap2Header,
17 pixel_format_override: Option<PixelFormat>,
18) -> Deepmap2Header {
19 let mut header = header.clone();
20 if let Some(pixel_format) = pixel_format_override {
21 header.pixel_format = pixel_format;
22 }
23 header
24}
25
26fn apple_stream_slice(data: &[u8]) -> &[u8] {
27 if apple_compression::is_apple_compression_stream(data) {
28 return data;
29 }
30
31 let scan_limit = data.len().min(APPLE_STREAM_SCAN_LIMIT);
32 if let Some(marker) = data[..scan_limit]
33 .windows(4)
34 .position(apple_compression::is_apple_compression_stream)
35 {
36 &data[marker..]
37 } else {
38 data
39 }
40}
41
42fn map_apple_compression_error(error: AppleCompressionError) -> Deepmap2Error {
43 match error {
44 AppleCompressionError::NativeFallbackUnavailable(message) => {
45 Deepmap2Error::NativeFallbackUnavailable(message)
46 }
47 AppleCompressionError::DecodeFailed(_) | AppleCompressionError::Io(_) => {
48 Deepmap2Error::LzfseDecompress
49 }
50 }
51}
52
53fn is_apple_stream_start_magic(data: &[u8]) -> bool {
54 matches!(
55 data.get(..4),
56 Some(b"bvx1") | Some(b"bvx2") | Some(b"bvxn") | Some(b"bvx-")
57 )
58}
59
60fn decode_lzfse_rust_only(data: &[u8]) -> Option<Vec<u8>> {
61 let cap = data.len().saturating_mul(16).max(4096);
62 let mut out = Vec::with_capacity(cap);
63 let mut decoder = LzfseRingDecoder::default();
64 if let Ok(n) = decoder.decode_bytes(data, &mut out)
65 && (n > 0 || data.is_empty())
66 {
67 out.truncate(n as usize);
68 Some(out)
69 } else {
70 None
71 }
72}
73
74fn find_concatenated_apple_segment_boundary(data: &[u8]) -> Option<(usize, usize)> {
75 let mut search_offset = 0usize;
76 while search_offset + APPLE_STREAM_EOS_MAGIC.len() <= data.len() {
77 let rel = data[search_offset..]
78 .windows(APPLE_STREAM_EOS_MAGIC.len())
79 .position(|window| window == APPLE_STREAM_EOS_MAGIC)?;
80 let eos_pos = search_offset + rel;
81 let next_scan_start = eos_pos + APPLE_STREAM_EOS_MAGIC.len();
82 let next_scan_end = data.len().min(next_scan_start + APPLE_STREAM_SCAN_LIMIT);
83
84 if let Some(next_rel) = data[next_scan_start..next_scan_end]
85 .windows(4)
86 .position(is_apple_stream_start_magic)
87 {
88 return Some((next_scan_start, next_scan_start + next_rel));
89 }
90
91 search_offset = next_scan_start;
92 }
93
94 None
95}
96
97fn decompress_concatenated_apple_stream(data: &[u8]) -> Deepmap2Result<Vec<u8>> {
98 let mut decoded = Vec::new();
99 let mut cursor = 0usize;
100
101 loop {
102 let remaining = &data[cursor..];
103 let segment = apple_stream_slice(remaining);
104 let start_offset = remaining.len().saturating_sub(segment.len());
105 if start_offset > APPLE_STREAM_SCAN_LIMIT
106 || !apple_compression::is_apple_compression_stream(segment)
107 {
108 return Err(Deepmap2Error::LzfseDecompress);
109 }
110 cursor += start_offset;
111
112 if let Some((segment_end, next_segment_start)) =
113 find_concatenated_apple_segment_boundary(&data[cursor..])
114 {
115 let segment = &data[cursor..cursor + segment_end];
116 let part = decode_lzfse_rust_only(segment).ok_or(Deepmap2Error::LzfseDecompress)?;
117 decoded.extend_from_slice(&part);
118 cursor += next_segment_start;
119 continue;
120 }
121
122 let part = decode_lzfse_rust_only(&data[cursor..]).ok_or(Deepmap2Error::LzfseDecompress)?;
123 decoded.extend_from_slice(&part);
124 return Ok(decoded);
125 }
126}
127
128fn decompress_lzfse(data: &[u8]) -> Deepmap2Result<Vec<u8>> {
130 let data = apple_stream_slice(data);
131 let is_apple_stream = apple_compression::is_apple_compression_stream(data);
132 if let Some(decoded) = decode_lzfse_rust_only(data) {
133 return Ok(decoded);
134 }
135
136 if !is_apple_stream {
137 return Err(Deepmap2Error::LzfseDecompress);
138 }
139
140 if let Ok(decoded) = decompress_concatenated_apple_stream(data) {
141 return Ok(decoded);
142 }
143
144 apple_compression::decode_lzfse_with_fallback(data).map_err(map_apple_compression_error)
145}
146
147pub fn decompress_lzvn(data: &[u8], expected_output_size: usize) -> Deepmap2Result<Vec<u8>> {
149 lzvn::decode_raw(data, expected_output_size).map_err(|_| Deepmap2Error::LzvnDecompress)
150}
151
152fn parse_size_prefixed_chunks(payload: &[u8]) -> Option<Vec<&[u8]>> {
157 let mut chunks = Vec::new();
158 let mut cursor = 0usize;
159 while cursor + 4 <= payload.len() {
160 let chunk_size = u32::from_le_bytes(payload[cursor..cursor + 4].try_into().ok()?) as usize;
161 cursor += 4;
162 if chunk_size == 0 || cursor + chunk_size > payload.len() {
163 return None;
164 }
165 chunks.push(&payload[cursor..cursor + chunk_size]);
166 cursor += chunk_size;
167 }
168 if cursor != payload.len() || chunks.is_empty() {
169 return None;
170 }
171 Some(chunks)
172}
173
174fn decompress_lzfse_or_lzvn(data: &[u8], expected_output_size: usize) -> Deepmap2Result<Vec<u8>> {
175 match decompress_lzfse(data) {
176 Ok(bytes) => Ok(bytes),
177 Err(err @ Deepmap2Error::LzfseDecompress) => {
178 decompress_lzvn(data, expected_output_size).map_err(|_| err)
179 }
180 Err(err) => Err(err),
181 }
182}
183
184fn decode_default_decompressed(
192 header: &Deepmap2Header,
193 decompressed: &[u8],
194 width: usize,
195 height: usize,
196) -> Deepmap2Result<Vec<u8>> {
197 let (rgba, _) =
198 decode_default_decompressed_with_state(header, decompressed, width, height, height, None)?;
199 Ok(rgba)
200}
201
202fn decode_default_decompressed_with_state(
203 header: &Deepmap2Header,
204 decompressed: &[u8],
205 width: usize,
206 source_height: usize,
207 height: usize,
208 mut prev_row: Option<Vec<i16>>,
209) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
210 let pixel_format = header.pixel_format;
211 let has_alpha = pixel_format.has_alpha();
212 let components = pixel_format.split_stream_components();
213 let pixel_count = width * source_height;
215 let alpha_size = if has_alpha { pixel_count } else { 0 };
216 let split_count = pixel_count * components;
217
218 let predictor_offset = alpha_size;
219 let predictor_end = predictor_offset + source_height;
220 let high_offset = predictor_end;
221 let high_end = high_offset + split_count;
222 let low_offset = high_end;
223 let low_end = low_offset + split_count;
224
225 if decompressed.len() < low_end {
226 return Err(Deepmap2Error::InvalidData(format!(
227 "Default 解压后数据不足: 需要 {} 字节,实际 {}",
228 low_end,
229 decompressed.len()
230 )));
231 }
232
233 let alpha_plane = if has_alpha {
234 Some(&decompressed[..alpha_size])
235 } else {
236 None
237 };
238 let predictor_bytes = &decompressed[predictor_offset..predictor_end];
239 let high_stream = &decompressed[high_offset..high_end];
240 let low_stream = &decompressed[low_offset..low_end];
241
242 let split_row_width = width * components;
243 let chroma_scale = header.chroma_scale();
244 let mut rgba = vec![0u8; width * height * 4];
245
246 for row in 0..height {
247 let row_offset = row * split_row_width;
248 let high_row = &high_stream[row_offset..row_offset + split_row_width];
249 let low_row = &low_stream[row_offset..row_offset + split_row_width];
250
251 let decoded_split_row: Vec<i16> = low_row
252 .iter()
253 .zip(high_row.iter())
254 .map(|(&lo, &hi)| {
255 let combined = (lo as u16) | ((hi as u16) << 8);
256 let magnitude = (combined >> 1) as i16;
257 if combined & 1 != 0 {
259 -magnitude
260 } else {
261 magnitude
262 }
263 })
264 .collect();
265
266 let decoded_split_row: Vec<i16> = if !pixel_format.is_color() {
269 decoded_split_row
270 .iter()
271 .flat_map(|&y| [y, 0i16, 0i16])
272 .collect()
273 } else {
274 decoded_split_row
275 };
276
277 let pred_type = predictor_bytes[row];
278 let predicted_row = apply_predictor(pred_type, &decoded_split_row, prev_row.as_deref());
279
280 let alpha_row = alpha_plane.map(|ap| &ap[row * width..(row + 1) * width]);
281 let rgba_row = row_to_rgba(pixel_format, &predicted_row, alpha_row, chroma_scale);
282
283 let base = row * width * 4;
284 rgba[base..base + rgba_row.len()].copy_from_slice(&rgba_row);
285
286 prev_row = Some(predicted_row);
287 }
288
289 Ok((rgba, prev_row))
290}
291
292fn decode_default(
294 header: &Deepmap2Header,
295 payload: &[u8],
296 width: usize,
297 height: usize,
298) -> Deepmap2Result<Vec<u8>> {
299 let decompressed = decompress_lzfse(payload)?;
300 decode_default_decompressed(header, &decompressed, width, height)
301}
302
303fn decode_default_with_state(
304 header: &Deepmap2Header,
305 payload: &[u8],
306 width: usize,
307 height: usize,
308 prev_row: Option<Vec<i16>>,
309) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
310 let decompressed = decompress_lzfse(payload)?;
311 decode_default_decompressed_with_state(header, &decompressed, width, height, height, prev_row)
312}
313
314fn decode_none_or_lossless(
323 header: &Deepmap2Header,
324 payload: &[u8],
325 compressed: bool,
326 width: usize,
327 height: usize,
328) -> Deepmap2Result<Vec<u8>> {
329 let pixel_format = header.pixel_format;
330 let expected = width * height * pixel_format.bytes_per_pixel();
331 let output_bytes = if compressed {
332 if expected < LZVN_THRESHOLD {
333 match decompress_lzvn(payload, expected) {
334 Ok(bytes) => bytes,
335 Err(_) => decompress_lzfse(payload)?,
336 }
337 } else {
338 decompress_lzfse(payload)?
339 }
340 } else {
341 payload.to_vec()
342 };
343 if output_bytes.len() < expected {
344 return Err(Deepmap2Error::InvalidData(format!(
345 "输出数据不足: 期望 {} 字节,实际 {}",
346 expected,
347 output_bytes.len()
348 )));
349 }
350 Ok(output_bytes_to_rgba(
351 pixel_format,
352 width,
353 height,
354 &output_bytes[..expected],
355 ))
356}
357
358fn decode_palette(
360 header: &Deepmap2Header,
361 payload: &[u8],
362 width: usize,
363 height: usize,
364) -> Deepmap2Result<Vec<u8>> {
365 let palette_size = header.palette_size.unwrap_or(0) as usize;
366 let palette_type = header.palette_type.unwrap_or(0);
367 if palette_size == 0 {
368 return Err(Deepmap2Error::InvalidData(
369 "Palette 模式缺少调色板".to_string(),
370 ));
371 }
372 let palette = &header.palette;
373
374 let pixel_count = width * height;
375 let expected = match palette_type {
376 3 => pixel_count * 2,
377 4 => pixel_count,
378 other => return Err(Deepmap2Error::InvalidPaletteType(other)),
379 };
380 let decompressed = if let Some(chunks) = parse_size_prefixed_chunks(payload) {
381 let mut out = Vec::with_capacity(expected);
382 for chunk in chunks {
383 if out.len() >= expected {
384 break;
385 }
386 let remaining = expected.saturating_sub(out.len());
387 let part = decompress_lzfse_or_lzvn(chunk, remaining)?;
388 out.extend_from_slice(&part);
389 }
390 out
391 } else {
392 decompress_lzfse_or_lzvn(payload, expected)?
393 };
394
395 match palette_type {
396 3 => {
397 let needed = expected;
399 if decompressed.len() < needed {
400 return Err(Deepmap2Error::InvalidData(format!(
401 "Palette type3 数据不足: 需要 {} 字节,实际 {}",
402 needed,
403 decompressed.len()
404 )));
405 }
406 let alpha_plane = &decompressed[..pixel_count];
407 let index_plane = &decompressed[pixel_count..pixel_count * 2];
408 let mut rgba = vec![0u8; pixel_count * 4];
409 for (i, (&idx, &alpha)) in index_plane.iter().zip(alpha_plane.iter()).enumerate() {
410 let entry = palette.get(idx as usize).copied().unwrap_or(0);
411 let bytes = palette_entry_to_rgba(entry, Some(alpha));
412 rgba[i * 4..i * 4 + 4].copy_from_slice(&bytes);
413 }
414 Ok(rgba)
415 }
416 4 => {
417 let needed = expected;
419 if decompressed.len() < needed {
420 return Err(Deepmap2Error::InvalidData(format!(
421 "Palette type4 数据不足: 需要 {} 字节,实际 {}",
422 needed,
423 decompressed.len()
424 )));
425 }
426 let mut rgba = vec![0u8; pixel_count * 4];
427 for (i, &idx) in decompressed[..needed].iter().enumerate() {
428 let entry = palette.get(idx as usize).copied().unwrap_or(0);
429 let bytes = palette_entry_to_rgba(entry, None);
430 rgba[i * 4..i * 4 + 4].copy_from_slice(&bytes);
431 }
432 Ok(rgba)
433 }
434 other => Err(Deepmap2Error::InvalidPaletteType(other)),
435 }
436}
437
438fn decode_size_prefixed_default(
443 header: &Deepmap2Header,
444 chunks: &[&[u8]],
445 width: usize,
446 total_height: usize,
447) -> Deepmap2Result<Vec<u8>> {
448 let pixel_format = header.pixel_format;
449 let components = pixel_format.split_stream_components();
450 let has_alpha = pixel_format.has_alpha();
451
452 let row_stride = (if has_alpha { width } else { 0 }) + 1 + width * components * 2;
454
455 let mut rgba = vec![0u8; width * total_height * 4];
456 let mut cursor_height = 0usize;
457
458 for chunk in chunks {
459 let remaining = total_height - cursor_height;
460 if remaining == 0 {
461 break;
462 }
463 let decompressed = decompress_lzfse(chunk)?;
464 let source_height = decompressed.len().checked_div(row_stride).unwrap_or(0);
465 if source_height == 0 {
466 return Err(Deepmap2Error::InvalidData(
467 "无法从 Default chunk 推断有效高度".to_string(),
468 ));
469 }
470 let chunk_h = source_height.min(remaining);
471
472 let (chunk_rgba, _) = decode_default_decompressed_with_state(
473 header,
474 &decompressed,
475 width,
476 source_height,
477 chunk_h,
478 None,
479 )?;
480 let copy_len = (width * chunk_h * 4).min(chunk_rgba.len());
481 let dst = cursor_height * width * 4;
482 rgba[dst..dst + copy_len].copy_from_slice(&chunk_rgba[..copy_len]);
483 cursor_height += chunk_h;
484 }
485
486 if cursor_height != total_height {
487 return Err(Deepmap2Error::InvalidData(format!(
488 "size-prefixed 容器输出高度不足: 期望 {total_height},实际 {cursor_height}"
489 )));
490 }
491
492 Ok(rgba)
493}
494
495fn decode_size_prefixed_default_with_state(
496 header: &Deepmap2Header,
497 chunks: &[&[u8]],
498 width: usize,
499 total_height: usize,
500 mut prev_row: Option<Vec<i16>>,
501) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
502 let pixel_format = header.pixel_format;
503 let components = pixel_format.split_stream_components();
504 let has_alpha = pixel_format.has_alpha();
505
506 let row_stride = (if has_alpha { width } else { 0 }) + 1 + width * components * 2;
508
509 let mut rgba = vec![0u8; width * total_height * 4];
510 let mut cursor_height = 0usize;
511
512 for chunk in chunks {
513 let remaining = total_height - cursor_height;
514 if remaining == 0 {
515 break;
516 }
517 let decompressed = decompress_lzfse(chunk)?;
518 let source_height = decompressed.len().checked_div(row_stride).unwrap_or(0);
519 if source_height == 0 {
520 return Err(Deepmap2Error::InvalidData(
521 "无法从 Default chunk 推断有效高度".to_string(),
522 ));
523 }
524 let chunk_h = source_height.min(remaining);
525
526 let (chunk_rgba, next_prev_row) = decode_default_decompressed_with_state(
527 header,
528 &decompressed,
529 width,
530 source_height,
531 chunk_h,
532 prev_row,
533 )?;
534 let copy_len = (width * chunk_h * 4).min(chunk_rgba.len());
535 let dst = cursor_height * width * 4;
536 rgba[dst..dst + copy_len].copy_from_slice(&chunk_rgba[..copy_len]);
537 cursor_height += chunk_h;
538 prev_row = next_prev_row;
539 }
540
541 if cursor_height != total_height {
542 return Err(Deepmap2Error::InvalidData(format!(
543 "size-prefixed 容器输出高度不足: 期望 {total_height},实际 {cursor_height}"
544 )));
545 }
546
547 Ok((rgba, prev_row))
548}
549
550#[allow(dead_code)]
552fn decode_size_prefixed_none_or_lossless(
553 header: &Deepmap2Header,
554 chunks: &[&[u8]],
555 compressed: bool,
556 width: usize,
557 total_height: usize,
558) -> Deepmap2Result<Vec<u8>> {
559 let source_height = header.height as usize;
561
562 let mut rgba = vec![0u8; width * total_height * 4];
563 let mut cursor_height = 0usize;
564
565 for chunk in chunks {
566 let remaining = total_height - cursor_height;
567 if remaining == 0 {
568 return Err(Deepmap2Error::InvalidData(
569 "size-prefixed 容器包含超出目标高度的额外 chunk".to_string(),
570 ));
571 }
572 let chunk_height = source_height.min(remaining);
573 let chunk_rgba = decode_none_or_lossless(header, chunk, compressed, width, chunk_height)?;
574 let copy_len = (width * chunk_height * 4).min(chunk_rgba.len());
575 let dst = cursor_height * width * 4;
576 rgba[dst..dst + copy_len].copy_from_slice(&chunk_rgba[..copy_len]);
577 cursor_height += chunk_height;
578 }
579
580 if cursor_height != total_height {
581 return Err(Deepmap2Error::InvalidData(format!(
582 "size-prefixed 容器输出高度不足: 期望 {total_height},实际 {cursor_height}"
583 )));
584 }
585
586 Ok(rgba)
587}
588
589fn decode_payload(
591 header: &Deepmap2Header,
592 payload: &[u8],
593 width: usize,
594 height: usize,
595) -> Deepmap2Result<Vec<u8>> {
596 let decode_type = header.decode_type;
597
598 if let PixelFormat::Unknown { tag } = header.pixel_format {
600 return Err(Deepmap2Error::UnsupportedPixelFormat(tag));
601 }
602
603 match decode_type {
604 DecodeType::Default => {
605 if let Some(chunks) = parse_size_prefixed_chunks(payload) {
606 decode_size_prefixed_default(header, &chunks, width, height)
607 } else {
608 decode_default(header, payload, width, height)
609 }
610 }
611 DecodeType::Lossless => {
612 if let Some(chunks) = parse_size_prefixed_chunks(payload) {
613 decode_size_prefixed_none_or_lossless(header, &chunks, true, width, height)
614 } else {
615 decode_none_or_lossless(header, payload, true, width, height)
616 }
617 }
618 DecodeType::None => decode_none_or_lossless(header, payload, false, width, height),
619 DecodeType::Palette => decode_palette(header, payload, width, height),
620 DecodeType::Unknown { tag } => Err(Deepmap2Error::UnsupportedDecodeType(tag)),
621 }
622}
623
624fn decode_payload_with_state(
625 header: &Deepmap2Header,
626 payload: &[u8],
627 width: usize,
628 height: usize,
629 prev_row: Option<Vec<i16>>,
630) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
631 if let PixelFormat::Unknown { tag } = header.pixel_format {
632 return Err(Deepmap2Error::UnsupportedPixelFormat(tag));
633 }
634
635 match header.decode_type {
636 DecodeType::Default => {
637 if let Some(chunks) = parse_size_prefixed_chunks(payload) {
638 decode_size_prefixed_default_with_state(header, &chunks, width, height, prev_row)
639 } else {
640 decode_default_with_state(header, payload, width, height, prev_row)
641 }
642 }
643 _ => Ok((decode_payload(header, payload, width, height)?, None)),
644 }
645}
646
647pub fn decode_raw(header: &Deepmap2Header, payload: &[u8]) -> Deepmap2Result<Vec<u8>> {
649 decode_raw_with_options(header, payload, None, None, None)
650}
651
652pub fn decode_raw_with_height(
654 header: &Deepmap2Header,
655 payload: &[u8],
656 output_height: u16,
657) -> Deepmap2Result<Vec<u8>> {
658 decode_raw_with_options(header, payload, None, Some(output_height), None)
659}
660
661pub fn decode_raw_with_options(
662 header: &Deepmap2Header,
663 payload: &[u8],
664 output_width: Option<u16>,
665 output_height: Option<u16>,
666 pixel_format_override: Option<PixelFormat>,
667) -> Deepmap2Result<Vec<u8>> {
668 let header = apply_pixel_format_override(header, pixel_format_override);
669 decode_payload(
670 &header,
671 payload,
672 output_width.unwrap_or(header.width) as usize,
673 output_height.unwrap_or(header.height) as usize,
674 )
675}
676
677pub fn decode_raw_with_options_and_state(
678 header: &Deepmap2Header,
679 payload: &[u8],
680 output_width: Option<u16>,
681 output_height: Option<u16>,
682 pixel_format_override: Option<PixelFormat>,
683 prev_row: Option<Vec<i16>>,
684) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
685 let header = apply_pixel_format_override(header, pixel_format_override);
686 decode_payload_with_state(
687 &header,
688 payload,
689 output_width.unwrap_or(header.width) as usize,
690 output_height.unwrap_or(header.height) as usize,
691 prev_row,
692 )
693}
694
695#[cfg(test)]
696mod tests {
697 use super::decompress_lzfse;
698 use lzfse_rust::{LzfseRingDecoder, LzfseRingEncoder};
699
700 #[cfg(target_arch = "wasm32")]
701 use crate::Deepmap2Error;
702
703 fn encode_lzfse(data: &[u8]) -> Vec<u8> {
704 let mut encoded = Vec::new();
705 let mut encoder = LzfseRingEncoder::default();
706 encoder
707 .encode_bytes(data, &mut encoded)
708 .expect("test lzfse encode should succeed");
709 encoded
710 }
711
712 fn build_palette4_size_prefixed_lzvn(width: u16, height: u16, indexes: &[u8]) -> Vec<u8> {
713 assert_eq!(indexes.len(), width as usize * height as usize);
714
715 let palette = [
716 0xFFFF0000u32, 0xFF00FF00u32, 0xFF0000FFu32, ];
720 let compressed = lzvn::encode_raw(indexes);
721 let mut data = Vec::new();
722 data.extend_from_slice(b"dmp2");
723 data.push(4); data.push(0);
725 data.push(0);
726 data.push(4); data.extend_from_slice(&width.to_le_bytes());
728 data.extend_from_slice(&height.to_le_bytes());
729 data.extend_from_slice(&(palette.len() as u16).to_le_bytes());
730 data.extend_from_slice(&4u16.to_le_bytes()); for entry in palette {
732 data.extend_from_slice(&entry.to_le_bytes());
733 }
734 data.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
735 data.extend_from_slice(&compressed);
736 data
737 }
738
739 fn padded_default_fixture(
740 pixel_format: crate::model::PixelFormat,
741 ) -> (crate::model::Deepmap2Header, Vec<u8>) {
742 use crate::model::{DecodeType, Deepmap2Header};
743 let header = Deepmap2Header {
744 decode_type: DecodeType::Default,
745 version: 0,
746 predictor_type: 0,
747 pixel_format,
748 width: 1,
749 height: 1,
750 palette_size: None,
751 palette_type: None,
752 palette: Vec::new(),
753 };
754 let mut raw = Vec::new();
755 if pixel_format.has_alpha() {
756 raw.extend_from_slice(&[255, 128]);
757 }
758 raw.extend_from_slice(&[3, 0]); let components = pixel_format.split_stream_components();
760 raw.extend(vec![0; components * 2]); if pixel_format.is_color() {
762 raw.extend_from_slice(&[20, 4, 8, 40, 6, 10]);
763 } else {
764 raw.extend_from_slice(&[20, 40]);
765 }
766 (header, encode_lzfse(&raw))
767 }
768
769 #[test]
770 fn padded_default_chunk_uses_source_plane_offsets() {
771 use crate::model::PixelFormat;
772 for pixel_format in [
773 PixelFormat::G8,
774 PixelFormat::GA88,
775 PixelFormat::Rgb888,
776 PixelFormat::Rgba8888,
777 ] {
778 let (header, chunk) = padded_default_fixture(pixel_format);
779 let rgba = super::decode_size_prefixed_default(&header, &[&chunk], 1, 1).unwrap();
780 let expected = if pixel_format.is_color() {
781 [7, 12, 9, 255]
782 } else {
783 [10, 10, 10, 255]
784 };
785 assert_eq!(rgba, expected, "{pixel_format:?}");
786
787 let rgba =
789 super::decode_size_prefixed_default(&header, &[&chunk, &chunk], 1, 3).unwrap();
790 assert_eq!(rgba.len(), 12);
791 assert_eq!(&rgba[8..], &expected);
792 }
793 }
794
795 #[test]
796 fn padded_default_chunk_state_ends_at_last_output_row() {
797 use crate::model::PixelFormat;
798 for pixel_format in [
799 PixelFormat::G8,
800 PixelFormat::GA88,
801 PixelFormat::Rgb888,
802 PixelFormat::Rgba8888,
803 ] {
804 let (header, chunk) = padded_default_fixture(pixel_format);
805 let (rgba, state) = super::decode_size_prefixed_default_with_state(
806 &header,
807 &[&chunk],
808 1,
809 1,
810 Some(vec![5, 0, 0]),
811 )
812 .unwrap();
813 let expected = if pixel_format.is_color() {
814 [12, 17, 14, 255]
815 } else {
816 [15, 15, 15, 255]
817 };
818 assert_eq!(rgba, expected, "{pixel_format:?}");
819 let expected_state = if pixel_format.is_color() {
820 vec![15, 2, 4]
821 } else {
822 vec![15, 0, 0]
823 };
824 assert_eq!(
825 state,
826 Some(expected_state),
827 "padding must not advance predictor state"
828 );
829
830 let (rgba, state) = super::decode_size_prefixed_default_with_state(
831 &header,
832 &[&chunk, &chunk],
833 1,
834 3,
835 Some(vec![5, 0, 0]),
836 )
837 .unwrap();
838 assert_eq!(rgba.len(), 12);
839 let expected_state = if pixel_format.is_color() {
840 vec![30, 5, 9]
841 } else {
842 vec![30, 0, 0]
843 };
844 assert_eq!(state, Some(expected_state), "state must flow across chunks");
845 }
846 }
847
848 #[test]
849 fn single_apple_stream_keeps_pure_rust_decode_path() {
850 let expected = b"single-stream-decode-regression";
851 let encoded = encode_lzfse(expected);
852
853 let decoded = decompress_lzfse(&encoded).expect("single Apple stream should decode");
854 assert_eq!(decoded, expected);
855 }
856
857 #[test]
858 fn concatenated_apple_streams_decode_in_pure_rust() {
859 let first_raw = b"deepmap2 concatenated apple stream part one";
860 let second_raw = b"deepmap2 concatenated apple stream part two";
861 let first = encode_lzfse(first_raw);
862 let second = encode_lzfse(second_raw);
863
864 let mut concatenated = Vec::new();
865 concatenated.extend_from_slice(&first);
866 concatenated.extend_from_slice(&[0u8; 4]);
867 concatenated.extend_from_slice(&second);
868
869 let mut single_stream_output = Vec::new();
870 let mut decoder = LzfseRingDecoder::default();
871 assert!(
872 decoder
873 .decode_bytes(&concatenated, &mut single_stream_output)
874 .is_err(),
875 "single-stream decoder should reject concatenated Apple streams"
876 );
877
878 let decoded =
879 decompress_lzfse(&concatenated).expect("concatenated Apple streams should decode");
880 assert_eq!(decoded, [first_raw.as_ref(), second_raw.as_ref()].concat());
881 }
882
883 #[test]
884 fn palette_type4_size_prefixed_lzvn_decodes() {
885 let indexes = [0, 1, 2, 2, 1, 0];
886 let data = build_palette4_size_prefixed_lzvn(3, 2, &indexes);
887
888 let image = crate::decode(&data).expect("palette LZVN payload should decode");
889
890 assert_eq!((image.width, image.height), (3, 2));
891 assert_eq!(
892 image.rgba,
893 vec![
894 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 0, 255,
895 255, 0, 0, 255,
896 ]
897 );
898 }
899
900 #[cfg(target_arch = "wasm32")]
901 use wasm_bindgen_test::wasm_bindgen_test;
902
903 #[cfg(target_arch = "wasm32")]
904 #[wasm_bindgen_test]
905 fn invalid_non_apple_stream_reports_lzfse_decompress() {
906 let err =
907 decompress_lzfse(b"not-valid-lzfse").expect_err("invalid non-Apple stream should fail");
908 assert!(matches!(err, Deepmap2Error::LzfseDecompress));
909 }
910
911 #[cfg(target_arch = "wasm32")]
912 #[wasm_bindgen_test]
913 fn invalid_apple_stream_reports_native_fallback_unavailable() {
914 let err = decompress_lzfse(b"bvx2not-valid-lzfse")
915 .expect_err("invalid Apple compression stream should fail on wasm fallback path");
916 assert!(matches!(err, Deepmap2Error::NativeFallbackUnavailable(_)));
917 }
918}