//! Utilities for checking the runtime availability of APIs. //! //! TODO: Upstream some of this to `std`? use core::cmp::Ordering; use core::fmt; #[cfg(target_vendor = "apple")] mod apple; /// The size of the fields here are limited by Mach-O's `LC_BUILD_VERSION`. #[repr(C)] #[derive(Clone, Copy)] pub struct OSVersion { // Shuffle the versions around a little so that OSVersion has the same bit // representation as the `u32` returned from `to_u32`, allowing // comparisons to compile down to just between two `u32`s. #[cfg(target_endian = "little")] pub patch: u8, #[cfg(target_endian = "little")] pub minor: u8, #[cfg(target_endian = "little")] pub major: u16, #[cfg(target_endian = "big")] pub major: u16, #[cfg(target_endian = "big")] pub minor: u8, #[cfg(target_endian = "big")] pub patch: u8, } #[track_caller] const fn parse_usize(mut bytes: &[u8]) -> (usize, &[u8]) { // Ensure we have at least one digit (that is not just a period). let mut ret: usize = if let Some((&ascii, rest)) = bytes.split_first() { bytes = rest; match ascii { b'0'..=b'9' => (ascii - b'0') as usize, _ => panic!("found invalid digit when parsing version"), } } else { panic!("found empty version number part") }; // Parse the remaining digits. while let Some((&ascii, rest)) = bytes.split_first() { let digit = match ascii { b'0'..=b'9' => ascii - b'0', _ => break, }; bytes = rest; // This handles leading zeroes as well. match ret.checked_mul(10) { Some(val) => match val.checked_add(digit as _) { Some(val) => ret = val, None => panic!("version is too large"), }, None => panic!("version is too large"), }; } (ret, bytes) } impl OSVersion { const MIN: Self = Self { major: 0, minor: 0, patch: 0, }; const MAX: Self = Self { major: u16::MAX, minor: u8::MAX, patch: u8::MAX, }; /// Parse the version from a string at `const` time. #[track_caller] pub const fn from_str(version: &str) -> Self { Self::from_bytes(version.as_bytes()) } #[track_caller] pub(crate) const fn from_bytes(bytes: &[u8]) -> Self { let (major, bytes) = parse_usize(bytes); if major > u16::MAX as usize { panic!("major version is too large"); } let major = major as u16; let bytes = if let Some((period, bytes)) = bytes.split_first() { if *period != b'.' { panic!("expected period between major and minor version") } bytes } else { return Self { major, minor: 0, patch: 0, }; }; let (minor, bytes) = parse_usize(bytes); if minor > u8::MAX as usize { panic!("minor version is too large"); } let minor = minor as u8; let bytes = if let Some((period, bytes)) = bytes.split_first() { if *period != b'.' { panic!("expected period after minor version") } bytes } else { return Self { major, minor, patch: 0, }; }; let (patch, bytes) = parse_usize(bytes); if patch > u8::MAX as usize { panic!("patch version is too large"); } let patch = patch as u8; if !bytes.is_empty() { panic!("too many parts to version"); } Self { major, minor, patch, } } /// Pack the version into a `u32`. /// /// This is used for faster comparisons. #[inline] pub const fn to_u32(self) -> u32 { // See comments in `OSVersion`, this should compile down to nothing. let (major, minor, patch) = (self.major as u32, self.minor as u32, self.patch as u32); (major << 16) | (minor << 8) | patch } /// Construct the version from a `u32`. #[inline] pub const fn from_u32(version: u32) -> Self { // See comments in `OSVersion`, this should compile down to nothing. let major = (version >> 16) as u16; let minor = (version >> 8) as u8; let patch = version as u8; Self { major, minor, patch, } } } impl PartialEq for OSVersion { #[inline] fn eq(&self, other: &Self) -> bool { self.to_u32() == other.to_u32() } } impl PartialOrd for OSVersion { #[inline] fn partial_cmp(&self, other: &Self) -> Option { self.to_u32().partial_cmp(&other.to_u32()) } } impl fmt::Debug for OSVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Same ordering on little and big endian. f.debug_struct("OSVersion") .field("major", &self.major) .field("minor", &self.minor) .field("patch", &self.patch) .finish() } } /// The combined availability. /// /// This generally works closely together with the `available!` macro to make /// syntax checking inside that easier. #[derive(Clone, Copy, Debug)] pub struct AvailableVersion { pub macos: OSVersion, pub ios: OSVersion, pub tvos: OSVersion, pub watchos: OSVersion, pub visionos: OSVersion, #[doc(hidden)] pub __others: OSVersion, } impl AvailableVersion { pub const MIN: Self = Self { macos: OSVersion::MIN, ios: OSVersion::MIN, tvos: OSVersion::MIN, watchos: OSVersion::MIN, visionos: OSVersion::MIN, __others: OSVersion::MIN, }; pub const MAX: Self = Self { macos: OSVersion::MAX, ios: OSVersion::MAX, tvos: OSVersion::MAX, watchos: OSVersion::MAX, visionos: OSVersion::MAX, __others: OSVersion::MAX, }; } #[inline] pub fn is_available(version: AvailableVersion) -> bool { let version = if cfg!(target_os = "macos") { version.macos } else if cfg!(target_os = "ios") { version.ios } else if cfg!(target_os = "tvos") { version.tvos } else if cfg!(target_os = "watchos") { version.watchos } else if cfg!(target_os = "visionos") { version.visionos } else { version.__others }; // In the special case that `version` was set to `OSVersion::MAX`, we // assume that there can never be an OS version that large, and hence we // want to avoid checking at all. // // This is useful for platforms where the version hasn't been specified. if version == OSVersion::MAX { return false; } #[cfg(target_vendor = "apple")] { // If the deployment target is high enough, the API is always available. // // This check should be optimized away at compile time. if version <= apple::DEPLOYMENT_TARGET { return true; } // Otherwise, compare against the version at runtime. version <= apple::current_version() } #[cfg(not(target_vendor = "apple"))] return true; } #[cfg(test)] mod tests { use super::*; use crate::{__available_version, available}; #[test] fn test_parse() { #[track_caller] fn check(expected: (u16, u8, u8), actual: OSVersion) { assert_eq!( OSVersion { major: expected.0, minor: expected.1, patch: expected.2, }, actual, ) } check((1, 0, 0), __available_version!(1)); check((1, 2, 0), __available_version!(1.2)); check((1, 2, 3), __available_version!(1.2.3)); check((9999, 99, 99), __available_version!(9999.99.99)); // Ensure that the macro handles leading zeroes correctly check((10, 0, 0), __available_version!(010)); check((10, 20, 0), __available_version!(010.020)); check((10, 20, 30), __available_version!(010.020.030)); check( (10000, 100, 100), __available_version!(000010000.00100.00100), ); } #[test] fn test_compare() { #[track_caller] fn check_lt(expected: (u16, u8, u8), actual: (u16, u8, u8)) { assert!( OSVersion { major: expected.0, minor: expected.1, patch: expected.2, } < OSVersion { major: actual.0, minor: actual.1, patch: actual.2, }, ) } check_lt((4, 99, 99), (5, 5, 5)); check_lt((5, 4, 99), (5, 5, 5)); check_lt((5, 5, 4), (5, 5, 5)); check_lt((10, 7, 0), (10, 10, 0)); } #[test] #[should_panic = "too many parts to version"] fn test_too_many_version_parts() { let _ = __available_version!(1.2.3 .4); } #[test] #[should_panic = "found invalid digit when parsing version"] fn test_macro_with_identifiers() { let _ = __available_version!(A.B); } #[test] #[should_panic = "found empty version number part"] fn test_empty_version() { let _ = __available_version!(); } #[test] #[should_panic = "found invalid digit when parsing version"] fn test_only_period() { let _ = __available_version!(.); } #[test] #[should_panic = "found invalid digit when parsing version"] fn test_has_leading_period() { let _ = __available_version!(.1); } #[test] #[should_panic = "found empty version number part"] fn test_has_trailing_period() { let _ = __available_version!(1.); } #[test] #[should_panic = "major version is too large"] fn test_major_too_large() { let _ = __available_version!(100000); } #[test] #[should_panic = "minor version is too large"] fn test_minor_too_large() { let _ = __available_version!(1.1000); } #[test] #[should_panic = "patch version is too large"] fn test_patch_too_large() { let _ = __available_version!(1.1.1000); } #[test] fn test_general_available() { // Always available assert!(available!(..)); // Never available assert!(!available!()); // Low versions, always available assert!(available!( macos = 10.0, ios = 1.0, tvos = 1.0, watchos = 1.0, visionos = 1.0, .. )); // High versions, never available assert!(!available!( macos = 99, ios = 99, tvos = 99, watchos = 99, visionos = 99 )); if !cfg!(target_os = "tvos") { // Available nowhere except tvOS assert!(!available!(tvos = 1.2)); // Available everywhere, except low tvOS versions assert!(available!(tvos = 1.2, ..)); } } #[test] fn test_u32_roundtrip() { let version = OSVersion { major: 1000, minor: 100, patch: 200, }; assert_eq!(version, OSVersion::from_u32(version.to_u32())); } }