├── .gitignore ├── .github └── workflows │ ├── ci.yml │ ├── prs.yml │ ├── test.yml │ └── build.yml ├── test_data ├── paired.plist ├── detached.plist ├── success-result.plist ├── command.plist └── attached.plist ├── examples ├── listen.rs └── connect.rs ├── Cargo.toml ├── README.md ├── LICENSE-MIT ├── src ├── lib.rs └── protocol.rs └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ 'main', 'develop', 'release/**' ] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | uses: ./.github/workflows/build.yml 11 | secrets: inherit 12 | test: 13 | name: Test 14 | uses: ./.github/workflows/test.yml 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/prs.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: [ '**' ] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | uses: ./.github/workflows/build.yml 11 | secrets: inherit 12 | test: 13 | name: Cargo Test 14 | uses: ./.github/workflows/test.yml 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /test_data/paired.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DeviceID 6 | 3 7 | MessageType 8 | Paired 9 | 10 | 11 | -------------------------------------------------------------------------------- /test_data/detached.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DeviceID 6 | 3 7 | MessageType 8 | Detached 9 | 10 | 11 | -------------------------------------------------------------------------------- /test_data/success-result.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MessageType 6 | Result 7 | Number 8 | 0 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | os: ["macos-latest", "windows-latest"] 11 | runs-on: '${{ matrix.os }}' 12 | name: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Test 17 | run: cargo test --all 18 | -------------------------------------------------------------------------------- /test_data/command.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MessageType 6 | Listen 7 | ProgName 8 | MyApp 9 | ClientVersionString 10 | 1.0 11 | 12 | -------------------------------------------------------------------------------- /examples/listen.rs: -------------------------------------------------------------------------------- 1 | use peertalk::DeviceListener; 2 | #[macro_use] 3 | extern crate log; 4 | 5 | fn main() { 6 | env_logger::builder() 7 | .filter(None, log::LevelFilter::Trace) 8 | .init(); 9 | let listener = DeviceListener::new().expect("Failed to create device listener"); 10 | info!("Listening for iOS devices..."); 11 | loop { 12 | match listener.next_event() { 13 | Some(event) => info!("Event: {:?}", event), 14 | None => std::thread::sleep(std::time::Duration::from_secs(5)), 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "peertalk" 3 | version = "0.2.0" 4 | authors = ["Jeremy Knope "] 5 | description = "Library for communicating with an iPad or iPhone over USB" 6 | keywords = ["ios", "iphone", "ipad", "peertalk", "usb"] 7 | categories = ["network-programming"] 8 | edition = "2018" 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/AstroHQ/peertalk-rs" 12 | 13 | [dependencies] 14 | byteorder = "1.3" 15 | log = "0.4" 16 | plist = "1" 17 | serde = { version = "1.0", features = ["derive"] } 18 | thiserror = "1" 19 | 20 | [dev-dependencies] 21 | env_logger = "0.10" 22 | -------------------------------------------------------------------------------- /test_data/attached.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DeviceID 6 | 3 7 | MessageType 8 | Attached 9 | Properties 10 | 11 | ConnectionType 12 | USB 13 | DeviceID 14 | 3 15 | LocationID 16 | 0 17 | ProductID 18 | 4779 19 | SerialNumber 20 | 00001011-000A111E0111001E 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build: 8 | strategy: 9 | matrix: 10 | os: ["macos-latest", "windows-latest", "ubuntu-latest"] 11 | runs-on: '${{ matrix.os }}' 12 | name: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Linux deps 17 | run: sudo apt install -y libavahi-compat-libdnssd-dev libavahi-compat-libdnssd1 18 | if: ${{ matrix.os == 'ubuntu-latest' }} 19 | - name: iOS Rust target 20 | run: rustup target add aarch64-apple-ios 21 | - name: Build 22 | run: cargo build --all --all-features 23 | - name: Build for iOS 24 | run: cargo build --workspace --all-features --target aarch64-apple-ios 25 | if: ${{ matrix.os == 'macos-latest' }} 26 | - name: Run Clippy 27 | uses: actions-rs/clippy-check@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | args: --all-features 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cross-platform PeerTalk Implemented in Rust 2 | 3 | This implements the ability to negotiate a network connection over USB to iOS devices via Apple's USB muxer. This can work across platforms assuming iTunes or Apple Mobile Supprot is present. May work with open source [usbmuxd/libimobiledevice](http://www.libimobiledevice.org/) on linux, but is untested. 4 | 5 | Based on [PeerTalk by Rasmus Andersson](https://github.com/rsms/peertalk) 6 | 7 | ## Usage 8 | 9 | This just provides the necessary code for the host (mac/windows) side to detect an iPad/iPhone & negotiate a connection to the device if it's listening. 10 | 11 | 1. iOS app sets up a TCP listener on a known port 12 | 2. Host app uses peertalk to wait for device to be plugged in 13 | 3. Upon plug, tell peertalk to establish a connection to the device with the port used in step 1 14 | 4. You'll have a ready to use `TcpStream` upon success 15 | 16 | ## Status 17 | 18 | - [x] Basic device listen protocol work started 19 | - [x] macOS/linux UNIX domain socket support 20 | - [x] Connect (network sockets) support 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/connect.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 2 | use peertalk::{connect_to_device, DeviceEvent, DeviceId, DeviceListener}; 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::io::{Error as IoError, Read, Write}; 6 | #[macro_use] 7 | extern crate log; 8 | 9 | const PT_PORT: u16 = 2345; 10 | const PT_VERSION: u32 = 1; 11 | const PT_FRAME_TYPE_DEVICE_INFO: u32 = 100; 12 | const PT_FRAME_TYPE_TEXT_MSG: u32 = 101; 13 | const PT_FRAME_TYPE_PING: u32 = 102; 14 | const PT_FRAME_TYPE_PONG: u32 = 103; 15 | 16 | fn main() { 17 | env_logger::builder() 18 | .filter(None, log::LevelFilter::Trace) 19 | .init(); 20 | let listener = 21 | DeviceListener::new().expect("Failed to create device listener, no Apple Mobile Support?"); 22 | loop { 23 | match listener.next_event() { 24 | Some(event) => process_event(event), 25 | None => std::thread::sleep(std::time::Duration::from_secs(5)), 26 | } 27 | } 28 | } 29 | fn process_event(event: DeviceEvent) { 30 | debug!("Event: {:?}", event); 31 | match event { 32 | DeviceEvent::Attached(info) => { 33 | info!("Device attached: {:?}", info); 34 | info!("Attempting to connect..."); 35 | start_example(info.device_id, PT_PORT); 36 | } 37 | DeviceEvent::Detached(device_id) => { 38 | info!("Device {} detached", device_id); 39 | } 40 | DeviceEvent::Paired(device_id) => { 41 | info!("Device {} was paired", device_id); 42 | } 43 | } 44 | } 45 | fn start_example(device_id: DeviceId, port: u16) { 46 | let mut socket = 47 | connect_to_device(device_id, port).expect("Failed to create device connection"); 48 | // say hi 49 | let hi = PTFrame::text("Hello from Rust!"); 50 | hi.write_into(&mut socket).unwrap(); 51 | loop { 52 | // wait for data from device 53 | match PTFrame::from_reader(&mut socket) { 54 | Ok(frame) => process_frame(frame), 55 | Err(e) => error!("Error reading frame: {}", e), 56 | } 57 | } 58 | } 59 | fn process_frame(frame: PTFrame) { 60 | // print out text if it's device info or text msg type 61 | if frame.frame_type == PT_FRAME_TYPE_DEVICE_INFO { 62 | // binary plist? 63 | let reader = std::io::Cursor::new(frame.payload); 64 | let info: plist::Value = plist::Value::from_reader(reader).unwrap(); 65 | info!("Got device info: {:?}", info); 66 | } else if frame.frame_type == PT_FRAME_TYPE_TEXT_MSG { 67 | if let Ok(string) = std::str::from_utf8(&frame.payload[4..]) { 68 | info!("Got text payload: {}", string); 69 | } else { 70 | error!("Failed to read payload of {} bytes", frame.payload.len()); 71 | } 72 | } else if frame.frame_type == PT_FRAME_TYPE_PING { 73 | info!("Ping!"); 74 | } else if frame.frame_type == PT_FRAME_TYPE_PONG { 75 | info!("Pong!"); 76 | } 77 | } 78 | // peertalk frame example protocol 79 | 80 | #[derive(Debug)] 81 | pub enum FrameError { 82 | IoError(IoError), 83 | } 84 | impl fmt::Display for FrameError { 85 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 86 | match self { 87 | FrameError::IoError(e) => write!(f, "IoError: {}", e), 88 | } 89 | } 90 | } 91 | impl Error for FrameError { 92 | fn source(&self) -> Option<&(dyn Error + 'static)> { 93 | match self { 94 | FrameError::IoError(e) => Some(e), 95 | } 96 | } 97 | } 98 | impl From for FrameError { 99 | fn from(error: IoError) -> Self { 100 | FrameError::IoError(error) 101 | } 102 | } 103 | 104 | type Result = ::std::result::Result; 105 | #[derive(Debug)] 106 | struct PTFrame { 107 | version: u32, 108 | frame_type: u32, 109 | tag: u32, 110 | // payload_size: u32, 111 | payload: Vec, 112 | } 113 | impl PTFrame { 114 | fn text(text: &str) -> PTFrame { 115 | /*typedef struct _PTExampleTextFrame { 116 | uint32_t length; 117 | uint8_t utf8text[0]; 118 | } PTExampleTextFrame;*/ 119 | let mut payload = Vec::with_capacity(text.len() + 4); 120 | payload.write_u32::(text.len() as u32).unwrap(); 121 | payload.write_all(text.as_bytes()).unwrap(); 122 | PTFrame { 123 | version: PT_VERSION, 124 | frame_type: PT_FRAME_TYPE_TEXT_MSG, 125 | tag: 0, 126 | payload, 127 | } 128 | } 129 | fn write_into(&self, writer: &mut W) -> Result<()> 130 | where 131 | W: Write, 132 | { 133 | writer.write_u32::(self.version).unwrap(); 134 | writer.write_u32::(self.frame_type).unwrap(); 135 | writer.write_u32::(self.tag).unwrap(); 136 | writer 137 | .write_u32::(self.payload.len() as u32) 138 | .unwrap(); 139 | writer.write_all(&self.payload[..]).unwrap(); 140 | Ok(()) 141 | } 142 | fn from_reader(reader: &mut R) -> Result 143 | where 144 | R: Read, 145 | { 146 | let version = reader.read_u32::()?; 147 | let frame_type = reader.read_u32::()?; 148 | let tag = reader.read_u32::()?; 149 | let payload_size = reader.read_u32::()?; 150 | let payload = if payload_size > 0 { 151 | let mut payload = vec![0; payload_size as usize]; 152 | reader.read_exact(&mut payload)?; 153 | payload 154 | } else { 155 | vec![] 156 | }; 157 | let frame = PTFrame { 158 | version, 159 | frame_type, 160 | tag, 161 | payload, 162 | }; 163 | Ok(frame) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Crate to handle establishing network connections over USB to apple devices 2 | #![forbid(missing_docs)] 3 | use std::cell::RefCell; 4 | 5 | #[macro_use] 6 | extern crate log; 7 | 8 | use std::collections::VecDeque; 9 | #[cfg(target_os = "windows")] 10 | use std::net::TcpStream; 11 | #[cfg(not(target_os = "windows"))] 12 | use std::os::unix::net::UnixStream; 13 | 14 | #[cfg(target_os = "windows")] 15 | const WINDOWS_TCP_PORT: u16 = 27015; 16 | 17 | mod protocol; 18 | pub use protocol::{ 19 | DeviceAttachedInfo, DeviceConnectionType, DeviceEvent, DeviceId, ProductType, ProtocolError, 20 | }; 21 | use protocol::{Packet, PacketType, Protocol}; 22 | 23 | /// Error for device listener etc 24 | #[derive(thiserror::Error, Debug)] 25 | pub enum Error { 26 | /// Error with usbmuxd protocol 27 | #[error("protocol error: {0}")] 28 | ProtocolError(#[from] protocol::ProtocolError), 29 | /// usbmuxd or Apple Mobile Service isn't available or installed 30 | #[error("Apple Mobile Device service (usbmuxd) likely not available: {0}")] 31 | ServiceUnavailable(#[from] std::io::Error), 32 | /// Error when registrering for device events failed 33 | #[error("error registering device listener: code {0}")] 34 | FailedToListen(i64), 35 | /// Error establishing network connection to device 36 | #[error("error connecting to device: {0}")] 37 | ConnectionRefused(i64), 38 | } 39 | 40 | /// Alias for any of this crate's results 41 | pub type Result = ::std::result::Result; 42 | 43 | /// Aliases UsbSocket to std::net::TcpStream on Windows 44 | #[cfg(target_os = "windows")] 45 | pub type UsbSocket = TcpStream; 46 | /// Aliases UsbSocket to std::os::unix::net::UnixStream on linux/macOS 47 | #[cfg(not(target_os = "windows"))] 48 | pub type UsbSocket = UnixStream; 49 | 50 | /// Connects to usbmuxd (linux oss lib or macOS's built-in muxer) 51 | #[cfg(not(target_os = "windows"))] 52 | fn connect_unix() -> Result { 53 | Ok(UnixStream::connect("/var/run/usbmuxd")?) 54 | } 55 | /// Connect's to Apple Mobile Support service on Windows if available (TCP 27015) 56 | #[cfg(target_os = "windows")] 57 | fn connect_windows() -> Result { 58 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 59 | let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), WINDOWS_TCP_PORT); 60 | Ok(TcpStream::connect_timeout( 61 | &addr, 62 | std::time::Duration::from_secs(5), 63 | )?) 64 | } 65 | 66 | fn send_payload( 67 | socket: &mut UsbSocket, 68 | packet_type: PacketType, 69 | protocol: Protocol, 70 | payload: Vec, 71 | ) -> Result<()> { 72 | let packet = Packet::new(protocol, packet_type, 0, payload); 73 | Ok(packet.write_into(socket)?) 74 | } 75 | /// Creates a network connection over USB to given device & port 76 | pub fn connect_to_device(device_id: protocol::DeviceId, port: u16) -> Result { 77 | #[cfg(target_os = "windows")] 78 | let mut socket = connect_windows()?; 79 | #[cfg(not(target_os = "windows"))] 80 | let mut socket = connect_unix()?; 81 | let command = protocol::Command::connect(port, device_id); 82 | let payload = command.to_bytes(); 83 | send_payload( 84 | &mut socket, 85 | PacketType::PlistPayload, 86 | Protocol::Plist, 87 | payload, 88 | )?; 89 | let packet = Packet::from_reader(&mut socket)?; 90 | let cursor = std::io::Cursor::new(&packet.data[..]); 91 | let res = protocol::ResultMessage::from_reader(cursor)?; 92 | if res.0 != 0 { 93 | return Err(Error::ConnectionRefused(res.0)); 94 | } 95 | 96 | Ok(socket) 97 | } 98 | 99 | /// Listens for iOS devices connecting over USB via Apple Mobile Support/usbmuxd 100 | pub struct DeviceListener { 101 | #[cfg(target_os = "windows")] 102 | socket: RefCell, 103 | #[cfg(not(target_os = "windows"))] 104 | socket: RefCell, 105 | events: RefCell>, 106 | } 107 | impl DeviceListener { 108 | /// Produces a new device listener, registering with usbmuxd/apple mobile support service 109 | /// 110 | /// # Errors 111 | /// Can produce an error, most commonly when the mobile service isn't available. It should be available on macOS, 112 | /// but on Windows it's only available if Apple Mobile Support is installed, typically via iTunes. 113 | pub fn new() -> Result { 114 | #[cfg(target_os = "windows")] 115 | let socket = connect_windows()?; 116 | #[cfg(not(target_os = "windows"))] 117 | let socket = connect_unix()?; 118 | let listener = DeviceListener { 119 | socket: RefCell::new(socket), 120 | events: RefCell::new(VecDeque::new()), 121 | }; 122 | listener.start_listen()?; 123 | listener.socket.borrow_mut().set_nonblocking(true)?; 124 | Ok(listener) 125 | } 126 | /// Receives an event, None if there's no pending events at this time 127 | pub fn next_event(&self) -> Option { 128 | self.drain_events(); 129 | self.events.borrow_mut().pop_front() 130 | } 131 | fn drain_events(&self) { 132 | // TODO: better way read on demand? maybe just thread it? 133 | use std::io::Read; 134 | let mut retries_left = 5; 135 | let mut data: Vec = Vec::with_capacity(10_000); 136 | let full_data = loop { 137 | let mut buf = [0; 4096]; 138 | match (*self.socket.borrow_mut()).read(&mut buf) { 139 | Ok(bytes) => { 140 | data.extend_from_slice(&buf[0..bytes]); 141 | } 142 | Err(e) => { 143 | if e.kind() == std::io::ErrorKind::WouldBlock { 144 | retries_left -= 1; 145 | std::thread::sleep(std::time::Duration::from_millis(100)); 146 | } 147 | } 148 | } 149 | if retries_left == 0 { 150 | break data; 151 | } 152 | }; 153 | let mut cursor = std::io::Cursor::new(&full_data[..]); 154 | loop { 155 | if cursor.position() == full_data.len() as u64 { 156 | break; 157 | } 158 | match Packet::from_reader(&mut cursor) { 159 | Ok(packet) => { 160 | let msg = DeviceEvent::from_vec(packet.data).unwrap(); 161 | self.events.borrow_mut().push_back(msg); 162 | } 163 | Err(ProtocolError::IoError(e)) => match e.kind() { 164 | std::io::ErrorKind::WouldBlock => { 165 | break; 166 | } 167 | _ => { 168 | error!("IO Error: {}", e); 169 | break; 170 | } 171 | }, 172 | Err(e) => { 173 | error!("Error receiving events: {}", e); 174 | break; 175 | } 176 | } 177 | } 178 | } 179 | fn start_listen(&self) -> Result<()> { 180 | info!("Starting device listen"); 181 | let command = protocol::Command::listen(); 182 | let payload = command.to_bytes(); 183 | send_payload( 184 | &mut self.socket.borrow_mut(), 185 | PacketType::PlistPayload, 186 | Protocol::Plist, 187 | payload, 188 | )?; 189 | let packet = Packet::from_reader(&mut *self.socket.borrow_mut())?; 190 | let cursor = std::io::Cursor::new(&packet.data[..]); 191 | let res = protocol::ResultMessage::from_reader(cursor)?; 192 | if res.0 != 0 { 193 | error!("Failed to setup device listen: {}", res.0); 194 | return Err(Error::FailedToListen(res.0)); 195 | } 196 | info!("Listen successful"); 197 | Ok(()) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | // use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 2 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 3 | use plist::Value; 4 | use serde::{Deserialize, Serialize}; 5 | use std::convert::TryFrom; 6 | use std::fmt; 7 | use std::io::{Error as IoError, Read, Seek, Write}; 8 | use std::mem::size_of; 9 | use thiserror::Error; 10 | 11 | /// Error type for any errors with talking to USB muxer/device support 12 | #[derive(Debug, Error)] 13 | pub enum ProtocolError { 14 | /// Message type is invalid, or unsupported 15 | #[error("invalid message type: {0}")] 16 | InvalidMessageType(String), 17 | /// Plist entry isn't the type expected 18 | #[error("invalid plist format/entry")] 19 | InvalidPlistEntry, 20 | /// Plist entry for key is invalid/wrong type 21 | #[error("invalid plist entry for key: {0}")] 22 | InvalidPlistEntryForKey(&'static str), 23 | /// Invalid packet type value 24 | #[error("invalid packet type: {0}")] 25 | InvalidPacketType(u32), 26 | /// Invalid protocol value (expect 0 or 1) 27 | #[error("invalid protocol: {0}")] 28 | InvalidProtocol(u32), 29 | /// Invalid reply code (expect 0-6 except 4, 5) 30 | #[error("invalid reply code: {0}")] 31 | InvalidReplyCode(u32), 32 | /// An IO error occurred, usually if reading from file/socket 33 | #[error(transparent)] 34 | IoError(#[from] IoError), 35 | } 36 | 37 | /// Result type 38 | pub type Result = ::std::result::Result; 39 | 40 | const BASE_PACKET_SIZE: u32 = size_of::() as u32 * 4; 41 | const USB_MESSAGE_TYPE_KEY: &str = "MessageType"; 42 | const USB_DEVICE_ID_KEY: &str = "DeviceID"; 43 | const USB_DEVICE_PROPERTIES_KEY: &str = "Properties"; 44 | 45 | #[repr(u32)] 46 | #[derive(Copy, Clone, Debug, PartialEq)] 47 | pub enum PacketType { 48 | Result = 1, 49 | Connect = 2, 50 | Listen = 3, 51 | DeviceAdd = 4, 52 | DeviceRemove = 5, 53 | // 6 unknown 54 | // 7 unknown 55 | PlistPayload = 8, 56 | } 57 | 58 | impl From for u32 { 59 | fn from(p_type: PacketType) -> Self { 60 | p_type as Self 61 | } 62 | } 63 | 64 | impl TryFrom for PacketType { 65 | type Error = ProtocolError; 66 | fn try_from(value: u32) -> Result { 67 | match value { 68 | 1 => Ok(Self::Result), 69 | 2 => Ok(Self::Connect), 70 | 3 => Ok(Self::Listen), 71 | 4 => Ok(Self::DeviceAdd), 72 | 5 => Ok(Self::DeviceRemove), 73 | 8 => Ok(Self::PlistPayload), 74 | c => Err(ProtocolError::InvalidPacketType(c)), 75 | } 76 | } 77 | } 78 | #[repr(u32)] 79 | #[derive(Copy, Clone, Debug, PartialEq)] 80 | pub enum Protocol { 81 | Binary = 0, 82 | Plist = 1, 83 | } 84 | 85 | impl From for u32 { 86 | fn from(protocol: Protocol) -> Self { 87 | protocol as Self 88 | } 89 | } 90 | 91 | impl TryFrom for Protocol { 92 | type Error = ProtocolError; 93 | fn try_from(value: u32) -> Result { 94 | match value { 95 | 0 => Ok(Protocol::Binary), 96 | 1 => Ok(Protocol::Plist), 97 | c => Err(ProtocolError::InvalidProtocol(c)), 98 | } 99 | } 100 | } 101 | 102 | #[repr(u32)] 103 | #[derive(Copy, Clone, Debug, PartialEq)] 104 | pub enum ReplyCode { 105 | Ok = 0, 106 | BadCommand = 1, 107 | BadDevice = 2, 108 | ConnectionRefused = 3, 109 | // 4 unknown 110 | // 5 unknown 111 | BadVersion = 6, 112 | } 113 | 114 | impl From for u32 { 115 | fn from(code: ReplyCode) -> Self { 116 | code as Self 117 | } 118 | } 119 | 120 | impl TryFrom for ReplyCode { 121 | type Error = ProtocolError; 122 | fn try_from(value: u32) -> Result { 123 | match value { 124 | 0 => Ok(ReplyCode::Ok), 125 | 1 => Ok(ReplyCode::BadCommand), 126 | 2 => Ok(ReplyCode::BadDevice), 127 | 3 => Ok(ReplyCode::ConnectionRefused), 128 | 6 => Ok(ReplyCode::BadVersion), 129 | c => Err(ProtocolError::InvalidReplyCode(c)), 130 | } 131 | } 132 | } 133 | pub struct Packet { 134 | pub size: u32, 135 | pub protocol: Protocol, 136 | pub packet_type: PacketType, 137 | pub tag: u32, 138 | pub data: Vec, 139 | } 140 | impl fmt::Debug for Packet { 141 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 142 | write!( 143 | f, 144 | "Packet {{ size: {}, protocol: {:?}, packet_type: {:?}, tag: {}, payload(bytes): {} }}", 145 | self.size, 146 | self.protocol, 147 | self.packet_type, 148 | self.tag, 149 | self.data.len() 150 | ) 151 | } 152 | } 153 | impl Packet { 154 | pub fn new(protocol: Protocol, packet_type: PacketType, tag: u32, payload: Vec) -> Self { 155 | assert!( 156 | payload.len() < u32::max_value() as usize, 157 | "Payload too large" 158 | ); 159 | Packet { 160 | size: BASE_PACKET_SIZE + payload.len() as u32, 161 | protocol, 162 | packet_type, 163 | tag, 164 | data: payload, 165 | } 166 | } 167 | pub fn write_into(&self, writer: &mut W) -> Result<()> 168 | where 169 | W: Write, 170 | { 171 | writer.write_u32::(self.size).unwrap(); 172 | writer 173 | .write_u32::(self.protocol as u32) 174 | .unwrap(); 175 | writer 176 | .write_u32::(self.packet_type.into()) 177 | .unwrap(); 178 | writer.write_u32::(self.tag).unwrap(); 179 | writer.write_all(&self.data).unwrap(); 180 | Ok(()) 181 | } 182 | pub fn from_reader(reader: &mut R) -> Result 183 | where 184 | R: Read, 185 | { 186 | let size = reader.read_u32::()?; 187 | let protocol = Protocol::try_from(reader.read_u32::()?)?; 188 | let packet_type = PacketType::try_from(reader.read_u32::()?)?; 189 | let tag = reader.read_u32::()?; 190 | let payload_size = size - BASE_PACKET_SIZE; // get what's left 191 | let data = if payload_size > 0 { 192 | let mut payload = vec![0; payload_size as usize]; 193 | reader.read_exact(&mut payload)?; 194 | payload 195 | } else { 196 | vec![] 197 | }; 198 | let mut packet = Packet::new(protocol, packet_type, tag, data); 199 | packet.size = size; 200 | Ok(packet) 201 | } 202 | } 203 | 204 | #[derive(Debug, PartialEq, Copy, Clone)] 205 | pub enum MessageType { 206 | Paired, 207 | Result, 208 | Detached, 209 | Attached, 210 | } 211 | impl TryFrom<&Value> for MessageType { 212 | type Error = ProtocolError; 213 | fn try_from(value: &Value) -> Result { 214 | match value { 215 | Value::String(s) => match s.as_str() { 216 | "Paired" => Ok(MessageType::Paired), 217 | "Result" => Ok(MessageType::Result), 218 | "Attached" => Ok(MessageType::Attached), 219 | "Detached" => Ok(MessageType::Detached), 220 | s => Err(ProtocolError::InvalidMessageType(s.to_owned())), 221 | }, 222 | _ => Err(ProtocolError::InvalidMessageType( 223 | "Invalid PLIST type".to_owned(), 224 | )), 225 | } 226 | } 227 | } 228 | 229 | /// Device ID type, currently u64 to hold max value stored in plist 230 | pub type DeviceId = u64; 231 | /// Product type of connected device, which typically is an iPad, iPhone, or iPod touch 232 | #[derive(Debug, Clone, Copy, PartialEq)] 233 | pub enum ProductType { 234 | /// Any iPhone that's connected 235 | IPhone, 236 | /// iPod touch 237 | IPodTouch, 238 | /// iPad/iPad Pro 239 | IPad, 240 | /// Unexpected product id we haven't coded for yet 241 | Unknown(u16), 242 | } 243 | impl From for ProductType { 244 | fn from(product_id: u16) -> Self { 245 | match product_id { 246 | 0x12A8 => ProductType::IPhone, 247 | 0x12AA => ProductType::IPodTouch, 248 | 0x12AB => ProductType::IPad, 249 | p => ProductType::Unknown(p), 250 | } 251 | } 252 | } 253 | /// How device is connected 254 | #[derive(Debug, PartialEq)] 255 | pub enum DeviceConnectionType { 256 | /// USB connection type 257 | USB, 258 | /// Wi-fi maybe? have yet to see it 259 | Unknown(String), 260 | } 261 | impl TryFrom<&Value> for DeviceConnectionType { 262 | type Error = ProtocolError; 263 | fn try_from(value: &Value) -> Result { 264 | match value.as_string() { 265 | Some("USB") => Ok(DeviceConnectionType::USB), 266 | Some(s) => Ok(DeviceConnectionType::Unknown(s.to_owned())), 267 | None => Err(ProtocolError::InvalidPlistEntryForKey("ConnectionType")), 268 | } 269 | } 270 | } 271 | /// Info about an attached device 272 | #[derive(Debug)] 273 | pub struct DeviceAttachedInfo { 274 | /// Type of connection device is using (USB or otherwise) 275 | pub connection_type: DeviceConnectionType, 276 | /// ID of device 277 | pub device_id: DeviceId, 278 | /// Unknown purpose/value 279 | pub location_id: u64, 280 | /// Product type of device, ipad, ipod, iphone, mysterious other device 281 | pub product_type: ProductType, 282 | /// Device's identifier/serial 283 | pub identifier: String, 284 | } 285 | // TODO: this likely could be done from within serde maybe? custom deserialization? 286 | impl TryFrom<&Value> for DeviceAttachedInfo { 287 | type Error = ProtocolError; 288 | fn try_from(value: &Value) -> Result { 289 | match value { 290 | Value::Dictionary(d) => { 291 | let connection_type = d 292 | .get("ConnectionType") 293 | .and_then(|t| DeviceConnectionType::try_from(t).ok()) 294 | .ok_or(ProtocolError::InvalidPlistEntryForKey("ConnectionType"))?; 295 | let device_id = d 296 | .get(USB_DEVICE_ID_KEY) 297 | .and_then(Value::as_unsigned_integer) 298 | .ok_or(ProtocolError::InvalidPlistEntryForKey(USB_DEVICE_ID_KEY))?; 299 | let location_id = d 300 | .get("LocationID") 301 | .and_then(Value::as_unsigned_integer) 302 | .ok_or(ProtocolError::InvalidPlistEntryForKey("LocationID"))?; 303 | let product_type = d 304 | .get("ProductID") 305 | .and_then(Value::as_unsigned_integer) 306 | .map(|i| ProductType::from(i as u16)) // product_id is USB product_id which is u16 307 | .ok_or(ProtocolError::InvalidPlistEntryForKey("ProductID"))?; 308 | let identifier = d 309 | .get("SerialNumber") 310 | .and_then(Value::as_string) 311 | .ok_or(ProtocolError::InvalidPlistEntryForKey("SerialNumber"))? 312 | .to_owned(); 313 | Ok(DeviceAttachedInfo { 314 | connection_type, 315 | device_id, 316 | location_id, 317 | product_type, 318 | identifier, 319 | }) 320 | } 321 | _ => Err(ProtocolError::InvalidPlistEntry), 322 | } 323 | } 324 | } 325 | #[derive(Debug)] 326 | /// Event that can occur on device listener 327 | pub enum DeviceEvent { 328 | /// Device was plugged into host 329 | Attached(DeviceAttachedInfo), 330 | /// Device was unplugged from host 331 | Detached(DeviceId), 332 | /// Device was paired to host (trusting computer was authorized) 333 | Paired(DeviceId), 334 | } 335 | impl TryFrom<&Value> for DeviceEvent { 336 | type Error = ProtocolError; 337 | fn try_from(value: &Value) -> Result { 338 | match value { 339 | Value::Dictionary(d) => { 340 | let msg_type = MessageType::try_from(d.get(USB_MESSAGE_TYPE_KEY).unwrap())?; 341 | let device_id = d 342 | .get(USB_DEVICE_ID_KEY) 343 | .and_then(Value::as_unsigned_integer) 344 | .ok_or(ProtocolError::InvalidPlistEntryForKey(USB_DEVICE_ID_KEY))?; 345 | match msg_type { 346 | MessageType::Attached => { 347 | let device_info = d 348 | .get(USB_DEVICE_PROPERTIES_KEY) 349 | .and_then(|p| DeviceAttachedInfo::try_from(p).ok()) 350 | .ok_or(ProtocolError::InvalidPlistEntryForKey( 351 | USB_DEVICE_PROPERTIES_KEY, 352 | ))?; 353 | Ok(DeviceEvent::Attached(device_info)) 354 | } 355 | MessageType::Detached => Ok(DeviceEvent::Detached(device_id)), 356 | MessageType::Paired => Ok(DeviceEvent::Paired(device_id)), 357 | MessageType::Result => { 358 | Err(ProtocolError::InvalidMessageType("Result".to_owned())) 359 | } 360 | } 361 | } 362 | _ => Err(ProtocolError::InvalidPlistEntry), 363 | } 364 | } 365 | } 366 | impl DeviceEvent { 367 | pub(crate) fn from_vec(data: Vec) -> Result { 368 | let cursor = std::io::Cursor::new(&data[..]); 369 | let dict: Value = Value::from_reader(cursor).unwrap(); 370 | DeviceEvent::try_from(&dict) 371 | } 372 | } 373 | 374 | #[derive(Debug)] 375 | pub struct ResultMessage(pub i64); 376 | impl ResultMessage { 377 | pub fn from_reader(reader: R) -> Result { 378 | let r: plist::Value = plist::Value::from_reader(reader).unwrap(); 379 | ResultMessage::try_from(&r) 380 | } 381 | } 382 | impl TryFrom<&Value> for ResultMessage { 383 | type Error = ProtocolError; 384 | fn try_from(value: &Value) -> Result { 385 | match value { 386 | Value::Dictionary(d) => { 387 | let num = d 388 | .get("Number") 389 | .and_then(Value::as_signed_integer) 390 | .ok_or(ProtocolError::InvalidPlistEntryForKey("SerialNumber"))?; 391 | Ok(ResultMessage(num)) 392 | } 393 | _ => Err(ProtocolError::InvalidPlistEntry), 394 | } 395 | } 396 | } 397 | 398 | #[derive(Serialize, Deserialize)] 399 | pub struct Command { 400 | #[serde(rename = "MessageType")] 401 | message_type: String, 402 | #[serde(rename = "ProgName")] 403 | prog_name: String, 404 | #[serde(rename = "ClientVersionString")] 405 | client_version_string: String, 406 | #[serde(rename = "PortNumber")] 407 | port_number: Option, 408 | #[serde(rename = "DeviceID")] 409 | device_id: Option, 410 | } 411 | impl Command { 412 | fn new>(command: C) -> Self { 413 | Command { 414 | message_type: command.as_ref().to_owned(), 415 | prog_name: String::from("Peertalk Example"), 416 | client_version_string: String::from("1"), 417 | port_number: None, 418 | device_id: None, 419 | } 420 | } 421 | pub fn listen() -> Self { 422 | Command::new("Listen") 423 | } 424 | pub fn connect(port: u16, device_id: DeviceId) -> Self { 425 | let mut command = Command::new("Connect"); 426 | command.port_number = Some(port.to_be()); // apple's service expects network byte order 427 | command.device_id = Some(device_id); 428 | command 429 | } 430 | pub fn to_bytes(&self) -> Vec { 431 | let mut payload: Vec = Vec::new(); 432 | plist::to_writer_xml(&mut payload, &self).unwrap(); 433 | assert_ne!(payload.len(), 0, "Should have > 0 bytes payload"); 434 | payload 435 | } 436 | } 437 | 438 | #[cfg(test)] 439 | mod tests { 440 | use super::*; 441 | fn value_for_testfile(file: &str) -> plist::Value { 442 | let mut path = std::path::PathBuf::new(); 443 | path.push("test_data"); 444 | path.push(file); 445 | plist::Value::from_file(path).unwrap() 446 | } 447 | #[test] 448 | fn it_decodes_plists() { 449 | let r = value_for_testfile("detached.plist"); 450 | match DeviceEvent::try_from(&r) { 451 | Ok(DeviceEvent::Detached(device_id)) => assert_eq!(device_id, 3), 452 | _ => assert!(false, "Invalid DeviceEvent"), 453 | } 454 | let r = value_for_testfile("paired.plist"); 455 | match DeviceEvent::try_from(&r) { 456 | Ok(DeviceEvent::Paired(device_id)) => assert_eq!(device_id, 3), 457 | _ => assert!(false, "Invalid DeviceEvent"), 458 | } 459 | let r = value_for_testfile("success-result.plist"); 460 | let msg = ResultMessage::try_from(&r); 461 | assert!(msg.is_ok()); 462 | println!("Test: {:?}", msg); 463 | } 464 | #[test] 465 | fn it_decodes_attached() { 466 | let r = value_for_testfile("attached.plist"); 467 | let msg = DeviceEvent::try_from(&r); 468 | assert!(msg.is_ok()); 469 | match DeviceEvent::try_from(&r) { 470 | Ok(DeviceEvent::Attached(device_info)) => { 471 | assert_eq!(device_info.device_id, 3); 472 | assert_eq!(device_info.connection_type, DeviceConnectionType::USB); 473 | assert_eq!(device_info.location_id, 0); 474 | assert_eq!(device_info.product_type, ProductType::IPad); 475 | assert_eq!(device_info.identifier, "00001011-000A111E0111001E"); 476 | } 477 | _ => assert!(false, "Invalid DeviceEvent"), 478 | } 479 | } 480 | 481 | #[test] 482 | fn it_decodes_command() { 483 | let command: Command = plist::from_file("test_data/command.plist").unwrap(); 484 | assert_eq!(command.message_type, "Listen"); 485 | assert_eq!(command.prog_name, "MyApp"); 486 | assert_eq!(command.client_version_string, "1.0"); 487 | } 488 | #[test] 489 | fn it_encodes_command() { 490 | let mut command = Command::new("Connect"); 491 | command.port_number = Some(12345); 492 | command.device_id = Some(16689); 493 | plist::to_file_xml("test.plist", &command).unwrap(); 494 | } 495 | } 496 | --------------------------------------------------------------------------------