use crate::asset::{AssetKind, asset_kind_for_item};
use crate::car::{CSIItem, Car, ReferenceResolveError};
use crate::metadata::{TlvMetadata, UnknownTlv};
use crate::model::rendition::{CompressionType, LayoutType, Rendition, RenditionKind};
use crate::model::{ColorModel, Encoding};
use crate::output;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticsReport {
pub totals: DiagnosticsTotals,
pub entries: Vec<EntryDiagnostics>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DiagnosticsTotals {
pub facets: usize,
pub entries: usize,
pub supported_outputs: usize,
pub unsupported_outputs: usize,
pub internal_references: usize,
pub unresolved_references: usize,
pub unknown_renditions: usize,
pub unknown_tlvs: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryDiagnostics {
pub facet_name: String,
pub rendition_name: String,
pub key_values: Vec<u16>,
pub width: u32,
pub height: u32,
pub scale: u32,
pub layout: LayoutType,
pub encoding: Encoding,
pub color_model: ColorModel,
pub rendition_kind: RenditionKind,
pub compression: Option<CompressionType>,
pub asset_kind: AssetKind,
pub tlv: TlvMetadata,
pub issues: Vec<DiagnosticIssue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticIssue {
Unsupported(UnsupportedReason),
UnknownRendition { tag: [u8; 4], byte_len: usize },
UnknownTlv(UnknownTlv),
UnresolvedReference(ReferenceResolveError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnsupportedReason {
Layout(LayoutType),
RenditionKind(RenditionKind),
Encoding(Encoding),
Compression(CompressionType),
}
impl Car {
pub fn diagnostics(&self) -> DiagnosticsReport {
let mut totals = DiagnosticsTotals::default();
let mut entries = Vec::new();
for (facet_name, facet) in self.named_facets() {
totals.facets += 1;
let Some(items) = self.items_for_facet(facet) else {
continue;
};
for item in items {
totals.entries += 1;
let diagnostics = self.entry_diagnostics(facet_name, item);
if diagnostics
.issues
.iter()
.any(|issue| matches!(issue, DiagnosticIssue::Unsupported(_)))
{
totals.unsupported_outputs += 1;
} else {
totals.supported_outputs += 1;
}
if matches!(item.layout(), LayoutType::InternalReference) {
totals.internal_references += 1;
}
for issue in &diagnostics.issues {
match issue {
DiagnosticIssue::UnknownRendition { .. } => totals.unknown_renditions += 1,
DiagnosticIssue::UnknownTlv(_) => totals.unknown_tlvs += 1,
DiagnosticIssue::UnresolvedReference(_) => {
totals.unresolved_references += 1
}
DiagnosticIssue::Unsupported(_) => {}
}
}
entries.push(diagnostics);
}
}
DiagnosticsReport { totals, entries }
}
fn entry_diagnostics(&self, facet_name: &str, item: &CSIItem) -> EntryDiagnostics {
let tlv = item.tlv_metadata();
let mut issues = Vec::new();
if let Some(reason) = unsupported_reason(item) {
issues.push(DiagnosticIssue::Unsupported(reason));
}
if let Some(Rendition::Unknown { tag, data }) = item.header().rendition.as_ref() {
issues.push(DiagnosticIssue::UnknownRendition {
tag: *tag,
byte_len: data.raw_data.len(),
});
}
issues.extend(tlv.unknown.iter().cloned().map(DiagnosticIssue::UnknownTlv));
if matches!(item.layout(), LayoutType::InternalReference)
&& let Err(error) = self.try_resolve_internal_reference(item)
{
issues.push(DiagnosticIssue::UnresolvedReference(error));
}
EntryDiagnostics {
facet_name: facet_name.to_string(),
rendition_name: item.name().to_string(),
key_values: item.key_values().to_vec(),
width: item.width(),
height: item.height(),
scale: item.scale(),
layout: item.layout(),
encoding: item.encoding(),
color_model: item.color_model(),
rendition_kind: item.rendition_kind(),
compression: item.compression(),
asset_kind: asset_kind_for_item(item),
tlv,
issues,
}
}
}
impl CSIItem {
pub fn unsupported_reason(&self) -> Option<UnsupportedReason> {
unsupported_reason(self)
}
}
fn unsupported_reason(item: &CSIItem) -> Option<UnsupportedReason> {
if matches!(item.layout(), LayoutType::InternalReference) {
return None;
}
match item.header().rendition.as_ref() {
Some(Rendition::Unknown { .. }) => {
Some(UnsupportedReason::RenditionKind(RenditionKind::Unknown))
}
None => Some(UnsupportedReason::RenditionKind(RenditionKind::None)),
Some(Rendition::Color(_) | Rendition::MultisizeImageSet(_)) => None,
Some(Rendition::ThemeCBCK(cbck)) if unsupported_compression(cbck.compression_type) => {
Some(UnsupportedReason::Compression(cbck.compression_type))
}
Some(_) if matches!(item.encoding(), Encoding::Unknown { .. } | Encoding::None) => {
Some(UnsupportedReason::Encoding(item.encoding()))
}
Some(_) if output::supported_output_identity(item).is_none() => {
Some(UnsupportedReason::Layout(item.layout()))
}
_ => None,
}
}
fn unsupported_compression(compression: CompressionType) -> bool {
!matches!(
compression,
CompressionType::Uncompressed
| CompressionType::Zip
| CompressionType::Lzfse
| CompressionType::Lzvn
| CompressionType::HEVC
| CompressionType::Deepmap2
)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../car-tests/data")
.join(name)
}
#[test]
fn metadata_without_pixel_encoding_are_supported_as_json() {
let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
let plan = crate::export::plan_export(&car, "out");
let report = car.diagnostics();
for (facet_name, kind) in [
("ActionSheet_Action_Icon_Color", RenditionKind::Color),
("AppIcon", RenditionKind::MultisizeImageSet),
] {
let jobs = plan
.jobs
.iter()
.filter(|job| {
job.logical_facet_name == facet_name
&& job.payload_item.rendition_kind() == kind
})
.collect::<Vec<_>>();
assert_eq!(jobs.len(), 2, "{facet_name} metadata outputs");
for job in jobs {
assert_eq!(job.format, crate::export::ExportFormat::Json);
assert_eq!(job.payload_item.encoding(), Encoding::None);
assert_eq!(job.payload_item.unsupported_reason(), None, "{facet_name}");
}
let entries = report
.entries
.iter()
.filter(|entry| entry.facet_name == facet_name && entry.rendition_kind == kind)
.collect::<Vec<_>>();
assert_eq!(entries.len(), 2, "{facet_name} metadata diagnostics");
assert!(entries.iter().all(|entry| {
!entry
.issues
.iter()
.any(|issue| matches!(issue, DiagnosticIssue::Unsupported(_)))
}));
}
}
#[test]
fn metadata_outputs_do_not_inflate_unsupported_totals() {
let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
let report = car.diagnostics();
for kind in [AssetKind::Color, AssetKind::MultisizeImageSet] {
assert!(report.entries.iter().any(|entry| entry.asset_kind == kind));
}
let unsupported_non_metadata = report
.entries
.iter()
.filter(|entry| {
!matches!(
entry.asset_kind,
AssetKind::Color | AssetKind::MultisizeImageSet
)
})
.filter(|entry| {
entry
.issues
.iter()
.any(|issue| matches!(issue, DiagnosticIssue::Unsupported(_)))
})
.count();
assert_eq!(report.totals.unsupported_outputs, unsupported_non_metadata);
assert_eq!(
report.totals.supported_outputs,
report.totals.entries - unsupported_non_metadata
);
}
#[test]
fn promptbar_hevc_is_supported_as_original_output() {
let car = Car::new(fixture_path("Assets.car")).expect("load smoke fixture");
let report = car.diagnostics();
let promptbar = report
.entries
.iter()
.filter(|entry| entry.facet_name == "PromptBarBkg")
.collect::<Vec<_>>();
assert_eq!(promptbar.len(), 4);
assert!(promptbar.iter().all(|entry| {
!entry.issues.iter().any(|issue| {
matches!(
issue,
DiagnosticIssue::Unsupported(UnsupportedReason::Compression(
CompressionType::HEVC
))
)
})
}));
}
}