Skip to main content

car/
diagnostics.rs

1//! Archive diagnostics for unsupported, unknown, and unresolved data.
2
3use crate::asset::{AssetKind, asset_kind_for_item};
4use crate::car::{CSIItem, Car, ReferenceResolveError};
5use crate::metadata::{TlvMetadata, UnknownTlv};
6use crate::model::rendition::{CompressionType, LayoutType, Rendition, RenditionKind};
7use crate::model::{ColorModel, Encoding};
8use crate::output;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct DiagnosticsReport {
12    pub totals: DiagnosticsTotals,
13    pub entries: Vec<EntryDiagnostics>,
14}
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct DiagnosticsTotals {
18    pub facets: usize,
19    pub entries: usize,
20    pub supported_outputs: usize,
21    pub unsupported_outputs: usize,
22    pub internal_references: usize,
23    pub unresolved_references: usize,
24    pub unknown_renditions: usize,
25    pub unknown_tlvs: usize,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct EntryDiagnostics {
30    pub facet_name: String,
31    pub rendition_name: String,
32    pub key_values: Vec<u16>,
33    pub width: u32,
34    pub height: u32,
35    pub scale: u32,
36    pub layout: LayoutType,
37    pub encoding: Encoding,
38    pub color_model: ColorModel,
39    pub rendition_kind: RenditionKind,
40    pub compression: Option<CompressionType>,
41    pub asset_kind: AssetKind,
42    pub tlv: TlvMetadata,
43    pub issues: Vec<DiagnosticIssue>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum DiagnosticIssue {
48    Unsupported(UnsupportedReason),
49    UnknownRendition { tag: [u8; 4], byte_len: usize },
50    UnknownTlv(UnknownTlv),
51    UnresolvedReference(ReferenceResolveError),
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum UnsupportedReason {
56    Layout(LayoutType),
57    RenditionKind(RenditionKind),
58    Encoding(Encoding),
59    Compression(CompressionType),
60}
61
62impl Car {
63    pub fn diagnostics(&self) -> DiagnosticsReport {
64        let mut totals = DiagnosticsTotals::default();
65        let mut entries = Vec::new();
66
67        for (facet_name, facet) in self.named_facets() {
68            totals.facets += 1;
69            let Some(items) = self.items_for_facet(facet) else {
70                continue;
71            };
72            for item in items {
73                totals.entries += 1;
74                let diagnostics = self.entry_diagnostics(facet_name, item);
75                if diagnostics
76                    .issues
77                    .iter()
78                    .any(|issue| matches!(issue, DiagnosticIssue::Unsupported(_)))
79                {
80                    totals.unsupported_outputs += 1;
81                } else {
82                    totals.supported_outputs += 1;
83                }
84                if matches!(item.layout(), LayoutType::InternalReference) {
85                    totals.internal_references += 1;
86                }
87                for issue in &diagnostics.issues {
88                    match issue {
89                        DiagnosticIssue::UnknownRendition { .. } => totals.unknown_renditions += 1,
90                        DiagnosticIssue::UnknownTlv(_) => totals.unknown_tlvs += 1,
91                        DiagnosticIssue::UnresolvedReference(_) => {
92                            totals.unresolved_references += 1
93                        }
94                        DiagnosticIssue::Unsupported(_) => {}
95                    }
96                }
97                entries.push(diagnostics);
98            }
99        }
100
101        DiagnosticsReport { totals, entries }
102    }
103
104    fn entry_diagnostics(&self, facet_name: &str, item: &CSIItem) -> EntryDiagnostics {
105        let tlv = item.tlv_metadata();
106        let mut issues = Vec::new();
107
108        if let Some(reason) = unsupported_reason(item) {
109            issues.push(DiagnosticIssue::Unsupported(reason));
110        }
111
112        if let Some(Rendition::Unknown { tag, data }) = item.header().rendition.as_ref() {
113            issues.push(DiagnosticIssue::UnknownRendition {
114                tag: *tag,
115                byte_len: data.raw_data.len(),
116            });
117        }
118
119        issues.extend(tlv.unknown.iter().cloned().map(DiagnosticIssue::UnknownTlv));
120
121        if matches!(item.layout(), LayoutType::InternalReference)
122            && let Err(error) = self.try_resolve_internal_reference(item)
123        {
124            issues.push(DiagnosticIssue::UnresolvedReference(error));
125        }
126
127        EntryDiagnostics {
128            facet_name: facet_name.to_string(),
129            rendition_name: item.name().to_string(),
130            key_values: item.key_values().to_vec(),
131            width: item.width(),
132            height: item.height(),
133            scale: item.scale(),
134            layout: item.layout(),
135            encoding: item.encoding(),
136            color_model: item.color_model(),
137            rendition_kind: item.rendition_kind(),
138            compression: item.compression(),
139            asset_kind: asset_kind_for_item(item),
140            tlv,
141            issues,
142        }
143    }
144}
145
146impl CSIItem {
147    pub fn unsupported_reason(&self) -> Option<UnsupportedReason> {
148        unsupported_reason(self)
149    }
150}
151
152fn unsupported_reason(item: &CSIItem) -> Option<UnsupportedReason> {
153    if matches!(item.layout(), LayoutType::InternalReference) {
154        return None;
155    }
156
157    match item.header().rendition.as_ref() {
158        Some(Rendition::Unknown { .. }) => {
159            Some(UnsupportedReason::RenditionKind(RenditionKind::Unknown))
160        }
161        None => Some(UnsupportedReason::RenditionKind(RenditionKind::None)),
162        Some(Rendition::ThemeCBCK(cbck)) if unsupported_compression(cbck.compression_type) => {
163            Some(UnsupportedReason::Compression(cbck.compression_type))
164        }
165        Some(_) if matches!(item.encoding(), Encoding::Unknown { .. } | Encoding::None) => {
166            Some(UnsupportedReason::Encoding(item.encoding()))
167        }
168        Some(_) if output::supported_output_identity(item).is_none() => {
169            Some(UnsupportedReason::Layout(item.layout()))
170        }
171        _ => None,
172    }
173}
174
175fn unsupported_compression(compression: CompressionType) -> bool {
176    !matches!(
177        compression,
178        CompressionType::Uncompressed
179            | CompressionType::Zip
180            | CompressionType::Lzfse
181            | CompressionType::Lzvn
182            | CompressionType::HEVC
183            | CompressionType::Deepmap2
184    )
185}
186
187#[cfg(test)]
188mod tests {
189    use std::path::PathBuf;
190
191    use super::*;
192
193    fn fixture_path(name: &str) -> PathBuf {
194        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
195            .join("../car-tests/data")
196            .join(name)
197    }
198
199    #[test]
200    fn promptbar_hevc_is_supported_as_original_output() {
201        let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
202        let report = car.diagnostics();
203        let promptbar = report
204            .entries
205            .iter()
206            .filter(|entry| entry.facet_name == "PromptBarBkg")
207            .collect::<Vec<_>>();
208
209        assert_eq!(promptbar.len(), 4);
210        assert!(promptbar.iter().all(|entry| {
211            !entry.issues.iter().any(|issue| {
212                matches!(
213                    issue,
214                    DiagnosticIssue::Unsupported(UnsupportedReason::Compression(
215                        CompressionType::HEVC
216                    ))
217                )
218            })
219        }));
220    }
221}