├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── client │ ├── Cargo.toml │ └── src │ │ └── main.rs └── echo-server │ ├── Cargo.toml │ ├── Makefile │ ├── com.example.echo.plist │ └── src │ └── main.rs ├── xpc-connection-sys ├── Cargo.toml ├── build.rs ├── src │ └── lib.rs └── wrapper.h └── xpc-connection ├── Cargo.toml ├── src ├── lib.rs └── message.rs └── tests └── message_round_trip.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: master 7 | 8 | jobs: 9 | test: 10 | name: Test Suite 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ macos-11, macos-12, macos-13 ] 15 | rust: [ nightly, stable ] 16 | 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v2 20 | 21 | - name: Install the toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: ${{ matrix.rust }} 26 | override: true 27 | 28 | - name: Build and run the echo server 29 | run: cd examples/echo-server && make && make install 30 | 31 | - name: Run cargo test 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: test 35 | args: -- --ignored 36 | 37 | lints: 38 | name: Lints 39 | runs-on: macos-latest 40 | steps: 41 | - name: Checkout sources 42 | uses: actions/checkout@v2 43 | 44 | - name: Install the stable toolchain 45 | uses: actions-rs/toolchain@v1 46 | with: 47 | profile: minimal 48 | toolchain: stable 49 | override: true 50 | components: rustfmt, clippy 51 | 52 | - name: Run cargo fmt 53 | uses: actions-rs/cargo@v1 54 | continue-on-error: true 55 | with: 56 | command: fmt 57 | args: --all -- --check 58 | 59 | - name: Run cargo clippy 60 | uses: actions-rs/cargo@v1 61 | continue-on-error: true 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /examples/echo-server/com.example.echo 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "examples/client", 4 | "examples/echo-server", 5 | "xpc-connection-sys", 6 | "xpc-connection" 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dylan Frankland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xpc-connection-rs 2 | 3 | [![shield]][crate] 4 | [![sys shield]][sys crate] 5 | 6 | [crate]: https://crates.io/crates/xpc-connection 7 | [shield]: https://img.shields.io/crates/v/xpc-connection?label=xpc-connection 8 | [sys crate]: https://crates.io/crates/xpc-connection-sys 9 | [sys shield]: https://img.shields.io/crates/v/xpc-connection-sys?label=xpc-connection-sys 10 | 11 | XPC connection bindings for Rust. 12 | 13 | ## What is XPC? 14 | 15 | A low-level (libSystem) interprocess communication mechanism that is based on 16 | serialized property lists for Mac OS. Read more at the 17 | [Apple Developer website][apple developer]. 18 | 19 | [apple developer]: https://developer.apple.com/documentation/xpc 20 | 21 | ## Features 22 | 23 | * `audit_token` enables retrieving the client's audit token. This requires 24 | using a private API, but it's the simplest way to securely validate clients. 25 | See [CVE-2020-0984](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0984) 26 | and [this useful blog post](https://theevilbit.github.io/posts/secure_coding_xpc_part2/). 27 | The [example echo server](examples/echo-server/src/main.rs) makes use of this. 28 | 29 | ## Supported Data Types 30 | 31 | * `array`: `Vec` 32 | * `bool`: `bool` 33 | * `data`: `Vec` 34 | * `date`: `SystemTime` 35 | * `dictionary`: `HashMap` 36 | * `double`: `f64` 37 | * `error`: `MessageError` 38 | * `fd`: `RawFd` 39 | * `int64`: `i64` 40 | * `string`: `String` 41 | * `uint64`: `u64` 42 | * `uuid`: `Vec` 43 | * `null` 44 | 45 | ## Yet to Be Supported Data Types 46 | 47 | * `activity` 48 | * `endpoint` 49 | * `shmem` 50 | -------------------------------------------------------------------------------- /examples/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | authors = ["Steven Joruk "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | futures = { version = "0.3" } 10 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 11 | xpc-connection = { path = "../../xpc-connection" } 12 | -------------------------------------------------------------------------------- /examples/client/src/main.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::StreamExt; 2 | use std::{collections::HashMap, error::Error, ffi::CString}; 3 | use xpc_connection::{Message, XpcClient}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<(), Box> { 7 | let mach_port_name = CString::new("com.example.echo")?; 8 | 9 | println!("Attempting to connect to {:?}", mach_port_name); 10 | let mut client = XpcClient::connect(&mach_port_name); 11 | 12 | let mut dictionary = HashMap::new(); 13 | dictionary.insert(CString::new("hello")?, Message::Int64(2)); 14 | 15 | println!("Sending a message"); 16 | client.send_message(Message::Dictionary(dictionary)); 17 | 18 | if let Some(message) = client.next().await { 19 | println!("Client received message {:?}", message); 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /examples/echo-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "echo-server" 3 | version = "0.1.0" 4 | authors = ["Steven Joruk "] 5 | edition = "2018" 6 | publish = false 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | core-foundation = "0.9" 12 | futures = { version = "0.3" } 13 | # Support for SecCode was added in 2.3.1 14 | security-framework = "^2.3.1" 15 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 16 | xpc-connection = { path = "../../xpc-connection", features = ["audit_token"] } 17 | -------------------------------------------------------------------------------- /examples/echo-server/Makefile: -------------------------------------------------------------------------------- 1 | all: ../../target/debug/echo-server 2 | 3 | ../../target/debug/echo-server: 4 | cargo build 5 | 6 | install: ../../target/debug/echo-server 7 | # GitHub's runners don't have this directory by default. 8 | sudo mkdir -p /Library/PrivilegedHelperTools/ 9 | sudo cp ../../target/debug/echo-server /Library/PrivilegedHelperTools/com.example.echo 10 | sudo cp com.example.echo.plist /Library/LaunchDaemons/ 11 | sudo launchctl load /Library/LaunchDaemons/com.example.echo.plist 12 | 13 | uninstall: 14 | sudo launchctl unload /Library/LaunchDaemons/com.example.echo.plist 15 | sudo rm /Library/PrivilegedHelperTools/com.example.echo 16 | sudo rm /Library/LaunchDaemons/com.example.echo.plist 17 | -------------------------------------------------------------------------------- /examples/echo-server/com.example.echo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.example.echo 7 | MachServices 8 | 9 | com.example.echo 10 | 11 | 12 | Program 13 | /Library/PrivilegedHelperTools/com.example.echo 14 | StandardOutPath 15 | /var/log/com.example.echo.log 16 | StandardErrorPath 17 | /var/log/com.example.echo.log 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/echo-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use core_foundation::{base::TCFType, data::CFData}; 2 | use futures::stream::StreamExt; 3 | use security_framework::os::macos::code_signing::{Flags, GuestAttributes, SecCode}; 4 | use std::{error::Error, ffi::CString}; 5 | use xpc_connection::{Message, MessageError, XpcClient, XpcListener}; 6 | 7 | fn get_code_object_for_client(client: &XpcClient) -> SecCode { 8 | let token_data = CFData::from_buffer(&client.audit_token()); 9 | let mut attrs = GuestAttributes::new(); 10 | attrs.set_audit_token(token_data.as_concrete_TypeRef()); 11 | SecCode::copy_guest_with_attribues(None, &attrs, Flags::NONE).unwrap() 12 | } 13 | 14 | #[allow(dead_code)] 15 | /// This isn't used because we don't sign our builds, but it's a useful example. 16 | fn validate_client_by_code_signing_requirement(client: &XpcClient) -> bool { 17 | let requirement = "anchor apple".parse().unwrap(); 18 | 19 | if get_code_object_for_client(client) 20 | .check_validity(Flags::NONE, &requirement) 21 | .is_ok() 22 | { 23 | println!("The client's code signature matches"); 24 | return true; 25 | } 26 | 27 | println!("The client's code signature doesn't match"); 28 | false 29 | } 30 | 31 | fn validate_client_by_path(client: &XpcClient) -> bool { 32 | if get_code_object_for_client(client) 33 | .path(Flags::NONE) 34 | .unwrap() 35 | // It'd be better to use to_path 36 | .get_string() 37 | .to_string() 38 | // This is insecure, it's just so the tests can be run from anywhere 39 | .contains("message_round_trip") 40 | { 41 | println!("The client was validated using its path"); 42 | return true; 43 | } 44 | 45 | println!("The client's path doesn't contain 'message_round_trip'"); 46 | false 47 | } 48 | 49 | async fn handle_client(mut client: XpcClient) { 50 | println!("New connection"); 51 | 52 | if !validate_client_by_path(&client) { 53 | return; 54 | } 55 | 56 | loop { 57 | match client.next().await { 58 | None => { 59 | break; 60 | } 61 | Some(Message::Error(MessageError::ConnectionInterrupted)) => { 62 | println!("The connection was interrupted."); 63 | } 64 | Some(m) => { 65 | println!("Server received {:?}", m); 66 | client.send_message(m); 67 | } 68 | } 69 | } 70 | 71 | println!("The connection was invalidated."); 72 | } 73 | 74 | #[tokio::main(flavor = "multi_thread")] 75 | async fn main() -> Result<(), Box> { 76 | let mach_port_name = CString::new("com.example.echo")?; 77 | 78 | println!( 79 | "Waiting for new connections on {:?}", 80 | mach_port_name.to_string_lossy() 81 | ); 82 | 83 | let mut listener = XpcListener::listen(&mach_port_name); 84 | 85 | while let Some(client) = listener.next().await { 86 | tokio::spawn(handle_client(client)); 87 | } 88 | 89 | println!("Server is shutting down"); 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /xpc-connection-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Dylan Frankland "] 3 | name = "xpc-connection-sys" 4 | version = "0.1.1" 5 | edition = "2018" 6 | license = "MIT" 7 | description = "XPC connection bindings for Rust" 8 | homepage = "https://github.com/dfrankland/xpc-connection-rs" 9 | repository = "https://github.com/dfrankland/xpc-connection-rs" 10 | keywords = ["xpc", "mac", "macOS"] 11 | categories = ["os", "api-bindings", "concurrency", "encoding"] 12 | 13 | [build-dependencies] 14 | bindgen = "0.58.1" 15 | 16 | [dependencies] 17 | -------------------------------------------------------------------------------- /xpc-connection-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 6 | 7 | bindgen::Builder::default() 8 | .header("wrapper.h") 9 | .rustfmt_bindings(true) 10 | .allowlist_function("dispatch_queue_create") 11 | .allowlist_function("xpc.*") 12 | .allowlist_var("xpc.*") 13 | .allowlist_var("_xpc.*") 14 | .allowlist_var("XPC.*") 15 | .allowlist_type("uuid_t") 16 | .generate() 17 | .expect("Unable to generate bindings") 18 | .write_to_file(out_path.join("bindings.rs")) 19 | .expect("Couldn't write bindings!"); 20 | } 21 | -------------------------------------------------------------------------------- /xpc-connection-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | dead_code, 3 | safe_packed_borrows, 4 | non_upper_case_globals, 5 | non_camel_case_types, 6 | non_snake_case, 7 | clippy::all 8 | )] 9 | 10 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 11 | -------------------------------------------------------------------------------- /xpc-connection-sys/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /xpc-connection/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Dylan Frankland "] 3 | name = "xpc-connection" 4 | version = "0.2.3" 5 | edition = "2018" 6 | license = "MIT" 7 | description = "XPC connection bindings for Rust" 8 | homepage = "https://github.com/dfrankland/xpc-connection-rs" 9 | repository = "https://github.com/dfrankland/xpc-connection-rs" 10 | keywords = ["xpc", "mac", "macOS"] 11 | categories = ["os", "api-bindings", "concurrency", "encoding"] 12 | 13 | [features] 14 | audit_token = [] 15 | default = [] 16 | 17 | [dependencies] 18 | block = "0.1.6" 19 | core-foundation = { version = "0.9", optional = true } 20 | futures = "0.3.4" 21 | xpc-connection-sys = { path = "../xpc-connection-sys", version = "0.1.0" } 22 | 23 | [dev-dependencies] 24 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 25 | -------------------------------------------------------------------------------- /xpc-connection/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[allow( 2 | dead_code, 3 | safe_packed_borrows, 4 | non_upper_case_globals, 5 | non_camel_case_types, 6 | non_snake_case, 7 | clippy::all 8 | )] 9 | extern crate xpc_connection_sys; 10 | 11 | mod message; 12 | pub use message::*; 13 | 14 | use block::ConcreteBlock; 15 | use futures::{ 16 | channel::mpsc::{unbounded as unbounded_channel, UnboundedReceiver, UnboundedSender}, 17 | Stream, 18 | }; 19 | use std::ffi::CStr; 20 | use std::{ffi::c_void, ops::Deref}; 21 | use std::{pin::Pin, task::Poll}; 22 | use xpc_connection_sys::{ 23 | xpc_connection_cancel, xpc_connection_create_mach_service, xpc_connection_resume, 24 | xpc_connection_send_message, xpc_connection_set_event_handler, xpc_connection_t, xpc_object_t, 25 | xpc_release, XPC_CONNECTION_MACH_SERVICE_LISTENER, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED, 26 | }; 27 | 28 | // A connection's event handler could still be waiting or running when we want 29 | // to drop a connection. We must cancel the handler and wait for the final 30 | // call to a handler to occur, which is always a message containing an 31 | // invalidation error. 32 | fn cancel_and_wait_for_event_handler(connection: xpc_connection_t) { 33 | let (tx, rx) = std::sync::mpsc::channel(); 34 | 35 | let block = ConcreteBlock::new(move |_: xpc_object_t| { 36 | tx.send(()) 37 | .expect("Failed to announce that the xpc connection's event handler has exited"); 38 | }); 39 | 40 | // We must move it from the stack to the heap so that when the libxpc 41 | // reference count is released we don't double free. This limitation is 42 | // explained in the blocks crate. 43 | let block = block.copy(); 44 | 45 | unsafe { 46 | xpc_connection_set_event_handler(connection, block.deref() as *const _ as *mut _); 47 | 48 | xpc_connection_cancel(connection); 49 | } 50 | 51 | rx.recv() 52 | .expect("Failed to wait for the xpc connection's event handler to exit"); 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct XpcListener { 57 | connection: xpc_connection_t, 58 | receiver: UnboundedReceiver, 59 | sender: UnboundedSender, 60 | } 61 | 62 | impl PartialEq for XpcListener { 63 | fn eq(&self, other: &Self) -> bool { 64 | std::ptr::eq(self.connection, other.connection) 65 | } 66 | } 67 | 68 | impl Drop for XpcListener { 69 | fn drop(&mut self) { 70 | unsafe { 71 | cancel_and_wait_for_event_handler(self.connection); 72 | xpc_release(self.connection as xpc_object_t); 73 | } 74 | } 75 | } 76 | 77 | impl Stream for XpcListener { 78 | type Item = XpcClient; 79 | 80 | fn poll_next( 81 | mut self: Pin<&mut Self>, 82 | cx: &mut std::task::Context<'_>, 83 | ) -> Poll> { 84 | Stream::poll_next(Pin::new(&mut self.receiver), cx) 85 | } 86 | } 87 | 88 | impl XpcListener { 89 | /// The connection must be a listener. 90 | fn from_raw(connection: xpc_connection_t) -> XpcListener { 91 | let (sender, receiver) = unbounded_channel(); 92 | let sender_clone = sender.clone(); 93 | 94 | let block = ConcreteBlock::new(move |event| match xpc_object_to_message(event) { 95 | Message::Client(client) => sender_clone.unbounded_send(client).ok(), 96 | _ => None, 97 | }); 98 | 99 | // We must move it from the stack to the heap so that when the libxpc 100 | // reference count is released we don't double free. This limitation is 101 | // explained in the blocks crate. 102 | let block = block.copy(); 103 | 104 | unsafe { 105 | xpc_connection_set_event_handler(connection, block.deref() as *const _ as *mut _); 106 | xpc_connection_resume(connection); 107 | } 108 | 109 | XpcListener { 110 | connection, 111 | receiver, 112 | sender, 113 | } 114 | } 115 | 116 | pub fn listen(name: impl AsRef) -> Self { 117 | let name = name.as_ref(); 118 | let flags = XPC_CONNECTION_MACH_SERVICE_LISTENER as u64; 119 | let connection = unsafe { 120 | xpc_connection_create_mach_service(name.as_ref().as_ptr(), std::ptr::null_mut(), flags) 121 | }; 122 | Self::from_raw(connection) 123 | } 124 | } 125 | 126 | #[derive(Debug)] 127 | pub struct XpcClient { 128 | connection: xpc_connection_t, 129 | event_handler_is_running: bool, 130 | receiver: UnboundedReceiver, 131 | sender: UnboundedSender, 132 | } 133 | 134 | unsafe impl Send for XpcClient {} 135 | 136 | impl PartialEq for XpcClient { 137 | fn eq(&self, other: &Self) -> bool { 138 | std::ptr::eq(self.connection, other.connection) 139 | } 140 | } 141 | 142 | impl Drop for XpcClient { 143 | fn drop(&mut self) { 144 | if self.event_handler_is_running { 145 | cancel_and_wait_for_event_handler(self.connection); 146 | } 147 | 148 | unsafe { xpc_release(self.connection as xpc_object_t) }; 149 | } 150 | } 151 | 152 | impl Stream for XpcClient { 153 | type Item = Message; 154 | 155 | /// `Poll::Ready(None)` returned in place of `MessageError::ConnectionInvalid` 156 | /// as it's not recoverable. `MessageError::ConnectionInterrupted` should 157 | /// be treated as recoverable. 158 | fn poll_next( 159 | mut self: Pin<&mut Self>, 160 | cx: &mut std::task::Context<'_>, 161 | ) -> Poll> { 162 | match Stream::poll_next(Pin::new(&mut self.receiver), cx) { 163 | Poll::Ready(Some(Message::Error(MessageError::ConnectionInvalid))) => { 164 | self.event_handler_is_running = false; 165 | Poll::Ready(None) 166 | } 167 | v => v, 168 | } 169 | } 170 | } 171 | 172 | impl XpcClient { 173 | /// This sets up a client connection's event handler so that its `Stream` 174 | /// implementation can be used. 175 | fn from_raw(connection: xpc_connection_t) -> Self { 176 | let (sender, receiver) = unbounded_channel(); 177 | let sender_clone = sender.clone(); 178 | 179 | // Handle messages received 180 | let block = ConcreteBlock::new(move |event| { 181 | let message = xpc_object_to_message(event); 182 | sender_clone.unbounded_send(message).ok() 183 | }); 184 | 185 | // We must move it from the stack to the heap so that when the libxpc 186 | // reference count is released we don't double free. This limitation is 187 | // explained in the blocks crate. 188 | let block = block.copy(); 189 | 190 | unsafe { 191 | xpc_connection_set_event_handler(connection, block.deref() as *const _ as *mut _); 192 | xpc_connection_resume(connection); 193 | } 194 | 195 | XpcClient { 196 | connection, 197 | event_handler_is_running: true, 198 | receiver, 199 | sender, 200 | } 201 | } 202 | 203 | /// The connection isn't established until the first call to `send_message`. 204 | pub fn connect(name: impl AsRef) -> Self { 205 | let name = name.as_ref(); 206 | let flags = XPC_CONNECTION_MACH_SERVICE_PRIVILEGED as u64; 207 | let connection = unsafe { 208 | xpc_connection_create_mach_service(name.as_ptr(), std::ptr::null_mut(), flags) 209 | }; 210 | Self::from_raw(connection) 211 | } 212 | 213 | /// The connection is established on the first call to `send_message`. You 214 | /// may receive an error relating to an invalid mach port name or a variety 215 | /// of other issues. 216 | pub fn send_message(&self, message: Message) { 217 | let xpc_object = message_to_xpc_object(message); 218 | unsafe { 219 | xpc_connection_send_message(self.connection, xpc_object); 220 | xpc_release(xpc_object); 221 | } 222 | } 223 | 224 | #[cfg(feature = "audit_token")] 225 | pub fn audit_token(&self) -> [u8; 32] { 226 | // This is a private API, but it's also required in order to 227 | // authenticate XPC clients without requiring a handshake. 228 | // See https://developer.apple.com/forums/thread/72881 for more info. 229 | extern "C" { 230 | fn xpc_connection_get_audit_token(con: xpc_connection_t, token: *mut c_void); 231 | } 232 | 233 | let mut token_buffer: [u8; 32] = [0; 32]; 234 | 235 | unsafe { 236 | xpc_connection_get_audit_token( 237 | self.connection as xpc_connection_t, 238 | token_buffer.as_mut_ptr() as _, 239 | ) 240 | } 241 | 242 | token_buffer 243 | } 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | use super::*; 249 | use futures::{executor::block_on, StreamExt}; 250 | use std::{collections::HashMap, ffi::CString}; 251 | use xpc_connection_sys::xpc_connection_cancel; 252 | 253 | // This also tests that the event handler block is only freed once, as a 254 | // double free is possible if the block isn't copied on to the heap. 255 | #[test] 256 | fn event_handler_receives_error_on_close() { 257 | let mach_port_name = CString::new("com.apple.blued").unwrap(); 258 | let mut client = XpcClient::connect(&mach_port_name); 259 | 260 | // Cancelling the connection will cause the event handler to be called 261 | // with an error message. This will happen under normal circumstances, 262 | // for example if the service invalidates the connection. 263 | unsafe { xpc_connection_cancel(client.connection) }; 264 | 265 | if let Some(message) = block_on(client.next()) { 266 | panic!("Expected `None`, but received {:?}", message); 267 | } 268 | } 269 | 270 | #[test] 271 | fn stream_closed_on_drop() -> Result<(), Box> { 272 | let mach_port_name = CString::new("com.apple.blued")?; 273 | let mut client = XpcClient::connect(&mach_port_name); 274 | 275 | let message = Message::Dictionary({ 276 | let mut dictionary = HashMap::new(); 277 | dictionary.insert(CString::new("kCBMsgId")?, Message::Int64(1)); 278 | dictionary.insert( 279 | CString::new("kCBMsgArgs")?, 280 | Message::Dictionary({ 281 | let mut temp = HashMap::new(); 282 | temp.insert(CString::new("kCBMsgArgAlert")?, Message::Int64(1)); 283 | temp.insert( 284 | CString::new("kCBMsgArgName")?, 285 | Message::String(CString::new("rust")?), 286 | ); 287 | temp 288 | }), 289 | ); 290 | dictionary 291 | }); 292 | 293 | // Can get data while the channel is open 294 | client.send_message(message); 295 | 296 | let mut count = 0; 297 | 298 | loop { 299 | match block_on(client.next()) { 300 | Some(Message::Error(error)) => { 301 | panic!("Error: {:?}", error); 302 | } 303 | Some(message) => { 304 | println!("Received message: {:?}", message); 305 | count += 1; 306 | 307 | // Explained in `event_handler_receives_error_on_close`. 308 | unsafe { xpc_connection_cancel(client.connection) }; 309 | } 310 | None => { 311 | // We can't be sure how many buffered messages we'll receive 312 | // from blued before the connection is cancelled, but it's 313 | // safe to say it should be less than 5. 314 | assert!(count < 5); 315 | return Ok(()); 316 | } 317 | } 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /xpc-connection/src/message.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | ffi::{CStr, CString}, 4 | mem, 5 | os::{raw::c_void, unix::io::RawFd}, 6 | ptr, 7 | sync::mpsc::channel, 8 | time::{Duration, SystemTime}, 9 | }; 10 | 11 | use block::{Block, ConcreteBlock}; 12 | 13 | use xpc_connection_sys::{ 14 | _xpc_error_connection_interrupted, _xpc_type_activity, _xpc_type_array, _xpc_type_bool, 15 | _xpc_type_connection, _xpc_type_data, _xpc_type_date, _xpc_type_dictionary, _xpc_type_double, 16 | _xpc_type_endpoint, _xpc_type_error, _xpc_type_fd, _xpc_type_int64, _xpc_type_null, 17 | _xpc_type_shmem, _xpc_type_string, _xpc_type_uint64, _xpc_type_uuid, uuid_t, 18 | xpc_array_append_value, xpc_array_apply, xpc_array_create, xpc_array_get_count, 19 | xpc_bool_create, xpc_bool_get_value, xpc_connection_t, xpc_data_create, xpc_data_get_bytes_ptr, 20 | xpc_data_get_length, xpc_date_create, xpc_date_get_value, xpc_dictionary_apply, 21 | xpc_dictionary_create, xpc_dictionary_get_count, xpc_dictionary_set_value, xpc_double_create, 22 | xpc_double_get_value, xpc_fd_create, xpc_fd_dup, xpc_get_type, xpc_int64_create, 23 | xpc_int64_get_value, xpc_null_create, xpc_object_t, xpc_release, xpc_retain, xpc_string_create, 24 | xpc_string_get_string_ptr, xpc_uint64_create, xpc_uint64_get_value, xpc_uuid_create, 25 | xpc_uuid_get_bytes, 26 | }; 27 | 28 | use crate::{XpcClient, XpcListener}; 29 | 30 | #[derive(Debug, Clone)] 31 | pub enum XpcType { 32 | Activity, 33 | Array, 34 | Bool, 35 | Connection, 36 | Data, 37 | Date, 38 | Dictionary, 39 | Double, 40 | Endpoint, 41 | Error, 42 | Fd, 43 | Int64, 44 | Null, 45 | Shmem, 46 | String, 47 | Uint64, 48 | Uuid, 49 | } 50 | 51 | macro_rules! check_xpctype { 52 | ($xpc_object:ident, $xpc_object_type:ident, [ $(($type:ident, $enum:ident),)+ ]) => { 53 | $( 54 | if $xpc_object_type == unsafe { &$type as *const _ } { 55 | return (XpcType::$enum, $xpc_object); 56 | } 57 | )+ 58 | } 59 | } 60 | 61 | pub fn xpc_object_to_xpctype(xpc_object: xpc_object_t) -> (XpcType, xpc_object_t) { 62 | let xpc_object_type = unsafe { xpc_get_type(xpc_object) }; 63 | check_xpctype!( 64 | xpc_object, 65 | xpc_object_type, 66 | [ 67 | (_xpc_type_activity, Activity), 68 | (_xpc_type_array, Array), 69 | (_xpc_type_bool, Bool), 70 | (_xpc_type_connection, Connection), 71 | (_xpc_type_data, Data), 72 | (_xpc_type_date, Date), 73 | (_xpc_type_dictionary, Dictionary), 74 | (_xpc_type_double, Double), 75 | (_xpc_type_endpoint, Endpoint), 76 | (_xpc_type_error, Error), 77 | (_xpc_type_fd, Fd), 78 | (_xpc_type_int64, Int64), 79 | (_xpc_type_null, Null), 80 | (_xpc_type_shmem, Shmem), 81 | (_xpc_type_string, String), 82 | (_xpc_type_uint64, Uint64), 83 | (_xpc_type_uuid, Uuid), 84 | ] 85 | ); 86 | panic!("Unknown `xpc` object type!") 87 | } 88 | 89 | unsafe fn copy_raw_to_vec(ptr: *const u8, length: usize) -> Vec { 90 | let mut vec = Vec::with_capacity(length); 91 | vec.set_len(length); 92 | std::ptr::copy_nonoverlapping(ptr, vec.as_mut_ptr(), length); 93 | vec 94 | } 95 | 96 | #[derive(Debug, PartialEq)] 97 | pub enum Message { 98 | Bool(bool), 99 | Client(XpcClient), 100 | Date(SystemTime), 101 | Double(f64), 102 | Fd(RawFd), 103 | Listener(XpcListener), 104 | Int64(i64), 105 | String(CString), 106 | Dictionary(HashMap), 107 | Array(Vec), 108 | Data(Vec), 109 | Uint64(u64), 110 | Uuid(Vec), 111 | Error(MessageError), 112 | Null, 113 | } 114 | 115 | #[derive(Debug, Clone, PartialEq, Eq)] 116 | pub enum MessageError { 117 | /// The connection was interrupted, but is still usable. Clients should 118 | /// send their previous message again. 119 | ConnectionInterrupted, 120 | /// The connection was closed and cannot be recovered. 121 | ConnectionInvalid, 122 | } 123 | 124 | pub fn xpc_object_to_message(xpc_object: xpc_object_t) -> Message { 125 | match xpc_object_to_xpctype(xpc_object).0 { 126 | XpcType::Bool => Message::Bool(unsafe { xpc_bool_get_value(xpc_object) }), 127 | XpcType::Connection => { 128 | let connection = xpc_object as xpc_connection_t; 129 | unsafe { xpc_retain(connection as xpc_object_t) }; 130 | let xpc_connection = XpcClient::from_raw(connection); 131 | Message::Client(xpc_connection) 132 | } 133 | XpcType::Date => { 134 | let nanos = unsafe { xpc_date_get_value(xpc_object) }; 135 | let time_since_epoch = Duration::from_nanos(nanos.abs() as u64); 136 | Message::Date(if nanos.is_negative() { 137 | SystemTime::UNIX_EPOCH - time_since_epoch 138 | } else { 139 | SystemTime::UNIX_EPOCH + time_since_epoch 140 | }) 141 | } 142 | XpcType::Double => Message::Double(unsafe { xpc_double_get_value(xpc_object) }), 143 | XpcType::Int64 => Message::Int64(unsafe { xpc_int64_get_value(xpc_object) }), 144 | XpcType::Uint64 => Message::Uint64(unsafe { xpc_uint64_get_value(xpc_object) }), 145 | XpcType::Fd => Message::Fd(unsafe { xpc_fd_dup(xpc_object) }), 146 | XpcType::String => Message::String( 147 | unsafe { CStr::from_ptr(xpc_string_get_string_ptr(xpc_object)) }.to_owned(), 148 | ), 149 | XpcType::Dictionary => { 150 | let (sender, receiver) = channel(); 151 | let mut rc_block = ConcreteBlock::new(move |key, value| { 152 | sender 153 | .send(( 154 | unsafe { CStr::from_ptr(key) }.to_owned(), 155 | xpc_object_to_message(value), 156 | )) 157 | .unwrap(); 158 | 1 159 | }); 160 | let block = &mut *rc_block; 161 | unsafe { xpc_dictionary_apply(xpc_object, block as *mut Block<_, _> as *mut c_void) }; 162 | 163 | let mut dictionary = HashMap::new(); 164 | for _ in 0..unsafe { xpc_dictionary_get_count(xpc_object) } { 165 | let (key, value) = receiver.recv().unwrap(); 166 | dictionary.insert(key, value); 167 | } 168 | 169 | Message::Dictionary(dictionary) 170 | } 171 | XpcType::Array => { 172 | let (sender, receiver) = channel(); 173 | let mut rc_block = ConcreteBlock::new(move |_index: usize, value| { 174 | sender.send(xpc_object_to_message(value)).unwrap(); 175 | 1 176 | }); 177 | let block = &mut *rc_block; 178 | unsafe { xpc_array_apply(xpc_object, block as *mut Block<_, _> as *mut c_void) }; 179 | 180 | let len = unsafe { xpc_array_get_count(xpc_object) } as usize; 181 | let mut array = Vec::with_capacity(len); 182 | 183 | for _ in 0..len { 184 | let value = receiver.recv().unwrap(); 185 | array.push(value); 186 | } 187 | 188 | Message::Array(array) 189 | } 190 | XpcType::Data => unsafe { 191 | let ptr = xpc_data_get_bytes_ptr(xpc_object) as *mut u8; 192 | let length = xpc_data_get_length(xpc_object) as usize; 193 | Message::Data(copy_raw_to_vec(ptr, length)) 194 | }, 195 | XpcType::Uuid => unsafe { 196 | let ptr = xpc_uuid_get_bytes(xpc_object) as *mut u8; 197 | let length = mem::size_of::(); 198 | Message::Uuid(copy_raw_to_vec(ptr, length)) 199 | }, 200 | XpcType::Error => unsafe { 201 | if std::ptr::eq( 202 | xpc_object as *const _, 203 | &_xpc_error_connection_interrupted as *const _ as *const _, 204 | ) { 205 | Message::Error(MessageError::ConnectionInterrupted) 206 | } else { 207 | Message::Error(MessageError::ConnectionInvalid) 208 | } 209 | }, 210 | XpcType::Null => Message::Null, 211 | _ => panic!("Unmapped `xpc` object type!"), 212 | } 213 | } 214 | 215 | pub fn message_to_xpc_object(message: Message) -> xpc_object_t { 216 | match message { 217 | Message::Bool(bool) => unsafe { xpc_bool_create(bool) }, 218 | Message::Client(client) => client.connection as xpc_object_t, 219 | Message::Date(date) => unsafe { 220 | xpc_date_create(if date >= SystemTime::UNIX_EPOCH { 221 | date.duration_since(SystemTime::UNIX_EPOCH) 222 | .unwrap() 223 | .as_nanos() as i64 224 | } else { 225 | -(SystemTime::UNIX_EPOCH 226 | .duration_since(date) 227 | .unwrap() 228 | .as_nanos() as i64) 229 | }) 230 | }, 231 | Message::Double(double) => unsafe { xpc_double_create(double) }, 232 | Message::Fd(fd) => unsafe { xpc_fd_create(fd) }, 233 | Message::Listener(listener) => listener.connection as xpc_object_t, 234 | Message::Int64(value) => unsafe { xpc_int64_create(value) }, 235 | Message::String(value) => unsafe { xpc_string_create(value.as_ptr()) }, 236 | Message::Dictionary(values) => { 237 | let dictionary = unsafe { 238 | xpc_dictionary_create(ptr::null(), ptr::null_mut() as *mut *mut c_void, 0) 239 | }; 240 | for (key, value) in values { 241 | unsafe { 242 | let xpc_value = message_to_xpc_object(value); 243 | xpc_dictionary_set_value(dictionary, key.as_ptr(), xpc_value); 244 | xpc_release(xpc_value); 245 | } 246 | } 247 | dictionary 248 | } 249 | Message::Array(values) => { 250 | let array = unsafe { xpc_array_create(ptr::null_mut() as *mut *mut _, 0) }; 251 | for value in values { 252 | unsafe { 253 | let xpc_value = message_to_xpc_object(value); 254 | xpc_array_append_value(array, xpc_value); 255 | xpc_release(xpc_value); 256 | } 257 | } 258 | array 259 | } 260 | Message::Data(value) => unsafe { 261 | xpc_data_create(value.as_ptr() as *const _, value.len() as u64) 262 | }, 263 | Message::Uint64(value) => unsafe { xpc_uint64_create(value) }, 264 | Message::Uuid(value) => unsafe { xpc_uuid_create(value.as_ptr()) }, 265 | Message::Error(_) => panic!("Cannot convert error to `xpc` object!"), 266 | Message::Null => unsafe { xpc_null_create() }, 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /xpc-connection/tests/message_round_trip.rs: -------------------------------------------------------------------------------- 1 | use futures::{executor::block_on, StreamExt}; 2 | use std::{ 3 | collections::HashMap, 4 | error::Error, 5 | ffi::CString, 6 | fs::File, 7 | os::unix::prelude::{FromRawFd, IntoRawFd, MetadataExt}, 8 | time::{Duration, SystemTime}, 9 | }; 10 | use xpc_connection::{Message, XpcClient}; 11 | 12 | #[test] 13 | #[ignore = "This test requires the echo server to be running"] 14 | fn send_and_receive_int64() -> Result<(), Box> { 15 | let mach_port_name = CString::new("com.example.echo")?; 16 | let mut con = XpcClient::connect(mach_port_name); 17 | 18 | let mut output = HashMap::new(); 19 | let key = CString::new("K")?; 20 | output.insert(key.clone(), Message::Int64(1)); 21 | con.send_message(Message::Dictionary(output)); 22 | 23 | let message = block_on(con.next()); 24 | if let Some(Message::Dictionary(d)) = message { 25 | let input = d.get(&key); 26 | if let Some(Message::Int64(1)) = input { 27 | return Ok(()); 28 | } 29 | 30 | panic!("Received unexpected value: {:?}", input); 31 | } 32 | 33 | panic!("Didn't receive the container dictionary: {:?}", message); 34 | } 35 | 36 | #[tokio::test] 37 | #[ignore = "This test requires the echo server to be running"] 38 | async fn send_and_receive_string() -> Result<(), Box> { 39 | let mach_port_name = CString::new("com.example.echo")?; 40 | let mut con = XpcClient::connect(mach_port_name); 41 | 42 | let mut output = HashMap::new(); 43 | let key = CString::new("K")?; 44 | let value = CString::new("V")?; 45 | output.insert(key.clone(), Message::String(value.clone())); 46 | 47 | con.send_message(Message::Dictionary(output)); 48 | 49 | match con.next().await { 50 | Some(Message::Dictionary(d)) => { 51 | let input = d.get(&key); 52 | if let Some(Message::String(s)) = input { 53 | assert_eq!(s, &value); 54 | return Ok(()); 55 | } 56 | panic!("Received unexpected value: {:?}", input); 57 | } 58 | Some(message) => panic!("Didn't receive the container dictionary: {:?}", message), 59 | None => panic!("Didn't receive a response"), 60 | } 61 | } 62 | 63 | #[test] 64 | #[ignore = "This test requires the echo server to be running"] 65 | fn send_and_receive_dictionary() -> Result<(), Box> { 66 | let mach_port_name = CString::new("com.example.echo")?; 67 | let mut con = XpcClient::connect(mach_port_name); 68 | 69 | let mut output = HashMap::new(); 70 | let outer_key = CString::new("O")?; 71 | let inner_key = CString::new("I")?; 72 | output.insert( 73 | outer_key.clone(), 74 | Message::Dictionary({ 75 | let mut inner = HashMap::new(); 76 | inner.insert(inner_key.clone(), Message::Int64(1)); 77 | inner 78 | }), 79 | ); 80 | 81 | con.send_message(Message::Dictionary(output)); 82 | 83 | let message = block_on(con.next()); 84 | if let Some(Message::Dictionary(outer_hashmap)) = message { 85 | let inner_dictionary = outer_hashmap.get(&outer_key); 86 | if let Some(Message::Dictionary(inner_hashmap)) = inner_dictionary { 87 | if let Some(Message::Int64(1)) = inner_hashmap.get(&inner_key) { 88 | return Ok(()); 89 | } 90 | 91 | panic!("Received unexpected value: {:?}", inner_hashmap); 92 | } 93 | 94 | panic!("Received unexpected value: {:?}", inner_dictionary); 95 | } 96 | 97 | panic!("Didn't receive the container dictionary: {:?}", message); 98 | } 99 | 100 | #[test] 101 | #[ignore = "This test requires the echo server to be running"] 102 | fn send_and_receive_array() -> Result<(), Box> { 103 | let mach_port_name = CString::new("com.example.echo")?; 104 | let mut con = XpcClient::connect(mach_port_name); 105 | 106 | let mut output = HashMap::new(); 107 | let key = CString::new("K")?; 108 | output.insert(key.clone(), Message::Array(vec![Message::Int64(1)])); 109 | 110 | con.send_message(Message::Dictionary(output)); 111 | 112 | let message = block_on(con.next()); 113 | if let Some(Message::Dictionary(d)) = message { 114 | let input = d.get(&key); 115 | if let Some(Message::Array(a)) = input { 116 | if let Message::Int64(1) = a[0] { 117 | return Ok(()); 118 | } 119 | 120 | panic!("Received unexpected value: {:?}", a); 121 | } 122 | 123 | panic!("Received unexpected value: {:?}", input); 124 | } 125 | 126 | panic!("Didn't receive the container dictionary: {:?}", message); 127 | } 128 | 129 | #[test] 130 | #[ignore = "This test requires the echo server to be running"] 131 | fn send_and_receive_data() -> Result<(), Box> { 132 | let mach_port_name = CString::new("com.example.echo")?; 133 | let mut con = XpcClient::connect(mach_port_name); 134 | 135 | let key = CString::new("K")?; 136 | let value = vec![0, 1]; 137 | let mut output = HashMap::new(); 138 | output.insert(key.clone(), Message::Data(value.clone())); 139 | 140 | con.send_message(Message::Dictionary(output)); 141 | 142 | let message = block_on(con.next()); 143 | if let Some(Message::Dictionary(d)) = message { 144 | let input = d.get(&key); 145 | if let Some(Message::Data(v)) = input { 146 | assert_eq!(*v, value); 147 | return Ok(()); 148 | } 149 | 150 | panic!("Received unexpected value: {:?}", input); 151 | } 152 | 153 | panic!("Didn't receive the container dictionary: {:?}", message); 154 | } 155 | 156 | #[test] 157 | #[ignore = "This test requires the echo server to be running"] 158 | fn send_and_receive_uint64() -> Result<(), Box> { 159 | let mach_port_name = CString::new("com.example.echo")?; 160 | let mut con = XpcClient::connect(mach_port_name); 161 | 162 | let key = CString::new("K")?; 163 | let value = 0x2d13772f7f30cc5d_u64; 164 | 165 | let mut output = HashMap::new(); 166 | output.insert(key.clone(), Message::Uint64(value)); 167 | 168 | con.send_message(Message::Dictionary(output)); 169 | 170 | let message = block_on(con.next()); 171 | if let Some(Message::Dictionary(d)) = message { 172 | let input = d.get(&key); 173 | if let Some(Message::Uint64(v)) = input { 174 | assert_eq!(*v, value); 175 | return Ok(()); 176 | } 177 | 178 | panic!("Received unexpected value: {:?}", input); 179 | } 180 | 181 | panic!("Didn't receive the container dictionary: {:?}", message); 182 | } 183 | 184 | #[test] 185 | #[ignore = "This test requires the echo server to be running"] 186 | fn send_and_receive_uuid() -> Result<(), Box> { 187 | let mach_port_name = CString::new("com.example.echo")?; 188 | let mut con = XpcClient::connect(mach_port_name); 189 | 190 | let key = CString::new("K")?; 191 | let value = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; 192 | 193 | let mut output = HashMap::new(); 194 | output.insert(key.clone(), Message::Uuid(value.clone())); 195 | 196 | con.send_message(Message::Dictionary(output)); 197 | 198 | let message = block_on(con.next()); 199 | if let Some(Message::Dictionary(d)) = message { 200 | let input = d.get(&key); 201 | if let Some(Message::Uuid(v)) = input { 202 | assert_eq!(*v, value); 203 | return Ok(()); 204 | } 205 | 206 | panic!("Received unexpected value: {:?}", input); 207 | } 208 | 209 | panic!("Didn't receive the container dictionary: {:?}", message); 210 | } 211 | 212 | #[test] 213 | #[ignore = "This test requires the echo server to be running"] 214 | fn send_and_receive_fd() -> Result<(), Box> { 215 | let mach_port_name = CString::new("com.example.echo")?; 216 | let mut con = XpcClient::connect(mach_port_name); 217 | 218 | let key = CString::new("K")?; 219 | let original = File::create("/tmp/a")?; 220 | let original_inode = original.metadata()?.ino(); 221 | 222 | let mut output = HashMap::new(); 223 | output.insert(key.clone(), Message::Fd(original.into_raw_fd())); 224 | 225 | con.send_message(Message::Dictionary(output)); 226 | 227 | let message = block_on(con.next()); 228 | if let Some(Message::Dictionary(d)) = message { 229 | let input = d.get(&key); 230 | if let Some(Message::Fd(v)) = input { 231 | let new = unsafe { File::from_raw_fd(*v) }; 232 | assert_eq!(original_inode, new.metadata()?.ino()); 233 | return Ok(()); 234 | } 235 | 236 | panic!("Received unexpected value: {:?}", input); 237 | } 238 | 239 | panic!("Didn't receive the container dictionary: {:?}", message); 240 | } 241 | 242 | #[test] 243 | #[ignore = "This test requires the echo server to be running"] 244 | fn send_and_receive_double() -> Result<(), Box> { 245 | let mach_port_name = CString::new("com.example.echo")?; 246 | let mut con = XpcClient::connect(mach_port_name); 247 | 248 | let key = CString::new("K")?; 249 | let value = 1.23456789_f64; 250 | 251 | let mut output = HashMap::new(); 252 | output.insert(key.clone(), Message::Double(value)); 253 | 254 | con.send_message(Message::Dictionary(output)); 255 | 256 | let message = block_on(con.next()); 257 | if let Some(Message::Dictionary(d)) = message { 258 | let input = d.get(&key); 259 | if let Some(Message::Double(v)) = input { 260 | assert!((*v - value).abs() < std::f64::EPSILON); 261 | return Ok(()); 262 | } 263 | 264 | panic!("Received unexpected value: {:?}", input); 265 | } 266 | 267 | panic!("Didn't receive the container dictionary: {:?}", message); 268 | } 269 | 270 | #[test] 271 | #[ignore = "This test requires the echo server to be running"] 272 | fn send_and_receive_bool() -> Result<(), Box> { 273 | let mach_port_name = CString::new("com.example.echo")?; 274 | let mut con = XpcClient::connect(mach_port_name); 275 | 276 | let key = CString::new("K")?; 277 | let value = true; 278 | 279 | let mut output = HashMap::new(); 280 | output.insert(key.clone(), Message::Bool(value)); 281 | 282 | con.send_message(Message::Dictionary(output)); 283 | 284 | let message = block_on(con.next()); 285 | if let Some(Message::Dictionary(d)) = message { 286 | let input = d.get(&key); 287 | if let Some(Message::Bool(v)) = input { 288 | assert_eq!(*v, value); 289 | return Ok(()); 290 | } 291 | 292 | panic!("Received unexpected value: {:?}", input); 293 | } 294 | 295 | panic!("Didn't receive the container dictionary: {:?}", message); 296 | } 297 | 298 | #[test] 299 | #[ignore = "This test requires the echo server to be running"] 300 | fn send_and_receive_date() -> Result<(), Box> { 301 | let mach_port_name = CString::new("com.example.echo")?; 302 | let mut con = XpcClient::connect(mach_port_name); 303 | 304 | let key = CString::new("K")?; 305 | let value = SystemTime::now(); 306 | 307 | let mut output = HashMap::new(); 308 | output.insert(key.clone(), Message::Date(value)); 309 | 310 | con.send_message(Message::Dictionary(output)); 311 | 312 | let message = block_on(con.next()); 313 | if let Some(Message::Dictionary(d)) = message { 314 | let input = d.get(&key); 315 | if let Some(Message::Date(v)) = input { 316 | assert_eq!(*v, value); 317 | return Ok(()); 318 | } 319 | 320 | panic!("Received unexpected value: {:?}", input); 321 | } 322 | 323 | panic!("Didn't receive the container dictionary: {:?}", message); 324 | } 325 | 326 | #[test] 327 | #[ignore = "This test requires the echo server to be running"] 328 | fn send_and_receive_negative_date() -> Result<(), Box> { 329 | let mach_port_name = CString::new("com.example.echo")?; 330 | let mut con = XpcClient::connect(mach_port_name); 331 | 332 | let key = CString::new("K")?; 333 | let value = SystemTime::UNIX_EPOCH - Duration::from_secs(90); 334 | 335 | let mut output = HashMap::new(); 336 | output.insert(key.clone(), Message::Date(value)); 337 | 338 | con.send_message(Message::Dictionary(output)); 339 | 340 | let message = block_on(con.next()); 341 | if let Some(Message::Dictionary(d)) = message { 342 | let input = d.get(&key); 343 | if let Some(Message::Date(v)) = input { 344 | assert_eq!(*v, value); 345 | return Ok(()); 346 | } 347 | 348 | panic!("Received unexpected value: {:?}", input); 349 | } 350 | 351 | panic!("Didn't receive the container dictionary: {:?}", message); 352 | } 353 | 354 | #[test] 355 | #[ignore = "This test requires the echo server to be running"] 356 | fn send_and_receive_null() -> Result<(), Box> { 357 | let mach_port_name = CString::new("com.example.echo")?; 358 | let mut con = XpcClient::connect(mach_port_name); 359 | 360 | let key = CString::new("K")?; 361 | 362 | let mut output = HashMap::new(); 363 | output.insert(key.clone(), Message::Null); 364 | 365 | con.send_message(Message::Dictionary(output)); 366 | 367 | let message = block_on(con.next()); 368 | if let Some(Message::Dictionary(d)) = message { 369 | let input = d.get(&key); 370 | if matches!(input, Some(Message::Null)) { 371 | return Ok(()); 372 | } 373 | 374 | panic!("Received unexpected value: {:?}", input); 375 | } 376 | 377 | panic!("Didn't receive the container dictionary: {:?}", message); 378 | } 379 | 380 | #[test] 381 | #[ignore = "This test requires the echo server to be running"] 382 | fn connect_and_disconnect() -> Result<(), Box> { 383 | let mach_port_name = CString::new("com.example.echo")?; 384 | let mut con = XpcClient::connect(mach_port_name); 385 | 386 | let key = CString::new("K")?; 387 | let value = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; 388 | 389 | let mut output = HashMap::new(); 390 | output.insert(key.clone(), Message::Uuid(value.clone())); 391 | 392 | con.send_message(Message::Dictionary(output)); 393 | 394 | let message = block_on(con.next()); 395 | if let Some(Message::Dictionary(d)) = message { 396 | let input = d.get(&key); 397 | if let Some(Message::Uuid(v)) = input { 398 | assert_eq!(*v, value); 399 | return Ok(()); 400 | } 401 | 402 | panic!("Received unexpected value: {:?}", input); 403 | } 404 | 405 | panic!("Didn't receive the container dictionary: {:?}", message); 406 | } 407 | --------------------------------------------------------------------------------