1use 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}