1use std::{collections::HashMap, collections::HashSet, io::Read, path::Path};
2
3use deku::prelude::*;
4use deku::reader::Reader;
5use thiserror::Error;
6
7use crate::model::*;
8use bom::{
9 BOM, BOMEror,
10 raw::{BOMBytes, BOMStr},
11};
12
13pub type CarResult<T> = Result<T, CarError>;
14
15type RenditionKey = Box<[u16]>;
16
17mod tree_key {
18 pub(crate) const APPEARANCE_KEYS: &[u8] = "APPEARANCEKEYS".as_bytes();
19 pub(crate) const EXTENDED_METADATA: &[u8] = "EXTENDED_METADATA".as_bytes();
20 pub(crate) const CAR_HEADER: &[u8] = "CARHEADER".as_bytes();
21 pub(crate) const KEY_FORMAT: &[u8] = "KEYFORMAT".as_bytes();
22 pub(crate) const FACET_KEYS: &[u8] = "FACETKEYS".as_bytes();
23 pub(crate) const RENDITIONS: &[u8] = "RENDITIONS".as_bytes();
24}
25
26#[derive(Debug)]
27pub struct CSIItem {
28 pub(crate) attrs: Vec<rendition::Attribute>,
29 pub(crate) header: CSIHeader,
30 pub(crate) key_values: RenditionKey,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ReferenceRect {
36 pub x: u32,
37 pub y: u32,
38 pub width: u32,
39 pub height: u32,
40}
41
42#[derive(Debug)]
44pub struct ResolvedInternalReference<'a> {
45 pub source: &'a CSIItem,
47 pub effective_layout: u16,
49 pub crops: Vec<ReferenceRect>,
51}
52
53pub struct Car {
54 pub(crate) header: Header,
55 pub(crate) appearance_keys: HashMap<String, BOMBytes>,
56 pub(crate) extended_metadata: ExtendedMetadata,
57 pub(crate) key_fmt: rendition::KeyFmt,
58 facet_keys: HashMap<String, rendition::KeyToken>,
59 items_by_identifier: HashMap<u16, Vec<CSIItem>>,
60 exact_item_by_key: HashMap<RenditionKey, (u16, usize)>,
61 attr_index_map: HashMap<u16, usize>,
62}
63
64impl Car {
65 pub fn new<P: AsRef<Path>>(file: P) -> CarResult<Self> {
66 Self::from_bom(BOM::new_with_file(file)?)
67 }
68
69 pub fn from_reader<R>(mut reader: R) -> CarResult<Self>
70 where
71 R: Read,
72 {
73 let mut bytes = Vec::new();
74 reader.read_to_end(&mut bytes)?;
75 Self::from_bytes(bytes)
76 }
77
78 pub fn from_bytes(bytes: Vec<u8>) -> CarResult<Self> {
79 Self::from_bom(BOM::from_bytes(bytes)?)
80 }
81
82 fn from_bom(mut bom: BOM) -> CarResult<Self> {
83 let key_fmt: rendition::KeyFmt = bom.read_block_with_name(tree_key::KEY_FORMAT)?;
84 let attr_id_index = key_fmt
85 .attribute_types
86 .iter()
87 .position(|attr_type| attr_type == &rendition::AttributeType::Identifier)
88 .ok_or(CarError::KeyFormatNotFoundIDAttr)?;
89
90 let appearance_keys: HashMap<_, _> =
92 match bom.read_tree_to_map::<BOMStr, BOMBytes>(tree_key::APPEARANCE_KEYS) {
93 Ok(keys) => keys,
94 Err(BOMEror::NotFoundVar(name)) if name.as_bytes() == tree_key::APPEARANCE_KEYS => {
95 HashMap::new()
96 }
97 Err(error) => return Err(error.into()),
98 }
99 .into_iter()
100 .map(|item| (item.0.content, item.1))
101 .collect();
102 let extended_metadata: ExtendedMetadata =
103 bom.read_block_with_name(tree_key::EXTENDED_METADATA)?;
104 let header: Header = bom.read_block_with_name(tree_key::CAR_HEADER)?;
105
106 let facet_keys: HashMap<_, _> = bom
107 .read_tree_to_map::<BOMStr, rendition::KeyToken>(tree_key::FACET_KEYS)?
108 .into_iter()
109 .map(|item| (item.0.content, item.1))
110 .collect();
111
112 let mut items_by_identifier: HashMap<u16, Vec<CSIItem>> = HashMap::new();
113 let mut exact_item_by_key: HashMap<RenditionKey, (u16, usize)> = HashMap::new();
114 bom.parse_tree(tree_key::RENDITIONS, |k, v| {
115 let mut k_reader = Reader::new(k);
116 let mut attrs: Vec<rendition::Attribute> =
117 Vec::with_capacity(key_fmt.attribute_types.len());
118 for attr_type in &key_fmt.attribute_types {
119 let attr_val = u16::from_reader_with_ctx(&mut k_reader, deku::ctx::Endian::Little)?;
120 attrs.push(rendition::Attribute {
121 name: *attr_type,
122 val: attr_val,
123 });
124 }
125
126 let header = CSIHeader::from_bom_block(v)?;
127
128 let id = attrs.get(attr_id_index).unwrap().val;
129 let key_values = attrs.iter().map(|attr| attr.val).collect::<Vec<_>>();
130 let item = CSIItem {
131 attrs,
132 header,
133 key_values: key_values.clone().into_boxed_slice(),
134 };
135 let items = items_by_identifier.entry(id).or_default();
136 let item_index = items.len();
137 exact_item_by_key
138 .entry(key_values.into_boxed_slice())
139 .or_insert((id, item_index));
140 items.push(item);
141
142 Ok(())
143 })?;
144
145 let attr_index_map: HashMap<u16, usize> = key_fmt
146 .attribute_types
147 .iter()
148 .enumerate()
149 .map(|(idx, &attr_type)| (attribute_type_tag(attr_type), idx))
150 .collect();
151
152 Ok(Self {
153 header,
154 appearance_keys,
155 extended_metadata,
156 key_fmt,
157 facet_keys,
158 items_by_identifier,
159 exact_item_by_key,
160 attr_index_map,
161 })
162 }
163
164 pub fn header(&self) -> &Header {
165 &self.header
166 }
167
168 pub fn appearance_keys(&self) -> &HashMap<String, BOMBytes> {
169 &self.appearance_keys
170 }
171
172 pub fn extended_metadata(&self) -> &ExtendedMetadata {
173 &self.extended_metadata
174 }
175
176 pub fn key_format(&self) -> &rendition::KeyFmt {
177 &self.key_fmt
178 }
179
180 pub fn item_with_key_values(&self, key_values: &[u16]) -> Option<&CSIItem> {
181 let &(identifier, item_index) = self.exact_item_by_key.get(key_values)?;
182 self.item_by_identifier(identifier, item_index)
183 }
184
185 pub fn resolved_source_bytes(&self, item: &CSIItem) -> CarResult<Vec<u8>> {
186 let source = if matches!(item.layout(), rendition::LayoutType::InternalReference) {
187 self.try_resolve_internal_reference(item)?.source
188 } else {
189 item
190 };
191
192 source.source_bytes_owned()
193 }
194
195 pub fn facets(&self) -> Vec<&rendition::KeyToken> {
196 self.named_facets()
197 .into_iter()
198 .map(|(_, facet)| facet)
199 .collect()
200 }
201
202 pub fn named_facets(&self) -> Vec<(&str, &rendition::KeyToken)> {
203 let mut facets: Vec<_> = self
204 .facet_keys
205 .iter()
206 .map(|(k, v)| (k.as_str(), v))
207 .collect();
208 facets.sort_unstable_by_key(|(name, _)| *name);
209 facets
210 }
211
212 pub fn facet(&self, name: &str) -> Option<&rendition::KeyToken> {
213 self.facet_keys.get(name)
214 }
215
216 pub fn facet_with_name(&self, name: &str) -> Option<&rendition::KeyToken> {
217 self.facet(name)
218 }
219
220 pub fn items_for_facet(&self, facet: &rendition::KeyToken) -> Option<&[CSIItem]> {
221 facet_identifier(facet)
222 .and_then(|identifier| self.items_by_identifier.get(&identifier))
223 .map(Vec::as_slice)
224 }
225
226 pub fn rendtions_with_facet(&self, facet: &rendition::KeyToken) -> Option<&[CSIItem]> {
227 self.items_for_facet(facet)
228 }
229
230 pub fn renditions_with_facet(&self, facet: &rendition::KeyToken) -> Option<&[CSIItem]> {
234 self.items_for_facet(facet)
235 }
236
237 pub fn renditions_for_facet(&self, facet: &rendition::KeyToken) -> Option<&[CSIItem]> {
239 self.items_for_facet(facet)
240 }
241
242 pub fn items_for_name(&self, name: &str) -> Option<&[CSIItem]> {
243 self.facet(name)
244 .and_then(|facet| self.items_for_facet(facet))
245 }
246
247 pub fn rendtions_with_name(&self, name: &str) -> Option<&[CSIItem]> {
248 self.items_for_name(name)
249 }
250
251 pub fn renditions_with_name(&self, name: &str) -> Option<&[CSIItem]> {
255 self.items_for_name(name)
256 }
257
258 pub fn renditions_for_name(&self, name: &str) -> Option<&[CSIItem]> {
260 self.items_for_name(name)
261 }
262
263 pub fn face_item_with_name(&self, name: &str) -> Option<FacetItem<'_>> {
264 self.facet(name).and_then(|facet| {
265 self.items_for_facet(facet)
266 .map(|resources| FacetItem { facet, resources })
267 })
268 }
269
270 pub fn facet_item_with_name(&self, name: &str) -> Option<FacetItem<'_>> {
274 self.face_item_with_name(name)
275 }
276
277 pub fn resolve_internal_reference<'a>(
285 &'a self,
286 item: &'a CSIItem,
287 ) -> Option<ResolvedInternalReference<'a>> {
288 self.try_resolve_internal_reference(item).ok()
289 }
290
291 pub fn try_resolve_internal_reference<'a>(
296 &'a self,
297 item: &'a CSIItem,
298 ) -> Result<ResolvedInternalReference<'a>, ReferenceResolveError> {
299 if !matches!(
300 item.header.metadata.layout_type,
301 rendition::LayoutType::InternalReference
302 ) {
303 return Err(ReferenceResolveError::NotInternalReference);
304 }
305
306 let reference = reference_tlv(item).ok_or(ReferenceResolveError::MissingReferenceTlv)?;
307
308 let effective_layout = reference.layout;
309
310 let mut crops = Vec::new();
311 let mut visited: HashSet<RenditionKey> = HashSet::new();
312
313 let source = self.try_resolve_ref_inner(reference, &mut crops, &mut visited, 0)?;
314
315 Ok(ResolvedInternalReference {
316 source,
317 effective_layout,
318 crops,
319 })
320 }
321
322 fn try_resolve_ref_inner<'a>(
323 &'a self,
324 reference: &rendition::RenditionTypeReference,
325 crops: &mut Vec<ReferenceRect>,
326 visited: &mut HashSet<RenditionKey>,
327 depth: usize,
328 ) -> Result<&'a CSIItem, ReferenceResolveError> {
329 if depth >= 16 {
330 return Err(ReferenceResolveError::DepthLimit { max_depth: 16 });
331 }
332
333 let mut key_array = vec![0u16; self.key_fmt.attribute_types.len()];
335 let mut chunks = reference.keys.chunks_exact(4);
336 for chunk in &mut chunks {
337 let attr_id = u16::from_le_bytes([chunk[0], chunk[1]]);
338 let value = u16::from_le_bytes([chunk[2], chunk[3]]);
339 if attr_id == 0 && value == 0 {
340 break;
341 }
342 let idx = *self
343 .attr_index_map
344 .get(&attr_id)
345 .ok_or(ReferenceResolveError::UnknownAttribute { attr_id })?;
346 key_array[idx] = value;
347 }
348 if !chunks.remainder().is_empty() {
349 return Err(ReferenceResolveError::MalformedKey {
350 byte_len: reference.keys.len(),
351 });
352 }
353
354 let key_values = key_array.into_boxed_slice();
356 if !visited.insert(key_values.clone()) {
357 return Err(ReferenceResolveError::Cycle {
358 key_values: key_values.to_vec(),
359 });
360 }
361
362 let &(identifier, item_index) = self
364 .exact_item_by_key
365 .get(key_values.as_ref())
366 .ok_or_else(|| ReferenceResolveError::TargetNotFound {
367 key_values: key_values.to_vec(),
368 })?;
369 let source = self
370 .item_by_identifier(identifier, item_index)
371 .ok_or_else(|| ReferenceResolveError::TargetNotFound {
372 key_values: key_values.to_vec(),
373 })?;
374
375 if matches!(
383 source.header.metadata.layout_type,
384 rendition::LayoutType::InternalReference
385 ) {
386 let next_ref =
387 reference_tlv(source).ok_or(ReferenceResolveError::MissingReferenceTlv)?;
388 let result = self.try_resolve_ref_inner(next_ref, crops, visited, depth + 1);
389 if result.is_ok() {
390 crops.push(reference_rect(reference));
391 }
392 result
393 } else {
394 crops.push(reference_rect(reference));
396 Ok(source)
397 }
398 }
399}
400
401fn reference_tlv(item: &CSIItem) -> Option<&rendition::RenditionTypeReference> {
402 item.header.tlv_data.iter().find_map(|tlv| {
403 if let rendition::RenditionType::Reference(reference) = tlv {
404 Some(reference)
405 } else {
406 None
407 }
408 })
409}
410
411fn reference_rect(reference: &rendition::RenditionTypeReference) -> ReferenceRect {
412 ReferenceRect {
413 x: reference.x,
414 y: reference.y,
415 width: reference.width,
416 height: reference.height,
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Error)]
421pub enum ReferenceResolveError {
422 #[error("rendition is not an InternalReference")]
423 NotInternalReference,
424 #[error("InternalReference has no Reference TLV")]
425 MissingReferenceTlv,
426 #[error("InternalReference key byte length is not divisible into attr/value pairs: {byte_len}")]
427 MalformedKey { byte_len: usize },
428 #[error("InternalReference uses unknown attribute id {attr_id}")]
429 UnknownAttribute { attr_id: u16 },
430 #[error("InternalReference target not found for key {key_values:?}")]
431 TargetNotFound { key_values: Vec<u16> },
432 #[error("InternalReference cycle detected at key {key_values:?}")]
433 Cycle { key_values: Vec<u16> },
434 #[error("InternalReference depth limit reached: {max_depth}")]
435 DepthLimit { max_depth: usize },
436}
437
438#[derive(Debug)]
439pub struct FacetItem<'a> {
440 facet: &'a rendition::KeyToken,
441 resources: &'a [CSIItem],
442}
443
444impl<'a> FacetItem<'a> {
445 pub fn facet(&self) -> &'a rendition::KeyToken {
446 self.facet
447 }
448
449 pub fn resources(&self) -> &'a [CSIItem] {
450 self.resources
451 }
452}
453
454impl CSIItem {
455 pub fn attributes(&self) -> &[rendition::Attribute] {
456 &self.attrs
457 }
458
459 pub fn header(&self) -> &CSIHeader {
460 &self.header
461 }
462
463 pub fn name(&self) -> &str {
464 &self.header.metadata.name
465 }
466
467 pub fn width(&self) -> u32 {
468 self.header.width
469 }
470
471 pub fn height(&self) -> u32 {
472 self.header.height
473 }
474
475 pub fn scale(&self) -> u32 {
476 self.attrs
477 .iter()
478 .find(|attr| attr.name == rendition::AttributeType::Scale && attr.val > 0)
479 .map(|attr| u32::from(attr.val))
480 .unwrap_or(self.header.scale_factor / 100)
481 }
482
483 pub fn layout(&self) -> rendition::LayoutType {
484 self.header.metadata.layout_type
485 }
486
487 pub fn encoding(&self) -> Encoding {
488 self.header.encoding
489 }
490
491 pub fn color_model(&self) -> ColorModel {
492 self.header.color_model
493 }
494
495 pub fn rendition_kind(&self) -> rendition::RenditionKind {
496 match &self.header.rendition {
497 Some(rendition::Rendition::Color(_)) => rendition::RenditionKind::Color,
498 Some(rendition::Rendition::RawData(_)) => rendition::RenditionKind::RawData,
499 Some(rendition::Rendition::ThemeCBCK(_)) => rendition::RenditionKind::ThemeCBCK,
500 Some(rendition::Rendition::MultisizeImageSet(_)) => {
501 rendition::RenditionKind::MultisizeImageSet
502 }
503 Some(rendition::Rendition::Unknown { .. }) => rendition::RenditionKind::Unknown,
504 None => rendition::RenditionKind::None,
505 }
506 }
507
508 pub fn compression(&self) -> Option<rendition::CompressionType> {
509 match &self.header.rendition {
510 Some(rendition::Rendition::ThemeCBCK(cbck)) => Some(cbck.compression_type),
511 _ => None,
512 }
513 }
514
515 pub fn key_values(&self) -> &[u16] {
516 &self.key_values
517 }
518
519 pub fn uti(&self) -> Option<String> {
520 self.header.tlv_data.iter().find_map(|tlv| match tlv {
521 rendition::RenditionType::UTI(uti) => Some(
522 String::from_utf8_lossy(&uti.string)
523 .trim_end_matches('\0')
524 .to_string(),
525 ),
526 _ => None,
527 })
528 }
529
530 pub(crate) fn bytes_per_row_tlv(&self) -> Option<u32> {
531 self.header.tlv_data.iter().find_map(|tlv| match tlv {
532 rendition::RenditionType::BytesPerRow { bytes_per_row, .. } => Some(*bytes_per_row),
533 _ => None,
534 })
535 }
536}
537
538impl Car {
539 fn item_by_identifier(&self, identifier: u16, item_index: usize) -> Option<&CSIItem> {
540 self.items_by_identifier
541 .get(&identifier)
542 .and_then(|items| items.get(item_index))
543 }
544}
545
546fn facet_identifier(facet: &rendition::KeyToken) -> Option<u16> {
547 facet
548 .attrs
549 .iter()
550 .find(|attr| attr.name == rendition::AttributeType::Identifier)
551 .map(|attr| attr.val)
552}
553
554fn attribute_type_tag(attr_type: rendition::AttributeType) -> u16 {
555 match attr_type {
556 rendition::AttributeType::ThemeLook => 0,
557 rendition::AttributeType::Element => 1,
558 rendition::AttributeType::Part => 2,
559 rendition::AttributeType::Size => 3,
560 rendition::AttributeType::Direction => 4,
561 rendition::AttributeType::Placeholder => 5,
562 rendition::AttributeType::Value => 6,
563 rendition::AttributeType::ThemeAppearance => 7,
564 rendition::AttributeType::Dimension1 => 8,
565 rendition::AttributeType::Dimension2 => 9,
566 rendition::AttributeType::State => 10,
567 rendition::AttributeType::Layer => 11,
568 rendition::AttributeType::Scale => 12,
569 rendition::AttributeType::Localization => 13,
570 rendition::AttributeType::PresentationState => 14,
571 rendition::AttributeType::Idiom => 15,
572 rendition::AttributeType::Subtype => 16,
573 rendition::AttributeType::Identifier => 17,
574 rendition::AttributeType::PreviousValue => 18,
575 rendition::AttributeType::PreviousState => 19,
576 rendition::AttributeType::HorizontalSizeClass => 20,
577 rendition::AttributeType::VerticalSizeClass => 21,
578 rendition::AttributeType::MemoryLevelClass => 22,
579 rendition::AttributeType::GraphicsFeatureSetClass => 23,
580 rendition::AttributeType::DisplayGamut => 24,
581 rendition::AttributeType::DeploymentTarget => 25,
582 rendition::AttributeType::GlyphWeight => 26,
583 rendition::AttributeType::GlyphSize => 27,
584 rendition::AttributeType::Unknown { tag } => tag,
585 }
586}
587
588#[derive(Error, Debug)]
589pub enum CarError {
590 #[error("Bom read failed {0}")]
591 BOM(#[from] BOMEror),
592 #[error("KeyFormat not found id attr")]
593 KeyFormatNotFoundIDAttr,
594 #[error("Rendition not found id attr")]
595 RenditionNotFoundIDAttr,
596 #[error("Unsupported compression type: {0:?}")]
597 UnsupportedCompression(rendition::CompressionType),
598 #[error("Decode failed: {0}")]
599 DecodeFailed(String),
600 #[error("Decode budget exceeded: {0}")]
601 DecodeBudgetExceeded(crate::decode::DecodeBudgetError),
602 #[error("Reference resolve failed: {0}")]
603 ReferenceResolve(#[from] ReferenceResolveError),
604 #[error("Deepmap2 error: {0}")]
605 Deepmap2(#[from] deepmap2::Deepmap2Error),
606 #[error("Unsupported encoding: {0:?}")]
607 UnsupportedEncoding(Encoding),
608 #[error("Native fallback unavailable: {0}")]
609 NativeFallbackUnavailable(String),
610 #[error("IO error: {0}")]
611 Io(#[from] std::io::Error),
612 #[cfg(feature = "image")]
613 #[error("Image error: {0}")]
614 Image(#[from] image::ImageError),
615}
616
617impl From<crate::apple_compression::AppleCompressionError> for CarError {
618 fn from(error: crate::apple_compression::AppleCompressionError) -> Self {
619 match error {
620 crate::apple_compression::AppleCompressionError::DecodeFailed(message) => {
621 Self::DecodeFailed(message)
622 }
623 crate::apple_compression::AppleCompressionError::NativeFallbackUnavailable(message) => {
624 Self::NativeFallbackUnavailable(message.to_string())
625 }
626 crate::apple_compression::AppleCompressionError::Io(error) => Self::Io(error),
627 }
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use std::io::Cursor;
634 use std::path::{Path, PathBuf};
635
636 use bom::raw::StoreHeader;
637 use deku::DekuContainerRead;
638
639 use super::{BOMEror, Car, CarError, ReferenceResolveError, tree_key};
640 use crate::asset::{AssetKind, PayloadKind};
641 use crate::{VariantQuery, export};
642
643 const SMOKE_IMAGE: &str = "2016_coin1";
644 const SMOKE_COLOR: &str = "ActionSheet_Action_Icon_Color";
645 const SMOKE_DOCUMENT: &str = "GameLifeChatListEmpty";
646 const DISPLAY_GAMUT_IMAGE: &str = "AS_YuanBao_Dark";
647
648 fn fixture_path(name: &str) -> PathBuf {
649 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
650 .join("../car-tests/data")
651 .join(name)
652 }
653
654 fn fixture_car() -> Car {
655 Car::new(fixture_path("Assets.car")).expect("load test Assets.car")
656 }
657
658 fn fixture_without_bom_variable(name: &[u8]) -> Vec<u8> {
659 let mut bytes = std::fs::read(fixture_path("Assets.car")).expect("read fixture bytes");
660 let var_offset = u32::from_be_bytes(bytes[24..28].try_into().unwrap()) as usize;
661 let var_count = u32::from_be_bytes(bytes[var_offset..var_offset + 4].try_into().unwrap());
662 let mut cursor = var_offset + 4;
663
664 for _ in 0..var_count {
665 let name_start = cursor + 5; let name_end = name_start + bytes[cursor + 4] as usize;
667 if &bytes[name_start..name_end] == name {
668 bytes[name_start] = b'!'; return bytes;
670 }
671 cursor = name_end;
672 }
673
674 panic!("fixture is missing BOM variable {name:?}");
675 }
676
677 #[test]
678 fn archives_without_appearance_keys_load() {
679 let expected = fixture_car();
680 assert!(!expected.appearance_keys().is_empty());
681
682 let bytes = fixture_without_bom_variable(tree_key::APPEARANCE_KEYS);
683 let car = Car::from_bytes(bytes).expect("APPEARANCEKEYS is optional");
684
685 assert!(car.appearance_keys().is_empty());
686 assert_eq!(car.entries(), expected.entries());
687 }
688
689 #[test]
690 fn malformed_appearance_keys_are_rejected() {
691 let mut bytes = std::fs::read(fixture_path("Assets.car")).expect("read fixture bytes");
692 let (_, header) = StoreHeader::from_bytes((&bytes, 0)).expect("parse fixture BOM header");
693 let var = header
694 .var_with_name(tree_key::APPEARANCE_KEYS)
695 .expect("fixture appearance tree");
696 let tree_offset = header.index_store.indexs[var.index as usize].offset as usize;
697 bytes[tree_offset] = b'!';
698
699 assert!(matches!(
700 Car::from_bytes(bytes),
701 Err(CarError::BOM(BOMEror::ParseStruct(_)))
702 ));
703 }
704
705 #[test]
706 fn missing_required_tree_is_rejected() {
707 let bytes = fixture_without_bom_variable(tree_key::FACET_KEYS);
708
709 assert!(matches!(
710 Car::from_bytes(bytes),
711 Err(CarError::BOM(BOMEror::NotFoundVar(name)))
712 if name.as_bytes() == tree_key::FACET_KEYS
713 ));
714 }
715
716 #[test]
717 fn named_facets_are_sorted_stably() {
718 let car = fixture_car();
719 let names: Vec<_> = car
720 .named_facets()
721 .into_iter()
722 .map(|(name, _)| name.to_string())
723 .collect();
724 let mut sorted = names.clone();
725 sorted.sort();
726 assert_eq!(names, sorted);
727 }
728
729 #[test]
730 fn from_bytes_matches_path_backed_document_info() {
731 let path_backed = fixture_car();
732 let bytes = std::fs::read(fixture_path("Assets.car")).expect("read fixture bytes");
733 let from_bytes = Car::from_bytes(bytes.clone()).expect("load from bytes");
734 let from_reader = Car::from_reader(Cursor::new(bytes)).expect("load from reader");
735
736 let expected = path_backed.document_info();
737 for actual in [from_bytes.document_info(), from_reader.document_info()] {
738 assert_eq!(expected.rendition_count, actual.rendition_count);
739 assert_eq!(expected.main_version_string, actual.main_version_string);
740 assert_eq!(expected.version_string, actual.version_string);
741 }
742
743 let path_items = path_backed
744 .renditions_with_name(SMOKE_IMAGE)
745 .expect("path-backed smoke image");
746 let bytes_items = from_bytes
747 .renditions_with_name(SMOKE_IMAGE)
748 .expect("bytes-backed smoke image");
749 let reader_items = from_reader
750 .renditions_with_name(SMOKE_IMAGE)
751 .expect("reader-backed smoke image");
752 assert_eq!(path_items.len(), bytes_items.len());
753 assert_eq!(path_items.len(), reader_items.len());
754 assert_eq!(path_items[0].key_values(), bytes_items[0].key_values());
755 assert_eq!(path_items[0].key_values(), reader_items[0].key_values());
756 }
757
758 #[test]
759 fn from_reader_delegates_to_owned_bytes() {
760 let bytes = std::fs::read(fixture_path("Assets.car")).expect("read fixture bytes");
761 let from_reader = Car::from_reader(Cursor::new(bytes)).expect("load from reader");
762
763 assert_eq!(
764 from_reader.document_info().rendition_count,
765 fixture_car().document_info().rendition_count
766 );
767 }
768
769 #[test]
770 fn correctly_spelled_compat_aliases_match_legacy_methods() {
771 let car = fixture_car();
772 let facet = car.facet(SMOKE_IMAGE).expect("smoke image facet");
773
774 let legacy_by_facet = car.rendtions_with_facet(facet).expect("legacy by facet");
775 let by_facet = car
776 .renditions_with_facet(facet)
777 .expect("correct spelling by facet");
778 let for_facet = car.renditions_for_facet(facet).expect("for_facet alias");
779 assert_eq!(legacy_by_facet.len(), by_facet.len());
780 assert_eq!(legacy_by_facet.len(), for_facet.len());
781
782 let legacy_by_name = car
783 .rendtions_with_name(SMOKE_IMAGE)
784 .expect("legacy by name");
785 let by_name = car
786 .renditions_with_name(SMOKE_IMAGE)
787 .expect("correct spelling by name");
788 let for_name = car
789 .renditions_for_name(SMOKE_IMAGE)
790 .expect("for_name alias");
791 assert_eq!(legacy_by_name[0].key_values(), by_name[0].key_values());
792 assert_eq!(legacy_by_name[0].key_values(), for_name[0].key_values());
793
794 let legacy_facet_item = car
795 .face_item_with_name(SMOKE_IMAGE)
796 .expect("legacy facet item");
797 let facet_item = car
798 .facet_item_with_name(SMOKE_IMAGE)
799 .expect("correct spelling facet item");
800 assert_eq!(
801 legacy_facet_item.resources().len(),
802 facet_item.resources().len()
803 );
804 }
805
806 #[test]
807 fn entries_are_stable_and_round_trip_by_id() {
808 let car = fixture_car();
809 let entries = car.entries();
810 assert!(!entries.is_empty());
811 assert_eq!(entries, car.entries());
812
813 for entry in &entries {
814 let round_trip = car.entry(entry.id).expect("entry id should resolve");
815 assert_eq!(&round_trip, entry);
816 }
817
818 let image = entries
819 .iter()
820 .find(|entry| entry.facet_name == SMOKE_IMAGE)
821 .expect("smoke image entry");
822 assert_eq!(image.kind, AssetKind::Image);
823 assert_eq!(image.payload_kind, PayloadKind::DecodedRaster);
824
825 let color = entries
826 .iter()
827 .find(|entry| entry.facet_name == SMOKE_COLOR)
828 .expect("smoke color entry");
829 assert_eq!(color.kind, AssetKind::Color);
830 assert_eq!(color.payload_kind, PayloadKind::ColorMetadata);
831
832 let document = entries
833 .iter()
834 .find(|entry| entry.facet_name == SMOKE_DOCUMENT)
835 .expect("smoke document entry");
836 assert_eq!(document.kind, AssetKind::Document);
837 }
838
839 #[test]
840 fn typed_variant_query_finds_requested_variant() {
841 let car = fixture_car();
842 let variant = car
843 .best_variant_for_name(DISPLAY_GAMUT_IMAGE, &VariantQuery::new().display_gamut(1))
844 .expect("display-gamut variant should match");
845
846 assert_eq!(variant.facet_name, DISPLAY_GAMUT_IMAGE);
847 assert_eq!(variant.attributes.display_gamut, Some(1));
848 }
849
850 #[test]
851 fn typed_variant_query_reports_miss() {
852 let car = fixture_car();
853 let err = car
854 .best_variant_for_name(SMOKE_IMAGE, &VariantQuery::new().scale(9))
855 .expect_err("scale 9 should not exist");
856 assert!(format!("{err}").contains("no variant matching"));
857 }
858
859 #[test]
860 fn diagnostics_report_counts_entries_and_unknowns() {
861 let car = fixture_car();
862 let report = car.diagnostics();
863
864 assert_eq!(report.totals.facets, car.named_facets().len());
865 assert_eq!(report.totals.entries, report.entries.len());
866 assert!(report.totals.entries > 0);
867 assert!(
868 report
869 .entries
870 .iter()
871 .any(|entry| entry.facet_name == SMOKE_IMAGE)
872 );
873 }
874
875 #[test]
876 fn export_plan_matches_existing_cli_paths() {
877 let car = fixture_car();
878 let plan = export::plan_export(&car, Path::new("out"));
879 let paths: Vec<_> = plan.jobs.iter().map(|job| job.path.clone()).collect();
880
881 assert!(paths.contains(&PathBuf::from("out/2016_coin1/2016_coin1@3x.png")));
882 assert!(paths.contains(&PathBuf::from(
883 "out/ActionSheet_Action_Icon_Color/ActionSheet_Action_Icon_Color.json"
884 )));
885 }
886
887 #[test]
888 fn typed_reference_resolve_reports_non_reference() {
889 let car = fixture_car();
890 let item = car
891 .renditions_with_name(SMOKE_IMAGE)
892 .and_then(|items| items.first())
893 .expect("smoke image item");
894
895 let err = car
896 .try_resolve_internal_reference(item)
897 .expect_err("direct image is not an internal reference");
898 assert_eq!(err, ReferenceResolveError::NotInternalReference);
899 }
900}