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::Color(_) | Rendition::MultisizeImageSet(_)) => None,
164 Some(Rendition::ThemeCBCK(cbck)) if unsupported_compression(cbck.compression_type) => {
165 Some(UnsupportedReason::Compression(cbck.compression_type))
166 }
167 Some(_) if matches!(item.encoding(), Encoding::Unknown { .. } | Encoding::None) => {
168 Some(UnsupportedReason::Encoding(item.encoding()))
169 }
170 Some(_) if output::supported_output_identity(item).is_none() => {
171 Some(UnsupportedReason::Layout(item.layout()))
172 }
173 _ => None,
174 }
175}
176
177fn unsupported_compression(compression: CompressionType) -> bool {
178 !matches!(
179 compression,
180 CompressionType::Uncompressed
181 | CompressionType::Zip
182 | CompressionType::Lzfse
183 | CompressionType::Lzvn
184 | CompressionType::HEVC
185 | CompressionType::Deepmap2
186 )
187}
188
189#[cfg(test)]
190mod tests {
191 use std::path::PathBuf;
192
193 use super::*;
194
195 fn fixture_path(name: &str) -> PathBuf {
196 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
197 .join("../car-tests/data")
198 .join(name)
199 }
200
201 #[test]
202 fn metadata_without_pixel_encoding_are_supported_as_json() {
203 let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
204 let plan = crate::export::plan_export(&car, "out");
205 let report = car.diagnostics();
206
207 for (facet_name, kind) in [
208 ("ActionSheet_Action_Icon_Color", RenditionKind::Color),
209 ("AppIcon", RenditionKind::MultisizeImageSet),
210 ] {
211 let jobs = plan
212 .jobs
213 .iter()
214 .filter(|job| {
215 job.logical_facet_name == facet_name
216 && job.payload_item.rendition_kind() == kind
217 })
218 .collect::<Vec<_>>();
219
220 assert_eq!(jobs.len(), 2, "{facet_name} metadata outputs");
221 for job in jobs {
222 assert_eq!(job.format, crate::export::ExportFormat::Json);
223 assert_eq!(job.payload_item.encoding(), Encoding::None);
224 assert_eq!(job.payload_item.unsupported_reason(), None, "{facet_name}");
225 }
226
227 let entries = report
228 .entries
229 .iter()
230 .filter(|entry| entry.facet_name == facet_name && entry.rendition_kind == kind)
231 .collect::<Vec<_>>();
232 assert_eq!(entries.len(), 2, "{facet_name} metadata diagnostics");
233 assert!(entries.iter().all(|entry| {
234 !entry
235 .issues
236 .iter()
237 .any(|issue| matches!(issue, DiagnosticIssue::Unsupported(_)))
238 }));
239 }
240 }
241
242 #[test]
243 fn metadata_outputs_do_not_inflate_unsupported_totals() {
244 let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
245 let report = car.diagnostics();
246 for kind in [AssetKind::Color, AssetKind::MultisizeImageSet] {
247 assert!(report.entries.iter().any(|entry| entry.asset_kind == kind));
248 }
249 let unsupported_non_metadata = report
250 .entries
251 .iter()
252 .filter(|entry| {
253 !matches!(
254 entry.asset_kind,
255 AssetKind::Color | AssetKind::MultisizeImageSet
256 )
257 })
258 .filter(|entry| {
259 entry
260 .issues
261 .iter()
262 .any(|issue| matches!(issue, DiagnosticIssue::Unsupported(_)))
263 })
264 .count();
265
266 assert_eq!(report.totals.unsupported_outputs, unsupported_non_metadata);
267 assert_eq!(
268 report.totals.supported_outputs,
269 report.totals.entries - unsupported_non_metadata
270 );
271 }
272
273 #[test]
274 fn promptbar_hevc_is_supported_as_original_output() {
275 let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
276 let report = car.diagnostics();
277 let promptbar = report
278 .entries
279 .iter()
280 .filter(|entry| entry.facet_name == "PromptBarBkg")
281 .collect::<Vec<_>>();
282
283 assert_eq!(promptbar.len(), 4);
284 assert!(promptbar.iter().all(|entry| {
285 !entry.issues.iter().any(|issue| {
286 matches!(
287 issue,
288 DiagnosticIssue::Unsupported(UnsupportedReason::Compression(
289 CompressionType::HEVC
290 ))
291 )
292 })
293 }));
294 }
295}