diff --git a/src/tz_macos.rs b/src/tz_macos.rs index 1e00846..59b92f2 100644 --- a/src/tz_macos.rs +++ b/src/tz_macos.rs @@ -1,80 +1,144 @@ -use core_foundation_sys::base::{Boolean, CFRange, CFRelease, CFTypeRef}; -use core_foundation_sys::string::{ - kCFStringEncodingUTF8, CFStringGetBytes, CFStringGetCStringPtr, CFStringGetLength, -}; -use core_foundation_sys::timezone::{CFTimeZoneCopySystem, CFTimeZoneGetName}; - pub(crate) fn get_timezone_inner() -> Result { - unsafe { get_timezone().ok_or(crate::GetTimezoneError::OsError) } + get_timezone().ok_or(crate::GetTimezoneError::OsError) } #[inline] -unsafe fn get_timezone() -> Option { +fn get_timezone() -> Option { // The longest name in the IANA time zone database is 25 ASCII characters long. const MAX_LEN: usize = 32; + let mut buf = [0; MAX_LEN]; // Get system time zone, and borrow its name. - let tz = Dropping::new(CFTimeZoneCopySystem())?; - let name = CFTimeZoneGetName(tz.0); - if name.is_null() { - return None; - } + let tz = system_time_zone::SystemTimeZone::new()?; + let name = tz.name()?; // If the name is encoded in UTF-8, copy it directly. - let cstr = CFStringGetCStringPtr(name, kCFStringEncodingUTF8); - if !cstr.is_null() { - let cstr = std::ffi::CStr::from_ptr(cstr); - if let Ok(name) = cstr.to_str() { - return Some(name.to_owned()); - } - } - - // Otherwise convert the name to UTF-8. - let mut buf = [0; MAX_LEN]; - let mut buf_bytes = 0; - let range = CFRange { - location: 0, - length: CFStringGetLength(name), + let name = if let Some(name) = name.as_utf8() { + name + } else { + // Otherwise convert the name to UTF-8. + name.to_utf8(&mut buf)? }; - if CFStringGetBytes( - name, - range, - kCFStringEncodingUTF8, - b'\0', - false as Boolean, - buf.as_mut_ptr(), - buf.len() as isize, - &mut buf_bytes, - ) != range.length - { - // Could not convert the name. - None - } else if buf_bytes < 1 || buf_bytes >= MAX_LEN as isize { + + if name.is_empty() || name.len() >= MAX_LEN { // The name should not be empty, or excessively long. None } else { - // Convert the name to a `String`. - let name = core::str::from_utf8(&buf[..buf_bytes as usize]).ok()?; Some(name.to_owned()) } } -struct Dropping(*const T); +mod system_time_zone { + //! create a safe wrapper around `CFTimeZoneRef` + + use core_foundation_sys::base::{CFRelease, CFTypeRef}; + use core_foundation_sys::timezone::{CFTimeZoneCopySystem, CFTimeZoneGetName, CFTimeZoneRef}; + + pub(crate) struct SystemTimeZone(CFTimeZoneRef); -impl Drop for Dropping { - #[inline] - fn drop(&mut self) { - unsafe { CFRelease(self.0 as CFTypeRef) }; + impl Drop for SystemTimeZone { + fn drop(&mut self) { + // SAFETY: `SystemTimeZone` is only ever created with a valid `CFTimeZoneRef`. + unsafe { CFRelease(self.0 as CFTypeRef) }; + } + } + + impl SystemTimeZone { + pub(crate) fn new() -> Option { + // SAFETY: No invariants to uphold. We'll release the pointer when we don't need it anymore. + let v: CFTimeZoneRef = unsafe { CFTimeZoneCopySystem() }; + if v.is_null() { + None + } else { + Some(SystemTimeZone(v)) + } + } + + /// Get the time zone name as a [super::string_ref::StringRef]. + /// + /// The lifetime of the `StringRef` is bound to our lifetime. Mutable + /// access is also prevented by taking a reference to `self`. + pub(crate) fn name(&self) -> Option> { + // SAFETY: `SystemTimeZone` is only ever created with a valid `CFTimeZoneRef`. + let string = unsafe { CFTimeZoneGetName(self.0) }; + if string.is_null() { + None + } else { + // SAFETY: here we ensure that `string` is a valid pointer. + Some(unsafe { super::string_ref::StringRef::new(string, self) }) + } + } } } -impl Dropping { - #[inline] - unsafe fn new(v: *const T) -> Option { - if v.is_null() { +mod string_ref { + //! create safe wrapper around `CFStringRef` + + use std::convert::TryInto; + + use core_foundation_sys::base::{Boolean, CFRange}; + use core_foundation_sys::string::{ + kCFStringEncodingUTF8, CFStringGetBytes, CFStringGetCStringPtr, CFStringGetLength, + CFStringRef, + }; + + pub(crate) struct StringRef<'a, T> { + string: CFStringRef, + // We exclude mutable access to the parent by taking a reference to the + // parent (rather than, for example, just using a marker to enforce the + // parent's lifetime). + _parent: &'a T, + } + + impl<'a, T> StringRef<'a, T> { + // SAFETY: `StringRef` must be valid pointer + pub(crate) unsafe fn new(string: CFStringRef, _parent: &'a T) -> Self { + Self { string, _parent } + } + + pub(crate) fn as_utf8(&self) -> Option<&'a str> { + // SAFETY: `StringRef` is only ever created with a valid `CFStringRef`. + let v = unsafe { CFStringGetCStringPtr(self.string, kCFStringEncodingUTF8) }; + if !v.is_null() { + // SAFETY: `CFStringGetCStringPtr()` returns NUL-terminated strings. + let v = unsafe { std::ffi::CStr::from_ptr(v) }; + if let Ok(v) = v.to_str() { + return Some(v); + } + } None - } else { - Some(Dropping(v)) + } + + pub(crate) fn to_utf8<'b>(&self, buf: &'b mut [u8]) -> Option<&'b str> { + // SAFETY: `StringRef` is only ever created with a valid `CFStringRef`. + let length = unsafe { CFStringGetLength(self.string) }; + + let mut buf_bytes = 0; + let range = CFRange { + location: 0, + length, + }; + + let converted_bytes = unsafe { + // SAFETY: `StringRef` is only ever created with a valid `CFStringRef`. + CFStringGetBytes( + self.string, + range, + kCFStringEncodingUTF8, + b'\0', + false as Boolean, + buf.as_mut_ptr(), + buf.len() as isize, + &mut buf_bytes, + ) + }; + if converted_bytes != length { + return None; + } + + let len = buf_bytes.try_into().ok()?; + let s = buf.get(..len)?; + std::str::from_utf8(s).ok() } } }