diff --git a/.github/workflows/python-build-test.yml b/.github/workflows/python-build-test.yml index 3f20093..c8a1031 100644 --- a/.github/workflows/python-build-test.yml +++ b/.github/workflows/python-build-test.yml @@ -45,7 +45,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.8", "3.9", "3.10" ] + python-version: [ "3.8", "3.9", "3.10", "3.11" ] + rust-version: [ "1.70.0", "stable" ] fail-fast: false steps: - name: Check out from Git @@ -62,9 +63,15 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: clippy,rustfmt - - name: Rust Lints - run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings + toolchain: ${{ matrix.rust-version }} - name: Rust Build - run: cd rust && cargo build --all-targets + run: cd rust && cargo build --all-targets && cargo build --all-features --all-targets + # Lints after build so what clippy needs is already built + - name: Rust Lints + run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings - name: Rust Tests - run: cd rust && cargo test \ No newline at end of file + run: cd rust && cargo test + # At some point, hook up publishing the binary. For now, just make sure it builds. + # Once we're ready to publish binaries, this should be built with `--release`. + - name: Build Bumble CLI + run: cd rust && cargo build --features bumble-tools --bin bumble \ No newline at end of file diff --git a/bumble/drivers/__init__.py b/bumble/drivers/__init__.py index 2decab7..bb63dd7 100644 --- a/bumble/drivers/__init__.py +++ b/bumble/drivers/__init__.py @@ -21,6 +21,9 @@ like loading firmware after a cold start. # ----------------------------------------------------------------------------- import abc import logging +import pathlib +import platform +import platformdirs from . import rtk @@ -66,3 +69,22 @@ async def get_driver_for_host(host): return driver return None + + +def project_data_dir() -> pathlib.Path: + """ + Returns: + A path to an OS-specific directory for bumble data. The directory is created if + it doesn't exist. + """ + if platform.system() == 'Darwin': + # platformdirs doesn't handle macOS right: it doesn't assemble a bundle id + # out of author & project + return platformdirs.user_data_path( + appname='com.google.bumble', ensure_exists=True + ) + else: + # windows and linux don't use the com qualifier + return platformdirs.user_data_path( + appname='bumble', appauthor='google', ensure_exists=True + ) diff --git a/bumble/drivers/rtk.py b/bumble/drivers/rtk.py index e7e3c1f..0bce67d 100644 --- a/bumble/drivers/rtk.py +++ b/bumble/drivers/rtk.py @@ -446,6 +446,11 @@ class Driver: # When the environment variable is set, don't look elsewhere return None + # Then, look where the firmware download tool writes by default + if (path := rtk_firmware_dir() / file_name).is_file(): + logger.debug(f"{file_name} found in project data dir") + return path + # Then, look in the package's driver directory if (path := pathlib.Path(__file__).parent / "rtk_fw" / file_name).is_file(): logger.debug(f"{file_name} found in package dir") @@ -646,3 +651,16 @@ class Driver: await self.download_firmware() await self.host.send_command(HCI_Reset_Command(), check_result=True) logger.info(f"loaded FW image {self.driver_info.fw_name}") + + +def rtk_firmware_dir() -> pathlib.Path: + """ + Returns: + A path to a subdir of the project data dir for Realtek firmware. + The directory is created if it doesn't exist. + """ + from bumble.drivers import project_data_dir + + p = project_data_dir() / "firmware" / "realtek" + p.mkdir(parents=True, exist_ok=True) + return p diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 399f916..024a1a2 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,33 +19,32 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -67,9 +66,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", "windows-sys", @@ -77,9 +76,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "atty" @@ -100,9 +99,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -113,6 +112,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" + [[package]] name = "bitflags" version = "1.3.2" @@ -121,16 +126,17 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bumble" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.4.1", + "directories", "env_logger", "hex", "itertools", @@ -142,6 +148,7 @@ dependencies = [ "pyo3", "pyo3-asyncio", "rand", + "reqwest", "rusb", "strum", "strum_macros", @@ -150,6 +157,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + [[package]] name = "bytes" version = "1.4.0" @@ -158,9 +171,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -188,9 +201,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" dependencies = [ "clap_builder", "clap_derive", @@ -199,26 +212,26 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" dependencies = [ "anstream", "anstyle", - "clap_lex 0.5.0", + "clap_lex 0.5.1", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -232,9 +245,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "colorchoice" @@ -242,12 +255,58 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -263,9 +322,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -288,6 +347,36 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.28" @@ -344,7 +433,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -390,9 +479,28 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] [[package]] name = "hashbrown" @@ -427,12 +535,93 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -451,9 +640,15 @@ checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "inventory" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53088c87cf71c9d4f3372a2cb9eea1e7b8a0b1bf8b7f7d23fe5b76dbb07e63b" +checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" @@ -475,6 +670,21 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -517,15 +727,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" [[package]] name = "memoffset" @@ -545,6 +755,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -572,17 +788,34 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.26.2" +name = "native-tls" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", "pin-utils", - "static_assertions", ] [[package]] @@ -607,9 +840,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -620,6 +853,56 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.0", + "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.29", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_str_bytes" version = "6.5.1" @@ -650,16 +933,22 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets", ] [[package]] -name = "pin-project-lite" -version = "0.2.10" +name = "percent-encoding" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -778,9 +1067,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -815,6 +1104,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -825,10 +1123,21 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.9.1" +name = "redox_users" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", @@ -838,9 +1147,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", @@ -849,15 +1158,52 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] [[package]] name = "rusb" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44a8c36914f9b1a3be712c1dfa48c9b397131f9a75707e570a391735f785c5d1" +checksum = "45fff149b6033f25e825cbb7b2c625a11ee8e6dac09264d49beb125e39aa97bf" dependencies = [ "libc", "libusb1-sys", @@ -871,11 +1217,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.6" +version = "0.38.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" +checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", @@ -888,12 +1234,93 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[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 = "signal-hook-registry" version = "1.4.1" @@ -905,9 +1332,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -929,10 +1356,14 @@ dependencies = [ ] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "socket2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] [[package]] name = "strsim" @@ -948,15 +1379,15 @@ checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -972,9 +1403,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -989,13 +1420,13 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys", ] @@ -1017,31 +1448,45 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] -name = "tokio" -version = "1.29.1" +name = "tinyvec" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -1050,7 +1495,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -1063,21 +1508,103 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unindent" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -1090,12 +1617,97 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[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.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.29", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1138,9 +1750,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1153,42 +1765,52 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 523074c..c12709f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,7 +10,7 @@ documentation = "https://docs.rs/crate/bumble" authors = ["Marshall Pierce "] keywords = ["bluetooth", "ble"] categories = ["api-bindings", "network-programming"] -rust-version = "1.69.0" +rust-version = "1.70.0" [dependencies] pyo3 = { version = "0.18.3", features = ["macros"] } @@ -23,7 +23,16 @@ hex = "0.4.3" itertools = "0.11.0" lazy_static = "1.4.0" thiserror = "1.0.41" + +# CLI anyhow = { version = "1.0.71", optional = true } +clap = { version = "4.3.3", features = ["derive"], optional = true } +directories = { version = "5.0.1", optional = true } +owo-colors = { version = "3.5.0", optional = true } +reqwest = { version = "0.11.20", features = ["blocking"], optional = true } +rusb = { version = "0.9.2", optional = true } +log = { version = "0.4.19", optional = true } +env_logger = { version = "0.10.0", optional = true } [dev-dependencies] tokio = { version = "1.28.2", features = ["full"] } @@ -32,17 +41,25 @@ nix = "0.26.2" anyhow = "1.0.71" pyo3 = { version = "0.18.3", features = ["macros", "anyhow"] } pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime", "attributes", "testing"] } +rusb = "0.9.2" +rand = "0.8.5" clap = { version = "4.3.3", features = ["derive"] } owo-colors = "3.5.0" log = "0.4.19" env_logger = "0.10.0" -rusb = "0.9.2" -rand = "0.8.5" + +[package.metadata.docs.rs] +rustdoc-args = ["--generate-link-to-definition"] [[bin]] name = "gen-assigned-numbers" path = "tools/gen_assigned_numbers.rs" -required-features = ["bumble-dev-tools"] +required-features = ["bumble-codegen"] + +[[bin]] +name = "bumble" +path = "src/main.rs" +required-features = ["bumble-tools"] # test entry point that uses pyo3_asyncio's test harness [[test]] @@ -53,4 +70,7 @@ harness = false [features] anyhow = ["pyo3/anyhow"] pyo3-asyncio-attributes = ["pyo3-asyncio/attributes"] -bumble-dev-tools = ["dep:anyhow"] \ No newline at end of file +bumble-codegen = ["dep:anyhow"] +# separate feature for CLI so that dependencies don't spend time building these +bumble-tools = ["dep:clap", "anyhow", "dep:anyhow", "dep:directories", "pyo3-asyncio-attributes", "dep:owo-colors", "dep:reqwest", "dep:rusb", "dep:log", "dep:env_logger"] +default = [] \ No newline at end of file diff --git a/rust/README.md b/rust/README.md index 2684875..23dec03 100644 --- a/rust/README.md +++ b/rust/README.md @@ -5,7 +5,8 @@ Rust wrappers around the [Bumble](https://github.com/google/bumble) Python API. Method calls are mapped to the equivalent Python, and return types adapted where relevant. -See the `examples` directory for usage. +See the CLI in `src/main.rs` or the `examples` directory for how to use the +Bumble API. # Usage @@ -27,6 +28,15 @@ PYTHONPATH=..:~/.virtualenvs/bumble/lib/python3.10/site-packages/ \ Run the corresponding `battery_server` Python example, and launch an emulator in Android Studio (currently, Canary is required) to run netsim. +# CLI + +Explore the available subcommands: + +``` +PYTHONPATH=..:[virtualenv site-packages] \ + cargo run --features bumble-tools --bin bumble -- --help +``` + # Development Run the tests: @@ -43,7 +53,7 @@ cargo clippy --all-targets ## Code gen -To have the fastest startup while keeping the build simple, code gen for +To have the fastest startup while keeping the build simple, code gen for assigned numbers is done with the `gen_assigned_numbers` tool. It should be re-run whenever the Python assigned numbers are changed. To ensure that the generated code is kept up to date, the Rust data is compared to the Python @@ -52,5 +62,5 @@ in tests at `pytests/assigned_numbers.rs`. To regenerate the assigned number tables based on the Python codebase: ``` -PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features bumble-dev-tools +PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features bumble-codegen ``` \ No newline at end of file diff --git a/rust/pytests/wrapper.rs b/rust/pytests/wrapper.rs index 333005b..8f69dd7 100644 --- a/rust/pytests/wrapper.rs +++ b/rust/pytests/wrapper.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use bumble::wrapper::transport::Transport; +use bumble::wrapper::{drivers::rtk::DriverInfo, transport::Transport}; use nix::sys::stat::Mode; use pyo3::PyResult; @@ -29,3 +29,9 @@ async fn fifo_transport_can_open() -> PyResult<()> { Ok(()) } + +#[pyo3_asyncio::tokio::test] +async fn realtek_driver_info_all_drivers() -> PyResult<()> { + assert_eq!(12, DriverInfo::all_drivers()?.len()); + Ok(()) +} diff --git a/rust/resources/test/firmware/realtek/README.md b/rust/resources/test/firmware/realtek/README.md new file mode 100644 index 0000000..4c49608 --- /dev/null +++ b/rust/resources/test/firmware/realtek/README.md @@ -0,0 +1,4 @@ +This dir contains samples firmware images in the format used for Realtek chips, +but with repetitions of the length of the section as a little-endian 32-bit int +for the patch data instead of actual firmware, since we only need the structure +to test parsing. \ No newline at end of file diff --git a/rust/resources/test/firmware/realtek/rtl8723b_fw_structure.bin b/rust/resources/test/firmware/realtek/rtl8723b_fw_structure.bin new file mode 100644 index 0000000..077cdc3 Binary files /dev/null and b/rust/resources/test/firmware/realtek/rtl8723b_fw_structure.bin differ diff --git a/rust/resources/test/firmware/realtek/rtl8761bu_fw_structure.bin b/rust/resources/test/firmware/realtek/rtl8761bu_fw_structure.bin new file mode 100644 index 0000000..94df0ba Binary files /dev/null and b/rust/resources/test/firmware/realtek/rtl8761bu_fw_structure.bin differ diff --git a/rust/src/cli/firmware/mod.rs b/rust/src/cli/firmware/mod.rs new file mode 100644 index 0000000..1fa1417 --- /dev/null +++ b/rust/src/cli/firmware/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) mod rtk; diff --git a/rust/src/cli/firmware/rtk.rs b/rust/src/cli/firmware/rtk.rs new file mode 100644 index 0000000..f5524a4 --- /dev/null +++ b/rust/src/cli/firmware/rtk.rs @@ -0,0 +1,265 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Realtek firmware tools + +use crate::{Download, Source}; +use anyhow::anyhow; +use bumble::wrapper::{ + drivers::rtk::{Driver, DriverInfo, Firmware}, + host::{DriverFactory, Host}, + transport::Transport, +}; +use owo_colors::{colors::css, OwoColorize}; +use pyo3::PyResult; +use std::{fs, path}; + +pub(crate) async fn download(dl: Download) -> PyResult<()> { + let data_dir = dl + .output_dir + .or_else(|| { + directories::ProjectDirs::from("com", "google", "bumble") + .map(|pd| pd.data_local_dir().join("firmware").join("realtek")) + }) + .unwrap_or_else(|| { + eprintln!("Could not determine standard data directory"); + path::PathBuf::from(".") + }); + fs::create_dir_all(&data_dir)?; + + let (base_url, uses_bin_suffix) = match dl.source { + Source::LinuxKernel => ("https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/rtl_bt", true), + Source::RealtekOpensource => ("https://github.com/Realtek-OpenSource/android_hardware_realtek/raw/rtk1395/bt/rtkbt/Firmware/BT", false), + Source::LinuxFromScratch => ("https://anduin.linuxfromscratch.org/sources/linux-firmware/rtl_bt", true), + }; + + println!("Downloading"); + println!("{} {}", "FROM:".green(), base_url); + println!("{} {}", "TO:".green(), data_dir.to_string_lossy()); + + let url_for_file = |file_name: &str| { + let url_suffix = if uses_bin_suffix { + file_name + } else { + file_name.trim_end_matches(".bin") + }; + + let mut url = base_url.to_string(); + url.push('/'); + url.push_str(url_suffix); + url + }; + + let to_download = if let Some(single) = dl.single { + vec![( + format!("{single}_fw.bin"), + Some(format!("{single}_config.bin")), + false, + )] + } else { + DriverInfo::all_drivers()? + .iter() + .map(|di| Ok((di.firmware_name()?, di.config_name()?, di.config_needed()?))) + .collect::>>()? + }; + + let client = SimpleClient::new(); + + for (fw_filename, config_filename, config_needed) in to_download { + println!("{}", "---".yellow()); + let fw_path = data_dir.join(&fw_filename); + let config_path = config_filename.as_ref().map(|f| data_dir.join(f)); + + if fw_path.exists() && !dl.overwrite { + println!( + "{}", + format!("{} already exists, skipping", fw_path.to_string_lossy()) + .fg::() + ); + continue; + } + if let Some(cp) = config_path.as_ref() { + if cp.exists() && !dl.overwrite { + println!( + "{}", + format!("{} already exists, skipping", cp.to_string_lossy()) + .fg::() + ); + continue; + } + } + + let fw_contents = match client.get(&url_for_file(&fw_filename)).await { + Ok(data) => { + println!("Downloaded {}: {} bytes", fw_filename, data.len()); + data + } + Err(e) => { + eprintln!( + "{} {} {:?}", + "Failed to download".red(), + fw_filename.red(), + e + ); + continue; + } + }; + + let config_contents = if let Some(cn) = &config_filename { + match client.get(&url_for_file(cn)).await { + Ok(data) => { + println!("Downloaded {}: {} bytes", cn, data.len()); + Some(data) + } + Err(e) => { + if config_needed { + eprintln!("{} {} {:?}", "Failed to download".red(), cn.red(), e); + continue; + } else { + eprintln!( + "{}", + format!("No config available as {cn}").fg::() + ); + None + } + } + } + } else { + None + }; + + fs::write(&fw_path, &fw_contents)?; + if !dl.no_parse && config_filename.is_some() { + println!("{} {}", "Parsing:".cyan(), &fw_filename); + match Firmware::parse(&fw_contents).map_err(|e| anyhow!("Parse error: {:?}", e)) { + Ok(fw) => dump_firmware_desc(&fw), + Err(e) => { + eprintln!( + "{} {:?}", + "Could not parse firmware:".fg::(), + e + ); + } + } + } + if let Some((cp, cd)) = config_path + .as_ref() + .and_then(|p| config_contents.map(|c| (p, c))) + { + fs::write(cp, &cd)?; + } + } + + Ok(()) +} + +pub(crate) fn parse(firmware_path: &path::Path) -> PyResult<()> { + let contents = fs::read(firmware_path)?; + let fw = Firmware::parse(&contents) + // squish the error into a string to avoid the error type requiring that the input be + // 'static + .map_err(|e| anyhow!("Parse error: {:?}", e))?; + + dump_firmware_desc(&fw); + + Ok(()) +} + +pub(crate) async fn info(transport: &str, force: bool) -> PyResult<()> { + let transport = Transport::open(transport).await?; + + let mut host = Host::new(transport.source()?, transport.sink()?)?; + host.reset(DriverFactory::None).await?; + + if !force && !Driver::check(&host).await? { + println!("USB device not supported by this RTK driver"); + } else if let Some(driver_info) = Driver::driver_info_for_host(&host).await? { + println!("Driver:"); + println!(" {:10} {:04X}", "ROM:", driver_info.rom()?); + println!(" {:10} {}", "Firmware:", driver_info.firmware_name()?); + println!( + " {:10} {}", + "Config:", + driver_info.config_name()?.unwrap_or_default() + ); + } else { + println!("Firmware already loaded or no supported driver for this device.") + } + + Ok(()) +} + +pub(crate) async fn load(transport: &str, force: bool) -> PyResult<()> { + let transport = Transport::open(transport).await?; + + let mut host = Host::new(transport.source()?, transport.sink()?)?; + host.reset(DriverFactory::None).await?; + + match Driver::for_host(&host, force).await? { + None => { + eprintln!("Firmware already loaded or no supported driver for this device."); + } + Some(mut d) => d.download_firmware().await?, + }; + + Ok(()) +} + +pub(crate) async fn drop(transport: &str) -> PyResult<()> { + let transport = Transport::open(transport).await?; + + let mut host = Host::new(transport.source()?, transport.sink()?)?; + host.reset(DriverFactory::None).await?; + + Driver::drop_firmware(&mut host).await?; + + Ok(()) +} + +fn dump_firmware_desc(fw: &Firmware) { + println!( + "Firmware: version=0x{:08X} project_id=0x{:04X}", + fw.version(), + fw.project_id() + ); + for p in fw.patches() { + println!( + " Patch: chip_id=0x{:04X}, {} bytes, SVN Version={:08X}", + p.chip_id(), + p.contents().len(), + p.svn_version() + ) + } +} + +struct SimpleClient { + client: reqwest::Client, +} + +impl SimpleClient { + fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + async fn get(&self, url: &str) -> anyhow::Result> { + let resp = self.client.get(url).send().await?; + if !resp.status().is_success() { + return Err(anyhow!("Bad status: {}", resp.status())); + } + let bytes = resp.bytes().await?; + Ok(bytes.as_ref().to_vec()) + } +} diff --git a/rust/src/cli/mod.rs b/rust/src/cli/mod.rs new file mode 100644 index 0000000..2648e12 --- /dev/null +++ b/rust/src/cli/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(crate) mod firmware; + +pub(crate) mod usb; diff --git a/rust/examples/usb_probe.rs b/rust/src/cli/usb/mod.rs similarity index 97% rename from rust/examples/usb_probe.rs rename to rust/src/cli/usb/mod.rs index 3ba3b61..7adbd75 100644 --- a/rust/examples/usb_probe.rs +++ b/rust/src/cli/usb/mod.rs @@ -23,7 +23,6 @@ //! whether it is a Bluetooth device that uses a non-standard Class, or some other //! type of device (there's no way to tell). -use clap::Parser as _; use itertools::Itertools as _; use owo_colors::{OwoColorize, Style}; use rusb::{Device, DeviceDescriptor, Direction, TransferType, UsbContext}; @@ -31,15 +30,12 @@ use std::{ collections::{HashMap, HashSet}, time::Duration, }; - const USB_DEVICE_CLASS_DEVICE: u8 = 0x00; const USB_DEVICE_CLASS_WIRELESS_CONTROLLER: u8 = 0xE0; const USB_DEVICE_SUBCLASS_RF_CONTROLLER: u8 = 0x01; const USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER: u8 = 0x01; -fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); - +pub(crate) fn probe(verbose: bool) -> anyhow::Result<()> { let mut bt_dev_count = 0; let mut device_serials_by_id: HashMap<(u16, u16), HashSet> = HashMap::new(); for device in rusb::devices()?.iter() { @@ -159,7 +155,7 @@ fn main() -> anyhow::Result<()> { println!("{:26}{}", " Product:".green(), p); } - if cli.verbose { + if verbose { print_device_details(&device, &device_desc)?; } @@ -332,11 +328,3 @@ impl From<&DeviceDescriptor> for ClassInfo { ) } } - -#[derive(clap::Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// Show additional info for each USB device - #[arg(long, default_value_t = false)] - verbose: bool, -} diff --git a/rust/src/internal/drivers/mod.rs b/rust/src/internal/drivers/mod.rs new file mode 100644 index 0000000..5e72c59 --- /dev/null +++ b/rust/src/internal/drivers/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device drivers + +pub(crate) mod rtk; diff --git a/rust/src/internal/drivers/rtk.rs b/rust/src/internal/drivers/rtk.rs new file mode 100644 index 0000000..2d4e685 --- /dev/null +++ b/rust/src/internal/drivers/rtk.rs @@ -0,0 +1,253 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Drivers for Realtek controllers + +use nom::{bytes, combinator, error, multi, number, sequence}; + +/// Realtek firmware file contents +pub struct Firmware { + version: u32, + project_id: u8, + patches: Vec, +} + +impl Firmware { + /// Parse a `*_fw.bin` file + pub fn parse(input: &[u8]) -> Result>> { + let extension_sig = [0x51, 0x04, 0xFD, 0x77]; + + let (_rem, (_tag, fw_version, patch_count, payload)) = + combinator::all_consuming(combinator::map_parser( + // ignore the sig suffix + sequence::terminated( + bytes::complete::take( + // underflow will show up as parse failure + input.len().saturating_sub(extension_sig.len()), + ), + bytes::complete::tag(extension_sig.as_slice()), + ), + sequence::tuple(( + bytes::complete::tag(b"Realtech"), + // version + number::complete::le_u32, + // patch count + combinator::map(number::complete::le_u16, |c| c as usize), + // everything else except suffix + combinator::rest, + )), + ))(input)?; + + // ignore remaining input, since patch offsets are relative to the complete input + let (_rem, (chip_ids, patch_lengths, patch_offsets)) = sequence::tuple(( + // chip id + multi::many_m_n(patch_count, patch_count, number::complete::le_u16), + // patch length + multi::many_m_n(patch_count, patch_count, number::complete::le_u16), + // patch offset + multi::many_m_n(patch_count, patch_count, number::complete::le_u32), + ))(payload)?; + + let patches = chip_ids + .into_iter() + .zip(patch_lengths.into_iter()) + .zip(patch_offsets.into_iter()) + .map(|((chip_id, patch_length), patch_offset)| { + combinator::map( + sequence::preceded( + bytes::complete::take(patch_offset), + // ignore trailing 4-byte suffix + sequence::terminated( + // patch including svn version, but not suffix + combinator::consumed(sequence::preceded( + // patch before svn version or version suffix + // prefix length underflow will show up as parse failure + bytes::complete::take(patch_length.saturating_sub(8)), + // svn version + number::complete::le_u32, + )), + // dummy suffix, overwritten with firmware version + bytes::complete::take(4_usize), + ), + ), + |(patch_contents_before_version, svn_version): (&[u8], u32)| { + let mut contents = patch_contents_before_version.to_vec(); + // replace what would have been the trailing dummy suffix with fw version + contents.extend_from_slice(&fw_version.to_le_bytes()); + + Patch { + contents, + svn_version, + chip_id, + } + }, + )(input) + .map(|(_rem, output)| output) + }) + .collect::, _>>()?; + + // look for project id from the end + let mut offset = payload.len(); + let mut project_id: Option = None; + while offset >= 2 { + // Won't panic, since offset >= 2 + let chunk = &payload[offset - 2..offset]; + let length: usize = chunk[0].into(); + let opcode = chunk[1]; + offset -= 2; + + if opcode == 0xFF { + break; + } + if length == 0 { + // report what nom likely would have done, if nom was good at parsing backwards + return Err(nom::Err::Error(error::Error::new( + chunk, + error::ErrorKind::Verify, + ))); + } + if opcode == 0 && length == 1 { + project_id = offset + .checked_sub(1) + .and_then(|index| payload.get(index)) + .copied(); + break; + } + + offset -= length; + } + + match project_id { + Some(project_id) => Ok(Firmware { + project_id, + version: fw_version, + patches, + }), + None => { + // we ran out of file without finding a project id + Err(nom::Err::Error(error::Error::new( + payload, + error::ErrorKind::Eof, + ))) + } + } + } + + /// Patch version + pub fn version(&self) -> u32 { + self.version + } + + /// Project id + pub fn project_id(&self) -> u8 { + self.project_id + } + + /// Patches + pub fn patches(&self) -> &[Patch] { + &self.patches + } +} + +/// Patch in a [Firmware} +pub struct Patch { + chip_id: u16, + contents: Vec, + svn_version: u32, +} + +impl Patch { + /// Chip id + pub fn chip_id(&self) -> u16 { + self.chip_id + } + /// Contents of the patch, including the 4-byte firmware version suffix + pub fn contents(&self) -> &[u8] { + &self.contents + } + /// SVN version + pub fn svn_version(&self) -> u32 { + self.svn_version + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::anyhow; + use std::{fs, io, path}; + + #[test] + fn parse_firmware_rtl8723b() -> anyhow::Result<()> { + let fw = Firmware::parse(&firmware_contents("rtl8723b_fw_structure.bin")?) + .map_err(|e| anyhow!("{:?}", e))?; + + let fw_version = 0x0E2F9F73; + assert_eq!(fw_version, fw.version()); + assert_eq!(0x0001, fw.project_id()); + assert_eq!( + vec![(0x0001, 0x00002BBF, 22368,), (0x0002, 0x00002BBF, 22496,),], + patch_summaries(fw, fw_version) + ); + + Ok(()) + } + + #[test] + fn parse_firmware_rtl8761bu() -> anyhow::Result<()> { + let fw = Firmware::parse(&firmware_contents("rtl8761bu_fw_structure.bin")?) + .map_err(|e| anyhow!("{:?}", e))?; + + let fw_version = 0xDFC6D922; + assert_eq!(fw_version, fw.version()); + assert_eq!(0x000E, fw.project_id()); + assert_eq!( + vec![(0x0001, 0x00005060, 14048,), (0x0002, 0xD6D525A4, 30204,),], + patch_summaries(fw, fw_version) + ); + + Ok(()) + } + + fn firmware_contents(filename: &str) -> io::Result> { + fs::read( + path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("resources/test/firmware/realtek") + .join(filename), + ) + } + + /// Return a tuple of (chip id, svn version, contents len, contents sha256) + fn patch_summaries(fw: Firmware, fw_version: u32) -> Vec<(u16, u32, usize)> { + fw.patches() + .iter() + .map(|p| { + let contents = p.contents(); + let mut dummy_contents = dummy_contents(contents.len()); + dummy_contents.extend_from_slice(&p.svn_version().to_le_bytes()); + dummy_contents.extend_from_slice(&fw_version.to_le_bytes()); + assert_eq!(&dummy_contents, contents); + (p.chip_id(), p.svn_version(), contents.len()) + }) + .collect::>() + } + + fn dummy_contents(len: usize) -> Vec { + let mut vec = (len as u32).to_le_bytes().as_slice().repeat(len / 4 + 1); + assert!(vec.len() >= len); + // leave room for svn version and firmware version + vec.truncate(len - 8); + vec + } +} diff --git a/rust/src/internal/mod.rs b/rust/src/internal/mod.rs new file mode 100644 index 0000000..f474c2d --- /dev/null +++ b/rust/src/internal/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! It's not clear where to put Rust code that isn't simply a wrapper around Python. Until we have +//! a good answer for what to do there, the idea is to put it in this (non-public) module, and +//! `pub use` it into the relevant areas of the `wrapper` module so that it's still easy for users +//! to discover. + +pub(crate) mod drivers; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 73001e6..2bcb398 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -29,3 +29,5 @@ pub mod wrapper; pub mod adv; + +pub(crate) mod internal; diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..f8401e9 --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,179 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! CLI tools for Bumble + +#![deny(missing_docs, unsafe_code)] + +use bumble::wrapper::logging::{bumble_env_logging_level, py_logging_basic_config}; +use clap::Parser as _; +use pyo3::PyResult; +use std::{fmt, path}; + +mod cli; + +#[pyo3_asyncio::tokio::main] +async fn main() -> PyResult<()> { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + + py_logging_basic_config(bumble_env_logging_level("INFO"))?; + + let cli: Cli = Cli::parse(); + + match cli.subcommand { + Subcommand::Firmware { subcommand: fw } => match fw { + Firmware::Realtek { subcommand: rtk } => match rtk { + Realtek::Download(dl) => { + cli::firmware::rtk::download(dl).await?; + } + Realtek::Drop { transport } => cli::firmware::rtk::drop(&transport).await?, + Realtek::Info { transport, force } => { + cli::firmware::rtk::info(&transport, force).await?; + } + Realtek::Load { transport, force } => { + cli::firmware::rtk::load(&transport, force).await? + } + Realtek::Parse { firmware_path } => cli::firmware::rtk::parse(&firmware_path)?, + }, + }, + Subcommand::Usb { subcommand } => match subcommand { + Usb::Probe(probe) => cli::usb::probe(probe.verbose)?, + }, + } + + Ok(()) +} + +#[derive(clap::Parser)] +struct Cli { + #[clap(subcommand)] + subcommand: Subcommand, +} + +#[derive(clap::Subcommand, Debug, Clone)] +enum Subcommand { + /// Manage device firmware + Firmware { + #[clap(subcommand)] + subcommand: Firmware, + }, + /// USB operations + Usb { + #[clap(subcommand)] + subcommand: Usb, + }, +} + +#[derive(clap::Subcommand, Debug, Clone)] +enum Firmware { + /// Manage Realtek chipset firmware + Realtek { + #[clap(subcommand)] + subcommand: Realtek, + }, +} + +#[derive(clap::Subcommand, Debug, Clone)] + +enum Realtek { + /// Download Realtek firmware + Download(Download), + /// Drop firmware from a USB device + Drop { + /// Bumble transport spec. Must be for a USB device. + /// + /// + #[arg(long)] + transport: String, + }, + /// Show driver info for a USB device + Info { + /// Bumble transport spec. Must be for a USB device. + /// + /// + #[arg(long)] + transport: String, + /// Try to resolve driver info even if USB info is not available, or if the USB + /// (vendor,product) tuple is not in the list of known compatible RTK USB dongles. + #[arg(long, default_value_t = false)] + force: bool, + }, + /// Load firmware onto a USB device + Load { + /// Bumble transport spec. Must be for a USB device. + /// + /// + #[arg(long)] + transport: String, + /// Load firmware even if the USB info doesn't match. + #[arg(long, default_value_t = false)] + force: bool, + }, + /// Parse a firmware file + Parse { + /// Firmware file to parse + firmware_path: path::PathBuf, + }, +} + +#[derive(clap::Args, Debug, Clone)] +struct Download { + /// Directory to download to. Defaults to an OS-specific path specific to the Bumble tool. + #[arg(long)] + output_dir: Option, + /// Source to download from + #[arg(long, default_value_t = Source::LinuxKernel)] + source: Source, + /// Only download a single image + #[arg(long, value_name = "base name")] + single: Option, + /// Overwrite existing files + #[arg(long, default_value_t = false)] + overwrite: bool, + /// Don't print the parse results for the downloaded file names + #[arg(long)] + no_parse: bool, +} + +#[derive(Debug, Clone, clap::ValueEnum)] +enum Source { + LinuxKernel, + RealtekOpensource, + LinuxFromScratch, +} + +impl fmt::Display for Source { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Source::LinuxKernel => write!(f, "linux-kernel"), + Source::RealtekOpensource => write!(f, "realtek-opensource"), + Source::LinuxFromScratch => write!(f, "linux-from-scratch"), + } + } +} + +#[derive(clap::Subcommand, Debug, Clone)] +enum Usb { + /// Probe the USB bus for Bluetooth devices + Probe(Probe), +} + +#[derive(clap::Args, Debug, Clone)] +struct Probe { + /// Show additional info for each USB device + #[arg(long, default_value_t = false)] + verbose: bool, +} diff --git a/rust/src/wrapper/device.rs b/rust/src/wrapper/device.rs index d635754..11770a3 100644 --- a/rust/src/wrapper/device.rs +++ b/rust/src/wrapper/device.rs @@ -20,12 +20,17 @@ use crate::{ core::AdvertisingData, gatt_client::{ProfileServiceProxy, ServiceProxy}, hci::Address, + host::Host, transport::{Sink, Source}, - ClosureCallback, + ClosureCallback, PyObjectExt, }, }; -use pyo3::types::PyDict; -use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject}; +use pyo3::{ + intern, + types::{PyDict, PyModule}, + PyObject, PyResult, Python, ToPyObject, +}; +use pyo3_asyncio::tokio::into_future; use std::path; /// A device that can send/receive HCI frames. @@ -65,7 +70,7 @@ impl Device { Python::with_gil(|py| { self.0 .call_method0(py, intern!(py, "power_on")) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await .map(|_| ()) @@ -76,7 +81,7 @@ impl Device { Python::with_gil(|py| { self.0 .call_method1(py, intern!(py, "connect"), (peer_addr,)) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await .map(Connection) @@ -89,7 +94,7 @@ impl Device { kwargs.set_item("filter_duplicates", filter_duplicates)?; self.0 .call_method(py, intern!(py, "start_scanning"), (), Some(kwargs)) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await .map(|_| ()) @@ -123,6 +128,15 @@ impl Device { .map(|_| ()) } + /// Returns the host used by the device, if any + pub fn host(&mut self) -> PyResult> { + Python::with_gil(|py| { + self.0 + .getattr(py, intern!(py, "host")) + .map(|obj| obj.into_option(Host::from)) + }) + } + /// Start advertising the data set with [Device.set_advertisement]. pub async fn start_advertising(&mut self, auto_restart: bool) -> PyResult<()> { Python::with_gil(|py| { @@ -131,7 +145,7 @@ impl Device { self.0 .call_method(py, intern!(py, "start_advertising"), (), Some(kwargs)) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await .map(|_| ()) @@ -142,7 +156,7 @@ impl Device { Python::with_gil(|py| { self.0 .call_method0(py, intern!(py, "stop_advertising")) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await .map(|_| ()) @@ -173,7 +187,7 @@ impl Peer { Python::with_gil(|py| { self.0 .call_method0(py, intern!(py, "discover_services")) - .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) })? .await .and_then(|list| { @@ -207,13 +221,7 @@ impl Peer { let class = module.getattr(P::PROXY_CLASS_NAME)?; self.0 .call_method1(py, intern!(py, "create_service_proxy"), (class,)) - .map(|obj| { - if obj.is_none(py) { - None - } else { - Some(P::wrap(obj)) - } - }) + .map(|obj| obj.into_option(P::wrap)) }) } } diff --git a/rust/src/wrapper/drivers/mod.rs b/rust/src/wrapper/drivers/mod.rs new file mode 100644 index 0000000..ff38ac1 --- /dev/null +++ b/rust/src/wrapper/drivers/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device drivers + +pub mod rtk; diff --git a/rust/src/wrapper/drivers/rtk.rs b/rust/src/wrapper/drivers/rtk.rs new file mode 100644 index 0000000..1f629d1 --- /dev/null +++ b/rust/src/wrapper/drivers/rtk.rs @@ -0,0 +1,141 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Drivers for Realtek controllers + +use crate::wrapper::{host::Host, PyObjectExt}; +use pyo3::{intern, types::PyModule, PyObject, PyResult, Python, ToPyObject}; +use pyo3_asyncio::tokio::into_future; + +pub use crate::internal::drivers::rtk::{Firmware, Patch}; + +/// Driver for a Realtek controller +pub struct Driver(PyObject); + +impl Driver { + /// Locate the driver for the provided host. + pub async fn for_host(host: &Host, force: bool) -> PyResult> { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.drivers.rtk"))? + .getattr(intern!(py, "Driver"))? + .call_method1(intern!(py, "for_host"), (&host.obj, force)) + .and_then(into_future) + })? + .await + .map(|obj| obj.into_option(Self)) + } + + /// Check if the host has a known driver. + pub async fn check(host: &Host) -> PyResult { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.drivers.rtk"))? + .getattr(intern!(py, "Driver"))? + .call_method1(intern!(py, "check"), (&host.obj,)) + .and_then(|obj| obj.extract::()) + }) + } + + /// Find the [DriverInfo] for the host, if one matches + pub async fn driver_info_for_host(host: &Host) -> PyResult> { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.drivers.rtk"))? + .getattr(intern!(py, "Driver"))? + .call_method1(intern!(py, "driver_info_for_host"), (&host.obj,)) + .and_then(into_future) + })? + .await + .map(|obj| obj.into_option(DriverInfo)) + } + + /// Send a command to the device to drop firmware + pub async fn drop_firmware(host: &mut Host) -> PyResult<()> { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.drivers.rtk"))? + .getattr(intern!(py, "Driver"))? + .call_method1(intern!(py, "drop_firmware"), (&host.obj,)) + .and_then(into_future) + })? + .await + .map(|_| ()) + } + + /// Load firmware onto the device. + pub async fn download_firmware(&mut self) -> PyResult<()> { + Python::with_gil(|py| { + self.0 + .call_method0(py, intern!(py, "download_firmware")) + .and_then(|coroutine| into_future(coroutine.as_ref(py))) + })? + .await + .map(|_| ()) + } +} + +/// Metadata about a known driver & applicable device +pub struct DriverInfo(PyObject); + +impl DriverInfo { + /// Returns a list of all drivers that Bumble knows how to handle. + pub fn all_drivers() -> PyResult> { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.drivers.rtk"))? + .getattr(intern!(py, "Driver"))? + .getattr(intern!(py, "DRIVER_INFOS"))? + .iter()? + .map(|r| r.map(|h| DriverInfo(h.to_object(py)))) + .collect::>>() + }) + } + + /// The firmware file name to load from the filesystem, e.g. `foo_fw.bin`. + pub fn firmware_name(&self) -> PyResult { + Python::with_gil(|py| { + self.0 + .getattr(py, intern!(py, "fw_name"))? + .as_ref(py) + .extract::() + }) + } + + /// The config file name, if any, to load from the filesystem, e.g. `foo_config.bin`. + pub fn config_name(&self) -> PyResult> { + Python::with_gil(|py| { + let obj = self.0.getattr(py, intern!(py, "config_name"))?; + let handle = obj.as_ref(py); + + if handle.is_none() { + Ok(None) + } else { + handle + .extract::() + .map(|s| if s.is_empty() { None } else { Some(s) }) + } + }) + } + + /// Whether or not config is required. + pub fn config_needed(&self) -> PyResult { + Python::with_gil(|py| { + self.0 + .getattr(py, intern!(py, "config_needed"))? + .as_ref(py) + .extract::() + }) + } + + /// ROM id + pub fn rom(&self) -> PyResult { + Python::with_gil(|py| self.0.getattr(py, intern!(py, "rom"))?.as_ref(py).extract()) + } +} diff --git a/rust/src/wrapper/host.rs b/rust/src/wrapper/host.rs new file mode 100644 index 0000000..ab81450 --- /dev/null +++ b/rust/src/wrapper/host.rs @@ -0,0 +1,71 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Host-side types + +use crate::wrapper::transport::{Sink, Source}; +use pyo3::{intern, prelude::PyModule, types::PyDict, PyObject, PyResult, Python}; + +/// Host HCI commands +pub struct Host { + pub(crate) obj: PyObject, +} + +impl Host { + /// Create a Host that wraps the provided obj + pub(crate) fn from(obj: PyObject) -> Self { + Self { obj } + } + + /// Create a new Host + pub fn new(source: Source, sink: Sink) -> PyResult { + Python::with_gil(|py| { + PyModule::import(py, intern!(py, "bumble.host"))? + .getattr(intern!(py, "Host"))? + .call((source.0, sink.0), None) + .map(|any| Self { obj: any.into() }) + }) + } + + /// Send a reset command and perform other reset tasks. + pub async fn reset(&mut self, driver_factory: DriverFactory) -> PyResult<()> { + Python::with_gil(|py| { + let kwargs = match driver_factory { + DriverFactory::None => { + let kw = PyDict::new(py); + kw.set_item("driver_factory", py.None())?; + Some(kw) + } + DriverFactory::Auto => { + // leave the default in place + None + } + }; + self.obj + .call_method(py, intern!(py, "reset"), (), kwargs) + .and_then(|coroutine| pyo3_asyncio::tokio::into_future(coroutine.as_ref(py))) + })? + .await + .map(|_| ()) + } +} + +/// Driver factory to use when initializing a host +#[derive(Debug, Clone)] +pub enum DriverFactory { + /// Do not load drivers + None, + /// Load appropriate driver, if any is found + Auto, +} diff --git a/rust/src/wrapper/mod.rs b/rust/src/wrapper/mod.rs index 2ab71c3..cb0730b 100644 --- a/rust/src/wrapper/mod.rs +++ b/rust/src/wrapper/mod.rs @@ -31,14 +31,17 @@ pub use pyo3_asyncio; pub mod assigned_numbers; pub mod core; pub mod device; + +pub mod drivers; pub mod gatt_client; pub mod hci; +pub mod host; pub mod logging; pub mod profile; pub mod transport; /// Convenience extensions to [PyObject] -pub trait PyObjectExt { +pub trait PyObjectExt: Sized { /// Get a GIL-bound reference fn gil_ref<'py>(&'py self, py: Python<'py>) -> &'py PyAny; @@ -49,6 +52,17 @@ pub trait PyObjectExt { { Python::with_gil(|py| self.gil_ref(py).extract::()) } + + /// If the Python object is a Python `None`, return a Rust `None`, otherwise `Some` with the mapped type + fn into_option(self, map_obj: impl Fn(Self) -> T) -> Option { + Python::with_gil(|py| { + if self.gil_ref(py).is_none() { + None + } else { + Some(map_obj(self)) + } + }) + } } impl PyObjectExt for PyObject { diff --git a/rust/src/wrapper/profile.rs b/rust/src/wrapper/profile.rs index 854ba80..fc473ff 100644 --- a/rust/src/wrapper/profile.rs +++ b/rust/src/wrapper/profile.rs @@ -14,7 +14,10 @@ //! GATT profiles -use crate::wrapper::gatt_client::{CharacteristicProxy, ProfileServiceProxy}; +use crate::wrapper::{ + gatt_client::{CharacteristicProxy, ProfileServiceProxy}, + PyObjectExt, +}; use pyo3::{intern, PyObject, PyResult, Python}; /// Exposes the battery GATT service @@ -26,13 +29,7 @@ impl BatteryServiceProxy { Python::with_gil(|py| { self.0 .getattr(py, intern!(py, "battery_level")) - .map(|level| { - if level.is_none(py) { - None - } else { - Some(CharacteristicProxy(level)) - } - }) + .map(|level| level.into_option(CharacteristicProxy)) }) } } diff --git a/setup.cfg b/setup.cfg index 53c28fd..74a90e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ install_requires = humanize >= 4.6.0; platform_system!='Emscripten' libusb1 >= 2.0.1; platform_system!='Emscripten' libusb-package == 1.0.26.1; platform_system!='Emscripten' + platformdirs == 3.10.0; platform_system!='Emscripten' prompt_toolkit >= 3.0.16; platform_system!='Emscripten' prettytable >= 3.6.0; platform_system!='Emscripten' protobuf >= 3.12.4; platform_system!='Emscripten' diff --git a/tools/rtk_fw_download.py b/tools/rtk_fw_download.py index b027141..89c49b2 100644 --- a/tools/rtk_fw_download.py +++ b/tools/rtk_fw_download.py @@ -67,8 +67,9 @@ def download_file(base_url, name, remove_suffix): @click.command @click.option( "--output-dir", - default=".", - help="Output directory where the files will be saved", + default="", + help="Output directory where the files will be saved. Defaults to the OS-specific" + "app data dir, which the driver will check when trying to find firmware", show_default=True, ) @click.option( @@ -84,7 +85,10 @@ def main(output_dir, source, single, force, parse): """Download RTK firmware images and configs.""" # Check that the output dir exists - output_dir = pathlib.Path(output_dir) + if output_dir == '': + output_dir = rtk.rtk_firmware_dir() + else: + output_dir = pathlib.Path(output_dir) if not output_dir.is_dir(): print("Output dir does not exist or is not a directory") return diff --git a/tools/rtk_util.py b/tools/rtk_util.py index 7452915..35afd92 100644 --- a/tools/rtk_util.py +++ b/tools/rtk_util.py @@ -61,9 +61,8 @@ async def do_load(usb_transport, force): # Get the driver. driver = await rtk.Driver.for_host(host, force) if driver is None: - if not force: - print("Firmware already loaded or no supported driver for this device.") - return + print("Firmware already loaded or no supported driver for this device.") + return await driver.download_firmware()