diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4dc6bc..b36dc43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,14 @@ jobs: - 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 + rustup target add x86_64-apple-darwin && \ + rustup target add aarch64-apple-ios-macabi && \ + rustup target add x86_64-apple-ios-macabi && \ + cargo install --force --locked bindgen-cli - name: Build all Apple targets and examples/tools run: | @@ -44,6 +50,12 @@ jobs: 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: diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 130f62f..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,3870 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" -dependencies = [ - "compression-codecs", - "compression-core", - "futures-core", - "futures-io", - "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.110", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async_zip" -version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" -dependencies = [ - "async-compression", - "crc32fast", - "futures-lite", - "pin-project", - "thiserror 2.0.17", - "tokio", - "tokio-util", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "avahi-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e00c83b3887835fd326daae1b2c4e7a435405033331a876ae1dd03f77b8274" -dependencies = [ - "bindgen 0.69.5", - "libc", -] - -[[package]] -name = "aws-lc-rs" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" -dependencies = [ - "bindgen 0.72.1", - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bindgen" -version = "0.68.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.110", - "which", -] - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.110", - "which", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.110", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "bonjour-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8655c4daf1447c831970e4134e8312a15c63fbff109b353cfe934c22f2b61a" -dependencies = [ - "bindgen 0.68.1", - "libc", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "c2rust-bitfields" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcee50917f9de1a018e3f4f9a8f2ff3d030a288cffa4b18d9b391e97c12e4cfb" -dependencies = [ - "c2rust-bitfields-derive", -] - -[[package]] -name = "c2rust-bitfields-derive" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b457277798202ccd365b9c112ebee08ddd57f1033916c8b8ea52f222e5b715d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "cbindgen" -version = "0.29.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" -dependencies = [ - "clap", - "heck", - "indexmap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 2.0.110", - "tempfile", - "toml", -] - -[[package]] -name = "cc" -version = "1.2.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim 0.11.1", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "compression-codecs" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" -dependencies = [ - "compression-core", - "flate2", -] - -[[package]] -name = "compression-core" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[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-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.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121161e240fcedaef95067636249fd1200e76921a2f0b101dca1648ba6d7ffa5" -dependencies = [ - "crossbeam-queue", - "crossbeam-utils", - "futures-core", - "parking_lot", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "der_derive", - "flagset", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive-getters" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive-new" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" -dependencies = [ - "darling", - "derive_builder_core", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_core" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.6.4", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getifaddrs" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c016cebf305060d144de015c98191ede05c210af588857bc2d4f8611c04663" -dependencies = [ - "bitflags", - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idevice" -version = "0.1.50" -dependencies = [ - "async-stream", - "async_zip", - "base64 0.22.1", - "byteorder", - "bytes", - "chacha20poly1305", - "chrono", - "crossfire", - "ed25519-dalek", - "futures", - "hkdf", - "idevice-srp", - "indexmap", - "json", - "ns-keyed-archive", - "obfstr", - "openssl", - "plist", - "rand 0.9.2", - "reqwest", - "rsa", - "rustls", - "serde", - "serde_json", - "sha2", - "thiserror 2.0.17", - "tokio", - "tokio-openssl", - "tokio-rustls", - "tracing", - "tun-rs", - "uuid", - "x25519-dalek", - "x509-cert", -] - -[[package]] -name = "idevice-ffi" -version = "0.1.0" -dependencies = [ - "cbindgen", - "futures", - "idevice", - "libc", - "once_cell", - "plist", - "plist_ffi", - "tokio", - "tracing", - "tracing-appender", - "tracing-subscriber", - "ureq", - "uuid", - "windows-sys 0.61.2", -] - -[[package]] -name = "idevice-srp" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84c9637ebbdfa523f352b62c8cf791288b3fbf59f6eac6c57c7c9949f5924b2" -dependencies = [ - "base64 0.21.7", - "digest", - "generic-array", - "lazy_static", - "num-bigint", - "subtle", -] - -[[package]] -name = "idevice-tools" -version = "0.1.0" -dependencies = [ - "clap", - "futures-util", - "idevice", - "ns-keyed-archive", - "plist", - "sha2", - "tokio", - "tracing", - "tracing-subscriber", - "ureq", - "uuid", - "zeroconf", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" -dependencies = [ - "equivalent", - "hashbrown", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix 0.29.0", - "winapi", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -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.17", - "widestring", - "windows", -] - -[[package]] -name = "netlink-packet-core" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" -dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-route" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" -dependencies = [ - "anyhow", - "bitflags", - "byteorder", - "libc", - "log", - "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.69", -] - -[[package]] -name = "netlink-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" -dependencies = [ - "bytes", - "libc", - "log", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "ns-keyed-archive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc340e0b7a5bb4b06338cafa693d0b585d597b024fff876c5dad385a00d83e7" -dependencies = [ - "nskeyedarchiver_converter", - "plist", -] - -[[package]] -name = "nskeyedarchiver_converter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c53158d1bf37bbbdd165f5220fda8bb5757c89eb700107992152c64c6cad7e" -dependencies = [ - "plist", - "thiserror 2.0.17", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "obfstr" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d354e9a302760d07e025701d40534f17dd1fe4c4db955b4e3bd2907c63bdee" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "paste" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "plist_ffi" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed070b06d9f2fdd7e816ef784fb07b09672f2acf37527f810dbedf450b7769" -dependencies = [ - "cbindgen", - "cc", - "libc", - "plist", - "serde_json", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.110", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "route_manager" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb012980f7bfadc330cc5b99e2a93bda641717554338b6ddb0e86de4188af65b" -dependencies = [ - "flume", - "libc", - "netlink-packet-core", - "netlink-packet-route", - "netlink-sys", - "windows-sys 0.60.2", -] - -[[package]] -name = "rsa" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustc-hash" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[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.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "tokio-openssl" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" -dependencies = [ - "openssl", - "openssl-sys", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" -dependencies = [ - "crossbeam-channel", - "thiserror 1.0.69", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tun-rs" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6179e34f9dfebbcf99cbe67aa142728d2f8481cae04e0c545ab2dfa1bc26b2c3" -dependencies = [ - "blocking", - "byteorder", - "bytes", - "c2rust-bitfields", - "encoding_rs", - "getifaddrs", - "ipnet", - "libc", - "libloading", - "log", - "mac_address", - "netconfig-rs", - "nix 0.30.1", - "route_manager", - "scopeguard", - "tokio", - "widestring", - "windows-sys 0.61.2", - "winreg", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" -dependencies = [ - "base64 0.22.1", - "flate2", - "log", - "percent-encoding", - "rustls", - "rustls-pki-types", - "ureq-proto", - "utf-8", - "webpki-roots", -] - -[[package]] -name = "ureq-proto" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" -dependencies = [ - "base64 0.22.1", - "http", - "httparse", - "log", -] - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "md-5", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.110", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -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 0.61.2", - "windows-future", - "windows-link 0.1.3", - "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 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[package]] -name = "x509-cert" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid", - "der", - "sha1", - "signature", - "spki", - "tls_codec", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", - "synstructure", -] - -[[package]] -name = "zeroconf" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e2acf5ff420bad258a6f8cc590a3361c8ddb8176077eb4714afa13e0d3d6b" -dependencies = [ - "avahi-sys", - "bonjour-sys", - "derive-getters", - "derive-new", - "derive_builder", - "libc", - "log", - "zeroconf-macros", -] - -[[package]] -name = "zeroconf-macros" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b52318880bad5d5a081f7ac4251b5144e73dfa859cbcd10562d9433eeed6b72" -dependencies = [ - "quote", - "syn 2.0.110", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] diff --git a/README.md b/README.md index 0cbd84b..16d73e0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ To keep dependency bloat and compile time down, everything is contained in featu |------------------------|-----------------------------------------------------------------------------| | `afc` | Apple File Conduit for file system access.| | `amfi` | Apple mobile file integrity service | +| `bt_packet_logger` | Capture Bluetooth packets. | +| `companion_proxy` | Manage paired Apple Watches. | | `core_device_proxy` | Start a secure tunnel to access protected services. | | `crashreportcopymobile`| Copy crash reports.| | `debug_proxy` | Send GDB commands to the device.| @@ -55,13 +57,19 @@ To keep dependency bloat and compile time down, everything is contained in featu | `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.| +| `installcoordination_proxy` | Manage app installation coordination.| | `location_simulation` | Simulate GPS locations on the device.| +| `misagent` | Manage provisioning profiles on the device.| +| `mobile_image_mounter` | Manage DDI images.| +| `mobileactivationd` | Activate/Deactivate device.| +| `mobilebackup2` | Manage backups.| | `pair` | Pair the device.| -| `syslog_relay` | Relay system logs from the device | +| `pcapd` | Capture network packets.| +| `preboard_service` | Interface with Preboard.| +| `restore_service` | Restore service (recovery/reboot).| +| `screenshotr` | Take screenshots.| +| `springboardservices` | Control SpringBoard (icons, wallpaper, orientation, etc.).| +| `syslog_relay` | Relay system logs and OS trace 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.| @@ -73,16 +81,11 @@ To keep dependency bloat and compile time down, everything is contained in featu Finish the following: -- springboard +- webinspector Implement the following: -- companion_proxy -- diagnostics -- mobilebackup2 - notification_proxy -- screenshot -- webinspector As this project is done in my free time within my busy schedule, there is no ETA for any of these. Feel free to contribute or donate! diff --git a/cpp/include/idevice++/diagnostics_relay.hpp b/cpp/include/idevice++/diagnostics_relay.hpp new file mode 100644 index 0000000..77a1bda --- /dev/null +++ b/cpp/include/idevice++/diagnostics_relay.hpp @@ -0,0 +1,60 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using DiagnosticsRelayPtr = + std::unique_ptr>; + +class DiagnosticsRelay { + 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); + + // API Methods - queries returning optional plist + Result, FfiError> ioregistry(Option current_plane, + Option entry_name, + Option entry_class) const; + + Result, FfiError> mobilegestalt(Option> keys) const; + + Result, FfiError> gasguage() const; + Result, FfiError> nand() const; + Result, FfiError> all() const; + Result, FfiError> wifi() const; + + // API Methods - actions + Result restart(); + Result shutdown(); + Result sleep(); + Result goodbye(); + + // RAII / moves + ~DiagnosticsRelay() noexcept = default; + DiagnosticsRelay(DiagnosticsRelay&&) noexcept = default; + DiagnosticsRelay& operator=(DiagnosticsRelay&&) noexcept = default; + DiagnosticsRelay(const DiagnosticsRelay&) = delete; + DiagnosticsRelay& operator=(const DiagnosticsRelay&) = delete; + + DiagnosticsRelayClientHandle* raw() const noexcept { return handle_.get(); } + static DiagnosticsRelay adopt(DiagnosticsRelayClientHandle* h) noexcept { + return DiagnosticsRelay(h); + } + + private: + explicit DiagnosticsRelay(DiagnosticsRelayClientHandle* h) noexcept : handle_(h) {} + DiagnosticsRelayPtr handle_{}; +}; + +} // namespace IdeviceFFI \ No newline at end of file diff --git a/cpp/include/idevice++/notification_proxy.hpp b/cpp/include/idevice++/notification_proxy.hpp new file mode 100644 index 0000000..f4bdcf8 --- /dev/null +++ b/cpp/include/idevice++/notification_proxy.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using NotificationProxyPtr = std::unique_ptr>; + +class NotificationProxy { + 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 post_notification(const std::string& name); + Result observe_notification(const std::string& name); + Result observe_notifications(const std::vector& names); + Result receive_notification(); + Result receive_notification_with_timeout(u_int64_t interval); + + // RAII / moves + ~NotificationProxy() noexcept = default; + NotificationProxy(NotificationProxy&&) noexcept = default; + NotificationProxy& operator=(NotificationProxy&&) noexcept = default; + NotificationProxy(const NotificationProxy&) = delete; + NotificationProxy& operator=(const NotificationProxy&) = delete; + + NotificationProxyClientHandle* raw() const noexcept { return handle_.get(); } + static NotificationProxy adopt(NotificationProxyClientHandle* h) noexcept { + return NotificationProxy(h); + } + + private: + explicit NotificationProxy(NotificationProxyClientHandle* h) noexcept : handle_(h) {} + NotificationProxyPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/diagnostics_relay.cpp b/cpp/src/diagnostics_relay.cpp new file mode 100644 index 0000000..398c923 --- /dev/null +++ b/cpp/src/diagnostics_relay.cpp @@ -0,0 +1,159 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +// -------- Factory Methods -------- + +Result DiagnosticsRelay::connect(Provider& provider) { + DiagnosticsRelayClientHandle* out = nullptr; + FfiError e(::diagnostics_relay_client_connect(provider.raw(), &out)); + if (e) { + return Err(e); + } + return Ok(DiagnosticsRelay::adopt(out)); +} + +Result DiagnosticsRelay::from_socket(Idevice&& socket) { + DiagnosticsRelayClientHandle* out = nullptr; + FfiError e(::diagnostics_relay_client_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(DiagnosticsRelay::adopt(out)); +} + +// -------- API Methods -------- + +Result, FfiError> +DiagnosticsRelay::ioregistry(Option current_plane, + Option entry_name, + Option entry_class) const { + plist_t res = nullptr; + + const char* plane_ptr = current_plane.is_some() ? current_plane.unwrap().c_str() : nullptr; + const char* name_ptr = entry_name.is_some() ? entry_name.unwrap().c_str() : nullptr; + const char* class_ptr = entry_class.is_some() ? entry_class.unwrap().c_str() : nullptr; + + FfiError e( + ::diagnostics_relay_client_ioregistry(handle_.get(), plane_ptr, name_ptr, class_ptr, &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> +DiagnosticsRelay::mobilegestalt(Option> keys) const { + plist_t res = nullptr; + + if (!keys.is_some() || keys.unwrap().empty()) { + return Err(FfiError::InvalidArgument()); + } + + FfiError e(::diagnostics_relay_client_mobilegestalt( + handle_.get(), keys.unwrap().data(), keys.unwrap().size(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::gasguage() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_gasguage(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::nand() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_nand(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::all() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_all(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result, FfiError> DiagnosticsRelay::wifi() const { + plist_t res = nullptr; + FfiError e(::diagnostics_relay_client_wifi(handle_.get(), &res)); + if (e) { + return Err(e); + } + + if (res == nullptr) { + return Ok(Option(None)); + } + return Ok(Some(res)); +} + +Result DiagnosticsRelay::restart() { + FfiError e(::diagnostics_relay_client_restart(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DiagnosticsRelay::shutdown() { + FfiError e(::diagnostics_relay_client_shutdown(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DiagnosticsRelay::sleep() { + FfiError e(::diagnostics_relay_client_sleep(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result DiagnosticsRelay::goodbye() { + FfiError e(::diagnostics_relay_client_goodbye(handle_.get())); + if (e) { + return Err(e); + } + return Ok(); +} + +} // namespace IdeviceFFI \ No newline at end of file diff --git a/cpp/src/notification_proxy.cpp b/cpp/src/notification_proxy.cpp new file mode 100644 index 0000000..dea5664 --- /dev/null +++ b/cpp/src/notification_proxy.cpp @@ -0,0 +1,82 @@ +// Jackson Coxson + +#include +#include +#include +#include + +namespace IdeviceFFI { + +Result NotificationProxy::connect(Provider& provider) { + NotificationProxyClientHandle* out = nullptr; + FfiError e(::notification_proxy_connect(provider.raw(), &out)); + if (e) { + provider.release(); + return Err(e); + } + return Ok(NotificationProxy::adopt(out)); +} + +Result NotificationProxy::from_socket(Idevice&& socket) { + NotificationProxyClientHandle* out = nullptr; + FfiError e(::notification_proxy_new(socket.raw(), &out)); + if (e) { + return Err(e); + } + socket.release(); + return Ok(NotificationProxy::adopt(out)); +} + +Result NotificationProxy::post_notification(const std::string& name) { + FfiError e(::notification_proxy_post(handle_.get(), name.c_str())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::observe_notification(const std::string& name) { + FfiError e(::notification_proxy_observe(handle_.get(), name.c_str())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::observe_notifications(const std::vector& names) { + std::vector ptrs; + ptrs.reserve(names.size() + 1); + for (const auto& n : names) { + ptrs.push_back(n.c_str()); + } + ptrs.push_back(nullptr); + FfiError e(::notification_proxy_observe_multiple(handle_.get(), ptrs.data())); + if (e) { + return Err(e); + } + return Ok(); +} + +Result NotificationProxy::receive_notification() { + char* name_ptr = nullptr; + FfiError e(::notification_proxy_receive(handle_.get(), &name_ptr)); + if (e) { + return Err(e); + } + std::string name(name_ptr); + ::notification_proxy_free_string(name_ptr); + return Ok(std::move(name)); +} + +Result NotificationProxy::receive_notification_with_timeout(u_int64_t interval) { + char* name_ptr = nullptr; + FfiError e(::notification_proxy_receive_with_timeout(handle_.get(), interval, &name_ptr)); + if (e) { + return Err(e); + } + std::string name(name_ptr); + ::notification_proxy_free_string(name_ptr); + return Ok(std::move(name)); +} + +} // namespace IdeviceFFI diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 13c2b36..6b8e70e 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"] diagnostics_relay = ["idevice/diagnostics_relay"] dvt = ["idevice/dvt"] heartbeat = ["idevice/heartbeat"] +notification_proxy = ["idevice/notification_proxy"] house_arrest = ["idevice/house_arrest"] installation_proxy = ["idevice/installation_proxy"] springboardservices = ["idevice/springboardservices"] @@ -50,6 +51,7 @@ tss = ["idevice/tss"] tunneld = ["idevice/tunneld"] usbmuxd = ["idevice/usbmuxd"] xpc = ["idevice/xpc"] +screenshotr = ["idevice/screenshotr"] full = [ "afc", "amfi", @@ -60,6 +62,7 @@ full = [ "diagnostics_relay", "dvt", "heartbeat", + "notification_proxy", "house_arrest", "installation_proxy", "misagent", @@ -75,6 +78,7 @@ full = [ "tunneld", "springboardservices", "syslog_relay", + "screenshotr", ] default = ["full", "aws-lc"] diff --git a/ffi/build.rs b/ffi/build.rs index a6d1b5d..f24a1ab 100644 --- a/ffi/build.rs +++ b/ffi/build.rs @@ -35,14 +35,20 @@ fn main() { .expect("Unable to generate bindings") .write_to_file("idevice.h"); - // 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"); + // Check if plist.h exists locally first, otherwise download + let plist_h_path = "plist.h"; + let h = if std::path::Path::new(plist_h_path).exists() { + std::fs::read_to_string(plist_h_path).expect("failed to read plist.h") + } else { + // 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"); + 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()) diff --git a/ffi/examples/afc.c b/ffi/examples/afc.c index c2ac39e..a6edfb4 100644 --- a/ffi/examples/afc.c +++ b/ffi/examples/afc.c @@ -254,7 +254,7 @@ int main(int argc, char **argv) { } else { uint8_t *data = NULL; size_t length = 0; - err = afc_file_read(file, &data, &length); + err = afc_file_read_entire(file, &data, &length); if (err == NULL) { if (write_file(dest_path, data, length)) { printf("File downloaded successfully\n"); diff --git a/ffi/src/afc.rs b/ffi/src/afc.rs index 65664af..19988da 100644 --- a/ffi/src/afc.rs +++ b/ffi/src/afc.rs @@ -1,12 +1,13 @@ // Jackson Coxson -use std::ptr::null_mut; +use std::{io::SeekFrom, ptr::null_mut}; use idevice::{ IdeviceError, IdeviceService, - afc::{AfcClient, DeviceInfo, FileInfo}, + afc::{AfcClient, DeviceInfo, FileInfo, file::FileDescriptor}, provider::IdeviceProvider, }; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; use crate::{ IdeviceFfiError, IdeviceHandle, LOCAL_RUNTIME, ffi_err, provider::IdeviceProviderHandle, @@ -53,6 +54,44 @@ pub unsafe extern "C" fn afc_client_connect( } } +/// Connects to the AFC2 service using a TCP provider +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated AfcClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc2_client_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut AfcClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res = run_sync_local(async { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + + AfcClient::new_afc2(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(AfcClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Creates a new AfcClient from an existing Idevice connection /// /// # Arguments @@ -555,12 +594,13 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev } } -/// Reads data from an open file +/// Reads data from an open file. This advances the cursor of the file. /// /// # Arguments /// * [`handle`] - File handle to read from /// * [`data`] - Will be set to point to the read data -/// * [`length`] - Will be set to the length of the read data +/// * [`len`] - Number of bytes to read from the file +/// * [`bytes_read`] - The number of bytes read from the file /// /// # Returns /// An IdeviceFfiError on error, null on success @@ -569,6 +609,53 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev /// All pointers must be valid and non-null #[unsafe(no_mangle)] pub unsafe extern "C" fn afc_file_read( + handle: *mut AfcFileHandle, + data: *mut *mut u8, + len: usize, + bytes_read: *mut libc::size_t, +) -> *mut IdeviceFfiError { + if handle.is_null() || data.is_null() || bytes_read.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; + let res: Result, IdeviceError> = run_sync({ + let mut buf = vec![0u8; len]; + async move { + let r = fd.read(&mut buf).await?; + buf.truncate(r); + Ok(buf) + } + }); + + match res { + Ok(bytes) => { + let mut boxed = bytes.into_boxed_slice(); + unsafe { + *data = boxed.as_mut_ptr(); + *bytes_read = boxed.len(); + } + std::mem::forget(boxed); + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Reads all data from an open file. +/// +/// # Arguments +/// * [`handle`] - File handle to read from +/// * [`data`] - Will be set to point to the read data +/// * [`length`] - The number of bytes read from the file +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// All pointers must be valid and non-null +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc_file_read_entire( handle: *mut AfcFileHandle, data: *mut *mut u8, length: *mut libc::size_t, @@ -594,6 +681,100 @@ pub unsafe extern "C" fn afc_file_read( } } +/// Moves the read/write cursor in an open file. +/// +/// # Arguments +/// * [`handle`] - File handle whose cursor should be moved +/// * [`offset`] - Distance to move the cursor, interpreted based on `whence` +/// * [`whence`] - Origin used for the seek operation: +/// * `0` — Seek from the start of the file (`SeekFrom::Start`) +/// * `1` — Seek from the current cursor position (`SeekFrom::Current`) +/// * `2` — Seek from the end of the file (`SeekFrom::End`) +/// * [`new_pos`] - Output parameter; will be set to the new absolute cursor position +/// +/// # Returns +/// An [`IdeviceFfiError`] on error, or null on success. +/// +/// # Safety +/// All pointers must be valid and non-null. +/// +/// # Notes +/// * If `whence` is invalid, this function returns `FfiInvalidArg`. +/// * The AFC protocol may restrict seeking beyond certain bounds; such errors +/// are reported through the returned [`IdeviceFfiError`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc_file_seek( + handle: *mut AfcFileHandle, + offset: i64, + whence: libc::c_int, + new_pos: *mut i64, +) -> *mut IdeviceFfiError { + if handle.is_null() || new_pos.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; + + let seek_from = match whence { + 0 => SeekFrom::Start(offset as u64), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => return ffi_err!(IdeviceError::FfiInvalidArg), + }; + + let res: Result = run_sync(async move { Ok(fd.seek(seek_from).await?) }); + + match res { + Ok(pos) => { + unsafe { + *new_pos = pos as i64; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Returns the current read/write cursor position of an open file. +/// +/// # Arguments +/// * [`handle`] - File handle whose cursor should be queried +/// * [`pos`] - Output parameter; will be set to the current absolute cursor position +/// +/// # Returns +/// An [`IdeviceFfiError`] on error, or null on success. +/// +/// # Safety +/// All pointers must be valid and non-null. +/// +/// # Notes +/// This function is equivalent to performing a seek operation with +/// `SeekFrom::Current(0)` internally. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn afc_file_tell( + handle: *mut AfcFileHandle, + pos: *mut i64, +) -> *mut IdeviceFfiError { + if handle.is_null() || pos.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let fd = unsafe { &mut *(handle as *mut FileDescriptor) }; + + let res: Result = + run_sync(async { Ok(fd.seek(SeekFrom::Current(0)).await?) }); + + match res { + Ok(cur) => { + unsafe { + *pos = cur as i64; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Writes data to an open file /// /// # Arguments diff --git a/ffi/src/diagnostics_relay.rs b/ffi/src/diagnostics_relay.rs index fb65a11..66e2c83 100644 --- a/ffi/src/diagnostics_relay.rs +++ b/ffi/src/diagnostics_relay.rs @@ -190,7 +190,7 @@ pub unsafe extern "C" fn diagnostics_relay_client_mobilegestalt( return ffi_err!(IdeviceError::FfiInvalidArg); } - let keys = if keys.is_null() { + let keys = if !keys.is_null() { let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) }; Some( keys.iter() diff --git a/ffi/src/house_arrest.rs b/ffi/src/house_arrest.rs new file mode 100644 index 0000000..d4ab0b3 --- /dev/null +++ b/ffi/src/house_arrest.rs @@ -0,0 +1,183 @@ +// Jackson Coxson + +use std::{ + ffi::{CStr, c_char}, + ptr::null_mut, +}; + +use idevice::{ + IdeviceError, IdeviceService, afc::AfcClient, house_arrest::HouseArrestClient, + provider::IdeviceProvider, +}; + +use crate::{ + IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle, + run_sync_local, +}; + +pub struct HouseArrestClientHandle(pub HouseArrestClient); + +/// Connects to the House Arrest service using a TCP provider +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_client_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut HouseArrestClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res = run_sync_local(async { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + + HouseArrestClient::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(HouseArrestClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Creates a new HouseArrestClient from an existing Idevice connection +/// +/// # Arguments +/// * [`socket`] - An IdeviceSocket handle +/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_client_new( + socket: *mut IdeviceHandle, + client: *mut *mut HouseArrestClientHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let socket = unsafe { Box::from_raw(socket) }.0; + let r = HouseArrestClient::new(socket); + let boxed = Box::new(HouseArrestClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() +} + +/// Vends a container for an app +/// +/// # Arguments +/// * [`client`] - The House Arrest client +/// * [`bundle_id`] - The bundle ID to vend for +/// * [`afc_client`] - The new AFC client for the underlying connection +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a allocated by this library +/// `bundle_id` must be a NULL-terminated string +/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_vend_container( + client: *mut HouseArrestClientHandle, + bundle_id: *const c_char, + afc_client: *mut *mut AfcClientHandle, +) -> *mut IdeviceFfiError { + if client.is_null() || bundle_id.is_null() || afc_client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy(); + let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop + + let res: Result = + run_sync_local(async move { client_ref.vend_container(bundle_id).await }); + + match res { + Ok(a) => { + let a = Box::into_raw(Box::new(AfcClientHandle(a))); + unsafe { *afc_client = a }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Vends documents for an app +/// +/// # Arguments +/// * [`client`] - The House Arrest client +/// * [`bundle_id`] - The bundle ID to vend for +/// * [`afc_client`] - The new AFC client for the underlying connection +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a allocated by this library +/// `bundle_id` must be a NULL-terminated string +/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_vend_documents( + client: *mut HouseArrestClientHandle, + bundle_id: *const c_char, + afc_client: *mut *mut AfcClientHandle, +) -> *mut IdeviceFfiError { + if client.is_null() || bundle_id.is_null() || afc_client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy(); + let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop + + let res: Result = + run_sync_local(async move { client_ref.vend_documents(bundle_id).await }); + + match res { + Ok(a) => { + let a = Box::into_raw(Box::new(AfcClientHandle(a))); + unsafe { *afc_client = a }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Frees an HouseArrestClient handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn house_arrest_client_free(handle: *mut HouseArrestClientHandle) { + if !handle.is_null() { + tracing::debug!("Freeing house_arrest_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index d237ed5..6b8d004 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -19,6 +19,8 @@ pub mod dvt; mod errors; #[cfg(feature = "heartbeat")] pub mod heartbeat; +#[cfg(feature = "house_arrest")] +pub mod house_arrest; #[cfg(feature = "installation_proxy")] pub mod installation_proxy; pub mod lockdown; @@ -27,12 +29,16 @@ pub mod logging; pub mod misagent; #[cfg(feature = "mobile_image_mounter")] pub mod mobile_image_mounter; +#[cfg(feature = "notification_proxy")] +pub mod notification_proxy; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; mod pairing_file; pub mod provider; #[cfg(feature = "xpc")] pub mod rsd; +#[cfg(feature = "screenshotr")] +pub mod screenshotr; #[cfg(feature = "springboardservices")] pub mod springboardservices; #[cfg(feature = "syslog_relay")] diff --git a/ffi/src/lockdown.rs b/ffi/src/lockdown.rs index 0912797..77b7717 100644 --- a/ffi/src/lockdown.rs +++ b/ffi/src/lockdown.rs @@ -179,7 +179,7 @@ pub unsafe extern "C" fn lockdownd_get_value( domain: *const libc::c_char, out_plist: *mut plist_t, ) -> *mut IdeviceFfiError { - if out_plist.is_null() { + if client.is_null() || out_plist.is_null() { return ffi_err!(IdeviceError::FfiInvalidArg); } @@ -221,6 +221,89 @@ pub unsafe extern "C" fn lockdownd_get_value( } } +/// Tells the device to enter recovery mode +/// +/// # Arguments +/// * `client` - A valid LockdowndClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lockdownd_enter_recovery( + client: *mut LockdowndClientHandle, +) -> *mut IdeviceFfiError { + if client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.enter_recovery().await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Sets a value in lockdownd +/// +/// # Arguments +/// * `client` - A valid LockdowndClient handle +/// * `key` - The key to set (null-terminated string) +/// * `value` - The value to set as a plist +/// * `domain` - The domain to set in (null-terminated string, optional) +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `key` must be a valid null-terminated string +/// `value` must be a valid plist +/// `domain` must be a valid null-terminated string or NULL +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lockdownd_set_value( + client: *mut LockdowndClientHandle, + key: *const libc::c_char, + value: plist_t, + domain: *const libc::c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || key.is_null() || value.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let key = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() { + Ok(k) => k, + Err(_) => return ffi_err!(IdeviceError::InvalidCString), + }; + + let domain = if domain.is_null() { + None + } else { + Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() { + Ok(d) => d, + Err(_) => return ffi_err!(IdeviceError::InvalidCString), + }) + }; + + let value = unsafe { &mut *value }.borrow_self().clone(); + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.set_value(key, value, domain).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + /// Frees a LockdowndClient handle /// /// # Arguments diff --git a/ffi/src/notification_proxy.rs b/ffi/src/notification_proxy.rs new file mode 100644 index 0000000..d883f88 --- /dev/null +++ b/ffi/src/notification_proxy.rs @@ -0,0 +1,311 @@ +// Jackson Coxson + +use std::ffi::{CStr, CString, c_char}; +use std::ptr::null_mut; + +use idevice::{ + IdeviceError, IdeviceService, notification_proxy::NotificationProxyClient, + provider::IdeviceProvider, +}; + +use crate::{ + IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync_local, +}; + +pub struct NotificationProxyClientHandle(pub NotificationProxyClient); + +/// Automatically creates and connects to Notification Proxy, returning a client handle +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut NotificationProxyClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + NotificationProxyClient::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(NotificationProxyClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => { + ffi_err!(e) + } + } +} + +/// Creates a new NotificationProxyClient from an existing Idevice connection +/// +/// # Arguments +/// * [`socket`] - An IdeviceSocket handle +/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed, +/// and may not be used again. +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_new( + socket: *mut IdeviceHandle, + client: *mut *mut NotificationProxyClientHandle, +) -> *mut IdeviceFfiError { + if socket.is_null() || client.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let socket = unsafe { Box::from_raw(socket) }.0; + let r = NotificationProxyClient::new(socket); + let boxed = Box::new(NotificationProxyClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() +} + +/// Posts a notification to the device +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name` - C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name` must be a valid null-terminated C string +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_post( + client: *mut NotificationProxyClientHandle, + name: *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let name_str = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.post_notification(name_str).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Observes a specific notification +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name` - C string containing the notification name to observe +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name` must be a valid null-terminated C string +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_observe( + client: *mut NotificationProxyClientHandle, + name: *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let name_str = match unsafe { CStr::from_ptr(name) }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + }; + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.observe_notification(name_str).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Observes multiple notifications at once +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `names` - A null-terminated array of C strings containing notification names +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `names` must be a valid pointer to a null-terminated array of null-terminated C strings +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_observe_multiple( + client: *mut NotificationProxyClientHandle, + names: *const *const c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || names.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let mut notification_names: Vec = Vec::new(); + let mut i = 0; + loop { + let ptr = unsafe { *names.add(i) }; + if ptr.is_null() { + break; + } + match unsafe { CStr::from_ptr(ptr) }.to_str() { + Ok(s) => notification_names.push(s.to_string()), + Err(_) => return ffi_err!(IdeviceError::FfiInvalidString), + } + i += 1; + } + + let refs: Vec<&str> = notification_names.iter().map(|s| s.as_str()).collect(); + + let res: Result<(), IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.observe_notifications(&refs).await + }); + + match res { + Ok(_) => null_mut(), + Err(e) => ffi_err!(e), + } +} + +/// Receives the next notification from the device +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_receive( + client: *mut NotificationProxyClientHandle, + name_out: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.receive_notification().await + }); + + match res { + Ok(name) => match CString::new(name) { + Ok(c_string) => { + unsafe { *name_out = c_string.into_raw() }; + null_mut() + } + Err(_) => ffi_err!(IdeviceError::FfiInvalidString), + }, + Err(e) => ffi_err!(e), + } +} + +/// Receives the next notification with a timeout +/// +/// # Arguments +/// * `client` - A valid NotificationProxyClient handle +/// * `interval` - Timeout in seconds to wait for a notification +/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_receive_with_timeout( + client: *mut NotificationProxyClientHandle, + interval: u64, + name_out: *mut *mut c_char, +) -> *mut IdeviceFfiError { + if client.is_null() || name_out.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.receive_notification_with_timeout(interval).await + }); + + match res { + Ok(name) => match CString::new(name) { + Ok(c_string) => { + unsafe { *name_out = c_string.into_raw() }; + null_mut() + } + Err(_) => ffi_err!(IdeviceError::FfiInvalidString), + }, + Err(e) => ffi_err!(e), + } +} + +/// Frees a string returned by notification_proxy_receive +/// +/// # Safety +/// `s` must be a valid pointer returned from `notification_proxy_receive` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_free_string(s: *mut c_char) { + if !s.is_null() { + let _ = unsafe { CString::from_raw(s) }; + } +} + +/// Frees a handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn notification_proxy_client_free( + handle: *mut NotificationProxyClientHandle, +) { + if !handle.is_null() { + tracing::debug!("Freeing notification_proxy_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/screenshotr.rs b/ffi/src/screenshotr.rs new file mode 100644 index 0000000..f6b52aa --- /dev/null +++ b/ffi/src/screenshotr.rs @@ -0,0 +1,132 @@ +// Jackson Coxson + +use std::ptr::null_mut; + +use idevice::{ + IdeviceError, IdeviceService, provider::IdeviceProvider, + services::screenshotr::ScreenshotService, +}; + +use crate::{IdeviceFfiError, ffi_err, provider::IdeviceProviderHandle, run_sync_local}; + +pub struct ScreenshotrClientHandle(pub ScreenshotService); + +/// Represents a screenshot data buffer +#[repr(C)] +pub struct ScreenshotData { + pub data: *mut u8, + pub length: usize, +} + +/// Connects to screenshotr service using provider +/// +/// # Arguments +/// * [`provider`] - An IdeviceProvider +/// * [`client`] - On success, will be set to point to a newly allocated ScreenshotrClient handle +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `provider` must be a valid pointer to a handle allocated by this library +/// `client` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_connect( + provider: *mut IdeviceProviderHandle, + client: *mut *mut ScreenshotrClientHandle, +) -> *mut IdeviceFfiError { + if provider.is_null() || client.is_null() { + tracing::error!("Null pointer provided"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result = run_sync_local(async move { + let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 }; + ScreenshotService::connect(provider_ref).await + }); + + match res { + Ok(r) => { + let boxed = Box::new(ScreenshotrClientHandle(r)); + unsafe { *client = Box::into_raw(boxed) }; + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Takes a screenshot from the device +/// +/// # Arguments +/// * `client` - A valid ScreenshotrClient handle +/// * `screenshot` - Pointer to store the screenshot data +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `screenshot` must be a valid pointer to store the screenshot data +/// The caller is responsible for freeing the screenshot data using screenshotr_screenshot_free +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_take_screenshot( + client: *mut ScreenshotrClientHandle, + screenshot: *mut ScreenshotData, +) -> *mut IdeviceFfiError { + if client.is_null() || screenshot.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let res: Result, IdeviceError> = run_sync_local(async move { + let client_ref = unsafe { &mut (*client).0 }; + client_ref.take_screenshot().await + }); + + match res { + Ok(data) => { + let len = data.len(); + let boxed = data.into_boxed_slice(); + let ptr = Box::into_raw(boxed) as *mut u8; + + unsafe { + (*screenshot).data = ptr; + (*screenshot).length = len; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Frees screenshot data +/// +/// # Arguments +/// * `screenshot` - The screenshot data to free +/// +/// # Safety +/// `screenshot` must be a valid ScreenshotData that was allocated by screenshotr_take_screenshot +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_screenshot_free(screenshot: ScreenshotData) { + if !screenshot.data.is_null() && screenshot.length > 0 { + tracing::debug!("Freeing screenshot data"); + let _ = + unsafe { Vec::from_raw_parts(screenshot.data, screenshot.length, screenshot.length) }; + } +} + +/// Frees a ScreenshotrClient handle +/// +/// # Arguments +/// * [`handle`] - The handle to free +/// +/// # Safety +/// `handle` must be a valid pointer to the handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn screenshotr_client_free(handle: *mut ScreenshotrClientHandle) { + if !handle.is_null() { + tracing::debug!("Freeing screenshotr_client"); + let _ = unsafe { Box::from_raw(handle) }; + } +} diff --git a/ffi/src/springboardservices.rs b/ffi/src/springboardservices.rs index 50d56d5..7f1cade 100644 --- a/ffi/src/springboardservices.rs +++ b/ffi/src/springboardservices.rs @@ -7,6 +7,7 @@ use idevice::{ IdeviceError, IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient, }; +use plist_ffi::plist_t; use crate::{ IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync, @@ -137,6 +138,169 @@ pub unsafe extern "C" fn springboard_services_get_icon( } } +/// Gets the home screen wallpaper preview as PNG image +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `out_result` - On success, will be set to point to newly allocated png image +/// * `out_result_len` - On success, will contain the size of the data in bytes +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `out_result` and `out_result_len` must be valid, non-null pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_home_screen_wallpaper_preview( + client: *mut SpringBoardServicesClientHandle, + out_result: *mut *mut c_void, + out_result_len: *mut libc::size_t, +) -> *mut IdeviceFfiError { + if client.is_null() || out_result.is_null() || out_result_len.is_null() { + tracing::error!("Invalid arguments: {client:?}, {out_result:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let res: Result, IdeviceError> = + run_sync(async { client.0.get_home_screen_wallpaper_preview_pngdata().await }); + + match res { + Ok(r) => { + let len = r.len(); + let boxed_slice = r.into_boxed_slice(); + let ptr = boxed_slice.as_ptr(); + std::mem::forget(boxed_slice); + + unsafe { + *out_result = ptr as *mut c_void; + *out_result_len = len; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Gets the lock screen wallpaper preview as PNG image +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `out_result` - On success, will be set to point to newly allocated png image +/// * `out_result_len` - On success, will contain the size of the data in bytes +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `out_result` and `out_result_len` must be valid, non-null pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_lock_screen_wallpaper_preview( + client: *mut SpringBoardServicesClientHandle, + out_result: *mut *mut c_void, + out_result_len: *mut libc::size_t, +) -> *mut IdeviceFfiError { + if client.is_null() || out_result.is_null() || out_result_len.is_null() { + tracing::error!("Invalid arguments: {client:?}, {out_result:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let res: Result, IdeviceError> = + run_sync(async { client.0.get_lock_screen_wallpaper_preview_pngdata().await }); + + match res { + Ok(r) => { + let len = r.len(); + let boxed_slice = r.into_boxed_slice(); + let ptr = boxed_slice.as_ptr(); + std::mem::forget(boxed_slice); + + unsafe { + *out_result = ptr as *mut c_void; + *out_result_len = len; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Gets the current interface orientation of the device +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `out_orientation` - On success, will contain the orientation value (0-4) +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `out_orientation` must be a valid, non-null pointer +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_interface_orientation( + client: *mut SpringBoardServicesClientHandle, + out_orientation: *mut u8, +) -> *mut IdeviceFfiError { + if client.is_null() || out_orientation.is_null() { + tracing::error!("Invalid arguments: {client:?}, {out_orientation:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let res = run_sync(async { client.0.get_interface_orientation().await }); + + match res { + Ok(orientation) => { + unsafe { + *out_orientation = orientation as u8; + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + +/// Gets the home screen icon layout metrics +/// +/// # Arguments +/// * `client` - A valid SpringBoardServicesClient handle +/// * `res` - On success, will point to a plist dictionary node containing the metrics +/// +/// # Returns +/// An IdeviceFfiError on error, null on success +/// +/// # Safety +/// `client` must be a valid pointer to a handle allocated by this library +/// `res` must be a valid, non-null pointer +#[unsafe(no_mangle)] +pub unsafe extern "C" fn springboard_services_get_homescreen_icon_metrics( + client: *mut SpringBoardServicesClientHandle, + res: *mut plist_t, +) -> *mut IdeviceFfiError { + if client.is_null() || res.is_null() { + tracing::error!("Invalid arguments: {client:?}, {res:?}"); + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let client = unsafe { &mut *client }; + + let output = run_sync(async { client.0.get_homescreen_icon_metrics().await }); + + match output { + Ok(metrics) => { + unsafe { + *res = + plist_ffi::PlistWrapper::new_node(plist::Value::Dictionary(metrics)).into_ptr(); + } + null_mut() + } + Err(e) => ffi_err!(e), + } +} + /// Frees an SpringBoardServicesClient handle /// /// # Arguments diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 2f49472..8d659bd 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.50" +version = "0.1.53" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" @@ -21,6 +21,7 @@ tokio-openssl = { version = "0.6", optional = true } openssl = { version = "0.10", optional = true } plist = { version = "1.8" } +plist-macro = { version = "0.1.3" } serde = { version = "1", features = ["derive"] } ns-keyed-archive = { version = "0.1.4", optional = true } crossfire = { version = "2.1", optional = true } @@ -40,7 +41,7 @@ json = { version = "0.12", optional = true } byteorder = { version = "1.5", optional = true } bytes = { version = "1.10", optional = true } -reqwest = { version = "0.12", features = [ +reqwest = { version = "0.13", features = [ "json", ], optional = true, default-features = false } rand = { version = "0.9", optional = true } @@ -76,7 +77,7 @@ ring = ["rustls", "rustls/ring", "tokio-rustls/ring"] rustls = ["dep:rustls", "dep:tokio-rustls"] openssl = ["dep:openssl", "dep:tokio-openssl"] -afc = ["dep:chrono"] +afc = ["dep:chrono", "dep:futures"] amfi = [] bt_packet_logger = [] companion_proxy = [] @@ -101,6 +102,7 @@ misagent = [] mobile_image_mounter = ["dep:sha2"] mobileactivationd = ["dep:reqwest"] mobilebackup2 = [] +notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pcapd = [] @@ -152,6 +154,7 @@ full = [ "mobile_image_mounter", "mobileactivationd", "mobilebackup2", + "notification_proxy", "pair", "pcapd", "preboard_service", diff --git a/idevice/src/cursor.rs b/idevice/src/cursor.rs new file mode 100644 index 0000000..20d0df4 --- /dev/null +++ b/idevice/src/cursor.rs @@ -0,0 +1,334 @@ +// Jackson Coxson + +#[derive(Clone, Debug)] +pub struct Cursor<'a> { + inner: &'a [u8], + pos: usize, +} + +impl<'a> Cursor<'a> { + /// Creates a new cursor + pub fn new(inner: &'a [u8]) -> Self { + Self { inner, pos: 0 } + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn at_end(&self) -> bool { + self.pos == self.inner.len() + } + + pub fn read(&mut self, to_read: usize) -> Option<&'a [u8]> { + // Check if the end of the slice (self.pos + to_read) is beyond the buffer length + if self + .pos + .checked_add(to_read) + .is_none_or(|end_pos| end_pos > self.inner.len()) + { + return None; + } + + // The end of the slice is self.pos + to_read + let end_pos = self.pos + to_read; + let res = Some(&self.inner[self.pos..end_pos]); + self.pos = end_pos; + res + } + + pub fn back(&mut self, to_back: usize) { + let to_back = if to_back > self.pos { + self.pos + } else { + to_back + }; + + self.pos -= to_back; + } + + /// True if actually all zeroes + pub fn read_assert_zero(&mut self, to_read: usize) -> Option<()> { + let bytes = self.read(to_read)?; + + #[cfg(debug_assertions)] + for b in bytes.iter() { + if *b > 0 { + eprintln!("Zero read contained non-zero values!"); + eprintln!("{bytes:02X?}"); + return None; + } + } + + Some(()) + } + + pub fn read_to(&mut self, end: usize) -> Option<&'a [u8]> { + if end > self.inner.len() { + return None; + } + let res = Some(&self.inner[self.pos..end]); + self.pos = end; + res + } + + pub fn peek_to(&mut self, end: usize) -> Option<&'a [u8]> { + if end > self.inner.len() { + return None; + } + Some(&self.inner[self.pos..end]) + } + + pub fn peek(&self, to_read: usize) -> Option<&'a [u8]> { + if self + .pos + .checked_add(to_read) + .is_none_or(|end_pos| end_pos > self.inner.len()) + { + return None; + } + + let end_pos = self.pos + to_read; + Some(&self.inner[self.pos..end_pos]) + } + + pub fn reveal(&self, surrounding: usize) { + let len = self.inner.len(); + + if self.pos > len { + println!("Cursor is past end of buffer"); + return; + } + + let start = self.pos.saturating_sub(surrounding); + let end = (self.pos + surrounding + 1).min(len); + + // HEADER + println!("Reveal around pos {} ({} bytes):", self.pos, surrounding); + + // --- HEX LINE --- + print!("Hex: "); + for i in start..end { + if i == self.pos { + print!("[{:02X}] ", self.inner[i]); + } else { + print!("{:02X} ", self.inner[i]); + } + } + println!(); + + // --- ASCII LINE --- + print!("Ascii: "); + for i in start..end { + let b = self.inner[i]; + let c = if b.is_ascii_graphic() || b == b' ' { + b as char + } else { + '.' + }; + + if i == self.pos { + print!("[{}] ", c); + } else { + print!("{} ", c); + } + } + println!(); + + // --- OFFSET LINE --- + print!("Offset: "); + for i in start..end { + let off = i as isize - self.pos as isize; + if i == self.pos { + print!("[{}] ", off); + } else { + print!("{:<3} ", off); + } + } + println!(); + } + + pub fn remaining(&mut self) -> &'a [u8] { + let res = &self.inner[self.pos..]; + self.pos = self.inner.len(); + res + } + + pub fn read_u8(&mut self) -> Option { + if self.pos == self.inner.len() { + return None; + } + let res = Some(self.inner[self.pos]); + self.pos += 1; + res + } + + pub fn read_le_u16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u16::from_le_bytes(bytes)) + } + + pub fn read_be_u16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u16::from_be_bytes(bytes)) + } + + pub fn read_le_u32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u32::from_le_bytes(bytes)) + } + + pub fn read_be_u32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u32::from_be_bytes(bytes)) + } + + pub fn read_le_u64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u64::from_le_bytes(bytes)) + } + + pub fn read_be_u64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u64::from_be_bytes(bytes)) + } + + pub fn read_le_u128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u128::from_le_bytes(bytes)) + } + + pub fn read_be_u128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(u128::from_be_bytes(bytes)) + } + + pub fn read_le_f32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(f32::from_le_bytes(bytes)) + } + + pub fn read_be_f32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(f32::from_be_bytes(bytes)) + } + + pub fn read_i8(&mut self) -> Option { + if self.pos == self.inner.len() { + return None; + } + let res = Some(self.inner[self.pos]).map(|x| x as i8); + self.pos += 1; + res + } + + pub fn read_le_i16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i16::from_le_bytes(bytes)) + } + + pub fn read_be_i16(&mut self) -> Option { + const SIZE: usize = 2; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i16::from_be_bytes(bytes)) + } + + pub fn read_le_i32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i32::from_le_bytes(bytes)) + } + + pub fn read_be_i32(&mut self) -> Option { + const SIZE: usize = 4; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i32::from_be_bytes(bytes)) + } + + pub fn read_le_i64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i64::from_le_bytes(bytes)) + } + + pub fn read_be_i64(&mut self) -> Option { + const SIZE: usize = 8; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i64::from_be_bytes(bytes)) + } + + pub fn read_le_i128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i128::from_le_bytes(bytes)) + } + + pub fn read_be_i128(&mut self) -> Option { + const SIZE: usize = 16; + let bytes = self.read(SIZE)?; + let bytes: [u8; SIZE] = bytes.try_into().unwrap(); + Some(i128::from_be_bytes(bytes)) + } + + pub fn take_2(&mut self) -> Option<[u8; 2]> { + let bytes = self.read(2)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_3(&mut self) -> Option<[u8; 3]> { + let bytes = self.read(3)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_4(&mut self) -> Option<[u8; 4]> { + let bytes = self.read(4)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_8(&mut self) -> Option<[u8; 8]> { + let bytes = self.read(8)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_20(&mut self) -> Option<[u8; 20]> { + let bytes = self.read(20)?; + Some(bytes.to_owned().try_into().unwrap()) + } + + pub fn take_32(&mut self) -> Option<[u8; 32]> { + let bytes = self.read(32)?; + Some(bytes.to_owned().try_into().unwrap()) + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 1b629f9..6416e12 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -5,8 +5,9 @@ #[cfg(all(feature = "pair", feature = "rustls"))] mod ca; +pub mod cursor; +mod obfuscation; pub mod pairing_file; -pub mod plist_macro; pub mod provider; #[cfg(feature = "remote_pairing")] pub mod remote_pairing; @@ -20,7 +21,6 @@ pub mod tss; pub mod tunneld; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; -mod util; pub mod utils; #[cfg(feature = "xpc")] pub mod xpc; @@ -31,6 +31,7 @@ pub use services::*; #[cfg(feature = "xpc")] pub use xpc::RemoteXpcClient; +use plist_macro::{plist, pretty_print_dictionary, pretty_print_plist}; use provider::{IdeviceProvider, RsdProvider}; #[cfg(feature = "rustls")] use rustls::{crypto::CryptoProvider, pki_types::ServerName}; @@ -40,9 +41,7 @@ use std::{ }; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tracing::{debug, error, trace}; - -pub use util::{pretty_print_dictionary, pretty_print_plist}; +use tracing::{debug, trace}; use crate::services::lockdown::LockdownClient; @@ -79,6 +78,7 @@ pub trait IdeviceService: Sized { async fn connect(provider: &dyn IdeviceProvider) -> Result { let mut lockdown = LockdownClient::connect(provider).await?; + #[cfg(feature = "openssl")] let legacy = lockdown .get_value(Some("ProductVersion"), None) .await @@ -90,6 +90,9 @@ pub trait IdeviceService: Sized { .map(|x| x < 5) .unwrap_or(false); + #[cfg(not(feature = "openssl"))] + let legacy = false; + lockdown .start_session(&provider.get_pairing_file().await?) .await?; @@ -194,7 +197,7 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if communication fails or response is invalid pub async fn get_type(&mut self) -> Result { - let req = crate::plist!({ + let req = plist!({ "Label": self.label.clone(), "Request": "QueryType", }); @@ -214,7 +217,7 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if the protocol sequence isn't followed correctly pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { - let req = crate::plist!({ + let req = plist!({ "Label": self.label.clone(), "ProtocolVersion": "2", "Request": "RSDCheckin", @@ -484,7 +487,13 @@ impl Idevice { if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { return Err(e); } else { - return Err(IdeviceError::UnknownErrorType(e)); + let msg = + if let Some(desc) = res.get("ErrorDescription").and_then(|x| x.as_string()) { + format!("{} ({})", e, desc) + } else { + e + }; + return Err(IdeviceError::UnknownErrorType(msg)); } } Ok(res) @@ -875,6 +884,9 @@ pub enum IdeviceError { #[cfg(feature = "remote_pairing")] #[error("Chacha encryption error")] ChachaEncryption(chacha20poly1305::Error) = -75, + #[cfg(feature = "notification_proxy")] + #[error("notification proxy died")] + NotificationProxyDeath = -76, } impl IdeviceError { @@ -1048,6 +1060,9 @@ impl IdeviceError { IdeviceError::PairVerifyFailed => -73, IdeviceError::SrpAuthFailed => -74, IdeviceError::ChachaEncryption(_) => -75, + + #[cfg(feature = "notification_proxy")] + IdeviceError::NotificationProxyDeath => -76, } } } diff --git a/idevice/src/obfuscation.rs b/idevice/src/obfuscation.rs new file mode 100644 index 0000000..d101c09 --- /dev/null +++ b/idevice/src/obfuscation.rs @@ -0,0 +1,15 @@ +// Jackson Coxson + +#[macro_export] +macro_rules! obf { + ($lit:literal) => {{ + #[cfg(feature = "obfuscate")] + { + std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string()) + } + #[cfg(not(feature = "obfuscate"))] + { + std::borrow::Cow::Borrowed($lit) + } + }}; +} diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index 94299d3..9c7ad92 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -38,7 +38,7 @@ pub struct PairingFile { /// Host identifier pub host_id: String, /// Escrow bag allowing for access while locked - pub escrow_bag: Vec, + pub escrow_bag: Option>, /// Device's WiFi MAC address pub wifi_mac_address: String, /// Device's Unique Device Identifier (optional) @@ -73,7 +73,7 @@ struct RawPairingFile { system_buid: String, #[serde(rename = "HostID")] host_id: String, - escrow_bag: Data, + escrow_bag: Option, // None on Apple Watch #[serde(rename = "WiFiMACAddress")] wifi_mac_address: String, #[serde(rename = "UDID")] @@ -206,7 +206,7 @@ impl TryFrom for PairingFile { root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?, system_buid: value.system_buid, host_id: value.host_id, - escrow_bag: value.escrow_bag.into(), + escrow_bag: value.escrow_bag.map(|x| x.into()), wifi_mac_address: value.wifi_mac_address, udid: value.udid, }) @@ -230,7 +230,7 @@ impl TryFrom for PairingFile { root_certificate: X509::from_pem(&Into::>::into(value.root_certificate))?, system_buid: value.system_buid, host_id: value.host_id, - escrow_bag: value.escrow_bag.into(), + escrow_bag: value.escrow_bag.map(|x| x.into()), wifi_mac_address: value.wifi_mac_address, udid: value.udid, }) @@ -258,7 +258,7 @@ impl From for RawPairingFile { root_certificate: Data::new(root_cert_data), system_buid: value.system_buid, host_id: value.host_id.clone(), - escrow_bag: Data::new(value.escrow_bag), + escrow_bag: value.escrow_bag.map(Data::new), wifi_mac_address: value.wifi_mac_address, udid: value.udid, } @@ -278,7 +278,7 @@ impl TryFrom for RawPairingFile { root_certificate: Data::new(value.root_certificate.to_pem()?), system_buid: value.system_buid, host_id: value.host_id.clone(), - escrow_bag: Data::new(value.escrow_bag), + escrow_bag: value.escrow_bag.map(Data::new), wifi_mac_address: value.wifi_mac_address, udid: value.udid, }) diff --git a/idevice/src/plist_macro.rs b/idevice/src/plist_macro.rs deleted file mode 100644 index 3453b43..0000000 --- a/idevice/src/plist_macro.rs +++ /dev/null @@ -1,714 +0,0 @@ -// Jackson Coxson -// Ported from serde's json! - -/// Construct a `plist::Value` from a JSON-like literal. -/// -/// ``` -/// # use idevice::plist; -/// # -/// let value = plist!({ -/// "code": 200, -/// "success": true, -/// "payload": { -/// "features": [ -/// "serde", -/// "plist" -/// ], -/// "homepage": null -/// } -/// }); -/// ``` -/// -/// Variables or expressions can be interpolated into the plist literal. Any type -/// interpolated into an array element or object value must implement `Into`. -/// If the conversion fails, the `plist!` macro will panic. -/// -/// ``` -/// # use idevice::plist; -/// # -/// let code = 200; -/// let features = vec!["serde", "plist"]; -/// -/// let value = plist!({ -/// "code": code, -/// "success": code == 200, -/// "payload": { -/// features[0]: features[1] -/// } -/// }); -/// ``` -/// -/// Trailing commas are allowed inside both arrays and objects. -/// -/// ``` -/// # use idevice::plist; -/// # -/// let value = plist!([ -/// "notice", -/// "the", -/// "trailing", -/// "comma -->", -/// ]); -/// ``` -#[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)+) - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! plist_internal { - ////////////////////////////////////////////////////////////////////////// - // TT muncher for parsing the inside of an array [...]. Produces a vec![...] - // of the elements. - // - // Must be invoked as: plist_internal!(@array [] $($tt)*) - ////////////////////////////////////////////////////////////////////////// - - // Done with trailing comma. - (@array [$($elems:expr,)*]) => { - vec![$($elems,)*] - }; - - // Done without trailing comma. - (@array [$($elems:expr),*]) => { - vec![$($elems),*] - }; - - // Next element is `null`. - (@array [$($elems:expr,)*] null $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(null)] $($rest)*) - }; - - // Next element is `true`. - (@array [$($elems:expr,)*] true $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(true)] $($rest)*) - }; - - // Next element is `false`. - (@array [$($elems:expr,)*] false $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(false)] $($rest)*) - }; - - // Next element is an array. - (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!([$($array)*])] $($rest)*) - }; - - // Next element is a map. - (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!({$($map)*})] $($rest)*) - }; - - // Next element is an expression followed by comma. - (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($next),] $($rest)*) - }; - - // Last element is an expression with no trailing comma. - (@array [$($elems:expr,)*] $last:expr) => { - $crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($last)]) - }; - - // Comma after the most recent element. - (@array [$($elems:expr),*] , $($rest:tt)*) => { - $crate::plist_internal!(@array [$($elems,)*] $($rest)*) - }; - - // Unexpected token after most recent element. - (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => { - $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. - // - // Must be invoked as: plist_internal!(@object $map () ($($tt)*) ($($tt)*)) - // - // We require two copies of the input tokens so that we can match on one - // copy and trigger errors on the other copy. - ////////////////////////////////////////////////////////////////////////// - - // Done. - (@object $object:ident () () ()) => {}; - - // Insert the current entry followed by trailing comma. - (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => { - let _ = $object.insert(($($key)+).into(), $value); - $crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*)); - }; - - // Current entry followed by unexpected token. - (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => { - $crate::plist_unexpected!($unexpected); - }; - - // Insert the last entry without trailing comma. - (@object $object:ident [$($key:tt)+] ($value:expr)) => { - let _ = $object.insert(($($key)+).into(), $value); - }; - - // Next value is `null`. - (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(null)) $($rest)*); - }; - - // Next value is `true`. - (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(true)) $($rest)*); - }; - - // Next value is `false`. - (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(false)) $($rest)*); - }; - - // Next value is an array. - (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!([$($array)*])) $($rest)*); - }; - - // Next value is a map. - (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => { - $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)*); - }; - - // Last value is an expression with no trailing comma. - (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => { - $crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value))); - }; - - // Missing value for last entry. Trigger a reasonable error message. - (@object $object:ident ($($key:tt)+) (:) $copy:tt) => { - // "unexpected end of macro invocation" - $crate::plist_internal!(); - }; - - // Missing colon and value for last entry. Trigger a reasonable error - // message. - (@object $object:ident ($($key:tt)+) () $copy:tt) => { - // "unexpected end of macro invocation" - $crate::plist_internal!(); - }; - - // Misplaced colon. Trigger a reasonable error message. - (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => { - // Takes no arguments so "no rules expected the token `:`". - $crate::plist_unexpected!($colon); - }; - - // Found a comma inside a key. Trigger a reasonable error message. - (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => { - // Takes no arguments so "no rules expected the token `,`". - $crate::plist_unexpected!($comma); - }; - - // Key is fully parenthesized. This avoids clippy double_parens false - // positives because the parenthesization may be necessary here. - (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*)); - }; - - // Refuse to absorb colon token into key expression. - (@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => { - $crate::plist_expect_expr_comma!($($unexpected)+); - }; - - // Munch a token into the current key. - (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => { - $crate::plist_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*)); - }; - - ////////////////////////////////////////////////////////////////////////// - // The main implementation. - // - // Must be invoked as: plist_internal!($($plist)+) - ////////////////////////////////////////////////////////////////////////// - - (null) => { - plist::Value::String("".to_string()) // plist doesn't have null, use empty string or consider other representation - }; - - (true) => { - plist::Value::Boolean(true) - }; - - (false) => { - plist::Value::Boolean(false) - }; - - ([]) => { - plist::Value::Array(vec![]) - }; - - ([ $($tt:tt)+ ]) => { - plist::Value::Array($crate::plist_internal!(@array [] $($tt)+)) - }; - - ({}) => { - plist::Value::Dictionary(plist::Dictionary::new()) - }; - - ({ $($tt:tt)+ }) => { - plist::Value::Dictionary({ - let mut object = plist::Dictionary::new(); - $crate::plist_internal!(@object object () ($($tt)+) ($($tt)+)); - object - }) - }; - - // Any type that can be converted to plist::Value: numbers, strings, variables etc. - // Must be below every other rule. - ($other:expr) => { - $crate::plist_macro::plist_to_value($other) - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! plist_unexpected { - () => {}; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! plist_expect_expr_comma { - ($e:expr , $($tt:tt)*) => {}; -} - -// Helper function to convert various types to plist::Value -#[doc(hidden)] -pub fn plist_to_value(value: T) -> plist::Value { - value.to_plist_value() -} - -// Trait for types that can be converted to plist::Value -pub trait PlistConvertible { - fn to_plist_value(self) -> plist::Value; -} - -// Implementations for common types -impl PlistConvertible for plist::Value { - fn to_plist_value(self) -> plist::Value { - self - } -} - -impl PlistConvertible for String { - fn to_plist_value(self) -> plist::Value { - plist::Value::String(self) - } -} - -impl PlistConvertible for &str { - fn to_plist_value(self) -> plist::Value { - plist::Value::String(self.to_string()) - } -} - -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()) - } -} - -impl PlistConvertible for i64 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer(self.into()) - } -} - -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()) - } -} - -impl PlistConvertible for u64 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Integer((self as i64).into()) - } -} - -impl PlistConvertible for f32 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Real(self as f64) - } -} - -impl PlistConvertible for f64 { - fn to_plist_value(self) -> plist::Value { - plist::Value::Real(self) - } -} - -impl PlistConvertible for bool { - fn to_plist_value(self) -> plist::Value { - plist::Value::Boolean(self) - } -} - -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()) - } -} - -impl PlistConvertible for &[T] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array( - self.iter() - .map(|item| item.clone().to_plist_value()) - .collect(), - ) - } -} - -impl PlistConvertible for [T; N] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect()) - } -} - -impl PlistConvertible for &[T; N] { - fn to_plist_value(self) -> plist::Value { - plist::Value::Array( - self.iter() - .map(|item| item.clone().to_plist_value()) - .collect(), - ) - } -} - -impl PlistConvertible for plist::Dictionary { - fn to_plist_value(self) -> plist::Value { - plist::Value::Dictionary(self) - } -} - -impl PlistConvertible for std::collections::HashMap -where - K: Into, - V: PlistConvertible, -{ - fn to_plist_value(self) -> plist::Value { - let mut dict = plist::Dictionary::new(); - for (key, value) in self { - dict.insert(key.into(), value.to_plist_value()); - } - plist::Value::Dictionary(dict) - } -} - -impl PlistConvertible for std::collections::BTreeMap -where - K: Into, - V: PlistConvertible, -{ - fn to_plist_value(self) -> plist::Value { - let mut dict = plist::Dictionary::new(); - for (key, value) in self { - dict.insert(key.into(), value.to_plist_value()); - } - plist::Value::Dictionary(dict) - } -} - -// 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 { - 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] - fn test_plist_macro_basic() { - let value = plist!({ - "name": "test", - "count": 42, - "active": true, - "items": ["a", ?"b", "c"] - }); - - if let plist::Value::Dictionary(dict) = value { - assert_eq!( - dict.get("name"), - Some(&plist::Value::String("test".to_string())) - ); - assert_eq!(dict.get("count"), Some(&plist::Value::Integer(42.into()))); - assert_eq!(dict.get("active"), Some(&plist::Value::Boolean(true))); - } else { - panic!("Expected dictionary"); - } - } - - #[test] - fn test_plist_macro_with_variables() { - 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, - "omit me":? none, - "keep me":? Some(123), - "replace me": 1, - :< to_merge, - :, OwnedInnerFileDescriptor; { let mut collected_bytes = Vec::with_capacity(n); for chunk in chunk_number(n, MAX_TRANSFER as usize) { - let header_payload = [self.fd.to_le_bytes(), chunk.to_le_bytes()].concat(); + let header_payload = [self.fd.to_le_bytes(), (chunk as u64).to_le_bytes()].concat(); let res = self .as_mut() .send_packet(AfcOpcode::Read, header_payload, Vec::new()) diff --git a/idevice/src/services/afc/mod.rs b/idevice/src/services/afc/mod.rs index f868585..8f8d71d 100644 --- a/idevice/src/services/afc/mod.rs +++ b/idevice/src/services/afc/mod.rs @@ -13,6 +13,7 @@ use tracing::warn; use crate::{ Idevice, IdeviceError, IdeviceService, afc::file::{FileDescriptor, OwnedFileDescriptor}, + lockdown::LockdownClient, obf, }; @@ -91,6 +92,43 @@ impl AfcClient { } } + /// Connects to afc2 from a provider + pub async fn new_afc2( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + + #[cfg(feature = "openssl")] + let legacy = lockdown + .get_value(Some("ProductVersion"), None) + .await + .ok() + .as_ref() + .and_then(|x| x.as_string()) + .and_then(|x| x.split(".").next()) + .and_then(|x| x.parse::().ok()) + .map(|x| x < 5) + .unwrap_or(false); + + #[cfg(not(feature = "openssl"))] + let legacy = false; + + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service(obf!("com.apple.afc2")).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?, legacy) + .await?; + } + + Self::from_stream(idevice).await + } + /// Lists the contents of a directory on the device /// /// # Arguments diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index 65f037b..792f250 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -1,5 +1,6 @@ // Jackson Coxson +use plist_macro::plist_to_xml_bytes; use serde::Deserialize; use tracing::warn; @@ -216,7 +217,7 @@ impl AppServiceClient { "user": { "active": true, }, - "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())), + "platformSpecificOptions": plist::Value::Data(plist_to_xml_bytes(&platform_options.unwrap_or_default())), }, }); diff --git a/idevice/src/services/lockdown.rs b/idevice/src/services/lockdown.rs index a6a4ea5..df147e4 100644 --- a/idevice/src/services/lockdown.rs +++ b/idevice/src/services/lockdown.rs @@ -260,6 +260,7 @@ impl LockdownClient { &mut self, host_id: impl Into, system_buid: impl Into, + host_name: Option<&str>, ) -> Result { let host_id = host_id.into(); let system_buid = system_buid.into(); @@ -297,6 +298,7 @@ impl LockdownClient { let req = crate::plist!({ "Label": self.idevice.label.clone(), "Request": "Pair", + "HostName":? host_name, "PairRecord": pair_record.clone(), "ProtocolVersion": "2", "PairingOptions": { @@ -326,6 +328,23 @@ impl LockdownClient { } } } + + /// Tell the device to enter recovery mode + pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> { + self.idevice + .send_plist(crate::plist!({ + "Request": "EnterRecovery" + })) + .await?; + + let res = self.idevice.read_plist().await?; + + if res.get("Request").and_then(|x| x.as_string()) == Some("EnterRecovery") { + Ok(()) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } } impl From for LockdownClient { diff --git a/idevice/src/services/mobile_image_mounter.rs b/idevice/src/services/mobile_image_mounter.rs index bb4c18b..1c2f1d1 100644 --- a/idevice/src/services/mobile_image_mounter.rs +++ b/idevice/src/services/mobile_image_mounter.rs @@ -90,7 +90,11 @@ impl ImageMounter { self.idevice.send_plist(req).await?; let res = self.idevice.read_plist().await?; - match res.get("ImageSignature") { + match res + .get("ImageSignature") + .and_then(|x| x.as_array()) + .and_then(|x| x.first()) + { Some(plist::Value::Data(signature)) => Ok(signature.clone()), _ => Err(IdeviceError::NotFound), } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index e795578..802b837 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -35,6 +35,8 @@ pub mod mobile_image_mounter; pub mod mobileactivationd; #[cfg(feature = "mobilebackup2")] pub mod mobilebackup2; +#[cfg(feature = "notification_proxy")] +pub mod notification_proxy; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; #[cfg(feature = "pcapd")] diff --git a/idevice/src/services/notification_proxy.rs b/idevice/src/services/notification_proxy.rs new file mode 100644 index 0000000..e389a43 --- /dev/null +++ b/idevice/src/services/notification_proxy.rs @@ -0,0 +1,212 @@ +//! iOS Device Notification Proxy Service +//! +//! Based on libimobiledevice's notification_proxy implementation +//! +//! Common notification identifiers: +//! Full list: include/libimobiledevice/notification_proxy.h +//! +//! - Notifications that can be sent (PostNotification): +//! - `com.apple.itunes-mobdev.syncWillStart` - Sync will start +//! - `com.apple.itunes-mobdev.syncDidStart` - Sync started +//! - `com.apple.itunes-mobdev.syncDidFinish` - Sync finished +//! - `com.apple.itunes-mobdev.syncLockRequest` - Request sync lock +//! +//! - Notifications that can be observed (ObserveNotification): +//! - `com.apple.itunes-client.syncCancelRequest` - Cancel sync request +//! - `com.apple.itunes-client.syncSuspendRequest` - Suspend sync +//! - `com.apple.itunes-client.syncResumeRequest` - Resume sync +//! - `com.apple.mobile.lockdown.phone_number_changed` - Phone number changed +//! - `com.apple.mobile.lockdown.device_name_changed` - Device name changed +//! - `com.apple.mobile.lockdown.timezone_changed` - Timezone changed +//! - `com.apple.mobile.lockdown.trusted_host_attached` - Trusted host attached +//! - `com.apple.mobile.lockdown.host_detached` - Host detached +//! - `com.apple.mobile.lockdown.host_attached` - Host attached +//! - `com.apple.mobile.lockdown.registration_failed` - Registration failed +//! - `com.apple.mobile.lockdown.activation_state` - Activation state +//! - `com.apple.mobile.lockdown.brick_state` - Brick state +//! - `com.apple.mobile.lockdown.disk_usage_changed` - Disk usage (iOS 4.0+) +//! - `com.apple.mobile.data_sync.domain_changed` - Data sync domain changed +//! - `com.apple.mobile.application_installed` - App installed +//! - `com.apple.mobile.application_uninstalled` - App uninstalled + +use std::pin::Pin; + +use futures::Stream; +use tracing::warn; + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; + +/// Client for interacting with the iOS notification proxy service +/// +/// The notification proxy service provides a mechanism to observe and post +/// system notifications. +/// +/// Use `observe_notification` to register for events, then `receive_notification` +/// to wait for them. +#[derive(Debug)] +pub struct NotificationProxyClient { + /// The underlying device connection with established notification_proxy service + pub idevice: Idevice, +} + +impl IdeviceService for NotificationProxyClient { + /// Returns the notification proxy service name as registered with lockdownd + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.mobile.notification_proxy") + } + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl NotificationProxyClient { + /// Creates a new notification proxy client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// Posts a notification to the device + /// + /// # Arguments + /// * `notification_name` - Name of the notification to post + /// + /// # Errors + /// Returns `IdeviceError` if the notification fails to send + pub async fn post_notification( + &mut self, + notification_name: impl Into, + ) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "PostNotification", + "Name": notification_name.into() + }); + self.idevice.send_plist(request).await + } + + /// Registers to observe a specific notification + /// + /// After calling this, use `receive_notification` to wait for events. + /// + /// # Arguments + /// * `notification_name` - Name of the notification to observe + /// + /// # Errors + /// Returns `IdeviceError` if the registration fails + pub async fn observe_notification( + &mut self, + notification_name: impl Into, + ) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "ObserveNotification", + "Name": notification_name.into() + }); + self.idevice.send_plist(request).await + } + + /// Registers to observe multiple notifications at once + /// + /// # Arguments + /// * `notification_names` - Slice of notification names to observe + /// + /// # Errors + /// Returns `IdeviceError` if any registration fails + pub async fn observe_notifications( + &mut self, + notification_names: &[&str], + ) -> Result<(), IdeviceError> { + for name in notification_names { + self.observe_notification(*name).await?; + } + Ok(()) + } + + /// Waits for and receives the next notification from the device + /// + /// # Returns + /// The name of the received notification + /// + /// # Errors + /// - `NotificationProxyDeath` if the proxy connection died + /// - `UnexpectedResponse` if the response format is invalid + pub async fn receive_notification(&mut self) -> Result { + let response = self.idevice.read_plist().await?; + + match response.get("Command").and_then(|c| c.as_string()) { + Some("RelayNotification") => match response.get("Name").and_then(|n| n.as_string()) { + Some(name) => Ok(name.to_string()), + None => Err(IdeviceError::UnexpectedResponse), + }, + Some("ProxyDeath") => { + warn!("NotificationProxy died!"); + Err(IdeviceError::NotificationProxyDeath) + } + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Waits for a notification with a timeout + /// + /// # Arguments + /// * `interval` - Timeout in seconds to wait for a notification + /// + /// # Returns + /// The name of the received notification + /// + /// # Errors + /// - `NotificationProxyDeath` if the proxy connection died + /// - `UnexpectedResponse` if the response format is invalid + /// - `HeartbeatTimeout` if no notification received before interval + pub async fn receive_notification_with_timeout( + &mut self, + interval: u64, + ) -> Result { + tokio::select! { + result = self.receive_notification() => result, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(interval)) => { + Err(IdeviceError::HeartbeatTimeout) + } + } + } + + /// Continuous stream of notifications. + pub fn into_stream( + mut self, + ) -> Pin> + Send>> { + Box::pin(async_stream::try_stream! { + loop { + let response = self.idevice.read_plist().await?; + + match response.get("Command").and_then(|c| c.as_string()) { + Some("RelayNotification") => { + match response.get("Name").and_then(|n| n.as_string()) { + Some(name) => yield name.to_string(), + None => Err(IdeviceError::UnexpectedResponse)?, + } + } + Some("ProxyDeath") => { + warn!("NotificationProxy died!"); + Err(IdeviceError::NotificationProxyDeath)?; + } + _ => Err(IdeviceError::UnexpectedResponse)?, + } + } + }) + } + + /// Shuts down the notification proxy connection + /// + /// # Errors + /// Returns `IdeviceError` if the shutdown command fails to send + pub async fn shutdown(&mut self) -> Result<(), IdeviceError> { + let request = crate::plist!({ + "Command": "Shutdown" + }); + self.idevice.send_plist(request).await?; + // Best-effort: wait for ProxyDeath ack + let _ = self.idevice.read_plist().await; + Ok(()) + } +} diff --git a/idevice/src/services/springboardservices.rs b/idevice/src/services/springboardservices.rs index 81a28b4..1fb98d7 100644 --- a/idevice/src/services/springboardservices.rs +++ b/idevice/src/services/springboardservices.rs @@ -3,7 +3,23 @@ //! Provides functionality for interacting with the SpringBoard services on iOS devices, //! which manages home screen and app icon related operations. -use crate::{Idevice, IdeviceError, IdeviceService, obf}; +use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds}; + +/// Orientation of the device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum InterfaceOrientation { + /// Orientation is unknown or cannot be determined + Unknown = 0, + /// Portrait mode (normal vertical) + Portrait = 1, + /// Portrait mode upside down + PortraitUpsideDown = 2, + /// Landscape with home button on the right (notch to the left) + LandscapeRight = 3, + /// Landscape with home button on the left (notch to the right) + LandscapeLeft = 4, +} /// Client for interacting with the iOS SpringBoard services /// @@ -70,4 +86,270 @@ impl SpringBoardServicesClient { _ => Err(IdeviceError::UnexpectedResponse), } } + + /// Retrieves the current icon state from the device + /// + /// The icon state contains the layout and organization of all apps on the home screen, + /// including folder structures and icon positions. This is a read-only operation. + /// + /// # Arguments + /// * `format_version` - Optional format version string for the icon state format + /// + /// # Returns + /// A plist Value containing the complete icon state structure + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// + /// # Example + /// ```rust + /// use idevice::services::springboardservices::SpringBoardServicesClient; + /// + /// let mut client = SpringBoardServicesClient::connect(&provider).await?; + /// let icon_state = client.get_icon_state(None).await?; + /// println!("Icon state: {:?}", icon_state); + /// ``` + /// + /// # Notes + /// This method successfully reads the home screen layout on all iOS versions. + pub async fn get_icon_state( + &mut self, + format_version: Option<&str>, + ) -> Result { + let req = crate::plist!({ + "command": "getIconState", + "formatVersion":? format_version, + }); + + self.idevice.send_plist(req).await?; + let mut res = self.idevice.read_plist_value().await?; + + // Some devices may return an error dictionary instead of icon state. + // Detect this and surface it as an UnexpectedResponse, similar to get_icon_pngdata. + if let plist::Value::Dictionary(ref dict) = res + && (dict.contains_key("error") || dict.contains_key("Error")) + { + return Err(IdeviceError::UnexpectedResponse); + } + + truncate_dates_to_seconds(&mut res); + + Ok(res) + } + + /// Sets the icon state on the device + /// + /// This method allows you to modify the home screen layout by providing a new icon state. + /// The icon state structure should match the format returned by `get_icon_state`. + /// + /// # Arguments + /// * `icon_state` - A plist Value containing the complete icon state structure + /// + /// # Returns + /// Ok(()) if the icon state was successfully set + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The icon state format is invalid + /// - The device rejects the new layout + /// + /// # Example + /// ```rust + /// use idevice::services::springboardservices::SpringBoardServicesClient; + /// + /// let mut client = SpringBoardServicesClient::connect(&provider).await?; + /// let mut icon_state = client.get_icon_state(None).await?; + /// + /// // Modify the icon state (e.g., swap two icons) + /// // ... modify icon_state ... + /// + /// client.set_icon_state(icon_state).await?; + /// println!("Icon state updated successfully"); + /// ``` + /// + /// # Notes + /// - Changes take effect immediately + /// - The device may validate the icon state structure before applying + /// - Invalid icon states will be rejected by the device + pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "command": "setIconState", + "iconState": icon_state, + }); + + self.idevice.send_plist(req).await?; + Ok(()) + } + + /// Sets the icon state with a specific format version + /// + /// This is similar to `set_icon_state` but allows specifying a format version. + /// + /// # Arguments + /// * `icon_state` - A plist Value containing the complete icon state structure + /// * `format_version` - Optional format version string + /// + /// # Returns + /// Ok(()) if the icon state was successfully set + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The icon state format is invalid + /// - The device rejects the new layout + pub async fn set_icon_state_with_version( + &mut self, + icon_state: plist::Value, + format_version: Option<&str>, + ) -> Result<(), IdeviceError> { + let req = crate::plist!({ + "command": "setIconState", + "iconState": icon_state, + "formatVersion":? format_version, + }); + + self.idevice.send_plist(req).await?; + Ok(()) + } + + /// Gets the home screen wallpaper preview as PNG data + /// + /// This gets a rendered preview of the home screen wallpaper. + /// + /// # Returns + /// The raw PNG data of the home screen wallpaper preview + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The device rejects the request + /// - The image is malformed/corupted + /// + /// # Example + /// ```rust + /// let wallpaper = client.get_home_screen_wallpaper_preview_pngdata().await?; + /// std::fs::write("home.png", wallpaper)?; + /// ``` + pub async fn get_home_screen_wallpaper_preview_pngdata( + &mut self, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "command": "getWallpaperPreviewImage", + "wallpaperName": "homescreen", + }); + self.idevice.send_plist(req).await?; + + let mut res = self.idevice.read_plist().await?; + match res.remove("pngData") { + Some(plist::Value::Data(res)) => Ok(res), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Gets the lock screen wallpaper preview as PNG data + /// + /// This gets a rendered preview of the lock screen wallpaper. + /// + /// # Returns + /// The raw PNG data of the lock screen wallpaper preview + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The device rejects the request + /// - The image is malformed/corupted + /// + /// # Example + /// ```rust + /// let wallpaper = client.get_lock_screen_wallpaper_preview_pngdata().await?; + /// std::fs::write("lock.png", wallpaper)?; + /// ``` + pub async fn get_lock_screen_wallpaper_preview_pngdata( + &mut self, + ) -> Result, IdeviceError> { + let req = crate::plist!({ + "command": "getWallpaperPreviewImage", + "wallpaperName": "lockscreen", + }); + self.idevice.send_plist(req).await?; + + let mut res = self.idevice.read_plist().await?; + match res.remove("pngData") { + Some(plist::Value::Data(res)) => Ok(res), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + /// Gets the current interface orientation of the device + /// + /// This gets which way the device is currently facing + /// + /// # Returns + /// The current `InterfaceOrientation` of the device + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The device doesn't support this command + /// - The response format is unexpected + /// + /// # Example + /// ```rust + /// let orientation = client.get_interface_orientation().await?; + /// println!("Device orientation: {:?}", orientation); + /// ``` + pub async fn get_interface_orientation( + &mut self, + ) -> Result { + let req = crate::plist!({ + "command": "getInterfaceOrientation", + }); + self.idevice.send_plist(req).await?; + + let res = self.idevice.read_plist().await?; + let orientation_value = res + .get("interfaceOrientation") + .and_then(|v| v.as_unsigned_integer()) + .ok_or(IdeviceError::UnexpectedResponse)?; + + let orientation = match orientation_value { + 1 => InterfaceOrientation::Portrait, + 2 => InterfaceOrientation::PortraitUpsideDown, + 3 => InterfaceOrientation::LandscapeRight, + 4 => InterfaceOrientation::LandscapeLeft, + _ => InterfaceOrientation::Unknown, + }; + + Ok(orientation) + } + + /// Gets the home screen icon layout metrics + /// + /// Returns icon spacing, size, and positioning information + /// + /// # Returns + /// A `plist::Dictionary` containing the icon layout metrics + /// + /// # Errors + /// Returns `IdeviceError` if: + /// - Communication fails + /// - The response is malformed + /// + /// # Example + /// ```rust + /// let metrics = client.get_homescreen_icon_metrics().await?; + /// println!("{:?}", metrics); + /// ``` + pub async fn get_homescreen_icon_metrics(&mut self) -> Result { + let req = crate::plist!({ + "command": "getHomeScreenIconMetrics", + }); + self.idevice.send_plist(req).await?; + + let res = self.idevice.read_plist().await?; + Ok(res) + } } diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs index 1e02e35..89de161 100644 --- a/idevice/src/tss.rs +++ b/idevice/src/tss.rs @@ -6,9 +6,10 @@ //! - Handle cryptographic signing operations use plist::Value; +use plist_macro::plist_to_xml_bytes; use tracing::{debug, warn}; -use crate::{IdeviceError, util::plist_to_xml_bytes}; +use crate::IdeviceError; /// TSS client version string sent in requests const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; @@ -30,7 +31,7 @@ impl TSSRequest { /// - Client version string /// - Random UUID for request identification pub fn new() -> Self { - let inner = crate::plist!(dict { + let inner = plist_macro::plist!(dict { "@HostPlatformInfo": "mac", "@VersionInfo": TSS_CLIENT_VERSION_STRING, "@UUID": uuid::Uuid::new_v4().to_string().to_uppercase() diff --git a/idevice/src/usbmuxd/raw_packet.rs b/idevice/src/usbmuxd/raw_packet.rs index 0a9aaba..7bc85a4 100644 --- a/idevice/src/usbmuxd/raw_packet.rs +++ b/idevice/src/usbmuxd/raw_packet.rs @@ -1,6 +1,6 @@ // Jackson Coxson -use crate::util::plist_to_xml_bytes; +use plist_macro::plist_to_xml_bytes; use tracing::warn; #[derive(Debug)] diff --git a/idevice/src/util.rs b/idevice/src/util.rs deleted file mode 100644 index 4e69565..0000000 --- a/idevice/src/util.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Utility Functions -//! -//! Provides helper functions for working with Apple's Property List (PLIST) format, -//! including serialization and pretty-printing utilities. -#![allow(dead_code)] // functions might not be used by all features - -use plist::Value; - -/// Converts a PLIST dictionary to XML-formatted bytes -/// -/// # Arguments -/// * `p` - The PLIST dictionary to serialize -/// -/// # Returns -/// A byte vector containing the XML representation -/// -/// # Panics -/// Will panic if serialization fails (should only happen with invalid data) -/// -/// # Example -/// ```rust -/// let mut dict = plist::Dictionary::new(); -/// dict.insert("key".into(), "value".into()); -/// let xml_bytes = plist_to_xml_bytes(&dict); -/// ``` -pub fn plist_to_xml_bytes(p: &plist::Dictionary) -> Vec { - let buf = Vec::new(); - let mut writer = std::io::BufWriter::new(buf); - plist::to_writer_xml(&mut writer, &p).unwrap(); - - writer.into_inner().unwrap() -} - -/// Pretty-prints a PLIST value with indentation -/// -/// # Arguments -/// * `p` - The PLIST value to format -/// -/// # Returns -/// A formatted string representation -pub fn pretty_print_plist(p: &Value) -> String { - print_plist(p, 0) -} - -/// Pretty-prints a PLIST dictionary with key-value pairs -/// -/// # Arguments -/// * `dict` - The dictionary to format -/// -/// # Returns -/// A formatted string representation with newlines and indentation -/// -/// # Example -/// ```rust -/// let mut dict = plist::Dictionary::new(); -/// dict.insert("name".into(), "John".into()); -/// dict.insert("age".into(), 30.into()); -/// println!("{}", pretty_print_dictionary(&dict)); -/// ``` -pub fn pretty_print_dictionary(dict: &plist::Dictionary) -> String { - let items: Vec = dict - .iter() - .map(|(k, v)| format!("{}: {}", k, print_plist(v, 2))) - .collect(); - format!("{{\n{}\n}}", items.join(",\n")) -} - -/// Internal recursive function for printing PLIST values with indentation -/// -/// # Arguments -/// * `p` - The PLIST value to format -/// * `indentation` - Current indentation level -/// -/// # Returns -/// Formatted string representation -fn print_plist(p: &Value, indentation: usize) -> String { - let indent = " ".repeat(indentation); - match p { - Value::Array(vec) => { - let items: Vec = vec - .iter() - .map(|v| { - format!( - "{}{}", - " ".repeat(indentation + 2), - print_plist(v, indentation + 2) - ) - }) - .collect(); - format!("[\n{}\n{}]", items.join(",\n"), indent) - } - Value::Dictionary(dict) => { - let items: Vec = dict - .iter() - .map(|(k, v)| { - format!( - "{}{}: {}", - " ".repeat(indentation + 2), - k, - print_plist(v, indentation + 2) - ) - }) - .collect(); - format!("{{\n{}\n{}}}", items.join(",\n"), indent) - } - Value::Boolean(b) => format!("{b}"), - Value::Data(vec) => { - let len = vec.len(); - let preview: String = vec - .iter() - .take(20) - .map(|b| format!("{b:02X}")) - .collect::>() - .join(" "); - if len > 20 { - format!("Data({preview}... Len: {len})") - } else { - 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::Uid(_uid) => "Uid(?)".to_string(), - _ => "Unknown".to_string(), - } -} - -#[macro_export] -macro_rules! obf { - ($lit:literal) => {{ - #[cfg(feature = "obfuscate")] - { - std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string()) - } - #[cfg(not(feature = "obfuscate"))] - { - std::borrow::Cow::Borrowed($lit) - } - }}; -} diff --git a/idevice/src/utils/installation/helpers.rs b/idevice/src/utils/installation/helpers.rs index f88ce0d..8e6057f 100644 --- a/idevice/src/utils/installation/helpers.rs +++ b/idevice/src/utils/installation/helpers.rs @@ -1,13 +1,12 @@ -use std::{io::Cursor, path::Path}; - use async_zip::base::read::seek::ZipFileReader; use futures::AsyncReadExt as _; +use plist_macro::plist; +use std::{io::Cursor, path::Path}; use tokio::io::{AsyncBufRead, AsyncSeek, BufReader}; use crate::{ IdeviceError, IdeviceService, afc::{AfcClient, opcode::AfcFopenMode}, - plist, provider::IdeviceProvider, }; diff --git a/idevice/src/utils/mod.rs b/idevice/src/utils/mod.rs index d30df75..ded61d4 100644 --- a/idevice/src/utils/mod.rs +++ b/idevice/src/utils/mod.rs @@ -2,3 +2,5 @@ #[cfg(all(feature = "afc", feature = "installation_proxy"))] pub mod installation; + +pub mod plist; diff --git a/idevice/src/utils/plist.rs b/idevice/src/utils/plist.rs new file mode 100644 index 0000000..fb763ac --- /dev/null +++ b/idevice/src/utils/plist.rs @@ -0,0 +1,155 @@ +/// Utilities for working with plist values +/// +/// Truncates all Date values in a plist structure to second precision. +/// +/// This function recursively walks through a plist Value and truncates any Date values +/// from nanosecond precision to second precision. This is necessary for compatibility +/// with iOS devices that reject high-precision date formats. +/// +/// # Arguments +/// * `value` - The plist Value to normalize (modified in place) +/// +/// # Example +/// ```rust,no_run +/// use idevice::utils::plist::truncate_dates_to_seconds; +/// use plist::Value; +/// +/// let mut icon_state = Value::Array(vec![]); +/// truncate_dates_to_seconds(&mut icon_state); +/// ``` +/// +/// # Details +/// - Converts dates from format: `2026-01-17T03:09:58.332738876Z` (nanosecond precision) +/// - To format: `2026-01-17T03:09:58Z` (second precision) +/// - Recursively processes Arrays and Dictionaries +/// - Other value types are left unchanged +pub fn truncate_dates_to_seconds(value: &mut plist::Value) { + match value { + plist::Value::Date(date) => { + let xml_string = date.to_xml_format(); + if let Some(dot_pos) = xml_string.find('.') + && xml_string[dot_pos..].contains('Z') + { + let truncated_string = format!("{}Z", &xml_string[..dot_pos]); + if let Ok(new_date) = plist::Date::from_xml_format(&truncated_string) { + *date = new_date; + } + } + } + plist::Value::Array(arr) => { + for item in arr.iter_mut() { + truncate_dates_to_seconds(item); + } + } + plist::Value::Dictionary(dict) => { + for (_, v) in dict.iter_mut() { + truncate_dates_to_seconds(v); + } + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_truncate_date_with_nanoseconds() { + let date_str = "2026-01-17T03:09:58.332738876Z"; + let date = plist::Date::from_xml_format(date_str).unwrap(); + let mut value = plist::Value::Date(date); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Date(truncated_date) = value { + let result = truncated_date.to_xml_format(); + assert!( + !result.contains('.'), + "Date should not contain fractional seconds" + ); + assert!(result.ends_with('Z'), "Date should end with Z"); + assert!( + result.starts_with("2026-01-17T03:09:58"), + "Date should preserve main timestamp" + ); + } else { + panic!("Value should still be a Date"); + } + } + + #[test] + fn test_truncate_date_already_truncated() { + let date_str = "2026-01-17T03:09:58Z"; + let date = plist::Date::from_xml_format(date_str).unwrap(); + let original_format = date.to_xml_format(); + let mut value = plist::Value::Date(date); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Date(truncated_date) = value { + let result = truncated_date.to_xml_format(); + assert_eq!( + result, original_format, + "Already truncated date should remain unchanged" + ); + } + } + + #[test] + fn test_truncate_dates_in_array() { + let date1 = plist::Date::from_xml_format("2026-01-17T03:09:58.123456Z").unwrap(); + let date2 = plist::Date::from_xml_format("2026-01-18T04:10:59.987654Z").unwrap(); + let mut value = + plist::Value::Array(vec![plist::Value::Date(date1), plist::Value::Date(date2)]); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Array(arr) = value { + for item in arr { + if let plist::Value::Date(date) = item { + let formatted = date.to_xml_format(); + assert!( + !formatted.contains('.'), + "Dates in array should be truncated" + ); + } + } + } + } + + #[test] + fn test_truncate_dates_in_dictionary() { + let date = plist::Date::from_xml_format("2026-01-17T03:09:58.999999Z").unwrap(); + let mut dict = plist::Dictionary::new(); + dict.insert("timestamp".to_string(), plist::Value::Date(date)); + let mut value = plist::Value::Dictionary(dict); + + truncate_dates_to_seconds(&mut value); + + if let plist::Value::Dictionary(dict) = value + && let Some(plist::Value::Date(date)) = dict.get("timestamp") + { + let formatted = date.to_xml_format(); + assert!( + !formatted.contains('.'), + "Date in dictionary should be truncated" + ); + } + } + + #[test] + fn test_other_value_types_unchanged() { + let mut string_val = plist::Value::String("test".to_string()); + let mut int_val = plist::Value::Integer(42.into()); + let mut bool_val = plist::Value::Boolean(true); + + truncate_dates_to_seconds(&mut string_val); + truncate_dates_to_seconds(&mut int_val); + truncate_dates_to_seconds(&mut bool_val); + + assert!(matches!(string_val, plist::Value::String(_))); + assert!(matches!(int_val, plist::Value::Integer(_))); + assert!(matches!(bool_val, plist::Value::Boolean(_))); + } +} diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index f280656..5803be5 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -1,3 +1,4 @@ +use plist_macro::plist; use std::{ ffi::CString, io::{BufRead, Cursor, Read}, @@ -169,7 +170,7 @@ impl XPCObject { plist::Value::Dictionary(dict) } Self::FileTransfer { msg_id, data } => { - crate::plist!({ + plist!({ "msg_id": *msg_id, "data": data.to_plist(), }) diff --git a/justfile b/justfile index 9e5d593..76a6cad 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ check-features: 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 +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 @@ -40,6 +40,9 @@ xcframework: apple-build lipo -create -output swift/libs/idevice-ios-sim.a \ target/aarch64-apple-ios-sim/release/libidevice_ffi.a \ target/x86_64-apple-ios/release/libidevice_ffi.a + lipo -create -output swift/libs/idevice-maccatalyst.a \ + target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \ + target/x86_64-apple-ios-macabi/release/libidevice_ffi.a lipo -create -output swift/libs/idevice-macos.a \ target/aarch64-apple-darwin/release/libidevice_ffi.a \ target/x86_64-apple-darwin/release/libidevice_ffi.a @@ -48,8 +51,9 @@ xcframework: apple-build -library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \ -library swift/libs/idevice-ios-sim.a -headers swift/include \ -library swift/libs/idevice-macos.a -headers swift/include \ + -library swift/libs/idevice-maccatalyst.a -headers swift/include \ -output swift/IDevice.xcframework - + zip -r swift/bundle.zip swift/IDevice.xcframework openssl dgst -sha256 swift/bundle.zip @@ -57,6 +61,7 @@ xcframework: apple-build apple-build: # requires a Mac # iOS device build BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphoneos --show-sdk-path)" \ + IPHONEOS_DEPLOYMENT_TARGET=17.0 \ cargo build --release --target aarch64-apple-ios --features obfuscate # iOS Simulator (arm64) @@ -67,7 +72,16 @@ apple-build: # requires a Mac BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \ cargo build --release --target x86_64-apple-ios + # Mac Catalyst (arm64) + # AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring. + BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \ + cargo build --release --target aarch64-apple-ios-macabi --no-default-features --features "ring full" + + # Mac Catalyst (x86_64) + # AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring. + BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \ + cargo build --release --target x86_64-apple-ios-macabi --no-default-features --features "ring full" + # macOS (native) – no special env needed cargo build --release --target aarch64-apple-darwin cargo build --release --target x86_64-apple-darwin - diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 78c88a4..781d2c8 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -2,80 +2,13 @@ name = "idevice-tools" description = "Rust binary tools to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.0" +version = "0.1.53" edition = "2024" license = "MIT" documentation = "https://docs.rs/idevice" repository = "https://github.com/jkcoxson/idevice" keywords = ["lockdownd", "ios"] - -# [[bin]] -# name = "ideviceinfo" -# path = "src/ideviceinfo.rs" -# -# [[bin]] -# name = "heartbeat_client" -# path = "src/heartbeat_client.rs" -# -# [[bin]] -# 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 = "idevice_id" -# path = "src/idevice_id.rs" -# -# [[bin]] -# name = "process_control" -# path = "src/process_control.rs" -# -# [[bin]] -# name = "dvt_packet_parser" -# path = "src/dvt_packet_parser.rs" -# -# [[bin]] -# name = "remotexpc" -# path = "src/remotexpc.rs" -# -# [[bin]] -# name = "debug_proxy" -# path = "src/debug_proxy.rs" -# -# [[bin]] -# name = "misagent" -# path = "src/misagent.rs" -# -# [[bin]] -# name = "location_simulation" -# path = "src/location_simulation.rs" -# -# [[bin]] -# name = "afc" -# path = "src/afc.rs" -# -# [[bin]] -# name = "crash_logs" -# path = "src/crash_logs.rs" -# -# [[bin]] -# name = "amfi" -# path = "src/amfi.rs" -# -# [[bin]] -# name = "pair" -# path = "src/pair.rs" +default-run = "idevice-tools" [[bin]] name = "pair_apple_tv" @@ -86,74 +19,16 @@ name = "pair_rsd_ios" path = "src/pair_rsd_ios.rs" # [[bin]] -# name = "syslog_relay" -# path = "src/syslog_relay.rs" -# -# [[bin]] -# name = "os_trace_relay" -# path = "src/os_trace_relay.rs" -# -# [[bin]] -# name = "app_service" -# path = "src/app_service.rs" -# -# [[bin]] -# name = "lockdown" -# path = "src/lockdown.rs" -# -# [[bin]] -# name = "restore_service" -# path = "src/restore_service.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" -# -# -# [[bin]] -# name = "screenshot" -# path = "src/screenshot.rs" -# -# [[bin]] -# name = "activation" -# path = "src/activation.rs" -# -# [[bin]] -# name = "notifications" -# path = "src/notifications.rs" -# -# -# [[bin]] -# name = "installcoordination_proxy" -# path = "src/installcoordination_proxy.rs" -# -# [[bin]] -# name = "iproxy" -# path = "src/iproxy.rs" +# name = "core_device_proxy_tun" +# path = "src/core_device_proxy_tun.rs" + +[[bin]] +name = "idevice_id" +path = "src/idevice_id.rs" + +[[bin]] +name = "iproxy" +path = "src/iproxy.rs" [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } @@ -164,7 +39,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } sha2 = { version = "0.10" } ureq = { version = "3" } clap = { version = "4.5" } +jkcli = { version = "0.1" } plist = { version = "1.7" } +plist-macro = { version = "0.1.3" } ns-keyed-archive = "0.1.2" uuid = "1.16" futures-util = { version = "0.3" } diff --git a/tools/src/activation.rs b/tools/src/activation.rs index 86f65e5..f9dee47 100644 --- a/tools/src/activation.rs +++ b/tools/src/activation.rs @@ -1,65 +1,23 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("activation") - .about("mobileactivationd") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage activation status on an iOS device") + .with_subcommand("state", JkCommand::new().help("Gets the activation state")) + .with_subcommand( + "deactivate", + JkCommand::new().help("Deactivates 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("state").about("Gets the activation state")) - .subcommand(Command::new("deactivate").about("Deactivates the device")) - .get_matches(); - - if matches.get_flag("about") { - println!("activation - activate the device"); - 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, "activation-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let activation_client = MobileActivationdClient::new(&*provider); let mut lc = LockdownClient::connect(&*provider) .await @@ -74,40 +32,19 @@ async fn main() { .into_string() .unwrap(); - if matches.subcommand_matches("state").is_some() { - let s = activation_client.state().await.expect("no state"); - println!("Activation State: {s}"); - } else if matches.subcommand_matches("deactivate").is_some() { - println!("CAUTION: You are deactivating {udid}, press enter to continue."); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).ok(); - activation_client.deactivate().await.expect("no deactivate"); - // } else if matches.subcommand_matches("accept").is_some() { - // amfi_client - // .accept_developer_mode() - // .await - // .expect("Failed to show"); - // } else if matches.subcommand_matches("status").is_some() { - // let status = amfi_client - // .get_developer_mode_status() - // .await - // .expect("Failed to get status"); - // println!("Enabled: {status}"); - // } else if let Some(matches) = matches.subcommand_matches("state") { - // let uuid: &String = match matches.get_one("uuid") { - // Some(u) => u, - // None => { - // eprintln!("No UUID passed. Invalid usage, pass -h for help"); - // return; - // } - // }; - // let status = amfi_client - // .trust_app_signer(uuid) - // .await - // .expect("Failed to get state"); - // println!("Enabled: {status}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, _sub_args) = arguments.first_subcommand().expect("no subarg passed"); + + match sub_name.as_str() { + "state" => { + let s = activation_client.state().await.expect("no state"); + println!("Activation State: {s}"); + } + "deactivate" => { + println!("CAUTION: You are deactivating {udid}, press enter to continue."); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + activation_client.deactivate().await.expect("no deactivate"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/afc.rs b/tools/src/afc.rs index e1fbb5f..f08719b 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -2,130 +2,119 @@ use std::path::PathBuf; -use clap::{Arg, Command, value_parser}; use idevice::{ IdeviceService, afc::{AfcClient, opcode::AfcFopenMode}, house_arrest::HouseArrestClient, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -mod common; +const DOCS_HELP: &str = "Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents"; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("afc") - .about("Manage files on the device") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_flag( + JkFlag::new("documents") + .with_help(DOCS_HELP) + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_flag( + JkFlag::new("container") + .with_help("Read the container contents of a bundle") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("udid") - .long("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), - ) - .arg( - Arg::new("documents") - .long("documents") - .value_name("BUNDLE_ID") - .help("Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents") - .global(true), - ) - .arg( - Arg::new("container") - .long("container") - .value_name("BUNDLE_ID") - .help("Read the container contents of a bundle") - .global(true), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("list") - .about("Lists the items in the directory") - .arg(Arg::new("path").required(true).index(1)), - ) - .subcommand( - Command::new("download") - .about("Downloads a file") - .arg(Arg::new("path").required(true).index(1)) - .arg(Arg::new("save").required(true).index(2)), - ) - .subcommand( - Command::new("upload") - .about("Creates a directory") - .arg( - Arg::new("file") + .with_subcommand( + "list", + JkCommand::new() + .help("Lists the items in the directory") + .with_argument( + JkArgument::new() .required(true) - .index(1) - .value_parser(value_parser!(PathBuf)), + .with_help("The directory to list in"), + ), + ) + .with_subcommand( + "download", + JkCommand::new() + .help("Download a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path in the AFC jail"), ) - .arg(Arg::new("path").required(true).index(2)), + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to save file to"), + ), ) - .subcommand( - Command::new("mkdir") - .about("Creates a directory") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "upload", + JkCommand::new() + .help("Upload a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to upload"), + ) + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to save file to in the AFC jail"), + ), ) - .subcommand( - Command::new("remove") - .about("Remove a provisioning profile") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "mkdir", + JkCommand::new().help("Create a folder").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the folder to create in the AFC jail"), + ), ) - .subcommand( - Command::new("remove_all") - .about("Remove a provisioning profile") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "remove", + JkCommand::new().help("Remove a file").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to remove"), + ), ) - .subcommand( - Command::new("info") - .about("Get info about a file") - .arg(Arg::new("path").required(true).index(1)), + .with_subcommand( + "remove_all", + JkCommand::new().help("Remove a folder").with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the folder to remove"), + ), ) - .subcommand(Command::new("device_info").about("Get info about the device")) - .get_matches(); + .with_subcommand( + "info", + JkCommand::new() + .help("Get info about a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the file to get info for"), + ), + ) + .with_subcommand( + "device_info", + JkCommand::new().help("Get info about the device"), + ) + .subcommand_required(true) +} - if matches.get_flag("about") { - println!("afc"); - 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, "afc-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; - - let mut afc_client = if let Some(bundle_id) = matches.get_one::("container") { +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut afc_client = if let Some(bundle_id) = arguments.get_flag::("container") { let h = HouseArrestClient::connect(&*provider) .await .expect("Failed to connect to house arrest"); h.vend_container(bundle_id) .await .expect("Failed to vend container") - } else if let Some(bundle_id) = matches.get_one::("documents") { + } else if let Some(bundle_id) = arguments.get_flag::("documents") { let h = HouseArrestClient::connect(&*provider) .await .expect("Failed to connect to house arrest"); @@ -138,59 +127,72 @@ async fn main() { .expect("Unable to connect to misagent") }; - if let Some(matches) = matches.subcommand_matches("list") { - let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client.list_dir(path).await.expect("Failed to read dir"); - println!("{path}\n{res:#?}"); - } else if let Some(matches) = matches.subcommand_matches("mkdir") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.mk_dir(path).await.expect("Failed to mkdir"); - } else if let Some(matches) = matches.subcommand_matches("download") { - let path = matches.get_one::("path").expect("No path passed"); - let save = matches.get_one::("save").expect("No path passed"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + match sub_name.as_str() { + "list" => { + let path = sub_args.next_argument::().expect("No path passed"); + let res = afc_client + .list_dir(&path) + .await + .expect("Failed to read dir"); + println!("{path}\n{res:#?}"); + } + "mkdir" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.mk_dir(path).await.expect("Failed to mkdir"); + } + "download" => { + let path = sub_args.next_argument::().expect("No path passed"); + let save = sub_args.next_argument::().expect("No path passed"); - let mut file = afc_client - .open(path, AfcFopenMode::RdOnly) - .await - .expect("Failed to open"); + let mut file = afc_client + .open(path, AfcFopenMode::RdOnly) + .await + .expect("Failed to open"); - let res = file.read_entire().await.expect("Failed to read"); - tokio::fs::write(save, res) - .await - .expect("Failed to write to file"); - } else if let Some(matches) = matches.subcommand_matches("upload") { - let file = matches.get_one::("file").expect("No path passed"); - let path = matches.get_one::("path").expect("No path passed"); + let res = file.read_entire().await.expect("Failed to read"); + tokio::fs::write(save, res) + .await + .expect("Failed to write to file"); + } + "upload" => { + let file = sub_args.next_argument::().expect("No path passed"); + let path = sub_args.next_argument::().expect("No path passed"); - let bytes = tokio::fs::read(file).await.expect("Failed to read file"); - let mut file = afc_client - .open(path, AfcFopenMode::WrOnly) - .await - .expect("Failed to open"); + let bytes = tokio::fs::read(file).await.expect("Failed to read file"); + let mut file = afc_client + .open(path, AfcFopenMode::WrOnly) + .await + .expect("Failed to open"); - file.write_entire(&bytes) - .await - .expect("Failed to upload bytes"); - } else if let Some(matches) = matches.subcommand_matches("remove") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.remove(path).await.expect("Failed to remove"); - } else if let Some(matches) = matches.subcommand_matches("remove_all") { - let path = matches.get_one::("path").expect("No path passed"); - afc_client.remove_all(path).await.expect("Failed to remove"); - } else if let Some(matches) = matches.subcommand_matches("info") { - let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client - .get_file_info(path) - .await - .expect("Failed to get file info"); - println!("{res:#?}"); - } else if matches.subcommand_matches("device_info").is_some() { - let res = afc_client - .get_device_info() - .await - .expect("Failed to get file info"); - println!("{res:#?}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + file.write_entire(&bytes) + .await + .expect("Failed to upload bytes"); + } + "remove" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.remove(path).await.expect("Failed to remove"); + } + "remove_all" => { + let path = sub_args.next_argument::().expect("No path passed"); + afc_client.remove_all(path).await.expect("Failed to remove"); + } + "info" => { + let path = sub_args.next_argument::().expect("No path passed"); + let res = afc_client + .get_file_info(path) + .await + .expect("Failed to get file info"); + println!("{res:#?}"); + } + "device_info" => { + let res = afc_client + .get_device_info() + .await + .expect("Failed to get file info"); + println!("{res:#?}"); + } + _ => unreachable!(), } } diff --git a/tools/src/amfi.rs b/tools/src/amfi.rs index bff4847..432449e 100644 --- a/tools/src/amfi.rs +++ b/tools/src/amfi.rs @@ -1,109 +1,81 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, amfi::AmfiClient}; +use idevice::{IdeviceService, amfi::AmfiClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("amfi") - .about("Mess with developer mode") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Mess with devleoper mode") + .with_subcommand( + "show", + JkCommand::new().help("Shows the developer mode option in settings"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand("enable", JkCommand::new().help("Enables developer mode")) + .with_subcommand( + "accept", + JkCommand::new().help("Shows the accept dialogue for developer mode"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "status", + JkCommand::new().help("Gets the developer mode status"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), + .with_subcommand( + "trust", + JkCommand::new() + .help("Trusts an app signer") + .with_argument(JkArgument::new().with_help("UUID").required(true)), ) - .subcommand(Command::new("show").about("Shows the developer mode option in settings")) - .subcommand(Command::new("enable").about("Enables developer mode")) - .subcommand(Command::new("accept").about("Shows the accept dialogue for developer mode")) - .subcommand(Command::new("status").about("Gets the developer mode status")) - .subcommand( - Command::new("trust") - .about("Trusts an app signer") - .arg(Arg::new("uuid").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("amfi - manage developer mode"); - 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; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut amfi_client = AmfiClient::connect(&*provider) .await .expect("Failed to connect to amfi"); - if matches.subcommand_matches("show").is_some() { - amfi_client - .reveal_developer_mode_option_in_ui() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("enable").is_some() { - amfi_client - .enable_developer_mode() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("accept").is_some() { - amfi_client - .accept_developer_mode() - .await - .expect("Failed to show"); - } else if matches.subcommand_matches("status").is_some() { - let status = amfi_client - .get_developer_mode_status() - .await - .expect("Failed to get status"); - println!("Enabled: {status}"); - } else if let Some(matches) = matches.subcommand_matches("state") { - let uuid: &String = match matches.get_one("uuid") { - Some(u) => u, - None => { - eprintln!("No UUID passed. Invalid usage, pass -h for help"); - return; - } - }; - let status = amfi_client - .trust_app_signer(uuid) - .await - .expect("Failed to get state"); - println!("Enabled: {status}"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "show" => { + amfi_client + .reveal_developer_mode_option_in_ui() + .await + .expect("Failed to show"); + } + "enable" => { + amfi_client + .enable_developer_mode() + .await + .expect("Failed to show"); + } + "accept" => { + amfi_client + .accept_developer_mode() + .await + .expect("Failed to show"); + } + "status" => { + let status = amfi_client + .get_developer_mode_status() + .await + .expect("Failed to get status"); + println!("Enabled: {status}"); + } + "trust" => { + let uuid: String = match sub_args.next_argument() { + Some(u) => u, + None => { + eprintln!("No UUID passed. Invalid usage, pass -h for help"); + return; + } + }; + let status = amfi_client + .trust_app_signer(uuid) + .await + .expect("Failed to get state"); + println!("Enabled: {status}"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index c286e4e..5cc5032 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -1,111 +1,72 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device::{AppServiceClient, OpenStdioSocketClient}, core_device_proxy::CoreDeviceProxy, + provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .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), - ) - .subcommand(Command::new("list").about("Lists the images mounted on the device")) - .subcommand( - Command::new("launch") - .about("Launch the app on the device") - .arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to launch"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the RemoteXPC app service on the device") + .with_subcommand("list", JkCommand::new().help("List apps on the device")) + .with_subcommand( + "launch", + JkCommand::new() + .help("Launch an app on the device") + .with_argument( + JkArgument::new() + .with_help("Bundle ID to launch") + .required(true), ), ) - .subcommand(Command::new("processes").about("List the processes running")) - .subcommand( - Command::new("uninstall").about("Uninstall an app").arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to uninstall"), + .with_subcommand( + "processes", + JkCommand::new().help("List the processes running"), + ) + .with_subcommand( + "uninstall", + JkCommand::new().help("Uninstall an app").with_argument( + JkArgument::new() + .with_help("Bundle ID to uninstall") + .required(true), ), ) - .subcommand( - Command::new("signal") - .about("Send a signal to an app") - .arg(Arg::new("pid").required(true).help("PID to send to")) - .arg(Arg::new("signal").required(true).help("Signal to send")), + .with_subcommand( + "signal", + JkCommand::new() + .help("Uninstall an app") + .with_argument(JkArgument::new().with_help("PID to signal").required(true)) + .with_argument(JkArgument::new().with_help("Signal to send").required(true)), ) - .subcommand( - Command::new("icon") - .about("Send a signal to an app") - .arg( - Arg::new("bundle_id") - .required(true) - .help("The bundle ID to fetch"), + .with_subcommand( + "icon", + JkCommand::new() + .help("Fetch an icon for an app") + .with_argument( + JkArgument::new() + .with_help("Bundle ID for the app") + .required(true), ) - .arg( - Arg::new("path") - .required(true) - .help("The path to save the icon to"), + .with_argument( + JkArgument::new() + .with_help("Path to save it to") + .required(true), ) - .arg(Arg::new("hw").required(false).help("The height and width")) - .arg(Arg::new("scale").required(false).help("The scale")), + .with_argument( + JkArgument::new() + .with_help("Height and width") + .required(true), + ) + .with_argument(JkArgument::new().with_help("Scale").required(true)), ) - .get_matches(); + .subcommand_required(true) +} - 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, "app_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -123,121 +84,122 @@ async fn main() { .await .expect("no connect"); - if matches.subcommand_matches("list").is_some() { - let apps = asc - .list_apps(true, true, true, true, true) - .await - .expect("Failed to get apps"); - println!("{apps:#?}"); - } else if let Some(matches) = matches.subcommand_matches("launch") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); + let mut sub_args = sub_args.clone(); - 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, &[], true, false, None, None, Some(stdio_uuid)) - .await - .expect("no launch"); - - 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); + match sub_name.as_str() { + "list" => { + let apps = asc + .list_apps(true, true, true, true, true) + .await + .expect("Failed to get apps"); + println!("{apps:#?}"); + } + "launch" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; } - 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); + }; + + 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, &[], true, false, None, None, Some(stdio_uuid)) + .await + .expect("no launch"); + + 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."); + } + // 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."); } - println!("\nLocal stdin closed."); - return; } } - } else if matches.subcommand_matches("processes").is_some() { - let p = asc.list_processes().await.expect("no processes?"); - println!("{p:#?}"); - } else if let Some(matches) = matches.subcommand_matches("uninstall") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + "processes" => { + let p = asc.list_processes().await.expect("no processes?"); + println!("{p:#?}"); + } + "uninstall" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; - asc.uninstall_app(bundle_id).await.expect("no launch") - } else if let Some(matches) = matches.subcommand_matches("signal") { - let pid: u32 = match matches.get_one::("pid") { - Some(b) => b.parse().expect("failed to parse PID as u32"), - None => { - eprintln!("No bundle PID passed"); - return; - } - }; - let signal: u32 = match matches.get_one::("signal") { - Some(b) => b.parse().expect("failed to parse signal as u32"), - None => { - eprintln!("No bundle signal passed"); - return; - } - }; + asc.uninstall_app(bundle_id).await.expect("no launch") + } + "signal" => { + let pid: u32 = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle PID passed"); + return; + } + }; + let signal: u32 = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle signal passed"); + return; + } + }; - let res = asc.send_signal(pid, signal).await.expect("no signal"); - println!("{res:#?}"); - } else if let Some(matches) = matches.subcommand_matches("icon") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; - let save_path: &String = match matches.get_one("path") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; - let hw: f32 = match matches.get_one::("hw") { - Some(b) => b.parse().expect("failed to parse PID as f32"), - None => 1.0, - }; - let scale: f32 = match matches.get_one::("scale") { - Some(b) => b.parse().expect("failed to parse signal as f32"), - None => 1.0, - }; + let res = asc.send_signal(pid, signal).await.expect("no signal"); + println!("{res:#?}"); + } + "icon" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let save_path: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + let hw: f32 = sub_args.next_argument().unwrap_or(1.0); + let scale: f32 = sub_args.next_argument().unwrap_or(1.0); - let res = asc - .fetch_app_icon(bundle_id, hw, hw, scale, true) - .await - .expect("no signal"); - println!("{res:?}"); - tokio::fs::write(save_path, res.data) - .await - .expect("failed to save"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let res = asc + .fetch_app_icon(bundle_id, hw, hw, scale, true) + .await + .expect("no signal"); + println!("{res:?}"); + tokio::fs::write(save_path, res.data) + .await + .expect("failed to save"); + } + _ => unreachable!(), } } diff --git a/tools/src/bt_packet_logger.rs b/tools/src/bt_packet_logger.rs index 7529200..346f115 100644 --- a/tools/src/bt_packet_logger.rs +++ b/tools/src/bt_packet_logger.rs @@ -1,71 +1,20 @@ // Jackson Coxson -use clap::{Arg, Command}; use futures_util::StreamExt; -use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient}; +use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use tokio::io::AsyncWrite; use crate::pcap::{write_pcap_header, write_pcap_record}; -mod common; -mod pcap; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Writes Bluetooth pcap data") + .with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)")) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let out: Option = arguments.clone().next_argument(); let logger_client = BtPacketLoggerClient::connect(&*provider) .await diff --git a/tools/src/companion_proxy.rs b/tools/src/companion_proxy.rs index f37f345..eec9699 100644 --- a/tools/src/companion_proxy.rs +++ b/tools/src/companion_proxy.rs @@ -1,83 +1,60 @@ // 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, + core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; +use plist_macro::{pretty_print_dictionary, pretty_print_plist}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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; - } - }; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Apple Watch proxy") + .with_subcommand( + "list", + JkCommand::new().help("List the companions on the device"), + ) + .with_subcommand("listen", JkCommand::new().help("Listen for devices")) + .with_subcommand( + "get", + JkCommand::new() + .help("Gets a value from an AW") + .with_argument( + JkArgument::new() + .with_help("The AW UDID to get from") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("The value to get") + .required(true), + ), + ) + .with_subcommand( + "start", + JkCommand::new() + .help("Starts a service on the Apple Watch") + .with_argument( + JkArgument::new() + .with_help("The port to listen on") + .required(true), + ) + .with_argument(JkArgument::new().with_help("The service name")), + ) + .with_subcommand( + "stop", + JkCommand::new() + .help("Stops a service on the Apple Watch") + .with_argument( + JkArgument::new() + .with_help("The port to stop") + .required(true), + ), + ) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core_device_proxy"); @@ -97,55 +74,72 @@ async fn main() { // .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"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); - match proxy.get_value(udid, key).await { - Ok(value) => { - println!("{}", pretty_print_plist(&value)); - } - Err(e) => { - eprintln!("Error getting value: {e}"); + match sub_name.as_str() { + "list" => { + proxy.get_device_registry().await.expect("Failed to show"); + } + "listen" => { + 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("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()); + "get" => { + let key: String = sub_args.next_argument::().expect("no value passed"); + let udid = sub_args + .next_argument::() + .expect("no AW udid passed"); - match proxy.start_forwarding_service_port(port, name, None).await { - Ok(value) => { - println!("started on port {value}"); + match proxy.get_value(udid, key).await { + Ok(value) => { + println!("{}", pretty_print_plist(&value)); + } + Err(e) => { + eprintln!("Error getting value: {e}"); + } } - Err(e) => { + } + "start" => { + let port: u16 = sub_args + .next_argument::() + .expect("no port passed") + .parse() + .expect("not a number"); + let name = sub_args.next_argument::(); + + match proxy + .start_forwarding_service_port( + port, + match &name { + Some(n) => Some(n.as_str()), + None => None, + }, + None, + ) + .await + { + Ok(value) => { + println!("started on port {value}"); + } + Err(e) => { + eprintln!("Error starting: {e}"); + } + } + } + "stop" => { + let port: u16 = sub_args + .next_argument::() + .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 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"); + _ => unreachable!(), } - return; } diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs index b17ef13..7265647 100644 --- a/tools/src/crash_logs.rs +++ b/tools/src/crash_logs.rs @@ -1,96 +1,79 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage crash logs") + .with_subcommand( + "list", + JkCommand::new() + .help("List crash logs in the directory") + .with_argument( + JkArgument::new() + .with_help("Path to list in") + .required(true), + ), + ) + .with_subcommand( + "flush", + JkCommand::new().help("Flushes reports to the directory"), + ) + .with_subcommand( + "pull", + JkCommand::new() + .help("Check the capabilities") + .with_argument( + JkArgument::new() + .with_help("Path to the log to pull") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("Path to save the log to") + .required(true), + ), + ) + .subcommand_required(true) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("crash_logs") - .about("Manage crash logs") - .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)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .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") - .about("Pulls a log") - .arg(Arg::new("path").required(true).index(1)) - .arg(Arg::new("save").required(true).index(2)) - .arg(Arg::new("dir").required(false).index(3)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("crash_logs - manage crash logs on the device"); - 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, "afc-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut crash_client = CrashReportCopyMobileClient::connect(&*provider) .await .expect("Unable to connect to misagent"); - if let Some(matches) = matches.subcommand_matches("list") { - let dir_path: Option<&String> = matches.get_one("dir"); - let res = crash_client - .ls(dir_path.map(|x| x.as_str())) - .await - .expect("Failed to read dir"); - println!("{res:#?}"); - } else if matches.subcommand_matches("flush").is_some() { - flush_reports(&*provider).await.expect("Failed to flush"); - } else if let Some(matches) = matches.subcommand_matches("pull") { - let path = matches.get_one::("path").expect("No path passed"); - let save = matches.get_one::("save").expect("No path passed"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub command passed"); + let mut sub_args = sub_args.clone(); - let res = crash_client.pull(path).await.expect("Failed to pull log"); - tokio::fs::write(save, res) - .await - .expect("Failed to write to file"); - } else { - eprintln!("Invalid usage, pass -h for help"); + match sub_name.as_str() { + "list" => { + let dir_path: Option = sub_args.next_argument(); + let res = crash_client + .ls(match &dir_path { + Some(d) => Some(d.as_str()), + None => None, + }) + .await + .expect("Failed to read dir"); + println!("{res:#?}"); + } + "flush" => { + flush_reports(&*provider).await.expect("Failed to flush"); + } + "pull" => { + let path = sub_args.next_argument::().expect("No path passed"); + let save = sub_args.next_argument::().expect("No path passed"); + + let res = crash_client.pull(path).await.expect("Failed to pull log"); + tokio::fs::write(save, res) + .await + .expect("Failed to write to file"); + } + _ => unreachable!(), } } diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs index f965bd8..4d09d4b 100644 --- a/tools/src/debug_proxy.rs +++ b/tools/src/debug_proxy.rs @@ -2,70 +2,17 @@ use std::io::Write; -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, - rsd::RsdHandshake, + provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Start a debug proxy shell") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .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, "debug-proxy-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/diagnostics.rs b/tools/src/diagnostics.rs index 674f757..bb7d1c5 100644 --- a/tools/src/diagnostics.rs +++ b/tools/src/diagnostics.rs @@ -1,106 +1,71 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's idevicediagnostics -use clap::{Arg, ArgMatches, Command}; -use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient}; +use idevice::{ + IdeviceService, provider::IdeviceProvider, services::diagnostics_relay::DiagnosticsRelayClient, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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)"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the diagnostics interface of a device") + .with_subcommand( + "ioregistry", + JkCommand::new() + .help("Print IORegistry information") + .with_flag( + JkFlag::new("plane") + .with_help("IORegistry plane to query (e.g., IODeviceTree, IOService)") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("name") - .long("name") - .value_name("NAME") - .help("Entry name to filter by"), + .with_flag( + JkFlag::new("name") + .with_help("Entry name to filter by") + .with_argument(JkArgument::new().required(true)), ) - .arg( - Arg::new("class") - .long("class") - .value_name("CLASS") - .help("Entry class to filter by"), + .with_flag( + JkFlag::new("class") + .with_help("Entry class to filter by") + .with_argument(JkArgument::new().required(true)), ), ) - .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..), + .with_subcommand( + "mobilegestalt", + JkCommand::new() + .help("Print MobileGestalt information") + .with_argument( + JkArgument::new() + .with_help("Comma-separated list of keys to query") + .required(true), ), ) - .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; - } - }; + .with_subcommand( + "gasguage", + JkCommand::new().help("Print gas gauge (battery) information"), + ) + .with_subcommand( + "nand", + JkCommand::new().help("Print NAND flash information"), + ) + .with_subcommand( + "all", + JkCommand::new().help("Print all available diagnostics information"), + ) + .with_subcommand( + "wifi", + JkCommand::new().help("Print WiFi diagnostics information"), + ) + .with_subcommand( + "goodbye", + JkCommand::new().help("Send Goodbye to diagnostics relay"), + ) + .with_subcommand("restart", JkCommand::new().help("Restart the device")) + .with_subcommand("shutdown", JkCommand::new().help("Shutdown the device")) + .with_subcommand("sleep", JkCommand::new().help("Put the device to sleep")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await { Ok(client) => client, Err(e) => { @@ -109,47 +74,52 @@ async fn main() { } }; - match matches.subcommand() { - Some(("ioregistry", sub_matches)) => { - handle_ioregistry(&mut diagnostics_client, sub_matches).await; + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_matches = sub_args.clone(); + + match sub_name.as_str() { + "ioregistry" => { + handle_ioregistry(&mut diagnostics_client, &sub_matches).await; } - Some(("mobilegestalt", sub_matches)) => { - handle_mobilegestalt(&mut diagnostics_client, sub_matches).await; + "mobilegestalt" => { + handle_mobilegestalt(&mut diagnostics_client, &mut sub_matches).await; } - Some(("gasguage", _)) => { + "gasguage" => { handle_gasguage(&mut diagnostics_client).await; } - Some(("nand", _)) => { + "nand" => { handle_nand(&mut diagnostics_client).await; } - Some(("all", _)) => { + "all" => { handle_all(&mut diagnostics_client).await; } - Some(("wifi", _)) => { + "wifi" => { handle_wifi(&mut diagnostics_client).await; } - Some(("restart", _)) => { + "restart" => { handle_restart(&mut diagnostics_client).await; } - Some(("shutdown", _)) => { + "shutdown" => { handle_shutdown(&mut diagnostics_client).await; } - Some(("sleep", _)) => { + "sleep" => { handle_sleep(&mut diagnostics_client).await; } - Some(("goodbye", _)) => { + "goodbye" => { handle_goodbye(&mut diagnostics_client).await; } - _ => { - eprintln!("No subcommand specified. Use --help for usage information."); - } + _ => unreachable!(), } } -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()); +async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &CollectedArguments) { + let plane = matches.get_flag::("plane"); + let name = matches.get_flag::("name"); + let class = matches.get_flag::("class"); + + let plane = plane.as_deref(); + let name = name.as_deref(); + let class = class.as_deref(); match client.ioregistry(plane, name, class).await { Ok(Some(data)) => { @@ -164,12 +134,14 @@ async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMat } } -async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) { - let keys = matches - .get_many::("keys") - .map(|values| values.map(|s| s.to_string()).collect::>()); +async fn handle_mobilegestalt( + client: &mut DiagnosticsRelayClient, + matches: &mut CollectedArguments, +) { + let keys = matches.next_argument::().unwrap(); + let keys = keys.split(',').map(|x| x.to_string()).collect(); - match client.mobilegestalt(keys).await { + match client.mobilegestalt(Some(keys)).await { Ok(Some(data)) => { println!("{data:#?}"); } diff --git a/tools/src/diagnosticsservice.rs b/tools/src/diagnosticsservice.rs index 74e6ca4..337e232 100644 --- a/tools/src/diagnosticsservice.rs +++ b/tools/src/diagnosticsservice.rs @@ -1,71 +1,18 @@ // Jackson Coxson -use clap::{Arg, Command}; use futures_util::StreamExt; use idevice::{ IdeviceService, RsdService, core_device::DiagnostisServiceClient, - core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, + core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkCommand}; use tokio::io::AsyncWriteExt; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Retrieve a sysdiagnose") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/dvt_packet_parser.rs b/tools/src/dvt_packet_parser.rs index 7249520..17d1e35 100644 --- a/tools/src/dvt_packet_parser.rs +++ b/tools/src/dvt_packet_parser.rs @@ -1,10 +1,22 @@ // Jackson Coxson -use idevice::dvt::message::Message; +use idevice::{dvt::message::Message, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - let file = std::env::args().nth(1).expect("No file passed"); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Parse a DVT packet from a file") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path the the packet file"), + ) +} + +pub async fn main(arguments: &CollectedArguments, _provider: Box) { + let mut arguments = arguments.clone(); + + let file: String = arguments.next_argument().expect("No file passed"); let mut bytes = tokio::fs::File::open(file).await.unwrap(); let message = Message::from_reader(&mut bytes).await.unwrap(); diff --git a/tools/src/heartbeat_client.rs b/tools/src/heartbeat_client.rs index b40ba08..b1e709d 100644 --- a/tools/src/heartbeat_client.rs +++ b/tools/src/heartbeat_client.rs @@ -1,60 +1,14 @@ // Jackson Coxson // Heartbeat client -use clap::{Arg, Command}; -use idevice::{IdeviceService, heartbeat::HeartbeatClient}; +use idevice::{IdeviceService, heartbeat::HeartbeatClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("heartbeat a device") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("heartbeat_client - heartbeat a device"); - 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, "heartbeat_client-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut heartbeat_client = HeartbeatClient::connect(&*provider) .await .expect("Unable to connect to heartbeat"); diff --git a/tools/src/ideviceinfo.rs b/tools/src/ideviceinfo.rs index d9f0d85..3ce7086 100644 --- a/tools/src/ideviceinfo.rs +++ b/tools/src/ideviceinfo.rs @@ -1,64 +1,14 @@ // Jackson Coxson // idevice Rust implementation of libimobiledevice's ideviceinfo -use clap::{Arg, Command}; -use idevice::{IdeviceService, lockdown::LockdownClient}; +use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "ideviceinfo - get information from the idevice. 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, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = match LockdownClient::connect(&*provider).await { Ok(l) => l, Err(e) => { diff --git a/tools/src/ideviceinstaller.rs b/tools/src/ideviceinstaller.rs index a1c94cf..d7a8290 100644 --- a/tools/src/ideviceinstaller.rs +++ b/tools/src/ideviceinstaller.rs @@ -1,103 +1,73 @@ // A minimal ideviceinstaller-like CLI to install/upgrade apps -use clap::{Arg, ArgAction, Command}; -use idevice::utils::installation; +use idevice::{provider::IdeviceProvider, utils::installation}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_subcommand( + "install", + JkCommand::new() + .help("Install a local .ipa or directory") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the .ipa or directory containing the app"), + ), + ) + .with_subcommand( + "upgrade", + JkCommand::new() + .help("Install a local .ipa or directory") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path to the .ipa or directory containing the app"), + ), + ) + .subcommand_required(true) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg"); + let mut sub_args = sub_args.clone(); - 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; + match sub_name.as_str() { + "install" => { + let path: String = sub_args.next_argument().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}"), + } } - }; - - 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}"), + "upgrade" => { + let path: String = sub_args.next_argument().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 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"); + _ => unreachable!(), } } diff --git a/tools/src/installcoordination_proxy.rs b/tools/src/installcoordination_proxy.rs index d00e867..08fb2f0 100644 --- a/tools/src/installcoordination_proxy.rs +++ b/tools/src/installcoordination_proxy.rs @@ -1,87 +1,39 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, - installcoordination_proxy::InstallcoordinationProxy, rsd::RsdHandshake, + installcoordination_proxy::InstallcoordinationProxy, provider::IdeviceProvider, + rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("installationcoordination_proxy") - .about("") - .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), - ) - .subcommand( - Command::new("info") - .about("Get info about an app on the device") - .arg( - Arg::new("bundle_id") +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the RemoteXPC installation coordination proxy") + .with_subcommand( + "info", + JkCommand::new() + .help("Get info about an app on the device") + .with_argument( + JkArgument::new() .required(true) - .help("The bundle ID to query"), + .with_help("The bundle ID to query"), ), ) - .subcommand( - Command::new("uninstall") - .about("Get info about an app on the device") - .arg( - Arg::new("bundle_id") + .with_subcommand( + "uninstall", + JkCommand::new() + .help("Uninstalls an app on the device") + .with_argument( + JkArgument::new() .required(true) - .help("The bundle ID to query"), + .with_help("The bundle ID to delete"), ), ) - .get_matches(); + .subcommand_required(true) +} - 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, "app_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -103,30 +55,38 @@ async fn main() { .await .expect("no connect"); - if let Some(matches) = matches.subcommand_matches("info") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); - let res = icp.query_app_path(bundle_id).await.expect("no info"); - println!("Path: {res}"); - } else if let Some(matches) = matches.subcommand_matches("uninstall") { - let bundle_id: &String = match matches.get_one("bundle_id") { - Some(b) => b, - None => { - eprintln!("No bundle ID passed"); - return; - } - }; + match sub_name.as_str() { + "info" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; - icp.uninstall_app(bundle_id) - .await - .expect("uninstall failed"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let res = icp + .query_app_path(bundle_id.as_str()) + .await + .expect("no info"); + println!("Path: {res}"); + } + "uninstall" => { + let bundle_id: String = match sub_args.next_argument() { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + icp.uninstall_app(bundle_id.as_str()) + .await + .expect("uninstall failed"); + } + _ => unreachable!(), } } diff --git a/tools/src/instproxy.rs b/tools/src/instproxy.rs index 54eb64e..f126519 100644 --- a/tools/src/instproxy.rs +++ b/tools/src/instproxy.rs @@ -1,108 +1,84 @@ // Jackson Coxson // Just lists apps for now -use clap::{Arg, Command}; -use idevice::{IdeviceService, installation_proxy::InstallationProxyClient}; +use idevice::{ + IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_subcommand( + "lookup", + JkCommand::new().help("Gets the apps on the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "browse", + JkCommand::new().help("Browses the apps on the device"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "check_capabilities", + JkCommand::new().help("Check the capabilities"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), + .with_subcommand( + "install", + JkCommand::new() + .help("Install an app in the AFC jail") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Path in the AFC jail"), + ), ) - .subcommand(Command::new("lookup").about("Gets the apps on the device")) - .subcommand(Command::new("browse").about("Browses the apps on the device")) - .subcommand(Command::new("check_capabilities").about("Check the capabilities")) - .subcommand( - Command::new("install") - .about("Install an app in the AFC jail") - .arg(Arg::new("path")), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "instproxy - query and manage apps installed on 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, "instproxy-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut instproxy_client = InstallationProxyClient::connect(&*provider) .await .expect("Unable to connect to instproxy"); - if matches.subcommand_matches("lookup").is_some() { - let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); - for app in apps.keys() { - println!("{app}"); - } - } else if matches.subcommand_matches("browse").is_some() { - instproxy_client.browse(None).await.expect("browse failed"); - } else if matches.subcommand_matches("check_capabilities").is_some() { - instproxy_client - .check_capabilities_match(Vec::new(), None) - .await - .expect("check failed"); - } else if let Some(matches) = matches.subcommand_matches("install") { - let path: &String = match matches.get_one("path") { - Some(p) => p, - None => { - eprintln!("No path passed, pass -h for help"); - return; - } - }; - instproxy_client - .install_with_callback( - path, - None, - async |(percentage, _)| { - println!("Installing: {percentage}"); - }, - (), - ) - .await - .expect("Failed to install") - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "lookup" => { + let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap(); + for app in apps.keys() { + println!("{app}"); + } + } + "browse" => { + instproxy_client.browse(None).await.expect("browse failed"); + } + "check_capabilities" => { + instproxy_client + .check_capabilities_match(Vec::new(), None) + .await + .expect("check failed"); + } + "install" => { + let path: String = match sub_args.next_argument() { + Some(p) => p, + None => { + eprintln!("No path passed, pass -h for help"); + return; + } + }; + + instproxy_client + .install_with_callback( + path, + None, + async |(percentage, _)| { + println!("Installing: {percentage}"); + }, + (), + ) + .await + .expect("Failed to install") + } + _ => unreachable!(), } } diff --git a/tools/src/location_simulation.rs b/tools/src/location_simulation.rs index a38fda9..ca364e1 100644 --- a/tools/src/location_simulation.rs +++ b/tools/src/location_simulation.rs @@ -1,70 +1,33 @@ // Jackson Coxson // Just lists apps for now -use clap::{Arg, Command}; +use idevice::provider::IdeviceProvider; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use idevice::dvt::location_simulation::LocationSimulationClient; use idevice::services::simulate_location::LocationSimulationService; -mod common; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +pub fn register() -> JkCommand { + JkCommand::new() + .help("Simulate device location") + .with_subcommand( + "clear", + JkCommand::new().help("Clears the location set on the device"), + ) + .with_subcommand( + "set", + JkCommand::new() + .help("Set the location on the device") + .with_argument(JkArgument::new().with_help("latitude").required(true)) + .with_argument(JkArgument::new().with_help("longitutde").required(true)), + ) + .subcommand_required(true) +} - let matches = Command::new("simulate_location") - .about("Simulate device location") - .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("clear").about("Clears the location set on the device")) - .subcommand( - Command::new("set") - .about("Set the location on the device") - .arg(Arg::new("latitude").required(true)) - .arg(Arg::new("longitude").required(true)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("simulate_location - Sets the simulated location on an iOS device"); - 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, "simulate_location-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub arg passed"); + let mut sub_args = sub_args.clone(); if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { let rsd_port = proxy.handshake.server_rsd_port; @@ -86,42 +49,44 @@ async fn main() { let mut ls_client = LocationSimulationClient::new(&mut ls_client) .await .expect("Unable to get channel for location simulation"); - if matches.subcommand_matches("clear").is_some() { - ls_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let latitude: f64 = latitude.parse().expect("Failed to parse as float"); - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - let longitude: f64 = longitude.parse().expect("Failed to parse as float"); - ls_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); - - println!("Location set!"); - println!("Press ctrl-c to stop"); - loop { + match sub_name.as_str() { + "clear" => { + ls_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } + "set" => { + let latitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; + let latitude: f64 = latitude.parse().expect("Failed to parse as float"); + let longitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + let longitude: f64 = longitude.parse().expect("Failed to parse as float"); ls_client .set(latitude, longitude) .await .expect("Failed to set location"); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + println!("Location set!"); + println!("Press ctrl-c to stop"); + loop { + 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"); + _ => unreachable!(), } } else { let mut location_client = match LocationSimulationService::connect(&*provider).await { @@ -133,35 +98,36 @@ async fn main() { return; } }; - if matches.subcommand_matches("clear").is_some() { - location_client.clear().await.expect("Unable to clear"); - println!("Location cleared!"); - } else if let Some(matches) = matches.subcommand_matches("set") { - let latitude: &String = match matches.get_one("latitude") { - Some(l) => l, - None => { - eprintln!("No latitude passed! Pass -h for help"); - return; - } - }; - let longitude: &String = match matches.get_one("longitude") { - Some(l) => l, - None => { - eprintln!("No longitude passed! Pass -h for help"); - return; - } - }; - location_client - .set(latitude, longitude) - .await - .expect("Failed to set location"); + match sub_name.as_str() { + "clear" => { + location_client.clear().await.expect("Unable to clear"); + println!("Location cleared!"); + } + "set" => { + let latitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No latitude passed! Pass -h for help"); + return; + } + }; - println!("Location set!"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let longitude: String = match sub_args.next_argument() { + Some(l) => l, + None => { + eprintln!("No longitude passed! Pass -h for help"); + return; + } + }; + location_client + .set(latitude.as_str(), longitude.as_str()) + .await + .expect("Failed to set location"); + + println!("Location set!"); + } + _ => unreachable!(), } }; - - return; } diff --git a/tools/src/lockdown.rs b/tools/src/lockdown.rs index 2fcf138..ea38c1c 100644 --- a/tools/src/lockdown.rs +++ b/tools/src/lockdown.rs @@ -1,92 +1,79 @@ // Jackson Coxson -use clap::{Arg, Command, arg}; -use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist}; +use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist::Value; +use plist_macro::pretty_print_plist; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("lockdown") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with lockdown") + .with_subcommand( + "get", + JkCommand::new() + .help("Gets a value from lockdown") + .with_argument(JkArgument::new().with_help("The value to get")), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "set", + JkCommand::new() + .help("Gets a value from lockdown") + .with_argument( + JkArgument::new() + .with_help("The value to set") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("The value key to set") + .required(true), + ), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "recovery", + JkCommand::new().help("Tell the device to enter recovery mode"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), + .with_flag( + JkFlag::new("domain") + .with_help("The domain to set/get in") + .with_argument(JkArgument::new().required(true)), ) - .subcommand( - Command::new("get") - .about("Gets a value") - .arg(arg!(-v --value "the value to get").required(false)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .subcommand( - Command::new("set") - .about("Sets a lockdown value") - .arg(arg!(-k --key "the key to set").required(true)) - .arg(arg!(-v --value "the value to set the key to").required(true)) - .arg(arg!(-d --domain "the domain to get in").required(false)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "lockdown - query and manage values on 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, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .with_flag(JkFlag::new("no-session").with_help("Don't start a TLS session")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown"); - lockdown_client - .start_session(&provider.get_pairing_file().await.expect("no pairing file")) - .await - .expect("no session"); + if !arguments.has_flag("no-session") { + lockdown_client + .start_session(&provider.get_pairing_file().await.expect("no pairing file")) + .await + .expect("no session"); + } - match matches.subcommand() { - Some(("get", sub_m)) => { - let key = sub_m.get_one::("value").map(|x| x.as_str()); - let domain = sub_m.get_one::("domain").map(|x| x.as_str()); + let domain: Option = arguments.get_flag("domain"); + let domain = domain.as_deref(); - match lockdown_client.get_value(key, domain).await { + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "get" => { + let key: Option = sub_args.next_argument(); + + match lockdown_client + .get_value( + match &key { + Some(k) => Some(k.as_str()), + None => None, + }, + domain, + ) + .await + { Ok(value) => { println!("{}", pretty_print_plist(&value)); } @@ -95,25 +82,21 @@ async fn main() { } } } - - 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"); + "set" => { + let value_str: String = sub_args.next_argument().unwrap(); + let key: String = sub_args.next_argument().unwrap(); let value = Value::String(value_str.clone()); - match lockdown_client - .set_value(key, value, domain.map(|x| x.as_str())) - .await - { + match lockdown_client.set_value(key, value, domain).await { Ok(()) => println!("Successfully set"), Err(e) => eprintln!("Error setting value: {e}"), } } - - _ => { - eprintln!("No subcommand provided. Try `--help` for usage."); - } + "recovery" => lockdown_client + .enter_recovery() + .await + .expect("Failed to enter recovery"), + _ => unreachable!(), } } diff --git a/tools/src/main.rs b/tools/src/main.rs new file mode 100644 index 0000000..db70ea4 --- /dev/null +++ b/tools/src/main.rs @@ -0,0 +1,338 @@ +// Jackson Coxson + +use std::{ + net::{IpAddr, SocketAddr}, + str::FromStr, +}; + +use idevice::{ + pairing_file::PairingFile, + provider::{IdeviceProvider, TcpProvider}, + usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice}, +}; +use jkcli::{JkArgument, JkCommand, JkFlag}; + +mod activation; +mod afc; +mod amfi; +mod app_service; +mod bt_packet_logger; +mod companion_proxy; +mod crash_logs; +mod debug_proxy; +mod diagnostics; +mod diagnosticsservice; +mod dvt_packet_parser; +mod heartbeat_client; +mod ideviceinfo; +mod ideviceinstaller; +mod installcoordination_proxy; +mod instproxy; +mod location_simulation; +mod lockdown; +mod misagent; +mod mobilebackup2; +mod mounter; +mod notification_proxy_client; +mod notifications; +mod os_trace_relay; +mod pair; +mod pcapd; +mod preboard; +mod process_control; +mod remotexpc; +mod restore_service; +mod screenshot; +mod springboardservices; +mod syslog_relay; + +mod pcap; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // Set the base CLI + let arguments = JkCommand::new() + .with_flag( + JkFlag::new("about") + .with_help("Prints the about message") + .with_short_curcuit(|| { + eprintln!("idevice-rs-tools - Jackson Coxson\n"); + eprintln!("Tools to manage and manipulate iOS devices"); + eprintln!("Version {}", env!("CARGO_PKG_VERSION")); + eprintln!("https://github.com/jkcoxson/idevice"); + eprintln!("\nOn to eternal perfection!"); + std::process::exit(0); + }), + ) + .with_flag( + JkFlag::new("version") + .with_help("Prints the version") + .with_short_curcuit(|| { + println!("{}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + }), + ) + .with_flag( + JkFlag::new("pairing-file") + .with_argument(JkArgument::new().required(true)) + .with_help("The path to the pairing file to use"), + ) + .with_flag( + JkFlag::new("host") + .with_argument(JkArgument::new().required(true)) + .with_help("The host to connect to"), + ) + .with_flag( + JkFlag::new("udid") + .with_argument(JkArgument::new().required(true)) + .with_help("The UDID to use"), + ) + .with_subcommand("activation", activation::register()) + .with_subcommand("afc", afc::register()) + .with_subcommand("amfi", amfi::register()) + .with_subcommand("app_service", app_service::register()) + .with_subcommand("bt_packet_logger", bt_packet_logger::register()) + .with_subcommand("companion_proxy", companion_proxy::register()) + .with_subcommand("crash_logs", crash_logs::register()) + .with_subcommand("debug_proxy", debug_proxy::register()) + .with_subcommand("diagnostics", diagnostics::register()) + .with_subcommand("diagnosticsservice", diagnosticsservice::register()) + .with_subcommand("dvt_packet_parser", dvt_packet_parser::register()) + .with_subcommand("heartbeat_client", heartbeat_client::register()) + .with_subcommand("ideviceinfo", ideviceinfo::register()) + .with_subcommand("ideviceinstaller", ideviceinstaller::register()) + .with_subcommand( + "installcoordination_proxy", + installcoordination_proxy::register(), + ) + .with_subcommand("instproxy", instproxy::register()) + .with_subcommand("location_simulation", location_simulation::register()) + .with_subcommand("lockdown", lockdown::register()) + .with_subcommand("misagent", misagent::register()) + .with_subcommand("mobilebackup2", mobilebackup2::register()) + .with_subcommand("mounter", mounter::register()) + .with_subcommand("notifications", notifications::register()) + .with_subcommand("notification_proxy", notification_proxy_client::register()) + .with_subcommand("os_trace_relay", os_trace_relay::register()) + .with_subcommand("pair", pair::register()) + .with_subcommand("pcapd", pcapd::register()) + .with_subcommand("preboard", preboard::register()) + .with_subcommand("process_control", process_control::register()) + .with_subcommand("remotexpc", remotexpc::register()) + .with_subcommand("restore_service", restore_service::register()) + .with_subcommand("screenshot", screenshot::register()) + .with_subcommand("springboard", springboardservices::register()) + .with_subcommand("syslog_relay", syslog_relay::register()) + .subcommand_required(true) + .collect() + .expect("Failed to collect CLI args"); + + let udid = arguments.get_flag::("udid"); + let host = arguments.get_flag::("host"); + let pairing_file = arguments.get_flag::("pairing-file"); + + let provider = match get_provider(udid, host, pairing_file, "idevice-rs-tools").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let (subcommand, sub_args) = match arguments.first_subcommand() { + Some(s) => s, + None => { + eprintln!("No subcommand passed, pass -h for help"); + return; + } + }; + + match subcommand.as_str() { + "activation" => { + activation::main(sub_args, provider).await; + } + "afc" => { + afc::main(sub_args, provider).await; + } + "amfi" => { + amfi::main(sub_args, provider).await; + } + "app_service" => { + app_service::main(sub_args, provider).await; + } + "bt_packet_logger" => { + bt_packet_logger::main(sub_args, provider).await; + } + "companion_proxy" => { + companion_proxy::main(sub_args, provider).await; + } + "crash_logs" => { + crash_logs::main(sub_args, provider).await; + } + "debug_proxy" => { + debug_proxy::main(sub_args, provider).await; + } + "diagnostics" => { + diagnostics::main(sub_args, provider).await; + } + "diagnosticsservice" => { + diagnosticsservice::main(sub_args, provider).await; + } + "dvt_packet_parser" => { + dvt_packet_parser::main(sub_args, provider).await; + } + "heartbeat_client" => { + heartbeat_client::main(sub_args, provider).await; + } + "ideviceinfo" => { + ideviceinfo::main(sub_args, provider).await; + } + "ideviceinstaller" => { + ideviceinstaller::main(sub_args, provider).await; + } + "installcoordination_proxy" => { + installcoordination_proxy::main(sub_args, provider).await; + } + "instproxy" => { + instproxy::main(sub_args, provider).await; + } + "location_simulation" => { + location_simulation::main(sub_args, provider).await; + } + "lockdown" => { + lockdown::main(sub_args, provider).await; + } + "misagent" => { + misagent::main(sub_args, provider).await; + } + "mobilebackup2" => { + mobilebackup2::main(sub_args, provider).await; + } + "mounter" => { + mounter::main(sub_args, provider).await; + } + "notifications" => { + notifications::main(sub_args, provider).await; + } + "notification_proxy" => { + notification_proxy_client::main(sub_args, provider).await; + } + "os_trace_relay" => { + os_trace_relay::main(sub_args, provider).await; + } + "pair" => { + pair::main(sub_args, provider).await; + } + "pcapd" => { + pcapd::main(sub_args, provider).await; + } + "preboard" => { + preboard::main(sub_args, provider).await; + } + "process_control" => { + process_control::main(sub_args, provider).await; + } + "remotexpc" => { + remotexpc::main(sub_args, provider).await; + } + "restore_service" => { + restore_service::main(sub_args, provider).await; + } + "screenshot" => { + screenshot::main(sub_args, provider).await; + } + "springboard" => { + springboardservices::main(sub_args, provider).await; + } + "syslog_relay" => { + syslog_relay::main(sub_args, provider).await; + } + _ => unreachable!(), + } +} + +async fn get_provider( + udid: Option, + host: Option, + pairing_file: Option, + label: &str, +) -> Result, String> { + 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) + .await + .expect("unable to connect to socket address"); + UsbmuxdConnection::new(Box::new(socket), 1) + } else { + UsbmuxdConnection::default() + .await + .expect("Unable to connect to usbmxud") + }; + + let dev = match usbmuxd.get_device(udid.as_str()).await { + Ok(d) => d, + Err(e) => { + return Err(format!("Device not found: {e:?}")); + } + }; + Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) + } else if let Some(host) = host + && let Some(pairing_file) = pairing_file + { + let host = match IpAddr::from_str(host.as_str()) { + Ok(h) => h, + Err(e) => { + return Err(format!("Invalid host: {e:?}")); + } + }; + let pairing_file = match PairingFile::read_from_file(pairing_file) { + Ok(p) => p, + Err(e) => { + return Err(format!("Unable to read pairing file: {e:?}")); + } + }; + + Box::new(TcpProvider { + addr: host, + pairing_file, + label: label.to_string(), + }) + } else { + 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) + .await + .expect("unable to connect to socket address"); + UsbmuxdConnection::new(Box::new(socket), 1) + } else { + UsbmuxdConnection::default() + .await + .expect("Unable to connect to usbmxud") + }; + let devs = match usbmuxd.get_devices().await { + Ok(d) => d, + Err(e) => { + return Err(format!("Unable to get devices from usbmuxd: {e:?}")); + } + }; + let usb_devs: Vec<&UsbmuxdDevice> = devs + .iter() + .filter(|x| x.connection_type == Connection::Usb) + .collect(); + + if devs.is_empty() { + return Err("No devices connected!".to_string()); + } + + let chosen_dev = if !usb_devs.is_empty() { + usb_devs[0] + } else { + &devs[0] + }; + Box::new(chosen_dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label)) + }; + Ok(provider) +} diff --git a/tools/src/misagent.rs b/tools/src/misagent.rs index 443607f..f535908 100644 --- a/tools/src/misagent.rs +++ b/tools/src/misagent.rs @@ -2,99 +2,89 @@ use std::path::PathBuf; -use clap::{Arg, Command, arg, value_parser}; -use idevice::{IdeviceService, misagent::MisagentClient}; +use idevice::{IdeviceService, misagent::MisagentClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .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)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand( - Command::new("list") - .about("Lists the images mounted on the device") - .arg( - arg!(-s --save "the folder to save the profiles to") - .value_parser(value_parser!(PathBuf)), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage provisioning profiles on the device") + .with_subcommand( + "list", + JkCommand::new() + .help("List profiles installed on the device") + .with_argument( + JkArgument::new() + .with_help("Path to save profiles from the device") + .required(false), ), ) - .subcommand( - Command::new("remove") - .about("Remove a provisioning profile") - .arg(Arg::new("id").required(true).index(1)), + .with_subcommand( + "remove", + JkCommand::new() + .help("Remove a profile installed on the device") + .with_argument( + JkArgument::new() + .with_help("ID of the profile to remove") + .required(true), + ), ) - .get_matches(); + .with_subcommand( + "install", + JkCommand::new() + .help("Install a provisioning profile on the device") + .with_argument( + JkArgument::new() + .with_help("Path to the provisioning profile to install") + .required(true), + ), + ) + .subcommand_required(true) +} - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on 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, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut misagent_client = MisagentClient::connect(&*provider) .await .expect("Unable to connect to misagent"); - if let Some(matches) = matches.subcommand_matches("list") { - let images = misagent_client - .copy_all() - .await - .expect("Unable to get images"); - if let Some(path) = matches.get_one::("save") { - tokio::fs::create_dir_all(path) - .await - .expect("Unable to create save DIR"); + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); - for (index, image) in images.iter().enumerate() { - let f = path.join(format!("{index}.pem")); - tokio::fs::write(f, image) + match sub_name.as_str() { + "list" => { + let images = misagent_client + .copy_all() + .await + .expect("Unable to get images"); + if let Some(path) = sub_args.next_argument::() { + tokio::fs::create_dir_all(&path) .await - .expect("Failed to write image"); + .expect("Unable to create save DIR"); + + for (index, image) in images.iter().enumerate() { + let f = path.join(format!("{index}.pem")); + tokio::fs::write(f, image) + .await + .expect("Failed to write image"); + } } } - } else if let Some(matches) = matches.subcommand_matches("remove") { - let id = matches.get_one::("id").expect("No ID passed"); - misagent_client.remove(id).await.expect("Failed to remove"); - } else { - eprintln!("Invalid usage, pass -h for help"); + "remove" => { + let id = sub_args.next_argument::().expect("No ID passed"); + misagent_client + .remove(id.as_str()) + .await + .expect("Failed to remove"); + } + "install" => { + let path = sub_args + .next_argument::() + .expect("No profile path passed"); + let profile = tokio::fs::read(path).await.expect("Unable to read profile"); + misagent_client + .install(profile) + .await + .expect("Failed to install profile"); + } + _ => unreachable!(), } } diff --git a/tools/src/mobilebackup2.rs b/tools/src/mobilebackup2.rs index 18749e8..15c827b 100644 --- a/tools/src/mobilebackup2.rs +++ b/tools/src/mobilebackup2.rs @@ -1,191 +1,135 @@ // Jackson Coxson // Mobile Backup 2 tool for iOS devices -use clap::{Arg, Command}; use idevice::{ IdeviceService, mobilebackup2::{MobileBackup2Client, RestoreOptions}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; use plist::Dictionary; use std::fs; use std::io::{Read, Write}; use std::path::Path; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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") +pub fn register() -> JkCommand { + JkCommand::new() + .help("Mobile Backup 2 tool for iOS devices") + .with_subcommand( + "info", + JkCommand::new() + .help("Get backup information from a local backup directory") + .with_argument( + JkArgument::new() + .with_help("Backup DIR to read from") .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"), + .with_argument( + JkArgument::new() + .with_help("Source identifier (defaults to current UDID)") + .required(true), ), ) - .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"), + .with_subcommand( + "list", + JkCommand::new() + .help("List files of the last backup from a local backup directory") + .with_argument( + JkArgument::new() + .with_help("Backup DIR to read from") + .required(true), ) - .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), + .with_argument( + JkArgument::new() + .with_help("Source identifier (defaults to current UDID)") + .required(true), ), ) - .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") + .with_subcommand( + "backup", + JkCommand::new() + .help("Start a backup operation") + .with_argument( + JkArgument::new() + .with_help("Backup directory on host") .required(true), ) - .arg( - Arg::new("path") - .long("path") - .value_name("REL_PATH") + .with_argument( + JkArgument::new() + .with_help("Target identifier for the backup") .required(true), ) - .arg(Arg::new("password").long("password").value_name("PWD")), + .with_argument( + JkArgument::new() + .with_help("Source identifier for the backup") + .required(true), + ), ) - .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")), + .with_subcommand( + "restore", + JkCommand::new() + .help("Restore from a local backup directory (DeviceLink)") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument( + JkArgument::new() + .with_help("Source UDID; defaults to current device UDID") + .required(true), + ) + .with_argument( + JkArgument::new() + .with_help("Backup password if encrypted") + .required(true), + ) + .with_flag(JkFlag::new("no-reboot")) + .with_flag(JkFlag::new("no-copy")) + .with_flag(JkFlag::new("no-settings")) + .with_flag(JkFlag::new("system")) + .with_flag(JkFlag::new("remove")), ) - .subcommand( - Command::new("erase-device") - .about("Erase the device via mobilebackup2") - .arg(Arg::new("dir").long("dir").value_name("DIR").required(true)), + .with_subcommand( + "unback", + JkCommand::new() + .help("Unpack a complete backup to device hierarchy") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Source")) + .with_argument(JkArgument::new().with_help("Password")), ) - .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; - } - }; + .with_subcommand( + "extract", + JkCommand::new() + .help("Extract a file from a previous backup") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Source").required(true)) + .with_argument(JkArgument::new().with_help("Domain").required(true)) + .with_argument(JkArgument::new().with_help("Path").required(true)) + .with_argument(JkArgument::new().with_help("Password").required(true)), + ) + .with_subcommand( + "change-password", + JkCommand::new() + .help("Change backup password") + .with_argument(JkArgument::new().with_help("DIR").required(true)) + .with_argument(JkArgument::new().with_help("Old password").required(true)) + .with_argument(JkArgument::new().with_help("New password").required(true)), + ) + .with_subcommand( + "erase-device", + JkCommand::new() + .help("Erase the device via mobilebackup2") + .with_argument(JkArgument::new().with_help("DIR").required(true)), + ) + .with_subcommand( + "freespace", + JkCommand::new().help("Get free space information"), + ) + .with_subcommand( + "encryption", + JkCommand::new().help("Check backup encryption status"), + ) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut backup_client = match MobileBackup2Client::connect(&*provider).await { Ok(client) => client, Err(e) => { @@ -194,11 +138,16 @@ async fn main() { } }; - 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 { + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "info" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + + match backup_client.info_from_path(Path::new(&dir), source).await { Ok(dict) => { println!("Backup Information:"); for (k, v) in dict { @@ -208,10 +157,12 @@ async fn main() { 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 { + "list" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + + match backup_client.list_from_path(Path::new(&dir), source).await { Ok(dict) => { println!("List Response:"); for (k, v) in dict { @@ -221,12 +172,12 @@ async fn main() { 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"); + "backup" => { + let target = sub_args.next_argument::(); + let target = target.as_deref(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let dir = sub_args.next_argument::().expect("dir is required"); println!("Starting backup operation..."); let res = backup_client @@ -234,95 +185,112 @@ async fn main() { .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 { + } 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()); + "restore" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let mut ropts = RestoreOptions::new(); - if sub.get_flag("no-reboot") { + if sub_args.has_flag("no-reboot") { ropts = ropts.with_reboot(false); } - if sub.get_flag("no-copy") { + if sub_args.has_flag("no-copy") { ropts = ropts.with_copy(false); } - if sub.get_flag("no-settings") { + if sub_args.has_flag("no-settings") { ropts = ropts.with_preserve_settings(false); } - if sub.get_flag("system") { + if sub_args.has_flag("system") { ropts = ropts.with_system_files(true); } - if sub.get_flag("remove") { + if sub_args.has_flag("remove") { ropts = ropts.with_remove_items_not_restored(true); } - if let Some(pw) = sub.get_one::("password") { + if let Some(pw) = sub_args.next_argument::() { ropts = ropts.with_password(pw); } match backup_client - .restore_from_path(Path::new(dir), source, Some(ropts)) + .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()); + "unback" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let password = sub_args.next_argument::(); + let password = password.as_deref(); + match backup_client - .unback_from_path(Path::new(dir), password, source) + .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()); + "extract" => { + let dir = sub_args.next_argument::().unwrap(); + let source = sub_args.next_argument::(); + let source = source.as_deref(); + let domain = sub_args.next_argument::().unwrap(); + let rel = sub_args.next_argument::().unwrap(); + let password = sub_args.next_argument::(); + let password = password.as_deref(); + match backup_client - .extract_from_path(domain, rel, Path::new(dir), password, source) + .extract_from_path( + domain.as_str(), + rel.as_str(), + 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()); + "change-password" => { + let dir = sub_args.next_argument::().unwrap(); + let old = sub_args.next_argument::(); + let old = old.as_deref(); + let newv = sub_args.next_argument::(); + let newv = newv.as_deref(); + match backup_client - .change_password_from_path(Path::new(dir), old, newv) + .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 { + "erase-device" => { + let dir = sub_args.next_argument::().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 { + "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 { + "encryption" => match backup_client.check_backup_encryption().await { Ok(is_encrypted) => { println!( "Backup encryption: {}", diff --git a/tools/src/mounter.rs b/tools/src/mounter.rs index 8b67056..90e9ebc 100644 --- a/tools/src/mounter.rs +++ b/tools/src/mounter.rs @@ -3,90 +3,62 @@ use std::{io::Write, path::PathBuf}; -use clap::{Arg, Command, arg, value_parser}; use idevice::{ IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter, - pretty_print_plist, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; +use plist_macro::pretty_print_plist; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("core_device_proxy_tun") - .about("Start a tunnel") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage mounts on an iOS device") + .with_subcommand( + "list", + JkCommand::new().help("Lists the images mounted on the device"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand( + "lookup", + JkCommand::new().help("Lookup the image signature on the device"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), + .with_subcommand( + "unmount", + JkCommand::new().help("Unmounts the developer disk image"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("list").about("Lists the images mounted on the device")) - .subcommand(Command::new("unmount").about("Unmounts the developer disk image")) - .subcommand( - Command::new("mount") - .about("Mounts the developer disk image") - .arg( - arg!(-i --image "the developer disk image to mount") - .value_parser(value_parser!(PathBuf)) + .with_subcommand( + "mount", + JkCommand::new() + .help("Mounts the developer disk image") + .with_flag( + JkFlag::new("image") + .with_short("i") + .with_argument(JkArgument::new().required(true)) + .with_help("A path to the image to mount") .required(true), ) - .arg( - arg!(-b --manifest "the build manifest (iOS 17+)") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("manifest") + .with_short("b") + .with_argument(JkArgument::new()) + .with_help("the build manifest (iOS 17+)"), ) - .arg( - arg!(-t --trustcache "the trust cache (iOS 17+)") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("trustcache") + .with_short("t") + .with_argument(JkArgument::new()) + .with_help("the trust cache (iOS 17+)"), ) - .arg( - arg!(-s --signature "the image signature (iOS < 17.0") - .value_parser(value_parser!(PathBuf)), + .with_flag( + JkFlag::new("signature") + .with_short("s") + .with_argument(JkArgument::new()) + .with_help("the image signature (iOS < 17.0"), ), ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on 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, "ideviceinfo-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let mut lockdown_client = LockdownClient::connect(&*provider) .await .expect("Unable to connect to lockdown"); @@ -119,114 +91,131 @@ async fn main() { .await .expect("Unable to connect to image mounter"); - if matches.subcommand_matches("list").is_some() { - let images = mounter_client - .copy_devices() - .await - .expect("Unable to get images"); - for i in images { - println!("{}", pretty_print_plist(&i)); - } - } else if matches.subcommand_matches("unmount").is_some() { - if product_version < 17 { - mounter_client - .unmount_image("/Developer") + let (subcommand, sub_args) = arguments + .first_subcommand() + .expect("No subcommand passed! Pass -h for help"); + + match subcommand.as_str() { + "list" => { + let images = mounter_client + .copy_devices() .await - .expect("Failed to unmount"); - } else { - mounter_client - .unmount_image("/System/Developer") - .await - .expect("Failed to unmount"); - } - } else if let Some(matches) = matches.subcommand_matches("mount") { - let image: &PathBuf = match matches.get_one("image") { - Some(i) => i, - None => { - eprintln!("No image was passed! Pass -h for help"); - return; + .expect("Unable to get images"); + for i in images { + println!("{}", pretty_print_plist(&i)); } - }; - let image = tokio::fs::read(image).await.expect("Unable to read image"); - if product_version < 17 { - let signature: &PathBuf = match matches.get_one("signature") { - Some(s) => s, - None => { - eprintln!("No signature was passed! Pass -h for help"); - return; - } - }; - let signature = tokio::fs::read(signature) - .await - .expect("Unable to read signature"); - - mounter_client - .mount_developer(&image, signature) - .await - .expect("Unable to mount"); - } else { - let manifest: &PathBuf = match matches.get_one("manifest") { - Some(s) => s, - None => { - eprintln!("No build manifest was passed! Pass -h for help"); - return; - } - }; - let build_manifest = &tokio::fs::read(manifest) - .await - .expect("Unable to read signature"); - - let trust_cache: &PathBuf = match matches.get_one("trustcache") { - Some(s) => s, - None => { - eprintln!("No trust cache was passed! Pass -h for help"); - return; - } - }; - let trust_cache = tokio::fs::read(trust_cache) - .await - .expect("Unable to read signature"); - - 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"); - - mounter_client - .mount_personalized_with_callback( - &*provider, - image, - trust_cache, - build_manifest, - None, - unique_chip_id, - async |((n, d), _)| { - let percent = (n as f64 / d as f64) * 100.0; - print!("\rProgress: {percent:.2}%"); - std::io::stdout().flush().unwrap(); // Make sure it prints immediately - if n == d { - println!(); - } - }, - (), - ) - .await - .expect("Unable to mount"); } - } else { - eprintln!("Invalid usage, pass -h for help"); + "lookup" => { + let sig = mounter_client + .lookup_image(if product_version < 17 { + "Developer" + } else { + "Personalized" + }) + .await + .expect("Failed to lookup images"); + println!("Image signature: {sig:02X?}"); + } + "unmount" => { + if product_version < 17 { + mounter_client + .unmount_image("/Developer") + .await + .expect("Failed to unmount"); + } else { + mounter_client + .unmount_image("/System/Developer") + .await + .expect("Failed to unmount"); + } + } + "mount" => { + let image: PathBuf = match sub_args.get_flag("image") { + Some(i) => i, + None => { + eprintln!("No image was passed! Pass -h for help"); + return; + } + }; + let image = tokio::fs::read(image).await.expect("Unable to read image"); + if product_version < 17 { + let signature: PathBuf = match sub_args.get_flag("signature") { + Some(s) => s, + None => { + eprintln!("No signature was passed! Pass -h for help"); + return; + } + }; + let signature = tokio::fs::read(signature) + .await + .expect("Unable to read signature"); + + mounter_client + .mount_developer(&image, signature) + .await + .expect("Unable to mount"); + } else { + let manifest: PathBuf = match sub_args.get_flag("manifest") { + Some(s) => s, + None => { + eprintln!("No build manifest was passed! Pass -h for help"); + return; + } + }; + let build_manifest = &tokio::fs::read(manifest) + .await + .expect("Unable to read signature"); + + let trust_cache: PathBuf = match sub_args.get_flag("trustcache") { + Some(s) => s, + None => { + eprintln!("No trust cache was passed! Pass -h for help"); + return; + } + }; + let trust_cache = tokio::fs::read(trust_cache) + .await + .expect("Unable to read signature"); + + 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"); + + mounter_client + .mount_personalized_with_callback( + &*provider, + image, + trust_cache, + build_manifest, + None, + unique_chip_id, + async |((n, d), _)| { + let percent = (n as f64 / d as f64) * 100.0; + print!("\rProgress: {percent:.2}%"); + std::io::stdout().flush().unwrap(); // Make sure it prints immediately + if n == d { + println!(); + } + }, + (), + ) + .await + .expect("Unable to mount"); + } + } + _ => unreachable!(), } - return; } diff --git a/tools/src/notification_proxy_client.rs b/tools/src/notification_proxy_client.rs new file mode 100644 index 0000000..9559d62 --- /dev/null +++ b/tools/src/notification_proxy_client.rs @@ -0,0 +1,85 @@ +// Jackson Coxson + +use idevice::{ + IdeviceService, notification_proxy::NotificationProxyClient, provider::IdeviceProvider, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; + +pub fn register() -> JkCommand { + JkCommand::new() + .help("Notification proxy") + .with_subcommand( + "observe", + JkCommand::new() + .help("Observe notifications from the device") + .with_argument( + JkArgument::new() + .with_help("The notification ID to observe") + .required(true), + ), + ) + .with_subcommand( + "post", + JkCommand::new() + .help("Post a notification to the device") + .with_argument( + JkArgument::new() + .with_help("The notification ID to post") + .required(true), + ), + ) + .subcommand_required(true) +} + +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut client = NotificationProxyClient::connect(&*provider) + .await + .expect("Unable to connect to notification proxy"); + + let (subcommand, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match subcommand.as_str() { + "observe" => { + let input: String = sub_args + .next_argument::() + .expect("No notification ID passed"); + + let notifications: Vec<&str> = input.split_whitespace().collect(); + client + .observe_notifications(¬ifications) + .await + .expect("Failed to observe notifications"); + + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("\nShutdown signal received, exiting."); + break; + } + + result = client.receive_notification() => { + match result { + Ok(notif) => println!("Received notification: {}", notif), + Err(e) => { + eprintln!("Failed to receive notification: {}", e); + break; + } + } + } + } + } + } + "post" => { + let notification: String = sub_args + .next_argument::() + .expect("No notification ID passed"); + + client + .post_notification(¬ification) + .await + .expect("Failed to post notification"); + } + _ => unreachable!(), + } +} diff --git a/tools/src/notifications.rs b/tools/src/notifications.rs index 703eb9d..7026489 100644 --- a/tools/src/notifications.rs +++ b/tools/src/notifications.rs @@ -1,59 +1,16 @@ // Monitor memory and app notifications -use clap::{Arg, Command}; -use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; -mod common; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, +}; +use jkcli::{CollectedArguments, JkCommand}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("notifications") - .about("start notifications") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - print!("notifications - start notifications to ios device"); - 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, "notifications-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("Notification proxy") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -80,7 +37,6 @@ async fn main() { .await .expect("Failed to start notifications"); - // Handle Ctrl+C gracefully loop { tokio::select! { _ = tokio::signal::ctrl_c() => { @@ -88,7 +44,6 @@ async fn main() { break; } - // Branch 2: Wait for the next batch of notifications. result = notification_client.get_notification() => { if let Err(e) = result { eprintln!("Failed to get notifications: {}", e); diff --git a/tools/src/os_trace_relay.rs b/tools/src/os_trace_relay.rs index f85f396..f3daf44 100644 --- a/tools/src/os_trace_relay.rs +++ b/tools/src/os_trace_relay.rs @@ -1,58 +1,13 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient}; +use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Relay OS logs") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("os_trace_relay") - .about("Relay system logs") - .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)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("Relay logs on the device"); - 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, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let log_client = OsTraceRelayClient::connect(&*provider) .await .expect("Unable to connect to misagent"); diff --git a/tools/src/pair.rs b/tools/src/pair.rs index fae58e8..fd5ec29 100644 --- a/tools/src/pair.rs +++ b/tools/src/pair.rs @@ -1,46 +1,35 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, lockdown::LockdownClient, + provider::IdeviceProvider, usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection}, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("pair") - .about("Pair with the device") - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)") - .index(1), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage files in the AFC jail of a device") + .with_argument(JkArgument::new().with_help("A UDID to override and pair with")) + .with_flag( + JkFlag::new("name") + .with_help("The host name to report to the device") + .with_argument(JkArgument::new().required(true)) + .with_short("n"), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); +} - if matches.get_flag("about") { - println!("pair - pair with the device"); - println!("Copyright (c) 2025 Jackson Coxson"); - return; - } - - let udid = matches.get_one::("udid"); +pub async fn main(arguments: &CollectedArguments, _provider: Box) { + let mut arguments = arguments.clone(); + let udid: Option = arguments.next_argument(); let mut u = UsbmuxdConnection::default() .await .expect("Failed to connect to usbmuxd"); let dev = match udid { Some(udid) => u - .get_device(udid) + .get_device(udid.as_str()) .await .expect("Failed to get device with specific udid"), None => u @@ -62,8 +51,11 @@ async fn main() { }; let id = uuid::Uuid::new_v4().to_string().to_uppercase(); + let name = arguments.get_flag::("name"); + let name = name.as_deref(); + let mut pairing_file = lockdown_client - .pair(id, u.get_buid().await.unwrap()) + .pair(id, u.get_buid().await.unwrap(), name) .await .expect("Failed to pair"); diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs index d489f45..2fca403 100644 --- a/tools/src/pcapd.rs +++ b/tools/src/pcapd.rs @@ -1,55 +1,20 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ IdeviceService, pcapd::{PcapFileWriter, PcapdClient}, + provider::IdeviceProvider, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Writes pcap network data") + .with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)")) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let out = arguments.clone().next_argument::(); let mut logger_client = PcapdClient::connect(&*provider) .await diff --git a/tools/src/preboard.rs b/tools/src/preboard.rs index 5655db3..a6bfdb3 100644 --- a/tools/src/preboard.rs +++ b/tools/src/preboard.rs @@ -1,76 +1,34 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, preboard_service::PreboardServiceClient}; +use idevice::{IdeviceService, preboard_service::PreboardServiceClient, provider::IdeviceProvider}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::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; - } - }; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the preboard service") + .with_subcommand("create", JkCommand::new().help("Create a stashbag??")) + .with_subcommand("commit", JkCommand::new().help("Commit a stashbag??")) + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { 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"); + let (sub_name, _) = arguments.first_subcommand().unwrap(); + + match sub_name.as_str() { + "create" => { + pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } + "commit" => { + pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) + .await + .expect("Failed to create"); + } + _ => unreachable!(), } - return; } diff --git a/tools/src/process_control.rs b/tools/src/process_control.rs index d0939b4..743d498 100644 --- a/tools/src/process_control.rs +++ b/tools/src/process_control.rs @@ -1,76 +1,26 @@ // Jackson Coxson -use clap::{Arg, Command}; +use idevice::provider::IdeviceProvider; use idevice::services::lockdown::LockdownClient; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Launch an app with process control") + .with_argument( + JkArgument::new() + .required(true) + .with_help("The bundle ID to launch"), + ) +} -#[tokio::main] -async fn main() { +pub async fn main(arguments: &CollectedArguments, provider: Box) { tracing_subscriber::fmt::init(); - let matches = Command::new("process_control") - .about("Query process control") - .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(2), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("tunneld") - .long("tunneld") - .help("Use tunneld for connection") - .action(clap::ArgAction::SetTrue), - ) - .arg( - Arg::new("bundle_id") - .value_name("Bundle ID") - .help("Bundle ID of the app to launch") - .index(1), - ) - .get_matches(); + let mut arguments = arguments.clone(); - if matches.get_flag("about") { - println!("process_control - launch and manage processes on the device"); - 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 bundle_id = matches - .get_one::("bundle_id") - .expect("No bundle ID specified"); - - let provider = - match common::get_provider(udid, host, pairing_file, "process_control-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + let bundle_id: String = arguments.next_argument().expect("No bundle ID specified"); let mut rs_client_opt: Option< idevice::dvt::remote_server::RemoteServerClient>, diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs index 31df4b6..5b25573 100644 --- a/tools/src/remotexpc.rs +++ b/tools/src/remotexpc.rs @@ -1,65 +1,17 @@ // Jackson Coxson // Print out all the RemoteXPC services -use clap::{Arg, Command}; use idevice::{ - IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, - tcp::stream::AdapterStream, + IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, tcp::stream::AdapterStream, }; +use jkcli::{CollectedArguments, JkCommand}; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("remotexpc") - .about("Get services from RemoteXPC") - .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), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("remotexpc - get info from RemoteXPC"); - 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, "remotexpc-jkcoxson").await - { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub fn register() -> JkCommand { + JkCommand::new().help("Get services from RemoteXPC") +} +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs index e2a2efd..3daecf6 100644 --- a/tools/src/restore_service.rs +++ b/tools/src/restore_service.rs @@ -1,76 +1,41 @@ // Jackson Coxson -use clap::{Arg, Command}; use idevice::{ - IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, restore_service::RestoreServiceClient, rsd::RsdHandshake, }; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; +use plist_macro::pretty_print_dictionary; -mod common; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("restore_service") - .about("Interact with the Restore Service service") - .arg( - Arg::new("host") - .long("host") - .value_name("HOST") - .help("IP address of the device"), +pub fn register() -> JkCommand { + JkCommand::new() + .help("Interact with the Restore Service service") + .with_subcommand("delay", JkCommand::new().help("Delay recovery image")) + .with_subcommand("recovery", JkCommand::new().help("Enter recovery mode")) + .with_subcommand("reboot", JkCommand::new().help("Reboots the device")) + .with_subcommand( + "preflightinfo", + JkCommand::new().help("Gets the preflight info"), ) - .arg( - Arg::new("pairing_file") - .long("pairing-file") - .value_name("PATH") - .help("Path to the pairing file"), + .with_subcommand("nonces", JkCommand::new().help("Gets the nonces")) + .with_subcommand( + "app_parameters", + JkCommand::new().help("Gets the app parameters"), ) - .arg( - Arg::new("udid") - .value_name("UDID") - .help("UDID of the device (overrides host/pairing file)"), + .with_subcommand( + "restore_lang", + JkCommand::new() + .help("Restores the language") + .with_argument( + JkArgument::new() + .required(true) + .with_help("Language to restore"), + ), ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .subcommand(Command::new("delay").about("Delay recovery image")) - .subcommand(Command::new("recovery").about("Enter recovery mode")) - .subcommand(Command::new("reboot").about("Reboots the device")) - .subcommand(Command::new("preflightinfo").about("Gets the preflight info")) - .subcommand(Command::new("nonces").about("Gets the nonces")) - .subcommand(Command::new("app_parameters").about("Gets the app parameters")) - .subcommand( - Command::new("restore_lang") - .about("Restores the language") - .arg(Arg::new("language").required(true).index(1)), - ) - .get_matches(); - - if matches.get_flag("about") { - println!( - "mounter - query and manage images mounted on 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, "restore_service-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + .subcommand_required(true) +} +pub async fn main(arguments: &CollectedArguments, provider: Box) { let proxy = CoreDeviceProxy::connect(&*provider) .await .expect("no core proxy"); @@ -88,37 +53,46 @@ async fn main() { .await .expect("Unable to connect to service"); - if matches.subcommand_matches("recovery").is_some() { - restore_client - .enter_recovery() - .await - .expect("command failed"); - } else if matches.subcommand_matches("reboot").is_some() { - restore_client.reboot().await.expect("command failed"); - } else if matches.subcommand_matches("preflightinfo").is_some() { - let info = restore_client - .get_preflightinfo() - .await - .expect("command failed"); - pretty_print_dictionary(&info); - } else if matches.subcommand_matches("nonces").is_some() { - let nonces = restore_client.get_nonces().await.expect("command failed"); - pretty_print_dictionary(&nonces); - } else if matches.subcommand_matches("app_parameters").is_some() { - let params = restore_client - .get_app_parameters() - .await - .expect("command failed"); - pretty_print_dictionary(¶ms); - } else if let Some(matches) = matches.subcommand_matches("restore_lang") { - let lang = matches - .get_one::("language") - .expect("No language passed"); - restore_client - .restore_lang(lang) - .await - .expect("failed to restore lang"); - } else { - eprintln!("Invalid usage, pass -h for help"); + let (sub_name, sub_args) = arguments.first_subcommand().unwrap(); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "recovery" => { + restore_client + .enter_recovery() + .await + .expect("command failed"); + } + "reboot" => { + restore_client.reboot().await.expect("command failed"); + } + "preflightinfo" => { + let info = restore_client + .get_preflightinfo() + .await + .expect("command failed"); + println!("{}", pretty_print_dictionary(&info)); + } + "nonces" => { + let nonces = restore_client.get_nonces().await.expect("command failed"); + println!("{}", pretty_print_dictionary(&nonces)); + } + "app_parameters" => { + let params = restore_client + .get_app_parameters() + .await + .expect("command failed"); + println!("{}", pretty_print_dictionary(¶ms)); + } + "restore_lang" => { + let lang: String = sub_args + .next_argument::() + .expect("No language passed"); + restore_client + .restore_lang(lang) + .await + .expect("failed to restore lang"); + } + _ => unreachable!(), } } diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs index db55613..82d1a17 100644 --- a/tools/src/screenshot.rs +++ b/tools/src/screenshot.rs @@ -1,69 +1,20 @@ -use clap::{Arg, Command}; -use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, + rsd::RsdHandshake, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand}; use std::fs; use idevice::screenshotr::ScreenshotService; -mod common; +pub fn register() -> JkCommand { + JkCommand::new() + .help("Take a screenshot") + .with_argument(JkArgument::new().with_help("Output path").required(true)) +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let matches = Command::new("screen_shot") - .about("take screenshot") - .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("output") - .short('o') - .long("output") - .value_name("FILE") - .help("Output file path for the screenshot (default: ./screenshot.png)") - .default_value("screenshot.png"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - print!("screen_shot - take screenshot from ios device"); - 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 output_path = matches.get_one::("output").unwrap(); - - let provider = - match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let output_path = arguments.clone().next_argument::().unwrap(); let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { println!("Using DVT over CoreDeviceProxy"); @@ -104,7 +55,7 @@ async fn main() { screenshot_client.take_screenshot().await.unwrap() }; - match fs::write(output_path, res) { + match fs::write(&output_path, res) { Ok(_) => println!("Screenshot saved to: {}", output_path), Err(e) => eprintln!("Failed to write screenshot to file: {}", e), } diff --git a/tools/src/springboardservices.rs b/tools/src/springboardservices.rs new file mode 100644 index 0000000..94d24cb --- /dev/null +++ b/tools/src/springboardservices.rs @@ -0,0 +1,161 @@ +// Jackson Coxson + +use idevice::{ + IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient, +}; +use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag}; +use plist_macro::{plist_value_to_xml_bytes, pretty_print_plist}; + +pub fn register() -> JkCommand { + JkCommand::new() + .help("Manage the springboard service") + .with_subcommand( + "get_icon_state", + JkCommand::new() + .help("Gets the icon state from the device") + .with_argument( + JkArgument::new() + .with_help("Version to query by") + .required(false), + ) + .with_flag( + JkFlag::new("save") + .with_help("Path to save to") + .with_argument(JkArgument::new().required(true)), + ), + ) + .with_subcommand( + "set_icon_state", + JkCommand::new().help("Sets the icon state").with_argument( + JkArgument::new() + .with_help("plist to set based on") + .required(true), + ), + ) + .with_subcommand( + "get_wallpaper_preview", + JkCommand::new() + .help("Gets wallpaper preview") + .with_subcommand("homescreen", JkCommand::new()) + .with_subcommand("lockscreen", JkCommand::new()) + .subcommand_required(true) + .with_flag( + JkFlag::new("save") + .with_help("Path to save the wallpaper PNG file, or preview.png by default") + .with_argument(JkArgument::new().required(true)), + ), + ) + .with_subcommand( + "get_interface_orientation", + JkCommand::new().help("Gets the device's current screen orientation"), + ) + .with_subcommand( + "get_homescreen_icon_metrics", + JkCommand::new().help("Gets home screen icon layout metrics"), + ) + .with_subcommand( + "get_icon", + JkCommand::new() + .help("Gets an app's icon as PNG") + .with_argument( + JkArgument::new() + .with_help("Bundle identifier (e.g. com.apple.Maps)") + .required(true), + ) + .with_flag( + JkFlag::new("save") + .with_help("Path to save the icon PNG file, or icon.png by default") + .with_argument(JkArgument::new().required(true)), + ), + ) + .subcommand_required(true) +} + +pub async fn main(arguments: &CollectedArguments, provider: Box) { + let mut sbc = SpringBoardServicesClient::connect(&*provider) + .await + .expect("Failed to connect to springboardservices"); + + let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed"); + let mut sub_args = sub_args.clone(); + + match sub_name.as_str() { + "get_icon_state" => { + let version: Option = sub_args.next_argument(); + let version = version.as_deref(); + let state = sbc + .get_icon_state(version) + .await + .expect("Failed to get icon state"); + println!("{}", pretty_print_plist(&state)); + + if let Some(path) = sub_args.get_flag::("save") { + tokio::fs::write(path, plist_value_to_xml_bytes(&state)) + .await + .expect("Failed to save to path"); + } + } + "set_icon_state" => { + let load_path = sub_args.next_argument::().unwrap(); + let load = tokio::fs::read(load_path) + .await + .expect("Failed to read plist"); + let load: plist::Value = + plist::from_bytes(&load).expect("Failed to parse bytes as plist"); + + sbc.set_icon_state(load) + .await + .expect("Failed to set icon state"); + } + "get_wallpaper_preview" => { + let (wallpaper_type, _) = sub_args.first_subcommand().unwrap(); + + let wallpaper = match wallpaper_type.as_str() { + "homescreen" => sbc.get_home_screen_wallpaper_preview_pngdata().await, + "lockscreen" => sbc.get_lock_screen_wallpaper_preview_pngdata().await, + _ => panic!("Invalid wallpaper type. Use 'homescreen' or 'lockscreen'"), + } + .expect("Failed to get wallpaper preview"); + + let save_path = sub_args + .get_flag::("save") + .unwrap_or("preview.png".to_string()); + + tokio::fs::write(&save_path, wallpaper) + .await + .expect("Failed to save wallpaper"); + } + "get_interface_orientation" => { + let orientation = sbc + .get_interface_orientation() + .await + .expect("Failed to get interface orientation"); + println!("{:?}", orientation); + } + "get_homescreen_icon_metrics" => { + let metrics = sbc + .get_homescreen_icon_metrics() + .await + .expect("Failed to get homescreen icon metrics"); + let metrics_value = plist::Value::Dictionary(metrics); + println!("{}", pretty_print_plist(&metrics_value)); + } + "get_icon" => { + let bundle_id = sub_args.next_argument::().unwrap(); + + let icon_data = sbc + .get_icon_pngdata(bundle_id) + .await + .expect("Failed to get icon"); + + let save_path = sub_args + .get_flag::("save") + .unwrap_or("icon.png".to_string()); + + tokio::fs::write(&save_path, icon_data) + .await + .expect("Failed to save icon"); + } + _ => unreachable!(), + } +} diff --git a/tools/src/syslog_relay.rs b/tools/src/syslog_relay.rs index 0552579..11b116c 100644 --- a/tools/src/syslog_relay.rs +++ b/tools/src/syslog_relay.rs @@ -1,58 +1,13 @@ // Jackson Coxson -use clap::{Arg, Command}; -use idevice::{IdeviceService, syslog_relay::SyslogRelayClient}; +use idevice::{IdeviceService, provider::IdeviceProvider, syslog_relay::SyslogRelayClient}; +use jkcli::{CollectedArguments, JkCommand}; -mod common; +pub fn register() -> JkCommand { + JkCommand::new().help("Relay system logs") +} -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - let matches = Command::new("syslog_relay") - .about("Relay system logs") - .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)"), - ) - .arg( - Arg::new("about") - .long("about") - .help("Show about information") - .action(clap::ArgAction::SetTrue), - ) - .get_matches(); - - if matches.get_flag("about") { - println!("Relay logs on the device"); - 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, "misagent-jkcoxson").await { - Ok(p) => p, - Err(e) => { - eprintln!("{e}"); - return; - } - }; +pub async fn main(_arguments: &CollectedArguments, provider: Box) { let mut log_client = SyslogRelayClient::connect(&*provider) .await .expect("Unable to connect to misagent");