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