| 
 | 1 | +//! Utilities for using a dynamically loaded ExecutionPolicy.framework.  | 
 | 2 | +//!  | 
 | 3 | +//! ExecutionPolicy is only available since macOS 10.15, while Rust's  | 
 | 4 | +//! minimum supported version for host tooling is macOS 10.12:  | 
 | 5 | +//! https://doc.rust-lang.org/rustc/platform-support/apple-darwin.html#host-tooling  | 
 | 6 | +//!  | 
 | 7 | +//! For this reason, we must load the framework dynamically instead of linking  | 
 | 8 | +//! it statically - which gets a bit more involved.  | 
 | 9 | +//!  | 
 | 10 | +//! See <https://docs.rs/objc2-execution-policy> for a safer interface that  | 
 | 11 | +//! can be used if support for lower macOS versions are dropped (or once Rust  | 
 | 12 | +//! gains better support for weak linking).  | 
 | 13 | +//!  | 
 | 14 | +//! NOTE: `addPolicyExceptionForURL:error:` probably isn't relevant for us,  | 
 | 15 | +//! that is more used for e.g. allowing running a recently downloaded binary  | 
 | 16 | +//! (and requires that you already have developer tool authorization).  | 
 | 17 | +
  | 
 | 18 | +use std::cell::Cell;  | 
 | 19 | +use std::ffi::{CStr, c_void};  | 
 | 20 | +use std::marker::PhantomData;  | 
 | 21 | +use std::rc::Rc;  | 
 | 22 | + | 
 | 23 | +use anyhow::Context;  | 
 | 24 | +use block2::{DynBlock, RcBlock};  | 
 | 25 | +use objc2::ffi::NSInteger;  | 
 | 26 | +use objc2::rc::Retained;  | 
 | 27 | +use objc2::runtime::{AnyClass, Bool, NSObject};  | 
 | 28 | +use objc2::{available, msg_send};  | 
 | 29 | + | 
 | 30 | +use crate::CargoResult;  | 
 | 31 | + | 
 | 32 | +/// A handle to the dynamically loaded ExecutionPolicy framework.  | 
 | 33 | +#[derive(Debug)]  | 
 | 34 | +pub struct ExecutionPolicyHandle(*mut c_void);  | 
 | 35 | + | 
 | 36 | +impl ExecutionPolicyHandle {  | 
 | 37 | +    /// Dynamically load the ExecutionPolicy framework, and return None if it  | 
 | 38 | +    /// isn't available.  | 
 | 39 | +    pub fn open() -> CargoResult<Option<Self>> {  | 
 | 40 | +        let path = c"/System/Library/Frameworks/ExecutionPolicy.framework/ExecutionPolicy";  | 
 | 41 | + | 
 | 42 | +        let handle = unsafe { libc::dlopen(path.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL) };  | 
 | 43 | + | 
 | 44 | +        if handle.is_null() {  | 
 | 45 | +            // SAFETY: `dlerror` is safe to call.  | 
 | 46 | +            let err = unsafe { libc::dlerror() };  | 
 | 47 | +            let err = if err.is_null() {  | 
 | 48 | +                None  | 
 | 49 | +            } else {  | 
 | 50 | +                // SAFETY: The error is a valid C string.  | 
 | 51 | +                Some(unsafe { CStr::from_ptr(err) })  | 
 | 52 | +            };  | 
 | 53 | + | 
 | 54 | +            // The framework was introduced in macOS 10.15+ / Mac Catalyst 13.0+.  | 
 | 55 | +            if available!(macos = 10.15, ios = 13.0) {  | 
 | 56 | +                Err(anyhow::format_err!(  | 
 | 57 | +                    "failed loading ExecutionPolicy.framework: {err:?}"  | 
 | 58 | +                ))  | 
 | 59 | +            } else {  | 
 | 60 | +                // The framework is not available on macOS 10.14 and below  | 
 | 61 | +                // (which also means that the antivirus doesn't exist yet, so  | 
 | 62 | +                // nothing for us to detect and warn against).  | 
 | 63 | +                Ok(None)  | 
 | 64 | +            }  | 
 | 65 | +        } else {  | 
 | 66 | +            Ok(Some(Self(handle)))  | 
 | 67 | +        }  | 
 | 68 | +    }  | 
 | 69 | +}  | 
 | 70 | + | 
 | 71 | +impl Drop for ExecutionPolicyHandle {  | 
 | 72 | +    fn drop(&mut self) {  | 
 | 73 | +        // SAFETY: The handle is valid.  | 
 | 74 | +        let _ = unsafe { libc::dlclose(self.0) };  | 
 | 75 | +        // Ignore errors when closing. This is also what `libloading` does:  | 
 | 76 | +        // https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374  | 
 | 77 | +    }  | 
 | 78 | +}  | 
 | 79 | + | 
 | 80 | +/// Query the "Developer Tool" status of the environment.  | 
 | 81 | +///  | 
 | 82 | +/// Internally, this calls the system via XPC.  | 
 | 83 | +///  | 
 | 84 | +/// See [`objc2_execution_policy::EPDeveloperTool`] for details.  | 
 | 85 | +///  | 
 | 86 | +/// [`objc2_execution_policy::EPDeveloperTool`]: https://docs.rs/objc2-execution-policy/0.3.1/objc2_execution_policy/struct.EPDeveloperTool.html  | 
 | 87 | +#[derive(Debug)]  | 
 | 88 | +pub struct EPDeveloperTool<'handle> {  | 
 | 89 | +    _handle: PhantomData<&'handle ExecutionPolicyHandle>,  | 
 | 90 | +    obj: Retained<NSObject>,  | 
 | 91 | +}  | 
 | 92 | + | 
 | 93 | +impl<'handle> EPDeveloperTool<'handle> {  | 
 | 94 | +    /// Call `+[EPDeveloperTool new]` to get a new handle.  | 
 | 95 | +    pub fn new(_handle: &'handle ExecutionPolicyHandle) -> CargoResult<Self> {  | 
 | 96 | +        // Dynamically query the class (loading the framework with dlopen  | 
 | 97 | +        // above should have made this available).  | 
 | 98 | +        let cls =  | 
 | 99 | +            AnyClass::get(c"EPDeveloperTool").context("failed finding `EPDeveloperTool` class")?;  | 
 | 100 | + | 
 | 101 | +        // SAFETY: The signature of +[EPDeveloperTool new] is correct and  | 
 | 102 | +        // the method is safe to call.  | 
 | 103 | +        let obj: Option<Retained<NSObject>> = unsafe { msg_send![cls, new] };  | 
 | 104 | + | 
 | 105 | +        // Null can happen in OOM situations, and maybe if failing to connect  | 
 | 106 | +        // via. XPC to the required services.  | 
 | 107 | +        let obj = obj.context("failed allocating and initializing `EPDeveloperTool` instance")?;  | 
 | 108 | + | 
 | 109 | +        let _handle = PhantomData;  | 
 | 110 | +        Ok(Self { _handle, obj })  | 
 | 111 | +    }  | 
 | 112 | + | 
 | 113 | +    /// Call `-[EPDeveloperTool authorizationStatus]`.  | 
 | 114 | +    pub fn authorization_status(&self) -> EPDeveloperToolStatus {  | 
 | 115 | +        // SAFETY: -[EPDeveloperTool authorizationStatus] correctly  | 
 | 116 | +        // returns EPDeveloperToolStatus and the method is safe to call.  | 
 | 117 | +        let status: NSInteger = unsafe { msg_send![&*self.obj, authorizationStatus] };  | 
 | 118 | +        EPDeveloperToolStatus(status)  | 
 | 119 | +    }  | 
 | 120 | + | 
 | 121 | +    /// Call `requestDeveloperToolAccessWithCompletionHandler:` and get the  | 
 | 122 | +    /// result.  | 
 | 123 | +    ////  | 
 | 124 | +    /// This allows the user to more easily see which application needs to be  | 
 | 125 | +    /// allowed (but _is_ also requesting higher privileges, so we need to be  | 
 | 126 | +    /// clear in messaging around that).  | 
 | 127 | +    pub fn request_access(&self) -> CargoResult<bool> {  | 
 | 128 | +        // Wrapper to make the signature easier to write.  | 
 | 129 | +        fn inner(obj: &NSObject, block: &DynBlock<dyn Fn(Bool) + 'static>) {  | 
 | 130 | +            // SAFETY:  | 
 | 131 | +            // - The method is safe to call, and we provide a correctly typed  | 
 | 132 | +            //   block, and constrain the signature to be void / unit return.  | 
 | 133 | +            // - No Send/Sync requirements are needed, because the block is  | 
 | 134 | +            //   not marked @Sendable in Swift.  | 
 | 135 | +            // - The 'static requirement on the block is needed because the  | 
 | 136 | +            //   block is marked as @escaping in Swift. Note that the fact  | 
 | 137 | +            //   that the API is annotated as such is kind of weird, there  | 
 | 138 | +            //   isn't really a way that it could call this block on the  | 
 | 139 | +            //   current thread later (which is what a lone @escaping means).  | 
 | 140 | +            unsafe { msg_send![obj, requestDeveloperToolAccessWithCompletionHandler: block] }  | 
 | 141 | +        }  | 
 | 142 | + | 
 | 143 | +        let result = Rc::new(Cell::new(None));  | 
 | 144 | +        let result_clone = result.clone();  | 
 | 145 | +        let block = RcBlock::new(move |granted: Bool| result_clone.set(Some(granted.as_bool())));  | 
 | 146 | +        inner(&self.obj, &block);  | 
 | 147 | +        result.get().context("failed getting result of -[EPDeveloperTool requestDeveloperToolAccessWithCompletionHandler:]")  | 
 | 148 | +    }  | 
 | 149 | +}  | 
 | 150 | + | 
 | 151 | +/// The Developer Tool status of the process.  | 
 | 152 | +#[repr(transparent)]  | 
 | 153 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]  | 
 | 154 | +pub struct EPDeveloperToolStatus(pub NSInteger);  | 
 | 155 | + | 
 | 156 | +impl EPDeveloperToolStatus {  | 
 | 157 | +    #[doc(alias = "EPDeveloperToolStatusNotDetermined")]  | 
 | 158 | +    pub const NOT_DETERMINED: Self = Self(0);  | 
 | 159 | +    #[doc(alias = "EPDeveloperToolStatusRestricted")]  | 
 | 160 | +    pub const RESTRICTED: Self = Self(1);  | 
 | 161 | +    #[doc(alias = "EPDeveloperToolStatusDenied")]  | 
 | 162 | +    pub const DENIED: Self = Self(2);  | 
 | 163 | +    #[doc(alias = "EPDeveloperToolStatusAuthorized")]  | 
 | 164 | +    pub const AUTHORIZED: Self = Self(3);  | 
 | 165 | +}  | 
 | 166 | + | 
 | 167 | +#[cfg(test)]  | 
 | 168 | +mod tests {  | 
 | 169 | +    use super::*;  | 
 | 170 | + | 
 | 171 | +    #[test]  | 
 | 172 | +    fn does_not_crash() {  | 
 | 173 | +        let Some(handle) = ExecutionPolicyHandle::open().unwrap() else {  | 
 | 174 | +            return;  | 
 | 175 | +        };  | 
 | 176 | + | 
 | 177 | +        let developer_tool = EPDeveloperTool::new(&handle).unwrap();  | 
 | 178 | + | 
 | 179 | +        let _ = developer_tool.authorization_status();  | 
 | 180 | + | 
 | 181 | +        // Test that requesting access doesn't crash either. This might be  | 
 | 182 | +        // slightly annoying for macOS Cargo developers if they _really_ don't  | 
 | 183 | +        // want their terminal to show up in their Developer Tools settings,  | 
 | 184 | +        // but in that case we should probably reconsider this feature.  | 
 | 185 | +        let _ = developer_tool.request_access().unwrap();  | 
 | 186 | +    }  | 
 | 187 | +}  | 
0 commit comments