Skip to main content

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}