diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..510daa2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,241 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + macos: + name: macOS Build + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: "true" + + - name: Install just + run: | + brew install just + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: macos-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} + restore-keys: macos-cargo- + + - name: Install rustup targets + run: | + rustup target add aarch64-apple-ios && rustup target add x86_64-apple-ios && \ + rustup target add aarch64-apple-ios-sim && rustup target add aarch64-apple-darwin && \ + rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli + + - name: Build all Apple targets and examples/tools + run: | + just macos-ci-check + + - name: Upload static libraries + uses: actions/upload-artifact@v4 + with: + name: libidevice-macos-a + path: | + target/*apple*/release/libidevice_ffi.a + + - name: Upload macOS+iOS XCFramework + uses: actions/upload-artifact@v4 + with: + name: idevice-xcframework + path: swift/bundle.zip + + - name: Upload C examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-c-examples-macos + path: ffi/examples/build/bin/* + + - name: Upload C++ examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-cpp-examples-macos + path: cpp/examples/build/bin/* + + - name: Stage Rust tools (arm64) + shell: bash + run: | + mkdir -p dist/arm64 + find target/release -maxdepth 1 -type f -exec sh -c ' + for f in "$@"; do + if file "$f" | grep -Eq "Mach-O .* executable|ELF .* executable"; then + cp "$f" dist/arm64/ + fi + done + ' sh {} + + + - name: Upload Rust tools (arm64) + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-macos-arm + path: dist/arm64/* + if-no-files-found: error + + - name: Stage Rust tools (x64) + shell: bash + run: | + mkdir -p dist/x64 + find target/x86_64-apple-darwin/release -maxdepth 1 -type f -exec sh -c ' + for f in "$@"; do + if file "$f" | grep -Eq "Mach-O .* executable|ELF .* executable"; then + cp "$f" dist/x64/ + fi + done + ' sh {} + + + - name: Upload Rust tools (x64) + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-macos-intel + path: dist/x64/* + if-no-files-found: error + + linux: + name: Linux Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: "true" + + - name: Install build dependencies + run: | + sudo apt-get update && sudo apt-get install -y build-essential cmake + + - name: Install just + run: | + curl -sL https://just.systems/install.sh | bash -s -- --to ~/.cargo/bin + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: linux-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} + restore-keys: linux-cargo- + + - name: Build Rust and examples/tools + run: | + just ci-check + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: libidevice-linux-a + path: target/release/libidevice_ffi.a + + - name: Upload headers + uses: actions/upload-artifact@v4 + with: + name: idevice-headers + path: ffi/idevice.h + + - name: Upload C examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-c-examples-linux + path: ffi/examples/build/bin/* + + - name: Upload C++ examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-cpp-examples-linux + path: cpp/examples/build/bin/* + + - name: Stage Rust tools (linux) + shell: bash + run: | + mkdir -p dist + find target/release -maxdepth 1 -type f -exec sh -c ' + for f in "$@"; do + if file "$f" | grep -Eq "ELF .* executable"; then + cp "$f" dist/ + fi + done + ' sh {} + + + - name: Upload Rust tools + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-linux + path: dist/* + if-no-files-found: error + + windows: + name: Windows Build + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: "true" + + # Install Rust (adds cargo/rustc to PATH) + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + + # Use Scoop to install just (reuse your existing scoop setup) + - uses: MinoruSekine/setup-scoop@v4.0.2 + with: + buckets: main extras + apps: just doxygen plantuml + + # (Paranoid) ensure shims and cargo bin are on PATH for subsequent steps + - name: Ensure tools on PATH + shell: pwsh + run: | + echo "$env:USERPROFILE\scoop\shims" >> $env:GITHUB_PATH + echo "$env:USERPROFILE\.cargo\bin" >> $env:GITHUB_PATH + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~\AppData\Local\cargo\registry + ~\AppData\Local\cargo\git + target + key: windows-cargo-${{ hashFiles('**/Cargo.lock', 'justfile') }} + restore-keys: windows-cargo- + + - name: Build Rust and examples/tools + run: just windows-ci-check + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: libidevice-windows-a + path: target\release\idevice_ffi.lib + + - name: Upload C++ examples/tools + uses: actions/upload-artifact@v4 + with: + name: idevice-cpp-examples-windows + path: cpp\examples\build\bin\* + + - name: Stage Rust tools (windows) + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path dist | Out-Null + Get-ChildItem target\release\*.exe -File | Copy-Item -Destination dist + + - name: Upload Rust tools + uses: actions/upload-artifact@v4 + with: + name: idevice-tools-windows + path: dist\*.exe + if-no-files-found: error diff --git a/.gitignore b/.gitignore index f5383ee..8874511 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Image.dmg.trustcache .DS_Store *.pcap idevice.h +plist.h /ffi/examples/build /.cache /ffi/examples/.cache @@ -16,3 +17,8 @@ bundle.zip /swift/include/*.h /swift/.build /swift/.swiftpm +xcuserdata + +._* +*.vcxproj.user +.vs diff --git a/.gitmodules b/.gitmodules index 1cffb04..9b7710e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ffi/libplist"] path = ffi/libplist url = https://github.com/libimobiledevice/libplist.git +[submodule "cpp/plist_ffi"] + path = cpp/plist_ffi + url = https://github.com/jkcoxson/plist_ffi.git diff --git a/Cargo.lock b/Cargo.lock index 0dcc8d6..b924425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -51,20 +51,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -77,50 +68,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -128,6 +119,28 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "async-task" version = "4.7.1" @@ -140,37 +153,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "autotools" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" -dependencies = [ - "cc", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-sys", "zeroize", @@ -178,11 +171,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ - "bindgen 0.69.5", + "bindgen", "cc", "cmake", "dunce", @@ -212,32 +205,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" - -[[package]] -name = "bindgen" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "clap 2.34.0", - "env_logger 0.9.3", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "which", -] +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" @@ -245,10 +215,10 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools", "lazy_static", "lazycell", "log", @@ -256,43 +226,17 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", - "syn 2.0.101", + "syn 2.0.106", "which", ] -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.101", -] - [[package]] name = "bitflags" -version = "1.3.2" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -305,9 +249,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -318,9 +262,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -334,33 +278,13 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "c2rust-bitfields" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367e5d1b30f28be590b6b3868da1578361d29d9bfac516d22f497d28ed7c9055" -dependencies = [ - "c2rust-bitfields-derive 0.19.0", -] - [[package]] name = "c2rust-bitfields" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46dc7d2bffa0d0b3d47eb2dc69973466858281446c2ac9f6d8a10e92ab1017df" dependencies = [ - "c2rust-bitfields-derive 0.20.0", -] - -[[package]] -name = "c2rust-bitfields-derive" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a279db9c50c4024eeca1a763b6e0f033848ce74e83e47454bcf8a8a98f7b0b56" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "c2rust-bitfields-derive", ] [[package]] @@ -376,29 +300,30 @@ dependencies = [ [[package]] name = "cbindgen" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684" dependencies = [ - "clap 4.5.39", - "heck 0.4.1", + "clap", + "heck", "indexmap", "log", "proc-macro2", "quote", "serde", "serde_json", - "syn 2.0.101", + "syn 2.0.106", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.25" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -415,9 +340,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -459,6 +384,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -487,24 +413,9 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "clap" -version = "4.5.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -512,33 +423,33 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" @@ -551,9 +462,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -570,6 +481,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -587,19 +508,81 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossfire" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a0068be78805aee0666b90092c77c935b51bd81ec0e22b36e40f991c6c57bf" +dependencies = [ + "crossbeam", + "enum_dispatch", + "futures", + "parking_lot", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -635,7 +618,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -659,14 +642,14 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -691,7 +674,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -712,9 +695,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", @@ -740,6 +723,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -750,19 +745,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -784,19 +766,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -825,6 +807,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "flagset" version = "0.4.7" @@ -833,9 +821,9 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -861,9 +849,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -924,9 +912,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "futures-core", "pin-project-lite", @@ -940,7 +928,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -985,11 +973,11 @@ dependencies = [ [[package]] name = "getifaddrs" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba121d81ab5ea05b0cd5858516266800bf965531a794f7ac58e3eeb804f364f" +checksum = "b1c016cebf305060d144de015c98191ede05c210af588857bc2d4f8611c04663" dependencies = [ - "bitflags 2.9.1", + "bitflags", "libc", "windows-sys 0.59.0", ] @@ -1003,7 +991,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1014,11 +1002,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", + "wasi 0.14.3+wasi-0.2.4", ] [[package]] @@ -1029,21 +1015,15 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "heck" @@ -1051,15 +1031,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hkdf" version = "0.12.4" @@ -1127,53 +1098,32 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.0", -] - [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -1305,15 +1255,17 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.35" +version = "0.1.41" dependencies = [ + "async-stream", "base64", "byteorder", "bytes", "chacha20poly1305", "chrono", + "crossfire", "ed25519-dalek", - "env_logger 0.11.8", + "env_logger", "futures", "hkdf", "indexmap", @@ -1322,17 +1274,17 @@ dependencies = [ "ns-keyed-archive", "obfstr", "plist", - "rand 0.9.1", + "rand 0.9.2", "reqwest", "rsa", "rustls", "serde", "serde_json", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tokio-rustls", - "tun-rs 2.1.4", + "tun-rs", "uuid", "x25519-dalek", "x509-cert", @@ -1343,38 +1295,42 @@ name = "idevice-ffi" version = "0.1.0" dependencies = [ "cbindgen", + "futures", "idevice", "libc", "log", "once_cell", "plist", - "plist_plus", + "plist_ffi", "simplelog", "tokio", + "ureq", + "uuid", + "windows-sys 0.60.2", ] [[package]] name = "idevice-tools" version = "0.1.0" dependencies = [ - "clap 4.5.39", - "env_logger 0.11.8", + "clap", + "env_logger", + "futures-util", "idevice", "log", "ns-keyed-archive", "plist", "sha2", "tokio", - "tun-rs 1.5.0", "ureq", "uuid", ] [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1393,9 +1349,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -1411,6 +1367,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1442,15 +1409,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -1459,9 +1417,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -1472,20 +1430,20 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1493,9 +1451,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -1524,9 +1482,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -1535,7 +1493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.3", ] [[package]] @@ -1574,15 +1532,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "mac_address" @@ -1596,9 +1548,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -1609,12 +1561,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1623,9 +1569,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -1637,7 +1583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1650,6 +1596,27 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "netconfig-rs" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207561c8758738388c2fd851c3e759f503583d8dd70ddc895baae806753ad4c9" +dependencies = [ + "cfg-if", + "core-foundation", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "nix 0.30.1", + "scopeguard", + "system-configuration-sys", + "thiserror 2.0.16", + "widestring", + "windows", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -1663,12 +1630,12 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags", "byteorder", "libc", "log", @@ -1705,7 +1672,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1718,10 +1685,11 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -1750,9 +1718,9 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f237a10fe003123daa55a74b63a0b0a65de1671b2d128711ffe5886891a8f77f" dependencies = [ - "clap 4.5.39", + "clap", "plist", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -1885,12 +1853,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1902,9 +1864,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1952,9 +1914,9 @@ dependencies = [ [[package]] name = "plist" -version = "1.7.1" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64", "indexmap", @@ -1964,17 +1926,16 @@ dependencies = [ ] [[package]] -name = "plist_plus" -version = "0.2.6" +name = "plist_ffi" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167429a361cacecf5cab907c235e620b3faf5f36b97d7f72c32907ccdca700cf" +checksum = "22a5ca928241bc2e8c5fd28b81772962389efdbfcb71dfc9ec694369e063cb3a" dependencies = [ - "autotools", - "bindgen 0.59.2", + "cbindgen", "cc", "libc", - "log", - "rand 0.8.5", + "plist", + "serde_json", ] [[package]] @@ -1990,9 +1951,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -2005,9 +1966,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -2029,87 +1990,32 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.33" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.1", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.12", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.40" @@ -2121,9 +2027,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -2131,16 +2037,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2186,18 +2091,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -2207,9 +2112,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -2218,15 +2123,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.12.19" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", @@ -2235,24 +2140,16 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2260,7 +2157,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.0", ] [[package]] @@ -2279,17 +2175,16 @@ dependencies = [ [[package]] name = "route_manager" -version = "0.1.4" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f6f4d3aee0d83fd4da6dc58abc2ae1765504e4b8e70d32f5876376c6c8f3f4c" +checksum = "bb012980f7bfadc330cc5b99e2a93bda641717554338b6ddb0e86de4188af65b" dependencies = [ - "bindgen 0.71.1", "flume", "libc", "netlink-packet-core", "netlink-packet-route", "netlink-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2315,9 +2210,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -2325,12 +2220,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2346,7 +2235,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2355,22 +2244,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", @@ -2397,15 +2286,14 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -2415,9 +2303,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2454,14 +2342,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -2471,9 +2359,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -2520,9 +2408,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -2550,27 +2438,24 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2598,12 +2483,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.11.1" @@ -2629,9 +2508,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -2655,20 +2534,30 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -2680,15 +2569,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -2700,11 +2580,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -2715,28 +2595,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -2748,15 +2627,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -2772,21 +2651,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tls_codec" version = "0.4.2" @@ -2805,25 +2669,27 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2834,7 +2700,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2849,9 +2715,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -2861,18 +2727,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -2884,9 +2750,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" @@ -2905,11 +2771,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags", "bytes", "futures-util", "http", @@ -2945,9 +2811,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2960,43 +2826,14 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tun-rs" -version = "1.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53141e64197ff7e758b8152615e50bb4a3b18c970738876e7906d31f242c7d6e" +checksum = "49f0699ba6cb89fd48f243b325d5943ec8ecf46fec969ceee2228b1e2f34a74a" dependencies = [ - "bitflags 2.9.1", "blocking", "byteorder", "bytes", - "c2rust-bitfields 0.19.0", - "cfg-if", - "encoding_rs", - "getifaddrs", - "ipnet", - "libc", - "libloading", - "log", - "mac_address", - "nix 0.29.0", - "scopeguard", - "thiserror 2.0.12", - "tokio", - "windows-sys 0.59.0", - "winreg 0.52.0", - "wintun-bindings", -] - -[[package]] -name = "tun-rs" -version = "2.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89aa659131eb386e5bfe7443d97e2dfb0e0ccef611f2de14fc73549e6db60a7a" -dependencies = [ - "bindgen 0.71.1", - "blocking", - "byteorder", - "bytes", - "c2rust-bitfields 0.20.0", + "c2rust-bitfields", "encoding_rs", "getifaddrs", "ipnet", @@ -3004,13 +2841,14 @@ dependencies = [ "libloading", "log", "mac_address", + "netconfig-rs", "nix 0.30.1", "route_manager", "scopeguard", "tokio", "widestring", - "windows-sys 0.59.0", - "winreg 0.55.0", + "windows-sys 0.60.2", + "winreg", ] [[package]] @@ -3025,12 +2863,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "universal-hash" version = "0.5.1" @@ -3049,9 +2881,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.0.11" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" +checksum = "00432f493971db5d8e47a65aeb3b02f8226b9b11f1450ff86bb772776ebadd70" dependencies = [ "base64", "flate2", @@ -3062,14 +2894,14 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf-8", - "webpki-roots 0.26.11", + "webpki-roots", ] [[package]] name = "ureq-proto" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" +checksum = "bbe120bb823a0061680e66e9075942fcdba06d46551548c2c259766b9558bc9a" dependencies = [ "base64", "http", @@ -3079,13 +2911,14 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3108,9 +2941,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -3118,12 +2951,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.5" @@ -3141,50 +2968,51 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -3195,9 +3023,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3205,41 +3033,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -3247,18 +3065,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.0", -] - -[[package]] -name = "webpki-roots" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -3299,11 +3108,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3312,6 +3121,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -3325,6 +3156,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -3333,7 +3175,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -3344,14 +3186,24 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] [[package]] name = "windows-result" @@ -3371,15 +3223,6 @@ dependencies = [ "windows-link", ] -[[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" @@ -3399,18 +3242,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 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", + "windows-targets 0.53.3", ] [[package]] @@ -3431,10 +3268,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3446,10 +3284,13 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-threading" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] [[package]] name = "windows_aarch64_gnullvm" @@ -3463,12 +3304,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[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.6" @@ -3481,12 +3316,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[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.6" @@ -3511,12 +3340,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[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.6" @@ -3529,12 +3352,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[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.6" @@ -3547,12 +3364,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[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.6" @@ -3565,12 +3376,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[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.6" @@ -3585,23 +3390,13 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -3613,26 +3408,10 @@ dependencies = [ ] [[package]] -name = "wintun-bindings" -version = "0.7.31" +name = "wit-bindgen" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605f50b13e12e1f9f99dc5e93701d779dbe47282fec186cb8a079165368d3124" -dependencies = [ - "c2rust-bitfields 0.19.0", - "libloading", - "log", - "thiserror 2.0.12", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "writeable" @@ -3686,28 +3465,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -3727,7 +3506,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "synstructure", ] @@ -3748,7 +3527,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -3764,9 +3543,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -3781,5 +3560,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] diff --git a/Cargo.toml b/Cargo.toml index 55f15b7..89e80ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,10 @@ [workspace] resolver = "2" -members = [ "ffi","idevice", "tools"] +members = ["ffi", "idevice", "tools"] + +[profile.release] +opt-level = "z" +codegen-units = 1 +lto = true +panic = "abort" diff --git a/README.md b/README.md index 2bed136..50a311a 100644 --- a/README.md +++ b/README.md @@ -28,20 +28,22 @@ To keep dependency bloat and compile time down, everything is contained in featu | `core_device_proxy` | Start a secure tunnel to access protected services. | | `crashreportcopymobile`| Copy crash reports.| | `debug_proxy` | Send GDB commands to the device.| +| `diagnostics_relay` | Access device diagnostics information (IORegistry, MobileGestalt, battery, NAND, device control).| | `dvt` | Access Apple developer tools (e.g. Instruments).| | `heartbeat` | Maintain a heartbeat connection.| | `house_arrest` | Manage files in app containers | | `installation_proxy` | Manage app installation and uninstallation.| | `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.| | `misagent` | Manage provisioning profiles on the device.| +| `mobilebackup2` | Manage backups.| | `mobile_image_mounter` | Manage DDI images.| | `location_simulation` | Simulate GPS locations on the device.| | `pair` | Pair the device.| | `syslog_relay` | Relay system logs from the device | | `tcp` | Connect to devices over TCP.| | `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.| -| `tss` | Make requests to Apple’s TSS servers. Partial support.| -| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)’s tunneld. | +| `tss` | Make requests to Apple's TSS servers. Partial support.| +| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. | | `usbmuxd` | Connect using the usbmuxd daemon.| | `xpc` | Access protected services via XPC over RSD. | diff --git a/cpp/.clang-format b/cpp/.clang-format new file mode 100644 index 0000000..4491888 --- /dev/null +++ b/cpp/.clang-format @@ -0,0 +1,32 @@ +# .clang-format +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +BreakBeforeBraces: Attach +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlignConsecutiveAssignments: AcrossEmptyLinesAndComments +AlignConsecutiveDeclarations: AcrossEmptyLinesAndComments +AlignTrailingComments: true +SortIncludes: CaseInsensitive +IncludeBlocks: Preserve +SpaceBeforeParens: ControlStatements +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: Never +SpaceAfterCStyleCast: true +Cpp11BracedListStyle: true +BreakBeforeBinaryOperators: NonAssignment +ReflowComments: true +PointerAlignment: Left +BinPackArguments: false +BinPackParameters: false +AllowShortBlocksOnASingleLine: Never +MacroBlockBegin: "^#define" +MacroBlockEnd: "^#undef" +InsertBraces: true diff --git a/cpp/.clangd b/cpp/.clangd new file mode 100644 index 0000000..aa2b394 --- /dev/null +++ b/cpp/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Add: ["-I/usr/local/include/"] + CompilationDatabase: "examples/build" diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..0bbee44 --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,119 @@ +cmake_minimum_required(VERSION 3.16) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Project +project(idevice++ + VERSION 0.1.0 + LANGUAGES CXX +) + +# ---- Options --------------------------------------------------------------- + +# Path to the Rust static library (override on the command line if needed): +# cmake -DIDEVICE_FFI_PATH=/absolute/path/to/libidevice_ffi.a .. +set(IDEVICE_FFI_PATH + "${CMAKE_CURRENT_SOURCE_DIR}/../target/release/libidevice_ffi.a" + CACHE FILEPATH "Path to libidevice_ffi.a produced by Rust build" +) + +# Optional extra libraries/frameworks if your Rust/C bridge needs them: +# e.g. -framework CoreFoundation on macOS, etc. +set(IDEVICEPP_EXTRA_LIBS + "" + CACHE STRING "Extra libraries to link (space-separated)" +) + +# ---- Tooling --------------------------------------------------------------- + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Helpful for shared libs + +# Threads +find_package(Threads REQUIRED) + +# On some platforms dl is required for Rust staticlibs that use libdl +include(CheckLibraryExists) +set(HAVE_LIBDL FALSE) +if(UNIX AND NOT APPLE) + check_library_exists(dl dlopen "" HAVE_LIBDL) +endif() + +# ---- Imported Rust static library ------------------------------------------ + +if(NOT EXISTS "${IDEVICE_FFI_PATH}") + message(FATAL_ERROR "IDEVICE_FFI_PATH does not exist: ${IDEVICE_FFI_PATH}") +endif() + +add_library(idevice_ffi STATIC IMPORTED GLOBAL) +set_target_properties(idevice_ffi PROPERTIES + IMPORTED_LOCATION "${IDEVICE_FFI_PATH}" +) + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/../ffi/idevice.h" "${CMAKE_CURRENT_SOURCE_DIR}/include" COPYONLY) + +# ---- Our C++ library ------------------------------------------------------- + +# Collect sources (convenience: tracks new files when you re-run CMake) +file(GLOB_RECURSE IDEVICEPP_SOURCES + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cxx" +) + +# Respect BUILD_SHARED_LIBS (OFF=static, ON=shared) +add_library(${PROJECT_NAME}) +target_sources(${PROJECT_NAME} PRIVATE ${IDEVICEPP_SOURCES}) + +# Public headers live under include/, e.g. include/idevice++/Foo.hpp +target_include_directories(${PROJECT_NAME} + PUBLIC + "$" + "$" +) + +# Link dependencies +target_link_libraries(${PROJECT_NAME} + PUBLIC + idevice_ffi + Threads::Threads +) + +# Link libdl on Linux if present +if(HAVE_LIBDL) + target_link_libraries(${PROJECT_NAME} PUBLIC dl) +endif() + +# Windows winsock (if you ever build there) +if(WIN32) + target_link_libraries(${PROJECT_NAME} PUBLIC ws2_32) +endif() + +# Extra user-specified libs/frameworks +if(IDEVICEPP_EXTRA_LIBS) + # Split by spaces and link each one + separate_arguments(_extra_libs NATIVE_COMMAND ${IDEVICEPP_EXTRA_LIBS}) + target_link_libraries(${PROJECT_NAME} PUBLIC ${_extra_libs}) +endif() + +# Visibility / warnings (tweak to taste) +if(MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE /permissive- /W4 /WX-) +else() + target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# ---- Examples (optional) --------------------------------------------------- + +option(BUILD_EXAMPLES "Build examples in examples/ directory" OFF) +if(BUILD_EXAMPLES) + file(GLOB EXAMPLE_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/examples/*.cpp") + foreach(ex_src IN LISTS EXAMPLE_SOURCES) + get_filename_component(exe_name "${ex_src}" NAME_WE) + add_executable(${exe_name} "${ex_src}") + target_link_libraries(${exe_name} PRIVATE ${PROJECT_NAME}) + target_include_directories(${exe_name} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") + endforeach() +endif() + diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt new file mode 100644 index 0000000..40e3dfb --- /dev/null +++ b/cpp/examples/CMakeLists.txt @@ -0,0 +1,107 @@ +# Jackson Coxson + +cmake_minimum_required(VERSION 3.15) +project(IdeviceFFI CXX) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Paths +set(IDEVICE_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../include) # public C++ headers +set(IDEVICE_CPP_SRC_DIR ${CMAKE_SOURCE_DIR}/../src) # C++ .cpp files +set(IDEVICE_FFI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../../ffi) # Rust FFI headers (idevice.h) +set(PLIST_CPP_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../plist_ffi/cpp/include) +set(PLIST_CPP_SRC_DIR ${CMAKE_SOURCE_DIR}/../plist_ffi/cpp/src) +if (MSVC) + set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/idevice_ffi.lib) +else() + set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) +endif() +set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Warnings +if (MSVC) + add_compile_options(/W4 /permissive- /EHsc) +else() + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# ---- Build the C++ wrapper library ----------------------------------------- + +# Collect your .cpps +file(GLOB IDEVICE_CPP_SOURCES + ${IDEVICE_CPP_SRC_DIR}/*.cpp +) + +file(GLOB PLIST_CPP_SOURCES + ${PLIST_CPP_SRC_DIR}/*.cpp +) + +add_library(idevice_cpp STATIC ${IDEVICE_CPP_SOURCES} ${PLIST_CPP_SOURCES}) + +target_include_directories(idevice_cpp + PUBLIC + ${IDEVICE_CPP_INCLUDE_DIR} + ${PLIST_CPP_INCLUDE_DIR} + PRIVATE + ${IDEVICE_FFI_INCLUDE_DIR} + ${PLIST_CPP_INCLUDE_DIR} +) + +# Link to the Rust static lib (+ system libs/frameworks). Mark as PUBLIC so dependents inherit. +target_link_libraries(idevice_cpp + PUBLIC + ${STATIC_LIB} +) + +# Unix-y extras frequently required by Rust static libs +if (UNIX AND NOT APPLE) + find_package(Threads REQUIRED) + target_link_libraries(idevice_cpp PUBLIC Threads::Threads dl m) +endif() + +# Apple frameworks (propagate to dependents) +if (APPLE) + target_link_libraries(idevice_cpp PUBLIC + "-framework CoreFoundation" + "-framework Security" + "-framework SystemConfiguration" + "-framework CoreServices" + "-framework IOKit" + "-framework CFNetwork" + ) +endif() + +if (WIN32) + target_link_libraries(idevice_cpp PUBLIC + ws2_32 + userenv + ntdll + bcrypt + ) +endif() + +set_target_properties(idevice_cpp PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# ---- Build each example and link against idevice_cpp ------------------------ + +file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.cpp) + +foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) + get_filename_component(EXAMPLE_NAME ${EXAMPLE_FILE} NAME_WE) + add_executable(${EXAMPLE_NAME} ${EXAMPLE_FILE}) + + # Examples include public headers and (if they directly include FFI headers) the FFI dir. + target_include_directories(${EXAMPLE_NAME} PRIVATE + ${IDEVICE_CPP_INCLUDE_DIR} + ${IDEVICE_FFI_INCLUDE_DIR} + ${PLIST_CPP_INCLUDE_DIR} + ) + + # Link to your C++ wrapper (inherits Rust lib + frameworks/system libs) + target_link_libraries(${EXAMPLE_NAME} PRIVATE idevice_cpp) +endforeach() + diff --git a/cpp/examples/app_service.cpp b/cpp/examples/app_service.cpp new file mode 100644 index 0000000..730c889 --- /dev/null +++ b/cpp/examples/app_service.cpp @@ -0,0 +1,211 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +[[noreturn]] +static void die(const char* msg, const FfiError& e) { + std::cerr << msg << ": " << e.message << "(" << e.code << ")\n"; + std::exit(1); +} + +static void usage(const char* argv0) { + std::cerr << "Usage:\n" + << " " << argv0 << " list\n" + << " " << argv0 << " launch \n" + << " " << argv0 << " processes\n" + << " " << argv0 << " uninstall \n" + << " " << argv0 << " signal \n" + << " " << argv0 << " icon [hw=1.0] [scale=1.0]\n"; +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 2; + } + + std::string cmd = argv[1]; + + FfiError err; + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); + + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (devices)[0]; + + auto udid = dev.get_udid(); + if (udid.is_none()) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (mux_id.is_none()) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); + + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); + + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); + + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); + + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); + + // 7) AppService over RSD (borrows adapter + handshake) + auto app = AppService::connect_rsd(adapter, rsd).unwrap_or_else([&](FfiError e) -> AppService { + die("failed to connect AppService", e); // never returns + }); + + // 8) Commands + if (cmd == "list") { + auto apps = app.list_apps(/*app_clips*/ true, + /*removable*/ true, + /*hidden*/ true, + /*internal*/ true, + /*default_apps*/ true) + .unwrap_or_else( + [](FfiError e) -> std::vector { die("list_apps failed", e); }); + + for (const auto& a : apps) { + std::cout << "- " << a.bundle_identifier << " | name=" << a.name << " | version=" + << (a.version.is_some() ? a.version.unwrap() : std::string("")) + << " | dev=" << (a.is_developer_app ? "y" : "n") + << " | hidden=" << (a.is_hidden ? "y" : "n") << "\n"; + } + return 0; + + } else if (cmd == "launch") { + if (argc < 3) { + std::cerr << "No bundle ID passed\n"; + return 2; + } + std::string bundle_id = argv[2]; + + std::vector args; // empty in this example + auto resp = + app.launch(bundle_id, + args, + /*kill_existing*/ false, + /*start_suspended*/ false) + .unwrap_or_else([](FfiError e) -> LaunchResponse { die("launch failed", e); }); + + std::cout << "Launched pid=" << resp.pid << " exe=" << resp.executable_url + << " piv=" << resp.process_identifier_version + << " audit_token_len=" << resp.audit_token.size() << "\n"; + return 0; + + } else if (cmd == "processes") { + auto procs = app.list_processes().unwrap_or_else( + [](FfiError e) -> std::vector { die("list_processes failed", e); }); + + for (const auto& p : procs) { + std::cout << p.pid << " : " + << (p.executable_url.is_some() ? p.executable_url.unwrap() + : std::string("")) + << "\n"; + } + return 0; + + } else if (cmd == "uninstall") { + if (argc < 3) { + std::cerr << "No bundle ID passed\n"; + return 2; + } + std::string bundle_id = argv[2]; + + app.uninstall(bundle_id).expect("Uninstall failed"); + std::cout << "Uninstalled " << bundle_id << "\n"; + return 0; + + } else if (cmd == "signal") { + if (argc < 4) { + std::cerr << "Usage: signal \n"; + return 2; + } + uint32_t pid = static_cast(std::stoul(argv[2])); + uint32_t signal = static_cast(std::stoul(argv[3])); + + auto res = app.send_signal(pid, signal).unwrap_or_else([](FfiError e) -> SignalResponse { + die("send_signal failed", e); + }); + + std::cout << "Signaled pid=" << res.pid << " signal=" << res.signal + << " ts_ms=" << res.device_timestamp_ms << " exe=" + << (res.executable_url.is_some() ? res.executable_url.unwrap() + : std::string("")) + << "\n"; + return 0; + + } else if (cmd == "icon") { + if (argc < 4) { + std::cerr << "Usage: icon [hw=1.0] [scale=1.0]\n"; + return 2; + } + std::string bundle_id = argv[2]; + std::string save_path = argv[3]; + float hw = (argc >= 5) ? std::stof(argv[4]) : 1.0f; + float scale = (argc >= 6) ? std::stof(argv[5]) : 1.0f; + + auto icon = + app.fetch_icon(bundle_id, hw, hw, scale, /*allow_placeholder*/ true) + .unwrap_or_else([](FfiError e) -> IconData { die("fetch_app_icon failed", e); }); + + std::ofstream out(save_path, std::ios::binary); + if (!out) { + std::cerr << "Failed to open " << save_path << " for writing\n"; + return 1; + } + out.write(reinterpret_cast(icon.data.data()), + static_cast(icon.data.size())); + out.close(); + + std::cout << "Saved icon to " << save_path << " (" << icon.data.size() << " bytes, " + << icon.icon_width << "x" << icon.icon_height << ", min " << icon.minimum_width + << "x" << icon.minimum_height << ")\n"; + return 0; + + } else { + usage(argv[0]); + return 2; + } +} diff --git a/cpp/examples/debug_proxy.cpp b/cpp/examples/debug_proxy.cpp new file mode 100644 index 0000000..a451cf8 --- /dev/null +++ b/cpp/examples/debug_proxy.cpp @@ -0,0 +1,128 @@ +// Jackson Coxson + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +[[noreturn]] +static void die(const char* msg, const IdeviceFFI::FfiError& e) { + std::cerr << msg << ": " << e.message << "\n"; + std::exit(1); +} + +static std::vector split_args(const std::string& line) { + std::istringstream iss(line); + std::vector toks; + std::string tok; + while (iss >> tok) { + toks.push_back(tok); + } + return toks; +} + +int main() { + IdeviceFFI::FfiError err; + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); + + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (devices)[0]; + + auto udid = dev.get_udid(); + if (udid.is_none()) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (mux_id.is_none()) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); + + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); + + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); + + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); + + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); + + // 6) DebugProxy over RSD + auto dbg = + IdeviceFFI::DebugProxy::connect_rsd(adapter, rsd).expect("failed to connect DebugProxy"); + + std::cout << "Shell connected! Type 'exit' to quit.\n"; + for (;;) { + std::cout << "> " << std::flush; + + std::string line; + if (!std::getline(std::cin, line)) { + break; + } + // trim + auto first = line.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + continue; + } + auto last = line.find_last_not_of(" \t\r\n"); + line = line.substr(first, last - first + 1); + + if (line == "exit") { + break; + } + + // Interpret: first token = command name, rest = argv + auto toks = split_args(line); + if (toks.empty()) { + continue; + } + + std::string name = toks.front(); + std::vector argv(toks.begin() + 1, toks.end()); + + auto res = dbg.send_command(name, argv); + match_result( + res, + ok_value, + { if_let_some(ok_value, some_value, { std::cout << some_value << "\n"; }); }, + err_value, + { std::cerr << "send_command failed: " << err_value.message << "\n"; }); + } + + return 0; +} diff --git a/cpp/examples/diagnosticsservice.cpp b/cpp/examples/diagnosticsservice.cpp new file mode 100644 index 0000000..ffde043 --- /dev/null +++ b/cpp/examples/diagnosticsservice.cpp @@ -0,0 +1,127 @@ +// Jackson Coxson + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +[[noreturn]] +static void die(const char* msg, const FfiError& e) { + std::cerr << msg; + if (e) { + std::cerr << ": " << e.message; + } + std::cerr << "\n"; + std::exit(1); +} + +int main() { + idevice_init_logger(Debug, Disabled, NULL); + FfiError err; + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); + + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (devices)[0]; + + auto udid = dev.get_udid(); + if (udid.is_none()) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (mux_id.is_none()) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); + + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); + + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); + + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); + + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); + + // 7) DebugProxy over RSD + auto diag = + DiagnosticsService::connect_rsd(adapter, rsd).expect("failed to connect DebugProxy"); + + std::cout << "Getting sysdiagnose, this takes a while! iOS is slow...\n"; + + auto cap = diag.capture_sysdiagnose(/*dry_run=*/false).expect("capture_sysdiagnose failed"); + + std::cout << "Got sysdiagnose! Saving to file: " << cap.preferred_filename << "\n"; + + // 7) Stream to file with progress + std::ofstream out(cap.preferred_filename, std::ios::binary); + if (!out) { + std::cerr << "failed to open output file\n"; + return 1; + } + + std::size_t written = 0; + const std::size_t total = cap.expected_length; + + for (;;) { + auto chunk = cap.stream.next_chunk(); + match_result( + chunk, + res, + { + if_let_some(res, chunk_res, { + out.write(reinterpret_cast(chunk_res.data()), + static_cast(chunk_res.size())); + if (!out) { + std::cerr << "write failed\n"; + return 1; + } + written += chunk_res.size(); + }); + if (res.is_none()) { + break; + } + }, + err, + { die("stream error", err); }); + std::cout << "wrote " << written << "/" << total << " bytes\r" << std::flush; + } + + out.flush(); + std::cout << "\nDone! Saved to " << cap.preferred_filename << "\n"; + return 0; +} diff --git a/cpp/examples/idevice_id.cpp b/cpp/examples/idevice_id.cpp new file mode 100644 index 0000000..79bcd5e --- /dev/null +++ b/cpp/examples/idevice_id.cpp @@ -0,0 +1,23 @@ +// Jackson Coxson + +#include +#include + +int main() { + auto u = IdeviceFFI::UsbmuxdConnection::default_new(0).expect("failed to connect to usbmuxd"); + auto devices = u.get_devices().expect("failed to get devices from usbmuxd"); + + for (IdeviceFFI::UsbmuxdDevice& d : devices) { + auto udid = d.get_udid(); + if (udid.is_none()) { + std::cerr << "failed to get udid"; + continue; + } + auto connection_type = d.get_connection_type(); + if (connection_type.is_none()) { + std::cerr << "failed to get connection type"; + continue; + } + std::cout << udid.unwrap() << " (" << connection_type.unwrap().to_string() << ")" << "\n"; + } +} diff --git a/cpp/examples/ideviceinfo.cpp b/cpp/examples/ideviceinfo.cpp new file mode 100644 index 0000000..9642db0 --- /dev/null +++ b/cpp/examples/ideviceinfo.cpp @@ -0,0 +1,58 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +int main() { + idevice_init_logger(Debug, Disabled, NULL); + + auto u = IdeviceFFI::UsbmuxdConnection::default_new(0).expect("failed to connect to usbmuxd"); + + auto devices = u.get_devices().expect("failed to get devices from usbmuxd"); + + if (devices.empty()) { + std::cerr << "no devices connected"; + return 1; + } + + auto& dev = (devices)[0]; + + auto udid = dev.get_udid(); + if (udid.is_none()) { + std::cerr << "no udid\n"; + return 1; + } + + auto id = dev.get_id(); + if (id.is_none()) { + std::cerr << "no id\n"; + return 1; + } + + IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto prov = IdeviceFFI::Provider::usbmuxd_new( + std::move(addr), /*tag*/ 0, udid.unwrap(), id.unwrap(), "reeeeeeeee") + .expect("Failed to create usbmuxd provider"); + + auto client = IdeviceFFI::Lockdown::connect(prov).expect("lockdown connect failed"); + + auto pf = prov.get_pairing_file().expect("failed to get pairing file"); + client.start_session(pf).expect("failed to start session"); + + auto values = client.get_value(NULL, NULL); + match_result( + values, + ok_val, + { + PList::Dictionary res = PList::Dictionary(ok_val); + std::cout << res.ToXml(); + }, + e, + { + std::cerr << "get values failed: " << e.message << "\n"; + return 1; + }); +} diff --git a/cpp/examples/location_simulation.cpp b/cpp/examples/location_simulation.cpp new file mode 100644 index 0000000..b458faa --- /dev/null +++ b/cpp/examples/location_simulation.cpp @@ -0,0 +1,114 @@ +// Jackson Coxson + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +[[noreturn]] +static void die(const char* msg, const FfiError& e) { + std::cerr << msg << ": " << e.message << "\n"; + std::exit(1); +} + +int main(int argc, char** argv) { + // Usage: + // simulate_location clear + // simulate_location set + bool do_clear = false; + Option lat, lon; + + if (argc == 2 && std::string(argv[1]) == "clear") { + do_clear = true; + } else if (argc == 4 && std::string(argv[1]) == "set") { + lat = std::stod(argv[2]); + lon = std::stod(argv[3]); + } else { + std::cerr << "Usage:\n" + << " " << argv[0] << " clear\n" + << " " << argv[0] << " set \n"; + return 2; + } + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0).expect("failed to connect to usbmuxd"); + + auto devices = mux.get_devices().expect("failed to list devices"); + if (devices.empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (devices)[0]; + + auto udid = dev.get_udid(); + if (udid.is_none()) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (mux_id.is_none()) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = + Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label) + .expect("failed to create provider"); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(provider).unwrap_or_else( + [](FfiError e) -> CoreDeviceProxy { die("failed to connect CoreDeviceProxy", e); }); + + auto rsd_port = cdp.get_server_rsd_port().unwrap_or_else( + [](FfiError err) -> uint16_t { die("failed to get server RSD port", err); }); + + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = + std::move(cdp).create_tcp_adapter().expect("failed to create software tunnel adapter"); + + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter.connect(rsd_port).expect("failed to connect RSD stream"); + + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(stream)).expect("failed RSD handshake"); + + // 8) RemoteServer over RSD (borrows adapter + handshake) + auto rs = RemoteServer::connect_rsd(adapter, rsd).expect("failed to connect to RemoteServer"); + + // 9) LocationSimulation client (borrows RemoteServer) + auto sim = LocationSimulation::create(rs).expect("failed to create LocationSimulation client"); + + if (do_clear) { + sim.clear().expect("clear failed"); + std::cout << "Location cleared!\n"; + return 0; + } + + // set path + sim.set(lat.unwrap(), lon.unwrap()).expect("set failed"); + std::cout << "Location set to (" << lat.unwrap() << ", " << lon.unwrap() << ")\n"; + std::cout << "Press Ctrl-C to stop\n"; + + // keep process alive like the Rust example + for (;;) { + sim.set(lat.unwrap(), lon.unwrap()).expect("set failed"); + std::this_thread::sleep_for(std::chrono::seconds(3)); + } +} diff --git a/cpp/idevice++.xcodeproj/project.pbxproj b/cpp/idevice++.xcodeproj/project.pbxproj new file mode 100644 index 0000000..13e2dcf --- /dev/null +++ b/cpp/idevice++.xcodeproj/project.pbxproj @@ -0,0 +1,521 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 1914C7972E623CC2002EAB6E /* option.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 1914C7962E623CC2002EAB6E /* option.hpp */; }; + 1914C7992E623CC8002EAB6E /* result.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 1914C7982E623CC8002EAB6E /* result.hpp */; }; + 198077932E5CA6EF00CB501E /* adapter_stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776C2E5CA69800CB501E /* adapter_stream.cpp */; }; + 198077942E5CA6EF00CB501E /* app_service.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776D2E5CA69800CB501E /* app_service.cpp */; }; + 198077952E5CA6EF00CB501E /* core_device.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776E2E5CA69800CB501E /* core_device.cpp */; }; + 198077962E5CA6EF00CB501E /* debug_proxy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980776F2E5CA69800CB501E /* debug_proxy.cpp */; }; + 198077972E5CA6EF00CB501E /* diagnosticsservice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077702E5CA69800CB501E /* diagnosticsservice.cpp */; }; + 198077982E5CA6EF00CB501E /* ffi.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077712E5CA69800CB501E /* ffi.cpp */; }; + 198077992E5CA6EF00CB501E /* idevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077722E5CA69800CB501E /* idevice.cpp */; }; + 1980779A2E5CA6EF00CB501E /* location_simulation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077732E5CA69800CB501E /* location_simulation.cpp */; }; + 1980779B2E5CA6EF00CB501E /* lockdown.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077742E5CA69800CB501E /* lockdown.cpp */; }; + 1980779C2E5CA6EF00CB501E /* pairing_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077752E5CA69800CB501E /* pairing_file.cpp */; }; + 1980779D2E5CA6EF00CB501E /* provider.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077762E5CA69800CB501E /* provider.cpp */; }; + 1980779E2E5CA6EF00CB501E /* remote_server.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077772E5CA69800CB501E /* remote_server.cpp */; }; + 1980779F2E5CA6EF00CB501E /* rsd.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077782E5CA69800CB501E /* rsd.cpp */; }; + 198077A02E5CA6EF00CB501E /* tcp_callback_feeder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 198077792E5CA69800CB501E /* tcp_callback_feeder.cpp */; }; + 198077A12E5CA6EF00CB501E /* usbmuxd.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1980777A2E5CA69800CB501E /* usbmuxd.cpp */; }; + 198077B72E5CA6FC00CB501E /* core_device_proxy.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A62E5CA6FC00CB501E /* core_device_proxy.hpp */; }; + 198077B82E5CA6FC00CB501E /* pairing_file.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AD2E5CA6FC00CB501E /* pairing_file.hpp */; }; + 198077B92E5CA6FC00CB501E /* bindings.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A52E5CA6FC00CB501E /* bindings.hpp */; }; + 198077BA2E5CA6FC00CB501E /* diagnosticsservice.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A82E5CA6FC00CB501E /* diagnosticsservice.hpp */; }; + 198077BB2E5CA6FC00CB501E /* idevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 198077B52E5CA6FC00CB501E /* idevice.h */; }; + 198077BC2E5CA6FC00CB501E /* debug_proxy.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A72E5CA6FC00CB501E /* debug_proxy.hpp */; }; + 198077BD2E5CA6FC00CB501E /* ffi.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A92E5CA6FC00CB501E /* ffi.hpp */; }; + 198077BE2E5CA6FC00CB501E /* rsd.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B12E5CA6FC00CB501E /* rsd.hpp */; }; + 198077BF2E5CA6FC00CB501E /* tcp_object_stack.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */; }; + 198077C02E5CA6FC00CB501E /* remote_server.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B02E5CA6FC00CB501E /* remote_server.hpp */; }; + 198077C12E5CA6FC00CB501E /* readwrite.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AF2E5CA6FC00CB501E /* readwrite.hpp */; }; + 198077C22E5CA6FC00CB501E /* location_simulation.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AB2E5CA6FC00CB501E /* location_simulation.hpp */; }; + 198077C32E5CA6FC00CB501E /* adapter_stream.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A32E5CA6FC00CB501E /* adapter_stream.hpp */; }; + 198077C42E5CA6FC00CB501E /* lockdown.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AC2E5CA6FC00CB501E /* lockdown.hpp */; }; + 198077C52E5CA6FC00CB501E /* usbmuxd.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */; }; + 198077C62E5CA6FC00CB501E /* app_service.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077A42E5CA6FC00CB501E /* app_service.hpp */; }; + 198077C72E5CA6FC00CB501E /* idevice.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AA2E5CA6FC00CB501E /* idevice.hpp */; }; + 198077C82E5CA6FC00CB501E /* provider.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 198077AE2E5CA6FC00CB501E /* provider.hpp */; }; + 198077DF2E5CCA2900CB501E /* libidevice_ffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 198077DB2E5CC33000CB501E /* libidevice_ffi.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1914C7962E623CC2002EAB6E /* option.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = option.hpp; sourceTree = ""; }; + 1914C7982E623CC8002EAB6E /* result.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = result.hpp; sourceTree = ""; }; + 1980776C2E5CA69800CB501E /* adapter_stream.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = adapter_stream.cpp; sourceTree = ""; }; + 1980776D2E5CA69800CB501E /* app_service.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = app_service.cpp; sourceTree = ""; }; + 1980776E2E5CA69800CB501E /* core_device.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = core_device.cpp; sourceTree = ""; }; + 1980776F2E5CA69800CB501E /* debug_proxy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = debug_proxy.cpp; sourceTree = ""; }; + 198077702E5CA69800CB501E /* diagnosticsservice.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = diagnosticsservice.cpp; sourceTree = ""; }; + 198077712E5CA69800CB501E /* ffi.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ffi.cpp; sourceTree = ""; }; + 198077722E5CA69800CB501E /* idevice.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = idevice.cpp; sourceTree = ""; }; + 198077732E5CA69800CB501E /* location_simulation.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = location_simulation.cpp; sourceTree = ""; }; + 198077742E5CA69800CB501E /* lockdown.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = lockdown.cpp; sourceTree = ""; }; + 198077752E5CA69800CB501E /* pairing_file.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = pairing_file.cpp; sourceTree = ""; }; + 198077762E5CA69800CB501E /* provider.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = provider.cpp; sourceTree = ""; }; + 198077772E5CA69800CB501E /* remote_server.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = remote_server.cpp; sourceTree = ""; }; + 198077782E5CA69800CB501E /* rsd.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = rsd.cpp; sourceTree = ""; }; + 198077792E5CA69800CB501E /* tcp_callback_feeder.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tcp_callback_feeder.cpp; sourceTree = ""; }; + 1980777A2E5CA69800CB501E /* usbmuxd.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = usbmuxd.cpp; sourceTree = ""; }; + 1980778F2E5CA6C700CB501E /* libidevice++.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libidevice++.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 198077A32E5CA6FC00CB501E /* adapter_stream.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = adapter_stream.hpp; sourceTree = ""; }; + 198077A42E5CA6FC00CB501E /* app_service.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = app_service.hpp; sourceTree = ""; }; + 198077A52E5CA6FC00CB501E /* bindings.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = bindings.hpp; sourceTree = ""; }; + 198077A62E5CA6FC00CB501E /* core_device_proxy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = core_device_proxy.hpp; sourceTree = ""; }; + 198077A72E5CA6FC00CB501E /* debug_proxy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = debug_proxy.hpp; sourceTree = ""; }; + 198077A82E5CA6FC00CB501E /* diagnosticsservice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = diagnosticsservice.hpp; sourceTree = ""; }; + 198077A92E5CA6FC00CB501E /* ffi.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ffi.hpp; sourceTree = ""; }; + 198077AA2E5CA6FC00CB501E /* idevice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = idevice.hpp; sourceTree = ""; }; + 198077AB2E5CA6FC00CB501E /* location_simulation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = location_simulation.hpp; sourceTree = ""; }; + 198077AC2E5CA6FC00CB501E /* lockdown.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = lockdown.hpp; sourceTree = ""; }; + 198077AD2E5CA6FC00CB501E /* pairing_file.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = pairing_file.hpp; sourceTree = ""; }; + 198077AE2E5CA6FC00CB501E /* provider.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = provider.hpp; sourceTree = ""; }; + 198077AF2E5CA6FC00CB501E /* readwrite.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = readwrite.hpp; sourceTree = ""; }; + 198077B02E5CA6FC00CB501E /* remote_server.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = remote_server.hpp; sourceTree = ""; }; + 198077B12E5CA6FC00CB501E /* rsd.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = rsd.hpp; sourceTree = ""; }; + 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tcp_object_stack.hpp; sourceTree = ""; }; + 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = usbmuxd.hpp; sourceTree = ""; }; + 198077B52E5CA6FC00CB501E /* idevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = idevice.h; sourceTree = ""; }; + 198077DB2E5CC33000CB501E /* libidevice_ffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libidevice_ffi.a; path = "${DESTINATION_PATH}/libidevice_ffi.a"; sourceTree = ""; }; + 198077E02E5CD49200CB501E /* xcode_build_rust.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = xcode_build_rust.sh; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1980778D2E5CA6C700CB501E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 198077DF2E5CCA2900CB501E /* libidevice_ffi.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 198077562E5CA62F00CB501E = { + isa = PBXGroup; + children = ( + 1980777B2E5CA69800CB501E /* src */, + 198077612E5CA62F00CB501E /* Products */, + 198077B62E5CA6FC00CB501E /* include */, + 198077D92E5CC31100CB501E /* Frameworks */, + 198077E02E5CD49200CB501E /* xcode_build_rust.sh */, + ); + sourceTree = ""; + }; + 198077612E5CA62F00CB501E /* Products */ = { + isa = PBXGroup; + children = ( + 1980778F2E5CA6C700CB501E /* libidevice++.a */, + ); + name = Products; + sourceTree = ""; + }; + 1980777B2E5CA69800CB501E /* src */ = { + isa = PBXGroup; + children = ( + 1980776C2E5CA69800CB501E /* adapter_stream.cpp */, + 1980776D2E5CA69800CB501E /* app_service.cpp */, + 1980776E2E5CA69800CB501E /* core_device.cpp */, + 1980776F2E5CA69800CB501E /* debug_proxy.cpp */, + 198077702E5CA69800CB501E /* diagnosticsservice.cpp */, + 198077712E5CA69800CB501E /* ffi.cpp */, + 198077722E5CA69800CB501E /* idevice.cpp */, + 198077732E5CA69800CB501E /* location_simulation.cpp */, + 198077742E5CA69800CB501E /* lockdown.cpp */, + 198077752E5CA69800CB501E /* pairing_file.cpp */, + 198077762E5CA69800CB501E /* provider.cpp */, + 198077772E5CA69800CB501E /* remote_server.cpp */, + 198077782E5CA69800CB501E /* rsd.cpp */, + 198077792E5CA69800CB501E /* tcp_callback_feeder.cpp */, + 1980777A2E5CA69800CB501E /* usbmuxd.cpp */, + ); + path = src; + sourceTree = ""; + }; + 198077B42E5CA6FC00CB501E /* idevice++ */ = { + isa = PBXGroup; + children = ( + 198077A32E5CA6FC00CB501E /* adapter_stream.hpp */, + 198077A42E5CA6FC00CB501E /* app_service.hpp */, + 198077A52E5CA6FC00CB501E /* bindings.hpp */, + 198077A62E5CA6FC00CB501E /* core_device_proxy.hpp */, + 198077A72E5CA6FC00CB501E /* debug_proxy.hpp */, + 198077A82E5CA6FC00CB501E /* diagnosticsservice.hpp */, + 198077A92E5CA6FC00CB501E /* ffi.hpp */, + 198077AA2E5CA6FC00CB501E /* idevice.hpp */, + 198077AB2E5CA6FC00CB501E /* location_simulation.hpp */, + 198077AC2E5CA6FC00CB501E /* lockdown.hpp */, + 198077AD2E5CA6FC00CB501E /* pairing_file.hpp */, + 198077AE2E5CA6FC00CB501E /* provider.hpp */, + 198077AF2E5CA6FC00CB501E /* readwrite.hpp */, + 198077B02E5CA6FC00CB501E /* remote_server.hpp */, + 198077B12E5CA6FC00CB501E /* rsd.hpp */, + 198077B22E5CA6FC00CB501E /* tcp_object_stack.hpp */, + 198077B32E5CA6FC00CB501E /* usbmuxd.hpp */, + 1914C7962E623CC2002EAB6E /* option.hpp */, + 1914C7982E623CC8002EAB6E /* result.hpp */, + ); + path = "idevice++"; + sourceTree = ""; + }; + 198077B62E5CA6FC00CB501E /* include */ = { + isa = PBXGroup; + children = ( + 198077B42E5CA6FC00CB501E /* idevice++ */, + 198077B52E5CA6FC00CB501E /* idevice.h */, + ); + path = include; + sourceTree = ""; + }; + 198077D92E5CC31100CB501E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 198077DB2E5CC33000CB501E /* libidevice_ffi.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 1980778B2E5CA6C700CB501E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 198077B72E5CA6FC00CB501E /* core_device_proxy.hpp in Headers */, + 198077B82E5CA6FC00CB501E /* pairing_file.hpp in Headers */, + 198077B92E5CA6FC00CB501E /* bindings.hpp in Headers */, + 198077BA2E5CA6FC00CB501E /* diagnosticsservice.hpp in Headers */, + 198077BB2E5CA6FC00CB501E /* idevice.h in Headers */, + 198077BC2E5CA6FC00CB501E /* debug_proxy.hpp in Headers */, + 198077BD2E5CA6FC00CB501E /* ffi.hpp in Headers */, + 198077BE2E5CA6FC00CB501E /* rsd.hpp in Headers */, + 198077BF2E5CA6FC00CB501E /* tcp_object_stack.hpp in Headers */, + 198077C02E5CA6FC00CB501E /* remote_server.hpp in Headers */, + 198077C12E5CA6FC00CB501E /* readwrite.hpp in Headers */, + 198077C22E5CA6FC00CB501E /* location_simulation.hpp in Headers */, + 198077C32E5CA6FC00CB501E /* adapter_stream.hpp in Headers */, + 1914C7972E623CC2002EAB6E /* option.hpp in Headers */, + 198077C42E5CA6FC00CB501E /* lockdown.hpp in Headers */, + 198077C52E5CA6FC00CB501E /* usbmuxd.hpp in Headers */, + 198077C62E5CA6FC00CB501E /* app_service.hpp in Headers */, + 198077C72E5CA6FC00CB501E /* idevice.hpp in Headers */, + 198077C82E5CA6FC00CB501E /* provider.hpp in Headers */, + 1914C7992E623CC8002EAB6E /* result.hpp in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 1980778E2E5CA6C700CB501E /* idevice++ */ = { + isa = PBXNativeTarget; + buildConfigurationList = 198077902E5CA6C700CB501E /* Build configuration list for PBXNativeTarget "idevice++" */; + buildPhases = ( + 198077E22E5CD4C900CB501E /* ShellScript */, + 1980778B2E5CA6C700CB501E /* Headers */, + 1980778C2E5CA6C700CB501E /* Sources */, + 1980778D2E5CA6C700CB501E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "idevice++"; + packageProductDependencies = ( + ); + productName = "idevice++"; + productReference = 1980778F2E5CA6C700CB501E /* libidevice++.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 198077572E5CA62F00CB501E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + 1980778E2E5CA6C700CB501E = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = 1980775A2E5CA62F00CB501E /* Build configuration list for PBXProject "idevice++" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 198077562E5CA62F00CB501E; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 198077612E5CA62F00CB501E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1980778E2E5CA6C700CB501E /* idevice++ */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 198077E22E5CD4C900CB501E /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + "", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\n./xcode_build_rust.sh\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1980778C2E5CA6C700CB501E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 198077932E5CA6EF00CB501E /* adapter_stream.cpp in Sources */, + 198077942E5CA6EF00CB501E /* app_service.cpp in Sources */, + 198077952E5CA6EF00CB501E /* core_device.cpp in Sources */, + 198077962E5CA6EF00CB501E /* debug_proxy.cpp in Sources */, + 198077972E5CA6EF00CB501E /* diagnosticsservice.cpp in Sources */, + 198077982E5CA6EF00CB501E /* ffi.cpp in Sources */, + 198077992E5CA6EF00CB501E /* idevice.cpp in Sources */, + 1980779A2E5CA6EF00CB501E /* location_simulation.cpp in Sources */, + 1980779B2E5CA6EF00CB501E /* lockdown.cpp in Sources */, + 1980779C2E5CA6EF00CB501E /* pairing_file.cpp in Sources */, + 1980779D2E5CA6EF00CB501E /* provider.cpp in Sources */, + 1980779E2E5CA6EF00CB501E /* remote_server.cpp in Sources */, + 1980779F2E5CA6EF00CB501E /* rsd.cpp in Sources */, + 198077A02E5CA6EF00CB501E /* tcp_callback_feeder.cpp in Sources */, + 198077A12E5CA6EF00CB501E /* usbmuxd.cpp in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 198077672E5CA63000CB501E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 4FW3Q8784L; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 198077682E5CA63000CB501E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 4FW3Q8784L; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 198077912E5CA6C700CB501E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 4FW3Q8784L; + EXECUTABLE_PREFIX = lib; + GCC_WARN_INHIBIT_ALL_WARNINGS = NO; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; + LIBRARY_SEARCH_PATHS = ( + "${TARGET_BUILD_DIR}/**", + "$(PROJECT_DIR)/${DESTINATION_PATH}", + ); + MACOSX_DEPLOYMENT_TARGET = 15.5; + OTHER_LDFLAGS = "-Wall"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + }; + name = Debug; + }; + 198077922E5CA6C700CB501E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 4FW3Q8784L; + EXECUTABLE_PREFIX = lib; + GCC_WARN_INHIBIT_ALL_WARNINGS = NO; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; + LIBRARY_SEARCH_PATHS = ( + "${TARGET_BUILD_DIR}/**", + "$(PROJECT_DIR)/${DESTINATION_PATH}", + ); + MACOSX_DEPLOYMENT_TARGET = 15.5; + OTHER_LDFLAGS = "-Wall"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1980775A2E5CA62F00CB501E /* Build configuration list for PBXProject "idevice++" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 198077672E5CA63000CB501E /* Debug */, + 198077682E5CA63000CB501E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 198077902E5CA6C700CB501E /* Build configuration list for PBXNativeTarget "idevice++" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 198077912E5CA6C700CB501E /* Debug */, + 198077922E5CA6C700CB501E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 198077572E5CA62F00CB501E /* Project object */; +} diff --git a/cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/cpp/idevice++.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme b/cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme new file mode 100644 index 0000000..779227d --- /dev/null +++ b/cpp/idevice++.xcodeproj/xcshareddata/xcschemes/idevice++.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cpp/include/idevice++/adapter_stream.hpp b/cpp/include/idevice++/adapter_stream.hpp new file mode 100644 index 0000000..f92a5b1 --- /dev/null +++ b/cpp/include/idevice++/adapter_stream.hpp @@ -0,0 +1,51 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include +#include + +struct IdeviceFfiError; +struct AdapterStreamHandle; + +namespace IdeviceFFI { + +// Non-owning view over a stream (must call close(); no implicit free provided) +class AdapterStream { + public: + explicit AdapterStream(AdapterStreamHandle* h) noexcept : h_(h) {} + + AdapterStream(const AdapterStream&) = delete; + AdapterStream& operator=(const AdapterStream&) = delete; + + AdapterStream(AdapterStream&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + AdapterStream& operator=(AdapterStream&& other) noexcept { + if (this != &other) { + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + ~AdapterStream() noexcept = default; // no auto-close; caller controls + + AdapterStreamHandle* raw() const noexcept { return h_; } + + Result close(); + Result send(const uint8_t* data, size_t len); + Result send(const std::vector& buf) { + return send(buf.data(), buf.size()); + } + + // recv into caller-provided buffer (resizes to actual length) + Result, FfiError> recv(size_t max_hint = 2048); + + private: + AdapterStreamHandle* h_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/app_service.hpp b/cpp/include/idevice++/app_service.hpp new file mode 100644 index 0000000..0adfb9c --- /dev/null +++ b/cpp/include/idevice++/app_service.hpp @@ -0,0 +1,108 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using AppServicePtr = + std::unique_ptr>; + +struct AppInfo { + bool is_removable{}; + std::string name; + bool is_first_party{}; + std::string path; + std::string bundle_identifier; + bool is_developer_app{}; + Option bundle_version; + bool is_internal{}; + bool is_hidden{}; + bool is_app_clip{}; + Option version; +}; + +struct LaunchResponse { + uint32_t process_identifier_version{}; + uint32_t pid{}; + std::string executable_url; + std::vector audit_token; // raw words +}; + +struct ProcessToken { + uint32_t pid{}; + Option executable_url; +}; + +struct SignalResponse { + uint32_t pid{}; + Option executable_url; + uint64_t device_timestamp_ms{}; + uint32_t signal{}; +}; + +struct IconData { + std::vector data; + double icon_width{}; + double icon_height{}; + double minimum_width{}; + double minimum_height{}; +}; + +class AppService { + public: + // Factory: connect via RSD (borrows adapter & handshake) + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); + + // Factory: from socket Box (consumes it). + static Result from_readwrite_ptr(ReadWriteOpaque* consumed); + + // nice ergonomic overload: consume a C++ ReadWrite by releasing it + static Result from_readwrite(ReadWrite&& rw); + + // API + Result, FfiError> + list_apps(bool app_clips, bool removable, bool hidden, bool internal, bool default_apps) const; + + Result launch(const std::string& bundle_id, + const std::vector& argv, + bool kill_existing, + bool start_suspended); + + Result, FfiError> list_processes() const; + + Result uninstall(const std::string& bundle_id); + + Result send_signal(uint32_t pid, uint32_t signal); + + Result fetch_icon(const std::string& bundle_id, + float width, + float height, + float scale, + bool allow_placeholder); + + // RAII / moves + ~AppService() noexcept = default; + AppService(AppService&&) noexcept = default; + AppService& operator=(AppService&&) noexcept = default; + AppService(const AppService&) = delete; + AppService& operator=(const AppService&) = delete; + + AppServiceHandle* raw() const noexcept { return handle_.get(); } + static AppService adopt(AppServiceHandle* h) noexcept { return AppService(h); } + + private: + explicit AppService(AppServiceHandle* h) noexcept : handle_(h) {} + AppServicePtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/bindings.hpp b/cpp/include/idevice++/bindings.hpp new file mode 100644 index 0000000..3646e86 --- /dev/null +++ b/cpp/include/idevice++/bindings.hpp @@ -0,0 +1,8 @@ +// Jackson Coxson + +#ifndef IDEVICE_BINDINGS_H +#define IDEVICE_BINDINGS_H +extern "C" { +#include // this file is generated by bindgen +} +#endif diff --git a/cpp/include/idevice++/core_device_proxy.hpp b/cpp/include/idevice++/core_device_proxy.hpp new file mode 100644 index 0000000..730fca4 --- /dev/null +++ b/cpp/include/idevice++/core_device_proxy.hpp @@ -0,0 +1,104 @@ +// Jackson Coxson + +#ifndef IDEVICE_CORE_DEVICE_PROXY_H +#define IDEVICE_CORE_DEVICE_PROXY_H + +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using CoreProxyPtr = std::unique_ptr>; +using AdapterPtr = std::unique_ptr>; + +struct CoreClientParams { + uint16_t mtu{}; + std::string address; // freed from Rust side after copy + std::string netmask; // freed from Rust side after copy +}; + +class Adapter { + public: + ~Adapter() noexcept = default; + Adapter(Adapter&&) noexcept = default; + Adapter& operator=(Adapter&&) noexcept = default; + Adapter(const Adapter&) = delete; + Adapter& operator=(const Adapter&) = delete; + + static Adapter adopt(AdapterHandle* h) noexcept { return Adapter(h); } + AdapterHandle* raw() const noexcept { return handle_.get(); } + + // Enable PCAP + Result pcap(const std::string& path) { + FfiError e(::adapter_pcap(handle_.get(), path.c_str())); + if (e) { + return Err(e); + } + return Ok(); + } + + // Connect to a port, returns a ReadWrite stream (to be consumed by + // RSD/CoreDeviceProxy) + Result connect(uint16_t port) { + ReadWriteOpaque* s = nullptr; + FfiError e(::adapter_connect(handle_.get(), port, &s)); + if (e) { + return Err(e); + } + return Ok(ReadWrite::adopt(s)); + } + + private: + explicit Adapter(AdapterHandle* h) noexcept : handle_(h) {} + AdapterPtr handle_{}; +}; + +class CoreDeviceProxy { + public: + // Factory: connect using a Provider (NOT consumed on success or error) + static Result connect(Provider& provider); + + // Factory: from a socket; Rust consumes the socket regardless of result → we + // release before call + static Result from_socket(Idevice&& socket); + + // Send/recv + Result send(const uint8_t* data, size_t len); + Result send(const std::vector& buf) { + return send(buf.data(), buf.size()); + } + + // recv into a pre-sized buffer; resizes to actual bytes received + Result recv(std::vector& out); + + // Handshake info + Result get_client_parameters() const; + Result get_server_address() const; + Result get_server_rsd_port() const; + + // Consuming creation of a TCP adapter: Rust consumes the proxy handle + Result create_tcp_adapter() &&; + + // RAII / moves + ~CoreDeviceProxy() noexcept = default; + CoreDeviceProxy(CoreDeviceProxy&&) noexcept = default; + CoreDeviceProxy& operator=(CoreDeviceProxy&&) noexcept = default; + CoreDeviceProxy(const CoreDeviceProxy&) = delete; + CoreDeviceProxy& operator=(const CoreDeviceProxy&) = delete; + + CoreDeviceProxyHandle* raw() const noexcept { return handle_.get(); } + CoreDeviceProxyHandle* release() noexcept { return handle_.release(); } + static CoreDeviceProxy adopt(CoreDeviceProxyHandle* h) noexcept { return CoreDeviceProxy(h); } + + private: + explicit CoreDeviceProxy(CoreDeviceProxyHandle* h) noexcept : handle_(h) {} + CoreProxyPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/debug_proxy.hpp b/cpp/include/idevice++/debug_proxy.hpp new file mode 100644 index 0000000..1e8b832 --- /dev/null +++ b/cpp/include/idevice++/debug_proxy.hpp @@ -0,0 +1,120 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include + +// Bring in the global C ABI (all C structs/functions are global) +#include +#include +#include +#include + +namespace IdeviceFFI { + +class DebugProxy { + public: + DebugProxy() = default; + DebugProxy(const DebugProxy&) = delete; + DebugProxy& operator=(const DebugProxy&) = delete; + + DebugProxy(DebugProxy&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + DebugProxy& operator=(DebugProxy&& other) noexcept { + if (this != &other) { + reset(); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + ~DebugProxy() { reset(); } + + // Factory: connect over RSD (borrows adapter & handshake; does not consume + // them) + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); + + // Factory: consume a ReadWrite stream (fat pointer) + static Result from_readwrite_ptr(::ReadWriteOpaque* consumed); + + // Convenience: consume a C++ ReadWrite wrapper by releasing it into the ABI + static Result from_readwrite(ReadWrite&& rw); + + // API + Result, FfiError> send_command(const std::string& name, + const std::vector& argv); + + Result, FfiError> read_response(); + + Result send_raw(const std::vector& data); + + // Reads up to `len` bytes; ABI returns a heap C string (we treat as bytes → + // string) + Result, FfiError> read(std::size_t len); + + // Sets argv, returns textual reply (OK/echo/etc) + Result, FfiError> set_argv(const std::vector& argv); + + Result send_ack(); + Result send_nack(); + + // No error object in ABI; immediate effect + void set_ack_mode(bool enabled) { ::debug_proxy_set_ack_mode(handle_, enabled ? 1 : 0); } + + ::DebugProxyHandle* raw() const { return handle_; } + + private: + explicit DebugProxy(::DebugProxyHandle* h) : handle_(h) {} + + void reset() { + if (handle_) { + ::debug_proxy_free(handle_); + handle_ = nullptr; + } + } + + ::DebugProxyHandle* handle_ = nullptr; +}; + +// Small helper that owns a DebugserverCommandHandle +class DebugCommand { + public: + DebugCommand() = default; + DebugCommand(const DebugCommand&) = delete; + DebugCommand& operator=(const DebugCommand&) = delete; + + DebugCommand(DebugCommand&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + DebugCommand& operator=(DebugCommand&& other) noexcept { + if (this != &other) { + reset(); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + ~DebugCommand() { reset(); } + + static Option make(const std::string& name, const std::vector& argv); + + ::DebugserverCommandHandle* raw() const { return handle_; } + + private: + explicit DebugCommand(::DebugserverCommandHandle* h) : handle_(h) {} + + void reset() { + if (handle_) { + ::debugserver_command_free(handle_); + handle_ = nullptr; + } + } + + ::DebugserverCommandHandle* handle_ = nullptr; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/diagnosticsservice.hpp b/cpp/include/idevice++/diagnosticsservice.hpp new file mode 100644 index 0000000..b0c81a0 --- /dev/null +++ b/cpp/include/idevice++/diagnosticsservice.hpp @@ -0,0 +1,109 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace IdeviceFFI { + +class SysdiagnoseStream { + public: + SysdiagnoseStream() = default; + SysdiagnoseStream(const SysdiagnoseStream&) = delete; + SysdiagnoseStream& operator=(const SysdiagnoseStream&) = delete; + + SysdiagnoseStream(SysdiagnoseStream&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + SysdiagnoseStream& operator=(SysdiagnoseStream&& other) noexcept { + if (this != &other) { + reset(); + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + ~SysdiagnoseStream() { reset(); } + + // Pull next chunk. Returns nullopt on end-of-stream. On error, returns + // nullopt and sets `err`. + Result>, FfiError> next_chunk(); + + SysdiagnoseStreamHandle* raw() const { return h_; } + + private: + friend class DiagnosticsService; + explicit SysdiagnoseStream(::SysdiagnoseStreamHandle* h) : h_(h) {} + + void reset() { + if (h_) { + ::sysdiagnose_stream_free(h_); + h_ = nullptr; + } + } + + ::SysdiagnoseStreamHandle* h_ = nullptr; +}; + +// The result of starting a sysdiagnose capture. +struct SysdiagnoseCapture { + std::string preferred_filename; + std::size_t expected_length = 0; + SysdiagnoseStream stream; +}; + +// RAII for Diagnostics service client +class DiagnosticsService { + public: + DiagnosticsService() = default; + DiagnosticsService(const DiagnosticsService&) = delete; + DiagnosticsService& operator=(const DiagnosticsService&) = delete; + + DiagnosticsService(DiagnosticsService&& other) noexcept : h_(other.h_) { other.h_ = nullptr; } + DiagnosticsService& operator=(DiagnosticsService&& other) noexcept { + if (this != &other) { + reset(); + h_ = other.h_; + other.h_ = nullptr; + } + return *this; + } + + ~DiagnosticsService() { reset(); } + + // Connect via RSD (borrows adapter & handshake; does not consume them) + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); + + // Create from a ReadWrite stream (consumes it) + static Result from_stream_ptr(::ReadWriteOpaque* consumed); + + static Result from_stream(ReadWrite&& rw) { + return from_stream_ptr(rw.release()); + } + + // Start sysdiagnose capture; on success returns filename, length and a byte + // stream + Result capture_sysdiagnose(bool dry_run); + + ::DiagnosticsServiceHandle* raw() const { return h_; } + + private: + explicit DiagnosticsService(::DiagnosticsServiceHandle* h) : h_(h) {} + + void reset() { + if (h_) { + ::diagnostics_service_free(h_); + h_ = nullptr; + } + } + + ::DiagnosticsServiceHandle* h_ = nullptr; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/ffi.hpp b/cpp/include/idevice++/ffi.hpp new file mode 100644 index 0000000..73cdf06 --- /dev/null +++ b/cpp/include/idevice++/ffi.hpp @@ -0,0 +1,24 @@ +// Jackson Coxson + +#ifndef IDEVICE_FFI +#define IDEVICE_FFI + +#include +#include + +namespace IdeviceFFI { +class FfiError { + public: + int32_t code = 0; + std::string message; + + FfiError(const IdeviceFfiError* err); + FfiError(); + + explicit operator bool() const { return code != 0; } + + static FfiError NotConnected(); + static FfiError InvalidArgument(); +}; +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/idevice.hpp b/cpp/include/idevice++/idevice.hpp new file mode 100644 index 0000000..2fe93ce --- /dev/null +++ b/cpp/include/idevice++/idevice.hpp @@ -0,0 +1,63 @@ +// Jackson Coxson + +#ifndef IDEVICE_CPP +#define IDEVICE_CPP + +#include +#include +#include +#include + +#if defined(_WIN32) && !defined(__MINGW32__) +// MSVC doesn't have BSD u_int* types +using u_int8_t = std::uint8_t; +using u_int16_t = std::uint16_t; +using u_int32_t = std::uint32_t; +using u_int64_t = std::uint64_t; +#endif + +namespace IdeviceFFI { + +// Generic “bind a free function” deleter +template struct FnDeleter { + void operator()(T* p) const noexcept { + if (p) { + FreeFn(p); + } + } +}; + +using IdevicePtr = std::unique_ptr>; + +class Idevice { + public: + static Result create(IdeviceSocketHandle* socket, const std::string& label); + + static Result + create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label); + + // Methods + Result get_type() const; + Result rsd_checkin(); + Result start_session(const PairingFile& pairing_file); + + // Ownership/RAII + ~Idevice() noexcept = default; + Idevice(Idevice&&) noexcept = default; + Idevice& operator=(Idevice&&) noexcept = default; + Idevice(const Idevice&) = delete; + Idevice& operator=(const Idevice&) = delete; + + static Idevice adopt(IdeviceHandle* h) noexcept { return Idevice(h); } + + // Accessor + IdeviceHandle* raw() const noexcept { return handle_.get(); } + IdeviceHandle* release() noexcept { return handle_.release(); } + + private: + explicit Idevice(IdeviceHandle* h) noexcept : handle_(h) {} + IdevicePtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/location_simulation.hpp b/cpp/include/idevice++/location_simulation.hpp new file mode 100644 index 0000000..be4dd85 --- /dev/null +++ b/cpp/include/idevice++/location_simulation.hpp @@ -0,0 +1,38 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include + +namespace IdeviceFFI { + +using LocSimPtr = std::unique_ptr>; + +class LocationSimulation { + public: + // Factory: borrows the RemoteServer; not consumed + static Result create(RemoteServer& server); + + Result clear(); + Result set(double latitude, double longitude); + + ~LocationSimulation() noexcept = default; + LocationSimulation(LocationSimulation&&) noexcept = default; + LocationSimulation& operator=(LocationSimulation&&) noexcept = default; + LocationSimulation(const LocationSimulation&) = delete; + LocationSimulation& operator=(const LocationSimulation&) = delete; + + LocationSimulationHandle* raw() const noexcept { return handle_.get(); } + static LocationSimulation adopt(LocationSimulationHandle* h) noexcept { + return LocationSimulation(h); + } + + private: + explicit LocationSimulation(LocationSimulationHandle* h) noexcept : handle_(h) {} + LocSimPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/lockdown.hpp b/cpp/include/idevice++/lockdown.hpp new file mode 100644 index 0000000..14dfdff --- /dev/null +++ b/cpp/include/idevice++/lockdown.hpp @@ -0,0 +1,43 @@ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using LockdownPtr = + std::unique_ptr>; + +class Lockdown { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result start_session(const PairingFile& pf); + Result, FfiError> start_service(const std::string& identifier); + Result get_value(const char* key, const char* domain); + + // RAII / moves + ~Lockdown() noexcept = default; + Lockdown(Lockdown&&) noexcept = default; + Lockdown& operator=(Lockdown&&) noexcept = default; + Lockdown(const Lockdown&) = delete; + Lockdown& operator=(const Lockdown&) = delete; + + LockdowndClientHandle* raw() const noexcept { return handle_.get(); } + static Lockdown adopt(LockdowndClientHandle* h) noexcept { return Lockdown(h); } + + private: + explicit Lockdown(LockdowndClientHandle* h) noexcept : handle_(h) {} + LockdownPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/option.hpp b/cpp/include/idevice++/option.hpp new file mode 100644 index 0000000..ccc9f09 --- /dev/null +++ b/cpp/include/idevice++/option.hpp @@ -0,0 +1,184 @@ +// So here's the thing, std::optional and friends weren't added until C++17. +// Some consumers of this codebase aren't on C++17 yet, so this won't work. +// Plus, as a professional Rust evangelist, it's my duty to place as many Rust +// idioms into other languages as possible to give everyone a taste of greatness. +// Required error handling is correct error handling. And they called me a mad man. + +// Heavily influced from https://github.com/oktal/result, thank you + +#pragma once + +#include +#include +#include + +namespace IdeviceFFI { + +struct none_t {}; +constexpr none_t None{}; + +template class Option { + bool has_; + typename std::aligned_storage::type storage_; + + T* ptr() { return reinterpret_cast(&storage_); } + const T* ptr() const { return reinterpret_cast(&storage_); } + + public: + // None + Option() noexcept : has_(false) {} + Option(none_t) noexcept : has_(false) {} + + // Some + Option(const T& v) : has_(true) { ::new (ptr()) T(v); } + Option(T&& v) : has_(true) { ::new (ptr()) T(std::move(v)); } + + // Copy / move + Option(const Option& o) : has_(o.has_) { + if (has_) { + ::new (ptr()) T(*o.ptr()); + } + } + Option(Option&& o) noexcept(std::is_nothrow_move_constructible::value) : has_(o.has_) { + if (has_) { + ::new (ptr()) T(std::move(*o.ptr())); + o.reset(); + } + } + + Option& operator=(Option o) noexcept(std::is_nothrow_move_constructible::value + && std::is_nothrow_move_assignable::value) { + swap(o); + return *this; + } + + ~Option() { reset(); } + + void reset() noexcept { + if (has_) { + ptr()->~T(); + has_ = false; + } + } + + void swap(Option& other) noexcept(std::is_nothrow_move_constructible::value) { + if (has_ && other.has_) { + using std::swap; + swap(*ptr(), *other.ptr()); + } else if (has_ && !other.has_) { + ::new (other.ptr()) T(std::move(*ptr())); + other.has_ = true; + reset(); + } else if (!has_ && other.has_) { + ::new (ptr()) T(std::move(*other.ptr())); + has_ = true; + other.reset(); + } + } + + // State + bool is_some() const noexcept { return has_; } + bool is_none() const noexcept { return !has_; } + + // Unwraps (ref-qualified) + T& unwrap() & { + if (!has_) { + throw std::runtime_error("unwrap on None"); + } + return *ptr(); + } + const T& unwrap() const& { + if (!has_) { + throw std::runtime_error("unwrap on None"); + } + return *ptr(); + } + T unwrap() && { + if (!has_) { + throw std::runtime_error("unwrap on None"); + } + T tmp = std::move(*ptr()); + reset(); + return tmp; + } + + // unwrap_or / unwrap_or_else + T unwrap_or(T default_value) const& { return has_ ? *ptr() : std::move(default_value); } + T unwrap_or(T default_value) && { return has_ ? std::move(*ptr()) : std::move(default_value); } + T unwrap_or(const T& default_value) const& { return has_ ? *ptr() : default_value; } + T unwrap_or(T&& default_value) const& { return has_ ? *ptr() : std::move(default_value); } + + template T unwrap_or_else(F&& f) const& { + return has_ ? *ptr() : static_cast(f()); + } + template T unwrap_or_else(F&& f) && { + return has_ ? std::move(*ptr()) : static_cast(f()); + } + + // map + template + auto map(F&& f) const& -> Option::type> { + using U = typename std::decay::type; + if (has_) { + return Option(f(*ptr())); + } + return Option(None); + } + + template + auto map(F&& f) && -> Option::type> { + using U = typename std::decay::type; + if (has_) { + // Move the value into the function + return Option(f(std::move(*ptr()))); + } + return Option(None); + } +}; + +// Helpers +template inline Option::type> Some(T&& v) { + return Option::type>(std::forward(v)); +} +inline Option Some() = delete; // no Option + +#define match_option(opt, some_name, some_block, none_block) \ + /* NOTE: you may return in a block, but not break/continue */ \ + do { \ + auto&& _option_val = (opt); \ + if (_option_val.is_some()) { \ + auto&& some_name = _option_val.unwrap(); \ + some_block \ + } else { \ + none_block \ + } \ + } while (0) + +// --- Option helpers: if_let_some / if_let_some_move / if_let_none --- + +#define _opt_concat(a, b) a##b +#define _opt_unique(base) _opt_concat(base, __LINE__) + +/* Bind a reference to the contained value if Some(...) */ +#define if_let_some(expr, name, block) \ + /* NOTE: you may return in a block, but not break/continue */ \ + do { \ + auto _opt_unique(_opt_) = (expr); \ + if (_opt_unique(_opt_).is_some()) { \ + auto&& name = _opt_unique(_opt_).unwrap(); \ + block \ + } \ + } while (0) + +/* Move the contained value out (consumes the Option) if Some(...) */ +#define if_let_some_move(expr, name, block) \ + /* NOTE: you may return in a block, but not break/continue */ \ + do { \ + auto _opt_unique(_opt_) = (expr); \ + if (_opt_unique(_opt_).is_some()) { \ + auto name = std::move(_opt_unique(_opt_)).unwrap(); \ + block \ + } \ + } while (0) + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/pairing_file.hpp b/cpp/include/idevice++/pairing_file.hpp new file mode 100644 index 0000000..724c836 --- /dev/null +++ b/cpp/include/idevice++/pairing_file.hpp @@ -0,0 +1,45 @@ +// Jackson Coxson + +#ifndef IDEVICE_PAIRING_FILE +#define IDEVICE_PAIRING_FILE + +#pragma once + +#include +#include +#include +#include +#include + +namespace IdeviceFFI { +struct PairingFileDeleter { + void operator()(IdevicePairingFile* p) const noexcept; +}; + +using PairingFilePtr = std::unique_ptr; + +class PairingFile { + public: + static Result read(const std::string& path); + static Result from_bytes(const uint8_t* data, size_t size); + + ~PairingFile() noexcept = default; // unique_ptr handles destruction + + PairingFile(const PairingFile&) = delete; + PairingFile& operator=(const PairingFile&) = delete; + + PairingFile(PairingFile&&) noexcept = default; // move is correct by default + PairingFile& operator=(PairingFile&&) noexcept = default; + + Result, FfiError> serialize() const; + + explicit PairingFile(IdevicePairingFile* ptr) noexcept : ptr_(ptr) {} + IdevicePairingFile* raw() const noexcept { return ptr_.get(); } + IdevicePairingFile* release() noexcept { return ptr_.release(); } + + private: + PairingFilePtr ptr_{}; // owns the handle +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/provider.hpp b/cpp/include/idevice++/provider.hpp new file mode 100644 index 0000000..65c1383 --- /dev/null +++ b/cpp/include/idevice++/provider.hpp @@ -0,0 +1,49 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +class FfiError; +class PairingFile; // has: IdevicePairingFile* raw() const; void + // release_on_success(); +class UsbmuxdAddr; // has: UsbmuxdAddrHandle* raw() const; void + // release_on_success(); + +using ProviderPtr = + std::unique_ptr>; + +class Provider { + public: + static Result + tcp_new(const idevice_sockaddr* ip, PairingFile&& pairing, const std::string& label); + + static Result usbmuxd_new(UsbmuxdAddr&& addr, + uint32_t tag, + const std::string& udid, + uint32_t device_id, + const std::string& label); + + ~Provider() noexcept = default; + Provider(Provider&&) noexcept = default; + Provider& operator=(Provider&&) noexcept = default; + Provider(const Provider&) = delete; + Provider& operator=(const Provider&) = delete; + + Result get_pairing_file(); + + IdeviceProviderHandle* raw() const noexcept { return handle_.get(); } + static Provider adopt(IdeviceProviderHandle* h) noexcept { return Provider(h); } + IdeviceProviderHandle* release() noexcept { return handle_.release(); } + + private: + explicit Provider(IdeviceProviderHandle* h) noexcept : handle_(h) {} + ProviderPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/readwrite.hpp b/cpp/include/idevice++/readwrite.hpp new file mode 100644 index 0000000..f9a8000 --- /dev/null +++ b/cpp/include/idevice++/readwrite.hpp @@ -0,0 +1,45 @@ +// Jackson Coxson + +#pragma once + +#include + +namespace IdeviceFFI { + +// A move-only holder for a fat-pointer stream. It does NOT free on destruction. +// Always pass ownership to an FFI that consumes it by calling release(). +class ReadWrite { + public: + ReadWrite() noexcept : ptr_(nullptr) {} + explicit ReadWrite(ReadWriteOpaque* p) noexcept : ptr_(p) {} + + ReadWrite(const ReadWrite&) = delete; + ReadWrite& operator=(const ReadWrite&) = delete; + + ReadWrite(ReadWrite&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } + ReadWrite& operator=(ReadWrite&& other) noexcept { + if (this != &other) { + ptr_ = other.ptr_; + other.ptr_ = nullptr; + } + return *this; + } + + ~ReadWrite() noexcept = default; // no dtor – Rust consumers own free/drop + + ReadWriteOpaque* raw() const noexcept { return ptr_; } + ReadWriteOpaque* release() noexcept { + auto* p = ptr_; + ptr_ = nullptr; + return p; + } + + static ReadWrite adopt(ReadWriteOpaque* p) noexcept { return ReadWrite(p); } + + explicit operator bool() const noexcept { return ptr_ != nullptr; } + + private: + ReadWriteOpaque* ptr_; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/remote_server.hpp b/cpp/include/idevice++/remote_server.hpp new file mode 100644 index 0000000..09b1022 --- /dev/null +++ b/cpp/include/idevice++/remote_server.hpp @@ -0,0 +1,42 @@ +// Jackson Coxson + +#ifndef IDEVICE_REMOTE_SERVER_H +#define IDEVICE_REMOTE_SERVER_H + +#pragma once +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using RemoteServerPtr = + std::unique_ptr>; + +class RemoteServer { + public: + // Factory: consumes the ReadWrite stream regardless of result + static Result from_socket(ReadWrite&& rw); + + // Factory: borrows adapter + handshake (neither is consumed) + static Result connect_rsd(Adapter& adapter, RsdHandshake& rsd); + + // RAII / moves + ~RemoteServer() noexcept = default; + RemoteServer(RemoteServer&&) noexcept = default; + RemoteServer& operator=(RemoteServer&&) noexcept = default; + RemoteServer(const RemoteServer&) = delete; + RemoteServer& operator=(const RemoteServer&) = delete; + + RemoteServerHandle* raw() const noexcept { return handle_.get(); } + static RemoteServer adopt(RemoteServerHandle* h) noexcept { return RemoteServer(h); } + + private: + explicit RemoteServer(RemoteServerHandle* h) noexcept : handle_(h) {} + RemoteServerPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/result.hpp b/cpp/include/idevice++/result.hpp new file mode 100644 index 0000000..0e545bb --- /dev/null +++ b/cpp/include/idevice++/result.hpp @@ -0,0 +1,306 @@ +// Jackson Coxson + +#pragma once + +#include +#include +#include +#include + +namespace IdeviceFFI { +namespace types { +template struct Ok { + T val; + + Ok(const T& val) : val(val) {} + Ok(T&& val) : val(std::move(val)) {} +}; + +template <> struct Ok {}; + +template struct Err { + E val; + + Err(const E& val) : val(val) {} + Err(E&& val) : val(std::move(val)) {} +}; +} // namespace types + +template inline types::Ok::type> Ok(T&& val) { + return types::Ok::type>(std::forward(val)); +} + +inline types::Ok Ok() { + return types::Ok(); +} + +template inline types::Err::type> Err(E&& val) { + return types::Err::type>(std::forward(val)); +} + +// ======================= +// Result +// ======================= +template class Result { + bool is_ok_; + union { + T ok_value_; + E err_value_; + }; + + public: + Result(types::Ok ok_val) : is_ok_(true), ok_value_(std::move(ok_val.val)) {} + Result(types::Err err_val) : is_ok_(false), err_value_(std::move(err_val.val)) {} + + Result(const Result& other) : is_ok_(other.is_ok_) { + if (is_ok_) { + new (&ok_value_) T(other.ok_value_); + } else { + new (&err_value_) E(other.err_value_); + } + } + + Result(Result&& other) noexcept : is_ok_(other.is_ok_) { + if (is_ok_) { + new (&ok_value_) T(std::move(other.ok_value_)); + } else { + new (&err_value_) E(std::move(other.err_value_)); + } + } + + ~Result() { + if (is_ok_) { + ok_value_.~T(); + } else { + err_value_.~E(); + } + } + + // Copy Assignment + Result& operator=(const Result& other) { + // Prevent self-assignment + if (this == &other) { + return *this; + } + + // Destroy the current value + if (is_ok_) { + ok_value_.~T(); + } else { + err_value_.~E(); + } + + is_ok_ = other.is_ok_; + + // Construct the new value + if (is_ok_) { + new (&ok_value_) T(other.ok_value_); + } else { + new (&err_value_) E(other.err_value_); + } + + return *this; + } + + // Move Assignment + Result& operator=(Result&& other) noexcept { + if (this == &other) { + return *this; + } + + // Destroy the current value + if (is_ok_) { + ok_value_.~T(); + } else { + err_value_.~E(); + } + + is_ok_ = other.is_ok_; + + // Construct the new value by moving + if (is_ok_) { + new (&ok_value_) T(std::move(other.ok_value_)); + } else { + new (&err_value_) E(std::move(other.err_value_)); + } + + return *this; + } + + bool is_ok() const { return is_ok_; } + bool is_err() const { return !is_ok_; } + + // lvalue (mutable) + T& unwrap() & { + if (!is_ok_) { + std::fprintf(stderr, "unwrap on Err\n"); + std::terminate(); + } + return ok_value_; + } + + // lvalue (const) + const T& unwrap() const& { + if (!is_ok_) { + std::fprintf(stderr, "unwrap on Err\n"); + std::terminate(); + } + return ok_value_; + } + + // rvalue (consume/move) + T unwrap() && { + if (!is_ok_) { + std::fprintf(stderr, "unwrap on Err\n"); + std::terminate(); + } + return std::move(ok_value_); + } + + E& unwrap_err() & { + if (is_ok_) { + std::fprintf(stderr, "unwrap_err on Ok\n"); + std::terminate(); + } + return err_value_; + } + + const E& unwrap_err() const& { + if (is_ok_) { + std::fprintf(stderr, "unwrap_err on Ok\n"); + std::terminate(); + } + return err_value_; + } + + E unwrap_err() && { + if (is_ok_) { + std::fprintf(stderr, "unwrap_err on Ok\n"); + std::terminate(); + } + return std::move(err_value_); + } + + T unwrap_or(T&& default_value) const { return is_ok_ ? ok_value_ : std::move(default_value); } + + T expect(const char* message) && { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return std::move(ok_value_); + } + + // Returns a mutable reference from an lvalue Result + T& expect(const char* message) & { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return ok_value_; + } + + // Returns a const reference from a const lvalue Result + const T& expect(const char* message) const& { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return ok_value_; + } + + template T unwrap_or_else(F&& f) & { + return is_ok_ ? ok_value_ : static_cast(f(err_value_)); + } + + // const lvalue: returns T by copy + template T unwrap_or_else(F&& f) const& { + return is_ok_ ? ok_value_ : static_cast(f(err_value_)); + } + + // rvalue: moves Ok(T) out; on Err(E), allow the handler to consume/move E + template T unwrap_or_else(F&& f) && { + if (is_ok_) { + return std::move(ok_value_); + } + return static_cast(std::forward(f)(std::move(err_value_))); + } +}; + +// Result specialization + +template class Result { + bool is_ok_; + union { + char dummy_; + E err_value_; + }; + + public: + Result(types::Ok) : is_ok_(true), dummy_() {} + Result(types::Err err_val) : is_ok_(false), err_value_(std::move(err_val.val)) {} + + Result(const Result& other) : is_ok_(other.is_ok_) { + if (!is_ok_) { + new (&err_value_) E(other.err_value_); + } + } + + Result(Result&& other) noexcept : is_ok_(other.is_ok_) { + if (!is_ok_) { + new (&err_value_) E(std::move(other.err_value_)); + } + } + + ~Result() { + if (!is_ok_) { + err_value_.~E(); + } + } + + bool is_ok() const { return is_ok_; } + bool is_err() const { return !is_ok_; } + + void unwrap() const { + if (!is_ok_) { + std::fprintf(stderr, "Attempted to unwrap an error Result\n"); + std::terminate(); + } + } + + const E& unwrap_err() const { + if (is_ok_) { + std::fprintf(stderr, "Attempted to unwrap_err on an ok Result\n"); + std::terminate(); + } + return err_value_; + } + + E& unwrap_err() { + if (is_ok_) { + std::fprintf(stderr, "Attempted to unwrap_err on an ok Result\n"); + std::terminate(); + } + return err_value_; + } + + void expect(const char* message) const { + if (is_err()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + } +}; + +#define match_result(res, ok_name, ok_block, err_name, err_block) \ + do { \ + auto&& _result_val = (res); \ + if (_result_val.is_ok()) { \ + auto&& ok_name = _result_val.unwrap(); \ + ok_block \ + } else { \ + auto&& err_name = _result_val.unwrap_err(); \ + err_block \ + } \ + } while (0) +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/rsd.hpp b/cpp/include/idevice++/rsd.hpp new file mode 100644 index 0000000..6532c3e --- /dev/null +++ b/cpp/include/idevice++/rsd.hpp @@ -0,0 +1,57 @@ +// Jackson Coxson + +#ifndef IDEVICE_RSD_H +#define IDEVICE_RSD_H + +#include +#include +#include +#include + +namespace IdeviceFFI { + +struct RsdService { + std::string name; + std::string entitlement; + uint16_t port{}; + bool uses_remote_xpc{}; + std::vector features; + int64_t service_version{-1}; +}; + +using RsdPtr = + std::unique_ptr>; + +class RsdHandshake { + public: + // Factory: consumes the ReadWrite socket regardless of result + static Result from_socket(ReadWrite&& rw); + + // Basic info + Result protocol_version() const; + Result uuid() const; + + // Services + Result, FfiError> services() const; + Result service_available(const std::string& name) const; + Result service_info(const std::string& name) const; + + // RAII / moves + ~RsdHandshake() noexcept = default; + RsdHandshake(RsdHandshake&&) noexcept = default; + RsdHandshake& operator=(RsdHandshake&&) noexcept = default; + + // Enable Copying + RsdHandshake(const RsdHandshake& other); + RsdHandshake& operator=(const RsdHandshake& other); + + RsdHandshakeHandle* raw() const noexcept { return handle_.get(); } + static RsdHandshake adopt(RsdHandshakeHandle* h) noexcept { return RsdHandshake(h); } + + private: + explicit RsdHandshake(RsdHandshakeHandle* h) noexcept : handle_(h) {} + RsdPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/include/idevice++/tcp_object_stack.hpp b/cpp/include/idevice++/tcp_object_stack.hpp new file mode 100644 index 0000000..7670ca0 --- /dev/null +++ b/cpp/include/idevice++/tcp_object_stack.hpp @@ -0,0 +1,176 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include + +#include +#include + +namespace IdeviceFFI { + +// ---------------- OwnedBuffer: RAII for zero-copy read buffers +// ---------------- +class OwnedBuffer { + public: + OwnedBuffer() noexcept : p_(nullptr), n_(0) {} + OwnedBuffer(const OwnedBuffer&) = delete; + OwnedBuffer& operator=(const OwnedBuffer&) = delete; + + OwnedBuffer(OwnedBuffer&& o) noexcept : p_(o.p_), n_(o.n_) { + o.p_ = nullptr; + o.n_ = 0; + } + OwnedBuffer& operator=(OwnedBuffer&& o) noexcept { + if (this != &o) { + reset(); + p_ = o.p_; + n_ = o.n_; + o.p_ = nullptr; + o.n_ = 0; + } + return *this; + } + + ~OwnedBuffer() { reset(); } + + const uint8_t* data() const noexcept { return p_; } + uint8_t* data() noexcept { return p_; } + std::size_t size() const noexcept { return n_; } + bool empty() const noexcept { return n_ == 0; } + + void reset() noexcept { + if (p_) { + ::idevice_data_free(p_, n_); + p_ = nullptr; + n_ = 0; + } + } + + private: + friend class TcpObjectStackEater; + void adopt(uint8_t* p, std::size_t n) noexcept { + reset(); + p_ = p; + n_ = n; + } + + uint8_t* p_; + std::size_t n_; +}; + +// ---------------- TcpFeeder: push inbound IP packets into the stack ---------- +class TcpObjectStackFeeder { + public: + TcpObjectStackFeeder() = default; + TcpObjectStackFeeder(const TcpObjectStackFeeder&) = delete; + TcpObjectStackFeeder& operator=(const TcpObjectStackFeeder&) = delete; + + TcpObjectStackFeeder(TcpObjectStackFeeder&& o) noexcept : h_(o.h_) { o.h_ = nullptr; } + TcpObjectStackFeeder& operator=(TcpObjectStackFeeder&& o) noexcept { + if (this != &o) { + reset(); + h_ = o.h_; + o.h_ = nullptr; + } + return *this; + } + + ~TcpObjectStackFeeder() { reset(); } + + bool write(const uint8_t* data, std::size_t len, FfiError& err) const; + ::TcpFeedObject* raw() const { return h_; } + + private: + friend class TcpObjectStack; + explicit TcpObjectStackFeeder(::TcpFeedObject* h) : h_(h) {} + + void reset() { + if (h_) { + ::idevice_free_tcp_feed_object(h_); + h_ = nullptr; + } + } + + ::TcpFeedObject* h_ = nullptr; +}; + +// ---------------- TcpEater: blocking read of outbound packets ---------------- +class TcpObjectStackEater { + public: + TcpObjectStackEater() = default; + TcpObjectStackEater(const TcpObjectStackEater&) = delete; + TcpObjectStackEater& operator=(const TcpObjectStackEater&) = delete; + + TcpObjectStackEater(TcpObjectStackEater&& o) noexcept : h_(o.h_) { o.h_ = nullptr; } + TcpObjectStackEater& operator=(TcpObjectStackEater&& o) noexcept { + if (this != &o) { + reset(); + h_ = o.h_; + o.h_ = nullptr; + } + return *this; + } + + ~TcpObjectStackEater() { reset(); } + + // Blocks until a packet is available. On success, 'out' adopts the buffer + // and you must keep 'out' alive until done (RAII frees via + // idevice_data_free). + bool read(OwnedBuffer& out, FfiError& err) const; + + ::TcpEatObject* raw() const { return h_; } + + private: + friend class TcpObjectStack; + explicit TcpObjectStackEater(::TcpEatObject* h) : h_(h) {} + + void reset() { + if (h_) { + ::idevice_free_tcp_eat_object(h_); + h_ = nullptr; + } + } + + ::TcpEatObject* h_ = nullptr; +}; + +// ---------------- Stack builder: returns feeder + eater + adapter ------------ +class TcpObjectStack { + public: + TcpObjectStack() = default; + TcpObjectStack(const TcpObjectStack&) = delete; // no sharing + TcpObjectStack& operator=(const TcpObjectStack&) = delete; + TcpObjectStack(TcpObjectStack&&) noexcept = default; // movable + TcpObjectStack& operator=(TcpObjectStack&&) noexcept = default; + + // Build the stack (dual-handle). Name kept to minimize churn. + static Result create(const std::string& our_ip, + const std::string& their_ip); + + TcpObjectStackFeeder& feeder(); + const TcpObjectStackFeeder& feeder() const; + + TcpObjectStackEater& eater(); + const TcpObjectStackEater& eater() const; + + Adapter& adapter(); + const Adapter& adapter() const; + + Option release_feeder(); // nullptr inside wrapper after call + Option release_eater(); // nullptr inside wrapper after call + Option release_adapter(); + + private: + struct Impl { + TcpObjectStackFeeder feeder; + TcpObjectStackEater eater; + Option adapter; + }; + // Unique ownership so there’s a single point of truth to release from + std::unique_ptr impl_; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/usbmuxd.hpp b/cpp/include/idevice++/usbmuxd.hpp new file mode 100644 index 0000000..018f312 --- /dev/null +++ b/cpp/include/idevice++/usbmuxd.hpp @@ -0,0 +1,124 @@ +// Jackson Coxson + +#ifndef IDEVICE_USBMUXD_HPP +#define IDEVICE_USBMUXD_HPP + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace IdeviceFFI { + +using AddrPtr = + std::unique_ptr>; +using DevicePtr = std::unique_ptr>; +using ConnectionPtr = + std::unique_ptr>; + +class UsbmuxdAddr { + public: + static Result tcp_new(const sockaddr* addr, socklen_t addr_len); +#if defined(__unix__) || defined(__APPLE__) + static Result unix_new(const std::string& path); +#endif + static UsbmuxdAddr default_new(); + + ~UsbmuxdAddr() noexcept = default; + UsbmuxdAddr(UsbmuxdAddr&&) noexcept = default; + UsbmuxdAddr& operator=(UsbmuxdAddr&&) noexcept = default; + UsbmuxdAddr(const UsbmuxdAddr&) = delete; + UsbmuxdAddr& operator=(const UsbmuxdAddr&) = delete; + + UsbmuxdAddrHandle* raw() const noexcept { return handle_.get(); } + UsbmuxdAddrHandle* release() noexcept { return handle_.release(); } + static UsbmuxdAddr adopt(UsbmuxdAddrHandle* h) noexcept { return UsbmuxdAddr(h); } + + private: + explicit UsbmuxdAddr(UsbmuxdAddrHandle* h) noexcept : handle_(h) {} + AddrPtr handle_{}; +}; + +class UsbmuxdConnectionType { + public: + enum class Value : uint8_t { Usb = 1, Network = 2, Unknown = 3 }; + explicit UsbmuxdConnectionType(uint8_t v) : _value(static_cast(v)) {} + + std::string to_string() const; // body in .cpp + Value value() const noexcept { return _value; } + bool operator==(Value other) const noexcept { return _value == other; } + + private: + Value _value{Value::Unknown}; +}; + +class UsbmuxdDevice { + public: + ~UsbmuxdDevice() noexcept = default; + UsbmuxdDevice(UsbmuxdDevice&&) noexcept = default; + UsbmuxdDevice& operator=(UsbmuxdDevice&&) noexcept = default; + UsbmuxdDevice(const UsbmuxdDevice&) = delete; + UsbmuxdDevice& operator=(const UsbmuxdDevice&) = delete; + + static UsbmuxdDevice adopt(UsbmuxdDeviceHandle* h) noexcept { return UsbmuxdDevice(h); } + + UsbmuxdDeviceHandle* raw() const noexcept { return handle_.get(); } + + Option get_udid() const; + Option get_id() const; + Option get_connection_type() const; + + private: + explicit UsbmuxdDevice(UsbmuxdDeviceHandle* h) noexcept : handle_(h) {} + DevicePtr handle_{}; + + friend class UsbmuxdConnection; +}; + +class PairingFile; + +class UsbmuxdConnection { + public: + static Result + tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag); +#if defined(__unix__) || defined(__APPLE__) + static Result unix_new(const std::string& path, uint32_t tag); +#endif + static Result default_new(uint32_t tag); + + ~UsbmuxdConnection() noexcept = default; + UsbmuxdConnection(UsbmuxdConnection&&) noexcept = default; + UsbmuxdConnection& operator=(UsbmuxdConnection&&) noexcept = default; + UsbmuxdConnection(const UsbmuxdConnection&) = delete; + UsbmuxdConnection& operator=(const UsbmuxdConnection&) = delete; + + Result, FfiError> get_devices() const; + Result get_buid() const; + Result get_pair_record(const std::string& udid); + + Result + connect_to_device(uint32_t device_id, uint16_t port, const std::string& path) &&; + Result connect_to_device(uint32_t, uint16_t, const std::string&) & = delete; + Result + connect_to_device(uint32_t, uint16_t, const std::string&) const& = delete; + + UsbmuxdConnectionHandle* raw() const noexcept { return handle_.get(); } + + private: + explicit UsbmuxdConnection(UsbmuxdConnectionHandle* h) noexcept : handle_(h) {} + ConnectionPtr handle_{}; +}; + +} // namespace IdeviceFFI +#endif diff --git a/cpp/plist_ffi b/cpp/plist_ffi new file mode 160000 index 0000000..66e6e63 --- /dev/null +++ b/cpp/plist_ffi @@ -0,0 +1 @@ +Subproject commit 66e6e6362b58bc361725c3e1b0a34af498aa2b3a diff --git a/cpp/sln/idevice++.sln b/cpp/sln/idevice++.sln new file mode 100644 index 0000000..2debadb --- /dev/null +++ b/cpp/sln/idevice++.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36327.8 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "idevice++", "idevice++.vcxproj", "{EBC5A8CF-BC80-454B-95B5-F2D14770A41D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|ARM64.Build.0 = Debug|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x64.ActiveCfg = Debug|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x64.Build.0 = Debug|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x86.ActiveCfg = Debug|Win32 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Debug|x86.Build.0 = Debug|Win32 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|ARM64.ActiveCfg = Release|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|ARM64.Build.0 = Release|ARM64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x64.ActiveCfg = Release|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x64.Build.0 = Release|x64 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x86.ActiveCfg = Release|Win32 + {EBC5A8CF-BC80-454B-95B5-F2D14770A41D}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9A195DE0-F99B-4101-80B4-C1CFC7BFC06F} + EndGlobalSection +EndGlobal diff --git a/cpp/sln/idevice++.vcxproj b/cpp/sln/idevice++.vcxproj new file mode 100644 index 0000000..93f0f54 --- /dev/null +++ b/cpp/sln/idevice++.vcxproj @@ -0,0 +1,354 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 17.0 + Win32Proj + {ebc5a8cf-bc80-454b-95b5-f2d14770a41d} + idevice + 10.0 + + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + true + Unicode + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + true + Unicode + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(SolutionDir)Build\64\$(Configuration)\ + $(SolutionDir)Build\64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath); + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\ + $(SolutionDir)Build\64\$(Configuration)\ + $(SolutionDir)Build\64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(SolutionDir)Build\aarch64\$(Configuration)\ + $(SolutionDir)Build\aarch64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\ + $(SolutionDir)Build\aarch64\$(Configuration)\ + $(SolutionDir)Build\aarch64\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\include; + $(SolutionDir)Build\32\$(Configuration)\ + $(SolutionDir)Build\32\$(Configuration)\Temp\$(ProjectName)\ + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\ + $(SolutionDir)Build\32\$(Configuration)\ + $(SolutionDir)Build\32\$(Configuration)\Temp\$(ProjectName)\ + + + + Level3 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + + + Default + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + + + Level3 + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + Default + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + + + Level3 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + Default + + + + + true + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + Default + + + + + true + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + + + Level3 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + Default + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + + + Level3 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + + + $(ProjectDir)..\include;$(ProjectDir)..\..\ffi + Default + + + + + true + + + call "$(ProjectDir)vs_build_rust.bat" $(Platform) $(OutDir) + + + idevice_ffi.lib + $(OutDir) + + + call "$(ProjectDir)..\vs_build_rust.bat" "$(Platform)" "$(OutDir)" + + + + + + \ No newline at end of file diff --git a/cpp/sln/idevice++.vcxproj.filters b/cpp/sln/idevice++.vcxproj.filters new file mode 100644 index 0000000..e5a301e --- /dev/null +++ b/cpp/sln/idevice++.vcxproj.filters @@ -0,0 +1,129 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {b4e9aee7-7e94-4e5f-b443-09677e8d69c2} + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files\idevice++ + + + Header Files + + + Header Files\idevice++ + + + Header Files\idevice++ + + + \ No newline at end of file diff --git a/cpp/src/adapter_stream.cpp b/cpp/src/adapter_stream.cpp new file mode 100644 index 0000000..2c2df76 --- /dev/null +++ b/cpp/src/adapter_stream.cpp @@ -0,0 +1,50 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +Result AdapterStream::close() { + if (!h_) + return Ok(); + + FfiError e(::adapter_close(h_)); + if (e) { + return Err(e); + } + + h_ = nullptr; + return Ok(); +} + +Result AdapterStream::send(const uint8_t *data, size_t len) { + if (!h_) + return Err(FfiError::NotConnected()); + FfiError e(::adapter_send(h_, data, len)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result, FfiError> AdapterStream::recv(size_t max_hint) { + if (!h_) + return Err(FfiError::NotConnected()); + + if (max_hint == 0) + max_hint = 2048; + + std::vector out(max_hint); + size_t actual = 0; + + FfiError e(::adapter_recv(h_, out.data(), &actual, out.size())); + if (e) { + return Err(e); + } + + out.resize(actual); + return Ok(std::move(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/app_service.cpp b/cpp/src/app_service.cpp new file mode 100644 index 0000000..ae9d9d9 --- /dev/null +++ b/cpp/src/app_service.cpp @@ -0,0 +1,181 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---- Factories ---- +Result AppService::connect_rsd(Adapter &adapter, + RsdHandshake &rsd) { + AppServiceHandle *out = nullptr; + if (IdeviceFfiError *e = + ::app_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + return Err(FfiError(e)); + } + return Ok(AppService::adopt(out)); +} + +Result +AppService::from_readwrite_ptr(ReadWriteOpaque *consumed) { + AppServiceHandle *out = nullptr; + if (IdeviceFfiError *e = ::app_service_new(consumed, &out)) { + return Err(FfiError(e)); + } + return Ok(AppService::adopt(out)); +} + +Result AppService::from_readwrite(ReadWrite &&rw) { + // Rust consumes the stream regardless of result → release BEFORE call + return from_readwrite_ptr(rw.release()); +} + +// ---- Helpers to copy/free C arrays ---- +static std::vector copy_and_free_app_list(AppListEntryC *arr, + size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + const auto &c = arr[i]; + AppInfo a; + a.is_removable = c.is_removable != 0; + if (c.name) + a.name = c.name; + a.is_first_party = c.is_first_party != 0; + if (c.path) + a.path = c.path; + if (c.bundle_identifier) + a.bundle_identifier = c.bundle_identifier; + a.is_developer_app = c.is_developer_app != 0; + if (c.bundle_version) + a.bundle_version = std::string(c.bundle_version); + a.is_internal = c.is_internal != 0; + a.is_hidden = c.is_hidden != 0; + a.is_app_clip = c.is_app_clip != 0; + if (c.version) + a.version = std::string(c.version); + out.emplace_back(std::move(a)); + } + ::app_service_free_app_list(arr, n); + return out; +} + +static std::vector copy_and_free_process_list(ProcessTokenC *arr, + size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + ProcessToken p; + p.pid = arr[i].pid; + if (arr[i].executable_url) + p.executable_url = std::string(arr[i].executable_url); + out.emplace_back(std::move(p)); + } + ::app_service_free_process_list(arr, n); + return out; +} + +// ---- API impls ---- +Result, FfiError> +AppService::list_apps(bool app_clips, bool removable, bool hidden, + bool internal, bool default_apps) const { + AppListEntryC *arr = nullptr; + size_t n = 0; + if (IdeviceFfiError *e = ::app_service_list_apps( + handle_.get(), app_clips ? 1 : 0, removable ? 1 : 0, hidden ? 1 : 0, + internal ? 1 : 0, default_apps ? 1 : 0, &arr, &n)) { + + return Err(FfiError(e)); + } + return Ok(copy_and_free_app_list(arr, n)); +} + +Result +AppService::launch(const std::string &bundle_id, + const std::vector &argv, bool kill_existing, + bool start_suspended) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto &s : argv) + c_argv.push_back(s.c_str()); + + LaunchResponseC *resp = nullptr; + if (IdeviceFfiError *e = ::app_service_launch_app( + handle_.get(), bundle_id.c_str(), + c_argv.empty() ? nullptr : c_argv.data(), c_argv.size(), + kill_existing ? 1 : 0, start_suspended ? 1 : 0, + NULL, // TODO: stdio handling + &resp)) { + return Err(FfiError(e)); + } + + LaunchResponse out; + out.process_identifier_version = resp->process_identifier_version; + out.pid = resp->pid; + if (resp->executable_url) + out.executable_url = resp->executable_url; + if (resp->audit_token && resp->audit_token_len > 0) { + out.audit_token.assign(resp->audit_token, + resp->audit_token + resp->audit_token_len); + } + ::app_service_free_launch_response(resp); + return Ok(std::move(out)); +} + +Result, FfiError> AppService::list_processes() const { + ProcessTokenC *arr = nullptr; + size_t n = 0; + if (IdeviceFfiError *e = + ::app_service_list_processes(handle_.get(), &arr, &n)) { + return Err(FfiError(e)); + } + return Ok(copy_and_free_process_list(arr, n)); +} + +Result AppService::uninstall(const std::string &bundle_id) { + if (IdeviceFfiError *e = + ::app_service_uninstall_app(handle_.get(), bundle_id.c_str())) { + return Err(FfiError(e)); + } + return Ok(); +} + +Result AppService::send_signal(uint32_t pid, + uint32_t signal) { + SignalResponseC *c = nullptr; + if (IdeviceFfiError *e = + ::app_service_send_signal(handle_.get(), pid, signal, &c)) { + return Err(FfiError(e)); + } + SignalResponse out; + out.pid = c->pid; + if (c->executable_url) + out.executable_url = std::string(c->executable_url); + out.device_timestamp_ms = c->device_timestamp; + out.signal = c->signal; + ::app_service_free_signal_response(c); + return Ok(std::move(out)); +} + +Result AppService::fetch_icon(const std::string &bundle_id, + float width, float height, + float scale, + bool allow_placeholder) { + IconDataC *c = nullptr; + if (IdeviceFfiError *e = ::app_service_fetch_app_icon( + handle_.get(), bundle_id.c_str(), width, height, scale, + allow_placeholder ? 1 : 0, &c)) { + return Err(FfiError(e)); + } + IconData out; + if (c->data && c->data_len) { + out.data.assign(c->data, c->data + c->data_len); + } + out.icon_width = c->icon_width; + out.icon_height = c->icon_height; + out.minimum_width = c->minimum_width; + out.minimum_height = c->minimum_height; + ::app_service_free_icon_data(c); + return Ok(std::move(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/core_device.cpp b/cpp/src/core_device.cpp new file mode 100644 index 0000000..5349211 --- /dev/null +++ b/cpp/src/core_device.cpp @@ -0,0 +1,120 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---- Factories ---- + +Result CoreDeviceProxy::connect(Provider &provider) { + CoreDeviceProxyHandle *out = nullptr; + FfiError e(::core_device_proxy_connect(provider.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(CoreDeviceProxy::adopt(out)); +} + +Result +CoreDeviceProxy::from_socket(Idevice &&socket) { + CoreDeviceProxyHandle *out = nullptr; + + // Rust consumes the socket regardless of result → release BEFORE call + IdeviceHandle *raw = socket.release(); + + FfiError e(::core_device_proxy_new(raw, &out)); + if (e) { + return Err(e); + } + return Ok(CoreDeviceProxy::adopt(out)); +} + +// ---- IO ---- + +Result CoreDeviceProxy::send(const uint8_t *data, size_t len) { + FfiError e(::core_device_proxy_send(handle_.get(), data, len)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result CoreDeviceProxy::recv(std::vector &out) { + if (out.empty()) + out.resize(4096); // a reasonable default; caller can pre-size + size_t actual = 0; + FfiError e( + ::core_device_proxy_recv(handle_.get(), out.data(), &actual, out.size())); + if (e) { + return Err(e); + } + out.resize(actual); + return Ok(); +} + +// ---- Handshake ---- + +Result +CoreDeviceProxy::get_client_parameters() const { + uint16_t mtu = 0; + char *addr_c = nullptr; + char *mask_c = nullptr; + + FfiError e(::core_device_proxy_get_client_parameters(handle_.get(), &mtu, + &addr_c, &mask_c)); + if (e) { + return Err(e); + } + + CoreClientParams params; + params.mtu = mtu; + if (addr_c) { + params.address = addr_c; + ::idevice_string_free(addr_c); + } + if (mask_c) { + params.netmask = mask_c; + ::idevice_string_free(mask_c); + } + return Ok(std::move(params)); +} + +Result CoreDeviceProxy::get_server_address() const { + char *addr_c = nullptr; + FfiError e(::core_device_proxy_get_server_address(handle_.get(), &addr_c)); + if (e) { + return Err(e); + } + std::string s; + if (addr_c) { + s = addr_c; + ::idevice_string_free(addr_c); + } + return Ok(s); +} + +Result CoreDeviceProxy::get_server_rsd_port() const { + uint16_t port = 0; + FfiError e(::core_device_proxy_get_server_rsd_port(handle_.get(), &port)); + if (e) { + return Err(e); + } + return Ok(port); +} + +// ---- Adapter creation (consumes *this) ---- + +Result CoreDeviceProxy::create_tcp_adapter() && { + AdapterHandle *out = nullptr; + + // Rust consumes the proxy regardless of result → release BEFORE call + CoreDeviceProxyHandle *raw = this->release(); + + FfiError e(::core_device_proxy_create_tcp_adapter(raw, &out)); + if (e) { + return Err(e); + } + return Ok(Adapter::adopt(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/debug_proxy.cpp b/cpp/src/debug_proxy.cpp new file mode 100644 index 0000000..9e8321d --- /dev/null +++ b/cpp/src/debug_proxy.cpp @@ -0,0 +1,143 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---- helpers ---- +static Option take_cstring(char *p) { + if (!p) + return None; + std::string s(p); + ::idevice_string_free(p); + return Some(s); +} + +// ---- DebugCommand ---- +Option DebugCommand::make(const std::string &name, + const std::vector &argv) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto &a : argv) + c_argv.push_back(a.c_str()); + + auto *h = ::debugserver_command_new( + name.c_str(), + c_argv.empty() ? nullptr : const_cast(c_argv.data()), + c_argv.size()); + if (!h) + return None; + return Some(DebugCommand(h)); +} + +// ---- DebugProxy factories ---- +Result DebugProxy::connect_rsd(Adapter &adapter, + RsdHandshake &rsd) { + ::DebugProxyHandle *out = nullptr; + FfiError e(::debug_proxy_connect_rsd(adapter.raw(), rsd.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(DebugProxy(out)); +} + +Result +DebugProxy::from_readwrite_ptr(::ReadWriteOpaque *consumed) { + ::DebugProxyHandle *out = nullptr; + FfiError e(::debug_proxy_new(consumed, &out)); + if (e) { + return Err(e); + } + return Ok(DebugProxy(out)); +} + +Result DebugProxy::from_readwrite(ReadWrite &&rw) { + // Rust consumes the pointer regardless of outcome; release before calling + return from_readwrite_ptr(rw.release()); +} + +// ---- DebugProxy API ---- +Result, FfiError> +DebugProxy::send_command(const std::string &name, + const std::vector &argv) { + auto cmdRes = DebugCommand::make(name, argv); + if (cmdRes.is_none()) { + // treat as invalid arg + FfiError err; + err.code = -1; + err.message = "debugserver_command_new failed"; + return Err(err); + } + auto cmd = std::move(cmdRes).unwrap(); + + char *resp_c = nullptr; + FfiError e(::debug_proxy_send_command(handle_, cmd.raw(), &resp_c)); + if (e) { + return Err(e); + } + + return Ok(take_cstring(resp_c)); +} + +Result, FfiError> DebugProxy::read_response() { + char *resp_c = nullptr; + FfiError e(::debug_proxy_read_response(handle_, &resp_c)); + if (e) { + return Err(e); + } + return Ok(take_cstring(resp_c)); +} + +Result DebugProxy::send_raw(const std::vector &data) { + FfiError e(::debug_proxy_send_raw(handle_, data.data(), data.size())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result, FfiError> DebugProxy::read(std::size_t len) { + char *resp_c = nullptr; + FfiError e(::debug_proxy_read(handle_, len, &resp_c)); + if (e) { + return Err(e); + } + return Ok(take_cstring(resp_c)); +} + +Result, FfiError> +DebugProxy::set_argv(const std::vector &argv) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto &a : argv) + c_argv.push_back(a.c_str()); + + char *resp_c = nullptr; + FfiError e(::debug_proxy_set_argv( + handle_, + c_argv.empty() ? nullptr : const_cast(c_argv.data()), + c_argv.size(), &resp_c)); + if (e) { + return Err(e); + } + return Ok(take_cstring(resp_c)); +} + +Result DebugProxy::send_ack() { + FfiError e(::debug_proxy_send_ack(handle_)); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DebugProxy::send_nack() { + FfiError e(::debug_proxy_send_nack(handle_)); + if (e) { + return Err(e); + } + return Ok(); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/diagnosticsservice.cpp b/cpp/src/diagnosticsservice.cpp new file mode 100644 index 0000000..8e95cdb --- /dev/null +++ b/cpp/src/diagnosticsservice.cpp @@ -0,0 +1,93 @@ +// Jackson Coxson + +#include +#include +#include + +namespace IdeviceFFI { + +// Local helper: take ownership of a C string and convert to std::string +static Option take_cstring(char* p) { + if (!p) { + return None; + } + + std::string s(p); + ::idevice_string_free(p); + return Some(std::move(s)); +} + +// -------- SysdiagnoseStream -------- +Result>, FfiError> SysdiagnoseStream::next_chunk() { + if (!h_) { + return Err(FfiError::NotConnected()); + } + + uint8_t* data = nullptr; + std::size_t len = 0; + + FfiError e(::sysdiagnose_stream_next(h_, &data, &len)); + if (e) { + return Err(e); + } + + if (!data || len == 0) { + // End of stream + return Ok(Option>(None)); + } + + // Copy into a C++ buffer + std::vector out(len); + std::memcpy(out.data(), data, len); + + idevice_data_free(data, len); + + return Ok(Some(out)); +} + +// -------- DiagnosticsService -------- +Result DiagnosticsService::connect_rsd(Adapter& adapter, + RsdHandshake& rsd) { + ::DiagnosticsServiceHandle* out = nullptr; + FfiError e(::diagnostics_service_connect_rsd(adapter.raw(), rsd.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(DiagnosticsService(out)); +} + +Result +DiagnosticsService::from_stream_ptr(::ReadWriteOpaque* consumed) { + ::DiagnosticsServiceHandle* out = nullptr; + FfiError e(::diagnostics_service_new(consumed, &out)); + if (e) { + return Err(e); + } + return Ok(DiagnosticsService(out)); +} + +Result DiagnosticsService::capture_sysdiagnose(bool dry_run) { + if (!h_) { + return Err(FfiError::NotConnected()); + } + + char* filename_c = nullptr; + std::size_t expected_len = 0; + ::SysdiagnoseStreamHandle* stream_h = nullptr; + + FfiError e(::diagnostics_service_capture_sysdiagnose( + h_, dry_run ? true : false, &filename_c, &expected_len, &stream_h)); + if (e) { + return Err(e); + } + + auto fname = take_cstring(filename_c).unwrap_or(std::string{}); + SysdiagnoseStream stream(stream_h); + + SysdiagnoseCapture cap{/*preferred_filename*/ std::move(fname), + /*expected_length*/ expected_len, + /*stream*/ std::move(stream)}; + return Ok(std::move(cap)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/ffi.cpp b/cpp/src/ffi.cpp new file mode 100644 index 0000000..51be26f --- /dev/null +++ b/cpp/src/ffi.cpp @@ -0,0 +1,32 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { +FfiError::FfiError(const IdeviceFfiError* err) + : code(err ? err->code : 0), message(err && err->message ? err->message : "") { + if (err) { + idevice_error_free(const_cast(err)); + } +} + +FfiError::FfiError() : code(0), message("") { +} + +FfiError FfiError::NotConnected() { + FfiError err; + err.code = -11; // from idevice/lib.rs + err.message = "No established socket connection"; + return err; +} +FfiError FfiError::InvalidArgument() { + FfiError err; + err.code = -57; // from idevice/lib.rs + err.message = "No established socket connection"; + return err; +} + +} // namespace IdeviceFFI diff --git a/cpp/src/idevice.cpp b/cpp/src/idevice.cpp new file mode 100644 index 0000000..caa490b --- /dev/null +++ b/cpp/src/idevice.cpp @@ -0,0 +1,53 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +Result Idevice::create(IdeviceSocketHandle* socket, const std::string& label) { + IdeviceHandle* h = nullptr; + FfiError e(idevice_new(socket, label.c_str(), &h)); + if (e) { + return Err(e); + } + return Ok(Idevice(h)); +} + +Result +Idevice::create_tcp(const sockaddr* addr, socklen_t addr_len, const std::string& label) { + IdeviceHandle* h = nullptr; + FfiError e(idevice_new_tcp_socket(addr, addr_len, label.c_str(), &h)); + if (e) { + return Err(e); + } + return Ok(Idevice(h)); +} + +Result Idevice::get_type() const { + char* cstr = nullptr; + FfiError e(idevice_get_type(handle_.get(), &cstr)); + if (e) { + return Err(e); + } + std::string out(cstr); + idevice_string_free(cstr); + return Ok(out); +} + +Result Idevice::rsd_checkin() { + FfiError e(idevice_rsd_checkin(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result Idevice::start_session(const PairingFile& pairing_file) { + FfiError e(idevice_start_session(handle_.get(), pairing_file.raw())); + if (e) { + return Err(e); + } + return Ok(); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/location_simulation.cpp b/cpp/src/location_simulation.cpp new file mode 100644 index 0000000..99abb85 --- /dev/null +++ b/cpp/src/location_simulation.cpp @@ -0,0 +1,32 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +Result LocationSimulation::create(RemoteServer& server) { + LocationSimulationHandle* out = nullptr; + FfiError e(::location_simulation_new(server.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(LocationSimulation::adopt(out)); +} + +Result LocationSimulation::clear() { + FfiError e(::location_simulation_clear(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result LocationSimulation::set(double latitude, double longitude) { + FfiError e(::location_simulation_set(handle_.get(), latitude, longitude)); + if (e) { + return Err(e); + } + return Ok(); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/lockdown.cpp b/cpp/src/lockdown.cpp new file mode 100644 index 0000000..2a798a1 --- /dev/null +++ b/cpp/src/lockdown.cpp @@ -0,0 +1,57 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +Result Lockdown::connect(Provider& provider) { + LockdowndClientHandle* out = nullptr; + FfiError e(::lockdownd_connect(provider.raw(), &out)); + if (e) { + provider.release(); + return Err(e); + } + return Ok(Lockdown::adopt(out)); +} + +Result Lockdown::from_socket(Idevice&& socket) { + LockdowndClientHandle* out = nullptr; + FfiError e(::lockdownd_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(Lockdown::adopt(out)); +} + +Result Lockdown::start_session(const PairingFile& pf) { + FfiError e(::lockdownd_start_session(handle_.get(), pf.raw())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result, FfiError> Lockdown::start_service(const std::string& identifier) { + uint16_t port = 0; + bool ssl = false; + FfiError e(::lockdownd_start_service(handle_.get(), identifier.c_str(), &port, &ssl)); + if (e) { + return Err(e); + } + return Ok(std::make_pair(port, ssl)); +} + +Result Lockdown::get_value(const char* key, const char* domain) { + plist_t out = nullptr; + FfiError e(::lockdownd_get_value(handle_.get(), key, domain, &out)); + if (e) { + return Err(e); + } + return Ok(out); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/pairing_file.cpp b/cpp/src/pairing_file.cpp new file mode 100644 index 0000000..9ba8f40 --- /dev/null +++ b/cpp/src/pairing_file.cpp @@ -0,0 +1,53 @@ +// Jackson Coxson + +#include +#include +#include + +namespace IdeviceFFI { + +// Deleter definition (out-of-line) +void PairingFileDeleter::operator()(IdevicePairingFile* p) const noexcept { + if (p) { + idevice_pairing_file_free(p); + } +} + +// Static member definitions +Result PairingFile::read(const std::string& path) { + IdevicePairingFile* ptr = nullptr; + FfiError e(idevice_pairing_file_read(path.c_str(), &ptr)); + if (e) { + return Err(e); + } + return Ok(PairingFile(ptr)); +} + +Result PairingFile::from_bytes(const uint8_t* data, size_t size) { + IdevicePairingFile* raw = nullptr; + FfiError e(idevice_pairing_file_from_bytes(data, size, &raw)); + if (e) { + return Err(e); + } + return Ok(PairingFile(raw)); +} + +Result, FfiError> PairingFile::serialize() const { + if (!ptr_) { + return Err(FfiError::InvalidArgument()); + } + + uint8_t* data = nullptr; + size_t size = 0; + + FfiError e(idevice_pairing_file_serialize(ptr_.get(), &data, &size)); + if (e) { + return Err(e); + } + + std::vector out(data, data + size); + idevice_data_free(data, size); + return Ok(out); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/provider.cpp b/cpp/src/provider.cpp new file mode 100644 index 0000000..7c47d56 --- /dev/null +++ b/cpp/src/provider.cpp @@ -0,0 +1,58 @@ +// Jackson Coxson + +#include +#include +#include + +namespace IdeviceFFI { + +Result +Provider::tcp_new(const idevice_sockaddr* ip, PairingFile&& pairing, const std::string& label) { + IdeviceProviderHandle* out = nullptr; + + FfiError e(idevice_tcp_provider_new( + ip, static_cast(pairing.raw()), label.c_str(), &out)); + if (e) { + return Err(e); + } + + // Success: Rust consumed the pairing file -> abandon our ownership + pairing.release(); + + return Ok(Provider::adopt(out)); +} + +Result Provider::usbmuxd_new(UsbmuxdAddr&& addr, + uint32_t tag, + const std::string& udid, + uint32_t device_id, + const std::string& label) { + IdeviceProviderHandle* out = nullptr; + + FfiError e(usbmuxd_provider_new(static_cast(addr.raw()), + tag, + udid.c_str(), + device_id, + label.c_str(), + &out)); + if (e) { + return Err(e); + } + + // Success: Rust consumed the addr -> abandon our ownership + addr.release(); + return Ok(Provider::adopt(out)); +} + +Result Provider::get_pairing_file() { + + IdevicePairingFile* out = nullptr; + FfiError e(idevice_provider_get_pairing_file(handle_.get(), &out)); + if (e) { + return Err(e); + } + + return Ok(PairingFile(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/remote_server.cpp b/cpp/src/remote_server.cpp new file mode 100644 index 0000000..acfc736 --- /dev/null +++ b/cpp/src/remote_server.cpp @@ -0,0 +1,29 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +Result RemoteServer::from_socket(ReadWrite&& rw) { + RemoteServerHandle* out = nullptr; + + // Rust consumes the stream regardless of result, release BEFORE the call + ReadWriteOpaque* raw = rw.release(); + + FfiError e(::remote_server_new(raw, &out)); + if (e) { + return Err(e); + } + return Ok(RemoteServer::adopt(out)); +} + +Result RemoteServer::connect_rsd(Adapter& adapter, RsdHandshake& rsd) { + RemoteServerHandle* out = nullptr; + FfiError e(::remote_server_connect_rsd(adapter.raw(), rsd.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(RemoteServer::adopt(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/rsd.cpp b/cpp/src/rsd.cpp new file mode 100644 index 0000000..281b09d --- /dev/null +++ b/cpp/src/rsd.cpp @@ -0,0 +1,151 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---------- helpers to copy/free CRsdService ---------- +static RsdService to_cpp_and_free(CRsdService* c) { + RsdService s; + if (c->name) { + s.name = c->name; + } + if (c->entitlement) { + s.entitlement = c->entitlement; + } + s.port = c->port; + s.uses_remote_xpc = c->uses_remote_xpc; + s.service_version = c->service_version; + + // features + if (c->features && c->features_count > 0) { + auto** arr = c->features; + s.features.reserve(c->features_count); + for (size_t i = 0; i < c->features_count; ++i) { + if (arr[i]) { + s.features.emplace_back(arr[i]); + } + } + } + + // release the C allocation now that we've copied + rsd_free_service(c); + return s; +} + +static std::vector to_cpp_and_free(CRsdServiceArray* arr) { + std::vector out; + if (!arr || !arr->services || arr->count == 0) { + if (arr) { + rsd_free_services(arr); + } + return out; + } + out.reserve(arr->count); + auto* begin = arr->services; + for (size_t i = 0; i < arr->count; ++i) { + out.emplace_back(RsdService{begin[i].name ? begin[i].name : "", + begin[i].entitlement ? begin[i].entitlement : "", + begin[i].port, + begin[i].uses_remote_xpc, + {}, // features, fill below + begin[i].service_version}); + // features for this service + if (begin[i].features && begin[i].features_count > 0) { + auto** feats = begin[i].features; + out.back().features.reserve(begin[i].features_count); + for (size_t j = 0; j < begin[i].features_count; ++j) { + if (feats[j]) { + out.back().features.emplace_back(feats[j]); + } + } + } + } + // free the array + nested C strings now that we've copied + rsd_free_services(arr); + return out; +} + +RsdHandshake::RsdHandshake(const RsdHandshake& other) { + if (other.handle_) { + // Call the Rust FFI to clone the underlying handle + handle_.reset(rsd_handshake_clone(other.handle_.get())); + } + // If other.handle_ is null, our new handle_ will also be null, which is correct. +} + +RsdHandshake& RsdHandshake::operator=(const RsdHandshake& other) { + // Check for self-assignment + if (this != &other) { + // Create a temporary copy, then swap ownership + RsdHandshake temp(other); + std::swap(handle_, temp.handle_); + } + return *this; +} + +// ---------- factory ---------- +Result RsdHandshake::from_socket(ReadWrite&& rw) { + RsdHandshakeHandle* out = nullptr; + + // Rust consumes the socket regardless of result ⇒ release BEFORE call. + ReadWriteOpaque* raw = rw.release(); + FfiError e(rsd_handshake_new(raw, &out)); + if (e) { + return Err(e); + } + return Ok(RsdHandshake::adopt(out)); +} + +// ---------- queries ---------- +Result RsdHandshake::protocol_version() const { + size_t v = 0; + FfiError e(rsd_get_protocol_version(handle_.get(), &v)); + if (e) { + return Err(e); + } + return Ok(v); +} + +Result RsdHandshake::uuid() const { + char* c = nullptr; + FfiError e(rsd_get_uuid(handle_.get(), &c)); + if (e) { + return Err(e); + } + std::string out; + if (c) { + out = c; + rsd_free_string(c); + } + return Ok(out); +} + +Result, FfiError> RsdHandshake::services() const { + CRsdServiceArray* arr = nullptr; + FfiError e(rsd_get_services(handle_.get(), &arr)); + if (e) { + return Err(e); + } + return Ok(to_cpp_and_free(arr)); +} + +Result RsdHandshake::service_available(const std::string& name) const { + bool avail = false; + FfiError e(rsd_service_available(handle_.get(), name.c_str(), &avail)); + if (e) { + return Err(e); + } + return Ok(avail); +} + +Result RsdHandshake::service_info(const std::string& name) const { + CRsdService* svc = nullptr; + FfiError e(rsd_get_service_info(handle_.get(), name.c_str(), &svc)); + if (e) { + return Err(e); + } + return Ok(to_cpp_and_free(svc)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/tcp_callback_feeder.cpp b/cpp/src/tcp_callback_feeder.cpp new file mode 100644 index 0000000..07c2e6b --- /dev/null +++ b/cpp/src/tcp_callback_feeder.cpp @@ -0,0 +1,118 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---------- TcpFeeder ---------- +bool TcpObjectStackFeeder::write(const uint8_t* data, std::size_t len, FfiError& err) const { + if (IdeviceFfiError* e = ::idevice_tcp_feed_object_write(h_, data, len)) { + err = FfiError(e); + return false; + } + return true; +} + +// ---------- TcpEater ---------- +bool TcpObjectStackEater::read(OwnedBuffer& out, FfiError& err) const { + uint8_t* ptr = nullptr; + std::size_t len = 0; + if (IdeviceFfiError* e = ::idevice_tcp_eat_object_read(h_, &ptr, &len)) { + err = FfiError(e); + return false; + } + // Success: adopt the buffer (freed via idevice_data_free in OwnedBuffer dtor) + out.adopt(ptr, len); + return true; +} + +// ---------- TcpStackFromCallback ---------- +Result TcpObjectStack::create(const std::string& our_ip, + const std::string& their_ip) { + ::TcpFeedObject* feeder_h = nullptr; + ::TcpEatObject* eater_h = nullptr; + ::AdapterHandle* adapter_h = nullptr; + + FfiError e(::idevice_tcp_stack_into_sync_objects( + our_ip.c_str(), their_ip.c_str(), &feeder_h, &eater_h, &adapter_h)); + if (e) { + return Err(e); + } + + auto impl = std::make_unique(); + impl->feeder = TcpObjectStackFeeder(feeder_h); + impl->eater = TcpObjectStackEater(eater_h); + impl->adapter = Adapter::adopt(adapter_h); + + TcpObjectStack out; + out.impl_ = std::move(impl); + return Ok(std::move(out)); +} + +TcpObjectStackFeeder& TcpObjectStack::feeder() { + return impl_->feeder; +} +const TcpObjectStackFeeder& TcpObjectStack::feeder() const { + return impl_->feeder; +} + +TcpObjectStackEater& TcpObjectStack::eater() { + return impl_->eater; +} +const TcpObjectStackEater& TcpObjectStack::eater() const { + return impl_->eater; +} + +Adapter& TcpObjectStack::adapter() { + if (!impl_ || impl_->adapter.is_some()) { + static Adapter* never = nullptr; + return *never; + } + return (impl_->adapter.unwrap()); +} +const Adapter& TcpObjectStack::adapter() const { + if (!impl_ || impl_->adapter.is_none()) { + static Adapter* never = nullptr; + return *never; + } + return (impl_->adapter.unwrap()); +} + +// ---------- Release APIs ---------- +Option TcpObjectStack::release_feeder() { + if (!impl_) { + return None; + } + auto has = impl_->feeder.raw() != nullptr; + if (!has) { + return None; + } + TcpObjectStackFeeder out = std::move(impl_->feeder); + // impl_->feeder is now empty (h_ == nullptr) thanks to move + return Some(std::move(out)); +} + +Option TcpObjectStack::release_eater() { + if (!impl_) { + return None; + } + auto has = impl_->eater.raw() != nullptr; + if (!has) { + return None; + } + TcpObjectStackEater out = std::move(impl_->eater); + return Some(std::move(out)); +} + +Option TcpObjectStack::release_adapter() { + if (!impl_ || impl_->adapter.is_none()) { + return None; + } + // Move out and clear our optional + auto out = std::move((impl_->adapter.unwrap())); + impl_->adapter.reset(); + return Some(std::move(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/src/usbmuxd.cpp b/cpp/src/usbmuxd.cpp new file mode 100644 index 0000000..5c4fb04 --- /dev/null +++ b/cpp/src/usbmuxd.cpp @@ -0,0 +1,155 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---------- UsbmuxdAddr ---------- +Result UsbmuxdAddr::tcp_new(const sockaddr* addr, socklen_t addr_len) { + UsbmuxdAddrHandle* h = nullptr; + FfiError e(idevice_usbmuxd_tcp_addr_new(addr, addr_len, &h)); + if (e) { + return Err(e); + } + return Ok(UsbmuxdAddr(h)); +} + +#if defined(__unix__) || defined(__APPLE__) +Result UsbmuxdAddr::unix_new(const std::string& path) { + UsbmuxdAddrHandle* h = nullptr; + FfiError e(idevice_usbmuxd_unix_addr_new(path.c_str(), &h)); + if (e) { + return Err(e); + } + return Ok(UsbmuxdAddr(h)); +} +#endif + +UsbmuxdAddr UsbmuxdAddr::default_new() { + UsbmuxdAddrHandle* h = nullptr; + idevice_usbmuxd_default_addr_new(&h); + return UsbmuxdAddr::adopt(h); +} + +// ---------- UsbmuxdConnectionType ---------- +std::string UsbmuxdConnectionType::to_string() const { + switch (_value) { + case Value::Usb: + return "USB"; + case Value::Network: + return "Network"; + case Value::Unknown: + return "Unknown"; + default: + return "UnknownEnumValue"; + } +} + +// ---------- UsbmuxdDevice ---------- +Option UsbmuxdDevice::get_udid() const { + char* c = idevice_usbmuxd_device_get_udid(handle_.get()); + if (!c) { + return None; + } + std::string out(c); + idevice_string_free(c); + return Some(out); +} + +Option UsbmuxdDevice::get_id() const { + uint32_t id = idevice_usbmuxd_device_get_device_id(handle_.get()); + if (id == 0) { + return None; + } + return Some(id); +} + +Option UsbmuxdDevice::get_connection_type() const { + uint8_t t = idevice_usbmuxd_device_get_connection_type(handle_.get()); + if (t == 0) { + return None; + } + return Some(UsbmuxdConnectionType(t)); +} + +// ---------- UsbmuxdConnection ---------- +Result +UsbmuxdConnection::tcp_new(const idevice_sockaddr* addr, idevice_socklen_t addr_len, uint32_t tag) { + UsbmuxdConnectionHandle* h = nullptr; + FfiError e(idevice_usbmuxd_new_tcp_connection(addr, addr_len, tag, &h)); + if (e) { + return Err(e); + } + return Ok(UsbmuxdConnection(h)); +} + +#if defined(__unix__) || defined(__APPLE__) +Result UsbmuxdConnection::unix_new(const std::string& path, + uint32_t tag) { + UsbmuxdConnectionHandle* h = nullptr; + FfiError e(idevice_usbmuxd_new_unix_socket_connection(path.c_str(), tag, &h)); + if (e) { + return Err(e); + } + return Ok(UsbmuxdConnection(h)); +} +#endif + +Result UsbmuxdConnection::default_new(uint32_t tag) { + UsbmuxdConnectionHandle* h = nullptr; + FfiError e(idevice_usbmuxd_new_default_connection(tag, &h)); + if (e) { + return Err(e); + } + return Ok(UsbmuxdConnection(h)); +} + +Result, FfiError> UsbmuxdConnection::get_devices() const { + UsbmuxdDeviceHandle** list = nullptr; + int count = 0; + FfiError e(idevice_usbmuxd_get_devices(handle_.get(), &list, &count)); + if (e) { + return Err(e); + } + std::vector out; + out.reserve(count); + for (int i = 0; i < count; ++i) { + out.emplace_back(UsbmuxdDevice::adopt(list[i])); + } + return Ok(std::move(out)); +} + +Result UsbmuxdConnection::get_buid() const { + char* c = nullptr; + FfiError e(idevice_usbmuxd_get_buid(handle_.get(), &c)); + if (e) { + return Err(e); + } + std::string out(c); + idevice_string_free(c); + return Ok(out); +} + +Result UsbmuxdConnection::get_pair_record(const std::string& udid) { + IdevicePairingFile* pf = nullptr; + FfiError e(idevice_usbmuxd_get_pair_record(handle_.get(), udid.c_str(), &pf)); + if (e) { + return Err(e); + } + return Ok(PairingFile(pf)); +} + +Result UsbmuxdConnection::connect_to_device(uint32_t device_id, + uint16_t port, + const std::string& path) && { + UsbmuxdConnectionHandle* raw = handle_.release(); + IdeviceHandle* out = nullptr; + FfiError e(idevice_usbmuxd_connect_to_device(raw, device_id, port, path.c_str(), &out)); + if (e) { + return Err(e); + } + return Ok(Idevice::adopt(out)); +} + +} // namespace IdeviceFFI diff --git a/cpp/vs_build_rust.bat b/cpp/vs_build_rust.bat new file mode 100644 index 0000000..1c15458 --- /dev/null +++ b/cpp/vs_build_rust.bat @@ -0,0 +1,65 @@ +@echo off +setlocal + +REM --- Configuration --- +SET "CRATE_NAME=idevice_ffi" +SET "RUST_PROJECT_PATH=%~dp0..\ffi" + +echo "--- Rust Build Script Started ---" +echo "Rust Project Path: %RUST_PROJECT_PATH%" +echo "Visual Studio Platform: %1" + +REM --- Header File Copy --- +xcopy /Y "%RUST_PROJECT_PATH%\idevice.h" "%~dp0\include\" + +REM --- Locate Cargo --- +REM Check if cargo is in the PATH. +where cargo >nul 2>nul +if %errorlevel% neq 0 ( + echo Error: cargo.exe not found in PATH. + echo Please ensure the Rust toolchain is installed and configured. + exit /b 1 +) + +REM --- Determine Rust Target --- +SET "RUST_TARGET=" +IF /I "%~1" == "x64" ( + SET "RUST_TARGET=x86_64-pc-windows-msvc" +) +IF /I "%~1" == "ARM64" ( + SET "RUST_TARGET=aarch64-pc-windows-msvc" +) +IF /I "%~1" == "Win32" ( + SET "RUST_TARGET=i686-pc-windows-msvc" +) + +IF NOT DEFINED RUST_TARGET ( + echo Error: Unsupported Visual Studio platform '%~1'. + echo This script supports 'x64' and 'ARM64'. + exit /b 1 +) + +echo "Building for Rust target: %RUST_TARGET%" + +REM --- Run Cargo Build --- +SET "STATIC_LIB_NAME=%CRATE_NAME%.lib" +SET "BUILT_LIB_PATH=%RUST_PROJECT_PATH%\..\target\%RUST_TARGET%\release\%STATIC_LIB_NAME%" + +REM Change to the Rust project directory and run the build. +pushd "%RUST_PROJECT_PATH%" +cargo build --release --target %RUST_TARGET% --features ring,full --no-default-features +if %errorlevel% neq 0 ( + echo Error: Cargo build failed. + popd + exit /b 1 +) +popd + +echo "Cargo build successful." + +REM --- Copy Artifacts --- +echo "Copying '%BUILT_LIB_PATH%' to '%2'" +xcopy /Y "%BUILT_LIB_PATH%" "%2" + +echo "--- Rust Build Script Finished ---" +exit /b 0 diff --git a/cpp/xcode_build_rust.sh b/cpp/xcode_build_rust.sh new file mode 100755 index 0000000..00b1b5a --- /dev/null +++ b/cpp/xcode_build_rust.sh @@ -0,0 +1,123 @@ +#!/bin/sh + +cp ../ffi/idevice.h include/ + +# This script builds a Rust library for use in an Xcode project. +# It's designed to be used as a "Run Script" build phase on a native Xcode target. +# It handles multiple architectures by building for each and combining them with `lipo`. + +# --- Configuration --- +# The name of your Rust crate (the name in Cargo.toml) +CRATE_NAME="idevice_ffi" +# The path to your Rust project's root directory (containing Cargo.toml) +RUST_PROJECT_PATH="${PROJECT_DIR}/../ffi" + +# --- Environment Setup --- +# Augment the PATH to include common locations for build tools like cmake and go. +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/local/go/bin:$PATH" + +# --- Locate Cargo --- +# Xcode's build environment often has a minimal PATH, so we need to find cargo explicitly. +if [ -x "${HOME}/.cargo/bin/cargo" ]; then + CARGO="${HOME}/.cargo/bin/cargo" +else + CARGO=$(command -v cargo) + if [ -z "$CARGO" ]; then + echo "Error: cargo executable not found." >&2 + echo "Please ensure Rust is installed and cargo is in your PATH or at ~/.cargo/bin/" >&2 + exit 1 + fi +fi + +# --- Script Logic --- + +# Exit immediately if a command exits with a non-zero status. +set -e + +# --- Platform & SDK Configuration --- +# In a "Run Script" phase on a native target, PLATFORM_NAME is reliable. +# We use it to determine the correct SDK and build parameters. +PLATFORM_SUFFIX="" +SDK_NAME="" + +if [ "$PLATFORM_NAME" = "iphoneos" ]; then + PLATFORM_SUFFIX="-iphoneos" + SDK_NAME="iphoneos" +elif [ "$PLATFORM_NAME" = "iphonesimulator" ]; then + PLATFORM_SUFFIX="-iphonesimulator" + SDK_NAME="iphonesimulator" +elif [ "$PLATFORM_NAME" = "macosx" ]; then + PLATFORM_SUFFIX="" + SDK_NAME="macosx" +else + echo "Error: Unsupported platform '$PLATFORM_NAME'" >&2 + exit 1 +fi + +# Get the SDK path. This is crucial for cross-compilation. +SDK_PATH=$(xcrun --sdk ${SDK_NAME} --show-sdk-path) +echo "Configured for cross-compilation with SDK: ${SDK_PATH}" + +# Export variables needed by crates like `bindgen` to find the correct headers. +export BINDGEN_EXTRA_CLANG_ARGS="--sysroot=${SDK_PATH}" +export SDKROOT="${SDK_PATH}" # Also respected by some build scripts. + +STATIC_LIB_NAME="lib$(echo $CRATE_NAME | sed 's/-/_/g').a" +LIPO_INPUT_FILES="" + +# Loop through each architecture specified by Xcode. +for ARCH in $ARCHS; do + # Determine the Rust target triple based on the architecture and platform. + if [ "$PLATFORM_NAME" = "macosx" ]; then + if [ "$ARCH" = "arm64" ]; then + RUST_TARGET="aarch64-apple-darwin" + else + RUST_TARGET="x86_64-apple-darwin" + fi + elif [ "$PLATFORM_NAME" = "iphoneos" ]; then + RUST_TARGET="aarch64-apple-ios" + elif [ "$PLATFORM_NAME" = "iphonesimulator" ]; then + if [ "$ARCH" = "arm64" ]; then + RUST_TARGET="aarch64-apple-ios-sim" + else + RUST_TARGET="x86_64-apple-ios" + fi + fi + + echo "Building for arch: ${ARCH}, Rust target: ${RUST_TARGET}" + + # --- Configure Linker for Cargo --- + # Use RUSTFLAGS to pass linker arguments directly to rustc. This is the most + # reliable way to ensure the linker finds system libraries in the correct SDK. + export RUSTFLAGS="-C link-arg=-L${SDK_PATH}/usr/lib" + # export PATH="${SDK_PATH}:$PATH" + + # Run the cargo build command. It will inherit the exported RUSTFLAGS. + (cd "$RUST_PROJECT_PATH" && ${CARGO} build --release --target ${RUST_TARGET}) + + BUILT_LIB_PATH="${RUST_PROJECT_PATH}/../target/${RUST_TARGET}/release/${STATIC_LIB_NAME}" + + # Add the path of the built library to our list for `lipo`. + LIPO_INPUT_FILES="${LIPO_INPUT_FILES} ${BUILT_LIB_PATH}" +done + +# --- Universal Library Creation --- + +# Construct the correct, platform-specific destination directory. +DESTINATION_DIR="${BUILT_PRODUCTS_DIR}" +mkdir -p "${DESTINATION_DIR}" +DESTINATION_PATH="${DESTINATION_DIR}/${STATIC_LIB_NAME}" + +echo "Creating universal library for architectures: $ARCHS" +echo "Input files: ${LIPO_INPUT_FILES}" +echo "Output path: ${DESTINATION_PATH}" + +# Use `lipo` to combine the individual architecture libraries into one universal library. +lipo -create ${LIPO_INPUT_FILES} -output "${DESTINATION_PATH}" + +echo "Universal library created successfully." + +# Verify the architectures in the final library. +lipo -info "${DESTINATION_PATH}" + +echo "Rust build script finished successfully." diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index a17fde6..833c700 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -5,19 +5,28 @@ edition = "2024" [dependencies] -idevice = { path = "../idevice" } +idevice = { path = "../idevice", default-features = false } +futures = { version = "0.3", optional = true } log = "0.4.26" simplelog = "0.12.2" once_cell = "1.21.1" tokio = { version = "1.44.1", features = ["full"] } libc = "0.2.171" plist = "1.7.1" -plist_plus = { version = "0.2.6", features = ["dynamic"] } +plist_ffi = { version = "0.1.5" } +uuid = { version = "1.12", features = ["v4"], optional = true } + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.60", features = ["Win32_Networking_WinSock"] } [features] +aws-lc = ["idevice/aws-lc"] +ring = ["idevice/ring"] + + afc = ["idevice/afc"] amfi = ["idevice/amfi"] -core_device = ["idevice/core_device"] +core_device = ["idevice/core_device", "dep:futures", "dep:uuid"] core_device_proxy = ["idevice/core_device_proxy"] crashreportcopymobile = ["idevice/crashreportcopymobile"] debug_proxy = ["idevice/debug_proxy"] @@ -64,10 +73,11 @@ full = [ "springboardservices", "syslog_relay", ] -default = ["full"] +default = ["full", "aws-lc"] [build-dependencies] -cbindgen = "0.28.0" +cbindgen = "0.29.0" +ureq = "3" [lib] -crate-type = ["staticlib"] +crate-type = ["staticlib", "cdylib"] diff --git a/ffi/build.rs b/ffi/build.rs index b777e71..d26e3b8 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -1,21 +1,50 @@ // Jackson Coxson -use std::env; +use std::{env, fs::OpenOptions, io::Write}; + +const HEADER: &str = r#"// Jackson Coxson +// Bindings to idevice - https://github.com/jkcoxson/idevice + +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include + #include + typedef int idevice_socklen_t; + typedef struct sockaddr idevice_sockaddr; +#else + #include + #include + typedef socklen_t idevice_socklen_t; + typedef struct sockaddr idevice_sockaddr; +#endif +"#; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::Builder::new() .with_crate(crate_dir) - .with_header( - "// Jackson Coxson\n// Bindings to idevice - https://github.com/jkcoxson/idevice", - ) + .with_header(HEADER) .with_language(cbindgen::Language::C) - .with_sys_include("sys/socket.h") - .with_sys_include("plist/plist.h") + .with_include_guard("IDEVICE_H") + .exclude_item("idevice_socklen_t") + .exclude_item("idevice_sockaddr") .generate() .expect("Unable to generate bindings") .write_to_file("idevice.h"); - println!("cargo:rustc-link-arg=-lplist-2.0"); + // download plist.h + let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h") + .call() + .expect("failed to download plist.h"); + let h = h + .into_body() + .read_to_string() + .expect("failed to get string content"); + let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap(); + f.write_all(b"\n\n\n").unwrap(); + f.write_all(&h.into_bytes()) + .expect("failed to append plist.h"); } diff --git a/ffi/examples/CMakeLists.txt b/ffi/examples/CMakeLists.txt index 6dbf217..e9d5567 100644 --- a/ffi/examples/CMakeLists.txt +++ b/ffi/examples/CMakeLists.txt @@ -8,10 +8,13 @@ project(IdeviceFFI C) set(HEADER_FILE ${CMAKE_SOURCE_DIR}/../idevice.h) set(STATIC_LIB ${CMAKE_SOURCE_DIR}/../../target/release/libidevice_ffi.a) set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/../examples) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Find all C example files file(GLOB EXAMPLE_SOURCES ${EXAMPLES_DIR}/*.c) +find_package(PkgConfig REQUIRED) + # Create an executable for each example file foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) # Extract the filename without the path @@ -26,29 +29,9 @@ foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) # Link the static Rust library target_link_libraries(${EXAMPLE_NAME} PRIVATE ${STATIC_LIB}) - # libplist - - if( APPLE ) - # use static linking - find_library( LIBPLIST libplist-2.0.a REQUIRED ) - message( STATUS "(Static linking) LIBPLIST " ${LIBPLIST} ) - target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) - elseif( WIN32) - pkg_search_module(PLIST REQUIRED libplist-2.0) - find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) - target_link_libraries ( ${EXAMPLE_NAME} PRIVATE ${LIBPLIST} ) - else () - pkg_search_module(PLIST libplist>=2.0) - if(NOT PLIST_FOUND) - pkg_search_module(PLIST REQUIRED libplist-2.0) - endif() - find_library( LIBPLIST ${PLIST_LIBRARIES} PATH ${PLIST_LIBDIR} ) - target_link_libraries ( ${EXAMPLE_NAME} PUBLIC ${LIBPLIST} ) + if(UNIX AND NOT APPLE) + target_link_libraries(${EXAMPLE_NAME} PRIVATE m) endif() - if ( PLIST_FOUND ) - message( STATUS "found libplist-${PLIST_VERSION}" ) - endif() - target_include_directories( ${EXAMPLE_NAME} PRIVATE ${PLIST_INCLUDE_DIRS} ) # Bulk-link common macOS system frameworks if(APPLE) diff --git a/ffi/examples/heartbeat.c b/ffi/examples/heartbeat.c index bda431b..7f71d3a 100644 --- a/ffi/examples/heartbeat.c +++ b/ffi/examples/heartbeat.c @@ -5,7 +5,6 @@ #include #include #include -#include int main() { // Initialize logger diff --git a/ffi/examples/lockdownd.c b/ffi/examples/lockdownd.c index 8c2d678..7b98775 100644 --- a/ffi/examples/lockdownd.c +++ b/ffi/examples/lockdownd.c @@ -1,7 +1,6 @@ // Jackson Coxson #include "idevice.h" -#include "plist/plist.h" #include #include #include @@ -122,7 +121,7 @@ int main() { // Get all values plist_t all_values = NULL; - err = lockdownd_get_all_values(client, &all_values); + err = lockdownd_get_value(client, NULL, NULL, &all_values); if (err != NULL) { fprintf(stderr, "Failed to get all values: [%d] %s", err->code, err->message); diff --git a/ffi/examples/mounter.c b/ffi/examples/mounter.c index 17870e9..79bc5fd 100644 --- a/ffi/examples/mounter.c +++ b/ffi/examples/mounter.c @@ -1,7 +1,6 @@ // Jackson Coxson #include "idevice.h" -#include "plist/plist.h" #include #include #include diff --git a/ffi/libplist b/ffi/libplist deleted file mode 160000 index cf5897a..0000000 --- a/ffi/libplist +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cf5897a71ea412ea2aeb1e2f6b5ea74d4fabfd8c diff --git a/ffi/src/adapter.rs b/ffi/src/adapter.rs index 8e6edb8..6c466b4 100644 --- a/ffi/src/adapter.rs +++ b/ffi/src/adapter.rs @@ -3,12 +3,13 @@ use std::ffi::{CStr, c_char}; use std::ptr::null_mut; -use idevice::tcp::stream::AdapterStream; +use idevice::tcp::handle::StreamHandle; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::core_device_proxy::AdapterHandle; use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; -pub struct AdapterStreamHandle<'a>(pub AdapterStream<'a>); +pub struct AdapterStreamHandle(pub StreamHandle); /// Connects the adapter to a specific port /// @@ -35,7 +36,7 @@ pub unsafe extern "C" fn adapter_connect( } let adapter = unsafe { &mut (*adapter_handle).0 }; - let res = RUNTIME.block_on(async move { AdapterStream::connect(adapter, port).await }); + let res = RUNTIME.block_on(async move { adapter.connect(port).await }); match res { Ok(r) => { @@ -46,7 +47,7 @@ pub unsafe extern "C" fn adapter_connect( null_mut() } Err(e) => { - log::error!("Adapter connect failed: {}", e); + log::error!("Adapter connect failed: {e}"); ffi_err!(e) } } @@ -85,13 +86,13 @@ pub unsafe extern "C" fn adapter_pcap( match res { Ok(_) => null_mut(), Err(e) => { - log::error!("Adapter pcap failed: {}", e); + log::error!("Adapter pcap failed: {e}"); ffi_err!(e) } } } -/// Closes the adapter connection +/// Closes the adapter stream connection /// /// # Arguments /// * [`handle`] - The adapter stream handle @@ -108,21 +109,15 @@ pub unsafe extern "C" fn adapter_close(handle: *mut AdapterStreamHandle) -> *mut } let adapter = unsafe { &mut (*handle).0 }; - let res = RUNTIME.block_on(async move { adapter.close().await }); + RUNTIME.block_on(async move { adapter.close() }); - match res { - Ok(_) => null_mut(), - Err(e) => { - log::error!("Adapter close failed: {}", e); - ffi_err!(e) - } - } + null_mut() } -/// Sends data through the adapter +/// Sends data through the adapter stream /// /// # Arguments -/// * [`handle`] - The adapter handle +/// * [`handle`] - The adapter stream handle /// * [`data`] - The data to send /// * [`length`] - The length of the data /// @@ -145,21 +140,21 @@ pub unsafe extern "C" fn adapter_send( let adapter = unsafe { &mut (*handle).0 }; let data_slice = unsafe { std::slice::from_raw_parts(data, length) }; - let res = RUNTIME.block_on(async move { adapter.psh(data_slice).await }); + let res = RUNTIME.block_on(async move { adapter.write_all(data_slice).await }); match res { Ok(_) => null_mut(), Err(e) => { - log::error!("Adapter send failed: {}", e); + log::error!("Adapter send failed: {e}"); ffi_err!(e) } } } -/// Receives data from the adapter +/// Receives data from the adapter stream /// /// # Arguments -/// * [`handle`] - The adapter handle +/// * [`handle`] - The adapter stream handle /// * [`data`] - Pointer to a buffer where the received data will be stored /// * [`length`] - Pointer to store the actual length of received data /// * [`max_length`] - Maximum number of bytes that can be stored in `data` @@ -183,7 +178,11 @@ pub unsafe extern "C" fn adapter_recv( } let adapter = unsafe { &mut (*handle).0 }; - let res = RUNTIME.block_on(async move { adapter.recv().await }); + let res: Result, std::io::Error> = RUNTIME.block_on(async move { + let mut buf = [0; 2048]; + let res = adapter.read(&mut buf).await?; + Ok(buf[..res].to_vec()) + }); match res { Ok(received_data) => { @@ -200,7 +199,7 @@ pub unsafe extern "C" fn adapter_recv( null_mut() } Err(e) => { - log::error!("Adapter recv failed: {}", e); + log::error!("Adapter recv failed: {e}"); ffi_err!(e) } } diff --git a/ffi/src/core_device/app_service.rs b/ffi/src/core_device/app_service.rs index a877485..6078635 100644 --- a/ffi/src/core_device/app_service.rs +++ b/ffi/src/core_device/app_service.rs @@ -5,12 +5,11 @@ use std::os::raw::{c_float, c_int}; use std::ptr::{self, null_mut}; use idevice::core_device::AppServiceClient; -use idevice::tcp::stream::AdapterStream; use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; -use crate::{IdeviceFfiError, RUNTIME, ffi_err}; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to an AppServiceClient pub struct AppServiceHandle(pub AppServiceClient>); @@ -91,16 +90,17 @@ pub unsafe extern "C" fn app_service_connect_rsd( return ffi_err!(IdeviceError::FfiInvalidArg); } - let res: Result, IdeviceError> = RUNTIME.block_on(async move { - let provider_ref = unsafe { &mut (*provider).0 }; - let handshake_ref = unsafe { &mut (*handshake).0 }; + let res: Result>, IdeviceError> = + RUNTIME.block_on(async move { + let provider_ref = unsafe { &mut (*provider).0 }; + let handshake_ref = unsafe { &mut (*handshake).0 }; - AppServiceClient::connect_rsd(provider_ref, handshake_ref).await - }); + AppServiceClient::connect_rsd(provider_ref, handshake_ref).await + }); match res { Ok(client) => { - let boxed = Box::new(AppServiceHandle(client.box_inner())); + let boxed = Box::new(AppServiceHandle(client)); unsafe { *handle = Box::into_raw(boxed) }; null_mut() } @@ -122,7 +122,7 @@ pub unsafe extern "C" fn app_service_connect_rsd( /// `handle` must be a valid pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn app_service_new( - socket: *mut Box, + socket: *mut ReadWriteOpaque, handle: *mut *mut AppServiceHandle, ) -> *mut IdeviceFfiError { if socket.is_null() || handle.is_null() { @@ -130,7 +130,7 @@ pub unsafe extern "C" fn app_service_new( } let socket = unsafe { Box::from_raw(socket) }; - let res = RUNTIME.block_on(async move { AppServiceClient::new(*socket).await }); + let res = RUNTIME.block_on(async move { AppServiceClient::new(socket.inner.unwrap()).await }); match res { Ok(client) => { @@ -299,6 +299,7 @@ pub unsafe extern "C" fn app_service_free_app_list(apps: *mut AppListEntryC, cou /// * [`argc`] - Number of arguments /// * [`kill_existing`] - Whether to kill existing instances /// * [`start_suspended`] - Whether to start suspended +/// * [`stdio_uuid`] - The UUID received from openstdiosocket, null for none /// * [`response`] - Pointer to store the launch response (caller must free) /// /// # Returns @@ -314,6 +315,7 @@ pub unsafe extern "C" fn app_service_launch_app( argc: usize, kill_existing: c_int, start_suspended: c_int, + stdio_uuid: *const u8, response: *mut *mut LaunchResponseC, ) -> *mut IdeviceFfiError { if handle.is_null() || bundle_id.is_null() || response.is_null() { @@ -329,14 +331,21 @@ pub unsafe extern "C" fn app_service_launch_app( if !argv.is_null() && argc > 0 { let argv_slice = unsafe { std::slice::from_raw_parts(argv, argc) }; for &arg in argv_slice { - if !arg.is_null() { - if let Ok(arg_str) = unsafe { CStr::from_ptr(arg) }.to_str() { - args.push(arg_str); - } + if !arg.is_null() + && let Ok(arg_str) = unsafe { CStr::from_ptr(arg) }.to_str() + { + args.push(arg_str); } } } + let stdio_uuid = if stdio_uuid.is_null() { + None + } else { + let stdio_uuid = unsafe { std::slice::from_raw_parts(stdio_uuid, 16) }; + Some(uuid::Uuid::from_bytes(stdio_uuid.try_into().unwrap())) + }; + let client = unsafe { &mut (*handle).0 }; let res = RUNTIME.block_on(async move { client @@ -347,6 +356,7 @@ pub unsafe extern "C" fn app_service_launch_app( start_suspended != 0, None, // environment None, // platform_options + stdio_uuid, ) .await }); diff --git a/ffi/src/core_device/diagnosticsservice.rs b/ffi/src/core_device/diagnosticsservice.rs new file mode 100644 index 0000000..31ca02a --- /dev/null +++ b/ffi/src/core_device/diagnosticsservice.rs @@ -0,0 +1,215 @@ +// Jackson Coxson + +use std::ffi::{CString, c_char}; +use std::pin::Pin; +use std::ptr::null_mut; + +use futures::{Stream, StreamExt}; +use idevice::core_device::DiagnostisServiceClient; +use idevice::{IdeviceError, ReadWrite, RsdService}; +use log::debug; + +use crate::core_device_proxy::AdapterHandle; +use crate::rsd::RsdHandshakeHandle; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; + +/// Opaque handle to an AppServiceClient +pub struct DiagnosticsServiceHandle(pub DiagnostisServiceClient>); +pub struct SysdiagnoseStreamHandle<'a>( + pub Pin, IdeviceError>> + 'a>>, +); + +/// Creates a new DiagnosticsServiceClient using RSD connection +/// +/// # Arguments +/// * [`provider`] - An adapter created by this library +/// * [`handshake`] - An RSD handshake from the same provider +/// * [`handle`] - Pointer to store the newly created handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` and `handshake` must be valid pointers to handles allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_connect_rsd( + provider: *mut AdapterHandle, + handshake: *mut RsdHandshakeHandle, + handle: *mut *mut DiagnosticsServiceHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || handshake.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result>, IdeviceError> = + RUNTIME.block_on(async move { + let provider_ref = unsafe { &mut (*provider).0 }; + let handshake_ref = unsafe { &mut (*handshake).0 }; + debug!( + "Connecting to DiagnosticsService: provider {provider_ref:?}, handshake: {:?}", + handshake_ref.uuid + ); + + DiagnostisServiceClient::connect_rsd(provider_ref, handshake_ref).await + }); + + match res { + Ok(client) => { + debug!("Connected to DiagnosticsService"); + let boxed = Box::new(DiagnosticsServiceHandle(client)); + unsafe { *handle = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Creates a new DiagnostisServiceClient from a socket +/// +/// # Arguments +/// * [`socket`] - The socket to use for communication +/// * [`handle`] - Pointer to store the newly created handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library +/// `handle` must be a valid pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_new( + socket: *mut ReadWriteOpaque, + handle: *mut *mut DiagnosticsServiceHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let socket = unsafe { Box::from_raw(socket) }; + let res = RUNTIME + .block_on(async move { DiagnostisServiceClient::from_stream(socket.inner.unwrap()).await }); + + match res { + Ok(client) => { + let new_handle = DiagnosticsServiceHandle(client); + unsafe { *handle = Box::into_raw(Box::new(new_handle)) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Captures a sysdiagnose from the device. +/// Note that this will take a LONG time to return while the device collects enough information to +/// return to the service. This function returns a stream that can be called on to get the next +/// chunk of data. A typical sysdiagnose is roughly 1-2 GB. +/// +/// # Arguments +/// * [`handle`] - The handle to the client +/// * [`dry_run`] - Whether or not to do a dry run with a simple .txt file from the device +/// * [`preferred_filename`] - The name the device wants to save the sysdaignose as +/// * [`expected_length`] - The size in bytes of the sysdiagnose +/// * [`stream_handle`] - The handle that will be set to capture bytes for +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// Pointers must be all valid. Handle must be allocated by this library. Preferred filename must +/// be freed `idevice_string_free`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_capture_sysdiagnose( + handle: *mut DiagnosticsServiceHandle, + dry_run: bool, + preferred_filename: *mut *mut c_char, + expected_length: *mut usize, + stream_handle: *mut *mut SysdiagnoseStreamHandle, +) -> *mut IdeviceFfiError { + if handle.is_null() + || preferred_filename.is_null() + || expected_length.is_null() + || stream_handle.is_null() + { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let handle = unsafe { &mut *handle }; + let res = RUNTIME.block_on(async move { handle.0.capture_sysdiagnose(dry_run).await }); + match res { + Ok(res) => { + let filename = CString::new(res.preferred_filename).unwrap(); + unsafe { + *preferred_filename = filename.into_raw(); + *expected_length = res.expected_length; + *stream_handle = Box::into_raw(Box::new(SysdiagnoseStreamHandle(res.stream))); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Gets the next packet from the stream. +/// Data will be set to 0 when there is no more data to get from the stream. +/// +/// # Arguments +/// * [`handle`] - The handle to the stream +/// * [`data`] - A pointer to the bytes +/// * [`len`] - The length of the bytes written +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// Pass valid pointers. The handle must be allocated by this library. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sysdiagnose_stream_next( + handle: *mut SysdiagnoseStreamHandle, + data: *mut *mut u8, + len: *mut usize, +) -> *mut IdeviceFfiError { + if handle.is_null() || data.is_null() || len.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let handle = unsafe { &mut *handle }; + let res = RUNTIME.block_on(async move { handle.0.next().await }); + match res { + Some(Ok(res)) => { + let mut res = res.into_boxed_slice(); + unsafe { + *len = res.len(); + *data = res.as_mut_ptr(); + } + std::mem::forget(res); + null_mut() + } + Some(Err(e)) => ffi_err!(e), + None => { + // we're empty + unsafe { *data = null_mut() }; + null_mut() + } + } +} + +/// Frees a DiagnostisServiceClient handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diagnostics_service_free(handle: *mut DiagnosticsServiceHandle) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} + +/// Frees a SysdiagnoseStreamHandle handle +/// +/// # Safety +/// `handle` must be a valid pointer to a handle allocated by this library or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sysdiagnose_stream_free(handle: *mut SysdiagnoseStreamHandle) { + if !handle.is_null() { + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/core_device/mod.rs b/ffi/src/core_device/mod.rs index 3eb1865..409a2df 100644 --- a/ffi/src/core_device/mod.rs +++ b/ffi/src/core_device/mod.rs @@ -1,3 +1,4 @@ // Jackson Coxson pub mod app_service; +pub mod diagnosticsservice; diff --git a/ffi/src/core_device_proxy.rs b/ffi/src/core_device_proxy.rs index 2d9960e..c3928db 100644 --- a/ffi/src/core_device_proxy.rs +++ b/ffi/src/core_device_proxy.rs @@ -7,13 +7,12 @@ use std::{ use idevice::{ IdeviceError, IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, - tcp::adapter::Adapter, }; use crate::{IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle}; pub struct CoreDeviceProxyHandle(pub CoreDeviceProxy); -pub struct AdapterHandle(pub Adapter); +pub struct AdapterHandle(pub idevice::tcp::handle::AdapterHandle); /// Automatically creates and connects to Core Device Proxy, returning a client handle /// @@ -312,7 +311,10 @@ pub unsafe extern "C" fn core_device_proxy_create_tcp_adapter( match result { Ok(adapter_obj) => { - let boxed = Box::new(AdapterHandle(adapter_obj)); + // We have to run this in the RUNTIME since we're spawning a new thread + let adapter_handle = RUNTIME.block_on(async move { adapter_obj.to_async_handle() }); + + let boxed = Box::new(AdapterHandle(adapter_handle)); unsafe { *adapter = Box::into_raw(boxed) }; null_mut() } diff --git a/ffi/src/debug_proxy.rs b/ffi/src/debug_proxy.rs index 1ab061e..8d7152a 100644 --- a/ffi/src/debug_proxy.rs +++ b/ffi/src/debug_proxy.rs @@ -5,12 +5,11 @@ use std::os::raw::c_int; use std::ptr::{self, null_mut}; use idevice::debug_proxy::{DebugProxyClient, DebugserverCommand}; -use idevice::tcp::stream::AdapterStream; use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; -use crate::{IdeviceFfiError, RUNTIME, ffi_err}; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to a DebugProxyClient pub struct DebugProxyHandle(pub DebugProxyClient>); @@ -136,13 +135,14 @@ pub unsafe extern "C" fn debug_proxy_connect_rsd( if provider.is_null() || handshake.is_null() || handshake.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } - let res: Result, IdeviceError> = RUNTIME.block_on(async move { - let provider_ref = unsafe { &mut (*provider).0 }; - let handshake_ref = unsafe { &mut (*handshake).0 }; + let res: Result>, IdeviceError> = + RUNTIME.block_on(async move { + let provider_ref = unsafe { &mut (*provider).0 }; + let handshake_ref = unsafe { &mut (*handshake).0 }; - // Connect using the reference - DebugProxyClient::connect_rsd(provider_ref, handshake_ref).await - }); + // Connect using the reference + DebugProxyClient::connect_rsd(provider_ref, handshake_ref).await + }); match res { Ok(d) => { @@ -170,7 +170,7 @@ pub unsafe extern "C" fn debug_proxy_connect_rsd( /// `handle` must be a valid pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn debug_proxy_new( - socket: *mut Box, + socket: *mut ReadWriteOpaque, handle: *mut *mut DebugProxyHandle, ) -> *mut IdeviceFfiError { if socket.is_null() || handle.is_null() { @@ -178,7 +178,7 @@ pub unsafe extern "C" fn debug_proxy_new( } let socket = unsafe { Box::from_raw(socket) }; - let client = DebugProxyClient::new(*socket); + let client = DebugProxyClient::new(socket.inner.unwrap()); let new_handle = DebugProxyHandle(client); unsafe { *handle = Box::into_raw(Box::new(new_handle)) }; diff --git a/ffi/src/installation_proxy.rs b/ffi/src/installation_proxy.rs index e3c4c65..a749ea4 100644 --- a/ffi/src/installation_proxy.rs +++ b/ffi/src/installation_proxy.rs @@ -6,10 +6,9 @@ use idevice::{ IdeviceError, IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider, }; +use plist_ffi::{PlistWrapper, plist_t}; -use crate::{ - IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle, util, -}; +use crate::{IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle}; pub struct InstallationProxyClientHandle(pub InstallationProxyClient); @@ -110,11 +109,14 @@ pub unsafe extern "C" fn installation_proxy_get_apps( let app_type = if application_type.is_null() { None } else { - Some(unsafe { - std::ffi::CStr::from_ptr(application_type) - .to_string_lossy() - .into_owned() - }) + Some( + match unsafe { std::ffi::CStr::from_ptr(application_type) }.to_str() { + Ok(a) => a, + Err(_) => { + return ffi_err!(IdeviceError::InvalidCString); + } + }, + ) }; let bundle_ids = if bundle_identifiers.is_null() { @@ -126,16 +128,16 @@ pub unsafe extern "C" fn installation_proxy_get_apps( .map(|&s| { unsafe { std::ffi::CStr::from_ptr(s) } .to_string_lossy() - .into_owned() + .to_string() }) - .collect(), + .collect::>(), ) }; - let res: Result, IdeviceError> = RUNTIME.block_on(async { + let res: Result, IdeviceError> = RUNTIME.block_on(async { client.0.get_apps(app_type, bundle_ids).await.map(|apps| { apps.into_values() - .map(|v| util::plist_to_libplist(&v)) + .map(|v| PlistWrapper::new_node(v).into_ptr()) .collect() }) }); @@ -192,7 +194,7 @@ pub unsafe extern "C" fn installation_proxy_client_free( pub unsafe extern "C" fn installation_proxy_install( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, ) -> *mut IdeviceFfiError { if client.is_null() || package_path.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -204,8 +206,9 @@ pub unsafe extern "C" fn installation_proxy_install( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -240,7 +243,7 @@ pub unsafe extern "C" fn installation_proxy_install( pub unsafe extern "C" fn installation_proxy_install_with_callback( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, callback: extern "C" fn(progress: u64, context: *mut c_void), context: *mut c_void, ) -> *mut IdeviceFfiError { @@ -254,8 +257,9 @@ pub unsafe extern "C" fn installation_proxy_install_with_callback( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { let callback_wrapper = |(progress, context)| async move { @@ -292,7 +296,7 @@ pub unsafe extern "C" fn installation_proxy_install_with_callback( pub unsafe extern "C" fn installation_proxy_upgrade( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, ) -> *mut IdeviceFfiError { if client.is_null() || package_path.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -304,8 +308,9 @@ pub unsafe extern "C" fn installation_proxy_upgrade( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -340,7 +345,7 @@ pub unsafe extern "C" fn installation_proxy_upgrade( pub unsafe extern "C" fn installation_proxy_upgrade_with_callback( client: *mut InstallationProxyClientHandle, package_path: *const libc::c_char, - options: *mut c_void, + options: plist_t, callback: extern "C" fn(progress: u64, context: *mut c_void), context: *mut c_void, ) -> *mut IdeviceFfiError { @@ -354,8 +359,9 @@ pub unsafe extern "C" fn installation_proxy_upgrade_with_callback( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { let callback_wrapper = |(progress, context)| async move { @@ -392,7 +398,7 @@ pub unsafe extern "C" fn installation_proxy_upgrade_with_callback( pub unsafe extern "C" fn installation_proxy_uninstall( client: *mut InstallationProxyClientHandle, bundle_id: *const libc::c_char, - options: *mut c_void, + options: plist_t, ) -> *mut IdeviceFfiError { if client.is_null() || bundle_id.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -404,8 +410,9 @@ pub unsafe extern "C" fn installation_proxy_uninstall( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -440,7 +447,7 @@ pub unsafe extern "C" fn installation_proxy_uninstall( pub unsafe extern "C" fn installation_proxy_uninstall_with_callback( client: *mut InstallationProxyClientHandle, bundle_id: *const libc::c_char, - options: *mut c_void, + options: plist_t, callback: extern "C" fn(progress: u64, context: *mut c_void), context: *mut c_void, ) -> *mut IdeviceFfiError { @@ -454,8 +461,9 @@ pub unsafe extern "C" fn installation_proxy_uninstall_with_callback( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { let callback_wrapper = |(progress, context)| async move { @@ -494,9 +502,9 @@ pub unsafe extern "C" fn installation_proxy_uninstall_with_callback( #[unsafe(no_mangle)] pub unsafe extern "C" fn installation_proxy_check_capabilities_match( client: *mut InstallationProxyClientHandle, - capabilities: *const *mut c_void, + capabilities: *const plist_t, capabilities_len: libc::size_t, - options: *mut c_void, + options: plist_t, out_result: *mut bool, ) -> *mut IdeviceFfiError { if client.is_null() || out_result.is_null() { @@ -508,15 +516,16 @@ pub unsafe extern "C" fn installation_proxy_check_capabilities_match( } else { unsafe { std::slice::from_raw_parts(capabilities, capabilities_len) } .iter() - .map(|&ptr| util::libplist_to_plist(ptr)) + .map(|ptr| unsafe { &mut **ptr }.borrow_self().clone()) .collect() }; let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); let res = RUNTIME.block_on(async { unsafe { &mut *client } @@ -553,8 +562,8 @@ pub unsafe extern "C" fn installation_proxy_check_capabilities_match( #[unsafe(no_mangle)] pub unsafe extern "C" fn installation_proxy_browse( client: *mut InstallationProxyClientHandle, - options: *mut c_void, - out_result: *mut *mut c_void, + options: plist_t, + out_result: *mut *mut plist_t, out_result_len: *mut libc::size_t, ) -> *mut IdeviceFfiError { if client.is_null() || out_result.is_null() || out_result_len.is_null() { @@ -564,25 +573,27 @@ pub unsafe extern "C" fn installation_proxy_browse( let options = if options.is_null() { None } else { - Some(util::libplist_to_plist(options)) - }; + Some(unsafe { &mut *options }) + } + .map(|x| x.borrow_self().clone()); - let res: Result, IdeviceError> = RUNTIME.block_on(async { + let res: Result, IdeviceError> = RUNTIME.block_on(async { unsafe { &mut *client }.0.browse(options).await.map(|apps| { apps.into_iter() - .map(|v| util::plist_to_libplist(&v)) + .map(|v| PlistWrapper::new_node(v).into_ptr()) .collect() }) }); match res { - Ok(mut r) => { + Ok(r) => { + let mut r = r.into_boxed_slice(); let ptr = r.as_mut_ptr(); let len = r.len(); std::mem::forget(r); unsafe { - *out_result = ptr as *mut c_void; + *out_result = ptr; *out_result_len = len; } null_mut() diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 04ed09f..f5738ab 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -39,6 +39,8 @@ pub mod rsd; pub mod springboardservices; #[cfg(feature = "syslog_relay")] pub mod syslog_relay; +#[cfg(feature = "tunnel_tcp_stack")] +pub mod tcp_object_stack; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; pub mod util; @@ -54,6 +56,9 @@ use std::{ }; use tokio::runtime::{self, Runtime}; +#[cfg(unix)] +use crate::util::{idevice_sockaddr, idevice_socklen_t}; + static RUNTIME: Lazy = Lazy::new(|| { runtime::Builder::new_multi_thread() .enable_io() @@ -73,6 +78,10 @@ pub struct ReadWriteOpaque { pub struct IdeviceHandle(pub Idevice); pub struct IdeviceSocketHandle(IdeviceSocket); +/// Stub to avoid header problems +#[allow(non_camel_case_types)] +pub type plist_t = *mut std::ffi::c_void; + // https://github.com/mozilla/cbindgen/issues/539 #[allow(non_camel_case_types, unused)] struct sockaddr; @@ -132,22 +141,25 @@ pub unsafe extern "C" fn idevice_new( /// `addr` must be a valid sockaddr /// `label` must be a valid null-terminated C string /// `idevice` must be a valid, non-null pointer to a location where the handle will be stored +#[cfg(unix)] #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_new_tcp_socket( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const idevice_sockaddr, + addr_len: idevice_socklen_t, label: *const c_char, idevice: *mut *mut IdeviceHandle, ) -> *mut IdeviceFfiError { - if addr.is_null() { - log::error!("socket addr null pointer"); + use crate::util::SockAddr; + + if addr.is_null() || label.is_null() || idevice.is_null() { + log::error!("null pointer(s) to idevice_new_tcp_socket"); return ffi_err!(IdeviceError::FfiInvalidArg); } + let addr = addr as *const SockAddr; - // Convert C string to Rust string let label = match unsafe { CStr::from_ptr(label).to_str() } { Ok(s) => s, - Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), }; let addr = match util::c_socket_to_rust(addr, addr_len) { @@ -155,9 +167,10 @@ pub unsafe extern "C" fn idevice_new_tcp_socket( Err(e) => return ffi_err!(e), }; - let device: Result = RUNTIME.block_on(async move { - Ok(idevice::Idevice::new( - Box::new(tokio::net::TcpStream::connect(addr).await?), + let device = RUNTIME.block_on(async move { + let stream = tokio::net::TcpStream::connect(addr).await?; + Ok::(idevice::Idevice::new( + Box::new(stream), label, )) }); @@ -166,7 +179,7 @@ pub unsafe extern "C" fn idevice_new_tcp_socket( Ok(dev) => { let boxed = Box::new(IdeviceHandle(dev)); unsafe { *idevice = Box::into_raw(boxed) }; - null_mut() + std::ptr::null_mut() } Err(e) => ffi_err!(e), } @@ -304,3 +317,18 @@ pub unsafe extern "C" fn idevice_string_free(string: *mut c_char) { let _ = unsafe { CString::from_raw(string) }; } } + +/// Frees data allocated by this library +/// +/// # Arguments +/// * [`data`] - The data to free +/// +/// # Safety +/// `data` must be a valid pointer to data that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_data_free(data: *mut u8, len: usize) { + if !data.is_null() { + let _ = unsafe { std::slice::from_raw_parts(data, len) }; + } +} diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 02c2cd1..a6f8959 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -1,8 +1,9 @@ // Jackson Coxson -use std::{ffi::c_void, ptr::null_mut}; +use std::ptr::null_mut; use idevice::{IdeviceError, IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use plist_ffi::plist_t; use crate::{ IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, @@ -11,7 +12,7 @@ use crate::{ pub struct LockdowndClientHandle(pub LockdownClient); -/// Connects to lockdownd service using TCP provider +/// Connects to lockdownd service using provider /// /// # Arguments /// * [`provider`] - An IdeviceProvider @@ -176,24 +177,32 @@ pub unsafe extern "C" fn lockdownd_get_value( client: *mut LockdowndClientHandle, key: *const libc::c_char, domain: *const libc::c_char, - out_plist: *mut *mut c_void, + out_plist: *mut plist_t, ) -> *mut IdeviceFfiError { - if key.is_null() || out_plist.is_null() { + if out_plist.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } - let value = unsafe { std::ffi::CStr::from_ptr(key) } - .to_string_lossy() - .into_owned(); + let value = if key.is_null() { + None + } else { + Some(match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() { + Ok(v) => v, + Err(_) => { + return ffi_err!(IdeviceError::InvalidCString); + } + }) + }; let domain = if domain.is_null() { None } else { - Some( - unsafe { std::ffi::CStr::from_ptr(domain) } - .to_string_lossy() - .into_owned(), - ) + Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() { + Ok(v) => v, + Err(_) => { + return ffi_err!(IdeviceError::InvalidCString); + } + }) }; let res: Result = RUNTIME.block_on(async move { @@ -204,55 +213,7 @@ pub unsafe extern "C" fn lockdownd_get_value( match res { Ok(value) => { unsafe { - *out_plist = crate::util::plist_to_libplist(&value); - } - null_mut() - } - Err(e) => ffi_err!(e), - } -} - -/// Gets all values from lockdownd -/// -/// # Arguments -/// * `client` - A valid LockdowndClient handle -/// * `out_plist` - Pointer to store the returned plist dictionary -/// -/// # Returns -/// An IdeviceFfiError on error, null on success -/// -/// # Safety -/// `client` must be a valid pointer to a handle allocated by this library -/// `out_plist` must be a valid pointer to store the plist -#[unsafe(no_mangle)] -pub unsafe extern "C" fn lockdownd_get_all_values( - client: *mut LockdowndClientHandle, - domain: *const libc::c_char, - out_plist: *mut *mut c_void, -) -> *mut IdeviceFfiError { - if out_plist.is_null() { - return ffi_err!(IdeviceError::FfiInvalidArg); - } - - let domain = if domain.is_null() { - None - } else { - Some( - unsafe { std::ffi::CStr::from_ptr(domain) } - .to_string_lossy() - .into_owned(), - ) - }; - - let res: Result = RUNTIME.block_on(async move { - let client_ref = unsafe { &mut (*client).0 }; - client_ref.get_all_values(domain).await - }); - - match res { - Ok(dict) => { - unsafe { - *out_plist = crate::util::plist_to_libplist(&plist::Value::Dictionary(dict)); + *out_plist = plist_ffi::PlistWrapper::new_node(value).into_ptr(); } null_mut() } diff --git a/ffi/src/mobile_image_mounter.rs b/ffi/src/mobile_image_mounter.rs index 9a4c797..a539509 100644 --- a/ffi/src/mobile_image_mounter.rs +++ b/ffi/src/mobile_image_mounter.rs @@ -6,10 +6,9 @@ use idevice::{ IdeviceError, IdeviceService, mobile_image_mounter::ImageMounter, provider::IdeviceProvider, }; use plist::Value; +use plist_ffi::{PlistWrapper, plist_t}; -use crate::{ - IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle, util, -}; +use crate::{IdeviceFfiError, IdeviceHandle, RUNTIME, ffi_err, provider::IdeviceProviderHandle}; pub struct ImageMounterHandle(pub ImageMounter); @@ -112,7 +111,7 @@ pub unsafe extern "C" fn image_mounter_free(handle: *mut ImageMounterHandle) { #[unsafe(no_mangle)] pub unsafe extern "C" fn image_mounter_copy_devices( client: *mut ImageMounterHandle, - devices: *mut *mut c_void, + devices: *mut *mut plist_t, devices_len: *mut libc::size_t, ) -> *mut IdeviceFfiError { let res: Result, IdeviceError> = RUNTIME.block_on(async move { @@ -124,14 +123,14 @@ pub unsafe extern "C" fn image_mounter_copy_devices( Ok(devices_list) => { let devices_list = devices_list .into_iter() - .map(|x| util::plist_to_libplist(&x)) - .collect::>(); + .map(|x| plist_ffi::PlistWrapper::new_node(x).into_ptr()) + .collect::>(); let len = devices_list.len(); let boxed_slice = devices_list.into_boxed_slice(); let ptr = Box::leak(boxed_slice).as_mut_ptr(); unsafe { - *devices = ptr as *mut c_void; + *devices = ptr as *mut plist_t; *devices_len = len; } null_mut() @@ -515,7 +514,7 @@ pub unsafe extern "C" fn image_mounter_query_nonce( let image_type = if !personalized_image_type.is_null() { let image_type_cstr = unsafe { std::ffi::CStr::from_ptr(personalized_image_type) }; match image_type_cstr.to_str() { - Ok(s) => Some(s.to_string()), + Ok(s) => Some(s), Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), } } else { @@ -558,7 +557,7 @@ pub unsafe extern "C" fn image_mounter_query_nonce( pub unsafe extern "C" fn image_mounter_query_personalization_identifiers( client: *mut ImageMounterHandle, image_type: *const libc::c_char, - identifiers: *mut *mut c_void, + identifiers: *mut plist_t, ) -> *mut IdeviceFfiError { if identifiers.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); @@ -567,7 +566,7 @@ pub unsafe extern "C" fn image_mounter_query_personalization_identifiers( let image_type = if !image_type.is_null() { let image_type_cstr = unsafe { std::ffi::CStr::from_ptr(image_type) }; match image_type_cstr.to_str() { - Ok(s) => Some(s.to_string()), + Ok(s) => Some(s), Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), } } else { @@ -583,7 +582,7 @@ pub unsafe extern "C" fn image_mounter_query_personalization_identifiers( match res { Ok(id) => { - let plist = util::plist_to_libplist(&plist::Value::Dictionary(id)); + let plist = PlistWrapper::new_node(Value::Dictionary(id)).into_ptr(); unsafe { *identifiers = plist }; null_mut() } diff --git a/ffi/src/process_control.rs b/ffi/src/process_control.rs index 2ed84b9..b771125 100644 --- a/ffi/src/process_control.rs +++ b/ffi/src/process_control.rs @@ -105,10 +105,10 @@ pub unsafe extern "C" fn process_control_launch_app( for &env_var in env_vars_slice { if !env_var.is_null() { let env_var = unsafe { CStr::from_ptr(env_var) }; - if let Ok(env_var) = env_var.to_str() { - if let Some((key, value)) = env_var.split_once('=') { - env_dict.insert(key.to_string(), Value::String(value.to_string())); - } + if let Ok(env_var) = env_var.to_str() + && let Some((key, value)) = env_var.split_once('=') + { + env_dict.insert(key.to_string(), Value::String(value.to_string())); } } } diff --git a/ffi/src/provider.rs b/ffi/src/provider.rs index f6cb00a..4ef967c 100644 --- a/ffi/src/provider.rs +++ b/ffi/src/provider.rs @@ -1,10 +1,13 @@ // Jackson Coxson use idevice::provider::{IdeviceProvider, TcpProvider, UsbmuxdProvider}; +use std::net::IpAddr; use std::os::raw::c_char; use std::{ffi::CStr, ptr::null_mut}; +use crate::util::{SockAddr, idevice_sockaddr}; use crate::{IdeviceFfiError, ffi_err, usbmuxd::UsbmuxdAddrHandle, util}; +use crate::{IdevicePairingFile, RUNTIME}; pub struct IdeviceProviderHandle(pub Box); @@ -24,33 +27,27 @@ pub struct IdeviceProviderHandle(pub Box); /// `pairing_file` is consumed must never be used again /// `label` must be a valid Cstr /// `provider` must be a valid, non-null pointer to a location where the handle will be stored -#[cfg(feature = "tcp")] #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_tcp_provider_new( - ip: *const libc::sockaddr, + ip: *const idevice_sockaddr, pairing_file: *mut crate::pairing_file::IdevicePairingFile, label: *const c_char, provider: *mut *mut IdeviceProviderHandle, ) -> *mut IdeviceFfiError { - if ip.is_null() || label.is_null() || provider.is_null() { - return ffi_err!(IdeviceError::FfiInvalidArg); - } - - let addr = match util::c_addr_to_rust(ip) { + let ip = ip as *const SockAddr; + let addr: IpAddr = match util::c_addr_to_rust(ip) { Ok(i) => i, - Err(e) => { - return ffi_err!(e); - } - }; - let label = match unsafe { CStr::from_ptr(label) }.to_str() { - Ok(l) => l.to_string(), - Err(e) => { - log::error!("Invalid label string: {e:?}"); - return ffi_err!(IdeviceError::FfiInvalidString); - } + Err(e) => return ffi_err!(e), }; + let label = match unsafe { CStr::from_ptr(label).to_str() } { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + // consume the pairing file on success let pairing_file = unsafe { Box::from_raw(pairing_file) }; + let t = TcpProvider { addr, pairing_file: pairing_file.0, @@ -59,7 +56,7 @@ pub unsafe extern "C" fn idevice_tcp_provider_new( let boxed = Box::new(IdeviceProviderHandle(Box::new(t))); unsafe { *provider = Box::into_raw(boxed) }; - null_mut() + std::ptr::null_mut() } /// Frees an IdeviceProvider handle @@ -140,3 +137,32 @@ pub unsafe extern "C" fn usbmuxd_provider_new( null_mut() } + +/// Gets the pairing file for the device +/// +/// # Arguments +/// * [`provider`] - A pointer to the provider +/// * [`pairing_file`] - A pointer to the newly allocated pairing file +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid, non-null pointer to the provider +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_provider_get_pairing_file( + provider: *mut IdeviceProviderHandle, + pairing_file: *mut *mut IdevicePairingFile, +) -> *mut IdeviceFfiError { + let provider = unsafe { &mut *provider }; + + let res = RUNTIME.block_on(async move { provider.0.get_pairing_file().await }); + match res { + Ok(pf) => { + let pf = Box::new(IdevicePairingFile(pf)); + unsafe { *pairing_file = Box::into_raw(pf) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} diff --git a/ffi/src/remote_server.rs b/ffi/src/remote_server.rs index 0646d11..a9d3c54 100644 --- a/ffi/src/remote_server.rs +++ b/ffi/src/remote_server.rs @@ -6,7 +6,6 @@ use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; use idevice::dvt::remote_server::RemoteServerClient; -use idevice::tcp::stream::AdapterStream; use idevice::{IdeviceError, ReadWrite, RsdService}; /// Opaque handle to a RemoteServerClient @@ -77,7 +76,7 @@ pub unsafe extern "C" fn remote_server_connect_rsd( if provider.is_null() || handshake.is_null() || handshake.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } - let res: Result, IdeviceError> = + let res: Result>, IdeviceError> = RUNTIME.block_on(async move { let provider_ref = unsafe { &mut (*provider).0 }; let handshake_ref = unsafe { &mut (*handshake).0 }; diff --git a/ffi/src/rsd.rs b/ffi/src/rsd.rs index 9ab2bcb..0ac757f 100644 --- a/ffi/src/rsd.rs +++ b/ffi/src/rsd.rs @@ -10,6 +10,7 @@ use idevice::rsd::RsdHandshake; use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to an RsdHandshake +#[derive(Clone)] pub struct RsdHandshakeHandle(pub RsdHandshake); /// C-compatible representation of an RSD service @@ -370,6 +371,22 @@ pub unsafe extern "C" fn rsd_get_service_info( null_mut() } +/// Clones an RSD handshake +/// +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rsd_handshake_clone( + handshake: *mut RsdHandshakeHandle, +) -> *mut RsdHandshakeHandle { + if handshake.is_null() { + return null_mut(); + } + let handshake = unsafe { &mut *handshake }; + let new_handshake = handshake.clone(); + Box::into_raw(Box::new(new_handshake)) +} + /// Frees a string returned by RSD functions /// /// # Arguments diff --git a/ffi/src/tcp_object_stack.rs b/ffi/src/tcp_object_stack.rs new file mode 100644 index 0000000..8917305 --- /dev/null +++ b/ffi/src/tcp_object_stack.rs @@ -0,0 +1,215 @@ +//! Just to make things more complicated, some setups need an IP input from FFI. Or maybe a packet +//! input that is sync only. This is a stupid simple shim between callbacks and an input for the +//! legendary idevice TCP stack. + +use std::{ + ffi::{CStr, c_char, c_void}, + ptr::null_mut, + sync::Arc, +}; + +use tokio::sync::Mutex; +use tokio::{ + io::AsyncWriteExt, + net::tcp::{OwnedReadHalf, OwnedWriteHalf}, +}; + +use crate::{IdeviceFfiError, RUNTIME, core_device_proxy::AdapterHandle, ffi_err}; + +pub struct TcpFeedObject { + sender: Arc>, +} +pub struct TcpEatObject { + receiver: Arc>, +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct UserContext(*mut c_void); +unsafe impl Send for UserContext {} +unsafe impl Sync for UserContext {} + +/// # Safety +/// Pass valid pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_stack_into_sync_objects( + our_ip: *const c_char, + their_ip: *const c_char, + feeder: *mut *mut TcpFeedObject, // feed the TCP stack with IP packets + tcp_receiver: *mut *mut TcpEatObject, + adapter_handle: *mut *mut AdapterHandle, // this object can be used throughout the rest of the + // idevice ecosystem +) -> *mut IdeviceFfiError { + if our_ip.is_null() || their_ip.is_null() || feeder.is_null() || adapter_handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let our_ip = unsafe { CStr::from_ptr(our_ip) } + .to_string_lossy() + .to_string(); + let our_ip = match our_ip.parse::() { + Ok(o) => o, + Err(_) => { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + }; + let their_ip = unsafe { CStr::from_ptr(their_ip) } + .to_string_lossy() + .to_string(); + let their_ip = match their_ip.parse::() { + Ok(o) => o, + Err(_) => { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + }; + + let res = RUNTIME.block_on(async { + let mut port = 4000; + loop { + if port > 4050 { + return None; + } + let listener = match tokio::net::TcpListener::bind(format!("127.0.0.1:{port}")).await { + Ok(l) => l, + Err(_) => { + port += 1; + continue; + } + }; + + let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")) + .await + .ok()?; + stream.set_nodelay(true).ok()?; + let (stream2, _) = listener.accept().await.ok()?; + stream2.set_nodelay(true).ok()?; + break Some((stream, stream2)); + } + }); + + let (stream, stream2) = match res { + Some(x) => x, + None => { + return ffi_err!(IdeviceError::NoEstablishedConnection); + } + }; + + let (r, w) = stream2.into_split(); + let w = Arc::new(Mutex::new(w)); + let r = Arc::new(Mutex::new(r)); + + // let w = Arc::new(Mutex::new(stream2)); + // let r = w.clone(); + + let feed_object = TcpFeedObject { sender: w }; + let eat_object = TcpEatObject { receiver: r }; + + // we must be inside the runtime for the inner function to spawn threads + let new_adapter = RUNTIME.block_on(async { + idevice::tcp::adapter::Adapter::new(Box::new(stream), our_ip, their_ip).to_async_handle() + }); + // this object can now be used with the rest of the idevice FFI library + + unsafe { + *feeder = Box::into_raw(Box::new(feed_object)); + *tcp_receiver = Box::into_raw(Box::new(eat_object)); + *adapter_handle = Box::into_raw(Box::new(AdapterHandle(new_adapter))); + } + + null_mut() +} + +/// Feed the TCP stack with data +/// # Safety +/// Pass valid pointers. Data is cloned out of slice. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_feed_object_write( + object: *mut TcpFeedObject, + data: *const u8, + len: usize, +) -> *mut IdeviceFfiError { + if object.is_null() || data.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let object = unsafe { &mut *object }; + let data = unsafe { std::slice::from_raw_parts(data, len) }; + RUNTIME.block_on(async move { + let mut lock = object.sender.lock().await; + match lock.write_all(data).await { + Ok(_) => { + lock.flush().await.ok(); + null_mut() + } + Err(e) => { + ffi_err!(IdeviceError::Socket(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + format!("could not send: {e:?}") + ))) + } + } + }) +} + +/// Block on getting a block of data to write to the underlying stream. +/// Write this to the stream as is, and free the data with idevice_data_free +/// +/// # Safety +/// Pass valid pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_eat_object_read( + object: *mut TcpEatObject, + data: *mut *mut u8, + len: *mut usize, +) -> *mut IdeviceFfiError { + let object = unsafe { &mut *object }; + let mut buf = [0; 2048]; + RUNTIME.block_on(async { + let lock = object.receiver.lock().await; + match lock.try_read(&mut buf) { + Ok(size) => { + let bytes = buf[..size].to_vec(); + let mut res = bytes.into_boxed_slice(); + unsafe { + *len = res.len(); + *data = res.as_mut_ptr(); + } + std::mem::forget(res); + std::ptr::null_mut() + } + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + unsafe { + *len = 0; + } + std::ptr::null_mut() + } + _ => { + ffi_err!(IdeviceError::Socket(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed" + ))) + } + }, + } + }) +} + +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free_tcp_feed_object(object: *mut TcpFeedObject) { + if object.is_null() { + return; + } + let _ = unsafe { Box::from_raw(object) }; +} + +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free_tcp_eat_object(object: *mut TcpEatObject) { + if object.is_null() { + return; + } + let _ = unsafe { Box::from_raw(object) }; +} diff --git a/ffi/src/usbmuxd.rs b/ffi/src/usbmuxd.rs index f39cf9f..3f0b932 100644 --- a/ffi/src/usbmuxd.rs +++ b/ffi/src/usbmuxd.rs @@ -1,18 +1,23 @@ // Jackson Coxson use std::{ - ffi::{CStr, c_char}, + ffi::{CStr, CString, c_char}, ptr::null_mut, }; -use crate::{IdeviceFfiError, RUNTIME, ffi_err, util::c_socket_to_rust}; +use crate::{ + IdeviceFfiError, IdeviceHandle, IdevicePairingFile, RUNTIME, ffi_err, + util::{SockAddr, c_socket_to_rust, idevice_sockaddr, idevice_socklen_t}, +}; use idevice::{ IdeviceError, - usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}, + usbmuxd::{UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice}, }; +use log::error; pub struct UsbmuxdConnectionHandle(pub UsbmuxdConnection); pub struct UsbmuxdAddrHandle(pub UsbmuxdAddr); +pub struct UsbmuxdDeviceHandle(pub UsbmuxdDevice); /// Connects to a usbmuxd instance over TCP /// @@ -30,26 +35,32 @@ pub struct UsbmuxdAddrHandle(pub UsbmuxdAddr); /// `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_new_tcp_connection( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const idevice_sockaddr, + addr_len: idevice_socklen_t, tag: u32, - usbmuxd_connection: *mut *mut UsbmuxdConnectionHandle, + out: *mut *mut UsbmuxdConnectionHandle, ) -> *mut IdeviceFfiError { - let addr = match c_socket_to_rust(addr, addr_len) { + if addr.is_null() || out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + // Reinterpret as the real platform sockaddr for parsing + let addr = addr as *const SockAddr; + + let addr = match c_socket_to_rust(addr, addr_len as _) { Ok(a) => a, Err(e) => return ffi_err!(e), }; - let res: Result = RUNTIME.block_on(async move { + let res = RUNTIME.block_on(async move { let stream = tokio::net::TcpStream::connect(addr).await?; - Ok(UsbmuxdConnection::new(Box::new(stream), tag)) + Ok::<_, IdeviceError>(UsbmuxdConnection::new(Box::new(stream), tag)) }); match res { - Ok(r) => { - let boxed = Box::new(UsbmuxdConnectionHandle(r)); - unsafe { *usbmuxd_connection = Box::into_raw(boxed) }; - null_mut() + Ok(conn) => { + unsafe { *out = Box::into_raw(Box::new(UsbmuxdConnectionHandle(conn))) }; + std::ptr::null_mut() } Err(e) => ffi_err!(e), } @@ -108,6 +119,7 @@ pub unsafe extern "C" fn idevice_usbmuxd_new_unix_socket_connection( /// # Safety /// `addr` must be a valid CStr /// `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_new_default_connection( tag: u32, usbmuxd_connection: *mut *mut UsbmuxdConnectionHandle, @@ -133,6 +145,203 @@ pub unsafe extern "C" fn idevice_usbmuxd_new_default_connection( } } +/// Gets a list of connected devices from usbmuxd. +/// +/// The returned list must be freed with `idevice_usbmuxd_device_list_free`. +/// +/// # Arguments +/// * `usbmuxd_conn` - A valid connection to usbmuxd. +/// * `devices` - A pointer to a C-style array of `UsbmuxdDeviceHandle` pointers. On success, this will be filled. +/// * `count` - A pointer to an integer. On success, this will be filled with the number of devices found. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_conn` must be a valid pointer. +/// * `devices` and `count` must be valid, non-null pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_get_devices( + usbmuxd_conn: *mut UsbmuxdConnectionHandle, + devices: *mut *mut *mut UsbmuxdDeviceHandle, + count: *mut libc::c_int, +) -> *mut IdeviceFfiError { + if usbmuxd_conn.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let conn = unsafe { &mut (*usbmuxd_conn).0 }; + + let res = RUNTIME.block_on(async { conn.get_devices().await }); + + match res { + Ok(device_vec) => { + unsafe { + *count = device_vec.len() as libc::c_int; + } + let mut c_arr = Vec::with_capacity(device_vec.len()); + for device in device_vec { + let handle = Box::new(UsbmuxdDeviceHandle(device)); + c_arr.push(Box::into_raw(handle)); + } + let mut c_arr = c_arr.into_boxed_slice(); + unsafe { + *devices = c_arr.as_mut_ptr(); + } + std::mem::forget(c_arr); // Prevent deallocation of the slice's buffer + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Connects to a service on a given device. +/// +/// This function consumes the `UsbmuxdConnectionHandle`. The handle will be invalid after this call +/// and must not be used again. The caller is NOT responsible for freeing it. +/// A new `IdeviceHandle` is returned on success, which must be freed by the caller. +/// +/// # Arguments +/// * `usbmuxd_connection` - The connection to use. It will be consumed. +/// * `device_id` - The ID of the device to connect to. +/// * `port` - The TCP port on the device to connect to. +/// * `idevice` - On success, points to the new device connection handle. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_connection` must be a valid pointer allocated by this library and never used again. +/// The value is consumed. +/// * `idevice` must be a valid, non-null pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_connect_to_device( + usbmuxd_connection: *mut UsbmuxdConnectionHandle, + device_id: u32, + port: u16, + label: *const c_char, + idevice: *mut *mut IdeviceHandle, +) -> *mut IdeviceFfiError { + if usbmuxd_connection.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + // Take ownership of the connection handle + let conn = unsafe { + let conn = std::ptr::read(&(*usbmuxd_connection).0); // move the inner connection + drop(Box::from_raw(usbmuxd_connection)); // free the wrapper + conn + }; + + let label = unsafe { + match CStr::from_ptr(label).to_str() { + Ok(s) => s, + Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), + } + }; + + let res = RUNTIME.block_on(async move { conn.connect_to_device(device_id, port, label).await }); + + match res { + Ok(device_conn) => { + let boxed = Box::new(IdeviceHandle(device_conn)); + unsafe { + *idevice = Box::into_raw(boxed); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Reads the pairing record for a given device UDID. +/// +/// The returned `PairingFileHandle` must be freed with `idevice_pair_record_free`. +/// +/// # Arguments +/// * `usbmuxd_conn` - A valid connection to usbmuxd. +/// * `udid` - The UDID of the device. +/// * `pair_record` - On success, points to the new pairing file handle. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_conn` must be a valid pointer. +/// * `udid` must be a valid, null-terminated C string. +/// * `pair_record` must be a valid, non-null pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_get_pair_record( + usbmuxd_conn: *mut UsbmuxdConnectionHandle, + udid: *const c_char, + pair_record: *mut *mut IdevicePairingFile, +) -> *mut IdeviceFfiError { + if usbmuxd_conn.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let conn = unsafe { &mut (*usbmuxd_conn).0 }; + + let udid_str = unsafe { + match CStr::from_ptr(udid).to_str() { + Ok(s) => s, + Err(_) => return ffi_err!(IdeviceError::FfiInvalidArg), + } + }; + + let res = RUNTIME.block_on(async { conn.get_pair_record(udid_str).await }); + + match res { + Ok(pf) => { + let boxed = Box::new(IdevicePairingFile(pf)); + unsafe { + *pair_record = Box::into_raw(boxed); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Reads the BUID (Boot-Unique ID) from usbmuxd. +/// +/// The returned string must be freed with `idevice_string_free`. +/// +/// # Arguments +/// * `usbmuxd_conn` - A valid connection to usbmuxd. +/// * `buid` - On success, points to a newly allocated, null-terminated C string. +/// +/// # Returns +/// An `IdeviceFfiError` on error, `null` on success. +/// +/// # Safety +/// * `usbmuxd_conn` must be a valid pointer. +/// * `buid` must be a valid, non-null pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_get_buid( + usbmuxd_conn: *mut UsbmuxdConnectionHandle, + buid: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if usbmuxd_conn.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let conn = unsafe { &mut (*usbmuxd_conn).0 }; + + let res = RUNTIME.block_on(async { conn.get_buid().await }); + + match res { + Ok(buid_str) => match CString::new(buid_str) { + Ok(c_str) => { + unsafe { *buid = c_str.into_raw() }; + null_mut() + } + Err(e) => { + error!("Unable to convert BUID string to CString: {e:?}. Null interior byte."); + ffi_err!(IdeviceError::UnexpectedResponse) + } + }, + Err(e) => ffi_err!(e), + } +} + /// Frees a UsbmuxdConnection handle /// /// # Arguments @@ -165,20 +374,28 @@ pub unsafe extern "C" fn idevice_usbmuxd_connection_free( /// `usbmuxd_Addr` must be a valid, non-null pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn idevice_usbmuxd_tcp_addr_new( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const idevice_sockaddr, // <- portable + addr_len: idevice_socklen_t, usbmuxd_addr: *mut *mut UsbmuxdAddrHandle, ) -> *mut IdeviceFfiError { - let addr = match c_socket_to_rust(addr, addr_len) { + if addr.is_null() || usbmuxd_addr.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + // Reinterpret as the real platform sockaddr for parsing + let addr = addr as *const SockAddr; + + let addr = match c_socket_to_rust(addr, addr_len as _) { Ok(a) => a, Err(e) => return ffi_err!(e), }; let u = UsbmuxdAddr::TcpSocket(addr); - let boxed = Box::new(UsbmuxdAddrHandle(u)); - unsafe { *usbmuxd_addr = Box::into_raw(boxed) }; - null_mut() + unsafe { + *usbmuxd_addr = Box::into_raw(boxed); + } + std::ptr::null_mut() } /// Creates a new UsbmuxdAddr struct with a unix socket @@ -211,6 +428,26 @@ pub unsafe extern "C" fn idevice_usbmuxd_unix_addr_new( null_mut() } +/// Creates a default UsbmuxdAddr struct for the platform +/// +/// # Arguments +/// * [`usbmuxd_addr`] - On success, will be set to point to a newly allocated UsbmuxdAddr handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `usbmuxd_addr` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_default_addr_new( + usbmuxd_addr: *mut *mut UsbmuxdAddrHandle, +) -> *mut IdeviceFfiError { + let addr = UsbmuxdAddr::default(); + let boxed = Box::new(UsbmuxdAddrHandle(addr)); + unsafe { *usbmuxd_addr = Box::into_raw(boxed) }; + null_mut() +} + /// Frees a UsbmuxdAddr handle /// /// # Arguments @@ -225,3 +462,108 @@ pub unsafe extern "C" fn idevice_usbmuxd_addr_free(usbmuxd_addr: *mut UsbmuxdAdd let _ = unsafe { Box::from_raw(usbmuxd_addr) }; } } + +/// Frees a list of devices returned by `idevice_usbmuxd_get_devices`. +/// +/// # Arguments +/// * `devices` - The array of device handles to free. +/// * `count` - The number of elements in the array. +/// +/// # Safety +/// `devices` must be a valid pointer to an array of `count` device handles +/// allocated by this library, or NULL. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_list_free( + devices: *mut *mut UsbmuxdDeviceHandle, + count: libc::c_int, +) { + if devices.is_null() { + return; + } + let slice = unsafe { std::slice::from_raw_parts_mut(devices, count as usize) }; + for &mut ptr in slice { + if !ptr.is_null() { + let _ = unsafe { Box::from_raw(ptr) }; + } + } +} + +/// Frees a usbmuxd device +/// +/// # Arguments +/// * `device` - The device handle to free. +/// +/// # Safety +/// `device` must be a valid pointer to the device handle +/// allocated by this library, or NULL. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_free(device: *mut UsbmuxdDeviceHandle) { + if device.is_null() { + return; + } + let _ = unsafe { Box::from_raw(device) }; +} + +/// Gets the UDID from a device handle. +/// The returned string must be freed by the caller using `idevice_string_free`. +/// +/// # Safety +/// `device` must be a valid pointer to a `UsbmuxdDeviceHandle`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_get_udid( + device: *const UsbmuxdDeviceHandle, +) -> *mut c_char { + if device.is_null() { + return null_mut(); + } + let device = unsafe { &(*device).0 }; + match CString::new(device.udid.as_str()) { + Ok(s) => s.into_raw(), + Err(_) => null_mut(), + } +} + +/// Gets the device ID from a device handle. +/// +/// # Safety +/// `device` must be a valid pointer to a `UsbmuxdDeviceHandle`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_get_device_id( + device: *const UsbmuxdDeviceHandle, +) -> u32 { + if device.is_null() { + return 0; + } + unsafe { (*device).0.device_id } +} + +#[repr(C)] +enum UsbmuxdConnectionType { + Usb = 1, + Network = 2, + Unknown = 3, +} + +/// Gets the connection type (UsbmuxdConnectionType) from a device handle. +/// +/// # Returns +/// The enum value of the connection type, or 0 for null device handles +/// +/// # Safety +/// `device` must be a valid pointer to a `UsbmuxdDeviceHandle`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_usbmuxd_device_get_connection_type( + device: *const UsbmuxdDeviceHandle, +) -> u8 { + if device.is_null() { + return 0; + } + let ct = unsafe { &(*device).0.connection_type }; + + let ct = match ct { + idevice::usbmuxd::Connection::Usb => UsbmuxdConnectionType::Usb, + idevice::usbmuxd::Connection::Network(_) => UsbmuxdConnectionType::Network, + idevice::usbmuxd::Connection::Unknown(_) => UsbmuxdConnectionType::Unknown, + }; + ct as u8 +} diff --git a/ffi/src/util.rs b/ffi/src/util.rs index 989cf77..6088109 100644 --- a/ffi/src/util.rs +++ b/ffi/src/util.rs @@ -1,98 +1,186 @@ // Jackson Coxson -use std::{ - ffi::c_int, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - os::raw::c_void, -}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use idevice::IdeviceError; -use libc::{sockaddr_in, sockaddr_in6}; -use plist::Value; + +// portable FFI-facing types (only used in signatures) +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct idevice_sockaddr { + _priv: [u8; 0], // opaque; acts as "struct sockaddr" placeholder +} +#[cfg(unix)] +#[allow(non_camel_case_types)] +pub type idevice_socklen_t = libc::socklen_t; +#[cfg(windows)] +#[allow(non_camel_case_types)] +pub type idevice_socklen_t = i32; + +// platform sockaddr aliases for implementation +#[cfg(unix)] +pub(crate) type SockAddr = libc::sockaddr; +#[cfg(windows)] +use windows_sys::Win32::Networking::WinSock as winsock; +#[cfg(windows)] +pub(crate) type SockAddr = winsock::SOCKADDR; + +#[cfg(unix)] +use libc::{self, sockaddr_in, sockaddr_in6}; + +#[cfg(windows)] +use windows_sys::Win32::Networking::WinSock::{ + AF_INET, AF_INET6, SOCKADDR_IN as sockaddr_in, SOCKADDR_IN6 as sockaddr_in6, +}; + +#[cfg(unix)] +type SockLen = libc::socklen_t; +#[cfg(windows)] +type SockLen = i32; // socklen_t is an int on Windows + +#[inline] +fn invalid_arg() -> Result { + Err(IdeviceError::FfiInvalidArg) +} pub(crate) fn c_socket_to_rust( - addr: *const libc::sockaddr, - addr_len: libc::socklen_t, + addr: *const SockAddr, + addr_len: SockLen, ) -> Result { - Ok(unsafe { - match (*addr).sa_family as c_int { + if addr.is_null() { + log::error!("null sockaddr"); + return invalid_arg(); + } + + unsafe { + let family = (*addr).sa_family; + + #[cfg(unix)] + match family as i32 { libc::AF_INET => { if (addr_len as usize) < std::mem::size_of::() { log::error!("Invalid sockaddr_in size"); - return Err(IdeviceError::FfiInvalidArg); + return invalid_arg(); } - let addr_in = *(addr as *const sockaddr_in); - let ip = std::net::Ipv4Addr::from(u32::from_be(addr_in.sin_addr.s_addr)); - let port = u16::from_be(addr_in.sin_port); - std::net::SocketAddr::V4(std::net::SocketAddrV4::new(ip, port)) + let a = &*(addr as *const sockaddr_in); + let ip = Ipv4Addr::from(u32::from_be(a.sin_addr.s_addr)); + let port = u16::from_be(a.sin_port); + Ok(SocketAddr::V4(std::net::SocketAddrV4::new(ip, port))) } libc::AF_INET6 => { - if addr_len as usize >= std::mem::size_of::() { - let addr_in6 = *(addr as *const sockaddr_in6); - let ip = std::net::Ipv6Addr::from(addr_in6.sin6_addr.s6_addr); - let port = u16::from_be(addr_in6.sin6_port); - std::net::SocketAddr::V6(std::net::SocketAddrV6::new( - ip, - port, - addr_in6.sin6_flowinfo, - addr_in6.sin6_scope_id, - )) - } else { + if (addr_len as usize) < std::mem::size_of::() { log::error!("Invalid sockaddr_in6 size"); - return Err(IdeviceError::FfiInvalidArg); + return invalid_arg(); } - } - _ => { - log::error!("Unsupported socket address family: {}", (*addr).sa_family); - return Err(IdeviceError::FfiInvalidArg); - } - } - }) -} - -pub(crate) fn c_addr_to_rust(addr: *const libc::sockaddr) -> Result { - unsafe { - // Check the address family - match (*addr).sa_family as c_int { - libc::AF_INET => { - // Convert sockaddr_in (IPv4) to IpAddr - let sockaddr_in = addr as *const sockaddr_in; - let ip = (*sockaddr_in).sin_addr.s_addr; - let octets = u32::from_be(ip).to_be_bytes(); - Ok(IpAddr::V4(Ipv4Addr::new( - octets[0], octets[1], octets[2], octets[3], + let a = &*(addr as *const sockaddr_in6); + let ip = Ipv6Addr::from(a.sin6_addr.s6_addr); + let port = u16::from_be(a.sin6_port); + Ok(SocketAddr::V6(std::net::SocketAddrV6::new( + ip, + port, + a.sin6_flowinfo, + a.sin6_scope_id, ))) } - libc::AF_INET6 => { - // Convert sockaddr_in6 (IPv6) to IpAddr - let sockaddr_in6 = addr as *const sockaddr_in6; - let ip = (*sockaddr_in6).sin6_addr.s6_addr; - Ok(IpAddr::V6(Ipv6Addr::from(ip))) + _ => { + log::error!( + "Unsupported socket address family: {}", + (*addr).sa_family as i32 + ); + invalid_arg() + } + } + + #[cfg(windows)] + match family { + AF_INET => { + if (addr_len as usize) < std::mem::size_of::() { + log::error!("Invalid SOCKADDR_IN size"); + return invalid_arg(); + } + let a = &*(addr as *const sockaddr_in); + // IN_ADDR is a union; use S_un.S_addr (network byte order) + let ip_be = a.sin_addr.S_un.S_addr; + let ip = Ipv4Addr::from(u32::from_be(ip_be)); + let port = u16::from_be(a.sin_port); + Ok(SocketAddr::V4(std::net::SocketAddrV4::new(ip, port))) + } + AF_INET6 => { + if (addr_len as usize) < std::mem::size_of::() { + log::error!("Invalid SOCKADDR_IN6 size"); + return invalid_arg(); + } + let a = &*(addr as *const sockaddr_in6); + // IN6_ADDR is a union; read the 16 Byte array + let bytes: [u8; 16] = a.sin6_addr.u.Byte; + let ip = Ipv6Addr::from(bytes); + let port = u16::from_be(a.sin6_port); + let scope_id = a.Anonymous.sin6_scope_id; + Ok(SocketAddr::V6(std::net::SocketAddrV6::new( + ip, + port, + a.sin6_flowinfo, + scope_id, + ))) } _ => { log::error!("Unsupported socket address family: {}", (*addr).sa_family); - Err(IdeviceError::FfiInvalidArg) + invalid_arg() } } } } -pub(crate) fn plist_to_libplist(v: &Value) -> *mut libc::c_void { - let buf = Vec::new(); - let mut writer = std::io::BufWriter::new(buf); - plist::to_writer_xml(&mut writer, v).unwrap(); - let buf = String::from_utf8(writer.into_inner().unwrap()).unwrap(); - let p = plist_plus::Plist::from_xml(buf).unwrap(); - let ptr = p.get_pointer(); - p.false_drop(); - ptr -} +pub(crate) fn c_addr_to_rust(addr: *const SockAddr) -> Result { + if addr.is_null() { + log::error!("null sockaddr"); + return invalid_arg(); + } -pub(crate) fn libplist_to_plist(v: *mut c_void) -> Value { - let v: plist_plus::Plist = v.into(); - let v_string = v.to_string(); - v.false_drop(); + unsafe { + #[cfg(unix)] + let family = (*addr).sa_family as i32; + #[cfg(windows)] + let family = (*addr).sa_family; - let reader = std::io::Cursor::new(v_string.as_bytes()); - plist::from_reader(reader).unwrap() + #[cfg(unix)] + match family { + libc::AF_INET => { + let a = &*(addr as *const sockaddr_in); + let octets = u32::from_be(a.sin_addr.s_addr).to_be_bytes(); + Ok(IpAddr::V4(Ipv4Addr::new( + octets[0], octets[1], octets[2], octets[3], + ))) + } + libc::AF_INET6 => { + let a = &*(addr as *const sockaddr_in6); + Ok(IpAddr::V6(Ipv6Addr::from(a.sin6_addr.s6_addr))) + } + _ => { + log::error!( + "Unsupported socket address family: {}", + (*addr).sa_family as i32 + ); + invalid_arg() + } + } + + #[cfg(windows)] + match family { + AF_INET => { + let a = &*(addr as *const sockaddr_in); + let ip_be = a.sin_addr.S_un.S_addr; + Ok(IpAddr::V4(Ipv4Addr::from(u32::from_be(ip_be)))) + } + AF_INET6 => { + let a = &*(addr as *const sockaddr_in6); + let bytes: [u8; 16] = a.sin6_addr.u.Byte; + Ok(IpAddr::V6(Ipv6Addr::from(bytes))) + } + _ => { + log::error!("Unsupported socket address family: {}", (*addr).sa_family); + invalid_arg() + } + } + } } diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 01b2ef4..1473648 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,8 +2,8 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.35" -edition = "2021" +version = "0.1.41" +edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" @@ -12,8 +12,12 @@ keywords = ["lockdownd", "ios"] [dependencies] tokio = { version = "1.43", features = ["io-util"] } -tokio-rustls = "0.26" -rustls = "0.23" +tokio-rustls = { version = "0.26", default-features = false } +rustls = { version = "0.23", default-features = false, features = [ + "std", + "tls12", +] } +crossfire = { version = "2.0", optional = true } # TODO: update to 2.1 when it comes out plist = { version = "1.7" } serde = { version = "1", features = ["derive"] } @@ -26,7 +30,9 @@ base64 = { version = "0.22" } indexmap = { version = "2.7", features = ["serde"], optional = true } uuid = { version = "1.12", features = ["serde", "v4"], optional = true } -chrono = { version = "0.4.40", optional = true, default-features = false } +chrono = { version = "0.4.40", optional = true, default-features = false, features = [ + "serde", +] } serde_json = { version = "1", optional = true } json = { version = "0.12", optional = true } @@ -35,10 +41,10 @@ bytes = { version = "1.10", optional = true } reqwest = { version = "0.12", features = [ "json", - "rustls-tls", ], optional = true, default-features = false } rand = { version = "0.9", optional = true } futures = { version = "0.3", optional = true } +async-stream = { version = "0.3.6", optional = true } sha2 = { version = "0.10", optional = true, features = ["oid"] } @@ -60,12 +66,19 @@ tun-rs = { version = "2.0.8", features = ["async_tokio"] } bytes = "1.10.1" [features] +default = ["aws-lc"] +aws-lc = ["rustls/aws-lc-rs", "tokio-rustls/aws-lc-rs"] +ring = ["rustls/ring", "tokio-rustls/ring"] + afc = ["dep:chrono"] amfi = [] +bt_packet_logger = [] +companion_proxy = [] core_device = ["xpc", "dep:uuid"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] crashreportcopymobile = ["afc"] debug_proxy = [] +diagnostics_relay = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] heartbeat = ["tokio/macros", "tokio/time"] house_arrest = ["afc"] @@ -73,8 +86,11 @@ installation_proxy = [] springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] +mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] +pcapd = [] +preboard_service = [] obfuscate = ["dep:obfstr"] restore_service = [] remote_pairing = [ @@ -88,18 +104,27 @@ remote_pairing = [ rsd = ["xpc"] syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] -tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"] +tunnel_tcp_stack = [ + "dep:rand", + "dep:futures", + "tokio/fs", + "tokio/sync", + "dep:crossfire", +] tss = ["dep:uuid", "dep:reqwest"] tunneld = ["dep:serde_json", "dep:json", "dep:reqwest"] usbmuxd = ["tokio/net"] -xpc = ["dep:indexmap", "dep:uuid"] +xpc = ["dep:indexmap", "dep:uuid", "dep:async-stream"] full = [ "afc", "amfi", + "bt_packet_logger", + "companion_proxy", "core_device", "core_device_proxy", "crashreportcopymobile", "debug_proxy", + "diagnostics_relay", "dvt", "heartbeat", "house_arrest", @@ -107,7 +132,10 @@ full = [ "location_simulation", "misagent", "mobile_image_mounter", + "mobilebackup2", "pair", + "pcapd", + "preboard_service", "restore_service", "usbmuxd", "xpc", diff --git a/idevice/src/ca.rs b/idevice/src/ca.rs index 72aab2e..9429a68 100644 --- a/idevice/src/ca.rs +++ b/idevice/src/ca.rs @@ -4,19 +4,19 @@ use std::str::FromStr; use rsa::{ + RsaPrivateKey, RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs1v15::SigningKey, pkcs8::{EncodePrivateKey, LineEnding, SubjectPublicKeyInfo}, - RsaPrivateKey, RsaPublicKey, }; use sha2::Sha256; use x509_cert::{ + Certificate, builder::{Builder, CertificateBuilder, Profile}, der::EncodePem, name::Name, serial_number::SerialNumber, time::Validity, - Certificate, }; #[derive(Clone, Debug)] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 3c4a6d1..68952d6 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -16,6 +16,7 @@ pub mod tunneld; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; mod util; +pub mod utils; #[cfg(feature = "xpc")] pub mod xpc; @@ -37,6 +38,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; pub use util::{pretty_print_dictionary, pretty_print_plist}; +use crate::services::lockdown::LockdownClient; + /// A trait combining all required characteristics for a device communication socket /// /// This serves as a convenience trait for any type that can be used as an asynchronous @@ -61,29 +64,58 @@ pub trait IdeviceService: Sized { /// /// # Arguments /// * `provider` - The device provider that can supply connections - fn connect( - provider: &dyn IdeviceProvider, - ) -> impl std::future::Future> + Send; + /// + // From the docs + // │ │ ├╴ use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified + // │ │ │ you can suppress this lint if you plan to use the trait only in your own code, or do not care about auto traits like `Send` on the `Future` + // │ │ │ `#[warn(async_fn_in_trait)]` on by default rustc (async_fn_in_trait) [66, 5] + #[allow(async_fn_in_trait)] + async fn connect(provider: &dyn IdeviceProvider) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + // Best-effort fetch UDID for downstream defaults (e.g., MobileBackup2 Target/Source identifiers) + let udid_value = match lockdown.get_value(Some("UniqueDeviceID"), None).await { + Ok(v) => v.as_string().map(|s| s.to_string()), + Err(_) => None, + }; + + let (port, ssl) = lockdown.start_service(Self::service_name()).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + + if let Some(udid) = udid_value { + idevice.set_udid(udid); + } + + Self::from_stream(idevice).await + } + + #[allow(async_fn_in_trait)] + async fn from_stream(idevice: Idevice) -> Result; } #[cfg(feature = "rsd")] pub trait RsdService: Sized { fn rsd_service_name() -> std::borrow::Cow<'static, str>; fn from_stream( - stream: Self::Stream, + stream: Box, ) -> impl std::future::Future> + Send; - fn connect_rsd<'a, S>( - provider: &'a mut impl RsdProvider<'a, Stream = S>, + fn connect_rsd( + provider: &mut impl RsdProvider, handshake: &mut rsd::RsdHandshake, ) -> impl std::future::Future> where - Self: crate::RsdService, - S: ReadWrite, + Self: crate::RsdService, { handshake.connect(provider) } - - type Stream: ReadWrite; } /// Type alias for boxed device connection sockets @@ -101,6 +133,8 @@ pub struct Idevice { socket: Option>, /// Unique label identifying this connection label: String, + /// Cached device UDID for convenience in higher-level protocols + udid: Option, } impl Idevice { @@ -113,9 +147,24 @@ impl Idevice { Self { socket: Some(socket), label: label.into(), + udid: None, } } + pub fn get_socket(self) -> Option> { + self.socket + } + + /// Sets cached UDID + pub fn set_udid(&mut self, udid: impl Into) { + self.udid = Some(udid.into()); + } + + /// Returns cached UDID if available + pub fn udid(&self) -> Option<&str> { + self.udid.as_deref() + } + /// Queries the device type /// /// Sends a QueryType request and parses the response @@ -126,11 +175,12 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if communication fails or response is invalid pub async fn get_type(&mut self) -> Result { - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.label.clone().into()); - req.insert("Request".into(), "QueryType".into()); - let message = plist::to_value(&req)?; - self.send_plist(message).await?; + let req = crate::plist!({ + "Label": self.label.clone(), + "Request": "QueryType", + }); + self.send_plist(req).await?; + let message: plist::Dictionary = self.read_plist().await?; match message.get("Type") { Some(m) => Ok(plist::from_value(m)?), @@ -145,11 +195,13 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if the protocol sequence isn't followed correctly pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.label.clone().into()); - req.insert("ProtocolVersion".into(), "2".into()); - req.insert("Request".into(), "RSDCheckin".into()); - self.send_plist(plist::to_value(&req).unwrap()).await?; + let req = crate::plist!({ + "Label": self.label.clone(), + "ProtocolVersion": "2", + "Request": "RSDCheckin", + }); + + self.send_plist(req).await?; let res = self.read_plist().await?; match res.get("Request").and_then(|x| x.as_string()) { Some(r) => { @@ -322,6 +374,35 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if reading, parsing fails, or device reports an error async fn read_plist(&mut self) -> Result { + let res = self.read_plist_value().await?; + let res: plist::Dictionary = plist::from_value(&res)?; + debug!("Received plist: {}", pretty_print_dictionary(&res)); + + if let Some(e) = res.get("Error") { + let e = match e { + plist::Value::String(e) => e.to_string(), + plist::Value::Integer(e) => { + if let Some(error_string) = res.get("ErrorString").and_then(|x| x.as_string()) { + error_string.to_string() + } else { + e.to_string() + } + } + _ => { + log::error!("Error is not a string or integer from read_plist: {e:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { + return Err(e); + } else { + return Err(IdeviceError::UnknownErrorType(e)); + } + } + Ok(res) + } + + async fn read_plist_value(&mut self) -> Result { if let Some(socket) = &mut self.socket { debug!("Reading response size"); let mut buf = [0u8; 4]; @@ -329,17 +410,7 @@ impl Idevice { let len = u32::from_be_bytes(buf); let mut buf = vec![0; len as usize]; socket.read_exact(&mut buf).await?; - let res: plist::Dictionary = plist::from_bytes(&buf)?; - debug!("Received plist: {}", pretty_print_dictionary(&res)); - - if let Some(e) = res.get("Error") { - let e: String = plist::from_value(e)?; - if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { - return Err(e); - } else { - return Err(IdeviceError::UnknownErrorType(e)); - } - } + let res: plist::Value = plist::from_bytes(&buf)?; Ok(res) } else { Err(IdeviceError::NoEstablishedConnection) @@ -390,9 +461,39 @@ impl Idevice { pairing_file: &pairing_file::PairingFile, ) -> Result<(), IdeviceError> { if CryptoProvider::get_default().is_none() { - if let Err(e) = - CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider()) - { + // rust-analyzer will choke on this block, don't worry about it + let crypto_provider: CryptoProvider = { + #[cfg(all(feature = "ring", not(feature = "aws-lc")))] + { + debug!("Using ring crypto backend"); + rustls::crypto::ring::default_provider() + } + + #[cfg(all(feature = "aws-lc", not(feature = "ring")))] + { + debug!("Using aws-lc crypto backend"); + rustls::crypto::aws_lc_rs::default_provider() + } + + #[cfg(not(any(feature = "ring", feature = "aws-lc")))] + { + compile_error!( + "No crypto backend was selected! Specify an idevice feature for a crypto backend" + ); + } + + #[cfg(all(feature = "ring", feature = "aws-lc"))] + { + // We can't throw a compile error because it breaks rust-analyzer. + // My sanity while debugging the workspace crates are more important. + + debug!("Using ring crypto backend, because both were passed"); + log::warn!("Both ring && aws-lc are selected as idevice crypto backends!"); + rustls::crypto::ring::default_provider() + } + }; + + if let Err(e) = CryptoProvider::install_default(crypto_provider) { // For whatever reason, getting the default provider will return None on iOS at // random. Installing the default provider a second time will return an error, so // we will log it but not propogate it. An issue should be opened with rustls. @@ -600,22 +701,22 @@ pub enum IdeviceError { #[cfg(feature = "remote_pairing")] #[error("could not parse as JSON")] - JsonParseFailed(#[from] json::Error) = -63, + JsonParseFailed(#[from] json::Error) = -67, #[cfg(feature = "remote_pairing")] #[error("unknown TLV type: {0}")] - UnknownTlv(u8) = -64, + UnknownTlv(u8) = -68, #[cfg(feature = "remote_pairing")] #[error("malformed TLV")] - MalformedTlv = -65, + MalformedTlv = -69, #[error("failed to decode base64 string")] - Base64Decode(#[from] base64::DecodeError) = -66, + Base64Decode(#[from] base64::DecodeError) = -70, #[cfg(feature = "remote_pairing")] #[error("pair verify failed")] - PairVerifyFailed = -67, + PairVerifyFailed = -71, #[error("invalid arguments were passed")] FfiInvalidArg = -60, @@ -623,6 +724,14 @@ pub enum IdeviceError { FfiInvalidString = -61, #[error("buffer passed is too small - needs {0}, got {1}")] FfiBufferTooSmall(usize, usize) = -62, + #[error("unsupported watch key")] + UnsupportedWatchKey = -63, + #[error("malformed command")] + MalformedCommand = -64, + #[error("integer overflow")] + IntegerOverflow = -65, + #[error("canceled by user")] + CanceledByUser = -66, } impl IdeviceError { @@ -635,6 +744,9 @@ impl IdeviceError { /// # Returns /// Some(IdeviceError) if the string maps to a known error type, None otherwise fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option { + if e.contains("NSDebugDescription=Canceled by user.") { + return Some(Self::CanceledByUser); + } match e { "GetProhibited" => Some(Self::GetProhibited), "InvalidHostID" => Some(Self::InvalidHostID), @@ -646,6 +758,8 @@ impl IdeviceError { "UserDeniedPairing" => Some(Self::UserDeniedPairing), #[cfg(feature = "pair")] "PasswordProtected" => Some(Self::PasswordProtected), + "UnsupportedWatchKey" => Some(Self::UnsupportedWatchKey), + "MalformedCommand" => Some(Self::MalformedCommand), "InternalError" => { let detailed_error = context .get("DetailedError") @@ -769,16 +883,20 @@ impl IdeviceError { IdeviceError::FfiInvalidArg => -60, IdeviceError::FfiInvalidString => -61, IdeviceError::FfiBufferTooSmall(_, _) => -62, + IdeviceError::UnsupportedWatchKey => -63, + IdeviceError::MalformedCommand => -64, + IdeviceError::IntegerOverflow => -65, + IdeviceError::CanceledByUser => -66, #[cfg(feature = "remote_pairing")] - IdeviceError::JsonParseFailed(_) => -63, + IdeviceError::JsonParseFailed(_) => -67, #[cfg(feature = "remote_pairing")] - IdeviceError::UnknownTlv(_) => -64, + IdeviceError::UnknownTlv(_) => -68, #[cfg(feature = "remote_pairing")] - IdeviceError::MalformedTlv => -65, - IdeviceError::Base64Decode(_) => -66, + IdeviceError::MalformedTlv => -69, + IdeviceError::Base64Decode(_) => -70, #[cfg(feature = "remote_pairing")] - IdeviceError::PairVerifyFailed => -67, + IdeviceError::PairVerifyFailed => -71, } } } diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index b4e8dda..9288bae 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -7,7 +7,7 @@ use std::path::Path; use log::warn; use plist::Data; -use rustls::pki_types::{pem::PemObject, CertificateDer}; +use rustls::pki_types::{CertificateDer, pem::PemObject}; use serde::{Deserialize, Serialize}; /// Represents a complete iOS device pairing record @@ -219,7 +219,7 @@ fn ensure_pem_headers(data: &[u8], pem_type: &str) -> Vec { let mut result = Vec::new(); // Add header - let header = format!("-----BEGIN {}-----\n", pem_type); + let header = format!("-----BEGIN {pem_type}-----\n"); result.extend_from_slice(header.as_bytes()); // Add base64 content with line breaks every 64 characters @@ -244,7 +244,7 @@ fn ensure_pem_headers(data: &[u8], pem_type: &str) -> Vec { result.push(b'\n'); // Add footer - let footer = format!("-----END {}-----", pem_type); + let footer = format!("-----END {pem_type}-----"); result.extend_from_slice(footer.as_bytes()); result diff --git a/idevice/src/plist_macro.rs b/idevice/src/plist_macro.rs index 6624e3b..3453b43 100644 --- a/idevice/src/plist_macro.rs +++ b/idevice/src/plist_macro.rs @@ -52,6 +52,23 @@ /// ``` #[macro_export] macro_rules! plist { + // Force: dictionary out + (dict { $($tt:tt)+ }) => {{ + let mut object = plist::Dictionary::new(); + $crate::plist_internal!(@object object () ($($tt)+) ($($tt)+)); + object + }}; + + // Force: value out (explicit, though default already does this) + (value { $($tt:tt)+ }) => { + $crate::plist_internal!({ $($tt)+ }) + }; + + // Force: raw vec of plist::Value out + (array [ $($tt:tt)+ ]) => { + $crate::plist_internal!(@array [] $($tt)+) + }; + // Hide distracting implementation details from the generated rustdoc. ($($plist:tt)+) => { $crate::plist_internal!($($plist)+) @@ -123,6 +140,21 @@ macro_rules! plist_internal { $crate::plist_unexpected!($unexpected) }; + (@array [$($elems:expr,)*] ? $maybe:expr , $($rest:tt)*) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) { + $crate::plist_internal!(@array [$($elems,)* __v,] $($rest)*) + } else { + $crate::plist_internal!(@array [$($elems,)*] $($rest)*) + } + }; + (@array [$($elems:expr,)*] ? $maybe:expr) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) { + $crate::plist_internal!(@array [$($elems,)* __v]) + } else { + $crate::plist_internal!(@array [$($elems,)*]) + } + }; + ////////////////////////////////////////////////////////////////////////// // TT muncher for parsing the inside of an object {...}. Each entry is // inserted into the given map variable. @@ -177,6 +209,62 @@ macro_rules! plist_internal { $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!({$($map)*})) $($rest)*); }; + // Optional insert with trailing comma: key?: expr, + (@object $object:ident ($($key:tt)+) (:? $value:expr , $($rest:tt)*) $copy:tt) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($value) { + let _ = $object.insert(($($key)+).into(), __v); + } + $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Optional insert, last entry: key?: expr + (@object $object:ident ($($key:tt)+) (:? $value:expr) $copy:tt) => { + if let Some(__v) = $crate::plist_macro::plist_maybe($value) { + let _ = $object.insert(($($key)+).into(), __v); + } + }; + + (@object $object:ident () ( :< $value:expr , $($rest:tt)*) $copy:tt) => { + { + let __v = $crate::plist_internal!($value); + let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v); + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Merge: last entry `:< expr` + (@object $object:ident () ( :< $value:expr ) $copy:tt) => { + { + let __v = $crate::plist_internal!($value); + let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v); + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + }; + + // Optional merge: `:< ? expr,` — only merge if Some(...) + (@object $object:ident () ( :< ? $value:expr , $($rest:tt)*) $copy:tt) => { + if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) { + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); + }; + + // Optional merge: last entry `:< ? expr` + (@object $object:ident () ( :< ? $value:expr ) $copy:tt) => { + if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) { + for (__k, __val) in __dict { + let _ = $object.insert(__k, __val); + } + } + }; + // Next value is an expression followed by comma. (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => { $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)) , $($rest)*); @@ -315,6 +403,12 @@ impl PlistConvertible for &str { } } +impl PlistConvertible for i16 { + fn to_plist_value(self) -> plist::Value { + plist::Value::Integer(self.into()) + } +} + impl PlistConvertible for i32 { fn to_plist_value(self) -> plist::Value { plist::Value::Integer(self.into()) @@ -327,6 +421,12 @@ impl PlistConvertible for i64 { } } +impl PlistConvertible for u16 { + fn to_plist_value(self) -> plist::Value { + plist::Value::Integer((self as i64).into()) + } +} + impl PlistConvertible for u32 { fn to_plist_value(self) -> plist::Value { plist::Value::Integer((self as i64).into()) @@ -357,6 +457,27 @@ impl PlistConvertible for bool { } } +impl<'a> PlistConvertible for std::borrow::Cow<'a, str> { + fn to_plist_value(self) -> plist::Value { + plist::Value::String(self.into_owned()) + } +} +impl PlistConvertible for Vec { + fn to_plist_value(self) -> plist::Value { + plist::Value::Data(self) + } +} +impl PlistConvertible for &[u8] { + fn to_plist_value(self) -> plist::Value { + plist::Value::Data(self.to_vec()) + } +} +impl PlistConvertible for std::time::SystemTime { + fn to_plist_value(self) -> plist::Value { + plist::Value::Date(self.into()) + } +} + impl PlistConvertible for Vec { fn to_plist_value(self) -> plist::Value { plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect()) @@ -423,15 +544,96 @@ where } } -impl PlistConvertible for Option { - fn to_plist_value(self) -> plist::Value { +// Treat plain T as Some(T) and Option as-is. +pub trait MaybePlist { + fn into_option_value(self) -> Option; +} + +impl MaybePlist for T { + fn into_option_value(self) -> Option { + Some(self.to_plist_value()) + } +} + +impl MaybePlist for Option { + fn into_option_value(self) -> Option { + self.map(|v| v.to_plist_value()) + } +} + +#[doc(hidden)] +pub fn plist_maybe(v: T) -> Option { + v.into_option_value() +} + +// Convert things into a Dictionary we can merge. +pub trait IntoPlistDict { + fn into_plist_dict(self) -> plist::Dictionary; +} + +impl IntoPlistDict for plist::Dictionary { + fn into_plist_dict(self) -> plist::Dictionary { + self + } +} + +impl IntoPlistDict for plist::Value { + fn into_plist_dict(self) -> plist::Dictionary { match self { - Some(value) => value.to_plist_value(), - None => plist::Value::String("".to_string()), // or however you want to handle None + plist::Value::Dictionary(d) => d, + other => panic!("plist :< expects a dictionary, got {other:?}"), } } } +impl IntoPlistDict for std::collections::HashMap +where + K: Into, + V: PlistConvertible, +{ + fn into_plist_dict(self) -> plist::Dictionary { + let mut d = plist::Dictionary::new(); + for (k, v) in self { + d.insert(k.into(), v.to_plist_value()); + } + d + } +} + +impl IntoPlistDict for std::collections::BTreeMap +where + K: Into, + V: PlistConvertible, +{ + fn into_plist_dict(self) -> plist::Dictionary { + let mut d = plist::Dictionary::new(); + for (k, v) in self { + d.insert(k.into(), v.to_plist_value()); + } + d + } +} + +// Optional version: T or Option. +pub trait MaybeIntoPlistDict { + fn into_option_plist_dict(self) -> Option; +} +impl MaybeIntoPlistDict for T { + fn into_option_plist_dict(self) -> Option { + Some(self.into_plist_dict()) + } +} +impl MaybeIntoPlistDict for Option { + fn into_option_plist_dict(self) -> Option { + self.map(|t| t.into_plist_dict()) + } +} + +#[doc(hidden)] +pub fn maybe_into_dict(v: T) -> Option { + v.into_option_plist_dict() +} + #[cfg(test)] mod tests { #[test] @@ -440,7 +642,7 @@ mod tests { "name": "test", "count": 42, "active": true, - "items": ["a", "b", "c"] + "items": ["a", ?"b", "c"] }); if let plist::Value::Dictionary(dict) = value { @@ -460,11 +662,24 @@ mod tests { let name = "dynamic"; let count = 100; let items = vec!["x", "y"]; + let none: Option = None; + let to_merge = plist!({ + "reee": "cool beans" + }); + let maybe_merge = Some(plist!({ + "yeppers": "what did I say about yeppers", + "replace me": 2, + })); let value = plist!({ "name": name, "count": count, - "items": items + "items": items, + "omit me":? none, + "keep me":? Some(123), + "replace me": 1, + :< to_merge, + : Pin> + Send>>; } -pub trait RsdProvider<'a>: Unpin + Send + Sync + std::fmt::Debug { +pub trait RsdProvider: Unpin + Send + Sync + std::fmt::Debug { fn connect_to_service_port( - &'a mut self, + &mut self, port: u16, - ) -> impl std::future::Future> + Send; - type Stream: ReadWrite; + ) -> impl std::future::Future, IdeviceError>> + Send; } /// TCP-based device connection provider @@ -159,13 +158,13 @@ impl IdeviceProvider for UsbmuxdProvider { } #[cfg(feature = "tcp")] -impl<'a> RsdProvider<'a> for std::net::IpAddr { +impl RsdProvider for std::net::IpAddr { async fn connect_to_service_port( - &'a mut self, + &mut self, port: u16, - ) -> Result { - Ok(tokio::net::TcpStream::connect((*self, port)).await?) + ) -> Result, IdeviceError> { + Ok(Box::new( + tokio::net::TcpStream::connect((*self, port)).await?, + )) } - - type Stream = tokio::net::TcpStream; } diff --git a/idevice/src/services/afc/errors.rs b/idevice/src/services/afc/errors.rs index 7aa9fc3..a6c726e 100644 --- a/idevice/src/services/afc/errors.rs +++ b/idevice/src/services/afc/errors.rs @@ -66,7 +66,7 @@ impl std::fmt::Display for AfcError { AfcError::NotEnoughData => "Not enough data", AfcError::DirNotEmpty => "Directory not empty", }; - write!(f, "{}", description) + write!(f, "{description}") } } diff --git a/idevice/src/services/afc/mod.rs b/idevice/src/services/afc/mod.rs index c84a692..c6319be 100644 --- a/idevice/src/services/afc/mod.rs +++ b/idevice/src/services/afc/mod.rs @@ -11,7 +11,7 @@ use log::warn; use opcode::{AfcFopenMode, AfcOpcode}; use packet::{AfcPacket, AfcPacketHeader}; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; pub mod errors; pub mod file; @@ -65,30 +65,7 @@ impl IdeviceService for AfcClient { obf!("com.apple.afc") } - /// Connects to the AFC service on the device - /// - /// # Arguments - /// * `provider` - The iDevice provider to use for the connection - /// - /// # Returns - /// A new `AfcClient` instance on success - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self { idevice, package_number: 0, @@ -399,11 +376,11 @@ impl AfcClient { /// /// # Returns /// A `FileDescriptor` struct for the opened file - pub async fn open( - &mut self, + pub async fn open<'f>( + &'f mut self, path: impl Into, mode: AfcFopenMode, - ) -> Result { + ) -> Result, IdeviceError> { let path = path.into(); let mut header_payload = (mode as u64).to_le_bytes().to_vec(); header_payload.extend(path.as_bytes()); diff --git a/idevice/src/services/amfi.rs b/idevice/src/services/amfi.rs index 41c92bd..3c468c8 100644 --- a/idevice/src/services/amfi.rs +++ b/idevice/src/services/amfi.rs @@ -1,8 +1,6 @@ //! Abstraction for Apple Mobile File Integrity -use plist::Dictionary; - -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the AMFI service on the device pub struct AmfiClient { @@ -16,41 +14,8 @@ impl IdeviceService for AmfiClient { obf!("com.apple.amfi.lockdown") } - /// Establishes a connection to the amfi service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `AmfiClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the amfi service port - /// 4. Establishes connection to the amfi port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } @@ -66,11 +31,10 @@ impl AmfiClient { /// Shows the developer mode option in settings in iOS 18+ /// Settings -> Privacy & Security -> Developer Mode pub async fn reveal_developer_mode_option_in_ui(&mut self) -> Result<(), IdeviceError> { - let mut request = Dictionary::new(); - request.insert("action".into(), 0.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 0, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; if res.get("success").is_some() { @@ -82,11 +46,10 @@ impl AmfiClient { /// Enables developer mode, triggering a reboot on iOS 18+ pub async fn enable_developer_mode(&mut self) -> Result<(), IdeviceError> { - let mut request = Dictionary::new(); - request.insert("action".into(), 1.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 1, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; if res.get("success").is_some() { @@ -98,11 +61,10 @@ impl AmfiClient { /// Shows the accept dialogue for enabling developer mode pub async fn accept_developer_mode(&mut self) -> Result<(), IdeviceError> { - let mut request = Dictionary::new(); - request.insert("action".into(), 2.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 2, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; if res.get("success").is_some() { @@ -114,11 +76,10 @@ impl AmfiClient { /// Gets the developer mode status pub async fn get_developer_mode_status(&mut self) -> Result { - let mut request = Dictionary::new(); - request.insert("action".into(), 3.into()); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 3, + }); + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; match res.get("success").and_then(|x| x.as_boolean()) { @@ -137,15 +98,12 @@ impl AmfiClient { &mut self, uuid: impl Into, ) -> Result { - let mut request = Dictionary::new(); - request.insert("action".into(), 4.into()); - request.insert( - "input_profile_uuid".into(), - plist::Value::String(uuid.into()), - ); - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "action": 4, + "input_profile_uuid": uuid.into(), + }); + + self.idevice.send_plist(request).await?; let res = self.idevice.read_plist().await?; match res.get("success").and_then(|x| x.as_boolean()) { diff --git a/idevice/src/services/bt_packet_logger.rs b/idevice/src/services/bt_packet_logger.rs new file mode 100644 index 0000000..ab17158 --- /dev/null +++ b/idevice/src/services/bt_packet_logger.rs @@ -0,0 +1,203 @@ +//! Abstraction for BTPacketLogger +//! You must have the Bluetooth profile installed, or you'll get no data. +//! https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth + +use std::pin::Pin; + +use futures::Stream; +use log::{debug, warn}; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// Client for interacting with the BTPacketLogger service on the device. +/// You must have the Bluetooth profile installed, or you'll get no data. +/// +/// ``https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth`` +pub struct BtPacketLoggerClient { + /// The underlying device connection with established logger service + pub idevice: Idevice, +} + +#[derive(Debug, Clone)] +pub struct BtFrame { + pub hdr: BtHeader, + pub kind: BtPacketKind, + /// H4-ready payload (first byte is H4 type: 0x01 cmd, 0x02 ACL, 0x03 SCO, 0x04 evt) + pub h4: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct BtHeader { + /// Advisory length for [kind + payload]; may not equal actual frame len - 12 + pub length: u32, // BE on the wire + pub ts_secs: u32, // BE + pub ts_usecs: u32, // BE +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BtPacketKind { + HciCmd, // 0x00 + HciEvt, // 0x01 + AclSent, // 0x02 + AclRecv, // 0x03 + ScoSent, // 0x08 + ScoRecv, // 0x09 + Other(u8), +} + +impl BtPacketKind { + fn from_byte(b: u8) -> Self { + match b { + 0x00 => BtPacketKind::HciCmd, + 0x01 => BtPacketKind::HciEvt, + 0x02 => BtPacketKind::AclSent, + 0x03 => BtPacketKind::AclRecv, + 0x08 => BtPacketKind::ScoSent, + 0x09 => BtPacketKind::ScoRecv, + x => BtPacketKind::Other(x), + } + } + fn h4_type(self) -> Option { + match self { + BtPacketKind::HciCmd => Some(0x01), + BtPacketKind::AclSent | BtPacketKind::AclRecv => Some(0x02), + BtPacketKind::ScoSent | BtPacketKind::ScoRecv => Some(0x03), + BtPacketKind::HciEvt => Some(0x04), + BtPacketKind::Other(_) => None, + } + } +} + +impl IdeviceService for BtPacketLoggerClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.bluetooth.BTPacketLogger") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl BtPacketLoggerClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Read a single *outer* frame and return one parsed record from it. + /// (This service typically delivers one record per frame.) + pub async fn next_packet( + &mut self, + ) -> Result)>, IdeviceError> { + // 2-byte outer length is **little-endian** + let len = self.idevice.read_raw(2).await?; + if len.len() != 2 { + return Ok(None); // EOF + } + let frame_len = u16::from_le_bytes([len[0], len[1]]) as usize; + + if !(13..=64 * 1024).contains(&frame_len) { + return Err(IdeviceError::UnexpectedResponse); + } + + let frame = self.idevice.read_raw(frame_len).await?; + if frame.len() != frame_len { + return Err(IdeviceError::NotEnoughBytes(frame.len(), frame_len)); + } + + // Parse header at fixed offsets (BE u32s) + let (hdr, off) = BtHeader::parse(&frame).ok_or(IdeviceError::UnexpectedResponse)?; + // packet_type at byte 12, payload starts at 13 + let kind = BtPacketKind::from_byte(frame[off]); + let payload = &frame[off + 1..]; // whatever remains + + // Optional soft check of advisory header.length + let advisory = hdr.length as usize; + let actual = 1 + payload.len(); // kind + payload + if advisory != actual { + debug!( + "BTPacketLogger advisory length {} != actual {}, proceeding", + advisory, actual + ); + } + + // Build H4 buffer (prepend type byte) + let mut h4 = Vec::with_capacity(1 + payload.len()); + if let Some(t) = kind.h4_type() { + h4.push(t); + } else { + return Ok(None); + } + h4.extend_from_slice(payload); + + Ok(Some((hdr, kind, h4))) + } + + /// Continuous stream of parsed frames. + pub fn into_stream( + mut self, + ) -> Pin> + Send>> { + Box::pin(async_stream::try_stream! { + loop { + // outer length (LE) + let len = self.idevice.read_raw(2).await?; + if len.len() != 2 { break; } + let frame_len = u16::from_le_bytes([len[0], len[1]]) as usize; + if !(13..=64 * 1024).contains(&frame_len) { + warn!("invalid frame_len {}", frame_len); + continue; + } + + // frame bytes + let frame = self.idevice.read_raw(frame_len).await?; + if frame.len() != frame_len { + Err(IdeviceError::NotEnoughBytes(frame.len(), frame_len))?; + } + + // header + kind + payload + let (hdr, off) = BtHeader::parse(&frame).ok_or(IdeviceError::UnexpectedResponse)?; + let kind = BtPacketKind::from_byte(frame[off]); + let payload = &frame[off + 1..]; + + // soft advisory check + let advisory = hdr.length as usize; + let actual = 1 + payload.len(); + if advisory != actual { + debug!("BTPacketLogger advisory length {} != actual {}", advisory, actual); + } + + // make H4 buffer + let mut h4 = Vec::with_capacity(1 + payload.len()); + if let Some(t) = kind.h4_type() { + h4.push(t); + } else { + // unknown kind + continue; + } + h4.extend_from_slice(payload); + + yield BtFrame { hdr, kind, h4 }; + } + }) + } +} + +impl BtHeader { + /// Parse 12-byte header at the start of a frame. + /// Returns (header, next_offset) where next_offset == 12 (start of packet_type). + fn parse(buf: &[u8]) -> Option<(Self, usize)> { + if buf.len() < 12 { + return None; + } + let length = u32::from_be_bytes(buf[0..4].try_into().ok()?); + let ts_secs = u32::from_be_bytes(buf[4..8].try_into().ok()?); + let ts_usecs = u32::from_be_bytes(buf[8..12].try_into().ok()?); + Some(( + BtHeader { + length, + ts_secs, + ts_usecs, + }, + 12, + )) + } +} diff --git a/idevice/src/services/companion_proxy.rs b/idevice/src/services/companion_proxy.rs new file mode 100644 index 0000000..462966d --- /dev/null +++ b/idevice/src/services/companion_proxy.rs @@ -0,0 +1,150 @@ +//! Companion Proxy is Apple's bridge to connect to the Apple Watch + +use log::warn; + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +pub struct CompanionProxy { + idevice: Idevice, +} + +pub struct CompanionProxyStream { + proxy: CompanionProxy, +} + +impl IdeviceService for CompanionProxy { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.companion_proxy") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for CompanionProxy { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.companion_proxy.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut idevice = Idevice::new(stream, ""); + idevice.rsd_checkin().await?; + Ok(Self::new(idevice)) + } +} + +impl CompanionProxy { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn get_device_registry(&mut self) -> Result, IdeviceError> { + let command = crate::plist!({ + "Command": "GetDeviceRegistry" + }); + + self.idevice.send_plist(command).await?; + let res = self.idevice.read_plist().await?; + let list = match res.get("PairedDevicesArray").and_then(|x| x.as_array()) { + Some(l) => l, + None => { + warn!("Didn't get PairedDevicesArray array"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let mut res = Vec::new(); + for l in list { + if let plist::Value::String(l) = l { + res.push(l.to_owned()); + } + } + + Ok(res) + } + + pub async fn listen_for_devices(mut self) -> Result { + let command = crate::plist!({ + "Command": "StartListeningForDevices" + }); + self.idevice.send_plist(command).await?; + + Ok(CompanionProxyStream { proxy: self }) + } + + pub async fn get_value( + &mut self, + udid: impl Into, + key: impl Into, + ) -> Result { + let udid = udid.into(); + let key = key.into(); + let command = crate::plist!({ + "Command": "GetValueFromRegistry", + "GetValueGizmoUDIDKey": udid, + "GetValueKeyKey": key.clone() + }); + self.idevice.send_plist(command).await?; + let mut res = self.idevice.read_plist().await?; + if let Some(v) = res + .remove("RetrievedValueDictionary") + .and_then(|x| x.into_dictionary()) + .and_then(|mut x| x.remove(&key)) + { + Ok(v) + } else { + Err(IdeviceError::NotFound) + } + } + + pub async fn start_forwarding_service_port( + &mut self, + port: u16, + service_name: Option<&str>, + options: Option, + ) -> Result { + let command = crate::plist!({ + "Command": "StartForwardingServicePort", + "GizmoRemotePortNumber": port, + "IsServiceLowPriority": false, + "PreferWifi": false, + "ForwardedServiceName":? service_name, + : Result<(), IdeviceError> { + let command = crate::plist!({ + "Command": "StopForwardingServicePort", + "GizmoRemotePortNumber": port + }); + + self.idevice.send_plist(command).await?; + let res = self.idevice.read_plist().await?; + if let Some(c) = res.get("Command").and_then(|x| x.as_string()) + && (c == "ComandSuccess" || c == "CommandSuccess") + // Apple you spelled this wrong, adding the right spelling just in case you fix it smh + { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } +} + +impl CompanionProxyStream { + pub async fn next(&mut self) -> Result { + self.proxy.idevice.read_plist().await + } +} diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index 5d276ab..5b177d9 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -3,22 +3,20 @@ use log::warn; use serde::Deserialize; -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::{IdeviceError, ReadWrite, RsdService, obf, xpc::XPCObject}; use super::CoreDeviceServiceClient; -impl RsdService for AppServiceClient { +impl RsdService for AppServiceClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.coredevice.appservice") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Ok(Self { inner: CoreDeviceServiceClient::new(stream).await?, }) } - - type Stream = R; } pub struct AppServiceClient { @@ -130,19 +128,13 @@ pub struct IconUuid { pub classes: Vec, } -impl<'a, R: ReadWrite + 'a> AppServiceClient { +impl AppServiceClient { pub async fn new(stream: R) -> Result { Ok(Self { inner: CoreDeviceServiceClient::new(stream).await?, }) } - pub fn box_inner(self) -> AppServiceClient> { - AppServiceClient { - inner: self.inner.box_inner(), - } - } - pub async fn list_apps( &mut self, app_clips: bool, @@ -151,15 +143,16 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { internal_apps: bool, default_apps: bool, ) -> Result, IdeviceError> { - let mut options = plist::Dictionary::new(); - options.insert("includeAppClips".into(), app_clips.into()); - options.insert("includeRemovableApps".into(), removable_apps.into()); - options.insert("includeHiddenApps".into(), hidden_apps.into()); - options.insert("includeInternalApps".into(), internal_apps.into()); - options.insert("includeDefaultApps".into(), default_apps.into()); + let options = crate::plist!(dict { + "includeAppClips": app_clips, + "includeRemovableApps": removable_apps, + "includeHiddenApps": hidden_apps, + "includeInternalApps": internal_apps, + "includeDefaultApps": default_apps, + }); let res = self .inner - .invoke("com.apple.coredevice.feature.listapps", Some(options)) + .invoke_with_plist("com.apple.coredevice.feature.listapps", options) .await?; let res = match res.as_array() { @@ -185,6 +178,16 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { Ok(desd) } + /// Launches an application by a bundle ID. + /// + /// # Notes + /// * `start_suspended` - If set to true, you will need to attach a debugger using + /// `DebugServer` to continue. + /// + /// * `stdio_uuid` - Create a new ``OpenStdioSocketClient``, read the UUID, and pass it to this + /// function. Note that if the process already has another stdio UUID, this parameter is ignored by + /// iOS. Either make sure the proccess isn't running, or pass ``kill_existing: true`` + #[allow(clippy::too_many_arguments)] // still didn't ask pub async fn launch_application( &mut self, bundle_id: impl Into, @@ -193,6 +196,7 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { start_suspended: bool, environment: Option, platform_options: Option, + stdio_uuid: Option, ) -> Result { let bundle_id = bundle_id.into(); @@ -203,20 +207,34 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { } }, "options": { - "arguments": arguments, // Now this will work directly + "arguments": arguments, "environmentVariables": environment.unwrap_or_default(), "standardIOUsesPseudoterminals": true, "startStopped": start_suspended, "terminateExisting": kill_existing, "user": { - "shortName": "mobile" + "active": true, }, "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())), }, - "standardIOIdentifiers": {} - }) - .into_dictionary() - .unwrap(); + }); + + let req: XPCObject = req.into(); + let mut req = req.to_dictionary().unwrap(); + req.insert( + "standardIOIdentifiers".into(), + match stdio_uuid { + Some(u) => { + let u = XPCObject::Uuid(u); + let mut d = crate::xpc::Dictionary::new(); + d.insert("standardInput".into(), u.clone()); + d.insert("standardOutput".into(), u.clone()); + d.insert("standardError".into(), u.clone()); + d.into() + } + None => crate::xpc::Dictionary::new().into(), + }, + ); let res = self .inner @@ -266,13 +284,11 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { ) -> Result<(), IdeviceError> { let bundle_id = bundle_id.into(); self.inner - .invoke( + .invoke_with_plist( "com.apple.coredevice.feature.uninstallapp", - Some( - crate::plist!({"bundleIdentifier": bundle_id}) - .into_dictionary() - .unwrap(), - ), + crate::plist!({"bundleIdentifier": bundle_id}) + .into_dictionary() + .unwrap(), ) .await?; @@ -286,16 +302,14 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { ) -> Result { let res = self .inner - .invoke( + .invoke_with_plist( "com.apple.coredevice.feature.sendsignaltoprocess", - Some( - crate::plist!({ - "process": { "processIdentifier": pid as i64}, - "signal": signal as i64, - }) - .into_dictionary() - .unwrap(), - ), + crate::plist!({ + "process": { "processIdentifier": pid as i64}, + "signal": signal as i64, + }) + .into_dictionary() + .unwrap(), ) .await?; @@ -321,19 +335,17 @@ impl<'a, R: ReadWrite + 'a> AppServiceClient { let bundle_id = bundle_id.into(); let res = self .inner - .invoke( + .invoke_with_plist( "com.apple.coredevice.feature.fetchappicons", - Some( - crate::plist!({ - "width": width, - "height": height, - "scale": scale, - "allowPlaceholder": allow_placeholder, - "bundleIdentifier": bundle_id - }) - .into_dictionary() - .unwrap(), - ), + crate::plist!({ + "width": width, + "height": height, + "scale": scale, + "allowPlaceholder": allow_placeholder, + "bundleIdentifier": bundle_id + }) + .into_dictionary() + .unwrap(), ) .await?; diff --git a/idevice/src/services/core_device/diagnosticsservice.rs b/idevice/src/services/core_device/diagnosticsservice.rs new file mode 100644 index 0000000..803f1fb --- /dev/null +++ b/idevice/src/services/core_device/diagnosticsservice.rs @@ -0,0 +1,72 @@ +// Jackson Coxson + +use std::pin::Pin; + +use futures::Stream; +use log::warn; + +use crate::{IdeviceError, ReadWrite, RsdService, obf}; + +impl RsdService for DiagnostisServiceClient> { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.coredevice.diagnosticsservice") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self { + inner: super::CoreDeviceServiceClient::new(stream).await?, + }) + } +} + +pub struct DiagnostisServiceClient { + inner: super::CoreDeviceServiceClient, +} + +pub struct SysdiagnoseResponse<'a> { + pub preferred_filename: String, + pub stream: Pin, IdeviceError>> + 'a>>, + pub expected_length: usize, +} + +impl DiagnostisServiceClient { + pub async fn capture_sysdiagnose<'a>( + &'a mut self, + dry_run: bool, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "options": { + "collectFullLogs": true + }, + "isDryRun": dry_run + }) + .into_dictionary() + .unwrap(); + + let res = self + .inner + .invoke_with_plist("com.apple.coredevice.feature.capturesysdiagnose", req) + .await?; + + if let Some(len) = res + .as_dictionary() + .and_then(|x| x.get("fileTransfer")) + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("expectedLength")) + .and_then(|x| x.as_unsigned_integer()) + && let Some(name) = res + .as_dictionary() + .and_then(|x| x.get("preferredFilename")) + .and_then(|x| x.as_string()) + { + Ok(SysdiagnoseResponse { + stream: Box::pin(self.inner.inner.iter_file_chunks(len as usize, 0)), + preferred_filename: name.to_string(), + expected_length: len as usize, + }) + } else { + warn!("Did not get expected responses from RemoteXPC"); + Err(IdeviceError::UnexpectedResponse) + } + } +} diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs index b5151d3..1f405ce 100644 --- a/idevice/src/services/core_device/mod.rs +++ b/idevice/src/services/core_device/mod.rs @@ -4,12 +4,16 @@ use log::warn; use crate::{ - xpc::{self, XPCObject}, IdeviceError, ReadWrite, RemoteXpcClient, + xpc::{self, XPCObject}, }; mod app_service; +mod diagnosticsservice; +mod openstdiosocket; pub use app_service::*; +pub use diagnosticsservice::*; +pub use openstdiosocket::*; const CORE_SERVICE_VERSION: &str = "443.18"; @@ -17,26 +21,33 @@ pub struct CoreDeviceServiceClient { inner: RemoteXpcClient, } -impl<'a, R: ReadWrite + 'a> CoreDeviceServiceClient { +impl CoreDeviceServiceClient { pub async fn new(inner: R) -> Result { let mut client = RemoteXpcClient::new(inner).await?; client.do_handshake().await?; Ok(Self { inner: client }) } - pub fn box_inner(self) -> CoreDeviceServiceClient> { - CoreDeviceServiceClient { - inner: self.inner.box_inner(), - } + pub async fn invoke_with_plist( + &mut self, + feature: impl Into, + input: plist::Dictionary, + ) -> Result { + let input: XPCObject = plist::Value::Dictionary(input).into(); + let input = input.to_dictionary().unwrap(); + self.invoke(feature, Some(input)).await } pub async fn invoke( &mut self, feature: impl Into, - input: Option, + input: Option, ) -> Result { let feature = feature.into(); - let input = input.unwrap_or_default(); + let input: crate::xpc::XPCObject = match input { + Some(i) => i.into(), + None => crate::xpc::Dictionary::new().into(), + }; let mut req = xpc::Dictionary::new(); req.insert( @@ -56,10 +67,7 @@ impl<'a, R: ReadWrite + 'a> CoreDeviceServiceClient { "CoreDevice.featureIdentifier".into(), XPCObject::String(feature), ); - req.insert( - "CoreDevice.input".into(), - plist::Value::Dictionary(input).into(), - ); + req.insert("CoreDevice.input".into(), input); req.insert( "CoreDevice.invocationIdentifier".into(), XPCObject::String(uuid::Uuid::new_v4().to_string()), diff --git a/idevice/src/services/core_device/openstdiosocket.rs b/idevice/src/services/core_device/openstdiosocket.rs new file mode 100644 index 0000000..e5630ce --- /dev/null +++ b/idevice/src/services/core_device/openstdiosocket.rs @@ -0,0 +1,33 @@ +// Jackson Coxson + +use tokio::io::AsyncReadExt; + +use crate::{IdeviceError, ReadWrite, RsdService, obf}; + +impl RsdService for OpenStdioSocketClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.coredevice.openstdiosocket") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self { inner: stream }) + } +} + +/// Call ``read_uuid`` to get the UUID. Pass that to app service launch to connect to the stream of +/// the launched app. Inner is exposed to read and write to, using Tokio's AsyncReadExt/AsyncWriteExt +pub struct OpenStdioSocketClient { + pub inner: Box, +} + +impl OpenStdioSocketClient { + /// iOS assigns a UUID to a newly opened stream. That UUID is then passed to the launch + /// parameters of app service to start a stream. + pub async fn read_uuid(&mut self) -> Result { + let mut buf = [0u8; 16]; + self.inner.read_exact(&mut buf).await?; + + let res = uuid::Uuid::from_bytes(buf); + Ok(res) + } +} diff --git a/idevice/src/services/core_device_proxy.rs b/idevice/src/services/core_device_proxy.rs index 6c714e3..36e4f5c 100644 --- a/idevice/src/services/core_device_proxy.rs +++ b/idevice/src/services/core_device_proxy.rs @@ -13,7 +13,7 @@ //! # Features //! - `tunnel_tcp_stack`: Enables software TCP/IP tunnel creation using a virtual adapter. See the tcp moduel. -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; use byteorder::{BigEndian, WriteBytesExt}; use serde::{Deserialize, Serialize}; @@ -97,34 +97,7 @@ impl IdeviceService for CoreDeviceProxy { obf!("com.apple.internal.devicecompute.CoreDeviceProxy") } - /// Connects to the CoreDeviceProxy service - /// - /// # Arguments - /// - /// * `provider` - An implementation of `IdeviceProvider` that supplies - /// pairing data and socket connections. - /// - /// # Returns - /// - /// * `Ok(CoreDeviceProxy)` if connection and handshake succeed. - /// * `Err(IdeviceError)` if any step fails. - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Self::new(idevice).await } } diff --git a/idevice/src/services/crashreportcopymobile.rs b/idevice/src/services/crashreportcopymobile.rs index afc2b1f..f98f1a4 100644 --- a/idevice/src/services/crashreportcopymobile.rs +++ b/idevice/src/services/crashreportcopymobile.rs @@ -9,7 +9,7 @@ use log::{debug, warn}; -use crate::{afc::AfcClient, lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, afc::AfcClient, lockdown::LockdownClient, obf}; /// Client for managing crash logs on an iOS device. /// @@ -26,43 +26,8 @@ impl IdeviceService for CrashReportCopyMobileClient { obf!("com.apple.crashreportcopymobile") } - /// Connects to the CrashReportCopyMobile service on the device. - /// - /// # Arguments - /// * `provider` - The provider used to access the device and pairing info. - /// - /// # Returns - /// A connected `CrashReportCopyMobileClient`. - /// - /// # Errors - /// Returns `IdeviceError` if the connection fails at any stage. - /// - /// # Process - /// 1. Connects to the lockdownd service. - /// 2. Starts a lockdown session. - /// 3. Requests the CrashReportCopyMobile service. - /// 4. Establishes a connection to the service. - /// 5. Performs SSL handshake if required. - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { - afc_client: AfcClient::new(idevice), - }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/services/debug_proxy.rs b/idevice/src/services/debug_proxy.rs index 97f230b..7e5c28f 100644 --- a/idevice/src/services/debug_proxy.rs +++ b/idevice/src/services/debug_proxy.rs @@ -8,21 +8,19 @@ use log::debug; use std::fmt::Write; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::{IdeviceError, ReadWrite, RsdService, obf}; -impl RsdService for DebugProxyClient { +impl RsdService for DebugProxyClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.internal.dt.remote.debugproxy") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Ok(Self { socket: stream, noack_mode: false, }) } - - type Stream = R; } /// Client for interacting with the iOS debug proxy service @@ -107,10 +105,10 @@ impl DebugProxyClient { let checksum = calculate_checksum(&packet_data); // Construct the full packet - let packet = format!("${}#{}", packet_data, checksum); + let packet = format!("${packet_data}#{checksum}"); // Log the packet for debugging - debug!("Sending packet: {}", packet); + debug!("Sending packet: {packet}"); // Send the packet self.socket.write_all(packet.as_bytes()).await?; @@ -237,7 +235,7 @@ impl DebugProxyClient { // Hex encode the argument for byte in arg.bytes() { - let hex = format!("{:02X}", byte); + let hex = format!("{byte:02X}"); pkt[pktp..pktp + 2].copy_from_slice(hex.as_bytes()); pktp += 2; } @@ -290,7 +288,7 @@ impl DebugProxyClient { /// between '$' and '#', formatted as two lowercase hex digits. fn calculate_checksum(data: &str) -> String { let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte)); - format!("{:02x}", checksum) + format!("{checksum:02x}") } /// Hex-encodes bytes as uppercase string diff --git a/idevice/src/services/diagnostics_relay.rs b/idevice/src/services/diagnostics_relay.rs new file mode 100644 index 0000000..8f65654 --- /dev/null +++ b/idevice/src/services/diagnostics_relay.rs @@ -0,0 +1,261 @@ +//! Diagnostics Relay + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// Client for interacting with the Diagnostics Relay +pub struct DiagnosticsRelayClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for DiagnosticsRelayClient { + /// Returns the service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobile.diagnostics_relay") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl DiagnosticsRelayClient { + /// Creates a new client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Requests data from the IO registry + /// + /// # Arguments + /// * `current_plane` - The plane to request the tree as + /// * `entry_name` - The entry to get + /// * `entry_class` - The class to filter by + /// + /// # Returns + /// A plist of the tree on success + pub async fn ioregistry( + &mut self, + current_plane: Option<&str>, + entry_name: Option<&str>, + entry_class: Option<&str>, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "Request": "IORegistry", + "CurrentPlane":? current_plane, + "EntryName":? entry_name, + "EntryClass":? entry_class, + }); + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res + .remove("Diagnostics") + .and_then(|x| x.into_dictionary()) + .and_then(|mut x| x.remove("IORegistry")) + .and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests MobileGestalt information from the device + /// + /// # Arguments + /// * `keys` - Optional list of specific keys to request. If None, requests all available keys + /// + /// # Returns + /// A dictionary containing the requested MobileGestalt information + pub async fn mobilegestalt( + &mut self, + keys: Option>, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "Request": "MobileGestalt", + "MobileGestaltKeys":? keys, + }); + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests gas gauge information from the device + /// + /// # Returns + /// A dictionary containing gas gauge (battery) information + pub async fn gasguage(&mut self) -> Result, IdeviceError> { + let req = crate::plist!({ + "Request": "GasGauge" + }); + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests NAND information from the device + /// + /// # Returns + /// A dictionary containing NAND flash information + pub async fn nand(&mut self) -> Result, IdeviceError> { + let req = crate::plist!({ + "Request": "NAND" + }); + + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Requests all available diagnostics information + /// + /// # Returns + /// A dictionary containing all diagnostics information + pub async fn all(&mut self) -> Result, IdeviceError> { + let req = crate::plist!({ + "Request": "All" + }); + + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Restarts the device + /// + /// # Returns + /// Result indicating success or failure + pub async fn restart(&mut self) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Request": "Restart", + }); + + self.idevice.send_plist(req).await?; + let res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Shuts down the device + /// + /// # Returns + /// Result indicating success or failure + pub async fn shutdown(&mut self) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Request": "Shutdown" + }); + + self.idevice.send_plist(req).await?; + let res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Puts the device to sleep + /// + /// # Returns + /// Result indicating success or failure + pub async fn sleep(&mut self) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Request": "Sleep" + }); + + self.idevice.send_plist(req).await?; + let res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Requests WiFi diagnostics from the device + pub async fn wifi(&mut self) -> Result, IdeviceError> { + let req = crate::plist!({ + "Request": "WiFi" + }); + + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist().await?; + + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => {} + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + } + + let res = res.remove("Diagnostics").and_then(|x| x.into_dictionary()); + + Ok(res) + } + + /// Sends Goodbye request signaling end of communication + pub async fn goodbye(&mut self) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Request": "Goodbye" + }); + + self.idevice.send_plist(req).await?; + let res = self.idevice.read_plist().await?; + match res.get("Status").and_then(|x| x.as_string()) { + Some("Success") => Ok(()), + Some("UnknownRequest") => Err(IdeviceError::UnexpectedResponse), + _ => Err(IdeviceError::UnexpectedResponse), + } + } +} diff --git a/idevice/src/services/dvt/location_simulation.rs b/idevice/src/services/dvt/location_simulation.rs index ecea710..ec90f51 100644 --- a/idevice/src/services/dvt/location_simulation.rs +++ b/idevice/src/services/dvt/location_simulation.rs @@ -37,11 +37,12 @@ use plist::Value; use crate::{ + IdeviceError, ReadWrite, dvt::{ message::AuxValue, remote_server::{Channel, RemoteServerClient}, }, - obf, IdeviceError, ReadWrite, + obf, }; /// A client for the location simulation service diff --git a/idevice/src/services/dvt/message.rs b/idevice/src/services/dvt/message.rs index 156d352..4298e77 100644 --- a/idevice/src/services/dvt/message.rs +++ b/idevice/src/services/dvt/message.rs @@ -278,9 +278,9 @@ impl Aux { let mut res = Vec::new(); let buffer_size = 496_u32; res.extend_from_slice(&buffer_size.to_le_bytes()); // TODO: find what - // this means and how to actually serialize it - // go-ios just uses 496 - // pymobiledevice3 doesn't seem to parse the header at all + // this means and how to actually serialize it + // go-ios just uses 496 + // pymobiledevice3 doesn't seem to parse the header at all res.extend_from_slice(&0_u32.to_le_bytes()); res.extend_from_slice(&(values_payload.len() as u32).to_le_bytes()); res.extend_from_slice(&0_u32.to_le_bytes()); @@ -501,15 +501,15 @@ impl Message { impl std::fmt::Debug for AuxValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AuxValue::String(s) => write!(f, "String({:?})", s), + AuxValue::String(s) => write!(f, "String({s:?})"), AuxValue::Array(arr) => write!( f, "Array(len={}, first_bytes={:?})", arr.len(), &arr[..arr.len().min(10)] ), // Show only first 10 bytes - AuxValue::U32(n) => write!(f, "U32({})", n), - AuxValue::I64(n) => write!(f, "I64({})", n), + AuxValue::U32(n) => write!(f, "U32({n})"), + AuxValue::I64(n) => write!(f, "I64({n})"), } } } diff --git a/idevice/src/services/dvt/mod.rs b/idevice/src/services/dvt/mod.rs index fced62f..d7db667 100644 --- a/idevice/src/services/dvt/mod.rs +++ b/idevice/src/services/dvt/mod.rs @@ -1,6 +1,8 @@ // Jackson Coxson -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::provider::IdeviceProvider; +use crate::services::lockdown::LockdownClient; +use crate::{Idevice, IdeviceError, ReadWrite, RsdService, obf}; #[cfg(feature = "location_simulation")] pub mod location_simulation; @@ -8,14 +10,74 @@ pub mod message; pub mod process_control; pub mod remote_server; -impl RsdService for remote_server::RemoteServerClient { +impl RsdService for remote_server::RemoteServerClient> { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.instruments.dtservicehub") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Ok(Self::new(stream)) } - - type Stream = R; +} + +// iOS version support notes: +// - com.apple.instruments.dtservicehub (RSD/XPC over HTTP2) is used on iOS 17+. +// - com.apple.instruments.remoteserver is available on pre-iOS 17 (and many older versions). +// - com.apple.instruments.remoteserver.DVTSecureSocketProxy is used by some iOS 14 builds. +// +// This impl enables Lockdown-based connection to Instruments Remote Server for iOS < 17 +// by reusing the same RemoteServerClient but sourcing the transport from StartService. +impl crate::IdeviceService for remote_server::RemoteServerClient> { + fn service_name() -> std::borrow::Cow<'static, str> { + // Primary name for Instruments Remote Server + obf!("com.apple.instruments.remoteserver") + } + + #[allow(async_fn_in_trait)] + async fn connect(provider: &dyn IdeviceProvider) -> Result { + // Establish Lockdown session + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + // Try main Instruments service first, then DVTSecureSocketProxy (seen on iOS 14) + let try_names = [ + obf!("com.apple.instruments.remoteserver"), + obf!("com.apple.instruments.remoteserver.DVTSecureSocketProxy"), + ]; + + let mut last_err: Option = None; + for name in try_names { + match lockdown.start_service(name).await { + Ok((port, ssl)) => { + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + // Convert to transport and build client + let socket = idevice + .get_socket() + .ok_or(IdeviceError::NoEstablishedConnection)?; + return Ok(remote_server::RemoteServerClient::new(socket)); + } + Err(e) => { + last_err = Some(e); + } + } + } + + Err(last_err.unwrap_or(IdeviceError::ServiceNotFound)) + } + + #[allow(async_fn_in_trait)] + async fn from_stream(idevice: Idevice) -> Result { + // Not used in our overridden connect path, but implemented for completeness + let socket = idevice + .get_socket() + .ok_or(IdeviceError::NoEstablishedConnection)?; + Ok(remote_server::RemoteServerClient::new(socket)) + } } diff --git a/idevice/src/services/dvt/process_control.rs b/idevice/src/services/dvt/process_control.rs index 8ae7e15..f31d046 100644 --- a/idevice/src/services/dvt/process_control.rs +++ b/idevice/src/services/dvt/process_control.rs @@ -37,7 +37,7 @@ use log::warn; use plist::{Dictionary, Value}; -use crate::{dvt::message::AuxValue, obf, IdeviceError, ReadWrite}; +use crate::{IdeviceError, ReadWrite, dvt::message::AuxValue, obf}; use super::remote_server::{Channel, RemoteServerClient}; @@ -98,9 +98,10 @@ impl<'a, R: ReadWrite> ProcessControlClient<'a, R> { "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:" .into(), ); - let mut options = Dictionary::new(); - options.insert("StartSuspendedKey".into(), start_suspended.into()); - options.insert("KillExisting".into(), kill_existing.into()); + let options = crate::plist!(dict { + "StartSuspendedKey": start_suspended, + "KillExisting": kill_existing + }); let env_vars = match env_vars { Some(e) => e, diff --git a/idevice/src/services/dvt/remote_server.rs b/idevice/src/services/dvt/remote_server.rs index 91e9684..c4111a8 100644 --- a/idevice/src/services/dvt/remote_server.rs +++ b/idevice/src/services/dvt/remote_server.rs @@ -55,8 +55,8 @@ use log::{debug, warn}; use tokio::io::AsyncWriteExt; use crate::{ - dvt::message::{Aux, AuxValue, Message, MessageHeader, PayloadHeader}, IdeviceError, ReadWrite, + dvt::message::{Aux, AuxValue, Message, MessageHeader, PayloadHeader}, }; /// Message type identifier for instruments protocol @@ -112,7 +112,7 @@ impl RemoteServerClient { } /// Returns a handle to the root channel (channel 0) - pub fn root_channel(&mut self) -> Channel { + pub fn root_channel<'c>(&'c mut self) -> Channel<'c, R> { Channel { client: self, channel: 0, @@ -131,10 +131,10 @@ impl RemoteServerClient { /// # Errors /// * `IdeviceError::UnexpectedResponse` if server responds with unexpected data /// * Other IO or serialization errors - pub async fn make_channel( - &mut self, + pub async fn make_channel<'c>( + &'c mut self, identifier: impl Into, - ) -> Result, IdeviceError> { + ) -> Result, IdeviceError> { let code = self.new_channel; self.new_channel += 1; @@ -164,7 +164,7 @@ impl RemoteServerClient { self.build_channel(code) } - fn build_channel(&mut self, code: u32) -> Result, IdeviceError> { + fn build_channel<'c>(&'c mut self, code: u32) -> Result, IdeviceError> { Ok(Channel { client: self, channel: code, diff --git a/idevice/src/services/heartbeat.rs b/idevice/src/services/heartbeat.rs index cf8d144..f198d48 100644 --- a/idevice/src/services/heartbeat.rs +++ b/idevice/src/services/heartbeat.rs @@ -3,7 +3,7 @@ //! iOS automatically closes service connections if there is no heartbeat client connected and //! responding. -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS device heartbeat service /// @@ -22,42 +22,8 @@ impl IdeviceService for HeartbeatClient { fn service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.mobile.heartbeat") } - - /// Establishes a connection to the heartbeat service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `HeartbeatClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the heartbeat service port - /// 4. Establishes connection to the heartbeat port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } @@ -124,11 +90,10 @@ impl HeartbeatClient { /// # Errors /// Returns `IdeviceError` if the message fails to send pub async fn send_polo(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "Polo".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req.clone())) - .await?; + let req = crate::plist!({ + "Command": "Polo" + }); + self.idevice.send_plist(req).await?; Ok(()) } } diff --git a/idevice/src/services/house_arrest.rs b/idevice/src/services/house_arrest.rs index 2a4c619..b7fb73e 100644 --- a/idevice/src/services/house_arrest.rs +++ b/idevice/src/services/house_arrest.rs @@ -4,9 +4,7 @@ //! installed on an iOS device. This is typically used for file transfer and inspection of //! app-specific data during development or diagnostics. -use plist::{Dictionary, Value}; - -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; use super::afc::AfcClient; @@ -25,41 +23,8 @@ impl IdeviceService for HouseArrestClient { obf!("com.apple.mobile.house_arrest") } - /// Establishes a connection to the HouseArrest service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `HouseArrestClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connect to the lockdownd service - /// 2. Start a lockdown session - /// 3. Request the HouseArrest service - /// 4. Connect to the returned service port - /// 5. Start TLS if required by the service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } @@ -124,10 +89,12 @@ impl HouseArrestClient { /// # Errors /// Returns `IdeviceError` if the request or AFC setup fails async fn vend(mut self, bundle_id: String, cmd: String) -> Result { - let mut req = Dictionary::new(); - req.insert("Command".into(), cmd.into()); - req.insert("Identifier".into(), bundle_id.into()); - self.idevice.send_plist(Value::Dictionary(req)).await?; + let req = crate::plist!({ + "Command": cmd, + "Identifier": bundle_id + }); + + self.idevice.send_plist(req).await?; self.idevice.read_plist().await?; Ok(AfcClient::new(self.idevice)) diff --git a/idevice/src/services/installation_proxy.rs b/idevice/src/services/installation_proxy.rs index f338d5d..325d210 100644 --- a/idevice/src/services/installation_proxy.rs +++ b/idevice/src/services/installation_proxy.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use log::warn; use plist::Dictionary; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS installation proxy service /// @@ -25,39 +25,7 @@ impl IdeviceService for InstallationProxyClient { obf!("com.apple.mobile.installation_proxy") } - /// Establishes a connection to the installation proxy service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `InstallationProxyClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the installation proxy service port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self::new(idevice)) } } @@ -98,26 +66,19 @@ impl InstallationProxyClient { /// ``` pub async fn get_apps( &mut self, - application_type: Option, + application_type: Option<&str>, bundle_identifiers: Option>, ) -> Result, IdeviceError> { - let application_type = application_type.unwrap_or("Any".to_string()); - let mut options = plist::Dictionary::new(); - if let Some(ids) = bundle_identifiers { - let ids = ids - .into_iter() - .map(plist::Value::String) - .collect::>(); - options.insert("BundleIDs".into(), ids.into()); - } - options.insert("ApplicationType".into(), application_type.into()); + let application_type = application_type.unwrap_or("Any"); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "Lookup".into()); - req.insert("ClientOptions".into(), plist::Value::Dictionary(options)); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "Lookup", + "ClientOptions": { + "ApplicationType": application_type, + "BundleIDs":? bundle_identifiers, + } + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("LookupResult") { @@ -187,14 +148,13 @@ impl InstallationProxyClient { let package_path = package_path.into(); let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Install".into()); - command.insert("ClientOptions".into(), options); - command.insert("PackagePath".into(), package_path.into()); + let command = crate::plist!({ + "Command": "Install", + "ClientOptions": options, + "PackagePath": package_path, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; self.watch_completion(callback, state).await } @@ -252,14 +212,13 @@ impl InstallationProxyClient { let package_path = package_path.into(); let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Upgrade".into()); - command.insert("ClientOptions".into(), options); - command.insert("PackagePath".into(), package_path.into()); + let command = crate::plist!({ + "Command": "Upgrade", + "ClientOptions": options, + "PackagePath": package_path, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; self.watch_completion(callback, state).await } @@ -317,14 +276,13 @@ impl InstallationProxyClient { let bundle_id = bundle_id.into(); let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Uninstall".into()); - command.insert("ApplicationIdentifier".into(), bundle_id.into()); - command.insert("ClientOptions".into(), options); + let command = crate::plist!({ + "Command": "Uninstall", + "ApplicationIdentifier": bundle_id, + "ClientOptions": options, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; self.watch_completion(callback, state).await } @@ -349,14 +307,13 @@ impl InstallationProxyClient { ) -> Result { let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "CheckCapabilitiesMatch".into()); - command.insert("ClientOptions".into(), options); - command.insert("Capabilities".into(), capabilities.into()); + let command = crate::plist!({ + "Command": "CheckCapabilitiesMatch", + "ClientOptions": options, + "Capabilities": capabilities + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; let mut res = self.idevice.read_plist().await?; if let Some(caps) = res.remove("LookupResult").and_then(|x| x.as_boolean()) { @@ -387,13 +344,12 @@ impl InstallationProxyClient { ) -> Result, IdeviceError> { let options = options.unwrap_or(plist::Value::Dictionary(Dictionary::new())); - let mut command = Dictionary::new(); - command.insert("Command".into(), "Browse".into()); - command.insert("ClientOptions".into(), options); + let command = crate::plist!({ + "Command": "Browse", + "ClientOptions": options, + }); - self.idevice - .send_plist(plist::Value::Dictionary(command)) - .await?; + self.idevice.send_plist(command).await?; let mut values = Vec::new(); loop { @@ -408,10 +364,10 @@ impl InstallationProxyClient { break; } - if let Some(status) = res.get("Status").and_then(|x| x.as_string()) { - if status == "Complete" { - break; - } + if let Some(status) = res.get("Status").and_then(|x| x.as_string()) + && status == "Complete" + { + break; } } Ok(values) @@ -456,10 +412,10 @@ impl InstallationProxyClient { callback((c, state.clone())).await; } - if let Some(c) = res.remove("Status").and_then(|x| x.into_string()) { - if c == "Complete" { - break; - } + if let Some(c) = res.remove("Status").and_then(|x| x.into_string()) + && c == "Complete" + { + break; } } Ok(()) diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index 46a3589..7c07972 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -7,7 +7,7 @@ use log::error; use plist::Value; use serde::{Deserialize, Serialize}; -use crate::{obf, pairing_file, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf, pairing_file}; /// Client for interacting with the iOS lockdown service /// @@ -42,6 +42,10 @@ impl IdeviceService for LockdownClient { let idevice = provider.connect(Self::LOCKDOWND_PORT).await?; Ok(Self::new(idevice)) } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } } /// Internal structure for lockdown protocol requests @@ -86,23 +90,16 @@ impl LockdownClient { /// ``` pub async fn get_value( &mut self, - key: impl Into, - domain: Option, + key: Option<&str>, + domain: Option<&str>, ) -> Result { - let key = key.into(); - - let mut request = plist::Dictionary::new(); - request.insert("Label".into(), self.idevice.label.clone().into()); - request.insert("Request".into(), "GetValue".into()); - request.insert("Key".into(), key.into()); - - if let Some(domain) = domain { - request.insert("Domain".into(), domain.into()); - } - - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + let request = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "GetValue", + "Key":? key, + "Domain":? domain + }); + self.idevice.send_plist(request).await?; let message: plist::Dictionary = self.idevice.read_plist().await?; match message.get("Value") { Some(m) => Ok(m.to_owned()), @@ -110,43 +107,6 @@ impl LockdownClient { } } - /// Retrieves all available values from the device - /// - /// # Returns - /// A dictionary containing all device values - /// - /// # Errors - /// Returns `IdeviceError` if: - /// - Communication fails - /// - The response is malformed - /// - /// # Example - /// ```rust - /// let all_values = client.get_all_values().await?; - /// for (key, value) in all_values { - /// println!("{}: {:?}", key, value); - /// } - /// ``` - pub async fn get_all_values( - &mut self, - domain: Option, - ) -> Result { - let mut request = plist::Dictionary::new(); - request.insert("Label".into(), self.idevice.label.clone().into()); - request.insert("Request".into(), "GetValue".into()); - if let Some(domain) = domain { - request.insert("Domain".into(), domain.into()); - } - - let message = plist::to_value(&request)?; - self.idevice.send_plist(message).await?; - let message: plist::Dictionary = self.idevice.read_plist().await?; - match message.get("Value") { - Some(m) => Ok(plist::from_value(m)?), - None => Err(IdeviceError::UnexpectedResponse), - } - } - /// Sets a value on the device /// /// # Arguments @@ -167,23 +127,19 @@ impl LockdownClient { &mut self, key: impl Into, value: Value, - domain: Option, + domain: Option<&str>, ) -> Result<(), IdeviceError> { let key = key.into(); - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.idevice.label.clone().into()); - req.insert("Request".into(), "SetValue".into()); - req.insert("Key".into(), key.into()); - req.insert("Value".into(), value); + let req = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "SetValue", + "Key": key, + "Value": value, + "Domain":? domain + }); - if let Some(domain) = domain { - req.insert("Domain".into(), domain.into()); - } - - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; self.idevice.read_plist().await?; Ok(()) @@ -210,28 +166,14 @@ impl LockdownClient { return Err(IdeviceError::NoEstablishedConnection); } - let mut request = plist::Dictionary::new(); - request.insert( - "Label".to_string(), - plist::Value::String(self.idevice.label.clone()), - ); + let request = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "StartSession", + "HostID": pairing_file.host_id.clone(), + "SystemBUID": pairing_file.system_buid.clone() - request.insert( - "Request".to_string(), - plist::Value::String("StartSession".to_string()), - ); - request.insert( - "HostID".to_string(), - plist::Value::String(pairing_file.host_id.clone()), - ); - request.insert( - "SystemBUID".to_string(), - plist::Value::String(pairing_file.system_buid.clone()), - ); - - self.idevice - .send_plist(plist::Value::Dictionary(request)) - .await?; + }); + self.idevice.send_plist(request).await?; let response = self.idevice.read_plist().await?; match response.get("EnableSessionSSL") { @@ -269,12 +211,11 @@ impl LockdownClient { identifier: impl Into, ) -> Result<(u16, bool), IdeviceError> { let identifier = identifier.into(); - let mut req = plist::Dictionary::new(); - req.insert("Request".into(), "StartService".into()); - req.insert("Service".into(), identifier.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Request": "StartService", + "Service": identifier, + }); + self.idevice.send_plist(req).await?; let response = self.idevice.read_plist().await?; let ssl = match response.get("EnableServiceSSL") { @@ -321,7 +262,7 @@ impl LockdownClient { let host_id = host_id.into(); let system_buid = system_buid.into(); - let pub_key = self.get_value("DevicePublicKey", None).await?; + let pub_key = self.get_value(Some("DevicePublicKey"), None).await?; let pub_key = match pub_key.as_data().map(|x| x.to_vec()) { Some(p) => p, None => { @@ -330,7 +271,7 @@ impl LockdownClient { } }; - let wifi_mac = self.get_value("WiFiAddress", None).await?; + let wifi_mac = self.get_value(Some("WiFiAddress"), None).await?; let wifi_mac = match wifi_mac.as_string() { Some(w) => w, None => { @@ -340,37 +281,29 @@ impl LockdownClient { }; let ca = crate::ca::generate_certificates(&pub_key, None).unwrap(); - let mut pair_record = plist::Dictionary::new(); - pair_record.insert("DevicePublicKey".into(), plist::Value::Data(pub_key)); - pair_record.insert("DeviceCertificate".into(), plist::Value::Data(ca.dev_cert)); - pair_record.insert( - "HostCertificate".into(), - plist::Value::Data(ca.host_cert.clone()), - ); - pair_record.insert("HostID".into(), host_id.into()); - pair_record.insert("RootCertificate".into(), plist::Value::Data(ca.host_cert)); - pair_record.insert( - "RootPrivateKey".into(), - plist::Value::Data(ca.private_key.clone()), - ); - pair_record.insert("WiFiMACAddress".into(), wifi_mac.into()); - pair_record.insert("SystemBUID".into(), system_buid.into()); + let mut pair_record = crate::plist!(dict { + "DevicePublicKey": pub_key, + "DeviceCertificate": ca.dev_cert, + "HostCertificate": ca.host_cert.clone(), + "HostID": host_id, + "RootCertificate": ca.host_cert, + "RootPrivateKey": ca.private_key.clone(), + "WiFiMACAddress": wifi_mac, + "SystemBUID": system_buid, + }); - let mut options = plist::Dictionary::new(); - options.insert("ExtendedPairingErrors".into(), true.into()); - - let mut req = plist::Dictionary::new(); - req.insert("Label".into(), self.idevice.label.clone().into()); - req.insert("Request".into(), "Pair".into()); - req.insert( - "PairRecord".into(), - plist::Value::Dictionary(pair_record.clone()), - ); - req.insert("ProtocolVersion".into(), "2".into()); - req.insert("PairingOptions".into(), plist::Value::Dictionary(options)); + let req = crate::plist!({ + "Label": self.idevice.label.clone(), + "Request": "Pair", + "PairRecord": pair_record.clone(), + "ProtocolVersion": "2", + "PairingOptions": { + "ExtendedPairingErrors": true + } + }); loop { - self.idevice.send_plist(req.clone().into()).await?; + self.idevice.send_plist(req.clone()).await?; match self.idevice.read_plist().await { Ok(escrow) => { pair_record.insert("HostPrivateKey".into(), plist::Value::Data(ca.private_key)); diff --git a/idevice/src/services/misagent.rs b/idevice/src/services/misagent.rs index 0018ba1..c033908 100644 --- a/idevice/src/services/misagent.rs +++ b/idevice/src/services/misagent.rs @@ -4,9 +4,8 @@ //! which manages provisioning profiles and certificates. use log::warn; -use plist::Dictionary; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; /// Client for interacting with the iOS misagent service /// @@ -19,45 +18,25 @@ pub struct MisagentClient { pub idevice: Idevice, } +impl RsdService for MisagentClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.misagent.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut stream = Idevice::new(stream, ""); + stream.rsd_checkin().await?; + Ok(Self::new(stream)) + } +} + impl IdeviceService for MisagentClient { /// Returns the misagent service name as registered with lockdownd fn service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.misagent") } - /// Establishes a connection to the misagent service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `MisagentClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the misagent service port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self::new(idevice)) } } @@ -91,21 +70,20 @@ impl MisagentClient { /// client.install(profile_data).await?; /// ``` pub async fn install(&mut self, profile: Vec) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("MessageType".into(), "Install".into()); - req.insert("Profile".into(), plist::Value::Data(profile)); - req.insert("ProfileType".into(), "Provisioning".into()); + let req = crate::plist!({ + "MessageType": "Install", + "Profile": profile, + "ProfileType": "Provisioning" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("Status") { Some(plist::Value::Integer(status)) => { if let Some(status) = status.as_unsigned() { - if status == 1 { + if status == 0 { Ok(()) } else { Err(IdeviceError::MisagentFailure) @@ -141,21 +119,20 @@ impl MisagentClient { /// client.remove("asdf").await?; /// ``` pub async fn remove(&mut self, id: &str) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("MessageType".into(), "Remove".into()); - req.insert("ProfileID".into(), id.into()); - req.insert("ProfileType".into(), "Provisioning".into()); + let req = crate::plist!({ + "MessageType": "Remove", + "ProfileID": id, + "ProfileType": "Provisioning" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("Status") { Some(plist::Value::Integer(status)) => { if let Some(status) = status.as_unsigned() { - if status == 1 { + if status == 0 { Ok(()) } else { Err(IdeviceError::MisagentFailure) @@ -190,13 +167,12 @@ impl MisagentClient { /// } /// ``` pub async fn copy_all(&mut self) -> Result>, IdeviceError> { - let mut req = Dictionary::new(); - req.insert("MessageType".into(), "CopyAll".into()); - req.insert("ProfileType".into(), "Provisioning".into()); + let req = crate::plist!({ + "MessageType": "CopyAll", + "ProfileType": "Provisioning" + }); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("Payload") { diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index 78c71d6..432acef 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -9,7 +9,7 @@ use log::debug; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; use sha2::{Digest, Sha384}; #[cfg(feature = "tss")] @@ -33,41 +33,8 @@ impl IdeviceService for ImageMounter { obf!("com.apple.mobile.mobile_image_mounter") } - /// Establishes a connection to the image mounter service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `ImageMounter` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the image mounter service port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } @@ -88,11 +55,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if communication fails or response is malformed pub async fn copy_devices(&mut self) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "CopyDevices".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "CopyDevices" + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("EntryList") { @@ -113,15 +79,14 @@ impl ImageMounter { /// Returns `IdeviceError::NotFound` if image doesn't exist pub async fn lookup_image( &mut self, - image_type: impl Into, + image_type: impl Into<&str>, ) -> Result, IdeviceError> { let image_type = image_type.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "LookupImage".into()); - req.insert("ImageType".into(), image_type.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "LookupImage", + "ImageType": image_type + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("ImageSignature") { @@ -185,14 +150,13 @@ impl ImageMounter { } }; - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "ReceiveBytes".into()); - req.insert("ImageType".into(), image_type.into()); - req.insert("ImageSize".into(), image_size.into()); - req.insert("ImageSignature".into(), plist::Value::Data(signature)); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "ReceiveBytes", + "ImageType": image_type, + "ImageSize": image_size, + "ImageSignature": signature, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status") { @@ -243,19 +207,14 @@ impl ImageMounter { ) -> Result<(), IdeviceError> { let image_type = image_type.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "MountImage".into()); - req.insert("ImageType".into(), image_type.into()); - req.insert("ImageSignature".into(), plist::Value::Data(signature)); - if let Some(trust_cache) = trust_cache { - req.insert("ImageTrustCache".into(), plist::Value::Data(trust_cache)); - } - if let Some(info_plist) = info_plist { - req.insert("ImageInfoPlist".into(), info_plist); - } - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "MountImage", + "ImageType": image_type, + "ImageSignature": signature, + "ImageTrustCache":? trust_cache, + "ImageInfoPlist":? info_plist, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; @@ -286,12 +245,11 @@ impl ImageMounter { mount_path: impl Into, ) -> Result<(), IdeviceError> { let mount_path = mount_path.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "UnmountImage".into()); - req.insert("MountPath".into(), mount_path.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "UnmountImage", + "MountPath": mount_path, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("Status") { @@ -321,14 +279,13 @@ impl ImageMounter { ) -> Result, IdeviceError> { let image_type = image_type.into(); - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryPersonalizationManifest".into()); - req.insert("PersonalizedImageType".into(), image_type.clone().into()); - req.insert("ImageType".into(), image_type.into()); - req.insert("ImageSignature".into(), plist::Value::Data(signature)); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryPersonalizationManifest", + "PersonalizedImageType": image_type.clone(), + "ImageType": image_type, + "ImageSignature": signature + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("ImageSignature") { @@ -345,11 +302,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if query fails pub async fn query_developer_mode_status(&mut self) -> Result { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryDeveloperModeStatus".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryDeveloperModeStatus" + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("DeveloperModeStatus") { @@ -370,16 +326,13 @@ impl ImageMounter { /// Returns `IdeviceError` if query fails pub async fn query_nonce( &mut self, - personalized_image_type: Option, + personalized_image_type: Option<&str>, ) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryNonce".into()); - if let Some(image_type) = personalized_image_type { - req.insert("PersonalizedImageType".into(), image_type.into()); - } - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryNonce", + "PersonalizedImageType":? personalized_image_type, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("PersonalizationNonce") { @@ -400,16 +353,13 @@ impl ImageMounter { /// Returns `IdeviceError` if query fails pub async fn query_personalization_identifiers( &mut self, - image_type: Option, + image_type: Option<&str>, ) -> Result { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "QueryPersonalizationIdentifiers".into()); - if let Some(image_type) = image_type { - req.insert("PersonalizedImageType".into(), image_type.into()); - } - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "QueryPersonalizationIdentifiers", + "PersonalizedImageType":? image_type, + }); + self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; match res.get("PersonalizationIdentifiers") { @@ -423,11 +373,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if operation fails pub async fn roll_personalization_nonce(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "RollPersonalizationNonce".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "RollPersonalizationNonce" + }); + self.idevice.send_plist(req).await?; Ok(()) } @@ -437,11 +386,10 @@ impl ImageMounter { /// # Errors /// Returns `IdeviceError` if operation fails pub async fn roll_cryptex_nonce(&mut self) -> Result<(), IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("Command".into(), "RollCryptexNonce".into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "Command": "RollCryptexNonce" + }); + self.idevice.send_plist(req).await?; Ok(()) } @@ -626,10 +574,7 @@ impl ImageMounter { request.insert("ApECID", unique_chip_id); request.insert( "ApNonce", - plist::Value::Data( - self.query_nonce(Some("DeveloperDiskImage".to_string())) - .await?, - ), + plist::Value::Data(self.query_nonce(Some("DeveloperDiskImage")).await?), ); request.insert("ApProductionMode", true); request.insert("ApSecurityDomain", 1); @@ -705,11 +650,12 @@ impl ImageMounter { } }; - let mut parameters = plist::Dictionary::new(); - parameters.insert("ApProductionMode".into(), true.into()); - parameters.insert("ApSecurityDomain".into(), 1.into()); - parameters.insert("ApSecurityMode".into(), true.into()); - parameters.insert("ApSupportsImg4".into(), true.into()); + let parameters = crate::plist!(dict { + "ApProductionMode": true, + "ApSecurityMode": 1, + "ApSecurityMode": true, + "ApSupportsImg4": true + }); for (key, manifest_item) in manifest { println!("{key}, {manifest_item:?}"); @@ -746,10 +692,9 @@ impl ImageMounter { .and_then(|l| l.as_dictionary()) .and_then(|l| l.get("Info")) .and_then(|i| i.as_dictionary()) + && let Some(plist::Value::Array(rules)) = info.get("RestoreRequestRules") { - if let Some(plist::Value::Array(rules)) = info.get("RestoreRequestRules") { - crate::tss::apply_restore_request_rules(&mut tss_entry, ¶meters, rules); - } + crate::tss::apply_restore_request_rules(&mut tss_entry, ¶meters, rules); } if manifest_item.get("Digest").is_none() { diff --git a/idevice/src/services/mobilebackup2.rs b/idevice/src/services/mobilebackup2.rs new file mode 100644 index 0000000..b186710 --- /dev/null +++ b/idevice/src/services/mobilebackup2.rs @@ -0,0 +1,1221 @@ +//! iOS Mobile Backup 2 Service Client +//! +//! Provides functionality for interacting with the mobilebackup2 service on iOS devices, +//! which allows creating, restoring, and managing device backups. + +use log::{debug, warn}; +use plist::Dictionary; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; +use tokio::io::AsyncReadExt; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// DeviceLink message codes used in MobileBackup2 binary streams +pub const DL_CODE_SUCCESS: u8 = 0x00; +pub const DL_CODE_ERROR_LOCAL: u8 = 0x06; +pub const DL_CODE_ERROR_REMOTE: u8 = 0x0b; +pub const DL_CODE_FILE_DATA: u8 = 0x0c; + +/// Client for interacting with the iOS mobile backup 2 service +/// +/// This service provides access to device backup functionality including +/// creating backups, restoring from backups, and managing backup data. +pub struct MobileBackup2Client { + /// The underlying device connection with established mobilebackup2 service + pub idevice: Idevice, + /// Protocol version negotiated with the device + pub protocol_version: f64, +} + +impl IdeviceService for MobileBackup2Client { + /// Returns the mobile backup 2 service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobilebackup2") + } + + async fn from_stream(idevice: Idevice) -> Result { + let mut client = Self::new(idevice); + // Perform DeviceLink handshake first + client.dl_version_exchange().await?; + // Perform version exchange after connection + client.version_exchange().await?; + Ok(client) + } +} + +/// Backup message types used in the mobilebackup2 protocol +#[derive(Debug, Clone)] +pub enum BackupMessageType { + /// Request to start a backup operation + BackupMessageTypeBackup, + /// Request to restore from a backup + BackupMessageTypeRestore, + /// Information message + BackupMessageTypeInfo, + /// List available backups + BackupMessageTypeList, + /// Upload files to backup + BackupMessageTypeUploadFiles, + /// Download files from backup + BackupMessageTypeDownloadFiles, + /// Clear backup data + BackupMessageTypeClearBackupData, + /// Move files in backup + BackupMessageTypeMoveFiles, + /// Remove files from backup + BackupMessageTypeRemoveFiles, + /// Create directory in backup + BackupMessageTypeCreateDirectory, + /// Acquire lock for backup operation + BackupMessageTypeAcquireLock, + /// Release lock after backup operation + BackupMessageTypeReleaseLock, + /// Copy item in backup + BackupMessageTypeCopyItem, + /// Disconnect from service + BackupMessageTypeDisconnect, + /// Process message + BackupMessageTypeProcessMessage, + /// Get freespace information + BackupMessageTypeGetFreespace, + /// Factory info + BackupMessageTypeFactoryInfo, + /// Check if backup is encrypted + BackupMessageTypeCheckBackupEncryption, +} + +impl BackupMessageType { + /// Convert message type to string representation + pub fn as_str(&self) -> &'static str { + match self { + // These map to MobileBackup2 request names per libimobiledevice + BackupMessageType::BackupMessageTypeBackup => "Backup", + BackupMessageType::BackupMessageTypeRestore => "Restore", + BackupMessageType::BackupMessageTypeInfo => "Info", + BackupMessageType::BackupMessageTypeList => "List", + // The following are DL control messages and not sent via MessageName + BackupMessageType::BackupMessageTypeUploadFiles => "DLMessageUploadFiles", + BackupMessageType::BackupMessageTypeDownloadFiles => "DLMessageDownloadFiles", + BackupMessageType::BackupMessageTypeClearBackupData => "DLMessageClearBackupData", + BackupMessageType::BackupMessageTypeMoveFiles => "DLMessageMoveFiles", + BackupMessageType::BackupMessageTypeRemoveFiles => "DLMessageRemoveFiles", + BackupMessageType::BackupMessageTypeCreateDirectory => "DLMessageCreateDirectory", + BackupMessageType::BackupMessageTypeAcquireLock => "DLMessageAcquireLock", + BackupMessageType::BackupMessageTypeReleaseLock => "DLMessageReleaseLock", + BackupMessageType::BackupMessageTypeCopyItem => "DLMessageCopyItem", + BackupMessageType::BackupMessageTypeDisconnect => "DLMessageDisconnect", + BackupMessageType::BackupMessageTypeProcessMessage => "DLMessageProcessMessage", + BackupMessageType::BackupMessageTypeGetFreespace => "DLMessageGetFreeDiskSpace", + BackupMessageType::BackupMessageTypeFactoryInfo => "FactoryInfo", + BackupMessageType::BackupMessageTypeCheckBackupEncryption => "CheckBackupEncryption", + } + } +} + +/// Backup information structure +#[derive(Debug, Clone)] +pub struct BackupInfo { + /// Backup UUID + pub uuid: String, + /// Device name + pub device_name: String, + /// Display name + pub display_name: String, + /// Last backup date + pub last_backup_date: Option, + /// Backup version + pub version: String, + /// Whether backup is encrypted + pub is_encrypted: bool, +} + +/// High-level builder for restore options so callers don't need to remember raw keys +#[derive(Debug, Clone)] +pub struct RestoreOptions { + pub reboot: bool, + pub copy: bool, + pub preserve_settings: bool, + pub system_files: bool, + pub remove_items_not_restored: bool, + pub password: Option, +} + +impl Default for RestoreOptions { + fn default() -> Self { + Self { + reboot: true, + copy: true, + preserve_settings: true, + system_files: false, + remove_items_not_restored: false, + password: None, + } + } +} + +impl RestoreOptions { + pub fn new() -> Self { + Self::default() + } + pub fn with_reboot(mut self, reboot: bool) -> Self { + self.reboot = reboot; + self + } + pub fn with_copy(mut self, copy: bool) -> Self { + self.copy = copy; + self + } + pub fn with_preserve_settings(mut self, preserve: bool) -> Self { + self.preserve_settings = preserve; + self + } + pub fn with_system_files(mut self, system: bool) -> Self { + self.system_files = system; + self + } + pub fn with_remove_items_not_restored(mut self, remove: bool) -> Self { + self.remove_items_not_restored = remove; + self + } + pub fn with_password(mut self, password: impl Into) -> Self { + self.password = Some(password.into()); + self + } + + pub fn to_plist(&self) -> Dictionary { + crate::plist!(dict { + "RestoreShouldReboot": self.reboot, + "RestoreDontCopyBackup": !self.copy, + "RestorePreserveSettings": self.preserve_settings, + "RestoreSystemFiles": self.system_files, + "RemoveItemsNotRestored": self.remove_items_not_restored, + "Password":? self.password.clone() + }) + } +} + +impl MobileBackup2Client { + /// Creates a new mobile backup 2 client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { + idevice, + protocol_version: 0.0, + } + } + + /// Performs DeviceLink version exchange handshake + /// + /// Sequence: + /// 1) Receive ["DLMessageVersionExchange", major, minor] + /// 2) Send ["DLMessageVersionExchange", "DLVersionsOk", 400] + /// 3) Receive ["DLMessageDeviceReady"] + async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting DeviceLink version exchange"); + // 1) Receive DLMessageVersionExchange + let (msg, _arr) = self.receive_dl_message().await?; + if msg != "DLMessageVersionExchange" { + warn!("Expected DLMessageVersionExchange, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // 2) Send DLVersionsOk with version 400 + let out = vec![ + plist::Value::String("DLMessageVersionExchange".into()), + plist::Value::String("DLVersionsOk".into()), + plist::Value::Integer(400u64.into()), + ]; + self.send_dl_array(out).await?; + + // 3) Receive DLMessageDeviceReady + let (msg2, _arr2) = self.receive_dl_message().await?; + if msg2 != "DLMessageDeviceReady" { + warn!("Expected DLMessageDeviceReady, got {msg2}"); + return Err(IdeviceError::UnexpectedResponse); + } + Ok(()) + } + + /// Sends a raw DL array as binary plist + async fn send_dl_array(&mut self, array: Vec) -> Result<(), IdeviceError> { + self.idevice.send_bplist(plist::Value::Array(array)).await + } + + /// Receives any DL* message and returns (message_tag, full_array_value) + pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> { + if let Some(socket) = &mut self.idevice.socket { + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut body = vec![0; len as usize]; + socket.read_exact(&mut body).await?; + let value: plist::Value = plist::from_bytes(&body)?; + if let plist::Value::Array(arr) = &value + && let Some(plist::Value::String(tag)) = arr.first() + { + return Ok((tag.clone(), value)); + } + warn!("Invalid DL message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + /// Performs version exchange with the device + /// + /// This is required by the mobilebackup2 protocol and must be called + /// before any other operations. + /// + /// # Returns + /// `Ok(())` on successful version negotiation + /// + /// # Errors + /// Returns `IdeviceError` if version exchange fails + async fn version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting mobilebackup2 version exchange"); + + // Send supported protocol versions (matching libimobiledevice) + let hello_dict = crate::plist!(dict { + "SupportedProtocolVersions": [ + 2.0, 2.1 + ] + }); + + self.send_device_link_message("Hello", Some(hello_dict)) + .await?; + + // Receive response + let response = self.receive_device_link_message("Response").await?; + + // Check for error + if let Some(error_code) = response.get("ErrorCode") + && let Some(code) = error_code.as_unsigned_integer() + && code != 0 + { + warn!("Version exchange failed with error code: {code}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // Get negotiated protocol version + if let Some(version) = response.get("ProtocolVersion").and_then(|v| v.as_real()) { + self.protocol_version = version; + debug!("Negotiated protocol version: {version}"); + } else { + warn!("No protocol version in response"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok(()) + } + + /// Sends a device link message (DLMessageProcessMessage format) + /// + /// This follows the device_link_service protocol used by mobilebackup2 + /// + /// # Arguments + /// * `message_name` - The message name (e.g., "Hello", "kBackupMessageTypeInfo") + /// * `options` - Optional dictionary of options for the message + /// + /// # Returns + /// `Ok(())` on successful message send + /// + /// # Errors + /// Returns `IdeviceError` if communication fails + async fn send_device_link_message( + &mut self, + message_name: &str, + options: Option, + ) -> Result<(), IdeviceError> { + // Create the actual message dictionary + let message_dict = crate::plist!(dict { + "MessageName": message_name, + : Result { + // Read raw bytes and parse as plist::Value to handle array format + if let Some(socket) = &mut self.idevice.socket { + debug!("Reading response size"); + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut buf = vec![0; len as usize]; + socket.read_exact(&mut buf).await?; + let response_value: plist::Value = plist::from_bytes(&buf)?; + + // Parse DLMessageProcessMessage format + if let plist::Value::Array(array) = response_value + && array.len() >= 2 + && let Some(plist::Value::String(dl_message)) = array.first() + && let Some(plist::Value::Dictionary(dict)) = array.get(1) + && dl_message == "DLMessageProcessMessage" + { + // Check MessageName if expected + if !expected_message.is_empty() { + if let Some(message_name) = dict.get("MessageName").and_then(|v| v.as_string()) + { + if message_name != expected_message { + warn!("Expected message '{expected_message}', got '{message_name}'"); + return Err(IdeviceError::UnexpectedResponse); + } + } else { + warn!("No MessageName in response"); + return Err(IdeviceError::UnexpectedResponse); + } + } + return Ok(dict.clone()); + } + + warn!("Invalid device link message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + /// Sends a backup message to the device + /// + /// # Arguments + /// * `message_type` - The type of backup message to send + /// * `options` - Optional dictionary of options for the message + /// + /// # Returns + /// `Ok(())` on successful message send + /// + /// # Errors + /// Returns `IdeviceError` if communication fails + async fn send_backup_message( + &mut self, + message_type: BackupMessageType, + options: Option, + ) -> Result<(), IdeviceError> { + self.send_device_link_message(message_type.as_str(), options) + .await + } + + /// Sends a MobileBackup2 request with proper envelope and identifiers + pub async fn send_request( + &mut self, + request: &str, + target_identifier: Option<&str>, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + let dict = crate::plist!(dict { + "TargetIdentifier":? target_identifier, + "SourceIdentifier":? source_identifier, + "Options":? options, + // Special cases like Unback/EnableCloudBackup are handled by caller if needed + }); + self.send_device_link_message(request, Some(dict)).await + } + + /// Sends a DLMessageStatusResponse array + pub async fn send_status_response( + &mut self, + status_code: i64, + status1: Option<&str>, + status2: Option, + ) -> Result<(), IdeviceError> { + let arr = vec![ + plist::Value::String("DLMessageStatusResponse".into()), + plist::Value::Integer(status_code.into()), + plist::Value::String(status1.unwrap_or("___EmptyParameterString___").into()), + status2.unwrap_or_else(|| plist::Value::String("___EmptyParameterString___".into())), + ]; + self.send_dl_array(arr).await + } + + /// Receives a response from the backup service + /// + /// # Returns + /// The response as a plist Dictionary + /// + /// # Errors + /// Returns `IdeviceError` if communication fails or response is malformed + async fn receive_backup_response(&mut self) -> Result { + self.receive_device_link_message("").await + } + + /// Requests device information for backup + /// + /// # Returns + /// A dictionary containing device information + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn request_backup_info(&mut self) -> Result { + // Per protocol use MessageName "Info" + self.send_backup_message(BackupMessageType::BackupMessageTypeInfo, None) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Backup info request failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok(response) + } + + /// Lists available backups on the device + /// + /// # Returns + /// A vector of backup information + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn list_backups(&mut self) -> Result, IdeviceError> { + self.send_backup_message(BackupMessageType::BackupMessageTypeList, None) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("List backups request failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + let mut backups = Vec::new(); + + if let Some(plist::Value::Array(backup_list)) = response.get("BackupList") { + for backup_item in backup_list { + if let plist::Value::Dictionary(backup_dict) = backup_item { + let uuid = backup_dict + .get("BackupUUID") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let device_name = backup_dict + .get("DeviceName") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let display_name = backup_dict + .get("DisplayName") + .and_then(|v| v.as_string()) + .unwrap_or_default() + .to_string(); + + let last_backup_date = backup_dict + .get("LastBackupDate") + .and_then(|v| v.as_string()) + .map(|s| s.to_string()); + + let version = backup_dict + .get("Version") + .and_then(|v| v.as_string()) + .unwrap_or("Unknown") + .to_string(); + + let is_encrypted = backup_dict + .get("IsEncrypted") + .and_then(|v| v.as_boolean()) + .unwrap_or(false); + + backups.push(BackupInfo { + uuid, + device_name, + display_name, + last_backup_date, + version, + is_encrypted, + }); + } + } + } + + Ok(backups) + } + + /// Starts a backup operation + /// + /// # Arguments + /// * `target_identifier` - Optional target identifier for the backup + /// * `source_identifier` - Optional source identifier for the backup + /// * `options` - Optional backup options + /// + /// # Returns + /// `Ok(())` on successful backup start + /// + /// # Errors + /// Returns `IdeviceError` if the backup fails to start + pub async fn start_backup( + &mut self, + target_identifier: Option<&str>, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + self.send_request( + BackupMessageType::BackupMessageTypeBackup.as_str(), + target_identifier, + source_identifier, + options, + ) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Backup start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + debug!("Backup started successfully"); + Ok(()) + } + + /// Starts a restore operation + /// + /// # Arguments + /// * `backup_uuid` - UUID of the backup to restore from + /// * `options` - Optional restore options + /// + /// # Returns + /// `Ok(())` on successful restore start + /// + /// # Errors + /// Returns `IdeviceError` if the restore fails to start + #[deprecated( + note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2" + )] + pub async fn start_restore( + &mut self, + _backup_uuid: &str, + options: Option, + ) -> Result<(), IdeviceError> { + let mut opts = options.unwrap_or_default(); + // Align default restore options with pymobiledevice semantics + // Caller-specified values (if any) take precedence. + if !opts.contains_key("RestoreShouldReboot") { + opts.insert("RestoreShouldReboot".into(), plist::Value::Boolean(true)); + } + if !opts.contains_key("RestoreDontCopyBackup") { + // pymobiledevice: copy=True -> RestoreDontCopyBackup=False + opts.insert("RestoreDontCopyBackup".into(), plist::Value::Boolean(false)); + } + if !opts.contains_key("RestorePreserveSettings") { + opts.insert( + "RestorePreserveSettings".into(), + plist::Value::Boolean(true), + ); + } + if !opts.contains_key("RestoreSystemFiles") { + opts.insert("RestoreSystemFiles".into(), plist::Value::Boolean(false)); + } + if !opts.contains_key("RemoveItemsNotRestored") { + opts.insert( + "RemoveItemsNotRestored".into(), + plist::Value::Boolean(false), + ); + } + // Avoid borrowing self while sending request + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + // default identifiers to current UDID if available + target_udid, + target_udid, + Some(opts), + ) + .await?; + + let response = self.receive_backup_response().await?; + + // Check for error in response + if let Some(error) = response.get("ErrorCode") { + warn!("Restore start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + + debug!("Restore started successfully"); + Ok(()) + } + + /// High-level API: Restore from a local backup directory using DeviceLink file exchange + /// + /// - `backup_root` should point to the backup root directory (which contains the `` subdirectory) + /// - If `source_identifier` is None, the current connected device's UDID will be used by default + /// - `options` should be constructed using the `RestoreOptions` builder; if not provided, defaults will be used + pub async fn restore_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + options: Option, + ) -> Result<(), IdeviceError> { + // Take owned UDID to avoid aliasing borrows + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + let source: &str = match source_identifier { + Some(s) => s, + None => target_udid.ok_or(IdeviceError::InvalidHostID)?, + }; + + // Simple existence check: backup_root/source must exist + let backup_dir = backup_root.join(source); + if !backup_dir.exists() { + return Err(IdeviceError::NotFound); + } + + let opts = options.unwrap_or_default().to_plist(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + target_udid, + Some(source), + Some(opts), + ) + .await?; + + // Enter the DeviceLink file exchange loop, and pass the root directory to backup_root + // (the protocol request contains the source prefix) + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + async fn process_restore_dl_loop( + &mut self, + host_dir: &Path, + ) -> Result, IdeviceError> { + loop { + let (tag, value) = self.receive_dl_message().await?; + match tag.as_str() { + "DLMessageDownloadFiles" => { + self.handle_download_files(&value, host_dir).await?; + } + "DLMessageUploadFiles" => { + self.handle_upload_files(&value, host_dir).await?; + } + "DLMessageGetFreeDiskSpace" => { + // Minimal implementation: report 0 with success + self.send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))) + .await?; + } + "DLContentsOfDirectory" => { + let empty = plist::Value::Dictionary(Dictionary::new()); + self.send_status_response(0, None, Some(empty)).await?; + } + "DLMessageCreateDirectory" => { + let status = Self::create_directory_from_message(&value, host_dir); + self.send_status_response(status, None, None).await?; + } + "DLMessageMoveFiles" | "DLMessageMoveItems" => { + let status = Self::move_files_from_message(&value, host_dir); + self.send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; + } + "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { + let status = Self::remove_files_from_message(&value, host_dir); + self.send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; + } + "DLMessageCopyItem" => { + let status = Self::copy_item_from_message(&value, host_dir); + self.send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; + } + "DLMessageProcessMessage" => { + if let plist::Value::Array(arr) = value + && let Some(plist::Value::Dictionary(dict)) = arr.get(1) + { + return Ok(Some(dict.clone())); + } + return Ok(None); + } + "DLMessageDisconnect" => { + return Ok(None); + } + other => { + warn!("Unsupported DL message: {other}"); + self.send_status_response(-1, Some("Operation not supported"), None) + .await?; + } + } + } + } + + async fn handle_download_files( + &mut self, + dl_value: &plist::Value, + host_dir: &Path, + ) -> Result<(), IdeviceError> { + let mut err_any = false; + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(files)) = arr.get(1) + { + for pv in files { + if let Some(path) = pv.as_string() + && let Err(e) = self.send_single_file(host_dir, path).await + { + warn!("Failed to send file {path}: {e}"); + err_any = true; + } + } + } + // terminating zero dword + self.idevice.send_raw(&0u32.to_be_bytes()).await?; + if err_any { + self.send_status_response( + -13, + Some("Multi status"), + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await + } else { + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await + } + } + + async fn send_single_file( + &mut self, + host_dir: &Path, + rel_path: &str, + ) -> Result<(), IdeviceError> { + let full = host_dir.join(rel_path); + let path_bytes = rel_path.as_bytes().to_vec(); + let nlen = (path_bytes.len() as u32).to_be_bytes(); + self.idevice.send_raw(&nlen).await?; + self.idevice.send_raw(&path_bytes).await?; + + let mut f = match std::fs::File::open(&full) { + Ok(f) => f, + Err(e) => { + // send error + let desc = e.to_string(); + let size = (desc.len() as u32 + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(DL_CODE_ERROR_LOCAL); + self.idevice.send_raw(&hdr).await?; + self.idevice.send_raw(desc.as_bytes()).await?; + return Ok(()); + } + }; + let mut buf = [0u8; 32768]; + loop { + let read = f.read(&mut buf).unwrap_or(0); + if read == 0 { + break; + } + let size = ((read as u32) + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(DL_CODE_FILE_DATA); + self.idevice.send_raw(&hdr).await?; + self.idevice.send_raw(&buf[..read]).await?; + } + // success trailer + let mut ok = [0u8; 5]; + ok[..4].copy_from_slice(&1u32.to_be_bytes()); + ok[4] = DL_CODE_SUCCESS; + self.idevice.send_raw(&ok).await?; + Ok(()) + } + + async fn handle_upload_files( + &mut self, + _dl_value: &plist::Value, + host_dir: &Path, + ) -> Result<(), IdeviceError> { + loop { + let dlen = self.read_be_u32().await?; + if dlen == 0 { + break; + } + let dname = self.read_exact_string(dlen as usize).await?; + let flen = self.read_be_u32().await?; + if flen == 0 { + break; + } + let fname = self.read_exact_string(flen as usize).await?; + let dst = host_dir.join(&fname); + if let Some(parent) = dst.parent() { + let _ = fs::create_dir_all(parent); + } + let mut file = std::fs::File::create(&dst) + .map_err(|e| IdeviceError::InternalError(e.to_string()))?; + loop { + let nlen = self.read_be_u32().await?; + if nlen == 0 { + break; + } + let code = self.read_one().await?; + if code == DL_CODE_FILE_DATA { + let size = (nlen - 1) as usize; + let data = self.read_exact(size).await?; + file.write_all(&data) + .map_err(|e| IdeviceError::InternalError(e.to_string()))?; + } else { + let _ = self.read_exact((nlen - 1) as usize).await?; + } + } + let _ = dname; // unused + } + self.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await + } + + async fn read_be_u32(&mut self) -> Result { + let buf = self.idevice.read_raw(4).await?; + Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + + async fn read_one(&mut self) -> Result { + let buf = self.idevice.read_raw(1).await?; + Ok(buf[0]) + } + + async fn read_exact(&mut self, size: usize) -> Result, IdeviceError> { + self.idevice.read_raw(size).await + } + + async fn read_exact_string(&mut self, size: usize) -> Result { + let buf = self.idevice.read_raw(size).await?; + Ok(String::from_utf8_lossy(&buf).to_string()) + } + + fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::String(dir)) = arr.get(1) + { + let path = host_dir.join(dir); + return match fs::create_dir_all(&path) { + Ok(_) => 0, + Err(_) => -1, + }; + } + -1 + } + + fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Dictionary(map)) = arr.get(1) + { + for (from, to_v) in map.iter() { + if let Some(to) = to_v.as_string() { + let old = host_dir.join(from); + let newp = host_dir.join(to); + if let Some(parent) = newp.parent() { + let _ = fs::create_dir_all(parent); + } + if fs::rename(&old, &newp).is_err() { + return -1; + } + } + } + return 0; + } + -1 + } + + fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(items)) = arr.get(1) + { + for it in items { + if let Some(p) = it.as_string() { + let path = host_dir.join(p); + if path.is_dir() { + if fs::remove_dir_all(&path).is_err() { + return -1; + } + } else if path.exists() && fs::remove_file(&path).is_err() { + return -1; + } + } + } + return 0; + } + -1 + } + + fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 3 + && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = + (arr.get(1), arr.get(2)) + { + let from = host_dir.join(src); + let to = host_dir.join(dst); + if let Some(parent) = to.parent() { + let _ = fs::create_dir_all(parent); + } + if from.is_dir() { + return match fs::create_dir_all(&to) { + Ok(_) => 0, + Err(_) => -1, + }; + } else { + return match fs::copy(&from, &to) { + Ok(_) => 0, + Err(_) => -1, + }; + } + } + -1 + } + + /// Starts a restore using the typed RestoreOptions builder + #[deprecated( + note = "Use restore_from_path; restore via BackupUUID is not supported by device/mobilebackup2" + )] + pub async fn start_restore_with( + &mut self, + _backup_uuid: &str, + opts: RestoreOptions, + ) -> Result<(), IdeviceError> { + let dict = opts.to_plist(); + // Avoid borrowing self during request + let target_udid_owned = self.idevice.udid().map(|s| s.to_string()); + let target_udid = target_udid_owned.as_deref(); + self.send_request( + BackupMessageType::BackupMessageTypeRestore.as_str(), + target_udid, + target_udid, + Some(dict), + ) + .await?; + + let response = self.receive_backup_response().await?; + if let Some(error) = response.get("ErrorCode") { + warn!("Restore start failed with error: {error:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + debug!("Restore started successfully"); + Ok(()) + } + + /// Assert backup dir structure exists for a given source identifier (UDID) + fn assert_backup_exists(&self, backup_root: &Path, source: &str) -> Result<(), IdeviceError> { + let device_dir = backup_root.join(source); + if device_dir.join("Info.plist").exists() + && device_dir.join("Manifest.plist").exists() + && device_dir.join("Status.plist").exists() + { + Ok(()) + } else { + Err(IdeviceError::NotFound) + } + } + + /// Get backup information using DeviceLink against a given backup root/source + pub async fn info_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + ) -> Result { + let target_udid = self.idevice.udid(); + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let dict = crate::plist!(dict { + "TargetIdentifier": target_udid.unwrap(), + "SourceIdentifier":? source_identifier, + }); + self.send_device_link_message("Info", Some(dict)).await?; + + match self.process_restore_dl_loop(backup_root).await? { + Some(res) => Ok(res), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + /// List last backup contents (returns raw response dictionary) + pub async fn list_from_path( + &mut self, + backup_root: &Path, + source_identifier: Option<&str>, + ) -> Result { + let target_udid = self.idevice.udid(); + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + + let dict = crate::plist!(dict { + "MessageName": "List", + "TargetIdentifier": target_udid.unwrap(), + "SourceIdentifier": source, + }); + self.send_device_link_message("List", Some(dict)).await?; + + match self.process_restore_dl_loop(backup_root).await? { + Some(res) => Ok(res), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Unpack a complete backup to device hierarchy + pub async fn unback_from_path( + &mut self, + backup_root: &Path, + password: Option<&str>, + source_identifier: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + let dict = crate::plist!(dict { + "TargetIdentifier": target_udid.unwrap(), + "MessageName": "Unback", + "SourceIdentifier": source, + "Password":? password + }); + self.send_device_link_message("Unback", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Extract a single file from a previous backup + pub async fn extract_from_path( + &mut self, + domain_name: &str, + relative_path: &str, + backup_root: &Path, + password: Option<&str>, + source_identifier: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let source = source_identifier + .or(target_udid) + .ok_or(IdeviceError::InvalidHostID)?; + self.assert_backup_exists(backup_root, source)?; + let dict = crate::plist!(dict { + "MessageName": "Extract", + "TargetIdentifier": target_udid.unwrap(), + "DomainName": domain_name, + "RelativePath": relative_path, + "SourceIdentifier": source, + "Password":? password, + }); + self.send_device_link_message("Extract", Some(dict)).await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Change backup password (enable/disable if new/old missing) + pub async fn change_password_from_path( + &mut self, + backup_root: &Path, + old: Option<&str>, + new: Option<&str>, + ) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let dict = crate::plist!(dict { + "MessageName": "ChangePassword", + "TargetIdentifier": target_udid.ok_or(IdeviceError::InvalidHostID)?, + "OldPassword":? old, + "NewPassword":? new + }); + self.send_device_link_message("ChangePassword", Some(dict)) + .await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Erase device via mobilebackup2 + pub async fn erase_device_from_path(&mut self, backup_root: &Path) -> Result<(), IdeviceError> { + let target_udid = self.idevice.udid(); + let dict = crate::plist!(dict { + "MessageName": "EraseDevice", + "TargetIdentifier": target_udid.ok_or(IdeviceError::InvalidHostID)? + }); + self.send_device_link_message("EraseDevice", Some(dict)) + .await?; + let _ = self.process_restore_dl_loop(backup_root).await?; + Ok(()) + } + + /// Gets free space information from the device + /// + /// # Returns + /// Free space in bytes + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn get_freespace(&mut self) -> Result { + // Not a valid host-initiated request in protocol; device asks via DLMessageGetFreeDiskSpace + Err(IdeviceError::UnexpectedResponse) + } + + /// Checks if backup encryption is enabled + /// + /// # Returns + /// `true` if backup encryption is enabled, `false` otherwise + /// + /// # Errors + /// Returns `IdeviceError` if the request fails + pub async fn check_backup_encryption(&mut self) -> Result { + // Not part of host-initiated MB2 protocol; caller should inspect Manifest/lockdown + Err(IdeviceError::UnexpectedResponse) + } + + /// Disconnects from the backup service + /// + /// # Returns + /// `Ok(())` on successful disconnection + /// + /// # Errors + /// Returns `IdeviceError` if disconnection fails + pub async fn disconnect(&mut self) -> Result<(), IdeviceError> { + // Send DLMessageDisconnect array per DeviceLink protocol + let arr = crate::plist!(array [ + "DLMessageDisconnect", + "___EmptyParameterString___" + ]); + self.send_dl_array(arr).await?; + debug!("Disconnected from backup service"); + Ok(()) + } +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 7a00438..5b578ee 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -2,6 +2,10 @@ pub mod afc; #[cfg(feature = "amfi")] pub mod amfi; +#[cfg(feature = "bt_packet_logger")] +pub mod bt_packet_logger; +#[cfg(feature = "companion_proxy")] +pub mod companion_proxy; #[cfg(feature = "core_device")] pub mod core_device; #[cfg(feature = "core_device_proxy")] @@ -10,6 +14,8 @@ pub mod core_device_proxy; pub mod crashreportcopymobile; #[cfg(feature = "debug_proxy")] pub mod debug_proxy; +#[cfg(feature = "diagnostics_relay")] +pub mod diagnostics_relay; #[cfg(feature = "dvt")] pub mod dvt; #[cfg(feature = "heartbeat")] @@ -23,8 +29,14 @@ pub mod lockdown; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "mobilebackup2")] +pub mod mobilebackup2; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; +#[cfg(feature = "pcapd")] +pub mod pcapd; +#[cfg(feature = "preboard_service")] +pub mod preboard_service; #[cfg(feature = "remote_pairing")] pub mod remote_pairing; #[cfg(feature = "restore_service")] diff --git a/idevice/src/services/os_trace_relay.rs b/idevice/src/services/os_trace_relay.rs index 8ff203d..e675ef6 100644 --- a/idevice/src/services/os_trace_relay.rs +++ b/idevice/src/services/os_trace_relay.rs @@ -4,10 +4,10 @@ //! https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/services/os_trace.py use chrono::{DateTime, NaiveDateTime}; -use plist::Dictionary; +use serde::{Deserialize, Serialize}; use tokio::io::AsyncWriteExt; -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS device OsTraceRelay service pub struct OsTraceRelayClient { @@ -21,40 +21,7 @@ impl IdeviceService for OsTraceRelayClient { obf!("com.apple.os_trace_relay") } - /// Establishes a connection to the OsTraceRelay service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `OsTraceRelayClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the OsTraceRelay service port - /// 4. Establishes connection to the OsTraceRelay port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - + async fn from_stream(idevice: Idevice) -> Result { Ok(Self { idevice }) } } @@ -64,7 +31,7 @@ pub struct OsTraceRelayReceiver { inner: OsTraceRelayClient, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct OsTraceLog { pub pid: u32, pub timestamp: NaiveDateTime, @@ -75,13 +42,13 @@ pub struct OsTraceLog { pub label: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SyslogLabel { pub subsystem: String, pub category: String, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum LogLevel { Notice = 0, Info = 1, @@ -103,15 +70,14 @@ impl OsTraceRelayClient { Some(p) => p as i64, None => -1, }; - let mut req = Dictionary::new(); - req.insert("Request".into(), "StartActivity".into()); - req.insert("Pid".into(), Into::into(pid)); - req.insert("MessageFilter".into(), Into::into(65_535)); - req.insert("StreamFlags".into(), Into::into(60)); + let req = crate::plist!({ + "Request": "StartActivity", + "Pid": pid, + "MessageFilter": 65_535, + "StreamFlags": 60 + }); - self.idevice - .send_bplist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_bplist(req).await?; // Read a single byte self.idevice.read_raw(1).await?; @@ -133,12 +99,11 @@ impl OsTraceRelayClient { /// Get the list of available PIDs pub async fn get_pid_list(&mut self) -> Result, IdeviceError> { - let mut req = Dictionary::new(); - req.insert("Request".into(), "PidList".into()); + let req = crate::plist!({ + "Request": "PidList" + }); - self.idevice - .send_bplist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_bplist(req).await?; // Read a single byte self.idevice.read_raw(1).await?; @@ -166,24 +131,14 @@ impl OsTraceRelayClient { age_limit: Option, start_time: Option, ) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("Request".into(), "CreateArchive".into()); + let req = crate::plist!({ + "Request": "CreateArchive", + "SizeLimit":? size_limit, + "AgeLimit":? age_limit, + "StartTime":? start_time, + }); - if let Some(size) = size_limit { - req.insert("SizeLimit".into(), size.into()); - } - - if let Some(age) = age_limit { - req.insert("AgeLimit".into(), age.into()); - } - - if let Some(time) = start_time { - req.insert("StartTime".into(), time.into()); - } - - self.idevice - .send_bplist(plist::Value::Dictionary(req)) - .await?; + self.idevice.send_bplist(req).await?; // Read a single byte if self.idevice.read_raw(1).await?[0] != 1 { diff --git a/idevice/src/services/pcapd.rs b/idevice/src/services/pcapd.rs new file mode 100644 index 0000000..15bb06f --- /dev/null +++ b/idevice/src/services/pcapd.rs @@ -0,0 +1,264 @@ +//! Abstraction for pcapd +//! Note that this service only works over USB or through RSD. + +use plist::Value; +use tokio::io::AsyncWrite; +use tokio::io::AsyncWriteExt; + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +const ETHERNET_HEADER: &[u8] = &[ + 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0x08, 0x00, +]; + +/// Client for interacting with the pcapd service on the device. +/// Note that this service only works over USB or through RSD. +pub struct PcapdClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for PcapdClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.pcapd") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for PcapdClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.pcapd.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut idevice = Idevice::new(stream, ""); + idevice.rsd_checkin().await?; + Ok(Self::new(idevice)) + } +} + +/// A Rust representation of the iOS pcapd device packet header and data. +#[derive(Debug, Clone)] +pub struct DevicePacket { + pub header_length: u32, + pub header_version: u8, + pub packet_length: u32, + pub interface_type: u8, + pub unit: u16, + pub io: u8, + pub protocol_family: u32, + pub frame_pre_length: u32, + pub frame_post_length: u32, + pub interface_name: String, + pub pid: u32, + pub comm: String, + pub svc: u32, + pub epid: u32, + pub ecomm: String, + pub seconds: u32, + pub microseconds: u32, + pub data: Vec, +} + +impl PcapdClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn next_packet(&mut self) -> Result { + let packet = self.idevice.read_plist_value().await?; + let packet = match packet { + Value::Data(p) => p, + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + let mut packet = DevicePacket::from_vec(&packet)?; + packet.normalize_data(); + Ok(packet) + } +} + +impl DevicePacket { + /// Normalizes the packet data by adding a fake Ethernet header if necessary. + /// This is required for tools like Wireshark to correctly dissect raw IP packets. + pub fn normalize_data(&mut self) { + if self.frame_pre_length == 0 { + // Prepend the fake ethernet header for raw IP packets. + let mut new_data = ETHERNET_HEADER.to_vec(); + new_data.append(&mut self.data); + self.data = new_data; + } else if self.interface_name.starts_with("pdp_ip") { + // For cellular interfaces, skip the first 4 bytes of the original data + // before prepending the header. + if self.data.len() >= 4 { + let mut new_data = ETHERNET_HEADER.to_vec(); + new_data.extend_from_slice(&self.data[4..]); + self.data = new_data; + } + } + } + + /// Parses a byte vector into a DevicePacket. + /// + /// This is the primary method for creating a struct from the raw data + /// received from the device. + /// + /// # Arguments + /// * `bytes` - A `Vec` containing the raw bytes of a single packet frame. + /// + /// # Returns + /// A `Result` containing the parsed `DevicePacket` + pub fn from_vec(bytes: &[u8]) -> Result { + let mut r = ByteReader::new(bytes); + + // --- Parse Header --- + let header_length = r.read_u32_be()?; + let header_version = r.read_u8()?; + let packet_length = r.read_u32_be()?; + let interface_type = r.read_u8()?; + let unit = r.read_u16_be()?; + let io = r.read_u8()?; + let protocol_family = r.read_u32_be()?; + let frame_pre_length = r.read_u32_be()?; + let frame_post_length = r.read_u32_be()?; + let interface_name = r.read_cstr(16)?; + let pid = r.read_u32_le()?; // Little Endian + let comm = r.read_cstr(17)?; + let svc = r.read_u32_be()?; + let epid = r.read_u32_le()?; // Little Endian + let ecomm = r.read_cstr(17)?; + let seconds = r.read_u32_be()?; + let microseconds = r.read_u32_be()?; + + // --- Extract Packet Data --- + // The data starts at an absolute offset defined by `header_length`. + let data_start = header_length as usize; + let data_end = data_start.saturating_add(packet_length as usize); + + if data_end > bytes.len() { + return Err(IdeviceError::NotEnoughBytes(bytes.len(), data_end)); + } + let data = bytes[data_start..data_end].to_vec(); + + Ok(DevicePacket { + header_length, + header_version, + packet_length, + interface_type, + unit, + io, + protocol_family, + frame_pre_length, + frame_post_length, + interface_name, + pid, + comm, + svc, + epid, + ecomm, + seconds, + microseconds, + data, + }) + } +} + +/// A helper struct to safely read from a byte slice. +struct ByteReader<'a> { + slice: &'a [u8], + cursor: usize, +} + +impl<'a> ByteReader<'a> { + fn new(slice: &'a [u8]) -> Self { + Self { slice, cursor: 0 } + } + + /// Reads an exact number of bytes and advances the cursor. + fn read_exact(&mut self, len: usize) -> Result<&'a [u8], IdeviceError> { + let end = self + .cursor + .checked_add(len) + .ok_or(IdeviceError::IntegerOverflow)?; + if end > self.slice.len() { + return Err(IdeviceError::NotEnoughBytes(len, self.slice.len())); + } + let result = &self.slice[self.cursor..end]; + self.cursor = end; + Ok(result) + } + + fn read_u8(&mut self) -> Result { + self.read_exact(1).map(|s| s[0]) + } + + fn read_u16_be(&mut self) -> Result { + self.read_exact(2) + .map(|s| u16::from_be_bytes(s.try_into().unwrap())) + } + + fn read_u32_be(&mut self) -> Result { + self.read_exact(4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + } + + fn read_u32_le(&mut self) -> Result { + self.read_exact(4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap())) + } + + /// Reads a fixed-size, null-padded C-style string. + fn read_cstr(&mut self, len: usize) -> Result { + let buffer = self.read_exact(len)?; + let end = buffer.iter().position(|&b| b == 0).unwrap_or(len); + String::from_utf8(buffer[..end].to_vec()).map_err(IdeviceError::Utf8) + } +} + +/// A writer for creating `.pcap` files from DevicePackets without external dependencies. +pub struct PcapFileWriter { + writer: W, +} + +impl PcapFileWriter { + /// Creates a new writer and asynchronously writes the pcap global header. + pub async fn new(mut writer: W) -> Result { + // Correct pcap global header for LINKTYPE_ETHERNET. + // We use big-endian format, as is traditional. + let header = [ + 0xa1, 0xb2, 0xc3, 0xd4, // magic number (big-endian) + 0x00, 0x02, // version_major + 0x00, 0x04, // version_minor + 0x00, 0x00, 0x00, 0x00, // thiszone (GMT) + 0x00, 0x00, 0x00, 0x00, // sigfigs (accuracy) + 0x00, 0x04, 0x00, 0x00, // snaplen (max packet size, 262144) + 0x00, 0x00, 0x00, 0x01, // network (LINKTYPE_ETHERNET) + ]; + writer.write_all(&header).await?; + Ok(Self { writer }) + } + + /// Asynchronously writes a single DevicePacket to the pcap file. + pub async fn write_packet(&mut self, packet: &DevicePacket) -> Result<(), std::io::Error> { + let mut record_header = [0u8; 16]; + + // Use the packet's own timestamp for accuracy. + record_header[0..4].copy_from_slice(&packet.seconds.to_be_bytes()); + record_header[4..8].copy_from_slice(&packet.microseconds.to_be_bytes()); + + // incl_len and orig_len + let len_bytes = (packet.data.len() as u32).to_be_bytes(); + record_header[8..12].copy_from_slice(&len_bytes); + record_header[12..16].copy_from_slice(&len_bytes); + + // Write the record header and packet data sequentially. + self.writer.write_all(&record_header).await?; + self.writer.write_all(&packet.data).await?; + + Ok(()) + } +} diff --git a/idevice/src/services/preboard_service.rs b/idevice/src/services/preboard_service.rs new file mode 100644 index 0000000..8f5d7f9 --- /dev/null +++ b/idevice/src/services/preboard_service.rs @@ -0,0 +1,72 @@ +//! Abstraction for preboard + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +/// Client for interacting with the preboard service on the device. +pub struct PreboardServiceClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for PreboardServiceClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.preboardservice_v2") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for PreboardServiceClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.preboardservice_v2.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + let mut idevice = Idevice::new(stream, ""); + idevice.rsd_checkin().await?; + Ok(Self::new(idevice)) + } +} + +impl PreboardServiceClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn create_stashbag(&mut self, manifest: &[u8]) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Command": "CreateStashbag", + "Manifest": manifest + }); + self.idevice.send_plist(req).await?; + let res = self.idevice.read_plist().await?; + if let Some(res) = res.get("ShowDialog").and_then(|x| x.as_boolean()) { + if !res { + log::warn!("ShowDialog is not true"); + return Err(IdeviceError::UnexpectedResponse); + } + } else { + log::warn!("No ShowDialog in response from service"); + return Err(IdeviceError::UnexpectedResponse); + } + + self.idevice.read_plist().await?; + Ok(()) + } + + pub async fn commit_stashbag(&mut self, manifest: &[u8]) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "Command": "CommitStashbag", + "Manifest": manifest + }); + self.idevice.send_plist(req).await?; + self.idevice.read_plist().await?; + Ok(()) + } + + pub async fn clear_system_token(&mut self) -> Result<(), IdeviceError> { + todo!() + } +} diff --git a/idevice/src/services/restore_service.rs b/idevice/src/services/restore_service.rs index aa23568..1f3dd73 100644 --- a/idevice/src/services/restore_service.rs +++ b/idevice/src/services/restore_service.rs @@ -3,52 +3,43 @@ use log::warn; use plist::Dictionary; -use crate::{obf, IdeviceError, ReadWrite, RemoteXpcClient, RsdService}; +use crate::{IdeviceError, ReadWrite, RemoteXpcClient, RsdService, obf}; /// Client for interacting with the Restore Service -pub struct RestoreServiceClient { +pub struct RestoreServiceClient { /// The underlying device connection with established Restore Service service - pub stream: RemoteXpcClient, + pub stream: RemoteXpcClient>, } -impl RsdService for RestoreServiceClient { +impl RsdService for RestoreServiceClient { fn rsd_service_name() -> std::borrow::Cow<'static, str> { obf!("com.apple.RestoreRemoteServices.restoreserviced") } - async fn from_stream(stream: R) -> Result { + async fn from_stream(stream: Box) -> Result { Self::new(stream).await } - - type Stream = R; } -impl<'a, R: ReadWrite + 'a> RestoreServiceClient { +impl RestoreServiceClient { /// Creates a new Restore Service client a socket connection, /// and connects to the RemoteXPC service. /// /// # Arguments /// * `idevice` - Pre-established device connection - pub async fn new(stream: R) -> Result { + pub async fn new(stream: Box) -> Result { let mut stream = RemoteXpcClient::new(stream).await?; stream.do_handshake().await?; Ok(Self { stream }) } - pub fn box_inner(self) -> RestoreServiceClient> { - RestoreServiceClient { - stream: self.stream.box_inner(), - } - } - /// Enter recovery pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("command".into(), "recovery".into()); + let req = crate::plist!({ + "command": "recovery" + }); - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -77,12 +68,10 @@ impl<'a, R: ReadWrite + 'a> RestoreServiceClient { /// Reboot pub async fn reboot(&mut self) -> Result<(), IdeviceError> { - let mut req = Dictionary::new(); - req.insert("command".into(), "reboot".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "reboot" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -111,12 +100,10 @@ impl<'a, R: ReadWrite + 'a> RestoreServiceClient { /// Get preflightinfo pub async fn get_preflightinfo(&mut self) -> Result { - let mut req = Dictionary::new(); - req.insert("command".into(), "getpreflightinfo".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "getpreflightinfo" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -141,12 +128,10 @@ impl<'a, R: ReadWrite + 'a> RestoreServiceClient { /// Get nonces /// Doesn't seem to work pub async fn get_nonces(&mut self) -> Result { - let mut req = Dictionary::new(); - req.insert("command".into(), "getnonces".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "getnonces" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -171,12 +156,10 @@ impl<'a, R: ReadWrite + 'a> RestoreServiceClient { /// Get app parameters /// Doesn't seem to work pub async fn get_app_parameters(&mut self) -> Result { - let mut req = Dictionary::new(); - req.insert("command".into(), "getappparameters".into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "getappparameters" + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { @@ -203,13 +186,11 @@ impl<'a, R: ReadWrite + 'a> RestoreServiceClient { pub async fn restore_lang(&mut self, language: impl Into) -> Result<(), IdeviceError> { let language = language.into(); - let mut req = Dictionary::new(); - req.insert("command".into(), "restorelang".into()); - req.insert("argument".into(), language.into()); - - self.stream - .send_object(plist::Value::Dictionary(req), true) - .await?; + let req = crate::plist!({ + "command": "restorelang", + "argument": language, + }); + self.stream.send_object(req, true).await?; let res = self.stream.recv().await?; let mut res = match res { diff --git a/idevice/src/services/rsd.rs b/idevice/src/services/rsd.rs index 1e78196..1f2555a 100644 --- a/idevice/src/services/rsd.rs +++ b/idevice/src/services/rsd.rs @@ -3,10 +3,10 @@ use std::collections::HashMap; -use log::warn; +use log::{debug, warn}; use serde::Deserialize; -use crate::{provider::RsdProvider, IdeviceError, ReadWrite, RemoteXpcClient}; +use crate::{IdeviceError, ReadWrite, RemoteXpcClient, provider::RsdProvider}; /// Describes an available XPC service #[derive(Debug, Clone, Deserialize)] @@ -23,6 +23,7 @@ pub struct RsdService { pub service_version: Option, } +#[derive(Debug, Clone)] pub struct RsdHandshake { pub services: HashMap, pub protocol_version: usize, @@ -156,13 +157,9 @@ impl RsdHandshake { }) } - pub async fn connect<'a, T, S>( - &mut self, - provider: &'a mut impl RsdProvider<'a, Stream = S>, - ) -> Result + pub async fn connect(&mut self, provider: &mut impl RsdProvider) -> Result where - T: crate::RsdService, - S: ReadWrite, + T: crate::RsdService, { let service_name = T::rsd_service_name(); let service = match self.services.get(&service_name.to_string()) { @@ -172,6 +169,10 @@ impl RsdHandshake { } }; + debug!( + "Connecting to RSD service {service_name} on port {}", + service.port + ); let stream = provider.connect_to_service_port(service.port).await?; T::from_stream(stream).await } diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index a2da261..e8edfec 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -3,7 +3,7 @@ //! Provides functionality for interacting with the SpringBoard services on iOS devices, //! which manages home screen and app icon related operations. -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS SpringBoard services /// @@ -20,41 +20,8 @@ impl IdeviceService for SpringBoardServicesClient { obf!("com.apple.springboardservices") } - /// Establishes a connection to the SpringBoard services - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `SpringBoardServicesClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the SpringBoard services port - /// 4. Establishes connection to the service port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } @@ -90,12 +57,11 @@ impl SpringBoardServicesClient { &mut self, bundle_identifier: String, ) -> Result, IdeviceError> { - let mut req = plist::Dictionary::new(); - req.insert("command".into(), "getIconPNGData".into()); - req.insert("bundleId".into(), bundle_identifier.into()); - self.idevice - .send_plist(plist::Value::Dictionary(req)) - .await?; + let req = crate::plist!({ + "command": "getIconPNGData", + "bundleId": bundle_identifier, + }); + self.idevice.send_plist(req).await?; let mut res = self.idevice.read_plist().await?; match res.remove("pngData") { diff --git a/idevice/src/services/syslog_relay.rs b/idevice/src/services/syslog_relay.rs index 3553757..0b35380 100644 --- a/idevice/src/services/syslog_relay.rs +++ b/idevice/src/services/syslog_relay.rs @@ -1,6 +1,6 @@ //! iOS Device SyslogRelay Service Abstraction -use crate::{lockdown::LockdownClient, obf, Idevice, IdeviceError, IdeviceService}; +use crate::{Idevice, IdeviceError, IdeviceService, obf}; /// Client for interacting with the iOS device SyslogRelay service pub struct SyslogRelayClient { @@ -14,41 +14,8 @@ impl IdeviceService for SyslogRelayClient { obf!("com.apple.syslog_relay") } - /// Establishes a connection to the SyslogRelay service - /// - /// # Arguments - /// * `provider` - Device connection provider - /// - /// # Returns - /// A connected `SyslogRelayClient` instance - /// - /// # Errors - /// Returns `IdeviceError` if any step of the connection process fails - /// - /// # Process - /// 1. Connects to lockdownd service - /// 2. Starts a lockdown session - /// 3. Requests the SyslogRelay service port - /// 4. Establishes connection to the SyslogRelay port - /// 5. Optionally starts TLS if required by service - async fn connect( - provider: &dyn crate::provider::IdeviceProvider, - ) -> Result { - let mut lockdown = LockdownClient::connect(provider).await?; - lockdown - .start_session(&provider.get_pairing_file().await?) - .await?; - - let (port, ssl) = lockdown.start_service(Self::service_name()).await?; - - let mut idevice = provider.connect(port).await?; - if ssl { - idevice - .start_session(&provider.get_pairing_file().await?) - .await?; - } - - Ok(Self { idevice }) + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) } } diff --git a/idevice/src/sni.rs b/idevice/src/sni.rs index 965d12c..1bf9806 100644 --- a/idevice/src/sni.rs +++ b/idevice/src/sni.rs @@ -7,16 +7,16 @@ // Assuming that there's no use for unchecked certs is naive. use rustls::{ - client::{ - danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - WebPkiServerVerifier, - }, - pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime}, ClientConfig, DigitallySignedStruct, + client::{ + WebPkiServerVerifier, + danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + }, + pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime, pem::PemObject}, }; use std::sync::Arc; -use crate::{pairing_file::PairingFile, IdeviceError}; +use crate::{IdeviceError, pairing_file::PairingFile}; #[derive(Debug)] pub struct NoServerNameVerification { diff --git a/idevice/src/tcp/adapter.rs b/idevice/src/tcp/adapter.rs index 0724fd0..9d60868 100644 --- a/idevice/src/tcp/adapter.rs +++ b/idevice/src/tcp/adapter.rs @@ -62,10 +62,13 @@ use std::{collections::HashMap, io::ErrorKind, net::IpAddr, path::Path, sync::Arc}; -use log::trace; -use tokio::{io::AsyncWriteExt, sync::Mutex}; +use log::{debug, trace, warn}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + sync::Mutex, +}; -use crate::ReadWrite; +use crate::{ReadWrite, tcp::packets::IpParseError}; use super::packets::{Ipv4Packet, Ipv6Packet, ProtocolNumber, TcpFlags, TcpPacket}; @@ -121,6 +124,8 @@ pub struct Adapter { /// The states of the connections states: HashMap, // host port by state dropped: Vec, + read_buf: [u8; 4096], + bytes_in_buf: usize, /// Optional PCAP file for packet logging pcap: Option>>, @@ -143,10 +148,19 @@ impl Adapter { peer_ip, states: HashMap::new(), dropped: Vec::new(), + read_buf: [0u8; 4096], + bytes_in_buf: 0, pcap: None, } } + /// Wraps this handle in a new thread. + /// Streams from this handle will be thread safe, with data sent through channels. + /// The handle supports the trait for RSD provider. + pub fn to_async_handle(self) -> super::handle::AdapterHandle { + super::handle::AdapterHandle::new(self) + } + /// Initiates a TCP connection to the specified port. /// /// # Arguments @@ -191,6 +205,7 @@ impl Adapter { // Wait for the syn ack self.states.insert(host_port, state); + let start_time = std::time::Instant::now(); loop { self.process_tcp_packet().await?; if let Some(s) = self.states.get(&host_port) { @@ -199,9 +214,15 @@ impl Adapter { break; } ConnectionStatus::Error(e) => { - return Err(std::io::Error::new(e, "failed to connect")) + return Err(std::io::Error::new(e, "failed to connect")); } ConnectionStatus::WaitingForSyn => { + if start_time.elapsed() > std::time::Duration::from_secs(5) { + return Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "didn't syn in time", + )); + } continue; } } @@ -229,7 +250,7 @@ impl Adapter { file.write_all(&0_i32.to_le_bytes()).await?; // timezone file.write_all(&0_u32.to_le_bytes()).await?; // accuracy file.write_all(&(u16::MAX as u32).to_le_bytes()).await?; // snaplen - // https://www.tcpdump.org/linktypes.html + // https://www.tcpdump.org/linktypes.html file.write_all(&101_u32.to_le_bytes()).await?; // link type self.pcap = Some(Arc::new(Mutex::new(file))); @@ -435,6 +456,19 @@ impl Adapter { } } + pub(crate) fn uncache_all(&mut self, host_port: u16) -> Result, std::io::Error> { + if let Some(state) = self.states.get_mut(&host_port) { + let res = state.read_buffer[..].to_vec(); + state.read_buffer.clear(); + Ok(res) + } else { + Err(std::io::Error::new( + ErrorKind::NotConnected, + "not connected", + )) + } + } + pub(crate) fn cache_read( &mut self, payload: &[u8], @@ -498,66 +532,108 @@ impl Adapter { async fn read_ip_packet(&mut self) -> Result, std::io::Error> { self.write_buffer_flush().await?; Ok(loop { - match self.host_ip { - IpAddr::V4(_) => { - let packet = Ipv4Packet::from_reader(&mut self.peer, &self.pcap).await?; - trace!("IPv4 packet: {packet:#?}"); - if packet.protocol == 6 { - break packet.payload; - } + // try the data we already have + match Ipv6Packet::parse(&self.read_buf[..self.bytes_in_buf], &self.pcap) { + IpParseError::Ok { + packet, + bytes_consumed, + } => { + // And remove it from the buffer by shifting the remaining bytes + self.read_buf + .copy_within(bytes_consumed..self.bytes_in_buf, 0); + self.bytes_in_buf -= bytes_consumed; + break packet.payload; } - IpAddr::V6(_) => { - let packet = Ipv6Packet::from_reader(&mut self.peer, &self.pcap).await?; - trace!("IPv6 packet: {packet:#?}"); - if packet.next_header == 6 { - break packet.payload; - } + IpParseError::NotEnough => { + // Buffer doesn't have a full packet, wait for the next read + } + IpParseError::Invalid => { + // Corrupted data, close the connection + return Err(std::io::Error::new( + ErrorKind::InvalidData, + "invalid IPv6 parse", + )); } } + // go get more + let s = self + .peer + .read(&mut self.read_buf[self.bytes_in_buf..]) + .await?; + + self.bytes_in_buf += s; }) } - async fn process_tcp_packet(&mut self) -> Result<(), std::io::Error> { - loop { - let ip_packet = self.read_ip_packet().await?; - let res = TcpPacket::parse(&ip_packet)?; - let mut ack_me = None; - if let Some(state) = self.states.get_mut(&res.destination_port) { - if state.peer_seq > res.sequence_number { - // ignore retransmission - continue; - } + pub(crate) async fn process_tcp_packet(&mut self) -> Result<(), std::io::Error> { + tokio::select! { + ip_packet = self.read_ip_packet() => { + let ip_packet = ip_packet?; + self.process_tcp_packet_from_payload(&ip_packet).await + } + _ = tokio::time::sleep(std::time::Duration::from_secs(15)) => { + Ok(()) + } + } + } - state.peer_seq = res.sequence_number + res.payload.len() as u32; - state.ack = res.sequence_number - + if res.payload.is_empty() && state.status != ConnectionStatus::Connected { - 1 - } else { - res.payload.len() as u32 - }; - if res.flags.psh || !res.payload.is_empty() { - ack_me = Some(res.destination_port); - state.read_buffer.extend(res.payload); - } - if res.flags.rst { - state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); - } - if res.flags.fin { - ack_me = Some(res.destination_port); - state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); - } - if res.flags.syn && res.flags.ack { - ack_me = Some(res.destination_port); - state.seq = state.seq.wrapping_add(1); - state.status = ConnectionStatus::Connected; - } + pub(crate) async fn process_tcp_packet_from_payload( + &mut self, + payload: &[u8], + ) -> Result<(), std::io::Error> { + let res = TcpPacket::parse(payload)?; + let mut ack_me = None; + + if let Some(state) = self.states.get(&res.destination_port) { + // A keep-alive probe: ACK set, no payload, and seq == RCV.NXT - 1 + let is_keepalive = res.flags.ack + && res.payload.is_empty() + && res.sequence_number.wrapping_add(1) == state.ack; + + if is_keepalive { + // Don't update any seq/ack state; just ACK what we already expect. + debug!("responding to keep-alive probe"); + let port = res.destination_port; + self.ack(port).await?; + return Ok(()); + } + } + + if let Some(state) = self.states.get_mut(&res.destination_port) { + if state.peer_seq > res.sequence_number { + // ignore retransmission + return Ok(()); } - // we have to ack outside of the mutable state borrow - if let Some(a) = ack_me { - self.ack(a).await?; + state.peer_seq = res.sequence_number + res.payload.len() as u32; + state.ack = res.sequence_number + + if res.payload.is_empty() && state.status != ConnectionStatus::Connected { + 1 + } else { + res.payload.len() as u32 + }; + if res.flags.psh || !res.payload.is_empty() { + ack_me = Some(res.destination_port); + state.read_buffer.extend(res.payload); } - break; + if res.flags.rst { + warn!("stream rst"); + state.status = ConnectionStatus::Error(ErrorKind::ConnectionReset); + } + if res.flags.fin { + ack_me = Some(res.destination_port); + state.status = ConnectionStatus::Error(ErrorKind::UnexpectedEof); + } + if res.flags.syn && res.flags.ack { + ack_me = Some(res.destination_port); + state.seq = state.seq.wrapping_add(1); + state.status = ConnectionStatus::Connected; + } + } + + // we have to ack outside of the mutable state borrow + if let Some(a) = ack_me { + self.ack(a).await?; } Ok(()) } diff --git a/idevice/src/tcp/handle.rs b/idevice/src/tcp/handle.rs new file mode 100644 index 0000000..183704b --- /dev/null +++ b/idevice/src/tcp/handle.rs @@ -0,0 +1,372 @@ +// So originally, streams wrote to the adapter via a mutable reference. +// This worked fine for most applications, but the lifetime requirement of the stream +// makes things difficult. This was especially apparent when trying to integrate with lockdown +// services that were swapped on the heap. This will also allow for usage across threads, +// especially in FFI. Judging the tradeoffs, we'll go forward with it. + +use std::{collections::HashMap, path::PathBuf, sync::Mutex, task::Poll}; + +use crossfire::{AsyncRx, MTx, Tx, mpsc, spsc, stream::AsyncStream}; +use futures::{StreamExt, stream::FuturesUnordered}; +use log::trace; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + sync::oneshot, + time::timeout, +}; + +use crate::tcp::adapter::ConnectionStatus; + +pub type ConnectToPortRes = + oneshot::Sender, std::io::Error>>), std::io::Error>>; + +enum HandleMessage { + /// Returns the host port + ConnectToPort { + target: u16, + res: ConnectToPortRes, + }, + Close { + host_port: u16, + }, + Send { + host_port: u16, + data: Vec, + res: oneshot::Sender>, + }, + Pcap { + path: PathBuf, + res: oneshot::Sender>, + }, +} + +#[derive(Debug)] +pub struct AdapterHandle { + sender: MTx, +} + +impl AdapterHandle { + pub fn new(mut adapter: super::adapter::Adapter) -> Self { + let (tx, rx) = mpsc::unbounded_async(); + tokio::spawn(async move { + let mut handles: HashMap, std::io::Error>>> = HashMap::new(); + let mut tick = tokio::time::interval(std::time::Duration::from_millis(1)); + + loop { + tokio::select! { + // check for messages for us + msg = rx.recv() => { + match msg { + Ok(m) => match m { + HandleMessage::ConnectToPort { target, res } => { + let connect_response = match adapter.connect(target).await { + Ok(c) => { + let (ptx, prx) = spsc::unbounded_async(); + handles.insert(c, ptx); + Ok((c, prx)) + } + Err(e) => Err(e), + }; + res.send(connect_response).ok(); + } + HandleMessage::Close { host_port } => { + handles.remove(&host_port); + adapter.close(host_port).await.ok(); + } + HandleMessage::Send { + host_port, + data, + res, + } => { + if let Err(e) = adapter.queue_send(&data, host_port) { + res.send(Err(e)).ok(); + } else { + let response = adapter.write_buffer_flush().await; + res.send(response).ok(); + } + } + HandleMessage::Pcap { + path, + res + } => { + res.send(adapter.pcap(path).await).ok(); + } + }, + Err(_) => { + break; + }, + } + } + + r = adapter.process_tcp_packet() => { + if let Err(e) = r { + // propagate error to all streams; close them + for (hp, tx) in handles.drain() { + let _ = tx.send(Err(e.kind().into())); // or clone/convert + let _ = adapter.close(hp).await; + } + break; + } + + // Push any newly available bytes to per-conn channels + let mut dead = Vec::new(); + for (&hp, tx) in &handles { + match adapter.uncache_all(hp) { + Ok(buf) if !buf.is_empty() => { + if tx.send(Ok(buf)).is_err() { + dead.push(hp); + } + } + Err(e) => { + let _ = tx.send(Err(e)); + dead.push(hp); + } + _ => {} + } + } + for hp in dead { + handles.remove(&hp); + let _ = adapter.close(hp).await; + } + + let mut to_close = Vec::new(); + for (&hp, tx) in &handles { + if let Ok(ConnectionStatus::Error(kind)) = adapter.get_status(hp) { + if kind == std::io::ErrorKind::UnexpectedEof { + to_close.push(hp); + } else { + let _ = tx.send(Err(std::io::Error::from(kind))); + to_close.push(hp); + } + } + } + for hp in to_close { + handles.remove(&hp); + // Best-effort close. For RST this just tidies state on our side + let _ = adapter.close(hp).await; + } + } + + _ = tick.tick() => { + let _ = adapter.write_buffer_flush().await; + } + } + } + }); + + Self { sender: tx } + } + + pub async fn connect(&mut self, port: u16) -> Result { + let (res_tx, res_rx) = oneshot::channel(); + if self + .sender + .send(HandleMessage::ConnectToPort { + target: port, + res: res_tx, + }) + .is_err() + { + return Err(std::io::Error::new( + std::io::ErrorKind::NetworkUnreachable, + "adapter closed", + )); + } + + match timeout(std::time::Duration::from_secs(8), res_rx).await { + Ok(Ok(r)) => { + let (host_port, recv_channel) = r?; + Ok(StreamHandle { + host_port, + recv_channel: Mutex::new(recv_channel.into_stream()), + send_channel: self.sender.clone(), + read_buffer: Vec::new(), + pending_writes: FuturesUnordered::new(), + }) + } + Ok(Err(_)) => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "adapter closed", + )), + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "channel recv timeout", + )), + } + } + + pub async fn pcap(&mut self, path: impl Into) -> Result<(), std::io::Error> { + let (res_tx, res_rx) = oneshot::channel(); + let path: PathBuf = path.into(); + + if self + .sender + .send(HandleMessage::Pcap { path, res: res_tx }) + .is_err() + { + return Err(std::io::Error::new( + std::io::ErrorKind::NetworkUnreachable, + "adapter closed", + )); + } + + match res_rx.await { + Ok(r) => r, + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "adapter closed", + )), + } + } +} + +#[derive(Debug)] +pub struct StreamHandle { + host_port: u16, + recv_channel: Mutex, std::io::Error>>>, + send_channel: MTx, + + read_buffer: Vec, + pending_writes: FuturesUnordered>>, +} + +impl StreamHandle { + pub fn close(&mut self) { + let _ = self.send_channel.send(HandleMessage::Close { + host_port: self.host_port, + }); + } +} + +impl AsyncRead for StreamHandle { + /// Attempts to read from the connection into the provided buffer. + /// + /// Uses an internal read buffer to cache any extra received data. + /// + /// # Returns + /// * `Poll::Ready(Ok(()))` if data was read successfully + /// * `Poll::Ready(Err(e))` if an error occurred + /// * `Poll::Pending` if operation would block + /// + /// # Errors + /// * Returns `NotConnected` if adapter isn't connected + /// * Propagates any underlying transport errors + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + // 1) Serve from cache first. + if !self.read_buffer.is_empty() { + let n = buf.remaining().min(self.read_buffer.len()); + buf.put_slice(&self.read_buffer[..n]); + self.read_buffer.drain(..n); // fewer allocs than to_vec + reassign + return Poll::Ready(Ok(())); + } + + // 2) Poll the channel directly; this registers the waker on Empty. + let mut lock = self + .recv_channel + .lock() + .expect("somehow the mutex was poisoned"); + // this should always return, since we're the only owner of the mutex. The mutex is only + // used to satisfy the `Send` bounds of ReadWrite. + let mut extend_slice = Vec::new(); + let res = match lock.poll_item(cx) { + Poll::Pending => Poll::Pending, + + // Disconnected/ended: map to BrokenPipe + Poll::Ready(None) => Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed", + ))), + + // Got a chunk: copy what fits; cache the tail. + Poll::Ready(Some(res)) => match res { + Ok(data) => { + let n = buf.remaining().min(data.len()); + buf.put_slice(&data[..n]); + if n < data.len() { + extend_slice = data[n..].to_vec(); + } + Poll::Ready(Ok(())) + } + Err(e) => Poll::Ready(Err(e)), + }, + }; + std::mem::drop(lock); + self.read_buffer.extend(extend_slice); + res + } +} + +impl AsyncWrite for StreamHandle { + /// Attempts to write data to the connection. + /// + /// Data is buffered internally until flushed. + /// + /// # Returns + /// * `Poll::Ready(Ok(n))` with number of bytes written + /// * `Poll::Ready(Err(e))` if an error occurred + /// * `Poll::Pending` if operation would block + /// + /// # Errors + /// * Returns `NotConnected` if adapter isn't connected + fn poll_write( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + trace!("poll psh {}", buf.len()); + let (tx, rx) = oneshot::channel(); + self.send_channel + .send(HandleMessage::Send { + host_port: self.host_port, + data: buf.to_vec(), + res: tx, + }) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?; + self.pending_writes.push(rx); + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + while let Poll::Ready(maybe) = self.pending_writes.poll_next_unpin(cx) { + match maybe { + Some(Ok(Ok(()))) => {} + Some(Ok(Err(e))) => return Poll::Ready(Err(e)), + Some(Err(_canceled)) => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed", + ))); + } + None => break, // nothing pending + } + } + if self.pending_writes.is_empty() { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + // Just a drop will close the channel, which will trigger a close + std::task::Poll::Ready(Ok(())) + } +} + +impl Drop for StreamHandle { + fn drop(&mut self) { + let _ = self.send_channel.send(HandleMessage::Close { + host_port: self.host_port, + }); + } +} diff --git a/idevice/src/tcp/mod.rs b/idevice/src/tcp/mod.rs index 0c4424d..c984672 100644 --- a/idevice/src/tcp/mod.rs +++ b/idevice/src/tcp/mod.rs @@ -5,18 +5,18 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use log::debug; -use stream::AdapterStream; +use log::trace; use tokio::io::AsyncWriteExt; -use crate::provider::RsdProvider; +use crate::{ReadWrite, provider::RsdProvider}; pub mod adapter; +pub mod handle; pub mod packets; pub mod stream; pub(crate) fn log_packet(file: &Arc>, packet: &[u8]) { - debug!("Logging {} byte packet", packet.len()); + trace!("Logging {} byte packet", packet.len()); let packet = packet.to_vec(); let file = file.to_owned(); let now = SystemTime::now(); @@ -39,16 +39,14 @@ pub(crate) fn log_packet(file: &Arc>, packet }); } -impl<'a> RsdProvider<'a> for adapter::Adapter { +impl RsdProvider for handle::AdapterHandle { async fn connect_to_service_port( - &'a mut self, + &mut self, port: u16, - ) -> Result, crate::IdeviceError> { - let s = stream::AdapterStream::connect(self, port).await?; - Ok(s) + ) -> Result, crate::IdeviceError> { + let s = self.connect(port).await?; + Ok(Box::new(s)) } - - type Stream = AdapterStream<'a>; } #[cfg(test)] diff --git a/idevice/src/tcp/packets.rs b/idevice/src/tcp/packets.rs index fa9e94f..4ae328f 100644 --- a/idevice/src/tcp/packets.rs +++ b/idevice/src/tcp/packets.rs @@ -6,6 +6,7 @@ use std::{ sync::Arc, }; +use log::debug; use tokio::{ io::{AsyncRead, AsyncReadExt}, sync::Mutex, @@ -109,6 +110,7 @@ impl Ipv4Packet { let ihl = (version_ihl & 0x0F) * 4; if version != 4 || ihl < 20 { + debug!("Got an invalid IPv4 header from reader"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IPv4 header", @@ -220,21 +222,37 @@ pub struct Ipv6Packet { pub payload: Vec, } +#[derive(Debug, Clone)] +pub(crate) enum IpParseError { + Ok { packet: T, bytes_consumed: usize }, + NotEnough, + Invalid, +} + impl Ipv6Packet { - pub fn parse(packet: &[u8]) -> Option { + pub(crate) fn parse( + packet: &[u8], + log: &Option>>, + ) -> IpParseError { if packet.len() < 40 { - return None; + return IpParseError::NotEnough; } let version = packet[0] >> 4; if version != 6 { - return None; + return IpParseError::Invalid; } let traffic_class = ((packet[0] & 0x0F) << 4) | (packet[1] >> 4); let flow_label = ((packet[1] as u32 & 0x0F) << 16) | ((packet[2] as u32) << 8) | packet[3] as u32; let payload_length = u16::from_be_bytes([packet[4], packet[5]]); + let total_packet_len = 40 + payload_length as usize; + + if packet.len() < total_packet_len { + return IpParseError::NotEnough; + } + let next_header = packet[6]; let hop_limit = packet[7]; let source = Ipv6Addr::new( @@ -258,19 +276,29 @@ impl Ipv6Packet { u16::from_be_bytes([packet[36], packet[37]]), u16::from_be_bytes([packet[38], packet[39]]), ); - let payload = packet[40..].to_vec(); + let payload = packet[40..total_packet_len].to_vec(); - Some(Self { - version, - traffic_class, - flow_label, - payload_length, - next_header, - hop_limit, - source, - destination, - payload, - }) + if let Some(log) = log { + let mut log_packet = Vec::new(); + log_packet.extend_from_slice(&packet[..40]); + log_packet.extend_from_slice(&payload); + super::log_packet(log, &log_packet); + } + + IpParseError::Ok { + packet: Self { + version, + traffic_class, + flow_label, + payload_length, + next_header, + hop_limit, + source, + destination, + payload, + }, + bytes_consumed: total_packet_len, + } } pub async fn from_reader( @@ -278,14 +306,17 @@ impl Ipv6Packet { log: &Option>>, ) -> Result { let mut log_packet = Vec::new(); + let mut header = [0u8; 40]; // IPv6 header size is fixed at 40 bytes reader.read_exact(&mut header).await?; + if log.is_some() { log_packet.extend_from_slice(&header); } let version = header[0] >> 4; if version != 6 { + debug!("Got an invalid IPv6 header from reader"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IPv6 header", @@ -457,6 +488,7 @@ pub struct TcpPacket { impl TcpPacket { pub fn parse(packet: &[u8]) -> Result { if packet.len() < 20 { + debug!("Got an invalid TCP header"); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Not enough bytes for TCP header", @@ -677,7 +709,7 @@ mod tests { ); println!("{b1:02X?}"); - let ip1 = Ipv6Packet::parse(&b1); + let ip1 = Ipv6Packet::parse(&b1, &None); println!("{ip1:#?}"); } diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs index d240ecc..46ff2cb 100644 --- a/idevice/src/tss.rs +++ b/idevice/src/tss.rs @@ -8,7 +8,7 @@ use log::{debug, warn}; use plist::Value; -use crate::{util::plist_to_xml_bytes, IdeviceError}; +use crate::{IdeviceError, util::plist_to_xml_bytes}; /// TSS client version string sent in requests const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; @@ -30,13 +30,11 @@ impl TSSRequest { /// - Client version string /// - Random UUID for request identification pub fn new() -> Self { - let mut inner = plist::Dictionary::new(); - inner.insert("@HostPlatformInfo".into(), "mac".into()); - inner.insert("@VersionInfo".into(), TSS_CLIENT_VERSION_STRING.into()); - inner.insert( - "@UUID".into(), - uuid::Uuid::new_v4().to_string().to_uppercase().into(), - ); + let inner = crate::plist!(dict { + "@HostPlatformInfo": "mac", + "@VersionInfo": TSS_CLIENT_VERSION_STRING, + "@UUID": uuid::Uuid::new_v4().to_string().to_uppercase() + }); Self { inner } } @@ -172,15 +170,15 @@ pub fn apply_restore_request_rules( for (key, value) in actions { // Skip special values (255 typically means "ignore") - if let Some(i) = value.as_unsigned_integer() { - if i == 255 { - continue; - } + if let Some(i) = value.as_unsigned_integer() + && i == 255 + { + continue; } - if let Some(i) = value.as_signed_integer() { - if i == 255 { - continue; - } + if let Some(i) = value.as_signed_integer() + && i == 255 + { + continue; } input.remove(key); // Explicitly remove before inserting diff --git a/idevice/src/tunneld.rs b/idevice/src/tunneld.rs index 2c4acbd..f6f747c 100644 --- a/idevice/src/tunneld.rs +++ b/idevice/src/tunneld.rs @@ -101,9 +101,8 @@ mod tests { async fn test_get_tunneld_devices() { let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT); match get_tunneld_devices(host).await { - Ok(devices) => println!("Found tunneld devices: {:#?}", devices), - Err(e) => println!("Error querying tunneld: {}", e), + Ok(devices) => println!("Found tunneld devices: {devices:#?}"), + Err(e) => println!("Error querying tunneld: {e}"), } } } - diff --git a/idevice/src/usbmuxd/mod.rs b/idevice/src/usbmuxd/mod.rs index 3d18005..9953feb 100644 --- a/idevice/src/usbmuxd/mod.rs +++ b/idevice/src/usbmuxd/mod.rs @@ -15,7 +15,7 @@ use log::{debug, warn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ - pairing_file::PairingFile, provider::UsbmuxdProvider, Idevice, IdeviceError, ReadWrite, + Idevice, IdeviceError, ReadWrite, pairing_file::PairingFile, provider::UsbmuxdProvider, }; mod des; @@ -335,6 +335,32 @@ impl UsbmuxdConnection { } } + /// Tells usbmuxd to save the pairing record in its storage + /// + /// # Arguments + /// * `device_id` - usbmuxd device ID + /// * `udid` - the device UDID/serial + /// * `pair_record` - a serialized plist of the pair record + pub async fn save_pair_record( + &mut self, + device_id: u32, + udid: &str, + pair_record: Vec, + ) -> Result<(), IdeviceError> { + let req = crate::plist!(dict { + "MessageType": "SavePairRecord", + "PairRecordData": pair_record, + "DeviceID": device_id, + "PairRecordID": udid, + }); + self.write_plist(req).await?; + let res = self.read_plist().await?; + match res.get("Number").and_then(|x| x.as_unsigned_integer()) { + Some(0) => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + /// Writes a PLIST message to usbmuxd async fn write_plist(&mut self, req: plist::Dictionary) -> Result<(), IdeviceError> { let raw = raw_packet::RawPacket::new( diff --git a/idevice/src/util.rs b/idevice/src/util.rs index 1b7ee06..4e69565 100644 --- a/idevice/src/util.rs +++ b/idevice/src/util.rs @@ -103,25 +103,25 @@ fn print_plist(p: &Value, indentation: usize) -> String { .collect(); format!("{{\n{}\n{}}}", items.join(",\n"), indent) } - Value::Boolean(b) => format!("{}", b), + Value::Boolean(b) => format!("{b}"), Value::Data(vec) => { let len = vec.len(); let preview: String = vec .iter() .take(20) - .map(|b| format!("{:02X}", b)) + .map(|b| format!("{b:02X}")) .collect::>() .join(" "); if len > 20 { - format!("Data({}... Len: {})", preview, len) + format!("Data({preview}... Len: {len})") } else { - format!("Data({} Len: {})", preview, len) + format!("Data({preview} Len: {len})") } } Value::Date(date) => format!("Date({})", date.to_xml_format()), - Value::Real(f) => format!("{}", f), - Value::Integer(i) => format!("{}", i), - Value::String(s) => format!("\"{}\"", s), + Value::Real(f) => format!("{f}"), + Value::Integer(i) => format!("{i}"), + Value::String(s) => format!("\"{s}\""), Value::Uid(_uid) => "Uid(?)".to_string(), _ => "Unknown".to_string(), } diff --git a/idevice/src/utils/installation.rs b/idevice/src/utils/installation.rs new file mode 100644 index 0000000..748363a --- /dev/null +++ b/idevice/src/utils/installation.rs @@ -0,0 +1,306 @@ +//! High-level install/upgrade helpers +//! +//! This module provides convenient wrappers that mirror ideviceinstaller's +//! behavior by uploading a package to `PublicStaging` via AFC and then +//! issuing `Install`/`Upgrade` commands through InstallationProxy. +//! +//! Notes: +//! - The package path used by InstallationProxy must be a path inside the +//! AFC jail (e.g. `PublicStaging/`) +//! - For `.ipa` files, we upload the whole file to `PublicStaging/` +//! - For directories (developer bundles), we recursively mirror the directory +//! into `PublicStaging/` and pass that directory path. + +use std::path::Path; + +use crate::{ + IdeviceError, IdeviceService, + provider::IdeviceProvider, + services::{ + afc::{AfcClient, opcode::AfcFopenMode}, + installation_proxy::InstallationProxyClient, + }, +}; + +const PUBLIC_STAGING: &str = "PublicStaging"; + +/// Result of a prepared upload, containing the remote path to use in Install/Upgrade +struct UploadedPackageInfo { + /// Path inside the AFC jail for InstallationProxy `PackagePath` + remote_package_path: String, +} + +/// Ensure `PublicStaging` exists on device via AFC +async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { + // Try to stat and if it fails, create directory + match afc.get_file_info(PUBLIC_STAGING).await { + Ok(_) => Ok(()), + Err(_) => afc.mk_dir(PUBLIC_STAGING).await, + } +} + +/// Upload a single file to a destination path on device using AFC +async fn afc_upload_file( + afc: &mut AfcClient, + local_path: &Path, + remote_path: &str, +) -> Result<(), IdeviceError> { + let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?; + let bytes = tokio::fs::read(local_path).await?; + fd.write(&bytes).await?; + fd.close().await +} + +/// Recursively upload a directory to device via AFC (mirror contents) +async fn afc_upload_dir( + afc: &mut AfcClient, + local_dir: &Path, + remote_dir: &str, +) -> Result<(), IdeviceError> { + use std::collections::VecDeque; + afc.mk_dir(remote_dir).await.ok(); + + let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new(); + queue.push_back((local_dir.to_path_buf(), remote_dir.to_string())); + + while let Some((cur_local, cur_remote)) = queue.pop_front() { + let mut rd = tokio::fs::read_dir(&cur_local).await?; + while let Some(entry) = rd.next_entry().await? { + let meta = entry.metadata().await?; + let name = entry.file_name(); + let name = name.to_string_lossy().into_owned(); + if name == "." || name == ".." { + continue; + } + let child_local = entry.path(); + let child_remote = format!("{}/{}", cur_remote, name); + if meta.is_dir() { + afc.mk_dir(&child_remote).await.ok(); + queue.push_back((child_local, child_remote)); + } else if meta.is_file() { + afc_upload_file(afc, &child_local, &child_remote).await?; + } + } + } + Ok(()) +} + +/// Upload a package to `PublicStaging` and return its InstallationProxy path +/// +/// - If `local_path` is a file, it will be uploaded to `PublicStaging/` +/// - If it is a directory, it will be mirrored to `PublicStaging/` +async fn upload_package_to_public_staging>( + provider: &dyn IdeviceProvider, + local_path: P, +) -> Result { + // Connect to AFC via the generic service connector + let mut afc = AfcClient::connect(provider).await?; + + ensure_public_staging(&mut afc).await?; + + let local_path = local_path.as_ref(); + let file_name: String = local_path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .ok_or_else(|| IdeviceError::InvalidArgument)?; + let remote_path = format!("{}/{}", PUBLIC_STAGING, file_name); + + let meta = tokio::fs::metadata(local_path).await?; + if meta.is_dir() { + afc_upload_dir(&mut afc, local_path, &remote_path).await?; + } else { + afc_upload_file(&mut afc, local_path, &remote_path).await?; + } + + Ok(UploadedPackageInfo { + remote_package_path: remote_path, + }) +} + +/// Install an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install(remote_package_path, options).await +} + +/// Upgrade an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade(remote_package_path, options).await +} + +/// Same as `install_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn install_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install_with_callback(remote_package_path, options, callback, state) + .await +} + +/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn upgrade_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_package_to_public_staging(provider, local_path).await?; + + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade_with_callback(remote_package_path, options, callback, state) + .await +} + +/// Upload raw bytes to `PublicStaging/` via AFC and return the remote package path. +/// +/// - This is useful when the package is not present on disk or is generated in-memory. +async fn upload_bytes_to_public_staging( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, +) -> Result { + // Connect to AFC + let mut afc = AfcClient::connect(provider).await?; + ensure_public_staging(&mut afc).await?; + + let remote_path = format!("{}/{}", PUBLIC_STAGING, remote_name); + let mut fd = afc.open(&remote_path, AfcFopenMode::WrOnly).await?; + fd.write(data.as_ref()).await?; + fd.close().await?; + + Ok(UploadedPackageInfo { + remote_package_path: remote_path, + }) +} + +/// Install an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Install`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install(remote_package_path, options).await +} + +/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the install operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn install_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.install_with_callback(remote_package_path, options, callback, state) + .await +} + +/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Upgrade`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, +) -> Result<(), IdeviceError> { + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade(remote_package_path, options).await +} + +/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the upgrade operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn upgrade_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + remote_name: &str, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let UploadedPackageInfo { + remote_package_path, + } = upload_bytes_to_public_staging(provider, data, remote_name).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + inst.upgrade_with_callback(remote_package_path, options, callback, state) + .await +} diff --git a/idevice/src/utils/mod.rs b/idevice/src/utils/mod.rs new file mode 100644 index 0000000..d30df75 --- /dev/null +++ b/idevice/src/utils/mod.rs @@ -0,0 +1,4 @@ +// Utility modules for higher-level operations built on top of services + +#[cfg(all(feature = "afc", feature = "installation_proxy"))] +pub mod installation; diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index 2c73029..bc67f63 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -18,6 +18,9 @@ pub enum XPCFlag { WantingReply, InitHandshake, + FileTxStreamRequest, + FileTxStreamResponse, + Custom(u32), } @@ -28,6 +31,8 @@ impl From for u32 { XPCFlag::DataFlag => 0x00000100, XPCFlag::WantingReply => 0x00010000, XPCFlag::InitHandshake => 0x00400000, + XPCFlag::FileTxStreamRequest => 0x00100000, + XPCFlag::FileTxStreamResponse => 0x00200000, XPCFlag::Custom(inner) => inner, } } @@ -68,6 +73,7 @@ pub enum XPCType { String = 0x00009000, Data = 0x00008000, Uuid = 0x0000a000, + FileTransfer = 0x0001a000, } impl TryFrom for XPCType { @@ -85,6 +91,7 @@ impl TryFrom for XPCType { 0x00009000 => Ok(Self::String), 0x00008000 => Ok(Self::Data), 0x0000a000 => Ok(Self::Uuid), + 0x0001a000 => Ok(Self::FileTransfer), _ => Err(IdeviceError::UnknownXpcType(value))?, } } @@ -107,6 +114,8 @@ pub enum XPCObject { String(String), Data(Vec), Uuid(uuid::Uuid), + + FileTransfer { msg_id: u64, data: Box }, } impl From for XPCObject { @@ -153,6 +162,12 @@ impl XPCObject { } plist::Value::Dictionary(dict) } + Self::FileTransfer { msg_id, data } => { + crate::plist!({ + "msg_id": *msg_id, + "data": data.to_plist(), + }) + } } } @@ -237,9 +252,13 @@ impl XPCObject { } XPCObject::Uuid(uuid) => { buf.extend_from_slice(&(XPCType::Uuid as u32).to_le_bytes()); - buf.extend_from_slice(&16_u32.to_le_bytes()); buf.extend_from_slice(uuid.as_bytes()); } + XPCObject::FileTransfer { msg_id, data } => { + buf.extend_from_slice(&(XPCType::FileTransfer as u32).to_le_bytes()); + buf.extend_from_slice(&msg_id.to_le_bytes()); + data.encode_object(buf)?; + } } Ok(()) } @@ -370,6 +389,18 @@ impl XPCObject { cursor.read_exact(&mut data)?; Ok(XPCObject::Uuid(uuid::Builder::from_bytes(data).into_uuid())) } + XPCType::FileTransfer => { + let mut id_buf = [0u8; 8]; + cursor.read_exact(&mut id_buf)?; + let msg_id = u64::from_le_bytes(id_buf); + + // The next thing in the stream is a full XPC object + let inner = Self::decode_object(cursor)?; + Ok(XPCObject::FileTransfer { + msg_id, + data: Box::new(inner), + }) + } } } @@ -380,6 +411,13 @@ impl XPCObject { } } + pub fn to_dictionary(self) -> Option { + match self { + XPCObject::Dictionary(dict) => Some(dict), + _ => None, + } + } + pub fn as_array(&self) -> Option<&Vec> { match self { XPCObject::Array(array) => Some(array), @@ -529,7 +567,7 @@ impl std::fmt::Debug for XPCMessage { let known_mask = 0x00000001 | 0x00000100 | 0x00010000 | 0x00400000; let custom_bits = self.flags & !known_mask; if custom_bits != 0 { - parts.push(format!("Custom(0x{:08X})", custom_bits)); + parts.push(format!("Custom(0x{custom_bits:08X})")); } write!( diff --git a/idevice/src/xpc/http2/mod.rs b/idevice/src/xpc/http2/mod.rs index 7e233e3..88ce037 100644 --- a/idevice/src/xpc/http2/mod.rs +++ b/idevice/src/xpc/http2/mod.rs @@ -17,7 +17,7 @@ pub struct Http2Client { cache: HashMap>>, } -impl<'a, R: ReadWrite + 'a> Http2Client { +impl Http2Client { /// Writes the magic and inits the caches pub async fn new(mut inner: R) -> Result { inner.write_all(HTTP2_MAGIC).await?; @@ -28,13 +28,6 @@ impl<'a, R: ReadWrite + 'a> Http2Client { }) } - pub fn box_inner(self) -> Http2Client> { - Http2Client { - inner: Box::new(self.inner), - cache: self.cache, - } - } - pub async fn set_settings( &mut self, settings: Vec, @@ -67,7 +60,8 @@ impl<'a, R: ReadWrite + 'a> Http2Client { } pub async fn open_stream(&mut self, stream_id: u32) -> Result<(), IdeviceError> { - self.cache.insert(stream_id, VecDeque::new()); + // Sometimes Apple is silly and sends data to a stream that isn't open + self.cache.entry(stream_id).or_default(); let frame = frame::HeadersFrame { stream_id }.serialize(); self.inner.write_all(&frame).await?; self.inner.flush().await?; @@ -131,11 +125,14 @@ impl<'a, R: ReadWrite + 'a> Http2Client { let c = match self.cache.get_mut(&data_frame.stream_id) { Some(c) => c, None => { + // Sometimes Apple is a little silly and sends data before the + // stream is open. warn!( "Received message for stream ID {} not in cache", data_frame.stream_id ); - continue; + self.cache.insert(data_frame.stream_id, VecDeque::new()); + self.cache.get_mut(&data_frame.stream_id).unwrap() } }; c.push_back(data_frame.payload); diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index 0931c00..3b580dc 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -1,5 +1,7 @@ // Jackson Coxson +use async_stream::try_stream; +use futures::Stream; use http2::Setting; use log::debug; @@ -17,26 +19,17 @@ const REPLY_CHANNEL: u32 = 3; pub struct RemoteXpcClient { h2_client: http2::Http2Client, root_id: u64, - reply_id: u64, + // reply_id: u64 // maybe not used? } -impl<'a, R: ReadWrite + 'a> RemoteXpcClient { +impl RemoteXpcClient { pub async fn new(socket: R) -> Result { Ok(Self { h2_client: http2::Http2Client::new(socket).await?, root_id: 1, - reply_id: 1, }) } - pub fn box_inner(self) -> RemoteXpcClient> { - RemoteXpcClient { - h2_client: self.h2_client.box_inner(), - root_id: self.root_id, - reply_id: self.reply_id, - } - } - pub async fn do_handshake(&mut self) -> Result<(), IdeviceError> { self.h2_client .set_settings( @@ -94,11 +87,11 @@ impl<'a, R: ReadWrite + 'a> RemoteXpcClient { match msg.message { Some(msg) => { - if let Some(d) = msg.as_dictionary() { - if d.is_empty() { - msg_buffer.clear(); - continue; - } + if let Some(d) = msg.as_dictionary() + && d.is_empty() + { + msg_buffer.clear(); + continue; } break Ok(msg.to_plist()); } @@ -142,4 +135,78 @@ impl<'a, R: ReadWrite + 'a> RemoteXpcClient { .await?; Ok(()) } + + pub fn iter_file_chunks<'a>( + &'a mut self, + total_size: usize, + file_idx: u32, + ) -> impl Stream, IdeviceError>> + 'a { + let stream_id = (file_idx + 1) * 2; + + try_stream! { + fn strip_xpc_wrapper_prefix(buf: &[u8]) -> (&[u8], bool) { + // Returns (data_after_wrapper, stripped_anything) + const MAGIC: u32 = 0x29b00b92; + + if buf.len() < 24 { + return (buf, false); + } + + let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); + if magic != MAGIC { + return (buf, false); + } + + // flags at [4..8] – not needed to compute size + let body_len = u64::from_le_bytes([ + buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], + ]) as usize; + + let wrapper_len = 24 + body_len; + if buf.len() < wrapper_len { + // Incomplete wrapper (shouldn’t happen with your read API), keep as-is. + return (buf, false); + } + + (&buf[wrapper_len..], true) + } + self.open_file_stream_for_response(stream_id).await?; + + let mut got = 0usize; + while got < total_size { + let bytes = self.h2_client.read(stream_id).await?; + let (after, stripped) = strip_xpc_wrapper_prefix(&bytes); + if stripped && after.is_empty() { + continue; // pure control wrapper, don't count + } + + let data = if stripped { after.to_vec() } else { bytes }; + + if data.is_empty() { + continue; + } + + got += data.len(); + yield data; + } + } + } + + pub async fn open_file_stream_for_response( + &mut self, + stream_id: u32, + ) -> Result<(), IdeviceError> { + // 1) Open the HTTP/2 stream + self.h2_client.open_stream(stream_id).await?; + + // 2) Send an empty XPC wrapper on that same stream with FILE_TX_STREAM_RESPONSE + let flags = XPCFlag::AlwaysSet | XPCFlag::FileTxStreamResponse; + + let msg = XPCMessage::new(Some(flags), None, Some(0)); + + // IMPORTANT: send on `stream_id`, not ROOT/REPLY + let bytes = msg.encode(0)?; + self.h2_client.send(bytes, stream_id).await?; + Ok(()) + } } diff --git a/justfile b/justfile index c80cac0..9b3788c 100644 --- a/justfile +++ b/justfile @@ -3,6 +3,35 @@ check-features: cargo hack check --feature-powerset --no-dev-deps cd .. +ci-check: build-ffi-native build-tools-native build-cpp build-c + cargo clippy --all-targets --all-features -- -D warnings + cargo fmt -- --check +macos-ci-check: ci-check xcframework + cd tools && cargo build --release --target x86_64-apple-darwin +windows-ci-check: build-ffi-native build-tools-native build-cpp + +[working-directory: 'ffi'] +build-ffi-native: + cargo build --release + +[working-directory: 'tools'] +build-tools-native: + cargo build --release + +create-example-build-folder: + mkdir -p cpp/examples/build + mkdir -p ffi/examples/build + +[working-directory: 'cpp/examples/build'] +build-cpp: build-ffi-native create-example-build-folder + cmake -S .. -B . -DCMAKE_BUILD_TYPE=Release + cmake --build . --config Release --parallel + +[working-directory: 'ffi/examples/build'] +build-c: build-ffi-native create-example-build-folder + cmake -S .. -B . -DCMAKE_BUILD_TYPE=Release + cmake --build . --config Release --parallel + xcframework: apple-build rm -rf swift/IDevice.xcframework rm -rf swift/libs @@ -21,8 +50,8 @@ xcframework: apple-build -library swift/libs/idevice-macos.a -headers swift/include \ -output swift/IDevice.xcframework - zip -r bundle.zip IDevice.xcframework - openssl dgst -sha256 bundle.zip + zip -r swift/bundle.zip swift/IDevice.xcframework + openssl dgst -sha256 swift/bundle.zip [working-directory: 'ffi'] apple-build: # requires a Mac @@ -42,150 +71,3 @@ apple-build: # requires a Mac cargo build --release --target aarch64-apple-darwin cargo build --release --target x86_64-apple-darwin -lib_name := "plist" -src_dir := "ffi/libplist" - -ios_out := "build/ios" -sim_out := "build/sim" -x86_64_sim_out := "build/x86_64_sim" -mac_out := "build/mac" -x86_64_mac_out := "build/x86_64_mac" - -plist_xcframework: plist_clean build_plist_ios build_plist_sim build_plist_x86_64_sim build_plist_mac build_plist_x86_64_mac plist_merge_archs - rm -rf {{lib_name}}.xcframework - xcodebuild -create-xcframework \ - -framework {{ios_out}}/plist.framework \ - -framework build/universal-sim/plist.framework \ - -framework build/universal-mac/plist.framework \ - -output swift/{{lib_name}}.xcframework - -plist_clean: - rm -rf build - rm -rf swift/plist.xcframework - -plist_merge_archs: - # Merge simulator dylibs (arm64 + x86_64) - mkdir -p build/universal-sim - lipo -create \ - {{sim_out}}/lib/libplist-2.0.4.dylib \ - {{x86_64_sim_out}}/lib/libplist-2.0.4.dylib \ - -output build/universal-sim/libplist-2.0.4.dylib - - mkdir -p build/universal-sim/plist.framework/Headers - mkdir -p build/universal-sim/plist.framework/Modules - cp build/universal-sim/libplist-2.0.4.dylib build/universal-sim/plist.framework/plist - cp {{sim_out}}/include/plist/*.h build/universal-sim/plist.framework/Headers - cp swift/Info.plist build/universal-sim/plist.framework/Info.plist - cp swift/plistinclude/module.modulemap build/universal-sim/plist.framework/Modules/module.modulemap - - # Merge macOS dylibs (arm64 + x86_64) - mkdir -p build/universal-mac - lipo -create \ - {{mac_out}}/lib/libplist-2.0.4.dylib \ - {{x86_64_mac_out}}/lib/libplist-2.0.4.dylib \ - -output build/universal-mac/libplist-2.0.4.dylib - - mkdir -p build/universal-mac/plist.framework/Headers - mkdir -p build/universal-mac/plist.framework/Modules - cp build/universal-mac/libplist-2.0.4.dylib build/universal-mac/plist.framework/plist - cp {{mac_out}}/include/plist/*.h build/universal-mac/plist.framework/Headers - cp swift/Info.plist build/universal-mac/plist.framework/Info.plist - cp swift/plistinclude/module.modulemap build/universal-mac/plist.framework/Modules/module.modulemap - -build_plist_ios: - rm -rf {{ios_out}} build/build-ios - rm -rf build/ios - mkdir -p {{ios_out}} - mkdir -p build/build-ios && cd build/build-ios && \ - ../../ffi/libplist/autogen.sh \ - --host=arm-apple-darwin \ - --prefix="$(pwd)/../../{{ios_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk iphoneos --find clang)" \ - CFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -mios-version-min=12.0" \ - CXX="$(xcrun --sdk iphoneos --find clang++)" \ - CXXFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -mios-version-min=12.0" \ - LDFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -mios-version-min=12.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{ios_out}}/lib/libplist-2.0.4.dylib - - mkdir -p {{ios_out}}/plist.framework/Headers - mkdir -p {{ios_out}}/plist.framework/Modules - cp {{ios_out}}/lib/libplist-2.0.4.dylib {{ios_out}}/plist.framework/plist - cp {{ios_out}}/include/plist/*.h {{ios_out}}/plist.framework/Headers - cp swift/Info.plist {{ios_out}}/plist.framework/Info.plist - cp swift/plistinclude/module.modulemap {{ios_out}}/plist.framework/Modules/module.modulemap - -build_plist_sim: - rm -rf {{sim_out}} build/build-sim - mkdir -p {{sim_out}} - mkdir -p build/build-sim && cd build/build-sim && \ - ../../ffi/libplist/autogen.sh \ - --host=arm-apple-darwin \ - --prefix="$(pwd)/../../{{sim_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk iphonesimulator --find clang)" \ - CFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - CXX="$(xcrun --sdk iphonesimulator --find clang++)" \ - CXXFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - LDFLAGS="-arch arm64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{sim_out}}/lib/libplist-2.0.4.dylib - -build_plist_x86_64_sim: - rm -rf {{x86_64_sim_out}} build/build-sim - mkdir -p {{x86_64_sim_out}} - mkdir -p build/build-sim && cd build/build-sim && \ - ../../ffi/libplist/autogen.sh \ - --host=x86_64-apple-darwin \ - --prefix="$(pwd)/../../{{x86_64_sim_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk iphonesimulator --find clang)" \ - CFLAGS="-arch x86_64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - CXX="$(xcrun --sdk iphonesimulator --find clang++)" \ - CXXFLAGS="-arch x86_64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" \ - LDFLAGS="-arch x86_64 -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path) -mios-simulator-version-min=12.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{x86_64_sim_out}}/lib/libplist-2.0.4.dylib - -build_plist_mac: - rm -rf {{mac_out}} build/build-mac - mkdir -p {{mac_out}} - mkdir -p build/build-mac && cd build/build-mac && \ - ../../ffi/libplist/autogen.sh \ - --host=aarch64-apple-darwin \ - --prefix="$(pwd)/../../{{mac_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk macosx --find clang)" \ - CFLAGS="-arch arm64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - CXX="$(xcrun --sdk macosx --find clang++)" \ - CXXFLAGS="-arch arm64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - LDFLAGS="-arch arm64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{mac_out}}/lib/libplist-2.0.4.dylib - -build_plist_x86_64_mac: - rm -rf {{x86_64_mac_out}} build/build-mac - mkdir -p {{x86_64_mac_out}} - mkdir -p build/build-mac && cd build/build-mac && \ - ../../ffi/libplist/autogen.sh \ - --host=x86_64-apple-darwin \ - --prefix="$(pwd)/../../{{x86_64_mac_out}}" \ - --without-cython \ - --without-tools \ - CC="$(xcrun --sdk macosx --find clang)" \ - CFLAGS="-arch x86_64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - CXX="$(xcrun --sdk macosx --find clang++)" \ - CXXFLAGS="-arch x86_64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" \ - LDFLAGS="-arch x86_64 -isysroot $(xcrun --sdk macosx --show-sdk-path) -mmacosx-version-min=11.0" && \ - make clean && make -j$(sysctl -n hw.ncpu) && make install - - install_name_tool -id @rpath/plist.framework/plist {{x86_64_mac_out}}/lib/libplist-2.0.4.dylib diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 6636461..6ac6f0e 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -3,7 +3,7 @@ name = "idevice-tools" description = "Rust binary tools to interact with services on iOS devices." authors = ["Jackson Coxson"] version = "0.1.0" -edition = "2021" +edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" @@ -21,13 +21,17 @@ path = "src/heartbeat_client.rs" name = "instproxy" path = "src/instproxy.rs" +[[bin]] +name = "ideviceinstaller" +path = "src/ideviceinstaller.rs" + [[bin]] name = "mounter" path = "src/mounter.rs" -[[bin]] -name = "core_device_proxy_tun" -path = "src/core_device_proxy_tun.rs" +# [[bin]] +# name = "core_device_proxy_tun" +# path = "src/core_device_proxy_tun.rs" [[bin]] name = "idevice_id" @@ -97,15 +101,49 @@ path = "src/restore_service.rs" name = "remote_pairing" path = "src/remote_pairing.rs" +[[bin]] +name = "companion_proxy" +path = "src/companion_proxy.rs" + +[[bin]] +name = "diagnostics" +path = "src/diagnostics.rs" + +[[bin]] +name = "mobilebackup2" +path = "src/mobilebackup2.rs" + +[[bin]] +name = "diagnosticsservice" +path = "src/diagnosticsservice.rs" + +[[bin]] +name = "bt_packet_logger" +path = "src/bt_packet_logger.rs" + +[[bin]] +name = "pcapd" +path = "src/pcapd.rs" + +[[bin]] +name = "preboard" +path = "src/preboard.rs" + [dependencies] -idevice = { path = "../idevice", features = ["full"] } -tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } +idevice = { path = "../idevice", features = ["full"], default-features = false } +tokio = { version = "1.43", features = ["full"] } log = { version = "0.4" } env_logger = { version = "0.11" } -tun-rs = { version = "1.5", features = ["async"] } +# tun-rs = { version = "1.5", features = ["async"] } sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } plist = { version = "1.7" } ns-keyed-archive = "0.1.2" uuid = "1.16" +futures-util = { version = "0.3" } + +[features] +default = ["aws-lc"] +aws-lc = ["idevice/aws-lc"] +ring = ["idevice/ring"] diff --git a/tools/src/afc.rs b/tools/src/afc.rs index d70c926..e2dcefe 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -2,11 +2,11 @@ use std::path::PathBuf; -use clap::{value_parser, Arg, Command}; +use clap::{Arg, Command, value_parser}; use idevice::{ - afc::{opcode::AfcFopenMode, AfcClient}, - house_arrest::HouseArrestClient, IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, + house_arrest::HouseArrestClient, }; mod common; diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs index ff0b4fa..48d1e37 100644 --- a/tools/src/amfi.rs +++ b/tools/src/amfi.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{amfi::AmfiClient, IdeviceService}; +use idevice::{IdeviceService, amfi::AmfiClient}; mod common; diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 34b24f3..334cb67 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -1,12 +1,11 @@ // Jackson Coxson -use std::io::Write; - use clap::{Arg, Command}; use idevice::{ - core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, - debug_proxy::DebugProxyClient, rsd::RsdHandshake, tcp::stream::AdapterStream, IdeviceService, - RsdService, + IdeviceService, RsdService, + core_device::{AppServiceClient, OpenStdioSocketClient}, + core_device_proxy::CoreDeviceProxy, + rsd::RsdHandshake, }; mod common; @@ -100,7 +99,7 @@ async fn main() { let host = matches.get_one::("host"); let provider = - match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await { + match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await { Ok(p) => p, Err(e) => { eprintln!("{e}"); @@ -112,15 +111,10 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - adapter - .pcap("/Users/jacksoncoxson/Desktop/rs_xpc.pcap") - .await - .unwrap(); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); @@ -144,12 +138,42 @@ async fn main() { } }; + let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no stdio"); + + let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid"); + println!("stdio uuid: {stdio_uuid:?}"); + let res = asc - .launch_application(bundle_id, &[], false, false, None, None) + .launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid)) .await .expect("no launch"); - println!("{res:#?}"); + println!("Launch response {res:#?}"); + + let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner); + let mut local_stdin = tokio::io::stdin(); + let mut local_stdout = tokio::io::stdout(); + + tokio::select! { + // Task 1: Copy data from the remote process to local stdout + res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => { + if let Err(e) = res { + eprintln!("Error copying from remote to local: {}", e); + } + println!("\nRemote connection closed."); + return; + } + // Task 2: Copy data from local stdin to the remote process + res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => { + if let Err(e) = res { + eprintln!("Error copying from local to remote: {}", e); + } + println!("\nLocal stdin closed."); + return; + } + } } else if matches.subcommand_matches("processes").is_some() { let p = asc.list_processes().await.expect("no processes?"); println!("{p:#?}"); diff --git a/tools/src/bt_packet_logger.rs b/tools/src/bt_packet_logger.rs new file mode 100644 index 0000000..eef5d2d --- /dev/null +++ b/tools/src/bt_packet_logger.rs @@ -0,0 +1,104 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use futures_util::StreamExt; +use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient}; +use tokio::io::AsyncWrite; + +use crate::pcap::{write_pcap_header, write_pcap_record}; + +mod common; +mod pcap; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("amfi") + .about("Capture Bluetooth packets") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("out") + .long("out") + .value_name("PCAP") + .help("Write PCAP to this file (use '-' for stdout)"), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("bt_packet_logger - capture bluetooth packets"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + let out = matches.get_one::("out").map(String::to_owned); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let logger_client = BtPacketLoggerClient::connect(&*provider) + .await + .expect("Failed to connect to amfi"); + + let mut s = logger_client.into_stream(); + + // Open output (default to stdout if --out omitted) + let mut out_writer: Box = match out.as_deref() { + Some("-") | None => Box::new(tokio::io::stdout()), + Some(path) => Box::new(tokio::fs::File::create(path).await.expect("open pcap")), + }; + + // Write global header + write_pcap_header(&mut out_writer) + .await + .expect("pcap header"); + + // Drain stream to PCAP + while let Some(res) = s.next().await { + match res { + Ok(frame) => { + write_pcap_record( + &mut out_writer, + frame.hdr.ts_secs, + frame.hdr.ts_usecs, + frame.kind, + &frame.h4, + ) + .await + .unwrap_or_else(|e| eprintln!("pcap write error: {e}")); + } + Err(e) => eprintln!("Failed to get next packet: {e:?}"), + } + } +} diff --git a/tools/src/common.rs b/tools/src/common.rs index ed9ecfd..34be9e5 100644 --- a/tools/src/common.rs +++ b/tools/src/common.rs @@ -18,9 +18,7 @@ pub async fn get_provider( pairing_file: Option<&String>, label: &str, ) -> Result, String> { - let provider: Box = if udid.is_some() { - let udid = udid.unwrap(); - + let provider: Box = if let Some(udid) = udid { let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") { let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS"); let socket = tokio::net::TcpStream::connect(socket) @@ -40,14 +38,16 @@ pub async fn get_provider( } }; Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) - } else if host.is_some() && pairing_file.is_some() { - let host = match IpAddr::from_str(host.unwrap()) { + } else if let Some(host) = host + && let Some(pairing_file) = pairing_file + { + let host = match IpAddr::from_str(host) { Ok(h) => h, Err(e) => { return Err(format!("Invalid host: {e:?}")); } }; - let pairing_file = match PairingFile::read_from_file(pairing_file.unwrap()) { + let pairing_file = match PairingFile::read_from_file(pairing_file) { Ok(p) => p, Err(e) => { return Err(format!("Unable to read pairing file: {e:?}")); diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs new file mode 100644 index 0000000..2568df5 --- /dev/null +++ b/tools/src/companion_proxy.rs @@ -0,0 +1,151 @@ +// Jackson Coxson + +use clap::{Arg, Command, arg}; +use idevice::{ + IdeviceService, RsdService, companion_proxy::CompanionProxy, + core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist, + rsd::RsdHandshake, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("companion_proxy") + .about("Apple Watch things") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("list").about("List the companions on the device")) + .subcommand(Command::new("listen").about("Listen for devices")) + .subcommand( + Command::new("get") + .about("Gets a value") + .arg(arg!(-d --device_udid "the device udid to get from").required(true)) + .arg(arg!(-v --value "the value to get").required(true)), + ) + .subcommand( + Command::new("start") + .about("Starts a service") + .arg(arg!(-p --port "the port").required(true)) + .arg(arg!(-n --name "the optional service name").required(false)), + ) + .subcommand( + Command::new("stop") + .about("Starts a service") + .arg(arg!(-p --port "the port").required(true)), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("companion_proxy"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core_device_proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + let mut provider = proxy + .create_software_tunnel() + .expect("no tunnel") + .to_async_handle(); + let mut handshake = RsdHandshake::new(provider.connect(rsd_port).await.unwrap()) + .await + .unwrap(); + let mut proxy = CompanionProxy::connect_rsd(&mut provider, &mut handshake) + .await + .expect("no companion proxy connect"); + + // let mut proxy = CompanionProxy::connect(&*provider) + // .await + // .expect("Failed to connect to companion proxy"); + + if matches.subcommand_matches("list").is_some() { + proxy.get_device_registry().await.expect("Failed to show"); + } else if matches.subcommand_matches("listen").is_some() { + let mut stream = proxy.listen_for_devices().await.expect("Failed to show"); + while let Ok(v) = stream.next().await { + println!("{}", pretty_print_dictionary(&v)); + } + } else if let Some(matches) = matches.subcommand_matches("get") { + let key = matches.get_one::("value").expect("no value passed"); + let udid = matches + .get_one::("device_udid") + .expect("no AW udid passed"); + + match proxy.get_value(udid, key).await { + Ok(value) => { + println!("{}", pretty_print_plist(&value)); + } + Err(e) => { + eprintln!("Error getting value: {e}"); + } + } + } else if let Some(matches) = matches.subcommand_matches("start") { + let port: u16 = matches + .get_one::("port") + .expect("no port passed") + .parse() + .expect("not a number"); + let name = matches.get_one::("name").map(|x| x.as_str()); + + match proxy.start_forwarding_service_port(port, name, None).await { + Ok(value) => { + println!("started on port {value}"); + } + Err(e) => { + eprintln!("Error starting: {e}"); + } + } + } else if let Some(matches) = matches.subcommand_matches("stop") { + let port: u16 = matches + .get_one::("port") + .expect("no port passed") + .parse() + .expect("not a number"); + + if let Err(e) = proxy.stop_forwarding_service_port(port).await { + eprintln!("Error starting: {e}"); + } + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +} diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs index cda1535..9cbfc24 100644 --- a/tools/src/crash_logs.rs +++ b/tools/src/crash_logs.rs @@ -2,8 +2,8 @@ use clap::{Arg, Command}; use idevice::{ - crashreportcopymobile::{flush_reports, CrashReportCopyMobileClient}, IdeviceService, + crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, }; mod common; @@ -37,7 +37,11 @@ async fn main() { .help("Show about information") .action(clap::ArgAction::SetTrue), ) - .subcommand(Command::new("list").about("Lists the items in the directory")) + .subcommand( + Command::new("list") + .about("Lists the items in the directory") + .arg(Arg::new("dir").required(false).index(1)), + ) .subcommand(Command::new("flush").about("Flushes reports to the directory")) .subcommand( Command::new("pull") diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs index ba3be4d..b7e5625 100644 --- a/tools/src/debug_proxy.rs +++ b/tools/src/debug_proxy.rs @@ -4,8 +4,8 @@ use std::io::Write; use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, rsd::RsdHandshake, - tcp::stream::AdapterStream, IdeviceService, RsdService, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, + rsd::RsdHandshake, }; mod common; @@ -71,10 +71,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs new file mode 100644 index 0000000..3ea5fe1 --- /dev/null +++ b/tools/src/diagnostics.rs @@ -0,0 +1,279 @@ +// Jackson Coxson +// idevice Rust implementation of libimobiledevice's idevicediagnostics + +use clap::{Arg, ArgMatches, Command}; +use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("idevicediagnostics") + .about("Interact with the diagnostics interface of a device") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand( + Command::new("ioregistry") + .about("Print IORegistry information") + .arg( + Arg::new("plane") + .long("plane") + .value_name("PLANE") + .help("IORegistry plane to query (e.g., IODeviceTree, IOService)"), + ) + .arg( + Arg::new("name") + .long("name") + .value_name("NAME") + .help("Entry name to filter by"), + ) + .arg( + Arg::new("class") + .long("class") + .value_name("CLASS") + .help("Entry class to filter by"), + ), + ) + .subcommand( + Command::new("mobilegestalt") + .about("Print MobileGestalt information") + .arg( + Arg::new("keys") + .long("keys") + .value_name("KEYS") + .help("Comma-separated list of keys to query") + .value_delimiter(',') + .num_args(1..), + ), + ) + .subcommand(Command::new("gasguage").about("Print gas gauge (battery) information")) + .subcommand(Command::new("nand").about("Print NAND flash information")) + .subcommand(Command::new("all").about("Print all available diagnostics information")) + .subcommand(Command::new("wifi").about("Print WiFi diagnostics information")) + .subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay")) + .subcommand(Command::new("restart").about("Restart the device")) + .subcommand(Command::new("shutdown").about("Shutdown the device")) + .subcommand(Command::new("sleep").about("Put the device to sleep")) + .get_matches(); + + if matches.get_flag("about") { + println!( + "idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary." + ); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = + match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!("Unable to connect to diagnostics relay: {e:?}"); + return; + } + }; + + match matches.subcommand() { + Some(("ioregistry", sub_matches)) => { + handle_ioregistry(&mut diagnostics_client, sub_matches).await; + } + Some(("mobilegestalt", sub_matches)) => { + handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; + } + Some(("gasguage", _)) => { + handle_gasguage(&mut diagnostics_client).await; + } + Some(("nand", _)) => { + handle_nand(&mut diagnostics_client).await; + } + Some(("all", _)) => { + handle_all(&mut diagnostics_client).await; + } + Some(("wifi", _)) => { + handle_wifi(&mut diagnostics_client).await; + } + Some(("restart", _)) => { + handle_restart(&mut diagnostics_client).await; + } + Some(("shutdown", _)) => { + handle_shutdown(&mut diagnostics_client).await; + } + Some(("sleep", _)) => { + handle_sleep(&mut diagnostics_client).await; + } + Some(("goodbye", _)) => { + handle_goodbye(&mut diagnostics_client).await; + } + _ => { + eprintln!("No subcommand specified. Use --help for usage information."); + } + } +} + +async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { + let plane = matches.get_one::("plane").map(|s| s.as_str()); + let name = matches.get_one::("name").map(|s| s.as_str()); + let class = matches.get_one::("class").map(|s| s.as_str()); + + match client.ioregistry(plane, name, class).await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No IORegistry data returned"); + } + Err(e) => { + eprintln!("Failed to get IORegistry data: {e:?}"); + } + } +} + +async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { + let keys = matches + .get_many::("keys") + .map(|values| values.map(|s| s.to_string()).collect::>()); + + match client.mobilegestalt(keys).await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No MobileGestalt data returned"); + } + Err(e) => { + eprintln!("Failed to get MobileGestalt data: {e:?}"); + } + } +} + +async fn handle_gasguage(client: &mut DiagnosticsRelayClient) { + match client.gasguage().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No gas gauge data returned"); + } + Err(e) => { + eprintln!("Failed to get gas gauge data: {e:?}"); + } + } +} + +async fn handle_nand(client: &mut DiagnosticsRelayClient) { + match client.nand().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No NAND data returned"); + } + Err(e) => { + eprintln!("Failed to get NAND data: {e:?}"); + } + } +} + +async fn handle_all(client: &mut DiagnosticsRelayClient) { + match client.all().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No diagnostics data returned"); + } + Err(e) => { + eprintln!("Failed to get all diagnostics data: {e:?}"); + } + } +} + +async fn handle_wifi(client: &mut DiagnosticsRelayClient) { + match client.wifi().await { + Ok(Some(data)) => { + println!("{data:#?}"); + } + Ok(None) => { + println!("No WiFi diagnostics returned"); + } + Err(e) => { + eprintln!("Failed to get WiFi diagnostics: {e:?}"); + } + } +} + +async fn handle_restart(client: &mut DiagnosticsRelayClient) { + match client.restart().await { + Ok(()) => { + println!("Device restart command sent successfully"); + } + Err(e) => { + eprintln!("Failed to restart device: {e:?}"); + } + } +} + +async fn handle_shutdown(client: &mut DiagnosticsRelayClient) { + match client.shutdown().await { + Ok(()) => { + println!("Device shutdown command sent successfully"); + } + Err(e) => { + eprintln!("Failed to shutdown device: {e:?}"); + } + } +} + +async fn handle_sleep(client: &mut DiagnosticsRelayClient) { + match client.sleep().await { + Ok(()) => { + println!("Device sleep command sent successfully"); + } + Err(e) => { + eprintln!("Failed to put device to sleep: {e:?}"); + } + } +} + +async fn handle_goodbye(client: &mut DiagnosticsRelayClient) { + match client.goodbye().await { + Ok(()) => println!("Goodbye acknowledged by device"), + Err(e) => eprintln!("Goodbye failed: {e:?}"), + } +} diff --git a/tools/src/diagnosticsservice.rs b/tools/src/diagnosticsservice.rs new file mode 100644 index 0000000..e043381 --- /dev/null +++ b/tools/src/diagnosticsservice.rs @@ -0,0 +1,106 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use futures_util::StreamExt; +use idevice::{ + IdeviceService, RsdService, core_device::DiagnostisServiceClient, + core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, +}; +use tokio::io::AsyncWriteExt; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("remotexpc") + .about("Gets a sysdiagnose") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("tunneld") + .long("tunneld") + .help("Use tunneld") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("debug_proxy - connect to the debug proxy and run commands"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let pairing_file = matches.get_one::("pairing_file"); + let host = matches.get_one::("host"); + + let provider = + match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + + let mut dsc = DiagnostisServiceClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no connect"); + + println!("Getting sysdiagnose, this takes a while! iOS is slow..."); + let mut res = dsc + .capture_sysdiagnose(false) + .await + .expect("no sysdiagnose"); + println!("Got sysdaignose! Saving to file"); + + let mut written = 0usize; + let mut out = tokio::fs::File::create(&res.preferred_filename) + .await + .expect("no file?"); + while let Some(chunk) = res.stream.next().await { + let buf = chunk.expect("stream stopped?"); + if !buf.is_empty() { + out.write_all(&buf).await.expect("no write all?"); + written += buf.len(); + } + println!("wrote {written}/{} bytes", res.expected_length); + } + println!("Done! Saved to {}", res.preferred_filename); +} diff --git a/tools/src/heartbeat_client.rs b/tools/src/heartbeat_client.rs index bfb8d80..2ed1979 100644 --- a/tools/src/heartbeat_client.rs +++ b/tools/src/heartbeat_client.rs @@ -2,7 +2,7 @@ // Heartbeat client use clap::{Arg, Command}; -use idevice::{heartbeat::HeartbeatClient, IdeviceService}; +use idevice::{IdeviceService, heartbeat::HeartbeatClient}; mod common; diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index 948a8c1..60bdf34 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -2,7 +2,7 @@ // idevice Rust implementation of libimobiledevice's ideviceinfo use clap::{Arg, Command}; -use idevice::{lockdown::LockdownClient, IdeviceService}; +use idevice::{IdeviceService, lockdown::LockdownClient}; mod common; @@ -39,7 +39,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary."); + println!( + "ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } @@ -67,7 +69,9 @@ async fn main() { println!( "{:?}", - lockdown_client.get_value("ProductVersion", None).await + lockdown_client + .get_value(Some("ProductVersion"), None) + .await ); println!( @@ -82,5 +86,5 @@ async fn main() { .await ); println!("{:?}", lockdown_client.idevice.get_type().await.unwrap()); - println!("{:#?}", lockdown_client.get_all_values(None).await); + println!("{:#?}", lockdown_client.get_value(None, None).await); } diff --git a/tools/src/ideviceinstaller.rs b/tools/src/ideviceinstaller.rs new file mode 100644 index 0000000..346af63 --- /dev/null +++ b/tools/src/ideviceinstaller.rs @@ -0,0 +1,103 @@ +// A minimal ideviceinstaller-like CLI to install/upgrade apps + +use clap::{Arg, ArgAction, Command}; +use idevice::utils::installation; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("ideviceinstaller") + .about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(ArgAction::SetTrue), + ) + .subcommand( + Command::new("install") + .about("Install a local .ipa or directory") + .arg(Arg::new("path").required(true).value_name("PATH")), + ) + .subcommand( + Command::new("upgrade") + .about("Upgrade from a local .ipa or directory") + .arg(Arg::new("path").required(true).value_name("PATH")), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)"); + println!("Copyright (c) 2025"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + if let Some(matches) = matches.subcommand_matches("install") { + let path: &String = matches.get_one("path").expect("required"); + match installation::install_package_with_callback( + &*provider, + path, + None, + |(percentage, _)| async move { + println!("Installing: {percentage}%"); + }, + (), + ) + .await + { + Ok(()) => println!("install success"), + Err(e) => eprintln!("Install failed: {e}"), + } + } else if let Some(matches) = matches.subcommand_matches("upgrade") { + let path: &String = matches.get_one("path").expect("required"); + match installation::upgrade_package_with_callback( + &*provider, + path, + None, + |(percentage, _)| async move { + println!("Upgrading: {percentage}%"); + }, + (), + ) + .await + { + Ok(()) => println!("upgrade success"), + Err(e) => eprintln!("Upgrade failed: {e}"), + } + } else { + eprintln!("Invalid usage, pass -h for help"); + } +} diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index e5f1980..960a80c 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -2,7 +2,7 @@ // Just lists apps for now use clap::{Arg, Command}; -use idevice::{installation_proxy::InstallationProxyClient, IdeviceService}; +use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; mod common; @@ -47,7 +47,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary."); + println!( + "instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } @@ -69,10 +71,7 @@ async fn main() { .await .expect("Unable to connect to instproxy"); if matches.subcommand_matches("lookup").is_some() { - let apps = instproxy_client - .get_apps(Some("User".to_string()), None) - .await - .unwrap(); + let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); for app in apps.keys() { println!("{app}"); } diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index 0ee50f5..c017fbb 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -2,10 +2,7 @@ // Just lists apps for now use clap::{Arg, Command}; -use idevice::{ - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, RsdService, -}; +use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; mod common; @@ -71,10 +68,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); @@ -118,7 +114,11 @@ async fn main() { println!("Location set!"); println!("Press ctrl-c to stop"); loop { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ls_client + .set(latitude, longitude) + .await + .expect("Failed to set location"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; } } else { eprintln!("Invalid usage, pass -h for help"); diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 00f3def..ec164c5 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,7 +1,7 @@ // Jackson Coxson -use clap::{arg, Arg, Command}; -use idevice::{lockdown::LockdownClient, pretty_print_plist, IdeviceService}; +use clap::{Arg, Command, arg}; +use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist}; use plist::Value; mod common; @@ -39,12 +39,7 @@ async fn main() { .subcommand( Command::new("get") .about("Gets a value") - .arg(arg!(-v --value "the value to get").required(true)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .subcommand( - Command::new("get_all") - .about("Gets all") + .arg(arg!(-v --value "the value to get").required(false)) .arg(arg!(-d --domain "the domain to get in").required(false)), ) .subcommand( @@ -57,7 +52,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary."); + println!( + "lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } @@ -86,8 +83,8 @@ async fn main() { match matches.subcommand() { Some(("get", sub_m)) => { - let key = sub_m.get_one::("value").unwrap(); - let domain = sub_m.get_one::("domain").cloned(); + let key = sub_m.get_one::("value").map(|x| x.as_str()); + let domain = sub_m.get_one::("domain").map(|x| x.as_str()); match lockdown_client.get_value(key, domain).await { Ok(value) => { @@ -98,27 +95,18 @@ async fn main() { } } } - Some(("get_all", sub_m)) => { - let domain = sub_m.get_one::("domain").cloned(); - - match lockdown_client.get_all_values(domain).await { - Ok(value) => { - println!("{}", pretty_print_plist(&plist::Value::Dictionary(value))); - } - Err(e) => { - eprintln!("Error getting value: {e}"); - } - } - } Some(("set", sub_m)) => { let key = sub_m.get_one::("key").unwrap(); let value_str = sub_m.get_one::("value").unwrap(); - let domain = sub_m.get_one::("domain").cloned(); + let domain = sub_m.get_one::("domain"); let value = Value::String(value_str.clone()); - match lockdown_client.set_value(key, value, domain).await { + match lockdown_client + .set_value(key, value, domain.map(|x| x.as_str())) + .await + { Ok(()) => println!("Successfully set"), Err(e) => eprintln!("Error setting value: {e}"), } diff --git a/tools/src/misagent.rs b/tools/src/misagent.rs index 04057ac..966b86f 100644 --- a/tools/src/misagent.rs +++ b/tools/src/misagent.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; -use clap::{arg, value_parser, Arg, Command}; -use idevice::{misagent::MisagentClient, IdeviceService}; +use clap::{Arg, Command, arg, value_parser}; +use idevice::{IdeviceService, misagent::MisagentClient}; mod common; @@ -52,7 +52,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!( + "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs new file mode 100644 index 0000000..e852d8f --- /dev/null +++ b/tools/src/mobilebackup2.rs @@ -0,0 +1,668 @@ +// Jackson Coxson +// Mobile Backup 2 tool for iOS devices + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, + mobilebackup2::{MobileBackup2Client, RestoreOptions}, +}; +use plist::Dictionary; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("mobilebackup2") + .about("Mobile Backup 2 tool for iOS devices") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand( + Command::new("info") + .about("Get backup information from a local backup directory") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg( + Arg::new("source") + .long("source") + .value_name("SOURCE") + .help("Source identifier (defaults to current UDID)"), + ), + ) + .subcommand( + Command::new("list") + .about("List files of the last backup from a local backup directory") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE")), + ) + .subcommand( + Command::new("backup") + .about("Start a backup operation") + .arg( + Arg::new("dir") + .long("dir") + .value_name("DIR") + .help("Backup directory on host") + .required(true), + ) + .arg( + Arg::new("target") + .long("target") + .value_name("TARGET") + .help("Target identifier for the backup"), + ) + .arg( + Arg::new("source") + .long("source") + .value_name("SOURCE") + .help("Source identifier for the backup"), + ), + ) + .subcommand( + Command::new("restore") + .about("Restore from a local backup directory (DeviceLink)") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg( + Arg::new("source") + .long("source") + .value_name("SOURCE") + .help("Source UDID; defaults to current device UDID"), + ) + .arg( + Arg::new("password") + .long("password") + .value_name("PWD") + .help("Backup password if encrypted"), + ) + .arg( + Arg::new("no-reboot") + .long("no-reboot") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("no-copy") + .long("no-copy") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("no-settings") + .long("no-settings") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("system") + .long("system") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("remove") + .long("remove") + .action(clap::ArgAction::SetTrue), + ), + ) + .subcommand( + Command::new("unback") + .about("Unpack a complete backup to device hierarchy") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE")) + .arg(Arg::new("password").long("password").value_name("PWD")), + ) + .subcommand( + Command::new("extract") + .about("Extract a file from a previous backup") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("source").long("source").value_name("SOURCE")) + .arg( + Arg::new("domain") + .long("domain") + .value_name("DOMAIN") + .required(true), + ) + .arg( + Arg::new("path") + .long("path") + .value_name("REL_PATH") + .required(true), + ) + .arg(Arg::new("password").long("password").value_name("PWD")), + ) + .subcommand( + Command::new("change-password") + .about("Change backup password") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)) + .arg(Arg::new("old").long("old").value_name("OLD")) + .arg(Arg::new("new").long("new").value_name("NEW")), + ) + .subcommand( + Command::new("erase-device") + .about("Erase the device via mobilebackup2") + .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), + ) + .subcommand(Command::new("freespace").about("Get free space information")) + .subcommand(Command::new("encryption").about("Check backup encryption status")) + .get_matches(); + + if matches.get_flag("about") { + println!("mobilebackup2 - manage device backups using Mobile Backup 2 service"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = + match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("Error creating provider: {e}"); + return; + } + }; + + let mut backup_client = match MobileBackup2Client::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!("Unable to connect to mobilebackup2 service: {e}"); + return; + } + }; + + match matches.subcommand() { + Some(("info", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + match backup_client.info_from_path(Path::new(dir), source).await { + Ok(dict) => { + println!("Backup Information:"); + for (k, v) in dict { + println!(" {k}: {v:?}"); + } + } + Err(e) => eprintln!("Failed to get info: {e}"), + } + } + Some(("list", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + match backup_client.list_from_path(Path::new(dir), source).await { + Ok(dict) => { + println!("List Response:"); + for (k, v) in dict { + println!(" {k}: {v:?}"); + } + } + Err(e) => eprintln!("Failed to list: {e}"), + } + } + Some(("backup", sub_matches)) => { + let target = sub_matches.get_one::("target").map(|s| s.as_str()); + let source = sub_matches.get_one::("source").map(|s| s.as_str()); + let dir = sub_matches + .get_one::("dir") + .expect("dir is required"); + + println!("Starting backup operation..."); + let res = backup_client + .send_request("Backup", target, source, None::) + .await; + if let Err(e) = res { + eprintln!("Failed to send backup request: {e}"); + } else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await { + eprintln!("Backup failed during DL loop: {e}"); + } else { + println!("Backup flow finished"); + } + } + Some(("restore", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let mut ropts = RestoreOptions::new(); + if sub.get_flag("no-reboot") { + ropts = ropts.with_reboot(false); + } + if sub.get_flag("no-copy") { + ropts = ropts.with_copy(false); + } + if sub.get_flag("no-settings") { + ropts = ropts.with_preserve_settings(false); + } + if sub.get_flag("system") { + ropts = ropts.with_system_files(true); + } + if sub.get_flag("remove") { + ropts = ropts.with_remove_items_not_restored(true); + } + if let Some(pw) = sub.get_one::("password") { + ropts = ropts.with_password(pw); + } + match backup_client + .restore_from_path(Path::new(dir), source, Some(ropts)) + .await + { + Ok(_) => println!("Restore flow finished"), + Err(e) => eprintln!("Restore failed: {e}"), + } + } + Some(("unback", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let password = sub.get_one::("password").map(|s| s.as_str()); + match backup_client + .unback_from_path(Path::new(dir), password, source) + .await + { + Ok(_) => println!("Unback finished"), + Err(e) => eprintln!("Unback failed: {e}"), + } + } + Some(("extract", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let source = sub.get_one::("source").map(|s| s.as_str()); + let domain = sub.get_one::("domain").unwrap(); + let rel = sub.get_one::("path").unwrap(); + let password = sub.get_one::("password").map(|s| s.as_str()); + match backup_client + .extract_from_path(domain, rel, Path::new(dir), password, source) + .await + { + Ok(_) => println!("Extract finished"), + Err(e) => eprintln!("Extract failed: {e}"), + } + } + Some(("change-password", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + let old = sub.get_one::("old").map(|s| s.as_str()); + let newv = sub.get_one::("new").map(|s| s.as_str()); + match backup_client + .change_password_from_path(Path::new(dir), old, newv) + .await + { + Ok(_) => println!("Change password finished"), + Err(e) => eprintln!("Change password failed: {e}"), + } + } + Some(("erase-device", sub)) => { + let dir = sub.get_one::("dir").unwrap(); + match backup_client.erase_device_from_path(Path::new(dir)).await { + Ok(_) => println!("Erase device command sent"), + Err(e) => eprintln!("Erase device failed: {e}"), + } + } + Some(("freespace", _)) => match backup_client.get_freespace().await { + Ok(freespace) => { + let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0); + println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)"); + } + Err(e) => eprintln!("Failed to get free space: {e}"), + }, + Some(("encryption", _)) => match backup_client.check_backup_encryption().await { + Ok(is_encrypted) => { + println!( + "Backup encryption: {}", + if is_encrypted { "Enabled" } else { "Disabled" } + ); + } + Err(e) => eprintln!("Failed to check backup encryption: {e}"), + }, + _ => { + println!("No subcommand provided. Use --help for available commands."); + } + } + + // Disconnect from the service + if let Err(e) = backup_client.disconnect().await { + eprintln!("Warning: Failed to disconnect cleanly: {e}"); + } +} + +use idevice::services::mobilebackup2::{ + DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL, DL_CODE_FILE_DATA as CODE_FILE_DATA, + DL_CODE_SUCCESS as CODE_SUCCESS, +}; + +async fn process_dl_loop( + client: &mut MobileBackup2Client, + host_dir: &Path, +) -> Result, idevice::IdeviceError> { + loop { + let (tag, value) = client.receive_dl_message().await?; + match tag.as_str() { + "DLMessageDownloadFiles" => { + handle_download_files(client, &value, host_dir).await?; + } + "DLMessageUploadFiles" => { + handle_upload_files(client, &value, host_dir).await?; + } + "DLMessageGetFreeDiskSpace" => { + // Minimal implementation: report unknown/zero with success + client + .send_status_response(0, None, Some(plist::Value::Integer(0u64.into()))) + .await?; + } + "DLContentsOfDirectory" => { + // Minimal: return empty listing + let empty = plist::Value::Dictionary(Dictionary::new()); + client.send_status_response(0, None, Some(empty)).await?; + } + "DLMessageCreateDirectory" => { + let status = create_directory_from_message(&value, host_dir); + client.send_status_response(status, None, None).await?; + } + "DLMessageMoveFiles" | "DLMessageMoveItems" => { + let status = move_files_from_message(&value, host_dir); + client + .send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; + } + "DLMessageRemoveFiles" | "DLMessageRemoveItems" => { + let status = remove_files_from_message(&value, host_dir); + client + .send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; + } + "DLMessageCopyItem" => { + let status = copy_item_from_message(&value, host_dir); + client + .send_status_response( + status, + None, + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await?; + } + "DLMessageProcessMessage" => { + // Final status/content: return inner dict + if let plist::Value::Array(arr) = value + && let Some(plist::Value::Dictionary(dict)) = arr.get(1) + { + return Ok(Some(dict.clone())); + } + return Ok(None); + } + "DLMessageDisconnect" => { + return Ok(None); + } + other => { + eprintln!("Unsupported DL message: {other}"); + client + .send_status_response(-1, Some("Operation not supported"), None) + .await?; + } + } + } +} + +async fn handle_download_files( + client: &mut MobileBackup2Client, + dl_value: &plist::Value, + host_dir: &Path, +) -> Result<(), idevice::IdeviceError> { + // dl_value is an array: ["DLMessageDownloadFiles", [paths...], progress?] + let mut err_any = false; + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(files)) = arr.get(1) + { + for pv in files { + if let Some(path) = pv.as_string() + && let Err(e) = send_single_file(client, host_dir, path).await + { + eprintln!("Failed to send file {path}: {e}"); + err_any = true; + } + } + } + // terminating zero dword + client.idevice.send_raw(&0u32.to_be_bytes()).await?; + // status response + if err_any { + client + .send_status_response( + -13, + Some("Multi status"), + Some(plist::Value::Dictionary(Dictionary::new())), + ) + .await + } else { + client + .send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await + } +} + +async fn send_single_file( + client: &mut MobileBackup2Client, + host_dir: &Path, + rel_path: &str, +) -> Result<(), idevice::IdeviceError> { + let full = host_dir.join(rel_path); + let path_bytes = rel_path.as_bytes().to_vec(); + let nlen = (path_bytes.len() as u32).to_be_bytes(); + client.idevice.send_raw(&nlen).await?; + client.idevice.send_raw(&path_bytes).await?; + + let mut f = match std::fs::File::open(&full) { + Ok(f) => f, + Err(e) => { + // send error + let desc = e.to_string(); + let size = (desc.len() as u32 + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(CODE_ERROR_LOCAL); + client.idevice.send_raw(&hdr).await?; + client.idevice.send_raw(desc.as_bytes()).await?; + return Ok(()); + } + }; + let mut buf = [0u8; 32768]; + loop { + let read = f.read(&mut buf).unwrap_or(0); + if read == 0 { + break; + } + let size = ((read as u32) + 1).to_be_bytes(); + let mut hdr = Vec::with_capacity(5); + hdr.extend_from_slice(&size); + hdr.push(CODE_FILE_DATA); + client.idevice.send_raw(&hdr).await?; + client.idevice.send_raw(&buf[..read]).await?; + } + // success trailer + let mut ok = [0u8; 5]; + ok[..4].copy_from_slice(&1u32.to_be_bytes()); + ok[4] = CODE_SUCCESS; + client.idevice.send_raw(&ok).await?; + Ok(()) +} + +async fn handle_upload_files( + client: &mut MobileBackup2Client, + _dl_value: &plist::Value, + host_dir: &Path, +) -> Result<(), idevice::IdeviceError> { + // Minimal receiver: read pairs of (dir, filename) and block stream + // Receive dir name + loop { + let dlen = read_be_u32(client).await?; + if dlen == 0 { + break; + } + let dname = read_exact_string(client, dlen as usize).await?; + let flen = read_be_u32(client).await?; + if flen == 0 { + break; + } + let fname = read_exact_string(client, flen as usize).await?; + let dst = host_dir.join(&fname); + if let Some(parent) = dst.parent() { + let _ = fs::create_dir_all(parent); + } + let mut file = std::fs::File::create(&dst) + .map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; + loop { + let nlen = read_be_u32(client).await?; + if nlen == 0 { + break; + } + let code = read_one(client).await?; + if code == CODE_FILE_DATA { + let size = (nlen - 1) as usize; + let data = read_exact(client, size).await?; + file.write_all(&data) + .map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?; + } else { + let _ = read_exact(client, (nlen - 1) as usize).await?; + } + } + let _ = dname; // not used + } + client + .send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new()))) + .await +} + +async fn read_be_u32(client: &mut MobileBackup2Client) -> Result { + let buf = client.idevice.read_raw(4).await?; + Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) +} + +async fn read_one(client: &mut MobileBackup2Client) -> Result { + let buf = client.idevice.read_raw(1).await?; + Ok(buf[0]) +} + +async fn read_exact( + client: &mut MobileBackup2Client, + size: usize, +) -> Result, idevice::IdeviceError> { + client.idevice.read_raw(size).await +} + +async fn read_exact_string( + client: &mut MobileBackup2Client, + size: usize, +) -> Result { + let buf = client.idevice.read_raw(size).await?; + Ok(String::from_utf8_lossy(&buf).to_string()) +} + +fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::String(dir)) = arr.get(1) + { + let path = host_dir.join(dir); + return match fs::create_dir_all(&path) { + Ok(_) => 0, + Err(_) => -1, + }; + } + -1 +} + +fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Dictionary(map)) = arr.get(1) + { + for (from, to_v) in map.iter() { + if let Some(to) = to_v.as_string() { + let old = host_dir.join(from); + let newp = host_dir.join(to); + if let Some(parent) = newp.parent() { + let _ = fs::create_dir_all(parent); + } + if fs::rename(&old, &newp).is_err() { + return -1; + } + } + } + return 0; + } + -1 +} + +fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 2 + && let Some(plist::Value::Array(items)) = arr.get(1) + { + for it in items { + if let Some(p) = it.as_string() { + let path = host_dir.join(p); + if path.is_dir() { + if fs::remove_dir_all(&path).is_err() { + return -1; + } + } else if path.exists() && fs::remove_file(&path).is_err() { + return -1; + } + } + } + return 0; + } + -1 +} + +fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 { + if let plist::Value::Array(arr) = dl_value + && arr.len() >= 3 + && let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) = + (arr.get(1), arr.get(2)) + { + let from = host_dir.join(src); + let to = host_dir.join(dst); + if let Some(parent) = to.parent() { + let _ = fs::create_dir_all(parent); + } + if from.is_dir() { + // shallow copy: create dir + return match fs::create_dir_all(&to) { + Ok(_) => 0, + Err(_) => -1, + }; + } else { + return match fs::copy(&from, &to) { + Ok(_) => 0, + Err(_) => -1, + }; + } + } + -1 +} diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 8e7eff1..1564a9d 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -3,10 +3,10 @@ use std::{io::Write, path::PathBuf}; -use clap::{arg, value_parser, Arg, Command}; +use clap::{Arg, Command, arg, value_parser}; use idevice::{ - lockdown::LockdownClient, mobile_image_mounter::ImageMounter, pretty_print_plist, - IdeviceService, + IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, + pretty_print_plist, }; mod common; @@ -67,7 +67,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!( + "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } @@ -89,7 +91,10 @@ async fn main() { .await .expect("Unable to connect to lockdown"); - let product_version = match lockdown_client.get_value("ProductVersion", None).await { + let product_version = match lockdown_client + .get_value(Some("ProductVersion"), None) + .await + { Ok(p) => p, Err(_) => { lockdown_client @@ -97,7 +102,7 @@ async fn main() { .await .unwrap(); lockdown_client - .get_value("ProductVersion", None) + .get_value(Some("ProductVersion"), None) .await .unwrap() } @@ -182,21 +187,22 @@ async fn main() { .await .expect("Unable to read signature"); - let unique_chip_id = match lockdown_client.get_value("UniqueChipID", None).await { - Ok(u) => u, - Err(_) => { - lockdown_client - .start_session(&provider.get_pairing_file().await.unwrap()) - .await - .expect("Unable to start session"); - lockdown_client - .get_value("UniqueChipID", None) - .await - .expect("Unable to get UniqueChipID") + let unique_chip_id = + match lockdown_client.get_value(Some("UniqueChipID"), None).await { + Ok(u) => u, + Err(_) => { + lockdown_client + .start_session(&provider.get_pairing_file().await.unwrap()) + .await + .expect("Unable to start session"); + lockdown_client + .get_value(Some("UniqueChipID"), None) + .await + .expect("Unable to get UniqueChipID") + } } - } - .as_unsigned_integer() - .expect("Unexpected value for chip IP"); + .as_unsigned_integer() + .expect("Unexpected value for chip IP"); mounter_client .mount_personalized_with_callback( @@ -208,7 +214,7 @@ async fn main() { unique_chip_id, async |((n, d), _)| { let percent = (n as f64 / d as f64) * 100.0; - print!("\rProgress: {:.2}%", percent); + print!("\rProgress: {percent:.2}%"); std::io::stdout().flush().unwrap(); // Make sure it prints immediately if n == d { println!(); diff --git a/tools/src/os_trace_relay.rs b/tools/src/os_trace_relay.rs index 45c1b76..b04d0f7 100644 --- a/tools/src/os_trace_relay.rs +++ b/tools/src/os_trace_relay.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{os_trace_relay::OsTraceRelayClient, IdeviceService}; +use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; mod common; diff --git a/tools/src/pair.rs b/tools/src/pair.rs index 4ca173d..3425e1f 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -2,9 +2,9 @@ use clap::{Arg, Command}; use idevice::{ + IdeviceService, lockdown::LockdownClient, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, - IdeviceService, }; #[tokio::main] @@ -74,10 +74,13 @@ async fn main() { .expect("Pairing file test failed"); // Add the UDID (jitterbug spec) - pairing_file.udid = Some(dev.udid); + pairing_file.udid = Some(dev.udid.clone()); + let pairing_file = pairing_file.serialize().expect("failed to serialize"); - println!( - "{}", - String::from_utf8(pairing_file.serialize().unwrap()).unwrap() - ); + println!("{}", String::from_utf8(pairing_file.clone()).unwrap()); + + // Save with usbmuxd + u.save_pair_record(dev.device_id, &dev.udid, pairing_file) + .await + .expect("no save"); } diff --git a/tools/src/pcap.rs b/tools/src/pcap.rs new file mode 100644 index 0000000..a752ecf --- /dev/null +++ b/tools/src/pcap.rs @@ -0,0 +1,60 @@ +use idevice::bt_packet_logger::BtPacketKind; +use tokio::io::{AsyncWrite, AsyncWriteExt}; + +// Classic PCAP (big-endian) global header for DLT_BLUETOOTH_HCI_H4_WITH_PHDR (201) +const PCAP_GLOBAL_HEADER_BE: [u8; 24] = [ + 0xA1, 0xB2, 0xC3, 0xD4, // magic (big-endian stream) + 0x00, 0x02, // version maj + 0x00, 0x04, // version min + 0x00, 0x00, 0x00, 0x00, // thiszone + 0x00, 0x00, 0x00, 0x00, // sigfigs + 0x00, 0x00, 0x08, 0x00, // snaplen = 2048 + 0x00, 0x00, 0x00, 201, // network = 201 (HCI_H4_WITH_PHDR) +]; + +#[inline] +fn be32(x: u32) -> [u8; 4] { + [(x >> 24) as u8, (x >> 16) as u8, (x >> 8) as u8, x as u8] +} + +#[inline] +fn dir_flag(kind: BtPacketKind) -> Option { + use BtPacketKind::*; + Some(match kind { + HciCmd | AclSent | ScoSent => 0, + HciEvt | AclRecv | ScoRecv => 1, + _ => return None, + }) +} + +pub async fn write_pcap_header(w: &mut W) -> std::io::Result<()> { + w.write_all(&PCAP_GLOBAL_HEADER_BE).await +} + +pub async fn write_pcap_record( + w: &mut W, + ts_sec: u32, + ts_usec: u32, + kind: BtPacketKind, + h4_payload: &[u8], // starts with H4 type followed by HCI bytes +) -> std::io::Result<()> { + // Prepend 4-byte direction flag to the packet body + let Some(dir) = dir_flag(kind) else { + return Ok(()); + }; + let cap_len = 4u32 + h4_payload.len() as u32; + + // PCAP record header (big-endian fields to match magic above) + // ts_sec, ts_usec, incl_len, orig_len + let mut rec = [0u8; 16]; + rec[0..4].copy_from_slice(&be32(ts_sec)); + rec[4..8].copy_from_slice(&be32(ts_usec)); + rec[8..12].copy_from_slice(&be32(cap_len)); + rec[12..16].copy_from_slice(&be32(cap_len)); + + // Write: rec hdr, dir flag (as 4 BE bytes), then H4 bytes + w.write_all(&rec).await?; + w.write_all(&be32(dir)).await?; + w.write_all(h4_payload).await?; + Ok(()) +} diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs new file mode 100644 index 0000000..167a4aa --- /dev/null +++ b/tools/src/pcapd.rs @@ -0,0 +1,83 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, + pcapd::{PcapFileWriter, PcapdClient}, +}; + +mod common; +mod pcap; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("pcapd") + .about("Capture IP packets") + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("out") + .long("out") + .value_name("PCAP") + .help("Write PCAP to this file (use '-' for stdout)"), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("bt_packet_logger - capture bluetooth packets"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let out = matches.get_one::("out").map(String::to_owned); + + let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut logger_client = PcapdClient::connect(&*provider) + .await + .expect("Failed to connect to pcapd! This service is only available over USB!"); + + logger_client.next_packet().await.unwrap(); + + // Open output (default to stdout if --out omitted) + let mut out_writer = match out.as_deref() { + Some(path) => Some( + PcapFileWriter::new(tokio::fs::File::create(path).await.expect("open pcap")) + .await + .expect("write header"), + ), + _ => None, + }; + + println!("Starting packet stream"); + loop { + let packet = logger_client + .next_packet() + .await + .expect("failed to read next packet"); + if let Some(writer) = &mut out_writer { + writer.write_packet(&packet).await.expect("write packet"); + } else { + println!("{packet:?}"); + } + } +} diff --git a/tools/src/preboard.rs b/tools/src/preboard.rs new file mode 100644 index 0000000..b245037 --- /dev/null +++ b/tools/src/preboard.rs @@ -0,0 +1,76 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("preboard") + .about("Mess with developer mode") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("create").about("Create a stashbag??")) + .subcommand(Command::new("commit").about("Commit a stashbag??")) + .get_matches(); + + if matches.get_flag("about") { + println!("preboard - no idea what this does"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut pc = PreboardServiceClient::connect(&*provider) + .await + .expect("Failed to connect to Preboard"); + + if matches.subcommand_matches("create").is_some() { + pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } else if matches.subcommand_matches("commit").is_some() { + pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } + return; +} diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index 173633d..eebe59f 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,10 +1,8 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{ - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, RsdService, -}; +use idevice::services::lockdown::LockdownClient; +use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; mod common; @@ -74,30 +72,70 @@ async fn main() { } }; - let proxy = CoreDeviceProxy::connect(&*provider) + let mut rs_client_opt: Option< + idevice::dvt::remote_server::RemoteServerClient>, + > = None; + + if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { + let rsd_port = proxy.handshake.server_rsd_port; + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC (iOS 17+) + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + let mut rs_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd( + &mut adapter, + &mut handshake, + ) .await - .expect("no core proxy"); - let rsd_port = proxy.handshake.server_rsd_port; + .expect("no connect"); + rs_client.read_message(0).await.expect("no read??"); + rs_client_opt = Some(rs_client); + } - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); - - // Make the connection to RemoteXPC - let mut handshake = RsdHandshake::new(stream).await.unwrap(); - - let mut rs_client = - idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) + let mut rs_client = if let Some(c) = rs_client_opt { + c + } else { + // Read iOS version to decide whether we can fallback to remoteserver + let mut lockdown = LockdownClient::connect(&*provider) .await - .expect("no connect"); + .expect("lockdown connect failed"); + lockdown + .start_session(&provider.get_pairing_file().await.expect("pairing file")) + .await + .expect("lockdown start_session failed"); + let pv = lockdown + .get_value(Some("ProductVersion"), None) + .await + .ok() + .and_then(|v| v.as_string().map(|s| s.to_string())) + .unwrap_or_default(); + let major: u32 = pv + .split('.') + .next() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + if major >= 17 { + // iOS 17+ with no CoreDeviceProxy: do not attempt remoteserver (would return InvalidService) + panic!("iOS {pv} detected and CoreDeviceProxy unavailable. RemoteXPC tunnel required."); + } + + // iOS 16 and earlier: fallback to Lockdown remoteserver (or DVTSecureSocketProxy) + idevice::dvt::remote_server::RemoteServerClient::connect(&*provider) + .await + .expect("failed to connect to Instruments Remote Server over Lockdown (iOS16-). Ensure Developer Disk Image is mounted.") + }; + + // Note: On both transports, protocol requires reading the initial message on root channel (0) rs_client.read_message(0).await.expect("no read??"); let mut pc_client = idevice::dvt::process_control::ProcessControlClient::new(&mut rs_client) .await .unwrap(); let pid = pc_client - .launch_app(bundle_id, None, None, true, false) + .launch_app(bundle_id, None, None, false, false) .await .expect("no launch??"); pc_client diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs index 7f79180..38a1a8a 100644 --- a/tools/src/remotexpc.rs +++ b/tools/src/remotexpc.rs @@ -3,8 +3,8 @@ use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, tcp::stream::AdapterStream, xpc::RemoteXpcClient, - IdeviceService, + IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + tcp::stream::AdapterStream, }; mod common; @@ -66,13 +66,12 @@ async fn main() { let rsd_port = proxy.handshake.server_rsd_port; let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - adapter.pcap("new_xpc.pcap").await.unwrap(); - let conn = AdapterStream::connect(&mut adapter, rsd_port) + + let stream = AdapterStream::connect(&mut adapter, rsd_port) .await .expect("no RSD connect"); // Make the connection to RemoteXPC - let mut client = RemoteXpcClient::new(Box::new(conn)).await.unwrap(); - - println!("{:#?}", client.do_handshake().await); + let handshake = RsdHandshake::new(stream).await.unwrap(); + println!("{:#?}", handshake.services); } diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index b1d1e85..448bd8c 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -2,9 +2,8 @@ use clap::{Arg, Command}; use idevice::{ - core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, - restore_service::RestoreServiceClient, rsd::RsdHandshake, tcp::stream::AdapterStream, - IdeviceService, RsdService, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, + restore_service::RestoreServiceClient, rsd::RsdHandshake, }; mod common; @@ -52,7 +51,9 @@ async fn main() { .get_matches(); if matches.get_flag("about") { - println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!( + "mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary." + ); println!("Copyright (c) 2025 Jackson Coxson"); return; } @@ -75,11 +76,9 @@ async fn main() { .expect("no core proxy"); let rsd_port = proxy.handshake.server_rsd_port; - let mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); - - let stream = AdapterStream::connect(&mut adapter, rsd_port) - .await - .expect("no RSD connect"); + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); // Make the connection to RemoteXPC let mut handshake = RsdHandshake::new(stream).await.unwrap(); diff --git a/tools/src/syslog_relay.rs b/tools/src/syslog_relay.rs index 0ebf483..c556fec 100644 --- a/tools/src/syslog_relay.rs +++ b/tools/src/syslog_relay.rs @@ -1,7 +1,7 @@ // Jackson Coxson use clap::{Arg, Command}; -use idevice::{syslog_relay::SyslogRelayClient, IdeviceService}; +use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; mod common;