Skip to main content

car/
car.rs

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/// Crop rectangle extracted from a `RenditionTypeReference` TLV.
34#[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/// Result of resolving an `InternalReference` rendition.
43#[derive(Debug)]
44pub struct ResolvedInternalReference<'a> {
45    /// The final non-reference source rendition item.
46    pub source: &'a CSIItem,
47    /// The raw layout value from the first `RenditionTypeReference.layout` field.
48    pub effective_layout: u16,
49    /// Crop rectangles accumulated from each resolved reference layer (innermost first).
50    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    /// Correctly-spelled alias for [`Car::rendtions_with_facet`].
224    ///
225    /// The historical misspelled method remains available for compatibility.
226    pub fn renditions_with_facet(&self, facet: &rendition::KeyToken) -> Option<&[CSIItem]> {
227        self.items_for_facet(facet)
228    }
229
230    /// Alias for [`Car::items_for_facet`] using the public rendition terminology.
231    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    /// Correctly-spelled alias for [`Car::rendtions_with_name`].
245    ///
246    /// The historical misspelled method remains available for compatibility.
247    pub fn renditions_with_name(&self, name: &str) -> Option<&[CSIItem]> {
248        self.items_for_name(name)
249    }
250
251    /// Alias for [`Car::items_for_name`] using the public rendition terminology.
252    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    /// Correctly-spelled alias for [`Car::face_item_with_name`].
264    ///
265    /// The historical misspelled method remains available for compatibility.
266    pub fn facet_item_with_name(&self, name: &str) -> Option<FacetItem<'_>> {
267        self.face_item_with_name(name)
268    }
269
270    /// Resolve an `InternalReference` rendition to its final source rendition.
271    ///
272    /// Searches ALL entries in the RENDITIONS tree using exact key matching, not just
273    /// facet-accessible items.  Recursion depth is capped at 16; cycles return `None`.
274    ///
275    /// Returns `None` if `item` is not an `InternalReference`, if the reference cannot
276    /// be parsed, or if the source cannot be located.
277    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    /// Resolve an `InternalReference` rendition and report typed failure reasons.
285    ///
286    /// The compatibility method [`Car::resolve_internal_reference`] maps all errors
287    /// to `None`.
288    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        // Parse Reference.keys as (attr_id: u16, value: u16) LE pairs until (0, 0).
327        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        // Cycle detection using the complete key array.
348        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        // Exact key match across ALL RENDITIONS entries (not just facet-accessible items).
356        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        // Recurse if the found source is itself an InternalReference.
369        //
370        // Push this layer's frame AFTER recursing so that `crops` is ordered
371        // innermost-first.  `save_image_with_crops` iterates the slice front-to-back
372        // and composites inner-frame first, then outer-frame — which is the correct
373        // order for a chain  A → B → C:  place C into B's frame, then the result
374        // into A's frame.
375        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            // Leaf source: push and return.
388            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}