verifyos_cli/rules/
xcode_requirements.rs1use crate::rules::core::{
2 AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6const APP_STORE_CONNECT_REQUIREMENT_DATE: &str = "April 28, 2026";
7const APP_STORE_CONNECT_REQUIREMENT_UNIX: u64 = 1_777_334_400;
8const MIN_XCODE_BUILD_NUMBER: u32 = 2600;
9const MIN_IOS_SDK_MAJOR: u32 = 26;
10
11pub struct XcodeVersionRule;
12
13impl AppStoreRule for XcodeVersionRule {
14 fn id(&self) -> &'static str {
15 "RULE_XCODE_26_MANDATE"
16 }
17
18 fn name(&self) -> &'static str {
19 "Xcode 26 / iOS 26 SDK Mandate"
20 }
21
22 fn category(&self) -> RuleCategory {
23 RuleCategory::Metadata
24 }
25
26 fn severity(&self) -> Severity {
27 Severity::Warning
28 }
29
30 fn recommendation(&self) -> &'static str {
31 "Starting April 28, 2026, iOS and iPadOS apps uploaded to App Store Connect must be built with Xcode 26 and the iOS 26 SDK or later."
32 }
33
34 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
35 let Some(plist) = artifact.info_plist else {
36 return Ok(RuleReport {
37 status: RuleStatus::Skip,
38 message: Some("Info.plist not found".to_string()),
39 evidence: None,
40 });
41 };
42
43 let mut failures = Vec::new();
44
45 match plist.get_string("DTXcode") {
46 Some(version) => match version.parse::<u32>() {
47 Ok(build_number) if build_number >= MIN_XCODE_BUILD_NUMBER => {}
48 Ok(_) => failures.push(format!(
49 "DTXcode={} is below the minimum build number {}",
50 version, MIN_XCODE_BUILD_NUMBER
51 )),
52 Err(_) => failures.push(format!("DTXcode={} is not a valid build number", version)),
53 },
54 None => failures.push("DTXcode key missing".to_string()),
55 }
56
57 match plist.get_string("DTPlatformVersion") {
58 Some(version) => match parse_major_version(version) {
59 Some(major) if major >= MIN_IOS_SDK_MAJOR => {}
60 Some(_) => failures.push(format!(
61 "DTPlatformVersion={} is below {}.0",
62 version, MIN_IOS_SDK_MAJOR
63 )),
64 None => failures.push(format!(
65 "DTPlatformVersion={} is not a valid platform version",
66 version
67 )),
68 },
69 None => failures.push("DTPlatformVersion key missing".to_string()),
70 }
71
72 match plist.get_string("DTSDKName") {
73 Some(name) => match parse_sdk_major_version(name) {
74 Some(major) if major >= MIN_IOS_SDK_MAJOR => {}
75 Some(_) => failures.push(format!(
76 "DTSDKName={} is below iphoneos{}",
77 name, MIN_IOS_SDK_MAJOR
78 )),
79 None => failures.push(format!(
80 "DTSDKName={} does not look like an iPhone OS SDK identifier",
81 name
82 )),
83 },
84 None => failures.push("DTSDKName key missing".to_string()),
85 }
86
87 if failures.is_empty() {
88 return Ok(RuleReport {
89 status: RuleStatus::Pass,
90 message: Some(success_message().to_string()),
91 evidence: None,
92 });
93 }
94
95 Ok(RuleReport {
96 status: RuleStatus::Fail,
97 message: Some(failure_message().to_string()),
98 evidence: Some(failures.join("; ")),
99 })
100 }
101}
102
103fn parse_major_version(value: &str) -> Option<u32> {
104 value.trim().split('.').next()?.parse::<u32>().ok()
105}
106
107fn parse_sdk_major_version(value: &str) -> Option<u32> {
108 let suffix = value.trim().strip_prefix("iphoneos")?;
109 parse_major_version(suffix)
110}
111
112fn requirement_is_live() -> bool {
113 SystemTime::now()
114 .duration_since(UNIX_EPOCH)
115 .map(|duration| duration.as_secs() >= APP_STORE_CONNECT_REQUIREMENT_UNIX)
116 .unwrap_or(true)
117}
118
119fn success_message() -> &'static str {
120 if requirement_is_live() {
121 "App meets the current App Store Connect Xcode 26 / iOS 26 SDK requirement"
122 } else {
123 "App already meets the upcoming App Store Connect Xcode 26 / iOS 26 SDK requirement"
124 }
125}
126
127fn failure_message() -> String {
128 if requirement_is_live() {
129 "App does not meet the current App Store Connect Xcode 26 / iOS 26 SDK requirement"
130 .to_string()
131 } else {
132 format!(
133 "App does not meet the upcoming App Store Connect requirement that takes effect on {APP_STORE_CONNECT_REQUIREMENT_DATE}"
134 )
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::{parse_major_version, parse_sdk_major_version};
141
142 #[test]
143 fn parses_major_platform_version() {
144 assert_eq!(parse_major_version("26.1"), Some(26));
145 assert_eq!(parse_major_version("26"), Some(26));
146 assert_eq!(parse_major_version(""), None);
147 assert_eq!(parse_major_version("abc"), None);
148 }
149
150 #[test]
151 fn parses_sdk_major_version() {
152 assert_eq!(parse_sdk_major_version("iphoneos26.0"), Some(26));
153 assert_eq!(parse_sdk_major_version("iphoneos26"), Some(26));
154 assert_eq!(parse_sdk_major_version("iphonesimulator26.0"), None);
155 assert_eq!(parse_sdk_major_version("iphoneos"), None);
156 }
157}