├── .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 |
--------------------------------------------------------------------------------