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!(data.get(..4), Some(b"bvx2") | Some(b"bvxn") | Some(b"bvx-"))
55}
56
57fn decode_lzfse_rust_only(data: &[u8]) -> Option<Vec<u8>> {
58 let cap = data.len().saturating_mul(16).max(4096);
59 let mut out = Vec::with_capacity(cap);
60 let mut decoder = LzfseRingDecoder::default();
61 if let Ok(n) = decoder.decode_bytes(data, &mut out)
62 && (n > 0 || data.is_empty())
63 {
64 out.truncate(n as usize);
65 Some(out)
66 } else {
67 None
68 }
69}
70
71fn find_concatenated_apple_segment_boundary(data: &[u8]) -> Option<(usize, usize)> {
72 let mut search_offset = 0usize;
73 while search_offset + APPLE_STREAM_EOS_MAGIC.len() <= data.len() {
74 let rel = data[search_offset..]
75 .windows(APPLE_STREAM_EOS_MAGIC.len())
76 .position(|window| window == APPLE_STREAM_EOS_MAGIC)?;
77 let eos_pos = search_offset + rel;
78 let next_scan_start = eos_pos + APPLE_STREAM_EOS_MAGIC.len();
79 let next_scan_end = data.len().min(next_scan_start + APPLE_STREAM_SCAN_LIMIT);
80
81 if let Some(next_rel) = data[next_scan_start..next_scan_end]
82 .windows(4)
83 .position(is_apple_stream_start_magic)
84 {
85 return Some((next_scan_start, next_scan_start + next_rel));
86 }
87
88 search_offset = next_scan_start;
89 }
90
91 None
92}
93
94fn decompress_concatenated_apple_stream(data: &[u8]) -> Deepmap2Result<Vec<u8>> {
95 let mut decoded = Vec::new();
96 let mut cursor = 0usize;
97
98 loop {
99 let remaining = &data[cursor..];
100 let segment = apple_stream_slice(remaining);
101 let start_offset = remaining.len().saturating_sub(segment.len());
102 if start_offset > APPLE_STREAM_SCAN_LIMIT
103 || !apple_compression::is_apple_compression_stream(segment)
104 {
105 return Err(Deepmap2Error::LzfseDecompress);
106 }
107 cursor += start_offset;
108
109 if let Some((segment_end, next_segment_start)) =
110 find_concatenated_apple_segment_boundary(&data[cursor..])
111 {
112 let segment = &data[cursor..cursor + segment_end];
113 let part = decode_lzfse_rust_only(segment).ok_or(Deepmap2Error::LzfseDecompress)?;
114 decoded.extend_from_slice(&part);
115 cursor += next_segment_start;
116 continue;
117 }
118
119 let part = decode_lzfse_rust_only(&data[cursor..]).ok_or(Deepmap2Error::LzfseDecompress)?;
120 decoded.extend_from_slice(&part);
121 return Ok(decoded);
122 }
123}
124
125fn decompress_lzfse(data: &[u8]) -> Deepmap2Result<Vec<u8>> {
127 let data = apple_stream_slice(data);
128 let is_apple_stream = apple_compression::is_apple_compression_stream(data);
129 if let Some(decoded) = decode_lzfse_rust_only(data) {
130 return Ok(decoded);
131 }
132
133 if !is_apple_stream {
134 return Err(Deepmap2Error::LzfseDecompress);
135 }
136
137 if let Ok(decoded) = decompress_concatenated_apple_stream(data) {
138 return Ok(decoded);
139 }
140
141 apple_compression::decode_lzfse_with_fallback(data).map_err(map_apple_compression_error)
142}
143
144pub fn decompress_lzvn(data: &[u8], expected_output_size: usize) -> Deepmap2Result<Vec<u8>> {
146 lzvn::decode_raw(data, expected_output_size).map_err(|_| Deepmap2Error::LzvnDecompress)
147}
148
149fn parse_size_prefixed_chunks(payload: &[u8]) -> Option<Vec<&[u8]>> {
154 let mut chunks = Vec::new();
155 let mut cursor = 0usize;
156 while cursor + 4 <= payload.len() {
157 let chunk_size = u32::from_le_bytes(payload[cursor..cursor + 4].try_into().ok()?) as usize;
158 cursor += 4;
159 if chunk_size == 0 || cursor + chunk_size > payload.len() {
160 return None;
161 }
162 chunks.push(&payload[cursor..cursor + chunk_size]);
163 cursor += chunk_size;
164 }
165 if cursor != payload.len() || chunks.is_empty() {
166 return None;
167 }
168 Some(chunks)
169}
170
171fn decompress_lzfse_or_lzvn(data: &[u8], expected_output_size: usize) -> Deepmap2Result<Vec<u8>> {
172 match decompress_lzfse(data) {
173 Ok(bytes) => Ok(bytes),
174 Err(err @ Deepmap2Error::LzfseDecompress) => {
175 decompress_lzvn(data, expected_output_size).map_err(|_| err)
176 }
177 Err(err) => Err(err),
178 }
179}
180
181fn decode_default_decompressed(
189 header: &Deepmap2Header,
190 decompressed: &[u8],
191 width: usize,
192 height: usize,
193) -> Deepmap2Result<Vec<u8>> {
194 let (rgba, _) =
195 decode_default_decompressed_with_state(header, decompressed, width, height, None)?;
196 Ok(rgba)
197}
198
199fn decode_default_decompressed_with_state(
200 header: &Deepmap2Header,
201 decompressed: &[u8],
202 width: usize,
203 height: usize,
204 mut prev_row: Option<Vec<i16>>,
205) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
206 let pixel_format = header.pixel_format;
207 let has_alpha = pixel_format.has_alpha();
208 let components = pixel_format.split_stream_components();
209 let pixel_count = width * height;
210 let alpha_size = if has_alpha { pixel_count } else { 0 };
211 let split_count = pixel_count * components;
212
213 let predictor_offset = alpha_size;
214 let predictor_end = predictor_offset + height;
215 let high_offset = predictor_end;
216 let high_end = high_offset + split_count;
217 let low_offset = high_end;
218 let low_end = low_offset + split_count;
219
220 if decompressed.len() < low_end {
221 return Err(Deepmap2Error::InvalidData(format!(
222 "Default 解压后数据不足: 需要 {} 字节,实际 {}",
223 low_end,
224 decompressed.len()
225 )));
226 }
227
228 let alpha_plane = if has_alpha {
229 Some(&decompressed[..alpha_size])
230 } else {
231 None
232 };
233 let predictor_bytes = &decompressed[predictor_offset..predictor_end];
234 let high_stream = &decompressed[high_offset..high_end];
235 let low_stream = &decompressed[low_offset..low_end];
236
237 let split_row_width = width * components;
238 let chroma_scale = header.chroma_scale();
239 let mut rgba = vec![0u8; pixel_count * 4];
240
241 for row in 0..height {
242 let row_offset = row * split_row_width;
243 let high_row = &high_stream[row_offset..row_offset + split_row_width];
244 let low_row = &low_stream[row_offset..row_offset + split_row_width];
245
246 let decoded_split_row: Vec<i16> = low_row
247 .iter()
248 .zip(high_row.iter())
249 .map(|(&lo, &hi)| {
250 let combined = (lo as u16) | ((hi as u16) << 8);
251 let magnitude = (combined >> 1) as i16;
252 if combined & 1 != 0 {
254 -magnitude
255 } else {
256 magnitude
257 }
258 })
259 .collect();
260
261 let decoded_split_row: Vec<i16> = if !pixel_format.is_color() {
264 decoded_split_row
265 .iter()
266 .flat_map(|&y| [y, 0i16, 0i16])
267 .collect()
268 } else {
269 decoded_split_row
270 };
271
272 let pred_type = predictor_bytes[row];
273 let predicted_row = apply_predictor(pred_type, &decoded_split_row, prev_row.as_deref());
274
275 let alpha_row = alpha_plane.map(|ap| &ap[row * width..(row + 1) * width]);
276 let rgba_row = row_to_rgba(pixel_format, &predicted_row, alpha_row, chroma_scale);
277
278 let base = row * width * 4;
279 rgba[base..base + rgba_row.len()].copy_from_slice(&rgba_row);
280
281 prev_row = Some(predicted_row);
282 }
283
284 Ok((rgba, prev_row))
285}
286
287fn decode_default(
289 header: &Deepmap2Header,
290 payload: &[u8],
291 width: usize,
292 height: usize,
293) -> Deepmap2Result<Vec<u8>> {
294 let decompressed = decompress_lzfse(payload)?;
295 decode_default_decompressed(header, &decompressed, width, height)
296}
297
298fn decode_default_with_state(
299 header: &Deepmap2Header,
300 payload: &[u8],
301 width: usize,
302 height: usize,
303 prev_row: Option<Vec<i16>>,
304) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
305 let decompressed = decompress_lzfse(payload)?;
306 decode_default_decompressed_with_state(header, &decompressed, width, height, prev_row)
307}
308
309fn decode_none_or_lossless(
318 header: &Deepmap2Header,
319 payload: &[u8],
320 compressed: bool,
321 width: usize,
322 height: usize,
323) -> Deepmap2Result<Vec<u8>> {
324 let pixel_format = header.pixel_format;
325 let expected = width * height * pixel_format.bytes_per_pixel();
326 let output_bytes = if compressed {
327 if expected < LZVN_THRESHOLD {
328 match decompress_lzvn(payload, expected) {
329 Ok(bytes) => bytes,
330 Err(_) => decompress_lzfse(payload)?,
331 }
332 } else {
333 decompress_lzfse(payload)?
334 }
335 } else {
336 payload.to_vec()
337 };
338 if output_bytes.len() < expected {
339 return Err(Deepmap2Error::InvalidData(format!(
340 "输出数据不足: 期望 {} 字节,实际 {}",
341 expected,
342 output_bytes.len()
343 )));
344 }
345 Ok(output_bytes_to_rgba(
346 pixel_format,
347 width,
348 height,
349 &output_bytes[..expected],
350 ))
351}
352
353fn decode_palette(
355 header: &Deepmap2Header,
356 payload: &[u8],
357 width: usize,
358 height: usize,
359) -> Deepmap2Result<Vec<u8>> {
360 let palette_size = header.palette_size.unwrap_or(0) as usize;
361 let palette_type = header.palette_type.unwrap_or(0);
362 if palette_size == 0 {
363 return Err(Deepmap2Error::InvalidData(
364 "Palette 模式缺少调色板".to_string(),
365 ));
366 }
367 let palette = &header.palette;
368
369 let pixel_count = width * height;
370 let expected = match palette_type {
371 3 => pixel_count * 2,
372 4 => pixel_count,
373 other => return Err(Deepmap2Error::InvalidPaletteType(other)),
374 };
375 let decompressed = if let Some(chunks) = parse_size_prefixed_chunks(payload) {
376 let mut out = Vec::with_capacity(expected);
377 for chunk in chunks {
378 if out.len() >= expected {
379 break;
380 }
381 let remaining = expected.saturating_sub(out.len());
382 let part = decompress_lzfse_or_lzvn(chunk, remaining)?;
383 out.extend_from_slice(&part);
384 }
385 out
386 } else {
387 decompress_lzfse_or_lzvn(payload, expected)?
388 };
389
390 match palette_type {
391 3 => {
392 let needed = expected;
394 if decompressed.len() < needed {
395 return Err(Deepmap2Error::InvalidData(format!(
396 "Palette type3 数据不足: 需要 {} 字节,实际 {}",
397 needed,
398 decompressed.len()
399 )));
400 }
401 let alpha_plane = &decompressed[..pixel_count];
402 let index_plane = &decompressed[pixel_count..pixel_count * 2];
403 let mut rgba = vec![0u8; pixel_count * 4];
404 for (i, (&idx, &alpha)) in index_plane.iter().zip(alpha_plane.iter()).enumerate() {
405 let entry = palette.get(idx as usize).copied().unwrap_or(0);
406 let bytes = palette_entry_to_rgba(entry, Some(alpha));
407 rgba[i * 4..i * 4 + 4].copy_from_slice(&bytes);
408 }
409 Ok(rgba)
410 }
411 4 => {
412 let needed = expected;
414 if decompressed.len() < needed {
415 return Err(Deepmap2Error::InvalidData(format!(
416 "Palette type4 数据不足: 需要 {} 字节,实际 {}",
417 needed,
418 decompressed.len()
419 )));
420 }
421 let mut rgba = vec![0u8; pixel_count * 4];
422 for (i, &idx) in decompressed[..needed].iter().enumerate() {
423 let entry = palette.get(idx as usize).copied().unwrap_or(0);
424 let bytes = palette_entry_to_rgba(entry, None);
425 rgba[i * 4..i * 4 + 4].copy_from_slice(&bytes);
426 }
427 Ok(rgba)
428 }
429 other => Err(Deepmap2Error::InvalidPaletteType(other)),
430 }
431}
432
433fn decode_size_prefixed_default(
438 header: &Deepmap2Header,
439 chunks: &[&[u8]],
440 width: usize,
441 total_height: usize,
442) -> Deepmap2Result<Vec<u8>> {
443 let pixel_format = header.pixel_format;
444 let components = pixel_format.split_stream_components();
445 let has_alpha = pixel_format.has_alpha();
446
447 let row_stride = (if has_alpha { width } else { 0 }) + 1 + width * components * 2;
449
450 let mut rgba = vec![0u8; width * total_height * 4];
451 let mut cursor_height = 0usize;
452
453 for chunk in chunks {
454 let remaining = total_height - cursor_height;
455 if remaining == 0 {
456 break;
457 }
458 let decompressed = decompress_lzfse(chunk)?;
459 let source_height = decompressed.len().checked_div(row_stride).unwrap_or(0);
460 if source_height == 0 {
461 return Err(Deepmap2Error::InvalidData(
462 "无法从 Default chunk 推断有效高度".to_string(),
463 ));
464 }
465 let chunk_h = source_height.min(remaining);
466
467 let alpha_size = if has_alpha { width * chunk_h } else { 0 };
468 let predictor_size = chunk_h;
469 let split_count = width * components * chunk_h;
470 let needed = alpha_size + predictor_size + split_count * 2;
471 let truncated: &[u8] = if decompressed.len() >= needed {
472 &decompressed[..needed]
473 } else {
474 decompressed.as_slice()
475 };
476
477 let chunk_rgba = decode_default_decompressed(header, truncated, width, chunk_h)?;
478 let copy_len = (width * chunk_h * 4).min(chunk_rgba.len());
479 let dst = cursor_height * width * 4;
480 rgba[dst..dst + copy_len].copy_from_slice(&chunk_rgba[..copy_len]);
481 cursor_height += chunk_h;
482 }
483
484 if cursor_height != total_height {
485 return Err(Deepmap2Error::InvalidData(format!(
486 "size-prefixed 容器输出高度不足: 期望 {total_height},实际 {cursor_height}"
487 )));
488 }
489
490 Ok(rgba)
491}
492
493fn decode_size_prefixed_default_with_state(
494 header: &Deepmap2Header,
495 chunks: &[&[u8]],
496 width: usize,
497 total_height: usize,
498 mut prev_row: Option<Vec<i16>>,
499) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
500 let pixel_format = header.pixel_format;
501 let components = pixel_format.split_stream_components();
502 let has_alpha = pixel_format.has_alpha();
503
504 let row_stride = (if has_alpha { width } else { 0 }) + 1 + width * components * 2;
506
507 let mut rgba = vec![0u8; width * total_height * 4];
508 let mut cursor_height = 0usize;
509
510 for chunk in chunks {
511 let remaining = total_height - cursor_height;
512 if remaining == 0 {
513 break;
514 }
515 let decompressed = decompress_lzfse(chunk)?;
516 let source_height = decompressed.len().checked_div(row_stride).unwrap_or(0);
517 if source_height == 0 {
518 return Err(Deepmap2Error::InvalidData(
519 "无法从 Default chunk 推断有效高度".to_string(),
520 ));
521 }
522 let chunk_h = source_height.min(remaining);
523
524 let alpha_size = if has_alpha { width * chunk_h } else { 0 };
525 let predictor_size = chunk_h;
526 let split_count = width * components * chunk_h;
527 let needed = alpha_size + predictor_size + split_count * 2;
528 let truncated: &[u8] = if decompressed.len() >= needed {
529 &decompressed[..needed]
530 } else {
531 decompressed.as_slice()
532 };
533
534 let (chunk_rgba, next_prev_row) =
535 decode_default_decompressed_with_state(header, truncated, width, chunk_h, prev_row)?;
536 let copy_len = (width * chunk_h * 4).min(chunk_rgba.len());
537 let dst = cursor_height * width * 4;
538 rgba[dst..dst + copy_len].copy_from_slice(&chunk_rgba[..copy_len]);
539 cursor_height += chunk_h;
540 prev_row = next_prev_row;
541 }
542
543 if cursor_height != total_height {
544 return Err(Deepmap2Error::InvalidData(format!(
545 "size-prefixed 容器输出高度不足: 期望 {total_height},实际 {cursor_height}"
546 )));
547 }
548
549 Ok((rgba, prev_row))
550}
551
552#[allow(dead_code)]
554fn decode_size_prefixed_none_or_lossless(
555 header: &Deepmap2Header,
556 chunks: &[&[u8]],
557 compressed: bool,
558 width: usize,
559 total_height: usize,
560) -> Deepmap2Result<Vec<u8>> {
561 let source_height = header.height as usize;
563
564 let mut rgba = vec![0u8; width * total_height * 4];
565 let mut cursor_height = 0usize;
566
567 for chunk in chunks {
568 let remaining = total_height - cursor_height;
569 if remaining == 0 {
570 return Err(Deepmap2Error::InvalidData(
571 "size-prefixed 容器包含超出目标高度的额外 chunk".to_string(),
572 ));
573 }
574 let chunk_height = source_height.min(remaining);
575 let chunk_rgba = decode_none_or_lossless(header, chunk, compressed, width, chunk_height)?;
576 let copy_len = (width * chunk_height * 4).min(chunk_rgba.len());
577 let dst = cursor_height * width * 4;
578 rgba[dst..dst + copy_len].copy_from_slice(&chunk_rgba[..copy_len]);
579 cursor_height += chunk_height;
580 }
581
582 if cursor_height != total_height {
583 return Err(Deepmap2Error::InvalidData(format!(
584 "size-prefixed 容器输出高度不足: 期望 {total_height},实际 {cursor_height}"
585 )));
586 }
587
588 Ok(rgba)
589}
590
591fn decode_payload(
593 header: &Deepmap2Header,
594 payload: &[u8],
595 width: usize,
596 height: usize,
597) -> Deepmap2Result<Vec<u8>> {
598 let decode_type = header.decode_type;
599
600 if let PixelFormat::Unknown { tag } = header.pixel_format {
602 return Err(Deepmap2Error::UnsupportedPixelFormat(tag));
603 }
604
605 match decode_type {
606 DecodeType::Default => {
607 if let Some(chunks) = parse_size_prefixed_chunks(payload) {
608 decode_size_prefixed_default(header, &chunks, width, height)
609 } else {
610 decode_default(header, payload, width, height)
611 }
612 }
613 DecodeType::Lossless => {
614 if let Some(chunks) = parse_size_prefixed_chunks(payload) {
615 decode_size_prefixed_none_or_lossless(header, &chunks, true, width, height)
616 } else {
617 decode_none_or_lossless(header, payload, true, width, height)
618 }
619 }
620 DecodeType::None => decode_none_or_lossless(header, payload, false, width, height),
621 DecodeType::Palette => decode_palette(header, payload, width, height),
622 DecodeType::Unknown { tag } => Err(Deepmap2Error::UnsupportedDecodeType(tag)),
623 }
624}
625
626fn decode_payload_with_state(
627 header: &Deepmap2Header,
628 payload: &[u8],
629 width: usize,
630 height: usize,
631 prev_row: Option<Vec<i16>>,
632) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
633 if let PixelFormat::Unknown { tag } = header.pixel_format {
634 return Err(Deepmap2Error::UnsupportedPixelFormat(tag));
635 }
636
637 match header.decode_type {
638 DecodeType::Default => {
639 if let Some(chunks) = parse_size_prefixed_chunks(payload) {
640 decode_size_prefixed_default_with_state(header, &chunks, width, height, prev_row)
641 } else {
642 decode_default_with_state(header, payload, width, height, prev_row)
643 }
644 }
645 _ => Ok((decode_payload(header, payload, width, height)?, None)),
646 }
647}
648
649pub fn decode_raw(header: &Deepmap2Header, payload: &[u8]) -> Deepmap2Result<Vec<u8>> {
651 decode_raw_with_options(header, payload, None, None, None)
652}
653
654pub fn decode_raw_with_height(
656 header: &Deepmap2Header,
657 payload: &[u8],
658 output_height: u16,
659) -> Deepmap2Result<Vec<u8>> {
660 decode_raw_with_options(header, payload, None, Some(output_height), None)
661}
662
663pub fn decode_raw_with_options(
664 header: &Deepmap2Header,
665 payload: &[u8],
666 output_width: Option<u16>,
667 output_height: Option<u16>,
668 pixel_format_override: Option<PixelFormat>,
669) -> Deepmap2Result<Vec<u8>> {
670 let header = apply_pixel_format_override(header, pixel_format_override);
671 decode_payload(
672 &header,
673 payload,
674 output_width.unwrap_or(header.width) as usize,
675 output_height.unwrap_or(header.height) as usize,
676 )
677}
678
679pub fn decode_raw_with_options_and_state(
680 header: &Deepmap2Header,
681 payload: &[u8],
682 output_width: Option<u16>,
683 output_height: Option<u16>,
684 pixel_format_override: Option<PixelFormat>,
685 prev_row: Option<Vec<i16>>,
686) -> Deepmap2Result<(Vec<u8>, Option<Vec<i16>>)> {
687 let header = apply_pixel_format_override(header, pixel_format_override);
688 decode_payload_with_state(
689 &header,
690 payload,
691 output_width.unwrap_or(header.width) as usize,
692 output_height.unwrap_or(header.height) as usize,
693 prev_row,
694 )
695}
696
697#[cfg(test)]
698mod tests {
699 use super::decompress_lzfse;
700 use lzfse_rust::{LzfseRingDecoder, LzfseRingEncoder};
701
702 #[cfg(target_arch = "wasm32")]
703 use crate::Deepmap2Error;
704
705 fn encode_lzfse(data: &[u8]) -> Vec<u8> {
706 let mut encoded = Vec::new();
707 let mut encoder = LzfseRingEncoder::default();
708 encoder
709 .encode_bytes(data, &mut encoded)
710 .expect("test lzfse encode should succeed");
711 encoded
712 }
713
714 fn build_palette4_size_prefixed_lzvn(width: u16, height: u16, indexes: &[u8]) -> Vec<u8> {
715 assert_eq!(indexes.len(), width as usize * height as usize);
716
717 let palette = [
718 0xFFFF0000u32, 0xFF00FF00u32, 0xFF0000FFu32, ];
722 let compressed = lzvn::encode_raw(indexes);
723 let mut data = Vec::new();
724 data.extend_from_slice(b"dmp2");
725 data.push(4); data.push(0);
727 data.push(0);
728 data.push(4); data.extend_from_slice(&width.to_le_bytes());
730 data.extend_from_slice(&height.to_le_bytes());
731 data.extend_from_slice(&(palette.len() as u16).to_le_bytes());
732 data.extend_from_slice(&4u16.to_le_bytes()); for entry in palette {
734 data.extend_from_slice(&entry.to_le_bytes());
735 }
736 data.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
737 data.extend_from_slice(&compressed);
738 data
739 }
740
741 #[test]
742 fn single_apple_stream_keeps_pure_rust_decode_path() {
743 let expected = b"single-stream-decode-regression";
744 let encoded = encode_lzfse(expected);
745
746 let decoded = decompress_lzfse(&encoded).expect("single Apple stream should decode");
747 assert_eq!(decoded, expected);
748 }
749
750 #[test]
751 fn concatenated_apple_streams_decode_in_pure_rust() {
752 let first_raw = b"deepmap2 concatenated apple stream part one";
753 let second_raw = b"deepmap2 concatenated apple stream part two";
754 let first = encode_lzfse(first_raw);
755 let second = encode_lzfse(second_raw);
756
757 let mut concatenated = Vec::new();
758 concatenated.extend_from_slice(&first);
759 concatenated.extend_from_slice(&[0u8; 4]);
760 concatenated.extend_from_slice(&second);
761
762 let mut single_stream_output = Vec::new();
763 let mut decoder = LzfseRingDecoder::default();
764 assert!(
765 decoder
766 .decode_bytes(&concatenated, &mut single_stream_output)
767 .is_err(),
768 "single-stream decoder should reject concatenated Apple streams"
769 );
770
771 let decoded =
772 decompress_lzfse(&concatenated).expect("concatenated Apple streams should decode");
773 assert_eq!(decoded, [first_raw.as_ref(), second_raw.as_ref()].concat());
774 }
775
776 #[test]
777 fn palette_type4_size_prefixed_lzvn_decodes() {
778 let indexes = [0, 1, 2, 2, 1, 0];
779 let data = build_palette4_size_prefixed_lzvn(3, 2, &indexes);
780
781 let image = crate::decode(&data).expect("palette LZVN payload should decode");
782
783 assert_eq!((image.width, image.height), (3, 2));
784 assert_eq!(
785 image.rgba,
786 vec![
787 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 0, 255,
788 255, 0, 0, 255,
789 ]
790 );
791 }
792
793 #[cfg(target_arch = "wasm32")]
794 use wasm_bindgen_test::wasm_bindgen_test;
795
796 #[cfg(target_arch = "wasm32")]
797 #[wasm_bindgen_test]
798 fn invalid_non_apple_stream_reports_lzfse_decompress() {
799 let err =
800 decompress_lzfse(b"not-valid-lzfse").expect_err("invalid non-Apple stream should fail");
801 assert!(matches!(err, Deepmap2Error::LzfseDecompress));
802 }
803
804 #[cfg(target_arch = "wasm32")]
805 #[wasm_bindgen_test]
806 fn invalid_apple_stream_reports_native_fallback_unavailable() {
807 let err = decompress_lzfse(b"bvx2not-valid-lzfse")
808 .expect_err("invalid Apple compression stream should fail on wasm fallback path");
809 assert!(matches!(err, Deepmap2Error::NativeFallbackUnavailable(_)));
810 }
811}