adb_wire/lib.rs
1//! Async ADB wire protocol client over TCP.
2//!
3//! Talks directly to the ADB server (`localhost:5037`) using the
4//! [ADB protocol](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/main/OVERVIEW.TXT)
5//! without spawning any child processes.
6//!
7//! # Quick start
8//!
9//! ```no_run
10//! use adb_wire::AdbWire;
11//!
12//! #[tokio::main(flavor = "current_thread")]
13//! async fn main() {
14//! let adb = AdbWire::new("emulator-5554");
15//!
16//! // Run a command — returns stdout, stderr, and exit code
17//! let result = adb.shell("getprop ro.build.version.sdk").await.unwrap();
18//! println!("SDK version: {}", result.stdout_str());
19//!
20//! // Check exit status
21//! let result = adb.shell("ls /nonexistent").await.unwrap();
22//! if !result.success() {
23//! eprintln!("exit {}: {}", result.exit_code, result.stderr_str());
24//! }
25//!
26//! // Fire-and-forget (returns immediately, command runs on device)
27//! adb.shell_detach("input tap 500 500").await.unwrap();
28//!
29//! // Stream output incrementally (e.g. logcat)
30//! use tokio::io::{AsyncBufReadExt, BufReader};
31//! let stream = adb.shell_stream("logcat -d").await.unwrap();
32//! let mut lines = BufReader::new(stream).lines();
33//! while let Some(line) = lines.next_line().await.unwrap() {
34//! println!("{line}");
35//! }
36//!
37//! // File transfer
38//! adb.push_file("local.txt", "/sdcard/remote.txt").await.unwrap();
39//! adb.pull_file("/sdcard/remote.txt", "downloaded.txt").await.unwrap();
40//!
41//! // Check if a remote file exists
42//! let st = adb.stat("/sdcard/remote.txt").await.unwrap();
43//! println!("exists={} size={}", st.exists(), st.size);
44//!
45//! // Install an APK
46//! adb.install("app.apk").await.unwrap();
47//! }
48//! ```
49//!
50//! # Host commands
51//!
52//! Commands that target the ADB server (not a specific device) are free
53//! functions:
54//!
55//! ```no_run
56//! use adb_wire::{list_devices, DEFAULT_SERVER};
57//!
58//! #[tokio::main(flavor = "current_thread")]
59//! async fn main() {
60//! for (serial, state) in list_devices(DEFAULT_SERVER).await.unwrap() {
61//! println!("{serial}\t{state}");
62//! }
63//! }
64//! ```
65
66mod error;
67mod shell;
68mod sync;
69mod wire;
70
71pub use error::{Error, Result};
72pub use shell::{ShellOutput, ShellStream};
73pub use sync::{DirEntry, RemoteStat};
74
75use std::net::SocketAddr;
76use std::time::Duration;
77
78use tokio::net::TcpStream;
79
80/// Default ADB server address (`127.0.0.1:5037`).
81pub const DEFAULT_SERVER: SocketAddr =
82 SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), 5037);
83
84// -- Host-level (device-independent) functions --------------------------------
85
86/// Send a host command to an ADB server and read the length-prefixed response.
87///
88/// These commands don't target a specific device. Examples:
89/// `"host:version"`, `"host:devices"`, `"host:track-devices"`.
90pub async fn host_command(server: SocketAddr, cmd: &str) -> Result<String> {
91 use tokio::io::AsyncReadExt;
92 let mut stream = tokio::time::timeout(
93 Duration::from_secs(2),
94 TcpStream::connect(server),
95 )
96 .await
97 .map_err(|_| Error::timed_out("connect timed out"))??;
98 stream.set_nodelay(true)?;
99
100 wire::send(&mut stream, cmd).await?;
101 wire::read_okay(&mut stream).await?;
102
103 // Response length is bounded by read_hex_len (max 1 MiB).
104 let len = wire::read_hex_len(&mut stream).await?;
105 let mut buf = vec![0u8; len];
106 stream.read_exact(&mut buf).await?;
107 String::from_utf8(buf)
108 .map_err(|e| Error::Protocol(format!("invalid utf-8 in response: {e}")))
109}
110
111/// Connect a TCP/IP device to the ADB server.
112///
113/// Equivalent to `adb connect <addr>`. The `addr` should be `"host:port"`,
114/// e.g. `"192.168.1.100:5555"`.
115///
116/// ```no_run
117/// use adb_wire::{connect_device, DEFAULT_SERVER};
118///
119/// # async fn example() {
120/// connect_device(DEFAULT_SERVER, "192.168.1.100:5555").await.unwrap();
121/// # }
122/// ```
123pub async fn connect_device(server: SocketAddr, addr: &str) -> Result<String> {
124 host_command(server, &format!("host:connect:{addr}")).await
125}
126
127/// Disconnect a TCP/IP device from the ADB server.
128///
129/// Equivalent to `adb disconnect <addr>`.
130///
131/// ```no_run
132/// use adb_wire::{disconnect_device, DEFAULT_SERVER};
133///
134/// # async fn example() {
135/// disconnect_device(DEFAULT_SERVER, "192.168.1.100:5555").await.unwrap();
136/// # }
137/// ```
138pub async fn disconnect_device(server: SocketAddr, addr: &str) -> Result<String> {
139 host_command(server, &format!("host:disconnect:{addr}")).await
140}
141
142/// List connected devices. Returns `(serial, state)` pairs.
143///
144/// `state` is typically `"device"`, `"offline"`, or `"unauthorized"`.
145pub async fn list_devices(server: SocketAddr) -> Result<Vec<(String, String)>> {
146 let raw = host_command(server, "host:devices").await?;
147 Ok(parse_device_list(&raw))
148}
149
150fn parse_device_list(raw: &str) -> Vec<(String, String)> {
151 raw.lines()
152 .filter_map(|line| {
153 let (serial, state) = line.split_once('\t')?;
154 Some((serial.to_string(), state.to_string()))
155 })
156 .collect()
157}
158
159/// Append a single-quoted, shell-safe version of `s` to `out`.
160///
161/// Wraps the value in single quotes, escaping any embedded single quotes
162/// with the `'\''` idiom. This prevents shell metacharacter injection.
163fn shell_quote_into(out: &mut String, s: &str) {
164 out.push('\'');
165 for ch in s.chars() {
166 if ch == '\'' {
167 out.push_str("'\\''");
168 } else {
169 out.push(ch);
170 }
171 }
172 out.push('\'');
173}
174
175// -- Device client ------------------------------------------------------------
176
177/// Async ADB wire protocol client bound to a specific device.
178///
179/// Each method opens a fresh TCP connection to the ADB server, so this type
180/// holds no open connections. All fields are plain data, making `AdbWire`
181/// [`Send`], [`Sync`], and [`Clone`].
182#[derive(Clone, Debug)]
183pub struct AdbWire {
184 serial: String,
185 server: SocketAddr,
186 connect_timeout: Duration,
187 read_timeout: Duration,
188}
189
190impl AdbWire {
191 /// Create a client for the given device serial (e.g. `"emulator-5554"`,
192 /// `"127.0.0.1:5555"`, `"R5CR1234567"`).
193 pub fn new(serial: &str) -> Self {
194 Self {
195 serial: serial.to_string(),
196 server: DEFAULT_SERVER,
197 connect_timeout: Duration::from_secs(2),
198 read_timeout: Duration::from_secs(30),
199 }
200 }
201
202 /// Use a custom ADB server address (default: `127.0.0.1:5037`).
203 pub fn server(mut self, addr: SocketAddr) -> Self {
204 self.server = addr;
205 self
206 }
207
208 /// Set the TCP connect timeout (default: 2s).
209 pub fn connect_timeout(mut self, dur: Duration) -> Self {
210 self.connect_timeout = dur;
211 self
212 }
213
214 /// Set the read timeout for buffered methods like [`shell`](Self::shell)
215 /// (default: 30s). For streaming methods, manage timeouts on the returned
216 /// stream directly.
217 pub fn read_timeout(mut self, dur: Duration) -> Self {
218 self.read_timeout = dur;
219 self
220 }
221
222 /// Get the device serial.
223 pub fn serial(&self) -> &str {
224 &self.serial
225 }
226
227 // -- Shell ----------------------------------------------------------------
228
229 /// Run a shell command, returning separated stdout, stderr, and exit code.
230 ///
231 /// ```no_run
232 /// # use adb_wire::AdbWire;
233 /// # async fn example() {
234 /// let adb = AdbWire::new("emulator-5554");
235 /// let result = adb.shell("ls /sdcard").await.unwrap();
236 /// if result.success() {
237 /// println!("{}", result.stdout_str());
238 /// } else {
239 /// eprintln!("exit {}: {}", result.exit_code, result.stderr_str());
240 /// }
241 /// # }
242 /// ```
243 pub async fn shell(&self, cmd: &str) -> Result<ShellOutput> {
244 let stream = self.connect_shell(cmd).await?;
245 self.with_timeout(shell::read_shell(stream)).await?
246 }
247
248 /// Run a shell command and return a streaming reader.
249 ///
250 /// The returned [`ShellStream`] implements [`AsyncRead`](tokio::io::AsyncRead)
251 /// yielding stdout bytes. Use [`collect_output`](ShellStream::collect_output)
252 /// to buffer everything, or read incrementally for long-running commands.
253 ///
254 /// ```no_run
255 /// # use adb_wire::AdbWire;
256 /// # async fn example() {
257 /// use tokio::io::{AsyncBufReadExt, BufReader};
258 ///
259 /// let adb = AdbWire::new("emulator-5554");
260 /// let stream = adb.shell_stream("logcat").await.unwrap();
261 /// let mut lines = BufReader::new(stream).lines();
262 /// while let Some(line) = lines.next_line().await.unwrap() {
263 /// println!("{line}");
264 /// }
265 /// # }
266 /// ```
267 pub async fn shell_stream(&self, cmd: &str) -> Result<ShellStream> {
268 Ok(ShellStream::new(self.connect_shell(cmd).await?))
269 }
270
271 /// Send a shell command without waiting for output.
272 ///
273 /// Opens a shell connection and immediately drops it. The ADB server
274 /// will still deliver the command to the device, but the command may
275 /// not have *started* on the device by the time this method returns.
276 ///
277 /// **Warning:** Because the connection is dropped immediately, there is
278 /// no guarantee the command will execute. If the device has not received
279 /// the command before the TCP connection closes, it will be lost. Use
280 /// [`shell`](Self::shell) if you need confirmation that the command ran.
281 ///
282 /// Best suited for input commands (tap, swipe, keyevent) where occasional
283 /// drops are acceptable.
284 pub async fn shell_detach(&self, cmd: &str) -> Result<()> {
285 let mut stream = self.connect_shell(cmd).await?;
286 // Wait briefly for the server to acknowledge, giving it time to
287 // forward the command to the device before we drop the connection.
288 let mut header = [0u8; 5];
289 let _ = tokio::time::timeout(
290 Duration::from_millis(50),
291 tokio::io::AsyncReadExt::read_exact(&mut stream, &mut header),
292 )
293 .await;
294 Ok(())
295 }
296
297 // -- File transfer (sync protocol) ----------------------------------------
298
299 /// Query file metadata on the device.
300 ///
301 /// Returns a [`RemoteStat`] with mode, size, and modification time.
302 /// If the path does not exist, all fields will be zero —
303 /// check with [`RemoteStat::exists()`].
304 ///
305 /// ```no_run
306 /// # use adb_wire::AdbWire;
307 /// # async fn example() {
308 /// let adb = AdbWire::new("emulator-5554");
309 /// let st = adb.stat("/sdcard/Download/test.txt").await.unwrap();
310 /// if st.exists() {
311 /// println!("size: {} bytes, mode: {:o}", st.size, st.mode);
312 /// }
313 /// # }
314 /// ```
315 pub async fn stat(&self, remote_path: &str) -> Result<RemoteStat> {
316 // Try STAT2 first for large file / extended metadata support.
317 let mut stream = self.connect_cmd("sync:").await?;
318 match sync::stat2_sync(&mut stream, remote_path).await {
319 Ok(st) => Ok(st),
320 Err(_) => {
321 // Fall back to STAT v1 on a fresh sync connection.
322 let mut stream = self.connect_cmd("sync:").await?;
323 sync::stat_v1_sync(&mut stream, remote_path).await
324 }
325 }
326 }
327
328 /// List files in a remote directory.
329 ///
330 /// Returns entries excluding `.` and `..`. Uses the sync LIST protocol.
331 ///
332 /// ```no_run
333 /// # use adb_wire::AdbWire;
334 /// # async fn example() {
335 /// let adb = AdbWire::new("emulator-5554");
336 /// for entry in adb.list_dir("/sdcard").await.unwrap() {
337 /// println!("{}\t{} bytes", entry.name, entry.size);
338 /// }
339 /// # }
340 /// ```
341 pub async fn list_dir(&self, remote_path: &str) -> Result<Vec<DirEntry>> {
342 let mut stream = self.connect_cmd("sync:").await?;
343 sync::list_sync(&mut stream, remote_path).await
344 }
345
346 /// Pull a file from the device, writing its contents to `writer`.
347 ///
348 /// Returns the number of bytes written.
349 pub async fn pull(
350 &self,
351 remote_path: &str,
352 writer: &mut (impl tokio::io::AsyncWrite + Unpin),
353 ) -> Result<u64> {
354 let mut stream = self.connect_cmd("sync:").await?;
355 sync::pull_sync(&mut stream, remote_path, writer).await
356 }
357
358 /// Pull a file from the device to a local path.
359 pub async fn pull_file(
360 &self,
361 remote_path: &str,
362 local_path: impl AsRef<std::path::Path>,
363 ) -> Result<u64> {
364 let mut f = tokio::fs::File::create(local_path.as_ref()).await?;
365 self.pull(remote_path, &mut f).await
366 }
367
368 /// Push data from `reader` to a file on the device.
369 ///
370 /// `mode` is the Unix file permission as an octal value (e.g. `0o644`,
371 /// `0o755`). `mtime` is seconds since the Unix epoch.
372 pub async fn push(
373 &self,
374 remote_path: &str,
375 mode: u32,
376 mtime: u32,
377 reader: &mut (impl tokio::io::AsyncRead + Unpin),
378 ) -> Result<()> {
379 let mut stream = self.connect_cmd("sync:").await?;
380 sync::push_sync(&mut stream, remote_path, mode, mtime, reader).await
381 }
382
383 /// Push a local file to the device with mode `0o644`.
384 pub async fn push_file(
385 &self,
386 local_path: impl AsRef<std::path::Path>,
387 remote_path: &str,
388 ) -> Result<()> {
389 self.push_file_with_mode(local_path, remote_path, 0o644).await
390 }
391
392 /// Push a local file to the device with a custom Unix file mode.
393 ///
394 /// ```no_run
395 /// # use adb_wire::AdbWire;
396 /// # async fn example() {
397 /// let adb = AdbWire::new("emulator-5554");
398 /// adb.push_file_with_mode("run.sh", "/data/local/tmp/run.sh", 0o755).await.unwrap();
399 /// # }
400 /// ```
401 pub async fn push_file_with_mode(
402 &self,
403 local_path: impl AsRef<std::path::Path>,
404 remote_path: &str,
405 mode: u32,
406 ) -> Result<()> {
407 let path = local_path.as_ref();
408 let meta = tokio::fs::metadata(path).await?;
409 let mtime = meta.modified().ok()
410 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
411 .map_or(0, |d| d.as_secs() as u32);
412 let mut f = tokio::fs::File::open(path).await?;
413 self.push(remote_path, mode, mtime, &mut f).await
414 }
415
416 // -- Install --------------------------------------------------------------
417
418 /// Install an APK on the device. Equivalent to `adb install <path>`.
419 ///
420 /// ```no_run
421 /// # use adb_wire::AdbWire;
422 /// # async fn example() {
423 /// let adb = AdbWire::new("emulator-5554");
424 /// adb.install("app-debug.apk").await.unwrap();
425 /// # }
426 /// ```
427 pub async fn install(&self, local_apk: impl AsRef<std::path::Path>) -> Result<()> {
428 self.install_with_args(local_apk, &[]).await
429 }
430
431 /// Install an APK with additional `pm install` flags.
432 ///
433 /// ```no_run
434 /// # use adb_wire::AdbWire;
435 /// # async fn example() {
436 /// let adb = AdbWire::new("emulator-5554");
437 /// adb.install_with_args("app.apk", &["-r", "-d"]).await.unwrap();
438 /// # }
439 /// ```
440 pub async fn install_with_args(
441 &self,
442 local_apk: impl AsRef<std::path::Path>,
443 args: &[&str],
444 ) -> Result<()> {
445 let local = local_apk.as_ref();
446 let filename = local
447 .file_name()
448 .and_then(|n| n.to_str())
449 .unwrap_or("install.apk");
450
451 // Reject filenames that could escape the shell or path.
452 if filename.contains('/') || filename.contains('\0') || filename.contains('\'') {
453 return Err(Error::Adb(format!("unsafe filename: {filename}")));
454 }
455
456 let remote = format!("/data/local/tmp/{filename}");
457 self.push_file_with_mode(local, &remote, 0o644).await?;
458
459 let mut cmd = String::from("pm install");
460 for arg in args {
461 cmd.push(' ');
462 shell_quote_into(&mut cmd, arg);
463 }
464 cmd.push(' ');
465 shell_quote_into(&mut cmd, &remote);
466
467 let result = self.shell(&cmd).await?;
468
469 // Best-effort cleanup of the temp APK.
470 let cleanup_ok = self
471 .shell(&format!("rm -f '{remote}'"))
472 .await
473 .map(|r| r.success())
474 .unwrap_or(false);
475
476 if result.success() {
477 Ok(())
478 } else {
479 let output = if result.stderr.is_empty() {
480 result.stdout_str()
481 } else {
482 result.stderr_str()
483 };
484 let mut msg = format!("pm install failed (exit {}): {output}", result.exit_code);
485 if !cleanup_ok {
486 msg.push_str(&format!(" (cleanup of {remote} also failed)"));
487 }
488 Err(Error::Adb(msg))
489 }
490 }
491
492 // -- Port forwarding ------------------------------------------------------
493
494 /// Set up port forwarding from a local socket to the device.
495 ///
496 /// `local` and `remote` are ADB socket specs, e.g. `"tcp:8080"`,
497 /// `"localabstract:chrome_devtools_remote"`.
498 pub async fn forward(&self, local: &str, remote: &str) -> Result<()> {
499 let mut stream = self.connect_raw().await?;
500 let cmd = format!("host-serial:{}:forward:{};{}", self.serial, local, remote);
501 wire::send(&mut stream, &cmd).await?;
502 wire::read_okay(&mut stream).await?;
503 wire::read_okay(&mut stream).await?; // second OKAY after setup
504 Ok(())
505 }
506
507 /// Set up reverse port forwarding from the device to a local socket.
508 pub async fn reverse(&self, remote: &str, local: &str) -> Result<()> {
509 let mut stream = self.connect_transport().await?;
510 let cmd = format!("reverse:forward:{remote};{local}");
511 wire::send(&mut stream, &cmd).await?;
512 wire::read_okay(&mut stream).await?;
513 Ok(())
514 }
515
516 // -- Lifecycle ------------------------------------------------------------
517
518 /// Wait until the device is online.
519 ///
520 /// Blocks until the ADB server reports the device as available.
521 /// Equivalent to `adb -s <serial> wait-for-device`.
522 ///
523 /// ```no_run
524 /// # use adb_wire::AdbWire;
525 /// # async fn example() {
526 /// let adb = AdbWire::new("192.168.1.100:5555");
527 /// adb.wait_for_device().await.unwrap();
528 /// println!("device online");
529 /// # }
530 /// ```
531 pub async fn wait_for_device(&self) -> Result<()> {
532 let mut stream = self.connect_raw().await?;
533 let cmd = format!("host-serial:{}:wait-for-any-device", self.serial);
534 wire::send(&mut stream, &cmd).await?;
535 wire::read_okay(&mut stream).await?;
536 // Server sends a second OKAY when the device reaches the requested state.
537 wire::read_okay(&mut stream).await?;
538 Ok(())
539 }
540
541 /// Wait until the device has finished booting.
542 ///
543 /// Waits for the device to come online, then polls `sys.boot_completed`
544 /// until it reports `1`.
545 ///
546 /// ```no_run
547 /// # use adb_wire::AdbWire;
548 /// # async fn example() {
549 /// let adb = AdbWire::new("emulator-5554");
550 /// adb.wait_for_boot().await.unwrap();
551 /// println!("device booted");
552 /// # }
553 /// ```
554 pub async fn wait_for_boot(&self) -> Result<()> {
555 self.wait_for_device().await?;
556 loop {
557 if let Ok(out) = self.shell("getprop sys.boot_completed").await {
558 if out.stdout_str() == "1" {
559 return Ok(());
560 }
561 }
562 tokio::time::sleep(Duration::from_secs(1)).await;
563 }
564 }
565
566 // -- Connection internals -------------------------------------------------
567
568 async fn connect_transport(&self) -> Result<TcpStream> {
569 let mut stream = self.connect_raw().await?;
570 wire::send(&mut stream, &format!("host:transport:{}", self.serial)).await?;
571 wire::read_okay(&mut stream).await?;
572 Ok(stream)
573 }
574
575 async fn connect_cmd(&self, cmd: &str) -> Result<TcpStream> {
576 let mut stream = self.connect_transport().await?;
577 wire::send(&mut stream, cmd).await?;
578 wire::read_okay(&mut stream).await?;
579 Ok(stream)
580 }
581
582 async fn connect_shell(&self, cmd: &str) -> Result<TcpStream> {
583 self.connect_cmd(&format!("shell,v2,raw:{cmd}")).await
584 }
585
586 async fn with_timeout<F: std::future::Future>(&self, f: F) -> Result<F::Output> {
587 tokio::time::timeout(self.read_timeout, f)
588 .await
589 .map_err(|_| Error::timed_out("read timed out"))
590 }
591
592 async fn connect_raw(&self) -> Result<TcpStream> {
593 let stream = tokio::time::timeout(
594 self.connect_timeout,
595 TcpStream::connect(self.server),
596 )
597 .await
598 .map_err(|_| Error::timed_out("connect timed out"))??;
599 stream.set_nodelay(true)?;
600 Ok(stream)
601 }
602}
603
604// -- Tests --------------------------------------------------------------------
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use std::io::Cursor;
610
611 #[test]
612 fn new_defaults() {
613 let adb = AdbWire::new("emulator-5554");
614 assert_eq!(adb.serial(), "emulator-5554");
615 assert_eq!(adb.server, DEFAULT_SERVER);
616 assert_eq!(adb.read_timeout, Duration::from_secs(30));
617 }
618
619 #[test]
620 fn builder() {
621 let addr = SocketAddr::from(([192, 168, 1, 100], 5037));
622 let adb = AdbWire::new("device123")
623 .server(addr)
624 .connect_timeout(Duration::from_millis(500))
625 .read_timeout(Duration::from_secs(60));
626 assert_eq!(adb.server, addr);
627 assert_eq!(adb.connect_timeout, Duration::from_millis(500));
628 assert_eq!(adb.read_timeout, Duration::from_secs(60));
629 }
630
631 #[tokio::test]
632 async fn send_format() {
633 let mut buf = Vec::new();
634 wire::send(&mut buf, "host:version").await.unwrap();
635 assert_eq!(&buf, b"000chost:version");
636 }
637
638 #[tokio::test]
639 async fn okay_response() {
640 let mut cur = Cursor::new(b"OKAY".to_vec());
641 assert!(wire::read_okay(&mut cur).await.is_ok());
642 }
643
644 #[tokio::test]
645 async fn fail_response() {
646 let mut cur = Cursor::new(b"FAIL0005nope!".to_vec());
647 let err = wire::read_okay(&mut cur).await.unwrap_err();
648 assert!(matches!(err, Error::Adb(msg) if msg == "nope!"));
649 }
650
651 #[tokio::test]
652 async fn hex_len() {
653 let mut cur = Cursor::new(b"001f".to_vec());
654 assert_eq!(wire::read_hex_len(&mut cur).await.unwrap(), 31);
655 }
656
657 #[tokio::test]
658 async fn bad_status() {
659 let mut cur = Cursor::new(b"WHAT".to_vec());
660 assert!(matches!(
661 wire::read_okay(&mut cur).await.unwrap_err(),
662 Error::Protocol(_)
663 ));
664 }
665
666 #[test]
667 fn parse_devices() {
668 let raw = "emulator-5554\tdevice\nR5CR1234567\toffline\n";
669 let devices = parse_device_list(raw);
670 assert_eq!(
671 devices,
672 vec![
673 ("emulator-5554".into(), "device".into()),
674 ("R5CR1234567".into(), "offline".into()),
675 ]
676 );
677 }
678
679 #[test]
680 fn parse_devices_empty() {
681 assert!(parse_device_list("").is_empty());
682 assert!(parse_device_list("\n").is_empty());
683 }
684
685 #[test]
686 fn shell_quote_simple() {
687 let mut out = String::new();
688 shell_quote_into(&mut out, "hello");
689 assert_eq!(out, "'hello'");
690 }
691
692 #[test]
693 fn shell_quote_with_spaces_and_semicolons() {
694 let mut out = String::new();
695 shell_quote_into(&mut out, "foo; rm -rf /");
696 assert_eq!(out, "'foo; rm -rf /'");
697 }
698
699 #[test]
700 fn shell_quote_with_single_quotes() {
701 let mut out = String::new();
702 shell_quote_into(&mut out, "it's");
703 assert_eq!(out, "'it'\\''s'");
704 }
705}