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/// List connected devices. Returns `(serial, state)` pairs.
112///
113/// `state` is typically `"device"`, `"offline"`, or `"unauthorized"`.
114pub async fn list_devices(server: SocketAddr) -> Result<Vec<(String, String)>> {
115 let raw = host_command(server, "host:devices").await?;
116 Ok(parse_device_list(&raw))
117}
118
119fn parse_device_list(raw: &str) -> Vec<(String, String)> {
120 raw.lines()
121 .filter_map(|line| {
122 let (serial, state) = line.split_once('\t')?;
123 Some((serial.to_string(), state.to_string()))
124 })
125 .collect()
126}
127
128/// Append a single-quoted, shell-safe version of `s` to `out`.
129///
130/// Wraps the value in single quotes, escaping any embedded single quotes
131/// with the `'\''` idiom. This prevents shell metacharacter injection.
132fn shell_quote_into(out: &mut String, s: &str) {
133 out.push('\'');
134 for ch in s.chars() {
135 if ch == '\'' {
136 out.push_str("'\\''");
137 } else {
138 out.push(ch);
139 }
140 }
141 out.push('\'');
142}
143
144// -- Device client ------------------------------------------------------------
145
146/// Async ADB wire protocol client bound to a specific device.
147///
148/// Each method opens a fresh TCP connection to the ADB server, so this type
149/// holds no open connections. All fields are plain data, making `AdbWire`
150/// [`Send`], [`Sync`], and [`Clone`].
151#[derive(Clone, Debug)]
152pub struct AdbWire {
153 serial: String,
154 server: SocketAddr,
155 connect_timeout: Duration,
156 read_timeout: Duration,
157}
158
159impl AdbWire {
160 /// Create a client for the given device serial (e.g. `"emulator-5554"`,
161 /// `"127.0.0.1:5555"`, `"R5CR1234567"`).
162 pub fn new(serial: &str) -> Self {
163 Self {
164 serial: serial.to_string(),
165 server: DEFAULT_SERVER,
166 connect_timeout: Duration::from_secs(2),
167 read_timeout: Duration::from_secs(30),
168 }
169 }
170
171 /// Use a custom ADB server address (default: `127.0.0.1:5037`).
172 pub fn server(mut self, addr: SocketAddr) -> Self {
173 self.server = addr;
174 self
175 }
176
177 /// Set the TCP connect timeout (default: 2s).
178 pub fn connect_timeout(mut self, dur: Duration) -> Self {
179 self.connect_timeout = dur;
180 self
181 }
182
183 /// Set the read timeout for buffered methods like [`shell`](Self::shell)
184 /// (default: 30s). For streaming methods, manage timeouts on the returned
185 /// stream directly.
186 pub fn read_timeout(mut self, dur: Duration) -> Self {
187 self.read_timeout = dur;
188 self
189 }
190
191 /// Get the device serial.
192 pub fn serial(&self) -> &str {
193 &self.serial
194 }
195
196 // -- Shell ----------------------------------------------------------------
197
198 /// Run a shell command, returning separated stdout, stderr, and exit code.
199 ///
200 /// ```no_run
201 /// # use adb_wire::AdbWire;
202 /// # async fn example() {
203 /// let adb = AdbWire::new("emulator-5554");
204 /// let result = adb.shell("ls /sdcard").await.unwrap();
205 /// if result.success() {
206 /// println!("{}", result.stdout_str());
207 /// } else {
208 /// eprintln!("exit {}: {}", result.exit_code, result.stderr_str());
209 /// }
210 /// # }
211 /// ```
212 pub async fn shell(&self, cmd: &str) -> Result<ShellOutput> {
213 let stream = self.connect_shell(cmd).await?;
214 self.with_timeout(shell::read_shell(stream)).await?
215 }
216
217 /// Run a shell command and return a streaming reader.
218 ///
219 /// The returned [`ShellStream`] implements [`AsyncRead`](tokio::io::AsyncRead)
220 /// yielding stdout bytes. Use [`collect_output`](ShellStream::collect_output)
221 /// to buffer everything, or read incrementally for long-running commands.
222 ///
223 /// ```no_run
224 /// # use adb_wire::AdbWire;
225 /// # async fn example() {
226 /// use tokio::io::{AsyncBufReadExt, BufReader};
227 ///
228 /// let adb = AdbWire::new("emulator-5554");
229 /// let stream = adb.shell_stream("logcat").await.unwrap();
230 /// let mut lines = BufReader::new(stream).lines();
231 /// while let Some(line) = lines.next_line().await.unwrap() {
232 /// println!("{line}");
233 /// }
234 /// # }
235 /// ```
236 pub async fn shell_stream(&self, cmd: &str) -> Result<ShellStream> {
237 Ok(ShellStream::new(self.connect_shell(cmd).await?))
238 }
239
240 /// Send a shell command without waiting for output.
241 ///
242 /// Opens a shell connection and immediately drops it. The ADB server
243 /// will still deliver the command to the device, but the command may
244 /// not have *started* on the device by the time this method returns.
245 ///
246 /// **Warning:** Because the connection is dropped immediately, there is
247 /// no guarantee the command will execute. If the device has not received
248 /// the command before the TCP connection closes, it will be lost. Use
249 /// [`shell`](Self::shell) if you need confirmation that the command ran.
250 ///
251 /// Best suited for input commands (tap, swipe, keyevent) where occasional
252 /// drops are acceptable.
253 pub async fn shell_detach(&self, cmd: &str) -> Result<()> {
254 let mut stream = self.connect_shell(cmd).await?;
255 // Wait briefly for the server to acknowledge, giving it time to
256 // forward the command to the device before we drop the connection.
257 let mut header = [0u8; 5];
258 let _ = tokio::time::timeout(
259 Duration::from_millis(50),
260 tokio::io::AsyncReadExt::read_exact(&mut stream, &mut header),
261 )
262 .await;
263 Ok(())
264 }
265
266 // -- File transfer (sync protocol) ----------------------------------------
267
268 /// Query file metadata on the device.
269 ///
270 /// Returns a [`RemoteStat`] with mode, size, and modification time.
271 /// If the path does not exist, all fields will be zero —
272 /// check with [`RemoteStat::exists()`].
273 ///
274 /// ```no_run
275 /// # use adb_wire::AdbWire;
276 /// # async fn example() {
277 /// let adb = AdbWire::new("emulator-5554");
278 /// let st = adb.stat("/sdcard/Download/test.txt").await.unwrap();
279 /// if st.exists() {
280 /// println!("size: {} bytes, mode: {:o}", st.size, st.mode);
281 /// }
282 /// # }
283 /// ```
284 pub async fn stat(&self, remote_path: &str) -> Result<RemoteStat> {
285 // Try STAT2 first for large file / extended metadata support.
286 let mut stream = self.connect_cmd("sync:").await?;
287 match sync::stat2_sync(&mut stream, remote_path).await {
288 Ok(st) => Ok(st),
289 Err(_) => {
290 // Fall back to STAT v1 on a fresh sync connection.
291 let mut stream = self.connect_cmd("sync:").await?;
292 sync::stat_v1_sync(&mut stream, remote_path).await
293 }
294 }
295 }
296
297 /// List files in a remote directory.
298 ///
299 /// Returns entries excluding `.` and `..`. Uses the sync LIST protocol.
300 ///
301 /// ```no_run
302 /// # use adb_wire::AdbWire;
303 /// # async fn example() {
304 /// let adb = AdbWire::new("emulator-5554");
305 /// for entry in adb.list_dir("/sdcard").await.unwrap() {
306 /// println!("{}\t{} bytes", entry.name, entry.size);
307 /// }
308 /// # }
309 /// ```
310 pub async fn list_dir(&self, remote_path: &str) -> Result<Vec<DirEntry>> {
311 let mut stream = self.connect_cmd("sync:").await?;
312 sync::list_sync(&mut stream, remote_path).await
313 }
314
315 /// Pull a file from the device, writing its contents to `writer`.
316 ///
317 /// Returns the number of bytes written.
318 pub async fn pull(
319 &self,
320 remote_path: &str,
321 writer: &mut (impl tokio::io::AsyncWrite + Unpin),
322 ) -> Result<u64> {
323 let mut stream = self.connect_cmd("sync:").await?;
324 sync::pull_sync(&mut stream, remote_path, writer).await
325 }
326
327 /// Pull a file from the device to a local path.
328 pub async fn pull_file(
329 &self,
330 remote_path: &str,
331 local_path: impl AsRef<std::path::Path>,
332 ) -> Result<u64> {
333 let mut f = tokio::fs::File::create(local_path.as_ref()).await?;
334 self.pull(remote_path, &mut f).await
335 }
336
337 /// Push data from `reader` to a file on the device.
338 ///
339 /// `mode` is the Unix file permission as an octal value (e.g. `0o644`,
340 /// `0o755`). `mtime` is seconds since the Unix epoch.
341 pub async fn push(
342 &self,
343 remote_path: &str,
344 mode: u32,
345 mtime: u32,
346 reader: &mut (impl tokio::io::AsyncRead + Unpin),
347 ) -> Result<()> {
348 let mut stream = self.connect_cmd("sync:").await?;
349 sync::push_sync(&mut stream, remote_path, mode, mtime, reader).await
350 }
351
352 /// Push a local file to the device with mode `0o644`.
353 pub async fn push_file(
354 &self,
355 local_path: impl AsRef<std::path::Path>,
356 remote_path: &str,
357 ) -> Result<()> {
358 self.push_file_with_mode(local_path, remote_path, 0o644).await
359 }
360
361 /// Push a local file to the device with a custom Unix file mode.
362 ///
363 /// ```no_run
364 /// # use adb_wire::AdbWire;
365 /// # async fn example() {
366 /// let adb = AdbWire::new("emulator-5554");
367 /// adb.push_file_with_mode("run.sh", "/data/local/tmp/run.sh", 0o755).await.unwrap();
368 /// # }
369 /// ```
370 pub async fn push_file_with_mode(
371 &self,
372 local_path: impl AsRef<std::path::Path>,
373 remote_path: &str,
374 mode: u32,
375 ) -> Result<()> {
376 let path = local_path.as_ref();
377 let meta = tokio::fs::metadata(path).await?;
378 let mtime = meta.modified().ok()
379 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
380 .map_or(0, |d| d.as_secs() as u32);
381 let mut f = tokio::fs::File::open(path).await?;
382 self.push(remote_path, mode, mtime, &mut f).await
383 }
384
385 // -- Install --------------------------------------------------------------
386
387 /// Install an APK on the device. Equivalent to `adb install <path>`.
388 ///
389 /// ```no_run
390 /// # use adb_wire::AdbWire;
391 /// # async fn example() {
392 /// let adb = AdbWire::new("emulator-5554");
393 /// adb.install("app-debug.apk").await.unwrap();
394 /// # }
395 /// ```
396 pub async fn install(&self, local_apk: impl AsRef<std::path::Path>) -> Result<()> {
397 self.install_with_args(local_apk, &[]).await
398 }
399
400 /// Install an APK with additional `pm install` flags.
401 ///
402 /// ```no_run
403 /// # use adb_wire::AdbWire;
404 /// # async fn example() {
405 /// let adb = AdbWire::new("emulator-5554");
406 /// adb.install_with_args("app.apk", &["-r", "-d"]).await.unwrap();
407 /// # }
408 /// ```
409 pub async fn install_with_args(
410 &self,
411 local_apk: impl AsRef<std::path::Path>,
412 args: &[&str],
413 ) -> Result<()> {
414 let local = local_apk.as_ref();
415 let filename = local
416 .file_name()
417 .and_then(|n| n.to_str())
418 .unwrap_or("install.apk");
419
420 // Reject filenames that could escape the shell or path.
421 if filename.contains('/') || filename.contains('\0') || filename.contains('\'') {
422 return Err(Error::Adb(format!("unsafe filename: {filename}")));
423 }
424
425 let remote = format!("/data/local/tmp/{filename}");
426 self.push_file_with_mode(local, &remote, 0o644).await?;
427
428 let mut cmd = String::from("pm install");
429 for arg in args {
430 cmd.push(' ');
431 shell_quote_into(&mut cmd, arg);
432 }
433 cmd.push(' ');
434 shell_quote_into(&mut cmd, &remote);
435
436 let result = self.shell(&cmd).await?;
437
438 // Best-effort cleanup of the temp APK.
439 let cleanup_ok = self
440 .shell(&format!("rm -f '{remote}'"))
441 .await
442 .map(|r| r.success())
443 .unwrap_or(false);
444
445 if result.success() {
446 Ok(())
447 } else {
448 let output = if result.stderr.is_empty() {
449 result.stdout_str()
450 } else {
451 result.stderr_str()
452 };
453 let mut msg = format!("pm install failed (exit {}): {output}", result.exit_code);
454 if !cleanup_ok {
455 msg.push_str(&format!(" (cleanup of {remote} also failed)"));
456 }
457 Err(Error::Adb(msg))
458 }
459 }
460
461 // -- Port forwarding ------------------------------------------------------
462
463 /// Set up port forwarding from a local socket to the device.
464 ///
465 /// `local` and `remote` are ADB socket specs, e.g. `"tcp:8080"`,
466 /// `"localabstract:chrome_devtools_remote"`.
467 pub async fn forward(&self, local: &str, remote: &str) -> Result<()> {
468 let mut stream = self.connect_raw().await?;
469 let cmd = format!("host-serial:{}:forward:{};{}", self.serial, local, remote);
470 wire::send(&mut stream, &cmd).await?;
471 wire::read_okay(&mut stream).await?;
472 wire::read_okay(&mut stream).await?; // second OKAY after setup
473 Ok(())
474 }
475
476 /// Set up reverse port forwarding from the device to a local socket.
477 pub async fn reverse(&self, remote: &str, local: &str) -> Result<()> {
478 let mut stream = self.connect_transport().await?;
479 let cmd = format!("reverse:forward:{remote};{local}");
480 wire::send(&mut stream, &cmd).await?;
481 wire::read_okay(&mut stream).await?;
482 Ok(())
483 }
484
485 // -- Connection internals -------------------------------------------------
486
487 async fn connect_transport(&self) -> Result<TcpStream> {
488 let mut stream = self.connect_raw().await?;
489 wire::send(&mut stream, &format!("host:transport:{}", self.serial)).await?;
490 wire::read_okay(&mut stream).await?;
491 Ok(stream)
492 }
493
494 async fn connect_cmd(&self, cmd: &str) -> Result<TcpStream> {
495 let mut stream = self.connect_transport().await?;
496 wire::send(&mut stream, cmd).await?;
497 wire::read_okay(&mut stream).await?;
498 Ok(stream)
499 }
500
501 async fn connect_shell(&self, cmd: &str) -> Result<TcpStream> {
502 self.connect_cmd(&format!("shell,v2,raw:{cmd}")).await
503 }
504
505 async fn with_timeout<F: std::future::Future>(&self, f: F) -> Result<F::Output> {
506 tokio::time::timeout(self.read_timeout, f)
507 .await
508 .map_err(|_| Error::timed_out("read timed out"))
509 }
510
511 async fn connect_raw(&self) -> Result<TcpStream> {
512 let stream = tokio::time::timeout(
513 self.connect_timeout,
514 TcpStream::connect(self.server),
515 )
516 .await
517 .map_err(|_| Error::timed_out("connect timed out"))??;
518 stream.set_nodelay(true)?;
519 Ok(stream)
520 }
521}
522
523// -- Tests --------------------------------------------------------------------
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use std::io::Cursor;
529
530 #[test]
531 fn new_defaults() {
532 let adb = AdbWire::new("emulator-5554");
533 assert_eq!(adb.serial(), "emulator-5554");
534 assert_eq!(adb.server, DEFAULT_SERVER);
535 assert_eq!(adb.read_timeout, Duration::from_secs(30));
536 }
537
538 #[test]
539 fn builder() {
540 let addr = SocketAddr::from(([192, 168, 1, 100], 5037));
541 let adb = AdbWire::new("device123")
542 .server(addr)
543 .connect_timeout(Duration::from_millis(500))
544 .read_timeout(Duration::from_secs(60));
545 assert_eq!(adb.server, addr);
546 assert_eq!(adb.connect_timeout, Duration::from_millis(500));
547 assert_eq!(adb.read_timeout, Duration::from_secs(60));
548 }
549
550 #[tokio::test]
551 async fn send_format() {
552 let mut buf = Vec::new();
553 wire::send(&mut buf, "host:version").await.unwrap();
554 assert_eq!(&buf, b"000chost:version");
555 }
556
557 #[tokio::test]
558 async fn okay_response() {
559 let mut cur = Cursor::new(b"OKAY".to_vec());
560 assert!(wire::read_okay(&mut cur).await.is_ok());
561 }
562
563 #[tokio::test]
564 async fn fail_response() {
565 let mut cur = Cursor::new(b"FAIL0005nope!".to_vec());
566 let err = wire::read_okay(&mut cur).await.unwrap_err();
567 assert!(matches!(err, Error::Adb(msg) if msg == "nope!"));
568 }
569
570 #[tokio::test]
571 async fn hex_len() {
572 let mut cur = Cursor::new(b"001f".to_vec());
573 assert_eq!(wire::read_hex_len(&mut cur).await.unwrap(), 31);
574 }
575
576 #[tokio::test]
577 async fn bad_status() {
578 let mut cur = Cursor::new(b"WHAT".to_vec());
579 assert!(matches!(
580 wire::read_okay(&mut cur).await.unwrap_err(),
581 Error::Protocol(_)
582 ));
583 }
584
585 #[test]
586 fn parse_devices() {
587 let raw = "emulator-5554\tdevice\nR5CR1234567\toffline\n";
588 let devices = parse_device_list(raw);
589 assert_eq!(
590 devices,
591 vec![
592 ("emulator-5554".into(), "device".into()),
593 ("R5CR1234567".into(), "offline".into()),
594 ]
595 );
596 }
597
598 #[test]
599 fn parse_devices_empty() {
600 assert!(parse_device_list("").is_empty());
601 assert!(parse_device_list("\n").is_empty());
602 }
603
604 #[test]
605 fn shell_quote_simple() {
606 let mut out = String::new();
607 shell_quote_into(&mut out, "hello");
608 assert_eq!(out, "'hello'");
609 }
610
611 #[test]
612 fn shell_quote_with_spaces_and_semicolons() {
613 let mut out = String::new();
614 shell_quote_into(&mut out, "foo; rm -rf /");
615 assert_eq!(out, "'foo; rm -rf /'");
616 }
617
618 #[test]
619 fn shell_quote_with_single_quotes() {
620 let mut out = String::new();
621 shell_quote_into(&mut out, "it's");
622 assert_eq!(out, "'it'\\''s'");
623 }
624}