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        // Catalogs without appearance variants may omit this tree.
91        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    /// Correctly-spelled alias for [`Car::rendtions_with_facet`].
231    ///
232    /// The historical misspelled method remains available for compatibility.
233    pub fn renditions_with_facet(&self, facet: &rendition::KeyToken) -> Option<&[CSIItem]> {
234        self.items_for_facet(facet)
235    }
236
237    /// Alias for [`Car::items_for_facet`] using the public rendition terminology.
238    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    /// Correctly-spelled alias for [`Car::rendtions_with_name`].
252    ///
253    /// The historical misspelled method remains available for compatibility.
254    pub fn renditions_with_name(&self, name: &str) -> Option<&[CSIItem]> {
255        self.items_for_name(name)
256    }
257
258    /// Alias for [`Car::items_for_name`] using the public rendition terminology.
259    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    /// Correctly-spelled alias for [`Car::face_item_with_name`].
271    ///
272    /// The historical misspelled method remains available for compatibility.
273    pub fn facet_item_with_name(&self, name: &str) -> Option<FacetItem<'_>> {
274        self.face_item_with_name(name)
275    }
276
277    /// Resolve an `InternalReference` rendition to its final source rendition.
278    ///
279    /// Searches ALL entries in the RENDITIONS tree using exact key matching, not just
280    /// facet-accessible items.  Recursion depth is capped at 16; cycles return `None`.
281    ///
282    /// Returns `None` if `item` is not an `InternalReference`, if the reference cannot
283    /// be parsed, or if the source cannot be located.
284    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    /// Resolve an `InternalReference` rendition and report typed failure reasons.
292    ///
293    /// The compatibility method [`Car::resolve_internal_reference`] maps all errors
294    /// to `None`.
295    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        // Parse Reference.keys as (attr_id: u16, value: u16) LE pairs until (0, 0).
334        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        // Cycle detection using the complete key array.
355        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        // Exact key match across ALL RENDITIONS entries (not just facet-accessible items).
363        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        // Recurse if the found source is itself an InternalReference.
376        //
377        // Push this layer's frame AFTER recursing so that `crops` is ordered
378        // innermost-first.  `save_image_with_crops` iterates the slice front-to-back
379        // and composites inner-frame first, then outer-frame — which is the correct
380        // order for a chain  A → B → C:  place C into B's frame, then the result
381        // into A's frame.
382        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            // Leaf source: push and return.
395            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; // block index (u32) and name length (u8)
666            let name_end = name_start + bytes[cursor + 4] as usize;
667            if &bytes[name_start..name_end] == name {
668                bytes[name_start] = b'!'; // Hide only this name, leaving all blocks intact.
669                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}