diff --git a/Cargo.lock b/Cargo.lock index e40c813..5c53a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,31 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "adb_client" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4974ac06396fce4424d9d2193f80ea5a177d6e2ddde6214ff7c14aaedd2a3104" +dependencies = [ + "base64", + "byteorder", + "chrono", + "image", + "log", + "num-bigint-dig", + "num-traits", + "num_enum", + "quick-protobuf", + "rand 0.10.1", + "rcgen", + "regex", + "rsa", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", +] + [[package]] name = "adler2" version = "2.0.1" @@ -37,6 +62,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "aligned" version = "0.4.3" @@ -227,6 +261,45 @@ dependencies = [ "zbus", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -473,7 +546,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -493,13 +566,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] [[package]] @@ -508,6 +587,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -541,6 +629,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[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 = "block2" version = "0.5.1" @@ -604,6 +701,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -704,10 +807,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "chrono" -version = "0.4.43" +name = "chacha20" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -854,6 +968,12 @@ 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 = "cookie" version = "0.18.1" @@ -986,6 +1106,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1039,6 +1177,16 @@ dependencies = [ "wgpu", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.4.0" @@ -1086,6 +1234,37 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.6" @@ -1095,6 +1274,17 @@ dependencies = [ "powerfmt", ] +[[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", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1597,6 +1787,16 @@ dependencies = [ "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 = "gethostname" version = "1.1.0" @@ -1639,6 +1839,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2177,9 +2378,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -2195,8 +2396,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "zune-core", + "zune-jpeg", ] [[package]] @@ -2351,6 +2552,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -2457,9 +2661,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" dependencies = [ "value-bag", ] @@ -2537,6 +2741,12 @@ dependencies = [ "paste", ] +[[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" @@ -2560,9 +2770,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -2682,6 +2892,16 @@ dependencies = [ "libc", ] +[[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 = "nom" version = "8.0.0" @@ -2707,6 +2927,23 @@ dependencies = [ "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.6", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2733,6 +2970,17 @@ 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-rational" version = "0.4.2" @@ -2766,9 +3014,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2776,9 +3024,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3145,6 +3393,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3242,6 +3499,25 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[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" @@ -3291,6 +3567,27 @@ dependencies = [ "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" @@ -3446,6 +3743,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3501,6 +3807,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3539,6 +3856,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "range-alloc" version = "0.1.4" @@ -3588,9 +3911,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -3627,6 +3950,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "read-fonts" version = "0.35.0" @@ -3676,6 +4013,35 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -3744,6 +4110,26 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3765,6 +4151,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.44" @@ -3793,9 +4188,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -3808,9 +4203,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -3969,6 +4364,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3985,6 +4391,16 @@ 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.8" @@ -4144,6 +4560,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -4153,6 +4575,16 @@ dependencies = [ "bitflags 2.10.0", ] +[[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" @@ -4306,16 +4738,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] @@ -4532,6 +4964,12 @@ dependencies = [ "core_maths", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "uad-cli" version = "1.2.0" @@ -4556,6 +4994,7 @@ dependencies = [ name = "uad-core" version = "1.2.0" dependencies = [ + "adb_client", "chrono", "csv", "dirs", @@ -5119,7 +5558,7 @@ checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" dependencies = [ "arrayvec", "bit-set", - "bit-vec", + "bit-vec 0.8.0", "bitflags 2.10.0", "bytemuck", "cfg_aliases 0.2.1", @@ -5891,6 +6330,24 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -5938,6 +6395,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec 0.9.1", + "time", +] + [[package]] name = "yazi" version = "0.2.1" @@ -6120,12 +6587,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.1" @@ -6141,22 +6602,13 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core 0.5.1", + "zune-core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0464938..9104739 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ edition = "2024" [workspace.dependencies] # Core dependencies +adb_client = "3.2.2" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" log = "^0.4" diff --git a/crates/uad-cli/Cargo.toml b/crates/uad-cli/Cargo.toml index 6045f95..9759dcf 100644 --- a/crates/uad-cli/Cargo.toml +++ b/crates/uad-cli/Cargo.toml @@ -14,8 +14,12 @@ edition.workspace = true name = "uad" path = "src/main.rs" +[features] +default = ["builtin-adb"] +builtin-adb = ["uad-core/builtin-adb"] + [dependencies] -uad-core = { path = "../uad-core" } +uad-core = { path = "../uad-core", default-features = false } clap.workspace = true clap_complete = "4.4" tokio.workspace = true diff --git a/crates/uad-cli/src/commands.rs b/crates/uad-cli/src/commands.rs index 62286bd..ff16469 100644 --- a/crates/uad-cli/src/commands.rs +++ b/crates/uad-cli/src/commands.rs @@ -2,10 +2,10 @@ use clap::CommandFactory; use clap_complete::{Shell, generate}; use std::collections::{HashMap, HashSet}; use std::io::Write; -use uad_core::adb::{ACommand, PmListPacksFlag}; +use uad_core::adb::{ACommand, AdbBackend, PmListPacksFlag}; use uad_core::sync::{ CorePackage, Phone, User, apply_pkg_state_commands, get_devices_list, get_package_state, - run_adb_shell_action, + run_adb_shell_action_with_backend, }; use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; use uad_core::utils::{matches_search, truncate_description}; @@ -15,9 +15,12 @@ use crate::filters::{ListFilter, RemovalFilter, StateFilter}; use crate::{Cli, print_or_exit, println_or_exit}; /// List all connected Android devices -pub fn list_devices() -> Result<(), Box> { - println_or_exit!("Scanning for connected devices..."); - let devices = get_devices_list(); +pub fn list_devices(backend: AdbBackend) -> Result<(), Box> { + println_or_exit!( + "Scanning for connected devices (using {} backend)...", + backend + ); + let devices = get_devices_list(backend); if devices.is_empty() { return Err(NO_DEVICES_FOUND.into()); @@ -58,28 +61,24 @@ impl PackageListContext { pkg_info: Option<&Package>, pkg_state: PackageState, ) -> bool { - // Removal filter if let Some(removal) = self.removal_filter { if !removal.matches(pkg_info) { return false; } } - // State filter if let Some(state) = self.state_filter { if !state.matches(pkg_state) { return false; } } - // List filter if let Some(list) = self.list_filter { if !list.matches(pkg_info) { return false; } } - // Search filter if let Some(ref search_term) = self.search { let description = pkg_info.map(|p| p.description.as_str()); if !matches_search(pkg_name, search_term, description) { @@ -104,8 +103,18 @@ pub struct DisplayConfig { pub show_removal: bool, } +pub(crate) fn resolve_pm_flag(state_filter: Option) -> Option { + match state_filter { + Some(StateFilter::Enabled | StateFilter::Disabled) => { + state_filter.and_then(StateFilter::to_pm_flag) + } + _ => Some(PmListPacksFlag::IncludeUninstalled), + } +} + /// List packages on a device with filtering pub fn list_packages( + backend: AdbBackend, device: Option, state_filter: Option, removal_filter: Option, @@ -113,7 +122,7 @@ pub fn list_packages( search: Option, user_id: Option, ) -> Result<(), Box> { - let target_device = get_target_device(device)?; + let target_device = get_target_device(backend, device)?; let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); println_or_exit!( @@ -129,8 +138,8 @@ pub fn list_packages( search, }; - let pm_flag = state_filter.and_then(StateFilter::to_pm_flag); - let system_packages = ACommand::new() + let pm_flag = resolve_pm_flag(state_filter); + let system_packages = ACommand::with_backend(backend) .shell(&target_device.adb_id) .pm() .list_packages_sys(pm_flag, user_id)?; @@ -141,6 +150,7 @@ pub fn list_packages( &target_device.adb_id, user_id, &context, + backend, )?; if displayed_count == 0 { @@ -159,18 +169,19 @@ pub fn display_package_list( device_serial: &str, user_id: Option, context: &PackageListContext, + backend: AdbBackend, ) -> Result> { let config = context.display_config(); let mut displayed_count = 0; - let enabled_packages: HashSet = ACommand::new() + let enabled_packages: HashSet = ACommand::with_backend(backend) .shell(device_serial) .pm() .list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id) .unwrap_or_default() .into_iter() .collect(); - let disabled_packages: HashSet = ACommand::new() + let disabled_packages: HashSet = ACommand::with_backend(backend) .shell(device_serial) .pm() .list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id) @@ -241,6 +252,7 @@ pub fn display_package_entry( /// Change the state of one or more packages pub fn change_package_state( + backend: AdbBackend, packages: &[String], device: Option, user_id: Option, @@ -252,7 +264,7 @@ pub fn change_package_state( return Err("No packages specified".into()); } - let target_device = get_target_device(device)?; + let target_device = get_target_device(backend, device)?; let user = get_user(&target_device, user_id)?; let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); @@ -275,6 +287,7 @@ pub fn change_package_state( user, target_state, dry_run, + backend, &uad_lists, )?; println!(); @@ -296,6 +309,7 @@ fn process_package_state_change( user: User, target_state: PackageState, dry_run: bool, + backend: AdbBackend, uad_lists: &HashMap, ) -> Result<(), Box> { let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id)) @@ -337,6 +351,7 @@ fn process_package_state_change( user, device, &commands, + backend, " ", )?; } @@ -345,6 +360,10 @@ fn process_package_state_change( } /// Execute commands and verify package state with fallback +#[allow( + clippy::too_many_arguments, + reason = "Keeps the shared fallback helper simple across CLI and REPL callers" +)] pub fn execute_with_fallback( package: &str, target_state: PackageState, @@ -352,15 +371,14 @@ pub fn execute_with_fallback( user: User, device: &Phone, commands: &[String], + backend: AdbBackend, indent: &str, ) -> Result<(), Box> { - // Capture the before-state of packages on other users for cross-user detection let before_cross_user_states = uad_core::sync::capture_cross_user_states(package, &device.adb_id, user.id, device); - // Execute commands for cmd in commands { - match run_adb_shell_action(&device.adb_id, cmd.as_str()) { + match run_adb_shell_action_with_backend(&device.adb_id, cmd.as_str(), backend) { Ok(_) => println!("{}✓ {}", indent, cmd), Err(e) => { return Err(format!("{indent}✗ Failed to execute `{cmd}`: {e:?}").into()); @@ -368,7 +386,6 @@ pub fn execute_with_fallback( } } - // Verify package state and attempt fallback if needed let actual_state = match get_package_state(&device.adb_id, package, Some(user.id)) { Some(state) => state, None if target_state == PackageState::Uninstalled => PackageState::Uninstalled, @@ -387,7 +404,6 @@ pub fn execute_with_fallback( indent, target_state, actual_state ); - // Attempt fallback if let Ok(fallback_action) = uad_core::sync::attempt_fallback(core_pkg, target_state, actual_state, user, device) { @@ -397,7 +413,6 @@ pub fn execute_with_fallback( } } - // Check for cross-user behavior if operation succeeded if actual_state == target_state { if let Some(notification) = uad_core::sync::detect_cross_user_behavior( package, @@ -417,6 +432,7 @@ pub fn execute_with_fallback( /// Show detailed information about a package pub fn show_package_info( + backend: AdbBackend, package: &str, device: Option, user_id: Option, @@ -436,7 +452,7 @@ pub fn show_package_info( } if let Some(device_id) = device { - let target_device = get_target_device(Some(device_id))?; + let target_device = get_target_device(backend, Some(device_id))?; let user = get_user(&target_device, user_id)?; println!("Device: {} ({})", target_device.model, target_device.adb_id); println!("User: {}", user.id); @@ -468,3 +484,19 @@ pub fn generate_completions(shell: Shell) { let name = cmd.get_name().to_string(); generate(shell, &mut cmd, name, &mut std::io::stdout()); } + +/// Show ADB backend and version information +pub fn show_adb_info(backend: AdbBackend) -> Result<(), Box> { + println!("ADB Backend: {}\n", backend); + + match ACommand::with_backend(backend).version() { + Ok(version) => { + println!("{version}"); + Ok(()) + } + Err(e) => { + eprintln!("Failed to get ADB version: {e}"); + Err(e.into()) + } + } +} diff --git a/crates/uad-cli/src/device.rs b/crates/uad-cli/src/device.rs index 54fee8c..5c9d366 100644 --- a/crates/uad-cli/src/device.rs +++ b/crates/uad-cli/src/device.rs @@ -1,11 +1,15 @@ +use uad_core::adb::AdbBackend; use uad_core::sync::{Phone, User, get_devices_list}; pub const NO_DEVICES_FOUND: &str = "No devices found. Make sure ADB is installed and devices are connected."; /// Get target device, either by serial or first available -pub fn get_target_device(device: Option) -> Result> { - let devices = get_devices_list(); +pub fn get_target_device( + backend: AdbBackend, + device: Option, +) -> Result> { + let devices = get_devices_list(backend); if devices.is_empty() { return Err(NO_DEVICES_FOUND.into()); diff --git a/crates/uad-cli/src/main.rs b/crates/uad-cli/src/main.rs index f473e28..560baa2 100644 --- a/crates/uad-cli/src/main.rs +++ b/crates/uad-cli/src/main.rs @@ -4,12 +4,14 @@ clippy::uninlined_format_args, clippy::map_unwrap_or, clippy::unnecessary_wraps, + clippy::exit, reason = "Suppress non-critical pedantic/style lints to keep build green" )] -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use clap_complete::Shell; use std::process::ExitCode; +use uad_core::adb::AdbBackend; use uad_core::uad_lists::PackageState; mod commands; @@ -20,12 +22,42 @@ mod repl; use filters::{ListFilter, RemovalFilter, StateFilter}; +/// CLI-compatible ADB backend selection +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum AdbBackendArg { + /// Built-in ADB implementation (no external dependencies) + #[cfg(feature = "builtin-adb")] + Builtin, + /// Use system-installed adb binary + System, +} + +impl From for AdbBackend { + fn from(arg: AdbBackendArg) -> Self { + match arg { + #[cfg(feature = "builtin-adb")] + AdbBackendArg::Builtin => AdbBackend::Builtin, + AdbBackendArg::System => AdbBackend::System, + } + } +} + #[derive(Parser)] #[command(name = "uad")] #[command(about = "Universal Android Debloater - Command Line Interface", long_about = None)] #[command(version)] #[command(propagate_version = true)] pub struct Cli { + /// ADB backend to use: system (default, uses adb binary) or builtin if enabled + #[arg( + short = 'B', + long = "backend", + value_enum, + global = true, + default_value = "system" + )] + backend: AdbBackendArg, + #[command(subcommand)] command: Commands, } @@ -136,6 +168,9 @@ enum Commands { /// Update UAD package lists from remote repository Update, + /// Show ADB backend and version information + Adb, + /// Generate shell completion script Completions { /// Shell to generate completions for @@ -169,10 +204,11 @@ async fn main() -> ExitCode { fn run() -> Result<(), Box> { let cli = Cli::parse(); + let backend: AdbBackend = cli.backend.into(); match cli.command { Commands::Devices => { - commands::list_devices()?; + commands::list_devices(backend)?; } Commands::List { device, @@ -182,7 +218,7 @@ fn run() -> Result<(), Box> { search, user, } => { - commands::list_packages(device, state, removal, list, search, user)?; + commands::list_packages(backend, device, state, removal, list, search, user)?; } Commands::Uninstall { packages, @@ -191,6 +227,7 @@ fn run() -> Result<(), Box> { dry_run, } => { commands::change_package_state( + backend, &packages, device, user, @@ -206,6 +243,7 @@ fn run() -> Result<(), Box> { dry_run, } => { commands::change_package_state( + backend, &packages, device, user, @@ -221,6 +259,7 @@ fn run() -> Result<(), Box> { dry_run, } => { commands::change_package_state( + backend, &packages, device, user, @@ -234,16 +273,19 @@ fn run() -> Result<(), Box> { device, user, } => { - commands::show_package_info(&package, device, user)?; + commands::show_package_info(backend, &package, device, user)?; } Commands::Update => { commands::update_lists()?; } + Commands::Adb => { + commands::show_adb_info(backend)?; + } Commands::Completions { shell } => { commands::generate_completions(shell); } Commands::Repl { device, user } => { - repl::repl_mode(device, user)?; + repl::repl_mode(backend, device, user)?; } } diff --git a/crates/uad-cli/src/output.rs b/crates/uad-cli/src/output.rs index cff4724..ae62a74 100644 --- a/crates/uad-cli/src/output.rs +++ b/crates/uad-cli/src/output.rs @@ -3,16 +3,22 @@ #[macro_export] macro_rules! println_or_exit { () => { - let _ = writeln!(std::io::stdout()); + if writeln!(std::io::stdout()).is_err() { + std::process::exit(0); + } }; ($($arg:tt)*) => { - let _ = writeln!(std::io::stdout(), $($arg)*); + if writeln!(std::io::stdout(), $($arg)*).is_err() { + std::process::exit(0); + } }; } #[macro_export] macro_rules! print_or_exit { ($($arg:tt)*) => { - let _ = write!(std::io::stdout(), $($arg)*); + if write!(std::io::stdout(), $($arg)*).is_err() { + std::process::exit(0); + } }; } diff --git a/crates/uad-cli/src/repl.rs b/crates/uad-cli/src/repl.rs index eeee0c6..47f75ee 100644 --- a/crates/uad-cli/src/repl.rs +++ b/crates/uad-cli/src/repl.rs @@ -2,7 +2,7 @@ use rustyline::DefaultEditor; use rustyline::error::ReadlineError; use std::collections::HashMap; use std::io::Write; -use uad_core::adb::ACommand; +use uad_core::adb::{ACommand, AdbBackend}; use uad_core::sync::{CorePackage, Phone, User, apply_pkg_state_commands, get_package_state}; use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; @@ -18,13 +18,15 @@ enum ReplAction { /// Start interactive REPL mode pub fn repl_mode( + backend: AdbBackend, device: Option, user_id: Option, ) -> Result<(), Box> { println!("Universal Android Debloater - Interactive Mode"); + println!("Using {} ADB backend", backend); println!("Type 'help' for available commands, 'exit' or 'quit' to leave\n"); - let target_device = get_target_device(device)?; + let target_device = get_target_device(backend, device)?; let user = get_user(&target_device, user_id)?; println!( @@ -46,7 +48,15 @@ pub fn repl_mode( let readline = rl.readline("uad> "); match readline { Ok(line) => { - match handle_repl_line(&line, &mut rl, &target_device, user, user_id, &uad_lists) { + match handle_repl_line( + &line, + &mut rl, + &target_device, + user, + user_id, + &uad_lists, + backend, + ) { Ok(ReplAction::Continue) => {} Ok(ReplAction::Exit) => break, Err(e) => { @@ -88,6 +98,7 @@ fn handle_repl_line( user: User, user_id: Option, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result> { let line = line.trim(); if line.is_empty() { @@ -108,7 +119,7 @@ fn handle_repl_line( return Ok(ReplAction::Exit); } "list" | "ls" => { - handle_list_command(&parts[1..], device, user_id, uad_lists)?; + handle_list_command(&parts[1..], device, user_id, uad_lists, backend)?; } "info" => { handle_info_command(&parts[1..], device, user.id, uad_lists)?; @@ -121,6 +132,7 @@ fn handle_repl_line( PackageState::Uninstalled, "Uninstalling", uad_lists, + backend, )?; } "enable" | "restore" => { @@ -131,6 +143,7 @@ fn handle_repl_line( PackageState::Enabled, "Enabling", uad_lists, + backend, )?; } "disable" => { @@ -141,12 +154,13 @@ fn handle_repl_line( PackageState::Disabled, "Disabling", uad_lists, + backend, )?; } "device" => { println!( - "Device: {} ({}), Android SDK: {}, User: {}", - device.model, device.adb_id, device.android_sdk, user.id + "Device: {} ({}), Android SDK: {}, User: {}, Backend: {}", + device.model, device.adb_id, device.android_sdk, user.id, backend ); } "clear" => { @@ -216,11 +230,12 @@ fn handle_list_command( device: &Phone, user_id: Option, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result<(), Box> { let parsed = ReplListArgs::parse(args)?; let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag); - let system_packages = ACommand::new() + let system_packages = ACommand::with_backend(backend) .shell(&device.adb_id) .pm() .list_packages_sys(pm_flag, user_id)?; @@ -238,6 +253,7 @@ fn handle_list_command( &device.adb_id, user_id, &context, + backend, )?; if displayed_count == 0 { @@ -287,6 +303,7 @@ fn handle_state_change_command( target_state: PackageState, action_name: &str, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result<(), Box> { if args.is_empty() { eprintln!( @@ -297,7 +314,15 @@ fn handle_state_change_command( } for pkg_name in args { - process_package_change(pkg_name, device, user, target_state, action_name, uad_lists)?; + process_package_change( + pkg_name, + device, + user, + target_state, + action_name, + uad_lists, + backend, + )?; } Ok(()) @@ -311,6 +336,7 @@ fn process_package_change( target_state: PackageState, action_name: &str, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result<(), Box> { let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id)) .ok_or("Package not found on device")?; @@ -346,6 +372,7 @@ fn process_package_change( user, device, &commands, + backend, " ", ) } diff --git a/crates/uad-core/Cargo.toml b/crates/uad-core/Cargo.toml index f7d3a43..4bd07fd 100644 --- a/crates/uad-core/Cargo.toml +++ b/crates/uad-core/Cargo.toml @@ -11,10 +11,12 @@ categories = ["command-line-utilities"] edition.workspace = true [features] -default = [] +default = ["builtin-adb"] +builtin-adb = ["dep:adb_client"] self-update = ["dep:flate2", "dep:tar"] [dependencies] +adb_client = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true log.workspace = true diff --git a/crates/uad-core/src/adb.rs b/crates/uad-core/src/adb.rs index e7d223c..a03ef94 100644 --- a/crates/uad-core/src/adb.rs +++ b/crates/uad-core/src/adb.rs @@ -4,8 +4,7 @@ //! //! Following the design philosophy of most of Rust `std`, //! `*Command` are intended to be "thin wrappers" (low-overhead abstractions) -//! around the ADB CLI or `adb_client` -//! ([in the future](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/700) ), +//! around the optional `adb_client` backend or the system ADB CLI, //! which implies: //! - no "magic" //! - no custom commands @@ -33,11 +32,26 @@ //! please, **PLEASE** refrain from falling-back to any `Command`-like API. //! Rather, please extend these APIs in a consistent way. //! +//! ## Backend Selection +//! +//! This module supports two ADB backends: +//! - **Builtin** (`adb_client`): Pure Rust implementation, no external dependencies +//! - **System**: Uses the system-installed `adb` binary +//! +//! Use [`ACommand::with_backend`] to select a specific backend, +//! or [`ACommand::new`] to use the default (System backend). +//! //! Thank you! ❤️ //! //! For comprehensive info about ADB, //! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/) +#[cfg(feature = "builtin-adb")] +use adb_client::{ADBDeviceExt, server::ADBServer}; +use serde::{Deserialize, Serialize}; +use std::fmt::Write as _; +#[cfg(feature = "builtin-adb")] +use std::io::Cursor; use std::rc::Rc; #[cfg(target_os = "windows")] @@ -53,25 +67,90 @@ pub fn to_trimmed_utf8(v: &[u8]) -> String { String::from_utf8_lossy(v).trim_end().to_string() } +/// ADB backend selection. +/// +/// - **Builtin**: Uses the `adb_client` crate (pure Rust, no external dependencies) +/// - **System**: Uses the system-installed `adb` binary +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum AdbBackend { + /// Built-in ADB implementation via `adb_client` crate. + /// The application can communicate with devices without needing `adb` installed. + #[cfg(feature = "builtin-adb")] + Builtin, + /// Uses the system-installed `adb` binary. + /// This is the default to preserve existing behavior. + /// Requires `adb` to be available in PATH. + /// Useful if you prefer using your own ADB installation or need specific ADB features. + #[default] + System, +} + +impl AdbBackend { + /// Returns all available backend variants for UI enumeration + #[cfg(feature = "builtin-adb")] + pub const ALL: [Self; 2] = [Self::Builtin, Self::System]; + + /// Returns all available backend variants for UI enumeration + #[cfg(not(feature = "builtin-adb"))] + pub const ALL: [Self; 1] = [Self::System]; +} + +impl std::fmt::Display for AdbBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "builtin-adb")] + Self::Builtin => write!(f, "Builtin"), + Self::System => write!(f, "System (adb)"), + } + } +} + #[cfg(debug_assertions)] #[must_use] fn is_version_triple(s: &str) -> bool { - let mut components = s.split('.'); - for _ in 0..3 { - if !components - .next() - .is_some_and(|comp| comp.as_bytes().iter().all(u8::is_ascii_digit)) - { - return false; - } - } - if components.next().is_some() { - return false; - } - true + let mut parts = s.split('.'); + let is_digits = |c: &str| !c.is_empty() && c.bytes().all(|b| b.is_ascii_digit()); + parts.next().is_some_and(is_digits) + && parts.next().is_some_and(is_digits) + && parts.next().is_some_and(is_digits) + && parts.next().is_none() } -/// Builder object for an Android Debug Bridge CLI command, +#[cfg(debug_assertions)] +fn assert_adb_version_output_format(out: &str) { + const ADBV: &str = "Android Debug Bridge version "; + const V: &str = "Version "; + + let mut lns = out.lines(); + + assert!( + lns.next() + .is_some_and(|ln| ln.starts_with(ADBV) && is_version_triple(&ln[ADBV.len()..])) + ); + assert!( + lns.next().is_some_and(|ln| ln.starts_with(V) + && is_version_triple(&ln[V.len()..ln.find('-').unwrap_or(ln.len())])) + ); + // missing test for valid path + assert!( + lns.next().is_some_and(|ln| ln.starts_with("Installed as ") + && (ln.ends_with("adb") || ln.ends_with("adb.exe"))) + ); + // missing test for x86/ARM (both 64b) + assert!(lns.next().is_some_and(|ln| ln.starts_with("Running on "))); + if lns.next().is_some() { + unreachable!("Expected < 5 lines") + } +} + +/// Internal state for `ACommand` - tracks the device serial and backend to use +#[derive(Debug)] +struct ACommandState { + device_serial: Option, + backend: AdbBackend, +} + +/// Builder object for an Android Debug Bridge command, /// using the type-state and new-type patterns. /// /// This is not intended to model the entire ADB API. @@ -79,12 +158,22 @@ fn is_version_triple(s: &str) -> bool { /// /// [More info here](https://developer.android.com/tools/adb) #[derive(Debug)] -pub struct ACommand(std::process::Command); +pub struct ACommand(ACommandState); + impl ACommand { - /// `adb` command builder + /// `adb` command builder with the default backend. #[must_use] pub fn new() -> Self { - Self(std::process::Command::new("adb")) + Self::with_backend(AdbBackend::default()) + } + + /// `adb` command builder with a specific backend + #[must_use] + pub fn with_backend(backend: AdbBackend) -> Self { + Self(ACommandState { + device_serial: None, + backend, + }) } /// `shell` sub-command builder. @@ -94,9 +183,8 @@ impl ACommand { pub fn shell>(mut self, device_serial: S) -> ShellCommand { let serial = device_serial.as_ref(); if !serial.is_empty() { - self.0.args(["-s", serial]); + self.0.device_serial = Some(serial.to_string()); } - self.0.arg("shell"); ShellCommand(self) } @@ -108,109 +196,195 @@ impl ACommand { /// Status can be (but not limited to): /// - "unauthorized" /// - "device" - pub fn devices(mut self) -> Result, String> { - self.0.arg("devices"); - Ok(self - .run()? - .lines() - .skip(1) // header - .map(|dev_stat| { - let tab_idx = dev_stat - // OS-specific? - .find('\t') - // True on Linux, - // no matter if ADB is piped or connected to terminal - .expect("There must be 1 tab after serial"); - ( - // serial - dev_stat[..tab_idx].to_string(), - // status - dev_stat[(tab_idx + 1)..].to_string(), - ) - }) - .collect()) + pub fn devices(self) -> Result, String> { + match self.0.backend { + #[cfg(feature = "builtin-adb")] + AdbBackend::Builtin => Self::devices_builtin(), + AdbBackend::System => Self::devices_system(), + } } - /// `version` sub-command + /// Returns version information from the ADB server/binary. /// - /// ## Format - /// This is just a sample, - /// we don't know which guarantees are stable (yet): + /// ## Builtin backend + /// Returns the ADB server protocol version: + /// ```txt + /// ADB Server Version: 1.0.41 + /// ``` + /// + /// ## System backend + /// Returns the full `adb version` output (format may vary by installation): /// ```txt /// Android Debug Bridge version 1.0.41 - /// Version 34.0.5-debian - /// Installed as /usr/lib/android-sdk/platform-tools/adb - /// Running on Linux 6.12.12-amd64 (x86_64) + /// Version 35.0.2-android-tools + /// Installed as /usr/bin/adb + /// Running on Linux 6.18 (x86_64) /// ``` - /// - /// The expected format should be like: - /// ```txt - /// Android Debug Bridge version .. - /// Version ..- - /// Installed as /platform-tools/adb[.exe] - /// Running on () - /// ``` - #[expect(clippy::panic_in_result_fn, reason = "Assertions are fine")] - pub fn version(mut self) -> Result { - self.0.arg("version"); - let out = self.run()?; + pub fn version(self) -> Result { + match self.0.backend { + #[cfg(feature = "builtin-adb")] + AdbBackend::Builtin => Self::version_builtin(), + AdbBackend::System => Self::version_system(), + } + } - #[cfg(debug_assertions)] - { - const ADBV: &str = "Android Debug Bridge version "; - const V: &str = "Version "; + // ========== Builtin backend implementation (adb_client) ========== - let mut lns = out.lines(); - - assert!( - lns.next() - .is_some_and(|ln| ln.starts_with(ADBV) && is_version_triple(&ln[ADBV.len()..])) - ); - assert!(lns.next().is_some_and(|ln| ln.starts_with(V) - && is_version_triple(&ln[V.len()..ln.find('-').unwrap_or(ln.len())]))); - // missing test for valid path - assert!(lns.next().is_some_and(|ln| ln.starts_with("Installed as ") - && (ln.ends_with("adb") || ln.ends_with("adb.exe")))); - // missing test for x86/ARM (both 64b) - assert!(lns.next().is_some_and(|ln| ln.starts_with("Running on "))); - if lns.next().is_some() { - unreachable!("Expected < 5 lines") + /// Get ADB server version using the builtin `adb_client` + #[cfg(feature = "builtin-adb")] + fn version_builtin() -> Result { + let mut server = ADBServer::default(); + match server.version() { + Ok(version) => Ok(format!("ADB Server Version: {version}")), + Err(e) => { + error!("Failed to get ADB server version: {e}"); + Err(format!("Cannot get ADB server version: {e}")) } } + } + + /// List devices using the builtin `adb_client` + #[cfg(feature = "builtin-adb")] + fn devices_builtin() -> Result, String> { + let mut server = ADBServer::default(); + server + .devices() + .map(|device_list| { + device_list + .into_iter() + .map(|dev| (dev.identifier, dev.state.to_string())) + .collect() + }) + .map_err(|e| { + error!("ADB: {e}"); + format!("Cannot connect to ADB server: {e}") + }) + } + + /// Execute a shell command via `adb_client` (builtin backend) + #[cfg(feature = "builtin-adb")] + fn run_shell_command_builtin(&self, shell_command: &str) -> Result { + let mut server = ADBServer::default(); + + // Validate device availability and serial + if let Some(ref serial) = self.0.device_serial { + let device_list = server + .devices() + .map_err(|e| format!("Cannot get device list: {e}"))?; + + if !device_list.iter().any(|d| d.identifier == *serial) { + let available = device_list + .iter() + .map(|d| d.identifier.as_str()) + .collect::>() + .join(", "); + return Err(format!( + "Device '{serial}' not found. Available: {available}" + )); + } + } + + let mut device = server + .get_device() + .map_err(|e| format!("Cannot connect to device: {e}"))?; + + if shell_command.trim().is_empty() { + return Err("Empty shell command".into()); + } + + info!("Ran command: adb shell {shell_command}"); + + let mut buffer = Vec::new(); + let _exit_code = device + .shell_command(&shell_command, Some(&mut Cursor::new(&mut buffer)), None) + .map_err(|e| { + error!("ADB shell command failed: {e}"); + format!("Shell command failed: {e}") + })?; + + Ok(String::from_utf8_lossy(&buffer).trim_end().to_string()) + } + + // ========== System backend implementation (adb binary) ========== + + /// Get ADB version using the system `adb` binary + fn version_system() -> Result { + let mut cmd = std::process::Command::new("adb"); + cmd.arg("version"); + let out = Self::run_system_command(cmd)?; + + #[cfg(debug_assertions)] + assert_adb_version_output_format(&out); Ok(out) } - /// General executor - fn run(self) -> Result { - let mut cmd = self.0; + /// List devices using the system `adb` binary + fn devices_system() -> Result, String> { + let mut cmd = std::process::Command::new("adb"); + cmd.arg("devices"); + Ok(Self::run_system_command(cmd)? + .lines() + .skip(1) // header + .filter_map(|line| { + let (serial, status) = line.split_once('\t')?; + Some((serial.to_string(), status.to_string())) + }) + .collect()) + } + + /// Execute a shell command via system `adb` binary + fn run_shell_command_system(&self, shell_command: &str) -> Result { + let mut cmd = std::process::Command::new("adb"); + + if let Some(ref serial) = self.0.device_serial { + cmd.args(["-s", serial]); + } + + cmd.arg("shell"); + cmd.arg(shell_command); + + info!("Ran command: adb shell {}", shell_command); + Self::run_system_command(cmd) + } + + /// General system command executor for adb binary + fn run_system_command(mut cmd: std::process::Command) -> Result { #[cfg(target_os = "windows")] let cmd = cmd.creation_flags(0x0800_0000); // do not open a cmd window info!( "Ran command: adb {}", cmd.get_args() - .map(|s| s.to_str().unwrap_or_else(|| unreachable!())) + .filter_map(|s| s.to_str()) .collect::>() .join(" ") ); - match cmd.output() { - Err(e) => { - error!("ADB: {e}"); - Err("Cannot run ADB, likely not found".to_string()) - } - Ok(o) => { - let stdout = to_trimmed_utf8(&o.stdout); - if o.status.success() { - Ok(stdout) - } else { - let stderr = to_trimmed_utf8(&o.stderr); - // ADB does really weird things: - // Some errors are not redirected to `stderr` - let err = if stdout.is_empty() { stderr } else { stdout }; - Err(err) - } - } + + let output = cmd.output().map_err(|e| { + error!("ADB: {e}"); + "Cannot run ADB, likely not found".to_string() + })?; + + let stdout = to_trimmed_utf8(&output.stdout); + if output.status.success() { + Ok(stdout) + } else { + // ADB sometimes outputs errors to stdout instead of stderr + Err(if stdout.is_empty() { + to_trimmed_utf8(&output.stderr) + } else { + stdout + }) + } + } + + /// Execute a shell command using the configured backend + fn run_shell_command(&self, shell_command: &str) -> Result { + match self.0.backend { + #[cfg(feature = "builtin-adb")] + AdbBackend::Builtin => self.run_shell_command_builtin(shell_command), + AdbBackend::System => self.run_shell_command_system(shell_command), } } } @@ -227,12 +401,13 @@ impl Default for ACommand { /// [More info](https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/upstream/shell_and_utilities). #[derive(Debug)] pub struct ShellCommand(ACommand); + impl ShellCommand { /// `pm` command builder - pub fn pm(mut self) -> PmCommand { - self.0.0.arg("pm"); + pub fn pm(self) -> PmCommand { PmCommand(self) } + /// Query a device property value, by its key. /// These can be of any type: /// - `boolean` @@ -241,36 +416,26 @@ impl ShellCommand { /// - etc... /// /// So to avoid lossy conversions, we return strs - pub fn getprop(mut self, key: &str) -> Result { - self.0.0.args(["getprop", key]); - self.0.run() + pub fn getprop(self, key: &str) -> Result { + self.0.run_shell_command(&format!("getprop {key}")) } + /// Reboots device - pub fn reboot(mut self) -> Result { - self.0.0.arg("reboot"); - self.0.run() + pub fn reboot(self) -> Result { + self.0.run_shell_command("reboot") } /// Execute an arbitrary shell action string on the device's default shell. /// The action string is passed as a single argument to `adb shell` and /// interpreted by the remote shell (which splits on spaces). - pub fn raw(mut self, action: &str) -> Result { - self.0.0.arg(action); - self.0.run() + pub fn raw(self, action: &str) -> Result { + self.0.run_shell_command(action) } } #[must_use] pub const fn is_pkg_component(s: &[u8]) -> bool { - if s.is_empty() { - return false; - } - s[0].is_ascii_alphabetic() - && if s.len() > 1 { - is_all_w_c(s.split_at(1).1) - } else { - true - } + !s.is_empty() && s[0].is_ascii_alphabetic() && (s.len() == 1 || is_all_w_c(s.split_at(1).1)) } /// String with the invariant of being a valid package-name. @@ -314,8 +479,7 @@ pub enum PmListPacksFlag { OnlyDisabled, } impl PmListPacksFlag { - // is there a trait for this? - fn to_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::IncludeUninstalled => "-u", Self::OnlyEnabled => "-e", @@ -323,10 +487,10 @@ impl PmListPacksFlag { } } } -#[expect(clippy::to_string_trait_impl, reason = "This is not user-facing")] -impl ToString for PmListPacksFlag { - fn to_string(&self) -> String { - self.to_str().to_string() + +impl std::fmt::Display for PmListPacksFlag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) } } @@ -348,29 +512,25 @@ impl PmCommand { /// - isn't sorted /// - duplicates never _seem_ to happen, but don't assume uniqueness pub fn list_packages_sys( - mut self, - f: Option, + self, + flag: Option, user_id: Option, ) -> Result, String> { - let cmd = &mut self.0.0.0; - - cmd.args(["list", "packages", "-s"]); - if let Some(s) = f { - cmd.arg(s.to_str()); + let mut command = String::from("pm list packages -s"); + if let Some(f) = flag { + write!(&mut command, " {f}").ok(); } - if let Some(u) = user_id { - cmd.arg("--user"); - cmd.arg(u.to_string()); + if let Some(uid) = user_id { + write!(&mut command, " --user {uid}").ok(); } - self.0.0.run().map(|pack_ls| { - pack_ls + self.0.raw(&command).map(|output| { + output .lines() - .map(|p_ln| { - debug_assert!(p_ln.starts_with(PACK_PREFIX)); - let p = &p_ln[PACK_PREFIX.len()..]; - debug_assert!(PackageId::new(p).is_some()); - String::from(p) + .filter_map(|line| { + let pkg = line.strip_prefix(PACK_PREFIX)?; + debug_assert!(PackageId::new(pkg).is_some()); + Some(pkg.to_string()) }) .collect() }) @@ -380,56 +540,22 @@ impl PmCommand { /// /// - /// - - pub fn list_users(mut self) -> Result, String> { - self.0.0.0.args(["list", "users"]); + pub fn list_users(self) -> Result, String> { + // Expected shape: "UserInfo{::}[ running]" + // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/content/pm/UserInfo.java Ok(self .0 - .0 - .run()? + .raw("pm list users")? .lines() .skip(1) // omit header - .map(|ln| { - // this could be optimized by making more API-stability assumptions - let ln = ln.trim_ascii_start(); - let ln = ln.strip_prefix("UserInfo").unwrap_or(ln).trim_ascii_start(); - let ln = ln.strip_prefix('{').unwrap_or(ln).trim_ascii(); - //let run; - let ln = if let Some(l) = ln.strip_suffix("running") { - //run = true; - l.trim_ascii_end() - } else { - //run = false; - ln - }; - let ln = ln.strip_suffix('}').unwrap_or(ln).trim_ascii_end(); - // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/content/pm/UserInfo.java - // The format looks stable today, but google may change it in future Android versions - // (and very old Androids might differ). Keep parsing defensive. - // Expected shape: "UserInfo{::}[ running]" - - let mut comps = ln.split(':'); - - let id = comps - .next() - .expect("There must be at least 1 ':'-separated component") - .parse() - .expect("string assumed to be UID numeral"); - //let name = comps - // .next() - // .expect("There must be at least 2 ':'-separated components. 2nd is user-name"); - //let flags = u32::from_str_radix( - // comps.next().expect( - // "There must be at least 3 ':'-separated components. 3rd is user bit-flags", - // ), - // 16, - //) - //.expect("string assumed to be hexadecimal bit-flags"); - UserInfo { - id, - //name: name.into(), - //flags, - //running: run, - } + .filter_map(|line| { + // Defensive parsing: strip known prefixes/suffixes, extract ID + let s = line.trim_ascii(); + let s = s.strip_prefix("UserInfo{").unwrap_or(s); + let s = s.strip_suffix("running").unwrap_or(s).trim_ascii_end(); + let s = s.strip_suffix('}').unwrap_or(s); + let id = s.split(':').next()?.parse().ok()?; + Some(UserInfo { id }) }) .collect()) } @@ -496,4 +622,16 @@ mod tests { assert_ne!(PackageId::new(p_id), None); } } + + #[test] + fn backend_display() { + #[cfg(feature = "builtin-adb")] + assert_eq!(AdbBackend::Builtin.to_string(), "Builtin"); + assert_eq!(AdbBackend::System.to_string(), "System (adb)"); + } + + #[test] + fn backend_default_is_system() { + assert_eq!(AdbBackend::default(), AdbBackend::System); + } } diff --git a/crates/uad-core/src/config.rs b/crates/uad-core/src/config.rs index 5522b16..4539261 100644 --- a/crates/uad-core/src/config.rs +++ b/crates/uad-core/src/config.rs @@ -1,5 +1,6 @@ use crate::CACHE_DIR; use crate::CONFIG_DIR; +use crate::adb::AdbBackend; use crate::sync::User; use crate::utils::DisplayablePath; use log::error; @@ -22,6 +23,9 @@ pub struct GeneralSettings { pub theme: String, pub expert_mode: bool, pub backup_folder: PathBuf, + /// ADB backend to use: Builtin (no external dependencies) or System (uses installed adb) + #[serde(default)] + pub adb_backend: AdbBackend, } #[derive(Default, Debug, Clone)] @@ -49,6 +53,7 @@ impl Default for GeneralSettings { theme: DEFAULT_THEME.to_string(), expert_mode: false, backup_folder: CACHE_DIR.join("backups"), + adb_backend: AdbBackend::default(), } } } diff --git a/crates/uad-core/src/sync.rs b/crates/uad-core/src/sync.rs index 9bdd7cc..5a1ca55 100644 --- a/crates/uad-core/src/sync.rs +++ b/crates/uad-core/src/sync.rs @@ -1,5 +1,5 @@ use crate::{ - adb::{ACommand as AdbCommand, PM_CLEAR_PACK, PackageId}, + adb::{ACommand as AdbCommand, AdbBackend, PM_CLEAR_PACK, PackageId}, uad_lists::PackageState, }; use log::{error, info}; @@ -61,13 +61,25 @@ pub enum AdbError { /// This replaces the deprecated `adb_shell_command`. /// /// If `serial` is empty, it lets ADB choose the default device. +/// Uses the default ADB backend. pub fn run_adb_shell_action>( device_serial: S, action: &str, +) -> Result { + run_adb_shell_action_with_backend(device_serial, action, AdbBackend::default()) +} + +/// Run an arbitrary shell action via the typed ADB wrapper with a specific backend. +/// +/// If `serial` is empty, it lets ADB choose the default device. +pub fn run_adb_shell_action_with_backend>( + device_serial: S, + action: &str, + backend: AdbBackend, ) -> Result { let serial = device_serial.as_ref(); - match AdbCommand::new().shell(serial).raw(action) { + match AdbCommand::with_backend(backend).shell(serial).raw(action) { Ok(o) => { if ["Error", "Failure"].iter().any(|&e| o.contains(e)) { let friendly_msg = make_friendly_error_message(&o, action); @@ -213,7 +225,7 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option) -> // guarantee local to the sink instead of relying on each caller to sanitise. // Fail closed: emit no command for a malformed name rather than an injectable // device-shell string. - if PackageId::new(package).is_some() { + if PackageId::new(package).is_none() { error!("request_builder: refusing invalid package name: {package:?}"); return Vec::new(); } @@ -224,23 +236,48 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option) -> .collect() } +#[must_use] +fn sanitize_device_prop(raw: &str) -> String { + raw.chars() + .filter(|c| !c.is_control()) + .collect::() + .trim() + .to_string() +} + +#[must_use] +fn parse_sdk_version(raw: &str) -> Option { + let sanitized = sanitize_device_prop(raw); + let digits: String = sanitized + .chars() + .skip_while(|c| !c.is_ascii_digit()) + .take_while(char::is_ascii_digit) + .collect(); + + if digits.is_empty() { + None + } else { + digits.parse().ok() + } +} + /// Get the model by querying the `ro.product.model` property. /// /// If `serial` is empty, it lets ADB choose the default device. #[must_use] pub fn get_device_model(serial: &str) -> String { - AdbCommand::new() - .shell(serial) - .getprop("ro.product.model") - .unwrap_or_else(|err| { + match AdbCommand::new().shell(serial).getprop("ro.product.model") { + Ok(model) => sanitize_device_prop(&model), + Err(err) => { eprintln!("ERROR: {err}"); error!("{err}"); if err.contains("adb: no devices/emulators found") { "no devices/emulators found".to_string() } else { - err + sanitize_device_prop(&err) } - }) + } + } } /// Get the brand by querying the `ro.product.brand` property. @@ -251,8 +288,7 @@ pub fn get_device_brand(serial: &str) -> String { AdbCommand::new() .shell(serial) .getprop("ro.product.brand") - // `trim` is just-in-case - .map(|s| s.trim().to_string()) + .map(|s| sanitize_device_prop(&s)) .unwrap_or_default() } @@ -266,7 +302,14 @@ pub fn get_android_sdk(device_serial: &str) -> u8 { .shell(device_serial) .getprop("ro.build.version.sdk") .map_or(0, |sdk| { - sdk.parse().expect("SDK version numeral must be valid") + if let Some(v) = parse_sdk_version(&sdk) { + v + } else { + error!( + "Invalid SDK version {sdk:?} for device '{device_serial}'. Falling back to 0" + ); + 0 + } }) } @@ -452,31 +495,39 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec { .unwrap_or_default() } -/// This matches serials (`getprop ro.serialno`) -/// that are authorized by the user. +/// Get list of connected devices using a specific ADB backend. #[must_use] -pub fn get_devices_list() -> Vec { +pub fn get_devices_list(backend: AdbBackend) -> Vec { retry( Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }), - || match AdbCommand::new().devices() { + || match AdbCommand::with_backend(backend).devices() { Ok(devices) => { let mut device_list: Vec = vec![]; if devices.iter().all(|(_, stat)| stat != "device") { return OperationResult::Retry(vec![]); } - for device in devices { - let serial = &device.0; + for (serial, status) in devices { + if status != "device" { + continue; + } device_list.push(Phone { - model: format!("{} {}", get_device_brand(serial), get_device_model(serial)), - android_sdk: get_android_sdk(serial), - user_list: list_users_idx_prot(serial), - adb_id: serial.clone(), + model: format!( + "{} {}", + get_device_brand(&serial), + get_device_model(&serial) + ), + android_sdk: get_android_sdk(&serial), + user_list: list_users_idx_prot(&serial), + adb_id: serial, }); } + if device_list.is_empty() { + return OperationResult::Retry(vec![]); + } OperationResult::Ok(device_list) } Err(err) => { - error!("get_devices_list() -> {err}"); + error!("get_devices_list(backend) -> {err}"); let test: Vec = vec![]; OperationResult::Retry(test) } @@ -486,8 +537,8 @@ pub fn get_devices_list() -> Vec { } #[must_use] -pub fn initial_load() -> bool { - match AdbCommand::new().devices() { +pub fn initial_load(backend: AdbBackend) -> bool { + match AdbCommand::with_backend(backend).devices() { Ok(_devices) => true, Err(_err) => false, } diff --git a/crates/uad-gui/Cargo.toml b/crates/uad-gui/Cargo.toml index 4a50636..0ef7f42 100644 --- a/crates/uad-gui/Cargo.toml +++ b/crates/uad-gui/Cargo.toml @@ -15,14 +15,15 @@ name = "uad-ng" path = "src/main.rs" [features] -default = ["wgpu", "self-update", "img"] -wgpu = [] # Iced/wgpu is default -self-update = ["flate2", "tar"] +default = ["wgpu", "self-update", "img", "builtin-adb"] +builtin-adb = ["uad-core/builtin-adb"] +self-update = ["flate2", "tar", "uad-core/self-update"] no-self-update = [] +wgpu = [] # Iced/wgpu is default img = ["image", "iced/image"] [dependencies] -uad-core = { path = "../uad-core", features = ["self-update"] } +uad-core = { path = "../uad-core", default-features = false } iced.workspace = true image = { workspace = true, optional = true } rfd.workspace = true diff --git a/crates/uad-gui/src/gui.rs b/crates/uad-gui/src/gui.rs index 925aedf..75994f1 100644 --- a/crates/uad-gui/src/gui.rs +++ b/crates/uad-gui/src/gui.rs @@ -83,21 +83,27 @@ impl UadGui { reason = "required by iced's Application trait interface" )] fn title(&self) -> String { - self.selected_device.as_ref().map_or_else( - || FULL_NAME.to_string(), - |device| format!("{FULL_NAME} - {}", device.model), - ) + if self.selected_device.is_some() { + format!("{FULL_NAME} - device connected") + } else { + FULL_NAME.to_string() + } } fn new() -> (Self, Task) { + let app = Self::default(); + let backend = app.settings_view.general.adb_backend; ( - Self::default(), + app, Task::batch([ // Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`. font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice()) .map(Message::FontLoaded), - Task::perform(async { initial_load() }, Message::ADBSatisfied), - Task::perform(async { get_devices_list() }, Message::LoadDevices), + Task::perform(async move { initial_load(backend) }, Message::ADBSatisfied), + Task::perform( + async move { get_devices_list(backend) }, + Message::LoadDevices, + ), Task::perform( async move { get_latest_release() }, Message::GetLatestRelease, @@ -196,16 +202,21 @@ impl UadGui { } Message::RefreshButtonPressed => { self.apps_view = AppsView::default(); + let backend = self.settings_view.general.adb_backend; #[expect(unused_must_use, reason = "side-effect")] { self.update(Message::AppsAction(AppsMessage::ADBSatisfied( self.adb_satisfied, ))); } - Task::perform(async { get_devices_list() }, Message::LoadDevices) + Task::perform( + async move { get_devices_list(backend) }, + Message::LoadDevices, + ) } Message::RebootButtonPressed => { self.apps_view = AppsView::default(); + let backend = self.settings_view.general.adb_backend; let serial = match &self.selected_device { Some(d) => d.adb_id.clone(), _ => String::default(), @@ -213,7 +224,7 @@ impl UadGui { self.selected_device = None; self.devices_list = vec![]; Task::perform( - async { adb::ACommand::new().shell(serial).reboot() }, + async move { adb::ACommand::with_backend(backend).shell(serial).reboot() }, |_| Message::Nothing, ) } diff --git a/crates/uad-gui/src/views/about.rs b/crates/uad-gui/src/views/about.rs index 42cb723..93d81df 100644 --- a/crates/uad-gui/src/views/about.rs +++ b/crates/uad-gui/src/views/about.rs @@ -3,10 +3,8 @@ use crate::theme::Theme; use crate::{gui::UpdateState, style, widgets::text}; use iced::widget::{Space, column, container, row}; use iced::{Alignment, Element, Length, Renderer}; -use log::error; use std::path::PathBuf; use uad_core::CACHE_DIR; -use uad_core::adb; use uad_core::uad_lists::LIST_FNAME; use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url}; @@ -95,38 +93,10 @@ impl About { .spacing(10) .width(550); - /* - There's no need to fetch this info every time the view is updated, - we could cache it in a `static` `LazyLock`. - - But what if the system updates ADB while the app is running? - the numbers will be out of sync! - - However, the server will still be the "old" version - until it's killed - */ - let adb_version_text = text(match adb::ACommand::new().version() { - Ok(s) => s - .lines() - .nth(0) - .unwrap_or_else(|| unreachable!()) - // This allocation is good. - // If it was a ref, the app would hold the entire string - // instead of the relevant slice. - .to_string(), - Err(e) => { - error!("{e}"); - "Couldn't fetch ADB version. Is it installed?".into() - // satisfy `match` by inferring the type of the `Ok` arm - } - }) - .width(250); - let adb_version_row = row![adb_version_text].align_y(Alignment::Center).width(550); - #[cfg(feature = "self-update")] - let update_column = column![uad_list_row, self_update_row, adb_version_row]; + let update_column = column![uad_list_row, self_update_row]; #[cfg(not(feature = "self-update"))] - let update_column = column![uad_list_row, adb_version_row]; + let update_column = column![uad_list_row]; let update_column = update_column.align_x(Alignment::Center).spacing(10); diff --git a/crates/uad-gui/src/views/list.rs b/crates/uad-gui/src/views/list.rs index a9792dd..8701e92 100644 --- a/crates/uad-gui/src/views/list.rs +++ b/crates/uad-gui/src/views/list.rs @@ -5,7 +5,7 @@ use crate::widgets::navigation_menu::ICONS; use log::{error, info, warn}; use std::path::PathBuf; use uad_core::config::DeviceSettings; -use uad_core::sync::{AdbError, Phone, User, apply_pkg_state_commands}; +use uad_core::sync::{AdbError, CorePackage, Phone, User, apply_pkg_state_commands}; use uad_core::uad_lists::{ Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists, }; @@ -130,6 +130,33 @@ impl From for SummaryEntry { } impl List { + #[inline] + fn current_user_index(&self) -> usize { + self.selected_user.unwrap_or_default().index + } + + #[inline] + fn refilter(&mut self) -> Task { + Self::filter_package_lists(self); + Task::none() + } + + #[inline] + fn map_core_to_row(uad_list: &PackageHashMap, core: &CorePackage) -> PackageRow { + let list = uad_list + .get(&core.name) + .map_or(UadList::Unlisted, |p| p.list); + PackageRow::new( + &core.name, + &core.description, + core.removal, + core.state, + list, + false, + false, + ) + } + pub fn update( &mut self, settings: &mut Settings, @@ -216,7 +243,7 @@ impl List { waiting_view("Finding connected devices...", None, style::Text::Default) } else { waiting_view( - "ADB is not installed on your system, install ADB and relaunch application.", + "No device connection detected. Connect a device with USB debugging enabled and authorized, then relaunch.", Some(button("Read on how to get started.") .on_press(Message::GoToUrl(PathBuf::from( "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Getting-started", @@ -329,12 +356,13 @@ impl List { settings: &Settings, selected_device: &Phone, ) -> Element<'_, Message, Theme, Renderer> { + let i_user = self.current_user_index(); let packages = self .filtered_packages .iter() .fold(column![].spacing(6), |col, &i| { col.push( - self.phone_packages[self.selected_user.unwrap_or_default().index][i] + self.phone_packages[i_user][i] .view(settings, selected_device) .map(move |msg| Message::List(i, msg)), ) @@ -748,8 +776,7 @@ impl List { .selected_removal .expect("removal recommendation must be selected"); - self.filtered_packages = self.phone_packages - [self.selected_user.expect("User must be selected").index] + self.filtered_packages = self.phone_packages[self.current_user_index()] .iter() // we must filter the indices associated with pack-rows, // that's why `enumerate` is before `filter`. @@ -776,7 +803,7 @@ impl List { vec![ fetch_packages(&uad_list, serial, None) .into_iter() - .map(PackageRow::from) + .map(|core| Self::map_core_to_row(&uad_list, &core)) .collect(), ] } else { @@ -785,7 +812,7 @@ impl List { .map(|user| { fetch_packages(&uad_list, serial, Some(user.id)) .into_iter() - .map(PackageRow::from) + .map(|core| Self::map_core_to_row(&uad_list, &core)) .collect() }) .collect() @@ -841,7 +868,7 @@ impl List { } fn on_restoring_device(&mut self, output: Result) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); if let Ok(p) = output { self.loading_state = LoadingState::RestoringDevice(self.phone_packages[i_user][p.index].name.clone()); @@ -886,7 +913,7 @@ impl List { } fn on_apply_filters(&mut self, packages: Vec>) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); self.phone_packages = packages; self.filtered_packages = (0..self.phone_packages[i_user].len()).collect(); self.selected_removal = Some(Removal::Recommended); @@ -906,7 +933,7 @@ impl List { selected_device: &mut Phone, list_update_state: &mut UadListState, ) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); for i in self.filtered_packages.clone() { if self.phone_packages[i_user][i].selected != selected { #[expect(unused_must_use, reason = "side-effect")] @@ -924,26 +951,22 @@ impl List { fn on_search_input_changed(&mut self, letter: String) -> Task { self.input_value = letter; - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_list_selected(&mut self, list: UadList) -> Task { self.selected_list = Some(list); - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_package_state_selected(&mut self, package_state: PackageState) -> Task { self.selected_package_state = Some(package_state); - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_removal_selected(&mut self, removal: Removal) -> Task { self.selected_removal = Some(removal); - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_list_row( @@ -953,7 +976,7 @@ impl List { settings: &Settings, selected_device: &mut Phone, ) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); #[expect(unused_must_use, reason = "side-effect")] { self.phone_packages[i_user][i_package] @@ -1031,8 +1054,7 @@ impl List { self.selected_user = Some(user); self.fallback_notifications.clear(); self.filtered_packages = (0..self.phone_packages[user.index].len()).collect(); - Self::filter_package_lists(self); - Task::none() + self.refilter() } #[allow( diff --git a/crates/uad-gui/src/views/settings.rs b/crates/uad-gui/src/views/settings.rs index c19b999..5760c04 100644 --- a/crates/uad-gui/src/views/settings.rs +++ b/crates/uad-gui/src/views/settings.rs @@ -15,6 +15,7 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment}; use log::{debug, error, info}; use std::path::PathBuf; use uad_core::{ + adb::{ACommand, AdbBackend}, config::{BackupSettings, Config, DeviceSettings, GeneralSettings}, save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup}, sync::{ @@ -35,15 +36,23 @@ pub struct Settings { pub device: DeviceSettings, is_loading: bool, modal: Option, + /// Cached ADB version string for display + adb_version: String, } impl Default for Settings { fn default() -> Self { + let general = Config::load_configuration_file().general; + // Fetch initial ADB version + let adb_version = ACommand::with_backend(general.adb_backend) + .version() + .unwrap_or_else(|e| format!("Error: {e}")); Self { - general: Config::load_configuration_file().general, + general, device: DeviceSettings::default(), is_loading: false, modal: None, + adb_version, } } } @@ -55,6 +64,8 @@ pub enum Message { DisableMode(bool), MultiUserMode(bool), ApplyTheme(Theme), + ApplyAdbBackend(AdbBackend), + AdbVersionFetched(Result), UrlPressed(PathBuf), BackupSelected(DisplayablePath), BackupDevice, @@ -83,6 +94,8 @@ impl Settings { Message::DisableMode(toggled) => self.handle_disable_mode(phone, toggled), Message::MultiUserMode(toggled) => self.handle_multi_user_mode(phone, toggled), Message::ApplyTheme(theme) => self.handle_apply_theme(phone, theme), + Message::ApplyAdbBackend(backend) => self.handle_apply_adb_backend(backend), + Message::AdbVersionFetched(result) => self.handle_adb_version_fetched(result), Message::UrlPressed(url) => Self::handle_url_pressed(url), Message::LoadDeviceSettings => self.handle_load_device_settings(phone), Message::BackupSelected(d_path) => self.handle_backup_selected(d_path), @@ -138,6 +151,28 @@ impl Settings { Task::none() } + fn handle_apply_adb_backend(&mut self, backend: AdbBackend) -> Task { + self.general.adb_backend = backend; + self.adb_version = "Fetching...".to_string(); + debug!("Config change: {self:?}"); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); + info!("ADB backend changed to: {backend}"); + // Fetch version asynchronously + Task::perform( + async move { ACommand::with_backend(backend).version() }, + Message::AdbVersionFetched, + ) + } + + fn handle_adb_version_fetched(&mut self, result: Result) -> Task { + self.adb_version = match result { + Ok(version) => version, + Err(e) => format!("Error: {e}"), + }; + Task::none() + } + fn handle_url_pressed(url: PathBuf) -> Task { open_url(url); Task::none() @@ -360,6 +395,8 @@ impl Settings { self.theme_container(), text("General").size(26), self.general_container(), + text("ADB").size(26), + self.adb_container(), text("Current device").size(26), Self::no_device_container(), text("Backup / Restore").size(26), @@ -380,6 +417,8 @@ impl Settings { self.theme_container(), text("General").size(26), self.general_container(), + text("ADB").size(26), + self.adb_container(), text("Current device").size(26), Self::warning_container(phone), self.device_specific_container(phone), @@ -459,6 +498,50 @@ impl Settings { .into() } + fn adb_container(&self) -> Element<'_, Message, Theme, Renderer> { + let backend_label = text("Backend:").size(16); + let radio_btn_backend = + AdbBackend::ALL + .iter() + .fold(row![].spacing(10), |row_content, option| { + row_content.push( + radio( + format!("{option}"), + *option, + Some(self.general.adb_backend), + Message::ApplyAdbBackend, + ) + .size(20), + ) + }); + + let backend_row = row![backend_label, radio_btn_backend] + .spacing(10) + .align_y(Alignment::Center); + + #[cfg(feature = "builtin-adb")] + let backend_description = "System: Uses your installed adb binary. Builtin: Uses embedded ADB (no external dependencies)."; + #[cfg(not(feature = "builtin-adb"))] + let backend_description = "System: Uses your installed adb binary."; + let backend_descr = text(backend_description).style(style::Text::Commentary); + + let version_row = row![ + text("Version: ").size(14), + text(&self.adb_version) + .size(14) + .style(style::Text::Commentary), + ] + .spacing(5) + .align_y(Alignment::Center); + + container(column![backend_row, backend_descr, version_row,].spacing(10)) + .padding(10) + .width(Length::Fill) + .height(Length::Shrink) + .style(style::Container::Frame) + .into() + } + fn warning_container(phone: &Phone) -> Element<'static, Message, Theme, Renderer> { container( row![ diff --git a/flake.lock b/flake.lock index b3b5542..d212ee9 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "lastModified": 1781607440, + "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", "type": "github" }, "original": {