Skip to main content

verifyos_cli/rules/
app_icon.rs

1use crate::rules::core::{
2    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4use std::path::Path;
5
6pub struct AppIconAlphaRule;
7
8impl AppStoreRule for AppIconAlphaRule {
9    fn id(&self) -> &'static str {
10        "RULE_APP_ICON_ALPHA"
11    }
12
13    fn name(&self) -> &'static str {
14        "App Icon Alpha Channel Check"
15    }
16
17    fn category(&self) -> RuleCategory {
18        RuleCategory::Metadata
19    }
20
21    fn severity(&self) -> Severity {
22        Severity::Error
23    }
24
25    fn recommendation(&self) -> &'static str {
26        "Remove the alpha channel from your app icon. App Store icons must be opaque."
27    }
28
29    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
30        let Some(plist) = artifact.info_plist else {
31            return Ok(RuleReport {
32                status: RuleStatus::Skip,
33                message: Some("Info.plist not found".to_string()),
34                evidence: None,
35            });
36        };
37
38        let icon_names = plist.get_app_icons();
39        if icon_names.is_empty() {
40            return Ok(RuleReport {
41                status: RuleStatus::Skip,
42                message: Some("No app icons found in Info.plist".to_string()),
43                evidence: None,
44            });
45        }
46
47        let mut alpha_icons = Vec::new();
48        let all_files = artifact.bundle_file_paths();
49
50        for name in icon_names {
51            // App Store icons are usually the largest PNGs.
52            // We search for files matching the name patterns.
53            for file_path in &all_files {
54                let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
55                if file_name.starts_with(&name) && file_name.ends_with(".png") {
56                    if let Ok(has_alpha) = check_png_alpha(file_path) {
57                        if has_alpha {
58                            alpha_icons.push(file_name.to_string());
59                        }
60                    }
61                }
62            }
63        }
64
65        if alpha_icons.is_empty() {
66            return Ok(RuleReport {
67                status: RuleStatus::Pass,
68                message: Some("No alpha channel detected in app icons".to_string()),
69                evidence: None,
70            });
71        }
72
73        alpha_icons.sort();
74        alpha_icons.dedup();
75
76        Ok(RuleReport {
77            status: RuleStatus::Fail,
78            message: Some("App icons contain alpha channel (transparency)".to_string()),
79            evidence: Some(format!("Icons with alpha: {}", alpha_icons.join(", "))),
80        })
81    }
82}
83
84fn check_png_alpha(path: &Path) -> std::io::Result<bool> {
85    let bytes = std::fs::read(path)?;
86    if bytes.len() < 26 {
87        return Ok(false);
88    }
89
90    // Check PNG signature
91    if &bytes[0..8] != b"\x89PNG\r\n\x1a\n" {
92        return Ok(false);
93    }
94
95    // Check IHDR chunk
96    if &bytes[12..16] != b"IHDR" {
97        return Ok(false);
98    }
99
100    // Color type is at index 25
101    let color_type = bytes[25];
102    match color_type {
103        4 | 6 => Ok(true), // Grayscale+Alpha or RGB+Alpha
104        3 => {
105            // Indexed color - check for tRNS chunk
106            Ok(bytes.windows(4).any(|w| w == b"tRNS"))
107        }
108        _ => Ok(false),
109    }
110}