forensic-adb-0.7.0/.cargo_vcs_info.json0000644000000001360000000000100133740ustar { "git": { "sha1": "de054089fb074cb43dc0bd084fa80aaf6f0f8719" }, "path_in_vcs": "" }forensic-adb-0.7.0/.github/workflows/rust.yml000064400000000000000000000031431046102023000173020ustar 00000000000000name: Rust on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '0 9 * * 1' env: CARGO_TERM_COLOR: always jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-latest, windows-latest] steps: - uses: actions/checkout@v3 - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo-debug- - name: Build run: cargo build --verbose unit-test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-latest, windows-latest] steps: - uses: actions/checkout@v3 - name: Set up cargo cache uses: actions/cache@v3 continue-on-error: false with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo-debug- - name: Run clippy run: cargo clippy -- -D warnings - name: Run tests run: cargo test --verbose fmt: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Run cargo fmt run: cargo fmt --all -- --check forensic-adb-0.7.0/.gitignore000064400000000000000000000000101046102023000141430ustar 00000000000000/target forensic-adb-0.7.0/Cargo.lock0000644000000447410000000000100113610ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bstr" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "forensic-adb" version = "0.7.0" dependencies = [ "bstr", "futures", "log", "once_cell", "regex", "tempfile", "thiserror", "tokio", "unix_path", "uuid", "walkdir", ] [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "syn" version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unix_path" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af8e291873ae77c4c8d9c9b34d0bee68a35b048fb39c263a5155e0e353783eaf" dependencies = [ "unix_str", ] [[package]] name = "unix_str" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ace0b4755d0a2959962769239d56267f8a024fef2d9b32666b3dcd0946b0906" [[package]] name = "uuid" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom", "serde", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" forensic-adb-0.7.0/Cargo.toml0000644000000027720000000000100114020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "forensic-adb" version = "0.7.0" authors = [ "Mozilla", "kpcyrd ", ] description = "Tokio based client library for the Android Debug Bridge (adb) based on mozdevice" readme = "README.md" keywords = [ "adb", "android", ] license = "MPL-2.0" repository = "https://github.com/kpcyrd/forensic-adb" [dependencies.bstr] version = "1.9.1" [dependencies.log] version = "0.4" features = ["std"] [dependencies.once_cell] version = "1.4.0" [dependencies.regex] version = "1" features = [ "perf", "std", ] default-features = false [dependencies.tempfile] version = "3" [dependencies.thiserror] version = "1.0.25" [dependencies.tokio] version = "1.26.0" features = [ "net", "fs", "io-util", ] [dependencies.unix_path] version = "1.0" [dependencies.uuid] version = "1.0" features = [ "serde", "v4", ] [dependencies.walkdir] version = "2" [dev-dependencies.futures] version = "0.3.27" [dev-dependencies.tokio] version = "1" features = [ "macros", "rt-multi-thread", ] forensic-adb-0.7.0/Cargo.toml.orig000064400000000000000000000014361046102023000150570ustar 00000000000000[package] name = "forensic-adb" version = "0.7.0" authors = ["Mozilla", "kpcyrd "] description = "Tokio based client library for the Android Debug Bridge (adb) based on mozdevice" keywords = [ "adb", "android" ] license = "MPL-2.0" repository = "https://github.com/kpcyrd/forensic-adb" edition = "2021" [dependencies] bstr = "1.9.1" log = { version = "0.4", features = ["std"] } once_cell = "1.4.0" regex = { version = "1", default-features = false, features = ["perf", "std"] } tempfile = "3" thiserror = "1.0.25" tokio = { version = "1.26.0", features = ["net", "fs", "io-util"] } unix_path = "1.0" uuid = { version = "1.0", features = ["serde", "v4"] } walkdir = "2" [dev-dependencies] futures = "0.3.27" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } forensic-adb-0.7.0/LICENSE000064400000000000000000000405261046102023000132000ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. forensic-adb-0.7.0/README.md000064400000000000000000000020361046102023000134440ustar 00000000000000# forensic-adb Tokio based client library for the Android Debug Bridge (adb) based on mozdevice for Rust. [Documentation](https://docs.rs/forensic-adb) This code has been extracted from [mozilla-central/testing/mozbase/rust/mozdevice][1] and ported to async Rust. It also removes root detection so no commands are executed on the remote device by default. [1]: https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/rust/mozdevice ```rust use forensic_adb::{AndroidStorageInput, DeviceError, Host}; #[tokio::main] async fn main() -> Result<(), DeviceError> { let host = Host::default(); let devices = host.devices::>().await?; println!("Found devices: {:?}", devices); let device = host .device_or_default(Option::<&String>::None, AndroidStorageInput::default()) .await?; println!("Selected device: {:?}", device); let output = device.execute_host_shell_command("id").await?; println!("Received response: {:?}", output); Ok(()) } ``` ## License Mozilla Public License (MPL-2.0) forensic-adb-0.7.0/examples/hello-world.rs000064400000000000000000000014401046102023000165770ustar 00000000000000use bstr::ByteSlice; use forensic_adb::{AndroidStorageInput, DeviceError, Host}; #[tokio::main] async fn main() -> Result<(), DeviceError> { let host = Host::default(); let devices = host.devices::>().await?; println!("Found devices: {:?}", devices); let device = host .device_or_default(Option::<&String>::None, AndroidStorageInput::default()) .await?; println!("Selected device: {:?}", device); let output = device.execute_host_exec_out_command("id").await?; println!("Received response: {:?}", bstr::BStr::new(&output)); let output = device .execute_host_exec_out_command("pm list packages -f") .await?; for line in output.lines() { println!("Received line: {:?}", bstr::BStr::new(line)); } Ok(()) } forensic-adb-0.7.0/src/adb.rs000064400000000000000000000015631046102023000140540ustar 00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #[derive(Debug, PartialEq)] pub enum SyncCommand { Data, Dent, Done, Fail, List, Okay, Quit, Recv, Send, Stat, } impl SyncCommand { // Returns the byte serialisation of the protocol status. pub fn code(&self) -> &'static [u8; 4] { use self::SyncCommand::*; match *self { Data => b"DATA", Dent => b"DENT", Done => b"DONE", Fail => b"FAIL", List => b"LIST", Okay => b"OKAY", Quit => b"QUIT", Recv => b"RECV", Send => b"SEND", Stat => b"STAT", } } } pub type DeviceSerial = String; forensic-adb-0.7.0/src/lib.rs000064400000000000000000001055561046102023000141030ustar 00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ pub mod adb; pub mod shell; #[cfg(test)] pub mod test; use log::{debug, trace, warn}; use once_cell::sync::Lazy; use regex::Regex; use std::collections::BTreeMap; use std::convert::TryFrom; use std::io; use std::iter::FromIterator; use std::num::{ParseIntError, TryFromIntError}; use std::path::{Component, Path}; use std::str::{FromStr, Utf8Error}; use std::time::SystemTime; use thiserror::Error; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; pub use unix_path::{Path as UnixPath, PathBuf as UnixPathBuf}; use uuid::Uuid; use walkdir::WalkDir; use crate::adb::{DeviceSerial, SyncCommand}; pub type Result = std::result::Result; static SYNC_REGEX: Lazy = Lazy::new(|| Regex::new(r"[^A-Za-z0-9_@%+=:,./-]").unwrap()); #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum AndroidStorageInput { #[default] Auto, App, Internal, Sdcard, } impl FromStr for AndroidStorageInput { type Err = DeviceError; fn from_str(s: &str) -> Result { match s { "auto" => Ok(AndroidStorageInput::Auto), "app" => Ok(AndroidStorageInput::App), "internal" => Ok(AndroidStorageInput::Internal), "sdcard" => Ok(AndroidStorageInput::Sdcard), _ => Err(DeviceError::InvalidStorage), } } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum AndroidStorage { App, Internal, Sdcard, } #[derive(Debug, Error)] pub enum DeviceError { #[error("{0}")] Adb(String), #[error(transparent)] FromInt(#[from] TryFromIntError), #[error("Invalid storage")] InvalidStorage, #[error(transparent)] Io(#[from] io::Error), #[error("Missing package")] MissingPackage, #[error("Multiple Android devices online")] MultipleDevices, #[error(transparent)] ParseInt(#[from] ParseIntError), #[error("Unknown Android device with serial '{0}'")] UnknownDevice(String), #[error(transparent)] Utf8(#[from] Utf8Error), #[error(transparent)] WalkDir(#[from] walkdir::Error), } fn encode_message(payload: &str) -> Result { let hex_length = u16::try_from(payload.len()).map(|len| format!("{:0>4X}", len))?; Ok(format!("{}{}", hex_length, payload)) } fn parse_device_info(line: &str) -> Option { // Turn "serial\tdevice key1:value1 key2:value2 ..." into a `DeviceInfo`. let mut pairs = line.split_whitespace(); let serial = pairs.next(); let state = pairs.next(); if let (Some(serial), Some("device")) = (serial, state) { let info: BTreeMap = pairs .filter_map(|pair| { let mut kv = pair.split(':'); if let (Some(k), Some(v), None) = (kv.next(), kv.next(), kv.next()) { Some((k.to_owned(), v.to_owned())) } else { None } }) .collect(); Some(DeviceInfo { serial: serial.to_owned(), info, }) } else { None } } /// Reads the payload length of a host message from the stream. async fn read_length(stream: &mut R) -> Result { let mut bytes: [u8; 4] = [0; 4]; stream.read_exact(&mut bytes).await?; let response = std::str::from_utf8(&bytes)?; Ok(usize::from_str_radix(response, 16)?) } /// Reads the payload length of a device message from the stream. async fn read_length_little_endian(reader: &mut R) -> Result { let mut bytes: [u8; 4] = [0; 4]; reader.read_exact(&mut bytes).await?; let n: usize = (bytes[0] as usize) + ((bytes[1] as usize) << 8) + ((bytes[2] as usize) << 16) + ((bytes[3] as usize) << 24); Ok(n) } /// Writes the payload length of a device message to the stream. async fn write_length_little_endian( writer: &mut W, n: usize, ) -> Result { let mut bytes = [0; 4]; bytes[0] = (n & 0xFF) as u8; bytes[1] = ((n >> 8) & 0xFF) as u8; bytes[2] = ((n >> 16) & 0xFF) as u8; bytes[3] = ((n >> 24) & 0xFF) as u8; writer.write(&bytes[..]).await.map_err(DeviceError::Io) } async fn read_response( stream: &mut TcpStream, has_output: bool, has_length: bool, ) -> Result> { let mut bytes: [u8; 1024] = [0; 1024]; stream.read_exact(&mut bytes[0..4]).await?; if !bytes.starts_with(SyncCommand::Okay.code()) { let n = bytes.len().min(read_length(stream).await?); stream.read_exact(&mut bytes[0..n]).await?; let message = std::str::from_utf8(&bytes[0..n]).map(|s| format!("adb error: {}", s))?; return Err(DeviceError::Adb(message)); } let mut response = Vec::new(); if has_output { stream.read_to_end(&mut response).await?; if response.starts_with(SyncCommand::Okay.code()) { // Sometimes the server produces OKAYOKAY. Sometimes there is a transport OKAY and // then the underlying command OKAY. This is straight from `chromedriver`. response = response.split_off(4); } if response.starts_with(SyncCommand::Fail.code()) { // The server may even produce OKAYFAIL, which means the underlying // command failed. First split-off the `FAIL` and length of the message. response = response.split_off(8); let message = std::str::from_utf8(&response).map(|s| format!("adb error: {}", s))?; return Err(DeviceError::Adb(message)); } if has_length { if response.len() >= 4 { let message = response.split_off(4); let slice: &mut &[u8] = &mut &*response; let n = read_length(slice).await?; if n != message.len() { warn!("adb server response contained hexstring len {} but remaining message length is {}", n, message.len()); } trace!( "adb server response was {:?}", std::str::from_utf8(&message)? ); return Ok(message); } else { return Err(DeviceError::Adb(format!( "adb server response did not contain expected hexstring length: {:?}", std::str::from_utf8(&response)? ))); } } } Ok(response) } /// Detailed information about an ADB device. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct DeviceInfo { pub serial: DeviceSerial, pub info: BTreeMap, } /// Represents a connection to an ADB host, which multiplexes the connections to /// individual devices. #[derive(Debug, Clone, PartialEq)] pub struct Host { /// The TCP host to connect to. Defaults to `"localhost"`. pub host: Option, /// The TCP port to connect to. Defaults to `5037`. pub port: Option, } impl Default for Host { fn default() -> Host { Host { host: Some("localhost".to_string()), port: Some(5037), } } } impl Host { /// Searches for available devices, and selects the one as specified by `device_serial`. /// /// If multiple devices are online, and no device has been specified, /// the `ANDROID_SERIAL` environment variable can be used to select one. pub async fn device_or_default>( self, device_serial: Option<&T>, storage: AndroidStorageInput, ) -> Result { let serials: Vec = self .devices::>() .await? .into_iter() .map(|d| d.serial) .collect(); if let Some(ref serial) = device_serial .map(|v| v.as_ref().to_owned()) .or_else(|| std::env::var("ANDROID_SERIAL").ok()) { if !serials.contains(serial) { return Err(DeviceError::UnknownDevice(serial.clone())); } return Device::new(self, serial.to_owned(), storage).await; } if serials.len() > 1 { return Err(DeviceError::MultipleDevices); } if let Some(ref serial) = serials.first() { return Device::new(self, serial.to_owned().to_string(), storage).await; } Err(DeviceError::Adb("No Android devices are online".to_owned())) } pub async fn connect(&self) -> Result { let stream = TcpStream::connect(format!( "{}:{}", self.host.clone().unwrap_or_else(|| "localhost".to_owned()), self.port.unwrap_or(5037) )) .await?; Ok(stream) } pub async fn execute_command( &self, command: &str, has_output: bool, has_length: bool, ) -> Result { let mut stream = self.connect().await?; stream .write_all(encode_message(command)?.as_bytes()) .await?; let bytes = read_response(&mut stream, has_output, has_length).await?; // TODO: should we assert no bytes were read? let response = std::str::from_utf8(&bytes)?; Ok(response.to_owned()) } pub async fn execute_host_command( &self, host_command: &str, has_length: bool, has_output: bool, ) -> Result { self.execute_command(&format!("host:{}", host_command), has_output, has_length) .await } pub async fn features>(&self) -> Result { let features = self.execute_host_command("features", true, true).await?; Ok(features.split(',').map(|x| x.to_owned()).collect()) } pub async fn devices>(&self) -> Result { let response = self.execute_host_command("devices-l", true, true).await?; let infos: B = response.lines().filter_map(parse_device_info).collect(); Ok(infos) } } /// Represents an ADB device. #[derive(Debug, Clone)] pub struct Device { /// ADB host that controls this device. pub host: Host, /// Serial number uniquely identifying this ADB device. pub serial: DeviceSerial, pub run_as_package: Option, pub storage: AndroidStorage, /// Cache intermediate tempfile name used in pushing via run_as. pub tempfile: UnixPathBuf, } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct RemoteDirEntry { depth: usize, metadata: RemoteMetadata, name: String, } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum RemoteMetadata { RemoteFile(RemoteFileMetadata), RemoteDir, RemoteSymlink, } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct RemoteFileMetadata { mode: usize, size: usize, } impl Device { pub async fn new( host: Host, serial: DeviceSerial, storage: AndroidStorageInput, ) -> Result { let mut device = Device { host, serial, run_as_package: None, storage: AndroidStorage::App, tempfile: UnixPathBuf::from("/data/local/tmp"), }; device .tempfile .push(Uuid::new_v4().as_hyphenated().to_string()); device.storage = match storage { AndroidStorageInput::App => AndroidStorage::App, AndroidStorageInput::Internal => AndroidStorage::Internal, AndroidStorageInput::Sdcard => AndroidStorage::Sdcard, AndroidStorageInput::Auto => AndroidStorage::Sdcard, }; Ok(device) } pub async fn clear_app_data(&self, package: &str) -> Result { self.execute_host_shell_command(&format!("pm clear {}", package)) .await .map(|v| v.contains("Success")) } pub async fn create_dir(&self, path: &UnixPath) -> Result<()> { debug!("Creating {}", path.display()); let enable_run_as = self.enable_run_as_for_path(path); self.execute_host_shell_command_as(&format!("mkdir -p {}", path.display()), enable_run_as) .await?; Ok(()) } pub async fn chmod(&self, path: &UnixPath, mask: &str, recursive: bool) -> Result<()> { let enable_run_as = self.enable_run_as_for_path(path); let recursive = match recursive { true => " -R", false => "", }; self.execute_host_shell_command_as( &format!("chmod {} {} {}", recursive, mask, path.display()), enable_run_as, ) .await?; Ok(()) } pub async fn execute_host_command( &self, command: &str, has_output: bool, has_length: bool, ) -> Result> { let mut stream = self.host.connect().await?; let switch_command = format!("host:transport:{}", self.serial); trace!("execute_host_command: >> {:?}", &switch_command); stream .write_all(encode_message(&switch_command)?.as_bytes()) .await?; let _bytes = read_response(&mut stream, false, false).await?; trace!("execute_host_command: << {:?}", _bytes); // TODO: should we assert no bytes were read? trace!("execute_host_command: >> {:?}", &command); stream .write_all(encode_message(command)?.as_bytes()) .await?; let bytes = read_response(&mut stream, has_output, has_length).await?; trace!("execute_host_command: << {:?}", bstr::BStr::new(&bytes)); Ok(bytes) } pub async fn execute_host_command_to_string( &self, command: &str, has_output: bool, has_length: bool, ) -> Result { let bytes = self .execute_host_command(command, has_output, has_length) .await?; let response = std::str::from_utf8(&bytes)?; // Unify new lines by removing possible carriage returns Ok(response.replace("\r\n", "\n")) } pub fn enable_run_as_for_path(&self, path: &UnixPath) -> bool { match &self.run_as_package { Some(package) => { let mut p = UnixPathBuf::from("/data/data/"); p.push(package); path.starts_with(p) } None => false, } } pub async fn execute_host_shell_command(&self, shell_command: &str) -> Result { self.execute_host_shell_command_as(shell_command, false) .await } pub async fn execute_host_exec_out_command(&self, shell_command: &str) -> Result> { self.execute_host_command(&format!("exec:{}", shell_command), true, false) .await } pub async fn execute_host_shell_command_as( &self, shell_command: &str, enable_run_as: bool, ) -> Result { // We don't want to duplicate su invocations. if shell_command.starts_with("su") { return self .execute_host_command_to_string(&format!("shell:{}", shell_command), true, false) .await; } let has_outer_quotes = shell_command.starts_with('"') && shell_command.ends_with('"') || shell_command.starts_with('\'') && shell_command.ends_with('\''); // Execute command as package if enable_run_as { let run_as_package = self .run_as_package .as_ref() .ok_or(DeviceError::MissingPackage)?; if has_outer_quotes { return self .execute_host_command_to_string( &format!("shell:run-as {} {}", run_as_package, shell_command), true, false, ) .await; } if SYNC_REGEX.is_match(shell_command) { let arg: &str = &shell_command.replace('\'', "'\"'\"'")[..]; return self .execute_host_command_to_string( &format!("shell:run-as {} {}", run_as_package, arg), true, false, ) .await; } return self .execute_host_command_to_string( &format!("shell:run-as {} \"{}\"", run_as_package, shell_command), true, false, ) .await; } self.execute_host_command_to_string(&format!("shell:{}", shell_command), true, false) .await } pub async fn is_app_installed(&self, package: &str) -> Result { self.execute_host_shell_command(&format!("pm path {}", package)) .await .map(|v| v.contains("package:")) } pub async fn launch>( &self, package: &str, activity: &str, am_start_args: &[T], ) -> Result { let mut am_start = format!("am start -W -n {}/{}", package, activity); for arg in am_start_args { am_start.push(' '); if SYNC_REGEX.is_match(arg.as_ref()) { am_start.push_str(&format!("\"{}\"", &shell::escape(arg.as_ref()))); } else { am_start.push_str(&shell::escape(arg.as_ref())); }; } self.execute_host_shell_command(&am_start) .await .map(|v| v.contains("Complete")) } pub async fn force_stop(&self, package: &str) -> Result<()> { debug!("Force stopping Android package: {}", package); self.execute_host_shell_command(&format!("am force-stop {}", package)) .await .and(Ok(())) } pub async fn forward_port(&self, local: u16, remote: u16) -> Result { let command = format!( "host-serial:{}:forward:tcp:{};tcp:{}", self.serial, local, remote ); let response = self.host.execute_command(&command, true, false).await?; if local == 0 { Ok(response.parse::()?) } else { Ok(local) } } pub async fn kill_forward_port(&self, local: u16) -> Result<()> { let command = format!("host-serial:{}:killforward:tcp:{}", self.serial, local); self.execute_host_command(&command, true, false) .await .and(Ok(())) } pub async fn kill_forward_all_ports(&self) -> Result<()> { let command = format!("host-serial:{}:killforward-all", self.serial); self.execute_host_command(&command, false, false) .await .and(Ok(())) } pub async fn reverse_port(&self, remote: u16, local: u16) -> Result { let command = format!("reverse:forward:tcp:{};tcp:{}", remote, local); let response = self .execute_host_command_to_string(&command, true, false) .await?; if remote == 0 { Ok(response.parse::()?) } else { Ok(remote) } } pub async fn kill_reverse_port(&self, remote: u16) -> Result<()> { let command = format!("reverse:killforward:tcp:{}", remote); self.execute_host_command(&command, true, true) .await .and(Ok(())) } pub async fn kill_reverse_all_ports(&self) -> Result<()> { let command = "reverse:killforward-all".to_owned(); self.execute_host_command(&command, false, false) .await .and(Ok(())) } pub async fn list_dir(&self, src: &UnixPath) -> Result> { let src = src.to_path_buf(); let mut queue = vec![(src.clone(), 0, "".to_string())]; let mut listings = Vec::new(); while let Some((next, depth, prefix)) = queue.pop() { for listing in self.list_dir_flat(&next, depth, prefix).await? { if listing.metadata == RemoteMetadata::RemoteDir { let mut child = src.clone(); child.push(listing.name.clone()); queue.push((child, depth + 1, listing.name.clone())); } listings.push(listing); } } Ok(listings) } async fn list_dir_flat( &self, src: &UnixPath, depth: usize, prefix: String, ) -> Result> { // Implement the ADB protocol to list a directory from the device. let mut stream = self.host.connect().await?; // Send "host:transport" command with device serial let message = encode_message(&format!("host:transport:{}", self.serial))?; stream.write_all(message.as_bytes()).await?; let _bytes = read_response(&mut stream, false, true).await?; // Send "sync:" command to initialize file transfer let message = encode_message("sync:")?; stream.write_all(message.as_bytes()).await?; let _bytes = read_response(&mut stream, false, true).await?; // Send "LIST" command with name of the directory stream.write_all(SyncCommand::List.code()).await?; let args_ = format!("{}", src.display()); let args = args_.as_bytes(); write_length_little_endian(&mut stream, args.len()).await?; stream.write_all(args).await?; // Use the maximum 64KB buffer to transfer the file contents. let mut buf = [0; 64 * 1024]; let mut listings = Vec::new(); // Read "DENT" command one or more times for the directory entries loop { stream.read_exact(&mut buf[0..4]).await?; if &buf[0..4] == SyncCommand::Dent.code() { // From https://github.com/cstyan/adbDocumentation/blob/6d025b3e4af41be6f93d37f516a8ac7913688623/README.md: // // A four-byte integer representing file mode - first 9 bits of this mode represent // the file permissions, as with chmod mode. Bits 14 to 16 seem to represent the // file type, one of 0b100 (file), 0b010 (directory), 0b101 (symlink) // A four-byte integer representing file size. // A four-byte integer representing last modified time in seconds since Unix Epoch. // A four-byte integer representing file name length. // A utf-8 string representing the file name. let mode = read_length_little_endian(&mut stream).await?; let size = read_length_little_endian(&mut stream).await?; let _time = read_length_little_endian(&mut stream).await?; let name_length = read_length_little_endian(&mut stream).await?; stream.read_exact(&mut buf[0..name_length]).await?; let mut name = std::str::from_utf8(&buf[0..name_length])?.to_owned(); if name == "." || name == ".." { continue; } if !prefix.is_empty() { name = format!("{}/{}", prefix, &name); } let file_type = (mode >> 13) & 0b111; let metadata = match file_type { 0b010 => RemoteMetadata::RemoteDir, 0b100 => RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: mode & 0b111111111, size, }), 0b101 => RemoteMetadata::RemoteSymlink, _ => return Err(DeviceError::Adb(format!("Invalid file mode {}", file_type))), }; listings.push(RemoteDirEntry { name, depth, metadata, }); } else if &buf[0..4] == SyncCommand::Done.code() { // "DONE" command indicates end of file transfer break; } else if &buf[0..4] == SyncCommand::Fail.code() { let n = buf.len().min(read_length_little_endian(&mut stream).await?); stream.read_exact(&mut buf[0..n]).await?; let message = std::str::from_utf8(&buf[0..n]) .map(|s| format!("adb error: {}", s)) .unwrap_or_else(|_| "adb error was not utf-8".into()); return Err(DeviceError::Adb(message)); } else { return Err(DeviceError::Adb("FAIL (unknown)".to_owned())); } } Ok(listings) } pub async fn path_exists(&self, path: &UnixPath, enable_run_as: bool) -> Result { self.execute_host_shell_command_as(format!("ls {}", path.display()).as_str(), enable_run_as) .await .map(|path| !path.contains("No such file or directory")) } pub async fn pull(&self, src: &UnixPath, buffer: &mut W) -> Result<()> { // Implement the ADB protocol to receive a file from the device. let mut stream = self.host.connect().await?; // Send "host:transport" command with device serial let message = encode_message(&format!("host:transport:{}", self.serial))?; stream.write_all(message.as_bytes()).await?; let _bytes = read_response(&mut stream, false, true).await?; // Send "sync:" command to initialize file transfer let message = encode_message("sync:")?; stream.write_all(message.as_bytes()).await?; let _bytes = read_response(&mut stream, false, true).await?; // Send "RECV" command with name of the file stream.write_all(SyncCommand::Recv.code()).await?; let args_string = format!("{}", src.display()); let args = args_string.as_bytes(); write_length_little_endian(&mut stream, args.len()).await?; stream.write_all(args).await?; // Use the maximum 64KB buffer to transfer the file contents. let mut buf = [0; 64 * 1024]; // Read "DATA" command one or more times for the file content loop { stream.read_exact(&mut buf[0..4]).await?; if &buf[0..4] == SyncCommand::Data.code() { let len = read_length_little_endian(&mut stream).await?; stream.read_exact(&mut buf[0..len]).await?; buffer.write_all(&buf[0..len]).await?; } else if &buf[0..4] == SyncCommand::Done.code() { // "DONE" command indicates end of file transfer break; } else if &buf[0..4] == SyncCommand::Fail.code() { let n = buf.len().min(read_length_little_endian(&mut stream).await?); stream.read_exact(&mut buf[0..n]).await?; let message = std::str::from_utf8(&buf[0..n]) .map(|s| format!("adb error: {}", s)) .unwrap_or_else(|_| "adb error was not utf-8".into()); return Err(DeviceError::Adb(message)); } else { return Err(DeviceError::Adb("FAIL (unknown)".to_owned())); } } Ok(()) } pub async fn pull_dir(&self, src: &UnixPath, dest_dir: &Path) -> Result<()> { let src = src.to_path_buf(); let dest_dir = dest_dir.to_path_buf(); for entry in self.list_dir(&src).await? { match entry.metadata { RemoteMetadata::RemoteSymlink => {} // Ignored. RemoteMetadata::RemoteDir => { let mut d = dest_dir.clone(); d.push(&entry.name); std::fs::create_dir_all(&d)?; } RemoteMetadata::RemoteFile(_) => { let mut s = src.clone(); s.push(&entry.name); let mut d = dest_dir.clone(); d.push(&entry.name); self.pull(&s, &mut File::create(d).await?).await?; } } } Ok(()) } pub async fn push( &self, buffer: &mut R, dest: &UnixPath, mode: u32, ) -> Result<()> { // Implement the ADB protocol to send a file to the device. // The protocol consists of the following steps: // * Send "host:transport" command with device serial // * Send "sync:" command to initialize file transfer // * Send "SEND" command with name and mode of the file // * Send "DATA" command one or more times for the file content // * Send "DONE" command to indicate end of file transfer let enable_run_as = self.enable_run_as_for_path(&dest.to_path_buf()); let dest1 = match enable_run_as { true => self.tempfile.as_path(), false => UnixPath::new(dest), }; // If the destination directory does not exist, adb will // create it and any necessary ancestors however it will not // set the directory permissions to 0o777. In addition, // Android 9 (P) has a bug in its push implementation which // will cause a push which creates directories to fail with // the error `secure_mkdirs failed: Operation not // permitted`. We can work around this by creating the // destination directories prior to the push. Collect the // ancestors of the destination directory which do not yet // exist so we can create them and adjust their permissions // prior to performing the push. let mut current = dest.parent(); let mut leaf: Option<&UnixPath> = None; let mut root: Option<&UnixPath> = None; while let Some(path) = current { if self.path_exists(path, enable_run_as).await? { break; } if leaf.is_none() { leaf = Some(path); } root = Some(path); current = path.parent(); } if let Some(path) = leaf { self.create_dir(path).await?; } if let Some(path) = root { self.chmod(path, "777", true).await?; } let mut stream = self.host.connect().await?; let message = encode_message(&format!("host:transport:{}", self.serial))?; stream.write_all(message.as_bytes()).await?; let _bytes = read_response(&mut stream, false, true).await?; let message = encode_message("sync:")?; stream.write_all(message.as_bytes()).await?; let _bytes = read_response(&mut stream, false, true).await?; stream.write_all(SyncCommand::Send.code()).await?; let args_ = format!("{},{}", dest1.display(), mode); let args = args_.as_bytes(); write_length_little_endian(&mut stream, args.len()).await?; stream.write_all(args).await?; // Use a 32KB buffer to transfer the file contents // TODO: Maybe adjust to maxdata (256KB) let mut buf = [0; 32 * 1024]; loop { let len = buffer.read(&mut buf).await?; if len == 0 { break; } stream.write_all(SyncCommand::Data.code()).await?; write_length_little_endian(&mut stream, len).await?; stream.write_all(&buf[0..len]).await?; } // https://android.googlesource.com/platform/system/core/+/master/adb/SYNC.TXT#66 // // When the file is transferred a sync request "DONE" is sent, where length is set // to the last modified time for the file. The server responds to this last // request (but not to chunk requests) with an "OKAY" sync response (length can // be ignored). let time: u32 = ((SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)) .unwrap() .as_secs() & 0xFFFF_FFFF) as u32; stream.write_all(SyncCommand::Done.code()).await?; write_length_little_endian(&mut stream, time as usize).await?; // Status. stream.read_exact(&mut buf[0..4]).await?; if buf.starts_with(SyncCommand::Okay.code()) { if enable_run_as { // Use cp -a to preserve the permissions set by push. let result = self .execute_host_shell_command_as( format!("cp -aR {} {}", dest1.display(), dest.display()).as_str(), enable_run_as, ) .await; if self.remove(dest1).await.is_err() { warn!("Failed to remove {}", dest1.display()); } result?; } Ok(()) } else if buf.starts_with(SyncCommand::Fail.code()) { if enable_run_as && self.remove(dest1).await.is_err() { warn!("Failed to remove {}", dest1.display()); } let n = buf.len().min(read_length_little_endian(&mut stream).await?); stream.read_exact(&mut buf[0..n]).await?; let message = std::str::from_utf8(&buf[0..n]) .map(|s| format!("adb error: {}", s)) .unwrap_or_else(|_| "adb error was not utf-8".into()); Err(DeviceError::Adb(message)) } else { if self.remove(dest1).await.is_err() { warn!("Failed to remove {}", dest1.display()); } Err(DeviceError::Adb("FAIL (unknown)".to_owned())) } } pub async fn push_dir(&self, source: &Path, dest_dir: &UnixPath, mode: u32) -> Result<()> { debug!("Pushing {} to {}", source.display(), dest_dir.display()); let walker = WalkDir::new(source).follow_links(false).into_iter(); for entry in walker { let entry = entry?; let path = entry.path(); if !entry.metadata()?.is_file() { continue; } let mut file = File::open(path).await?; let tail = path .strip_prefix(source) .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; let dest = append_components(dest_dir, tail)?; self.push(&mut file, &dest, mode).await?; } Ok(()) } pub async fn remove(&self, path: &UnixPath) -> Result<()> { debug!("Deleting {}", path.display()); self.execute_host_shell_command_as( &format!("rm -rf {}", path.display()), self.enable_run_as_for_path(path), ) .await?; Ok(()) } } pub(crate) fn append_components( base: &UnixPath, tail: &Path, ) -> std::result::Result { let mut buf = base.to_path_buf(); for component in tail.components() { if let Component::Normal(segment) = component { let utf8 = segment.to_str().ok_or_else(|| { io::Error::new( io::ErrorKind::Other, "Could not represent path segment as UTF-8", ) })?; buf.push(utf8); } else { return Err(io::Error::new( io::ErrorKind::Other, "Unexpected path component".to_owned(), )); } } Ok(buf) } forensic-adb-0.7.0/src/shell.rs000064400000000000000000000043461046102023000144370ustar 00000000000000// Copyright (c) 2017 Jimmy Cuadra // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. use regex::Regex; /// Escapes a string so it will be interpreted as a single word by the UNIX Bourne shell. /// /// If the input string is empty, this function returns an empty quoted string. pub fn escape(input: &str) -> String { // Stolen from // https://docs.rs/shellwords/1.0.0/src/shellwords/lib.rs.html#24-37. // Added space to the pattern to exclude spaces from being escaped // which can cause problems when combining strings to form a full // command. let escape_pattern: Regex = Regex::new(r"([^A-Za-z0-9_\-.,:/@ \n])").unwrap(); if input.is_empty() { return "''".to_owned(); } let output = &escape_pattern.replace_all(input, "\\$1"); output.replace("'\n'", r"\n") } #[cfg(test)] mod tests { use super::escape; #[test] fn empty_escape() { assert_eq!(escape(""), "''"); } #[test] fn full_escape() { assert_eq!(escape("foo '\"' bar"), "foo \\'\\\"\\' bar"); } #[test] fn escape_multibyte() { assert_eq!(escape("あい"), "\\あ\\い"); } #[test] fn escape_newline() { assert_eq!(escape(r"'\n'"), "\\\'\\\\n\\\'"); } } forensic-adb-0.7.0/src/test.rs000064400000000000000000000672061046102023000143130ustar 00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Currently the mozdevice API is not safe for multiple requests at the same // time. It is recommended to run each of the unit tests on its own. Also adb // specific tests cannot be run in CI yet. To check those locally, also run // the ignored tests. // // Use the following command to accomplish that: // // $ cargo test -- --ignored --test-threads=1 use crate::*; use futures::future::BoxFuture; use std::collections::BTreeSet; use std::panic; use std::path::PathBuf; use tempfile::{tempdir, TempDir}; #[tokio::test] async fn read_length_from_valid_string() { async fn test(message: &str) -> Result { read_length(&mut tokio::io::BufReader::new(message.as_bytes())).await } assert_eq!(test("0000").await.unwrap(), 0); assert_eq!(test("0001").await.unwrap(), 1); assert_eq!(test("000F").await.unwrap(), 15); assert_eq!(test("00FF").await.unwrap(), 255); assert_eq!(test("0FFF").await.unwrap(), 4095); assert_eq!(test("FFFF").await.unwrap(), 65535); assert_eq!(test("FFFF0").await.unwrap(), 65535); } #[tokio::test] async fn read_length_from_invalid_string() { async fn test(message: &str) -> Result { read_length(&mut tokio::io::BufReader::new(message.as_bytes())).await } test("").await.expect_err("empty string"); test("G").await.expect_err("invalid hex character"); test("-1").await.expect_err("negative number"); test("000").await.expect_err("shorter than 4 bytes"); } #[test] fn encode_message_with_valid_string() { assert_eq!(encode_message("").unwrap(), "0000".to_string()); assert_eq!(encode_message("a").unwrap(), "0001a".to_string()); assert_eq!( encode_message(&"a".repeat(15)).unwrap(), format!("000F{}", "a".repeat(15)) ); assert_eq!( encode_message(&"a".repeat(255)).unwrap(), format!("00FF{}", "a".repeat(255)) ); assert_eq!( encode_message(&"a".repeat(4095)).unwrap(), format!("0FFF{}", "a".repeat(4095)) ); assert_eq!( encode_message(&"a".repeat(65535)).unwrap(), format!("FFFF{}", "a".repeat(65535)) ); } #[test] fn encode_message_with_invalid_string() { encode_message(&"a".repeat(65536)).expect_err("string lengths exceeds 4 bytes"); } async fn run_device_test(test: F) where F: for<'a> FnOnce(&'a Device, &'a TempDir, &'a UnixPath) -> BoxFuture<'a, ()> + panic::UnwindSafe, { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Auto) .await .expect("device_or_default"); let tmp_dir = tempdir().expect("create temp dir"); let response = device .execute_host_shell_command("echo $EXTERNAL_STORAGE") .await .unwrap(); let mut test_root = UnixPathBuf::from(response.trim_end_matches('\n')); test_root.push("mozdevice"); let _ = device.remove(&test_root); // TODO: we've removed panic::catch_unwind here, if the test crashes the forwarding isn't cleaned up let _result = test(&device, &tmp_dir, &test_root).await; let _ = device.kill_forward_all_ports().await; // let _ = device.kill_reverse_all_ports(); // assert!(result.is_ok()) } #[tokio::test] #[ignore] async fn host_features() { let host = Host { ..Default::default() }; let set = host .features::>() .await .expect("to query features"); assert!(set.contains("cmd")); assert!(set.contains("shell_v2")); } #[tokio::test] #[ignore] async fn host_devices() { let host = Host { ..Default::default() }; let set: BTreeSet<_> = host.devices().await.expect("to query devices"); assert_eq!(1, set.len()); } #[tokio::test] #[ignore] async fn host_device_or_default() { let host = Host { ..Default::default() }; let devices: Vec<_> = host.devices().await.expect("to query devices"); let expected_device = devices.first().expect("found a device"); let device = host .device_or_default::(Some(&expected_device.serial), AndroidStorageInput::App) .await .expect("connected device with serial"); assert_eq!(device.run_as_package, None); assert_eq!(device.serial, expected_device.serial); assert!(device.tempfile.starts_with("/data/local/tmp")); } #[tokio::test] #[ignore] async fn host_device_or_default_invalid_serial() { let host = Host { ..Default::default() }; host.device_or_default::(Some(&"foobar".to_owned()), AndroidStorageInput::Auto) .await .expect_err("invalid serial"); } #[tokio::test] #[ignore] async fn host_device_or_default_no_serial() { let host = Host { ..Default::default() }; let devices: Vec<_> = host.devices().await.expect("to query devices"); let expected_device = devices.first().expect("found a device"); let device = host .device_or_default::(None, AndroidStorageInput::Auto) .await .expect("connected device with serial"); assert_eq!(device.serial, expected_device.serial); } #[tokio::test] #[ignore] async fn host_device_or_default_storage_as_app() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::App) .await .expect("connected device"); assert_eq!(device.storage, AndroidStorage::App); } #[tokio::test] #[ignore] async fn host_device_or_default_storage_as_auto() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Auto) .await .expect("connected device"); assert_eq!(device.storage, AndroidStorage::Sdcard); } #[tokio::test] #[ignore] async fn host_device_or_default_storage_as_internal() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Internal) .await .expect("connected device"); assert_eq!(device.storage, AndroidStorage::Internal); } #[tokio::test] #[ignore] async fn host_device_or_default_storage_as_sdcard() { let host = Host { ..Default::default() }; let device = host .device_or_default::(None, AndroidStorageInput::Sdcard) .await .expect("connected device"); assert_eq!(device.storage, AndroidStorage::Sdcard); } #[tokio::test] #[ignore] async fn device_shell_command() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { assert_eq!( "Linux\n", device .execute_host_shell_command("uname") .await .expect("to have shell output") ); }) }) .await; } #[tokio::test] #[ignore] async fn device_forward_port_hardcoded() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { assert_eq!( 3035, device .forward_port(3035, 3036) .await .expect("forwarded local port") ); // TODO: check with forward --list }) }) .await; } // #[test] // #[ignore] // TODO: "adb server response to `forward tcp:0 ...` was not a u16: \"000559464\"") // fn device_forward_port_system_allocated() { // run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { // let local_port = device.forward_port(0, 3037).expect("local_port"); // assert_ne!(local_port, 0); // // TODO: check with forward --list // }); // } #[tokio::test] #[ignore] async fn device_kill_forward_port_no_forwarded_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { device .kill_forward_port(3038) .await .expect_err("adb error: listener 'tcp:3038' "); }) }) .await; } #[tokio::test] #[ignore] async fn device_kill_forward_port_twice() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { let local_port = device .forward_port(3039, 3040) .await .expect("forwarded local port"); assert_eq!(local_port, 3039); // TODO: check with forward --list device .kill_forward_port(local_port) .await .expect("to remove forwarded port"); device .kill_forward_port(local_port) .await .expect_err("adb error: listener 'tcp:3039' "); }) }) .await; } #[tokio::test] #[ignore] async fn device_kill_forward_all_ports_no_forwarded_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { device .kill_forward_all_ports() .await .expect("to not fail for no forwarded ports"); }) }) .await; } #[tokio::test] #[ignore] async fn device_kill_forward_all_ports_twice() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { let local_port1 = device .forward_port(3039, 3040) .await .expect("forwarded local port"); assert_eq!(local_port1, 3039); let local_port2 = device .forward_port(3041, 3042) .await .expect("forwarded local port"); assert_eq!(local_port2, 3041); // TODO: check with forward --list device .kill_forward_all_ports() .await .expect("to remove all forwarded ports"); device .kill_forward_all_ports() .await .expect("to not fail for no forwarded ports"); }) }) .await; } #[tokio::test] #[ignore] async fn device_reverse_port_hardcoded() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { assert_eq!( 4035, device.reverse_port(4035, 4036).await.expect("remote_port") ); // TODO: check with reverse --list }) }) .await; } // #[test] // #[ignore] // TODO: No adb response: ParseInt(ParseIntError { kind: Empty }) // fn device_reverse_port_system_allocated() { // run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { // let reverse_port = device.reverse_port(0, 4037).expect("remote port"); // assert_ne!(reverse_port, 0); // // TODO: check with reverse --list // }); // } #[tokio::test] #[ignore] async fn device_kill_reverse_port_no_reverse_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { device .kill_reverse_port(4038) .await .expect_err("listener 'tcp:4038' not found"); }) }) .await; } // #[test] // #[ignore] // TODO: "adb error: adb server response did not contain expected hexstring length: \"\"" // fn device_kill_reverse_port_twice() { // run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { // let remote_port = device // .reverse_port(4039, 4040) // .expect("reversed local port"); // assert_eq!(remote_port, 4039); // // TODO: check with reverse --list // device // .kill_reverse_port(remote_port) // .expect("to remove reverse port"); // device // .kill_reverse_port(remote_port) // .expect_err("listener 'tcp:4039' not found"); // }); // } #[tokio::test] #[ignore] async fn device_kill_reverse_all_ports_no_reversed_port() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { device .kill_reverse_all_ports() .await .expect("to not fail for no reversed ports"); }) }) .await; } #[tokio::test] #[ignore] async fn device_kill_reverse_all_ports_twice() { run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { Box::pin(async { let local_port1 = device .forward_port(4039, 4040) .await .expect("forwarded local port"); assert_eq!(local_port1, 4039); let local_port2 = device .forward_port(4041, 4042) .await .expect("forwarded local port"); assert_eq!(local_port2, 4041); // TODO: check with reverse --list device .kill_reverse_all_ports() .await .expect("to remove all reversed ports"); device .kill_reverse_all_ports() .await .expect("to not fail for no reversed ports"); }) }) .await; } #[tokio::test] #[ignore] async fn device_push_pull_text_file() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { Box::pin(async { let content = "test"; let remote_path = remote_root_path.join("foo.txt"); device .push( &mut tokio::io::BufReader::new(content.as_bytes()), &remote_path, 0o777, ) .await .expect("file has been pushed"); let file_content = device .execute_host_shell_command(&format!("cat {}", remote_path.display())) .await .expect("host shell command for 'cat' to succeed"); assert_eq!(file_content, content); // And as second step pull it off the device. let mut buffer = Vec::new(); device .pull(&remote_path, &mut buffer) .await .expect("file has been pulled"); assert_eq!(buffer, content.as_bytes()); }) }, ) .await; } #[tokio::test] #[ignore] async fn device_push_pull_large_binary_file() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { Box::pin(async { let remote_path = remote_root_path.join("foo.binary"); let mut content = Vec::new(); // Needs to be larger than 64kB to test multiple chunks. for i in 0..100000u32 { content.push('0' as u8 + (i % 10) as u8); } device .push( &mut std::io::Cursor::new(content.clone()), &remote_path, 0o777, ) .await .expect("large file has been pushed"); let output = device .execute_host_shell_command(&format!("ls -l {}", remote_path.display())) .await .expect("host shell command for 'ls' to succeed"); assert!(output.contains(remote_path.to_str().unwrap())); let mut buffer = Vec::new(); device .pull(&remote_path, &mut buffer) .await .expect("large binary file has been pulled"); assert_eq!(buffer, content); }) }, ) .await; } #[tokio::test] #[ignore] async fn device_push_permission() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { Box::pin(async { fn adjust_mode(mode: u32) -> u32 { // Adjust the mode by copying the user permissions to // group and other as indicated in // [send_impl](https://android.googlesource.com/platform/system/core/+/master/adb/daemon/file_sync_service.cpp#516). // This ensures that group and other can both access a // file if the user can access it. let mut m = mode & 0o777; m |= (m >> 3) & 0o070; m |= (m >> 3) & 0o007; m } fn get_permissions(mode: u32) -> String { // Convert the mode integer into the string representation // of the mode returned by `ls`. This assumes the object is // a file and not a directory. let mut perms = vec!["-", "r", "w", "x", "r", "w", "x", "r", "w", "x"]; let mut bit_pos = 0; while bit_pos < 9 { if (1 << bit_pos) & mode == 0 { perms[9 - bit_pos] = "-" } bit_pos += 1; } perms.concat() } let content = "test"; let remote_path = remote_root_path.join("foo.bar"); // First push the file to the device let modes = vec![0o421, 0o644, 0o666, 0o777]; for mode in modes { let adjusted_mode = adjust_mode(mode); let adjusted_perms = get_permissions(adjusted_mode); device .push( &mut tokio::io::BufReader::new(content.as_bytes()), &remote_path, mode, ) .await .expect("file has been pushed"); let output = device .execute_host_shell_command(&format!("ls -l {}", remote_path.display())) .await .expect("host shell command for 'ls' to succeed"); assert!(output.contains(remote_path.to_str().unwrap())); assert!(output.starts_with(&adjusted_perms)); } let output = device .execute_host_shell_command(&format!("ls -ld {}", remote_root_path.display())) .await .expect("host shell command for 'ls parent' to succeed"); assert!(output.contains(remote_root_path.to_str().unwrap())); assert!(output.starts_with("drwxrwxrwx")); }) }, ) .await; } #[tokio::test] #[ignore] async fn device_pull_fails_for_missing_file() { run_device_test( |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { Box::pin(async { let mut buffer = Vec::new(); device .pull(&remote_root_path.join("missing"), &mut buffer) .await .expect_err("missing file should not be pulled"); }) }, ) .await; } #[tokio::test] #[ignore] async fn device_push_and_list_dir() { run_device_test( |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { Box::pin(async move { let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"]; for file in files.iter() { let path = tmp_dir.path().join(Path::new(file)); let _ = std::fs::create_dir_all(path.parent().unwrap()); let f = File::create(path).await.expect("to create file"); let mut f = tokio::io::BufWriter::new(f); f.write_all(file.as_bytes()).await.expect("to write data"); } device .push_dir(tmp_dir.path(), &remote_root_path, 0o777) .await .expect("to push_dir"); for file in files.iter() { let path = append_components(remote_root_path, Path::new(file)).unwrap(); let output = device .execute_host_shell_command(&format!("ls {}", path.display())) .await .expect("host shell command for 'ls' to succeed"); assert!(output.contains(path.to_str().unwrap())); } let mut listings = device .list_dir(&remote_root_path) .await .expect("to list_dir"); listings.sort(); assert_eq!( listings, vec![ RemoteDirEntry { depth: 0, name: "foo1.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 8 }) }, RemoteDirEntry { depth: 0, name: "foo2.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 8 }) }, RemoteDirEntry { depth: 0, name: "bar".to_string(), metadata: RemoteMetadata::RemoteDir }, RemoteDirEntry { depth: 1, name: "bar/foo3.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 12 }) }, RemoteDirEntry { depth: 1, name: "bar/more".to_string(), metadata: RemoteMetadata::RemoteDir }, RemoteDirEntry { depth: 2, name: "bar/more/foo3.bar".to_string(), metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 17 }) } ] ); }) }, ) .await; } #[tokio::test] #[ignore] async fn device_push_and_pull_dir() { run_device_test( |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { Box::pin(async move { let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"]; let src_dir = tmp_dir.path().join(Path::new("src")); let dest_dir = tmp_dir.path().join(Path::new("src")); for file in files.iter() { let path = src_dir.join(Path::new(file)); let _ = std::fs::create_dir_all(path.parent().unwrap()); let f = File::create(path).await.expect("to create file"); let mut f = tokio::io::BufWriter::new(f); f.write_all(file.as_bytes()).await.expect("to write data"); } device .push_dir(&src_dir, &remote_root_path, 0o777) .await .expect("to push_dir"); device .pull_dir(remote_root_path, &dest_dir) .await .expect("to pull_dir"); for file in files.iter() { let path = dest_dir.join(Path::new(file)); let mut f = File::open(path).await.expect("to open file"); let mut buf = String::new(); f.read_to_string(&mut buf).await.expect("to read content"); assert_eq!(buf, *file); } }) }, ) .await } #[tokio::test] #[ignore] async fn device_push_and_list_dir_flat() { run_device_test( |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { Box::pin(async move { let content = "test"; let files = [ PathBuf::from("foo1.bar"), PathBuf::from("foo2.bar"), PathBuf::from("bar").join("foo3.bar"), ]; for file in files.iter() { let path = tmp_dir.path().join(&file); let _ = std::fs::create_dir_all(path.parent().unwrap()); let f = File::create(path).await.expect("to create file"); let mut f = tokio::io::BufWriter::new(f); f.write_all(content.as_bytes()) .await .expect("to write data"); } device .push_dir(tmp_dir.path(), &remote_root_path, 0o777) .await .expect("to push_dir"); for file in files.iter() { let path = append_components(remote_root_path, file).unwrap(); let output = device .execute_host_shell_command(&format!("ls {}", path.display())) .await .expect("host shell command for 'ls' to succeed"); assert!(output.contains(path.to_str().unwrap())); } let mut listings = device .list_dir_flat(&remote_root_path, 7, "prefix".to_string()) .await .expect("to list_dir_flat"); listings.sort(); assert_eq!( listings, vec![ RemoteDirEntry { depth: 7, metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 4 }), name: "prefix/foo1.bar".to_string(), }, RemoteDirEntry { depth: 7, metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { mode: 0b110110000, size: 4 }), name: "prefix/foo2.bar".to_string(), }, RemoteDirEntry { depth: 7, metadata: RemoteMetadata::RemoteDir, name: "prefix/bar".to_string(), }, ] ); }) }, ) .await; } #[test] fn format_own_device_error_types() { assert_eq!( format!("{}", DeviceError::InvalidStorage), "Invalid storage".to_string() ); assert_eq!( format!("{}", DeviceError::MissingPackage), "Missing package".to_string() ); assert_eq!( format!("{}", DeviceError::MultipleDevices), "Multiple Android devices online".to_string() ); assert_eq!( format!("{}", DeviceError::Adb("foo".to_string())), "foo".to_string() ); }