1use std::{borrow::Cow, io::Read};
2
3use flate2::read::MultiGzDecoder;
4use lzfse_rust::LzfseRingDecoder;
5use thiserror::Error;
6
7use crate::car::{CarError, CarResult};
8use crate::model::rendition::{
9 CompressionType, MultisizeImageSetEntry, Rendition, RenditionThemeCBCK,
10};
11use crate::model::{ColorSpace, Encoding};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OrientationPolicy {
15 Ignore,
16 ApplyExif,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct DecodeOptions {
21 pub max_output_bytes: Option<usize>,
22 pub orientation_policy: OrientationPolicy,
23}
24
25impl Default for DecodeOptions {
26 fn default() -> Self {
27 Self {
28 max_output_bytes: None,
29 orientation_policy: OrientationPolicy::Ignore,
30 }
31 }
32}
33
34impl DecodeOptions {
35 pub fn with_max_output_bytes(mut self, max_output_bytes: usize) -> Self {
36 self.max_output_bytes = Some(max_output_bytes);
37 self
38 }
39
40 pub fn apply_exif_orientation(mut self, apply: bool) -> Self {
41 self.orientation_policy = if apply {
42 OrientationPolicy::ApplyExif
43 } else {
44 OrientationPolicy::Ignore
45 };
46 self
47 }
48
49 pub(crate) fn check_output_bytes(&self, requested: usize, stage: DecodeStage) -> CarResult<()> {
50 if let Some(limit) = self.max_output_bytes
51 && requested > limit
52 {
53 return Err(CarError::DecodeBudgetExceeded(
54 DecodeBudgetError::OutputBytesExceeded {
55 limit,
56 requested,
57 stage,
58 },
59 ));
60 }
61 Ok(())
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum DecodeStage {
67 BorrowedPayload,
68 Uncompressed,
69 Lzfse,
70 Zip,
71 Lzvn,
72 Deepmap2,
73 ImageConversion,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Error)]
77pub enum DecodeBudgetError {
78 #[error(
79 "decoded output would exceed budget at {stage:?}: requested {requested}, limit {limit}"
80 )]
81 OutputBytesExceeded {
82 limit: usize,
83 requested: usize,
84 stage: DecodeStage,
85 },
86}
87
88#[derive(Debug)]
89pub enum RenditionData {
90 Color {
91 color_space: ColorSpace,
92 components: Vec<f64>,
93 },
94 Image {
95 data: Vec<u8>,
96 },
97 RawData {
98 data: Vec<u8>,
99 },
100 MultisizeImageSet {
101 entries: Vec<MultisizeImageSetEntry>,
102 },
103}
104
105#[derive(Debug)]
106pub enum RenditionDataRef<'a> {
107 Color {
108 color_space: Cow<'a, ColorSpace>,
109 components: Cow<'a, [f64]>,
110 },
111 Image {
112 data: Cow<'a, [u8]>,
113 },
114 RawData {
115 data: Cow<'a, [u8]>,
116 },
117 MultisizeImageSet {
118 entries: Cow<'a, [MultisizeImageSetEntry]>,
119 },
120}
121
122impl<'a> RenditionDataRef<'a> {
123 pub fn into_owned(self) -> RenditionData {
124 match self {
125 Self::Color {
126 color_space,
127 components,
128 } => RenditionData::Color {
129 color_space: color_space.into_owned(),
130 components: components.into_owned(),
131 },
132 Self::Image { data } => RenditionData::Image {
133 data: data.into_owned(),
134 },
135 Self::RawData { data } => RenditionData::RawData {
136 data: data.into_owned(),
137 },
138 Self::MultisizeImageSet { entries } => RenditionData::MultisizeImageSet {
139 entries: entries.into_owned(),
140 },
141 }
142 }
143
144 fn from_owned(data: RenditionData) -> Self {
145 match data {
146 RenditionData::Color {
147 color_space,
148 components,
149 } => Self::Color {
150 color_space: Cow::Owned(color_space),
151 components: Cow::Owned(components),
152 },
153 RenditionData::Image { data } => Self::Image {
154 data: Cow::Owned(data),
155 },
156 RenditionData::RawData { data } => Self::RawData {
157 data: Cow::Owned(data),
158 },
159 RenditionData::MultisizeImageSet { entries } => Self::MultisizeImageSet {
160 entries: Cow::Owned(entries),
161 },
162 }
163 }
164}
165
166fn check_borrowed_payload_bytes<T>(options: &DecodeOptions, payload: &[T]) -> CarResult<()> {
167 options.check_output_bytes(std::mem::size_of_val(payload), DecodeStage::BorrowedPayload)
168}
169
170impl crate::car::CSIItem {
171 pub fn decode_data_ref(&self) -> CarResult<Option<RenditionDataRef<'_>>> {
172 self.decode_data_ref_with_options(&DecodeOptions::default())
173 }
174
175 pub fn decode_data_ref_with_options(
176 &self,
177 options: &DecodeOptions,
178 ) -> CarResult<Option<RenditionDataRef<'_>>> {
179 let rendition = match &self.header.rendition {
180 None => return Ok(None),
181 Some(r) => r,
182 };
183
184 match rendition {
185 Rendition::Color(rc) => {
186 check_borrowed_payload_bytes(options, rc.components.as_slice())?;
187 Ok(Some(RenditionDataRef::Color {
188 color_space: Cow::Borrowed(&rc.color_space),
189 components: Cow::Borrowed(&rc.components),
190 }))
191 }
192 Rendition::RawData(rd) => {
193 check_borrowed_payload_bytes(options, rd.raw_data.as_slice())?;
194 Ok(Some(RenditionDataRef::RawData {
195 data: Cow::Borrowed(rd.raw_data.as_slice()),
196 }))
197 }
198 Rendition::MultisizeImageSet(mis) => {
199 check_borrowed_payload_bytes(options, mis.entries.as_slice())?;
200 Ok(Some(RenditionDataRef::MultisizeImageSet {
201 entries: Cow::Borrowed(mis.entries.as_slice()),
202 }))
203 }
204 Rendition::ThemeCBCK(cbck) => decode_theme_cbck(self, cbck, options)
205 .map(|data| data.map(RenditionDataRef::from_owned)),
206 Rendition::Unknown { tag, .. } => Err(CarError::DecodeFailed(format!(
207 "Unknown rendition type: {:?}",
208 tag
209 ))),
210 }
211 }
212
213 pub fn decode_data_owned(&self) -> CarResult<Option<RenditionData>> {
214 self.decode_data_ref_with_options(&DecodeOptions::default())
215 .map(|data| data.map(RenditionDataRef::into_owned))
216 }
217
218 pub fn decode_data_owned_with_options(
219 &self,
220 options: &DecodeOptions,
221 ) -> CarResult<Option<RenditionData>> {
222 self.decode_data_ref_with_options(options)
223 .map(|data| data.map(RenditionDataRef::into_owned))
224 }
225
226 pub fn decode_data(&self) -> CarResult<Option<RenditionData>> {
227 self.decode_data_owned()
228 }
229
230 pub fn decode_data_with_options(
231 &self,
232 options: &DecodeOptions,
233 ) -> CarResult<Option<RenditionData>> {
234 self.decode_data_owned_with_options(options)
235 }
236}
237
238fn deepmap2_pixel_format_override(encoding: Encoding) -> Option<deepmap2::PixelFormat> {
239 match encoding {
240 Encoding::ARGB => Some(deepmap2::PixelFormat::Rgba8888),
241 Encoding::ARGB16 => Some(deepmap2::PixelFormat::Rgba8888),
242 Encoding::GA8 => Some(deepmap2::PixelFormat::GA88),
243 _ => None,
244 }
245}
246
247fn encoding_bytes_per_pixel(encoding: Encoding) -> Option<usize> {
249 match encoding {
250 Encoding::ARGB => Some(4),
251 Encoding::GRAY => Some(1),
252 Encoding::GA8 => Some(2),
253 Encoding::ARGB16 => Some(8),
254 Encoding::GA16 => Some(4),
255 Encoding::RGB5 => Some(2),
256 _ => None,
257 }
258}
259
260fn chunk_output_height(
261 chunk: &crate::model::rendition::ThemeChunk,
262 total_chunks: usize,
263 full_height: u16,
264) -> CarResult<Option<u16>> {
265 match chunk.part_header.as_ref().map(|header| header.height) {
266 Some(height) if total_chunks == 1 && height == 0 => Ok(Some(full_height)),
267 None if total_chunks == 1 => Ok(Some(full_height)),
268 Some(0) | None => Ok(None),
269 Some(height) => u16::try_from(height).map(Some).map_err(|_| {
270 CarError::DecodeFailed(format!("Theme part height exceeds u16: {height}"))
271 }),
272 }
273}
274
275fn dmp2_region(chunk: &[u8], dmp2_pos: usize) -> &[u8] {
276 let search_start = dmp2_pos.saturating_sub(0x40);
277 if let Some(prev_kcbc_rel) = deepmap2::tile::find_bytes(&chunk[search_start..dmp2_pos], b"KCBC")
278 {
279 let kcbc_pos = search_start + prev_kcbc_rel;
280 if kcbc_pos + 20 <= chunk.len() {
281 let block_length =
282 u32::from_le_bytes(chunk[kcbc_pos + 0x10..kcbc_pos + 0x14].try_into().unwrap())
283 as usize;
284 let block_end = kcbc_pos.saturating_add(block_length);
285 if block_length >= 0x20 && block_end > dmp2_pos && block_end <= chunk.len() {
286 return &chunk[dmp2_pos..block_end];
287 }
288 }
289 }
290
291 if let Some(next_rel) = deepmap2::tile::find_bytes(&chunk[dmp2_pos + 4..], b"KCBC") {
292 let next_kcbc = dmp2_pos + 4 + next_rel;
293 return &chunk[dmp2_pos..next_kcbc];
294 }
295
296 &chunk[dmp2_pos..]
297}
298
299fn decode_theme_cbck(
300 item: &crate::car::CSIItem,
301 cbck: &RenditionThemeCBCK,
302 options: &DecodeOptions,
303) -> CarResult<Option<RenditionData>> {
304 match &cbck.compression_type {
305 CompressionType::Uncompressed => {
306 let total: usize = cbck.chunks.iter().map(|c| c.raw_data.len()).sum();
307 options.check_output_bytes(total, DecodeStage::Uncompressed)?;
308 let mut data = Vec::with_capacity(total);
309 for chunk in &cbck.chunks {
310 data.extend_from_slice(chunk.raw_data.as_slice());
311 }
312 Ok(Some(RenditionData::Image { data }))
313 }
314 CompressionType::Lzfse => {
315 let decode_chunk = |raw: &[u8]| -> CarResult<Vec<u8>> {
316 let cap = raw.len().saturating_mul(16).max(4096);
317 let mut output = Vec::with_capacity(cap);
318 let mut decoder = LzfseRingDecoder::default();
319 let n = decoder.decode_bytes(raw, &mut output).map_err(|_| {
320 CarError::DecodeFailed("LZFSE decompression failed".to_string())
321 })?;
322 if n == 0 && !raw.is_empty() {
323 return Err(CarError::DecodeFailed(
324 "LZFSE produced zero bytes for non-empty input".to_string(),
325 ));
326 }
327 output.truncate(n as usize);
328 Ok(output)
329 };
330
331 let mut all_data = Vec::new();
333 let mut total = 0usize;
334 for chunk in &cbck.chunks {
335 let part = decode_chunk(chunk.raw_data.as_slice())?;
336 total = total.checked_add(part.len()).ok_or_else(|| {
337 CarError::DecodeFailed("LZFSE output length overflow".to_string())
338 })?;
339 options.check_output_bytes(total, DecodeStage::Lzfse)?;
340 all_data.reserve(part.len());
341 all_data.extend_from_slice(&part);
342 }
343
344 Ok(Some(RenditionData::Image { data: all_data }))
345 }
346 CompressionType::Zip => {
347 let mut all_data = Vec::new();
348 let mut total = 0usize;
349 for chunk in &cbck.chunks {
350 let mut decoder = MultiGzDecoder::new(chunk.raw_data.as_slice());
351 let mut part = Vec::new();
352 decoder.read_to_end(&mut part).map_err(|err| {
353 CarError::DecodeFailed(format!("Zip/gzip decompression failed: {err}"))
354 })?;
355 total = total.checked_add(part.len()).ok_or_else(|| {
356 CarError::DecodeFailed("Zip/gzip output length overflow".to_string())
357 })?;
358 options.check_output_bytes(total, DecodeStage::Zip)?;
359 all_data.reserve(part.len());
360 all_data.extend_from_slice(&part);
361 }
362
363 Ok(Some(RenditionData::Image { data: all_data }))
364 }
365 CompressionType::Lzvn => {
366 let width = item.header.width as usize;
367 let height = item.header.height as usize;
368 let bpp = encoding_bytes_per_pixel(item.header.encoding).ok_or_else(|| {
372 CarError::DecodeFailed(format!(
373 "LZVN decompression unsupported for encoding {:?}",
374 item.header.encoding
375 ))
376 })?;
377 let min_row = width * bpp;
378 let bytes_per_row = match item.bytes_per_row_tlv() {
381 Some(bpr) => {
382 let bpr = bpr as usize;
383 if bpr < min_row {
384 return Err(CarError::DecodeFailed(format!(
385 "bytes_per_row ({}) smaller than minimum row size ({})",
386 bpr, min_row
387 )));
388 }
389 bpr
390 }
391 None => min_row,
392 };
393 let expected_total = bytes_per_row * height;
394 options.check_output_bytes(expected_total, DecodeStage::Lzvn)?;
395 let mut all_data = Vec::with_capacity(expected_total);
396 let is_single = cbck.chunks.len() == 1;
397 for chunk in &cbck.chunks {
398 let expected_chunk = if is_single {
400 expected_total
401 } else {
402 let chunk_height = chunk
403 .part_header
404 .as_ref()
405 .map(|h| h.height as usize)
406 .filter(|h| *h > 0)
407 .ok_or_else(|| {
408 CarError::DecodeFailed(
409 "LZVN multi-chunk missing part_header height".to_string(),
410 )
411 })?;
412 chunk_height * bytes_per_row
413 };
414 let decompressed =
415 deepmap2::codec::decompress_lzvn(chunk.raw_data.as_slice(), expected_chunk)?;
416 all_data.extend_from_slice(&decompressed);
417 }
418 if all_data.len() != expected_total {
419 return Err(CarError::DecodeFailed(format!(
420 "LZVN output size mismatch: expected {}, got {}",
421 expected_total,
422 all_data.len()
423 )));
424 }
425 Ok(Some(RenditionData::Image { data: all_data }))
426 }
427 CompressionType::Deepmap2 => {
428 let width = u16::try_from(item.header.width)
429 .map_err(|_| CarError::DecodeFailed("CSI width exceeds u16".to_string()))?;
430 let height = u16::try_from(item.header.height)
431 .map_err(|_| CarError::DecodeFailed("CSI height exceeds u16".to_string()))?;
432 let pixel_format_override = deepmap2_pixel_format_override(item.header.encoding);
433 let mut all_data = Vec::new();
434 options.check_output_bytes(
435 (width as usize)
436 .saturating_mul(height as usize)
437 .saturating_mul(4),
438 DecodeStage::Deepmap2,
439 )?;
440 let mut expected_width: Option<u16> = None;
441 let mut decoded_height = 0u32;
442 for chunk in &cbck.chunks {
443 let output_height = chunk_output_height(chunk, cbck.chunks.len(), height)?;
444 let chunk = chunk.raw_data.as_slice();
445 let kcbc_pos = deepmap2::tile::find_bytes(chunk, b"KCBC");
446 let dmp2_pos = deepmap2::tile::find_bytes(chunk, b"dmp2");
447 let img = match (kcbc_pos, dmp2_pos) {
448 (Some(k), Some(d)) if k <= d => deepmap2::decode_kcbc_with_options(
449 &chunk[k..],
450 Some(width),
451 None,
452 pixel_format_override,
453 )?,
454 (Some(_), Some(d)) => deepmap2::decode_with_options(
455 dmp2_region(chunk, d),
456 Some(width),
457 output_height,
458 pixel_format_override,
459 )?,
460 (Some(k), None) => deepmap2::decode_kcbc_with_options(
461 &chunk[k..],
462 Some(width),
463 None,
464 pixel_format_override,
465 )?,
466 (None, Some(d)) => deepmap2::decode_with_options(
467 dmp2_region(chunk, d),
468 Some(width),
469 output_height,
470 pixel_format_override,
471 )?,
472 (None, None) => {
473 return Err(CarError::DecodeFailed(
474 "No deepmap2 magic found in chunk".to_string(),
475 ));
476 }
477 };
478 match expected_width {
479 None => expected_width = Some(img.width),
480 Some(w) if w != img.width => {
481 return Err(CarError::DecodeFailed(format!(
482 "Deepmap2 chunk width mismatch: expected {}, got {}",
483 w, img.width
484 )));
485 }
486 _ => {}
487 }
488 decoded_height = decoded_height.saturating_add(u32::from(img.height));
489 all_data.extend_from_slice(&img.rgba);
490 options.check_output_bytes(all_data.len(), DecodeStage::Deepmap2)?;
491 }
492 if cbck.chunks.len() > 1 && decoded_height != u32::from(height) {
493 return Err(CarError::DecodeFailed(format!(
494 "Deepmap2 chunk height mismatch: expected {}, got {}",
495 height, decoded_height
496 )));
497 }
498 Ok(Some(RenditionData::Image { data: all_data }))
499 }
500 _ => Err(CarError::UnsupportedCompression(cbck.compression_type)),
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use std::borrow::Cow;
507 use std::io::{Cursor, Write};
508
509 use deku::{DekuReader, reader::Reader};
510 use flate2::{Compression, write::GzEncoder};
511 use std::path::PathBuf;
512
513 use super::{
514 DecodeBudgetError, DecodeOptions, DecodeStage, RenditionData, RenditionDataRef,
515 deepmap2_pixel_format_override, dmp2_region,
516 };
517 use crate::car::{CSIItem, Car, CarError};
518 use crate::model::CSIHeader;
519 use crate::model::rendition::{AttributeType, CompressionType, LayoutType};
520
521 const COMP_UNCOMPRESSED: u32 = 0;
522 const COMP_ZIP: u32 = 2;
523 const COMP_LZVN: u32 = 3;
524 const COMP_LZFSE: u32 = 4;
525 const COMP_HEVC: u32 = 9;
526 const COMP_DEEPMAP2: u32 = 11;
527
528 fn fixture_path(name: &str) -> PathBuf {
529 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
530 .join("../car-tests/data")
531 .join(name)
532 }
533
534 fn fixture_car() -> Car {
535 Car::new(fixture_path("Assets.car")).expect("load test Assets.car")
536 }
537
538 #[test]
539 fn decode_data_returns_none_when_rendition_is_missing() {
540 let item = build_item(2, 2, None);
541
542 assert!(matches!(item.decode_data(), Ok(None)));
543 }
544
545 #[test]
546 fn decode_data_rejects_unknown_rendition_variant() {
547 let item = build_item(2, 2, Some(&build_unknown_rendition(*b"FAIL", b"raw")));
548
549 let err = match item.decode_data() {
550 Ok(_) => panic!("unknown rendition should fail"),
551 Err(err) => err,
552 };
553 assert!(matches!(err, CarError::DecodeFailed(_)));
554 assert!(format!("{err}").contains("Unknown rendition type"));
555 }
556
557 #[test]
558 fn decode_data_rejects_invalid_lzfse_payload() {
559 let item = build_item(
560 2,
561 2,
562 Some(&build_theme_cbck_single(COMP_LZFSE, b"not-valid-lzfse")),
563 );
564
565 let err = match item.decode_data() {
566 Ok(_) => panic!("invalid LZFSE should fail"),
567 Err(err) => err,
568 };
569 assert!(matches!(err, CarError::DecodeFailed(_)));
570 assert!(format!("{err}").contains("LZFSE"));
571 }
572
573 #[test]
574 fn decode_data_rejects_unsupported_compression() {
575 let item = build_item(
576 2,
577 2,
578 Some(&build_theme_cbck_single(COMP_HEVC, b"hevc-bytes")),
579 );
580
581 let err = match item.decode_data() {
582 Ok(_) => panic!("unsupported compression should fail"),
583 Err(err) => err,
584 };
585 assert!(matches!(
586 err,
587 CarError::UnsupportedCompression(CompressionType::HEVC)
588 ));
589 }
590
591 #[test]
592 fn decode_data_decodes_zip_gzip_chunks() {
593 let first = gzip_bytes(&[1, 2, 3, 4]);
594 let second = gzip_bytes(&[5, 6, 7, 8]);
595 let item = build_item(
596 2,
597 1,
598 Some(&build_theme_cbck_multi(COMP_ZIP, &[&first, &second])),
599 );
600
601 let decoded = item.decode_data().expect("gzip Zip decode should succeed");
602 let Some(RenditionData::Image { data }) = decoded else {
603 panic!("expected RenditionData::Image");
604 };
605 assert_eq!(data, vec![1, 2, 3, 4, 5, 6, 7, 8]);
606 }
607
608 #[test]
609 fn decode_data_rejects_invalid_zip_gzip_payload() {
610 let item = build_item(
611 2,
612 1,
613 Some(&build_theme_cbck_single(COMP_ZIP, b"not-valid-gzip")),
614 );
615
616 let err = item
617 .decode_data()
618 .expect_err("invalid gzip Zip payload should fail");
619 assert!(
620 matches!(err, CarError::DecodeFailed(_)),
621 "expected DecodeFailed, got {err:?}"
622 );
623 assert!(format!("{err}").contains("Zip/gzip decompression failed"));
624 }
625
626 #[test]
627 fn decode_data_with_options_rejects_uncompressed_output_over_budget() {
628 let item = build_item(
629 2,
630 2,
631 Some(&build_theme_cbck_single(COMP_UNCOMPRESSED, &[1, 2, 3, 4])),
632 );
633 let options = DecodeOptions::default().with_max_output_bytes(3);
634
635 let err = item
636 .decode_data_with_options(&options)
637 .expect_err("decoded bytes should exceed budget");
638 assert!(matches!(err, CarError::DecodeBudgetExceeded(_)));
639 }
640
641 #[test]
642 fn decode_data_ref_with_options_rejects_raw_data_payload_over_budget() {
643 let item = build_item(1, 1, Some(&build_raw_data_rendition(&[1, 2, 3, 4])));
644 let options = DecodeOptions::default().with_max_output_bytes(3);
645
646 let err = item
647 .decode_data_ref_with_options(&options)
648 .expect_err("borrowed raw data should exceed budget");
649 assert_borrowed_payload_budget(err);
650 }
651
652 #[test]
653 fn raw_data_payload_bytes_view_and_borrowed_decode() {
654 let payload = [1, 2, 3, 4];
655 let item = build_item(1, 1, Some(&build_raw_data_rendition(&payload)));
656 let Some(crate::model::rendition::Rendition::RawData(raw)) = &item.header.rendition else {
657 panic!("expected raw data rendition");
658 };
659
660 assert_eq!(raw.raw_data.as_slice(), payload);
661 assert_eq!(raw.raw_data.to_vec(), payload);
662
663 let decoded = item
664 .decode_data_ref()
665 .expect("raw data should decode")
666 .expect("raw data should be present");
667 let RenditionDataRef::RawData {
668 data: Cow::Borrowed(bytes),
669 } = decoded
670 else {
671 panic!("expected borrowed raw data");
672 };
673 assert_eq!(bytes, payload);
674 }
675
676 #[test]
677 fn decode_data_with_options_rejects_color_payload_over_budget() {
678 let item = build_item(1, 1, Some(&build_color_rendition(&[1.0])));
679 let options = DecodeOptions::default().with_max_output_bytes(7);
680
681 let err = item
682 .decode_data_with_options(&options)
683 .expect_err("borrowed color data should exceed budget");
684 assert_borrowed_payload_budget(err);
685 }
686
687 #[test]
688 fn decode_data_with_options_rejects_multisize_payload_over_budget() {
689 let item = build_item(1, 1, Some(&build_multisize_rendition(&[(16, 16, 0, 0)])));
690 let options = DecodeOptions::default().with_max_output_bytes(0);
691
692 let err = item
693 .decode_data_with_options(&options)
694 .expect_err("borrowed multisize data should exceed budget");
695 assert_borrowed_payload_budget(err);
696 }
697
698 #[test]
699 fn decode_data_scans_for_offset_dmp2_magic() {
700 let dmp2 = build_dmp2_none_g8(2, 1, &[1, 2]);
701 let prefixed = build_prefixed_chunk(b"prefix", &dmp2);
702 let item = build_item(
703 2,
704 1,
705 Some(&build_theme_cbck_single(COMP_DEEPMAP2, &prefixed)),
706 );
707
708 let decoded = item.decode_data().expect("decode should succeed");
709 let Some(RenditionData::Image { data }) = decoded else {
710 panic!("expected image data");
711 };
712 assert_eq!(data, rgba_from_grays(&[1, 2]));
713 }
714
715 #[test]
716 fn decode_data_scans_for_offset_kcbc_magic() {
717 let dmp2 = build_dmp2_none_g8(2, 1, &[7, 8]);
718 let kcbc = build_kcbc(&[(0, 0, 1, dmp2.as_slice())]);
719 let prefixed = build_prefixed_chunk(b"wrapper", &kcbc);
720 let item = build_item(
721 2,
722 1,
723 Some(&build_theme_cbck_single(COMP_DEEPMAP2, &prefixed)),
724 );
725
726 let decoded = item.decode_data().expect("decode should succeed");
727 let Some(RenditionData::Image { data }) = decoded else {
728 panic!("expected image data");
729 };
730 assert_eq!(data, rgba_from_grays(&[7, 8]));
731 }
732
733 #[test]
734 fn decode_data_prefers_earlier_dmp2_over_later_kcbc() {
735 let dmp2 = build_dmp2_none_g8(2, 1, &[5, 6]);
737 let mut chunk = Vec::new();
738 chunk.extend_from_slice(b"junk");
739 chunk.extend_from_slice(&dmp2);
740 chunk.extend_from_slice(b"KCBC");
742 chunk.extend_from_slice(&[0u8; 16]);
743 let item = build_item(2, 1, Some(&build_theme_cbck_single(COMP_DEEPMAP2, &chunk)));
744
745 let decoded = item.decode_data().expect("decode should succeed");
746 let Some(RenditionData::Image { data }) = decoded else {
747 panic!("expected image data");
748 };
749 assert_eq!(data, rgba_from_grays(&[5, 6]));
750 }
751
752 #[test]
753 fn decode_data_prefers_earlier_kcbc_over_later_dmp2() {
754 let inner_dmp2 = build_dmp2_none_g8(2, 1, &[9, 10]);
756 let kcbc = build_kcbc(&[(0, 0, 1, inner_dmp2.as_slice())]);
757 let mut chunk = Vec::new();
758 chunk.extend_from_slice(b"hdr");
759 chunk.extend_from_slice(&kcbc);
760 let trailing_dmp2 = build_dmp2_none_g8(2, 1, &[99, 99]);
762 chunk.extend_from_slice(&trailing_dmp2);
763 let item = build_item(2, 1, Some(&build_theme_cbck_single(COMP_DEEPMAP2, &chunk)));
764
765 let decoded = item.decode_data().expect("decode should succeed");
766 let Some(RenditionData::Image { data }) = decoded else {
767 panic!("expected image data");
768 };
769 assert_eq!(data, rgba_from_grays(&[9, 10]));
770 }
771
772 #[test]
773 fn decode_data_reassembles_multi_chunk_deepmap2_strips() {
774 let first = build_dmp2_none_g8(2, 1, &[1, 2]);
775 let second = build_dmp2_none_g8(2, 1, &[3, 4]);
776 let item = build_item(
777 2,
778 2,
779 Some(&build_theme_cbck_multi(
780 COMP_DEEPMAP2,
781 &[first.as_slice(), second.as_slice()],
782 )),
783 );
784
785 let decoded = item.decode_data().expect("decode should succeed");
786 let Some(RenditionData::Image { data }) = decoded else {
787 panic!("expected image data");
788 };
789 assert_eq!(data, rgba_from_grays(&[1, 2, 3, 4]));
790 }
791
792 #[test]
793 fn decode_data_rejects_deepmap2_chunk_without_magic() {
794 let item = build_item(
795 2,
796 1,
797 Some(&build_theme_cbck_single(COMP_DEEPMAP2, b"missing-magic")),
798 );
799
800 let err = match item.decode_data() {
801 Ok(_) => panic!("missing magic should fail"),
802 Err(err) => err,
803 };
804 assert!(matches!(err, CarError::DecodeFailed(_)));
805 assert!(format!("{err}").contains("magic"));
806 }
807
808 #[test]
809 fn decode_data_rejects_invalid_multi_chunk_geometry() {
810 let first = build_dmp2_none_g8(2, 1, &[1, 2]);
811 let second = build_dmp2_none_g8(1, 1, &[3]);
812 let item = build_item(
813 2,
814 2,
815 Some(&build_theme_cbck_multi(
816 COMP_DEEPMAP2,
817 &[first.as_slice(), second.as_slice()],
818 )),
819 );
820
821 let err = match item.decode_data() {
822 Ok(_) => panic!("mismatched widths should fail"),
823 Err(err) => err,
824 };
825 assert!(matches!(
826 err,
827 CarError::Deepmap2(_) | CarError::DecodeFailed(_)
828 ));
829 }
830
831 #[test]
832 fn decode_data_lzvn_honors_bytes_per_row_stride() {
833 let raw_pixels: [u8; 12] = [
841 0x11, 0x22, 0x00, 0x00, 0x33, 0x44, 0x00, 0x00, 0x55, 0x66, 0x00, 0x00, ];
845 let compressed = lzvn::encode_raw(&raw_pixels);
846 let rendition = build_theme_cbck_single(COMP_LZVN, &compressed);
847 let tlv = bytes_per_row_tlv(4);
848 let item = build_item_ex(2, 3, b"YARG", &tlv, Some(&rendition));
849
850 let decoded = item
851 .decode_data()
852 .expect("LZVN decode with stride should succeed");
853 let Some(RenditionData::Image { data }) = decoded else {
854 panic!("expected RenditionData::Image");
855 };
856 assert_eq!(data, raw_pixels);
859 }
860
861 #[test]
862 fn decode_data_lzvn_rejects_bytes_per_row_below_minimum() {
863 let raw_pixels: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
865 let compressed = lzvn::encode_raw(&raw_pixels);
866 let rendition = build_theme_cbck_single(COMP_LZVN, &compressed);
867 let tlv = bytes_per_row_tlv(1); let item = build_item_ex(2, 2, b"YARG", &tlv, Some(&rendition));
869
870 let err = match item.decode_data() {
871 Ok(_) => panic!("expected bytes_per_row underflow to fail"),
872 Err(err) => err,
873 };
874 assert!(matches!(err, CarError::DecodeFailed(_)));
875 assert!(format!("{err}").contains("bytes_per_row"));
876 }
877
878 #[test]
879 fn decode_data_decodes_real_assets_car_deepmap2_problem_cases() {
880 let car = fixture_car();
881 for name in [
882 "2016_coin1",
883 "AirKissHelper",
884 "CarPlayCallRecord",
885 "CardPkgGiftCardBackGroundImage",
886 "ChatMessagesPreviewSelectZoomBorder",
887 "DeleteData_Loading",
888 ] {
889 let item = car
890 .rendtions_with_name(name)
891 .and_then(|items| {
892 items
893 .iter()
894 .find(|item| !matches!(item.layout(), LayoutType::InternalReference))
895 })
896 .unwrap_or_else(|| panic!("missing fixture rendition: {name}"));
897 let decoded = item
898 .decode_data()
899 .unwrap_or_else(|err| panic!("decode should succeed for {name}: {err}"));
900 assert!(
901 matches!(decoded, Some(RenditionData::Image { .. })),
902 "{name} should decode to image data"
903 );
904 }
905
906 let item = car
907 .rendtions_with_name("live_playmembers_background")
908 .and_then(|items| {
909 items
910 .iter()
911 .find(|item| !matches!(item.layout(), LayoutType::InternalReference))
912 })
913 .expect("missing fixture rendition: live_playmembers_background");
914 let decoded = item.decode_data().unwrap_or_else(|err| {
915 panic!("decode should succeed for live_playmembers_background: {err}")
916 });
917 let Some(RenditionData::Image { data }) = decoded else {
918 panic!("live_playmembers_background should decode to image data");
919 };
920 assert_eq!(data.len(), 1170 * 828 * 4);
921 }
922
923 #[test]
924 fn decode_data_decodes_bootimage_ipad_palette_lzvn_tail_chunk() {
925 let car = fixture_car();
926 let item = car
927 .rendtions_with_name("BootImage")
928 .and_then(|items| {
929 items
930 .iter()
931 .find(|item| item.name() == "Default@2x~ipad.png")
932 })
933 .expect("missing BootImage Default@2x~ipad.png rendition");
934
935 let decoded = item
936 .decode_data()
937 .unwrap_or_else(|err| panic!("decode should succeed for BootImage iPad: {err}"));
938 let Some(RenditionData::Image { data }) = decoded else {
939 panic!("BootImage iPad should decode to image data");
940 };
941 assert_eq!(data.len(), 1536 * 2048 * 4);
942 }
943
944 #[test]
945 fn decode_data_decodes_real_assets_car_zip_app_icons() {
946 let car = fixture_car();
947 let items = car
948 .rendtions_with_name("AppIcon")
949 .expect("AppIcon fixture facet");
950
951 for (name, expected_len) in [
952 ("AppIcon_1024.png", 1024 * 1024 * 4),
953 ("AppIcon_1024_dark.png", 1024 * 1024 * 4),
954 ("AppIcon_1024_tinted.png", 1024 * 1024 * 2),
955 ] {
956 let variants = items
957 .iter()
958 .filter(|item| item.name() == name)
959 .collect::<Vec<_>>();
960 assert_eq!(
961 variants.len(),
962 2,
963 "{name} should have phone and pad variants"
964 );
965
966 let mut decoded_by_idiom = Vec::new();
967 for item in variants {
968 assert_eq!(item.compression(), Some(CompressionType::Zip));
969 assert_eq!(item.unsupported_reason(), None);
970 let idiom = item
971 .attributes()
972 .iter()
973 .find(|attr| attr.name == AttributeType::Idiom)
974 .map(|attr| attr.val)
975 .expect("AppIcon Zip variant should carry idiom");
976 let decoded = item
977 .decode_data()
978 .unwrap_or_else(|err| panic!("Zip decode should succeed for {name}: {err}"));
979 let Some(RenditionData::Image { data }) = decoded else {
980 panic!("{name} should decode to image data");
981 };
982 assert_eq!(data.len(), expected_len, "{name} decoded length");
983 decoded_by_idiom.push((idiom, item.key_values().to_vec(), data));
984 }
985
986 decoded_by_idiom.sort_by_key(|(idiom, _, _)| *idiom);
987 assert_eq!(
988 decoded_by_idiom[0].0, 1,
989 "{name} first idiom should be phone"
990 );
991 assert_eq!(
992 decoded_by_idiom[1].0, 2,
993 "{name} second idiom should be pad"
994 );
995 assert_ne!(
996 decoded_by_idiom[0].1, decoded_by_idiom[1].1,
997 "{name} phone and pad keys should differ"
998 );
999 assert_eq!(
1000 decoded_by_idiom[0].2, decoded_by_idiom[1].2,
1001 "{name} phone and pad payloads should match"
1002 );
1003 }
1004 }
1005
1006 #[test]
1007 fn deepmap2_pixel_format_override_matches_csi_mapping() {
1008 assert_eq!(
1009 deepmap2_pixel_format_override(crate::model::Encoding::ARGB),
1010 Some(deepmap2::PixelFormat::Rgba8888)
1011 );
1012 assert_eq!(
1013 deepmap2_pixel_format_override(crate::model::Encoding::ARGB16),
1014 Some(deepmap2::PixelFormat::Rgba8888)
1015 );
1016 assert_eq!(
1017 deepmap2_pixel_format_override(crate::model::Encoding::GA8),
1018 Some(deepmap2::PixelFormat::GA88)
1019 );
1020 }
1021
1022 #[test]
1023 fn dmp2_region_stops_before_next_kcbc() {
1024 let mut chunk = Vec::new();
1025 chunk.extend_from_slice(b"prefix");
1026 let dmp2_pos = chunk.len();
1027 chunk.extend_from_slice(b"dmp2");
1028 chunk.extend_from_slice(&[1, 2, 3, 4]);
1029 chunk.extend_from_slice(b"KCBC");
1030 chunk.extend_from_slice(&[9, 9, 9, 9]);
1031
1032 assert_eq!(dmp2_region(&chunk, dmp2_pos), b"dmp2\x01\x02\x03\x04");
1033 }
1034
1035 fn build_item(width: u32, height: u32, rendition: Option<&[u8]>) -> CSIItem {
1036 build_item_ex(width, height, b"ATAD", &[], rendition)
1037 }
1038
1039 fn build_item_ex(
1040 width: u32,
1041 height: u32,
1042 encoding: &[u8; 4],
1043 tlv: &[u8],
1044 rendition: Option<&[u8]>,
1045 ) -> CSIItem {
1046 let bytes = build_csi_header(width, height, encoding, tlv, rendition);
1047 let mut reader = Reader::new(Cursor::new(bytes));
1048 let header =
1049 CSIHeader::from_reader_with_ctx(&mut reader, ()).expect("test header should decode");
1050 CSIItem {
1051 attrs: Vec::new(),
1052 header,
1053 key_values: Box::default(),
1054 }
1055 }
1056
1057 fn build_csi_header(
1058 width: u32,
1059 height: u32,
1060 encoding: &[u8; 4],
1061 tlv: &[u8],
1062 rendition: Option<&[u8]>,
1063 ) -> Vec<u8> {
1064 let mut bytes = Vec::new();
1065 bytes.extend_from_slice(b"ISTC");
1066 bytes.extend_from_slice(&1u32.to_le_bytes());
1067 bytes.extend_from_slice(&0u32.to_le_bytes());
1068 bytes.extend_from_slice(&width.to_le_bytes());
1069 bytes.extend_from_slice(&height.to_le_bytes());
1070 bytes.extend_from_slice(&100u32.to_le_bytes());
1071 bytes.extend_from_slice(encoding);
1072 bytes.extend_from_slice(&0u32.to_le_bytes());
1073 bytes.extend_from_slice(&0u32.to_le_bytes());
1074 bytes.extend_from_slice(&1000u32.to_le_bytes());
1075 bytes.extend_from_slice(&[0u8; 128]);
1076 bytes.extend_from_slice(&(tlv.len() as u32).to_le_bytes());
1077 bytes.extend_from_slice(&1u32.to_le_bytes());
1078 bytes.extend_from_slice(&0u32.to_le_bytes());
1079 bytes.extend_from_slice(&(rendition.map_or(0, |data| data.len()) as u32).to_le_bytes());
1080 bytes.extend_from_slice(tlv);
1081 if let Some(rendition) = rendition {
1082 bytes.extend_from_slice(rendition);
1083 }
1084 bytes
1085 }
1086
1087 fn bytes_per_row_tlv(bpr: u32) -> Vec<u8> {
1088 let mut bytes = Vec::new();
1089 bytes.extend_from_slice(&1007u32.to_le_bytes()); bytes.extend_from_slice(&4u32.to_le_bytes()); bytes.extend_from_slice(&bpr.to_le_bytes());
1092 bytes
1093 }
1094
1095 fn assert_borrowed_payload_budget(err: CarError) {
1096 match err {
1097 CarError::DecodeBudgetExceeded(DecodeBudgetError::OutputBytesExceeded {
1098 stage: DecodeStage::BorrowedPayload,
1099 ..
1100 }) => {}
1101 err => panic!("expected borrowed payload budget error, got {err:?}"),
1102 }
1103 }
1104
1105 fn build_color_rendition(components: &[f64]) -> Vec<u8> {
1106 let mut bytes = Vec::new();
1107 bytes.extend_from_slice(b"RLOC");
1108 bytes.extend_from_slice(&1u32.to_le_bytes());
1109 bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&(components.len() as u32).to_le_bytes());
1111 for component in components {
1112 bytes.extend_from_slice(&component.to_le_bytes());
1113 }
1114 bytes
1115 }
1116
1117 fn build_raw_data_rendition(raw_data: &[u8]) -> Vec<u8> {
1118 let mut bytes = Vec::new();
1119 bytes.extend_from_slice(b"DWAR");
1120 bytes.extend_from_slice(&1u32.to_le_bytes());
1121 bytes.extend_from_slice(&(raw_data.len() as u32).to_le_bytes());
1122 bytes.extend_from_slice(raw_data);
1123 bytes
1124 }
1125
1126 fn build_multisize_rendition(entries: &[(u32, u32, u16, u16)]) -> Vec<u8> {
1127 let mut bytes = Vec::new();
1128 bytes.extend_from_slice(b"SISM");
1129 bytes.extend_from_slice(&1u32.to_le_bytes());
1130 bytes.extend_from_slice(&(entries.len() as u32).to_le_bytes());
1131 for &(width, height, index, idiom) in entries {
1132 bytes.extend_from_slice(&width.to_le_bytes());
1133 bytes.extend_from_slice(&height.to_le_bytes());
1134 bytes.extend_from_slice(&index.to_le_bytes());
1135 bytes.extend_from_slice(&idiom.to_le_bytes());
1136 }
1137 bytes
1138 }
1139
1140 fn build_unknown_rendition(tag: [u8; 4], raw_data: &[u8]) -> Vec<u8> {
1141 let mut bytes = Vec::new();
1142 bytes.extend_from_slice(&tag);
1143 bytes.extend_from_slice(&1u32.to_le_bytes());
1144 bytes.extend_from_slice(&(raw_data.len() as u32).to_le_bytes());
1145 bytes.extend_from_slice(raw_data);
1146 bytes
1147 }
1148
1149 fn build_theme_cbck_single(compression: u32, chunk: &[u8]) -> Vec<u8> {
1150 let mut bytes = Vec::new();
1151 bytes.extend_from_slice(b"MLEC");
1152 bytes.extend_from_slice(&0u32.to_le_bytes());
1153 bytes.extend_from_slice(&compression.to_le_bytes());
1154 bytes.extend_from_slice(&(chunk.len() as u32).to_le_bytes());
1155 bytes.extend_from_slice(chunk);
1156 bytes
1157 }
1158
1159 fn build_theme_cbck_multi(compression: u32, chunks: &[&[u8]]) -> Vec<u8> {
1160 let mut bytes = Vec::new();
1161 bytes.extend_from_slice(b"MLEC");
1162 bytes.extend_from_slice(&1u32.to_le_bytes());
1163 bytes.extend_from_slice(&compression.to_le_bytes());
1164 bytes.extend_from_slice(&(chunks.len() as u32).to_le_bytes());
1165 for chunk in chunks {
1166 bytes.extend_from_slice(b"PART");
1167 bytes.extend_from_slice(&0u32.to_le_bytes());
1168 bytes.extend_from_slice(&0u32.to_le_bytes());
1169 bytes.extend_from_slice(&0u32.to_le_bytes());
1170 bytes.extend_from_slice(&(chunk.len() as u32).to_le_bytes());
1171 bytes.extend_from_slice(chunk);
1172 }
1173 bytes
1174 }
1175
1176 fn gzip_bytes(raw: &[u8]) -> Vec<u8> {
1177 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1178 encoder.write_all(raw).expect("write gzip test payload");
1179 encoder.finish().expect("finish gzip test payload")
1180 }
1181
1182 fn build_prefixed_chunk(prefix: &[u8], payload: &[u8]) -> Vec<u8> {
1183 let mut bytes = Vec::new();
1184 bytes.extend_from_slice(prefix);
1185 bytes.extend_from_slice(payload);
1186 bytes
1187 }
1188
1189 fn build_dmp2_none_g8(width: u16, height: u16, pixels: &[u8]) -> Vec<u8> {
1190 assert_eq!(pixels.len(), width as usize * height as usize);
1191 let mut data = Vec::new();
1192 data.extend_from_slice(b"dmp2");
1193 data.push(0x01);
1194 data.push(0x00);
1195 data.push(0x00);
1196 data.push(0x01);
1197 data.extend_from_slice(&width.to_le_bytes());
1198 data.extend_from_slice(&height.to_le_bytes());
1199 data.extend_from_slice(pixels);
1200 data
1201 }
1202
1203 fn build_kcbc(tiles: &[(u32, u32, u32, &[u8])]) -> Vec<u8> {
1204 let mut data = Vec::new();
1205 for &(row, col, row_hint, dmp2) in tiles {
1206 let block_len = (20 + dmp2.len()) as u32;
1207 data.extend_from_slice(b"KCBC");
1208 data.extend_from_slice(&col.to_le_bytes());
1209 data.extend_from_slice(&row.to_le_bytes());
1210 data.extend_from_slice(&row_hint.to_le_bytes());
1211 data.extend_from_slice(&block_len.to_le_bytes());
1212 data.extend_from_slice(dmp2);
1213 }
1214 data
1215 }
1216
1217 fn rgba_from_grays(values: &[u8]) -> Vec<u8> {
1218 let mut data = Vec::with_capacity(values.len() * 4);
1219 for value in values {
1220 data.extend_from_slice(&[*value, *value, *value, 0xFF]);
1221 }
1222 data
1223 }
1224
1225 fn lzfse_compress(data: &[u8]) -> Vec<u8> {
1226 let mut encoder = lzfse_rust::LzfseRingEncoder::default();
1227 let mut out = Vec::new();
1228 encoder.encode_bytes(data, &mut out).expect("lzfse encode");
1229 out
1230 }
1231
1232 #[test]
1233 fn decode_data_lzfse_multi_chunk_preserves_order() {
1234 let chunk0 = lzfse_compress(&[1u8, 2, 3]);
1236 let chunk1 = lzfse_compress(&[4u8, 5, 6]);
1237 let item = build_item(
1238 3,
1239 2,
1240 Some(&build_theme_cbck_multi(
1241 COMP_LZFSE,
1242 &[chunk0.as_slice(), chunk1.as_slice()],
1243 )),
1244 );
1245 let decoded = item
1246 .decode_data()
1247 .expect("multi-chunk LZFSE should succeed");
1248 let Some(RenditionData::Image { data }) = decoded else {
1249 panic!("expected Image data");
1250 };
1251 assert_eq!(data, [1u8, 2, 3, 4, 5, 6]);
1252 }
1253
1254 #[test]
1255 fn decode_data_lzfse_multi_chunk_fails_on_bad_chunk() {
1256 let good = lzfse_compress(&[1u8, 2, 3]);
1258 let bad = b"not-valid-lzfse-data".as_slice();
1259 let item = build_item(
1260 3,
1261 2,
1262 Some(&build_theme_cbck_multi(COMP_LZFSE, &[good.as_slice(), bad])),
1263 );
1264 let err = item.decode_data().expect_err("bad LZFSE chunk should fail");
1265 assert!(matches!(err, CarError::DecodeFailed(_)));
1266 assert!(format!("{err}").contains("LZFSE"));
1267 }
1268}