├── .github ├── pull_request_template.md └── workflows │ └── rust.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── browser │ ├── Cargo.toml │ └── src │ │ └── main.rs └── service │ ├── Cargo.toml │ └── src │ └── main.rs ├── zeroconf-macros ├── Cargo.toml └── src │ └── lib.rs └── zeroconf ├── Cargo.toml └── src ├── avahi ├── avahi_util.rs ├── browser.rs ├── client.rs ├── entry_group.rs ├── event_loop.rs ├── mod.rs ├── poll.rs ├── raw_browser.rs ├── resolver.rs ├── service.rs ├── string_list.rs └── txt_record.rs ├── bonjour ├── bonjour_util.rs ├── browser.rs ├── constants.rs ├── event_loop.rs ├── mod.rs ├── service.rs ├── service_ref.rs ├── txt_record.rs └── txt_record_ref.rs ├── browser.rs ├── error.rs ├── event_loop.rs ├── ffi ├── c_str.rs └── mod.rs ├── interface.rs ├── lib.rs ├── macros.rs ├── prelude.rs ├── service.rs ├── service_type.rs ├── tests ├── event_loop_test.rs ├── mod.rs └── service_test.rs └── txt_record.rs /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What 2 | Brief summary of these changes and their impact. The easier it is to follow and understand, the easier to review. 3 | 4 | ## Why 5 | Reason for these changes. And why not some other changes (if applicable). Context here can be helpful when later blaming this code/reading logs. 6 | 7 | ## Manual Tests 8 | What did you do to verify your changes? 9 | 10 | ## Documentation Updates 11 | Did you document the purpose of any new code? Did you add comments explaining anything that might look strange to 12 | someone trying to figure out what your code is doing a year from now who doesn't know the full context? Is the README 13 | for this repo still accurate and useful? 14 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | name: zeroconf-rs 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | features: [serde, ""] 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Set up Rust 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | target: ${{ matrix.os == 'windows-latest' && 'x86_64-pc-windows-msvc' || matrix.os == 'macos-latest' && 'x86_64-apple-darwin' || 'x86_64-unknown-linux-gnu' }} 30 | 31 | - name: Prepare for Linux 32 | if: matrix.os == 'ubuntu-latest' 33 | run: | 34 | sudo apt -y install avahi-daemon libavahi-client-dev 35 | sudo systemctl start avahi-daemon.service 36 | 37 | - name: Prepare for Windows 38 | if: matrix.os == 'windows-latest' 39 | run: "choco install -y bonjour" 40 | 41 | - name: Cache Cargo 42 | uses: actions/cache@v3 43 | with: 44 | path: | 45 | ~/.cargo/registry 46 | ~/.cargo/git 47 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 48 | 49 | - name: Build 50 | run: cargo build --features "${{ matrix.features }}" 51 | 52 | - name: Run tests 53 | run: cargo test --features "${{ matrix.features }}" -- --skip service_register_is_browsable 54 | 55 | - name: Check formatting 56 | run: cargo fmt -- --check 57 | 58 | - name: Run Clippy 59 | run: cargo clippy --features "${{ matrix.features }}" -- -D warnings 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug unit tests in library 'zeroconf'", 11 | "cargo": { 12 | "args": [ 13 | "test", 14 | "--no-run", 15 | "--lib", 16 | "--package=zeroconf" 17 | ], 18 | "filter": { 19 | "name": "zeroconf", 20 | "kind": "lib" 21 | } 22 | }, 23 | "args": [], 24 | "cwd": "${workspaceFolder}", 25 | "env": { 26 | "RUST_LOG": "debug" 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [ 4 | "aprotocol", 5 | "errno", 6 | "regtype", 7 | "strlst", 8 | "unspec", 9 | "userdata" 10 | ], 11 | "rust-analyzer.showUnlinkedFileNotification": false 12 | } 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "avahi-sys" 66 | version = "0.10.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "70e00c83b3887835fd326daae1b2c4e7a435405033331a876ae1dd03f77b8274" 69 | dependencies = [ 70 | "bindgen 0.69.5", 71 | "libc", 72 | ] 73 | 74 | [[package]] 75 | name = "bindgen" 76 | version = "0.68.1" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" 79 | dependencies = [ 80 | "bitflags", 81 | "cexpr", 82 | "clang-sys", 83 | "lazy_static", 84 | "lazycell", 85 | "log", 86 | "peeking_take_while", 87 | "prettyplease", 88 | "proc-macro2", 89 | "quote", 90 | "regex", 91 | "rustc-hash", 92 | "shlex", 93 | "syn 2.0.100", 94 | "which", 95 | ] 96 | 97 | [[package]] 98 | name = "bindgen" 99 | version = "0.69.5" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" 102 | dependencies = [ 103 | "bitflags", 104 | "cexpr", 105 | "clang-sys", 106 | "itertools", 107 | "lazy_static", 108 | "lazycell", 109 | "log", 110 | "prettyplease", 111 | "proc-macro2", 112 | "quote", 113 | "regex", 114 | "rustc-hash", 115 | "shlex", 116 | "syn 2.0.100", 117 | "which", 118 | ] 119 | 120 | [[package]] 121 | name = "bitflags" 122 | version = "2.9.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 125 | 126 | [[package]] 127 | name = "bonjour-sys" 128 | version = "0.3.1" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "be8655c4daf1447c831970e4134e8312a15c63fbff109b353cfe934c22f2b61a" 131 | dependencies = [ 132 | "bindgen 0.68.1", 133 | "libc", 134 | ] 135 | 136 | [[package]] 137 | name = "cexpr" 138 | version = "0.6.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 141 | dependencies = [ 142 | "nom", 143 | ] 144 | 145 | [[package]] 146 | name = "cfg-if" 147 | version = "1.0.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 150 | 151 | [[package]] 152 | name = "clang-sys" 153 | version = "1.8.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 156 | dependencies = [ 157 | "glob", 158 | "libc", 159 | "libloading", 160 | ] 161 | 162 | [[package]] 163 | name = "clap" 164 | version = "4.5.35" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 167 | dependencies = [ 168 | "clap_builder", 169 | "clap_derive", 170 | ] 171 | 172 | [[package]] 173 | name = "clap_builder" 174 | version = "4.5.35" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 177 | dependencies = [ 178 | "anstream", 179 | "anstyle", 180 | "clap_lex", 181 | "strsim 0.11.1", 182 | ] 183 | 184 | [[package]] 185 | name = "clap_derive" 186 | version = "4.5.32" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 189 | dependencies = [ 190 | "heck", 191 | "proc-macro2", 192 | "quote", 193 | "syn 2.0.100", 194 | ] 195 | 196 | [[package]] 197 | name = "clap_lex" 198 | version = "0.7.4" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 201 | 202 | [[package]] 203 | name = "colorchoice" 204 | version = "1.0.3" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 207 | 208 | [[package]] 209 | name = "darling" 210 | version = "0.10.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" 213 | dependencies = [ 214 | "darling_core", 215 | "darling_macro", 216 | ] 217 | 218 | [[package]] 219 | name = "darling_core" 220 | version = "0.10.2" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" 223 | dependencies = [ 224 | "fnv", 225 | "ident_case", 226 | "proc-macro2", 227 | "quote", 228 | "strsim 0.9.3", 229 | "syn 1.0.109", 230 | ] 231 | 232 | [[package]] 233 | name = "darling_macro" 234 | version = "0.10.2" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" 237 | dependencies = [ 238 | "darling_core", 239 | "quote", 240 | "syn 1.0.109", 241 | ] 242 | 243 | [[package]] 244 | name = "derive-getters" 245 | version = "0.3.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" 248 | dependencies = [ 249 | "proc-macro2", 250 | "quote", 251 | "syn 1.0.109", 252 | ] 253 | 254 | [[package]] 255 | name = "derive-new" 256 | version = "0.5.9" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "syn 1.0.109", 263 | ] 264 | 265 | [[package]] 266 | name = "derive_builder" 267 | version = "0.9.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" 270 | dependencies = [ 271 | "darling", 272 | "derive_builder_core", 273 | "proc-macro2", 274 | "quote", 275 | "syn 1.0.109", 276 | ] 277 | 278 | [[package]] 279 | name = "derive_builder_core" 280 | version = "0.9.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" 283 | dependencies = [ 284 | "darling", 285 | "proc-macro2", 286 | "quote", 287 | "syn 1.0.109", 288 | ] 289 | 290 | [[package]] 291 | name = "either" 292 | version = "1.15.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 295 | 296 | [[package]] 297 | name = "env_logger" 298 | version = "0.10.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 301 | dependencies = [ 302 | "humantime", 303 | "is-terminal", 304 | "log", 305 | "regex", 306 | "termcolor", 307 | ] 308 | 309 | [[package]] 310 | name = "errno" 311 | version = "0.3.11" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 314 | dependencies = [ 315 | "libc", 316 | "windows-sys", 317 | ] 318 | 319 | [[package]] 320 | name = "fnv" 321 | version = "1.0.7" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 324 | 325 | [[package]] 326 | name = "glob" 327 | version = "0.3.2" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 330 | 331 | [[package]] 332 | name = "heck" 333 | version = "0.5.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 336 | 337 | [[package]] 338 | name = "hermit-abi" 339 | version = "0.5.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" 342 | 343 | [[package]] 344 | name = "home" 345 | version = "0.5.11" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 348 | dependencies = [ 349 | "windows-sys", 350 | ] 351 | 352 | [[package]] 353 | name = "humantime" 354 | version = "2.2.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 357 | 358 | [[package]] 359 | name = "ident_case" 360 | version = "1.0.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 363 | 364 | [[package]] 365 | name = "is-terminal" 366 | version = "0.4.16" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 369 | dependencies = [ 370 | "hermit-abi", 371 | "libc", 372 | "windows-sys", 373 | ] 374 | 375 | [[package]] 376 | name = "is_terminal_polyfill" 377 | version = "1.70.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 380 | 381 | [[package]] 382 | name = "itertools" 383 | version = "0.12.1" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 386 | dependencies = [ 387 | "either", 388 | ] 389 | 390 | [[package]] 391 | name = "itoa" 392 | version = "1.0.15" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 395 | 396 | [[package]] 397 | name = "lazy_static" 398 | version = "1.5.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 401 | 402 | [[package]] 403 | name = "lazycell" 404 | version = "1.3.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 407 | 408 | [[package]] 409 | name = "libc" 410 | version = "0.2.171" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 413 | 414 | [[package]] 415 | name = "libloading" 416 | version = "0.8.6" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" 419 | dependencies = [ 420 | "cfg-if", 421 | "windows-targets", 422 | ] 423 | 424 | [[package]] 425 | name = "linux-raw-sys" 426 | version = "0.4.15" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 429 | 430 | [[package]] 431 | name = "log" 432 | version = "0.4.27" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 435 | 436 | [[package]] 437 | name = "maplit" 438 | version = "1.0.2" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 441 | 442 | [[package]] 443 | name = "memchr" 444 | version = "2.7.4" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 447 | 448 | [[package]] 449 | name = "minimal-lexical" 450 | version = "0.2.1" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 453 | 454 | [[package]] 455 | name = "nom" 456 | version = "7.1.3" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 459 | dependencies = [ 460 | "memchr", 461 | "minimal-lexical", 462 | ] 463 | 464 | [[package]] 465 | name = "once_cell" 466 | version = "1.21.3" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 469 | 470 | [[package]] 471 | name = "peeking_take_while" 472 | version = "0.1.2" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 475 | 476 | [[package]] 477 | name = "prettyplease" 478 | version = "0.2.32" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" 481 | dependencies = [ 482 | "proc-macro2", 483 | "syn 2.0.100", 484 | ] 485 | 486 | [[package]] 487 | name = "proc-macro2" 488 | version = "1.0.94" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 491 | dependencies = [ 492 | "unicode-ident", 493 | ] 494 | 495 | [[package]] 496 | name = "quote" 497 | version = "1.0.40" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 500 | dependencies = [ 501 | "proc-macro2", 502 | ] 503 | 504 | [[package]] 505 | name = "regex" 506 | version = "1.11.1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 509 | dependencies = [ 510 | "aho-corasick", 511 | "memchr", 512 | "regex-automata", 513 | "regex-syntax", 514 | ] 515 | 516 | [[package]] 517 | name = "regex-automata" 518 | version = "0.4.9" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 521 | dependencies = [ 522 | "aho-corasick", 523 | "memchr", 524 | "regex-syntax", 525 | ] 526 | 527 | [[package]] 528 | name = "regex-syntax" 529 | version = "0.8.5" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 532 | 533 | [[package]] 534 | name = "rustc-hash" 535 | version = "1.1.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 538 | 539 | [[package]] 540 | name = "rustix" 541 | version = "0.38.44" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 544 | dependencies = [ 545 | "bitflags", 546 | "errno", 547 | "libc", 548 | "linux-raw-sys", 549 | "windows-sys", 550 | ] 551 | 552 | [[package]] 553 | name = "ryu" 554 | version = "1.0.20" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 557 | 558 | [[package]] 559 | name = "serde" 560 | version = "1.0.219" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 563 | dependencies = [ 564 | "serde_derive", 565 | ] 566 | 567 | [[package]] 568 | name = "serde_derive" 569 | version = "1.0.219" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 572 | dependencies = [ 573 | "proc-macro2", 574 | "quote", 575 | "syn 2.0.100", 576 | ] 577 | 578 | [[package]] 579 | name = "serde_json" 580 | version = "1.0.140" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 583 | dependencies = [ 584 | "itoa", 585 | "memchr", 586 | "ryu", 587 | "serde", 588 | ] 589 | 590 | [[package]] 591 | name = "shlex" 592 | version = "1.3.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 595 | 596 | [[package]] 597 | name = "strsim" 598 | version = "0.9.3" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" 601 | 602 | [[package]] 603 | name = "strsim" 604 | version = "0.11.1" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 607 | 608 | [[package]] 609 | name = "syn" 610 | version = "1.0.109" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 613 | dependencies = [ 614 | "proc-macro2", 615 | "quote", 616 | "unicode-ident", 617 | ] 618 | 619 | [[package]] 620 | name = "syn" 621 | version = "2.0.100" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 624 | dependencies = [ 625 | "proc-macro2", 626 | "quote", 627 | "unicode-ident", 628 | ] 629 | 630 | [[package]] 631 | name = "termcolor" 632 | version = "1.4.1" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 635 | dependencies = [ 636 | "winapi-util", 637 | ] 638 | 639 | [[package]] 640 | name = "unicode-ident" 641 | version = "1.0.18" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 644 | 645 | [[package]] 646 | name = "utf8parse" 647 | version = "0.2.2" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 650 | 651 | [[package]] 652 | name = "which" 653 | version = "4.4.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 656 | dependencies = [ 657 | "either", 658 | "home", 659 | "once_cell", 660 | "rustix", 661 | ] 662 | 663 | [[package]] 664 | name = "winapi-util" 665 | version = "0.1.9" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 668 | dependencies = [ 669 | "windows-sys", 670 | ] 671 | 672 | [[package]] 673 | name = "windows-sys" 674 | version = "0.59.0" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 677 | dependencies = [ 678 | "windows-targets", 679 | ] 680 | 681 | [[package]] 682 | name = "windows-targets" 683 | version = "0.52.6" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 686 | dependencies = [ 687 | "windows_aarch64_gnullvm", 688 | "windows_aarch64_msvc", 689 | "windows_i686_gnu", 690 | "windows_i686_gnullvm", 691 | "windows_i686_msvc", 692 | "windows_x86_64_gnu", 693 | "windows_x86_64_gnullvm", 694 | "windows_x86_64_msvc", 695 | ] 696 | 697 | [[package]] 698 | name = "windows_aarch64_gnullvm" 699 | version = "0.52.6" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 702 | 703 | [[package]] 704 | name = "windows_aarch64_msvc" 705 | version = "0.52.6" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 708 | 709 | [[package]] 710 | name = "windows_i686_gnu" 711 | version = "0.52.6" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 714 | 715 | [[package]] 716 | name = "windows_i686_gnullvm" 717 | version = "0.52.6" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 720 | 721 | [[package]] 722 | name = "windows_i686_msvc" 723 | version = "0.52.6" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 726 | 727 | [[package]] 728 | name = "windows_x86_64_gnu" 729 | version = "0.52.6" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 732 | 733 | [[package]] 734 | name = "windows_x86_64_gnullvm" 735 | version = "0.52.6" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 738 | 739 | [[package]] 740 | name = "windows_x86_64_msvc" 741 | version = "0.52.6" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 744 | 745 | [[package]] 746 | name = "zeroconf" 747 | version = "0.15.1" 748 | dependencies = [ 749 | "avahi-sys", 750 | "bonjour-sys", 751 | "clap", 752 | "derive-getters", 753 | "derive-new", 754 | "derive_builder", 755 | "env_logger", 756 | "libc", 757 | "log", 758 | "maplit", 759 | "serde", 760 | "serde_json", 761 | "zeroconf-macros", 762 | ] 763 | 764 | [[package]] 765 | name = "zeroconf-browser-example" 766 | version = "0.1.0" 767 | dependencies = [ 768 | "clap", 769 | "env_logger", 770 | "log", 771 | "zeroconf", 772 | ] 773 | 774 | [[package]] 775 | name = "zeroconf-macros" 776 | version = "0.1.4" 777 | dependencies = [ 778 | "quote", 779 | "syn 2.0.100", 780 | ] 781 | 782 | [[package]] 783 | name = "zeroconf-service-example" 784 | version = "0.1.0" 785 | dependencies = [ 786 | "clap", 787 | "env_logger", 788 | "log", 789 | "zeroconf", 790 | ] 791 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "zeroconf", 4 | "zeroconf-macros", 5 | "examples/browser", 6 | "examples/service", 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020-2024 Walker Crouse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zeroconf 2 | 3 | `zeroconf` is a cross-platform library that wraps underlying [ZeroConf/mDNS] implementations 4 | such as [Bonjour] or [Avahi], providing an easy and idiomatic way to both register and 5 | browse services. 6 | 7 | Check-out [zeroconf-tokio](https://github.com/windy1/zeroconf-tokio) if you are interested in using `zeroconf` in an 8 | async setting. 9 | 10 | ## Prerequisites 11 | 12 | On Linux: 13 | 14 | ```bash 15 | $ sudo apt install xorg-dev libxcb-shape0-dev libxcb-xfixes0-dev clang avahi-daemon libavahi-client-dev 16 | ``` 17 | 18 | On Windows: 19 | 20 | Bonjour must be installed. It comes bundled with [iTunes][] or [Bonjour Print Services][]. Further redistribution & 21 | bundling details are available on the [Apple Developer Site][]. 22 | 23 | ## Examples 24 | 25 | ### Register a service 26 | 27 | When registering a service, you may optionally pass a "context" to pass state through the 28 | callback. The only requirement is that this context implements the [`Any`] trait, which most 29 | types will automatically. See `MdnsService` for more information about contexts. 30 | 31 | ```rust 32 | #[macro_use] 33 | extern crate log; 34 | 35 | use clap::Parser; 36 | 37 | use std::any::Any; 38 | use std::sync::{Arc, Mutex}; 39 | use std::time::Duration; 40 | use zeroconf::prelude::*; 41 | use zeroconf::{MdnsService, ServiceRegistration, ServiceType, TxtRecord}; 42 | 43 | #[derive(Parser, Debug)] 44 | #[command(author, version, about)] 45 | struct Args { 46 | /// Name of the service type to register 47 | #[clap(short, long, default_value = "http")] 48 | name: String, 49 | 50 | /// Protocol of the service type to register 51 | #[clap(short, long, default_value = "tcp")] 52 | protocol: String, 53 | 54 | /// Sub-types of the service type to register 55 | #[clap(short, long)] 56 | sub_types: Vec, 57 | } 58 | 59 | #[derive(Default, Debug)] 60 | pub struct Context { 61 | service_name: String, 62 | } 63 | 64 | fn main() -> zeroconf::Result<()> { 65 | env_logger::init(); 66 | 67 | let Args { 68 | name, 69 | protocol, 70 | sub_types, 71 | } = Args::parse(); 72 | 73 | let sub_types = sub_types.iter().map(|s| s.as_str()).collect::>(); 74 | let service_type = ServiceType::with_sub_types(&name, &protocol, sub_types)?; 75 | let mut service = MdnsService::new(service_type, 8080); 76 | let mut txt_record = TxtRecord::new(); 77 | let context: Arc> = Arc::default(); 78 | 79 | txt_record.insert("foo", "bar")?; 80 | 81 | service.set_name("zeroconf_example_service"); 82 | service.set_registered_callback(Box::new(on_service_registered)); 83 | service.set_context(Box::new(context)); 84 | service.set_txt_record(txt_record); 85 | 86 | let event_loop = service.register()?; 87 | 88 | loop { 89 | // calling `poll()` will keep this service alive 90 | event_loop.poll(Duration::from_secs(0))?; 91 | } 92 | } 93 | 94 | fn on_service_registered( 95 | result: zeroconf::Result, 96 | context: Option>, 97 | ) { 98 | let service = result.expect("failed to register service"); 99 | 100 | info!("Service registered: {:?}", service); 101 | 102 | let context = context 103 | .as_ref() 104 | .expect("could not get context") 105 | .downcast_ref::>>() 106 | .expect("error down-casting context") 107 | .clone(); 108 | 109 | context 110 | .lock() 111 | .expect("failed to obtain context lock") 112 | .service_name = service.name().clone(); 113 | 114 | info!("Context: {:?}", context); 115 | 116 | // ... 117 | } 118 | ``` 119 | 120 | ### Browsing services 121 | 122 | ```rust 123 | #[macro_use] 124 | extern crate log; 125 | 126 | use clap::Parser; 127 | 128 | use std::any::Any; 129 | use std::sync::Arc; 130 | use std::time::Duration; 131 | use zeroconf::prelude::*; 132 | use zeroconf::{MdnsBrowser, ServiceDiscovery, ServiceType}; 133 | 134 | /// Example of a simple mDNS browser 135 | #[derive(Parser, Debug)] 136 | #[command(author, version, about)] 137 | struct Args { 138 | /// Name of the service type to browse 139 | #[clap(short, long, default_value = "http")] 140 | name: String, 141 | 142 | /// Protocol of the service type to browse 143 | #[clap(short, long, default_value = "tcp")] 144 | protocol: String, 145 | 146 | /// Sub-type of the service type to browse 147 | #[clap(short, long)] 148 | sub_type: Option, 149 | } 150 | 151 | fn main() -> zeroconf::Result<()> { 152 | env_logger::init(); 153 | 154 | let Args { 155 | name, 156 | protocol, 157 | sub_type, 158 | } = Args::parse(); 159 | 160 | let sub_types: Vec<&str> = match sub_type.as_ref() { 161 | Some(sub_type) => vec![sub_type], 162 | None => vec![], 163 | }; 164 | 165 | let service_type = 166 | ServiceType::with_sub_types(&name, &protocol, sub_types).expect("invalid service type"); 167 | 168 | let mut browser = MdnsBrowser::new(service_type); 169 | 170 | browser.set_service_discovered_callback(Box::new(on_service_discovered)); 171 | 172 | let event_loop = browser.browse_services()?; 173 | 174 | loop { 175 | // calling `poll()` will keep this browser alive 176 | event_loop.poll(Duration::from_secs(0))?; 177 | } 178 | } 179 | 180 | fn on_service_discovered( 181 | result: zeroconf::Result, 182 | _context: Option>, 183 | ) { 184 | info!( 185 | "Service discovered: {:?}", 186 | result.expect("service discovery failed") 187 | ); 188 | 189 | // ... 190 | } 191 | ``` 192 | 193 | ## Features 194 | 195 | - `serde` - enables serialization on relevant data structures 196 | 197 | ## Resources 198 | 199 | * [Avahi docs] 200 | * [Bonjour docs] 201 | 202 | [ZeroConf/mDNS]: https://en.wikipedia.org/wiki/Zero-configuration_networking 203 | [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) 204 | [Avahi]: https://en.wikipedia.org/wiki/Avahi_(software) 205 | [`Any`]: https://doc.rust-lang.org/std/any/trait.Any.html 206 | [Avahi docs]: https://avahi.org/doxygen/html/ 207 | [Bonjour docs]: https://developer.apple.com/documentation/dnssd/dns_service_discovery_c 208 | [iTunes]: https://support.apple.com/en-us/HT210384 209 | [Bonjour Print Services]: https://developer.apple.com/licensing-trademarks/bonjour/ 210 | [Apple Developer Site]: https://developer.apple.com/licensing-trademarks/bonjour/ 211 | -------------------------------------------------------------------------------- /examples/browser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zeroconf-browser-example" 3 | version = "0.1.0" 4 | authors = ["Walker Crouse "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | zeroconf = { path = "../../zeroconf" } 9 | env_logger = "0.10.0" 10 | log = "0.4.20" 11 | clap = { version = "4.4.4", features = ["derive"] } 12 | -------------------------------------------------------------------------------- /examples/browser/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use clap::Parser; 5 | 6 | use std::any::Any; 7 | use std::sync::Arc; 8 | use std::time::Duration; 9 | use zeroconf::prelude::*; 10 | use zeroconf::{BrowserEvent, MdnsBrowser, ServiceType}; 11 | 12 | /// Example of a simple mDNS browser 13 | #[derive(Parser, Debug)] 14 | #[command(author, version, about)] 15 | struct Args { 16 | /// Name of the service type to browse 17 | #[clap(short, long, default_value = "http")] 18 | name: String, 19 | 20 | /// Protocol of the service type to browse 21 | #[clap(short, long, default_value = "tcp")] 22 | protocol: String, 23 | 24 | /// Sub-type of the service type to browse 25 | #[clap(short, long)] 26 | sub_type: Option, 27 | } 28 | 29 | fn main() -> zeroconf::Result<()> { 30 | env_logger::Builder::from_env(env_logger::Env::new().filter_or("RUST_LOG", "info")).init(); 31 | 32 | let Args { 33 | name, 34 | protocol, 35 | sub_type, 36 | } = Args::parse(); 37 | 38 | let sub_types: Vec<&str> = match sub_type.as_ref() { 39 | Some(sub_type) => vec![sub_type], 40 | None => vec![], 41 | }; 42 | 43 | let service_type = 44 | ServiceType::with_sub_types(&name, &protocol, sub_types).expect("invalid service type"); 45 | 46 | let mut browser = MdnsBrowser::new(service_type); 47 | 48 | browser.set_service_callback(Box::new(on_service_discovery_event)); 49 | 50 | let event_loop = browser.browse_services()?; 51 | 52 | loop { 53 | // calling `poll()` will keep this browser alive 54 | event_loop.poll(Duration::from_secs(0))?; 55 | } 56 | } 57 | 58 | fn on_service_discovery_event( 59 | result: zeroconf::Result, 60 | _context: Option>, 61 | ) { 62 | info!( 63 | "Service event: {:?}", 64 | result.expect("service discovery failed") 65 | ); 66 | 67 | // ... 68 | } 69 | -------------------------------------------------------------------------------- /examples/service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zeroconf-service-example" 3 | version = "0.1.0" 4 | authors = ["Walker Crouse "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | zeroconf = { path = "../../zeroconf" } 9 | env_logger = "0.10.0" 10 | log = "0.4.20" 11 | clap = { version = "4.4.4", features = ["derive"] } 12 | -------------------------------------------------------------------------------- /examples/service/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use clap::Parser; 5 | 6 | use std::any::Any; 7 | use std::sync::{Arc, Mutex}; 8 | use std::time::Duration; 9 | use zeroconf::prelude::*; 10 | use zeroconf::{MdnsService, ServiceRegistration, ServiceType, TxtRecord}; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(author, version, about)] 14 | struct Args { 15 | /// Name of the service type to register 16 | #[clap(short, long, default_value = "http")] 17 | name: String, 18 | 19 | /// Protocol of the service type to register 20 | #[clap(short, long, default_value = "tcp")] 21 | protocol: String, 22 | 23 | /// Sub-types of the service type to register 24 | #[clap(short, long)] 25 | sub_types: Vec, 26 | } 27 | 28 | #[derive(Default, Debug)] 29 | pub struct Context { 30 | service_name: String, 31 | } 32 | 33 | fn main() -> zeroconf::Result<()> { 34 | env_logger::init(); 35 | 36 | let Args { 37 | name, 38 | protocol, 39 | sub_types, 40 | } = Args::parse(); 41 | 42 | let sub_types = sub_types.iter().map(|s| s.as_str()).collect::>(); 43 | let service_type = ServiceType::with_sub_types(&name, &protocol, sub_types)?; 44 | let mut service = MdnsService::new(service_type, 8080); 45 | let mut txt_record = TxtRecord::new(); 46 | let context: Arc> = Arc::default(); 47 | 48 | txt_record.insert("foo", "bar")?; 49 | 50 | service.set_name("zeroconf_example_service"); 51 | service.set_registered_callback(Box::new(on_service_registered)); 52 | service.set_context(Box::new(context)); 53 | service.set_txt_record(txt_record); 54 | 55 | let event_loop = service.register()?; 56 | 57 | loop { 58 | // calling `poll()` will keep this service alive 59 | event_loop.poll(Duration::from_secs(0))?; 60 | } 61 | } 62 | 63 | fn on_service_registered( 64 | result: zeroconf::Result, 65 | context: Option>, 66 | ) { 67 | let service = result.expect("failed to register service"); 68 | 69 | info!("Service registered: {:?}", service); 70 | 71 | let context = context 72 | .as_ref() 73 | .expect("could not get context") 74 | .downcast_ref::>>() 75 | .expect("error down-casting context") 76 | .clone(); 77 | 78 | context 79 | .lock() 80 | .expect("failed to obtain context lock") 81 | .service_name = service.name().to_string(); 82 | 83 | info!("Context: {:?}", context); 84 | 85 | // ... 86 | } 87 | -------------------------------------------------------------------------------- /zeroconf-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zeroconf-macros" 3 | version = "0.1.4" 4 | authors = ["Walker Crouse "] 5 | edition = "2018" 6 | description = "Macros for zeroconf crate" 7 | readme = "../README.md" 8 | homepage = "https://github.com/windy1/zeroconf-rs" 9 | repository = "https://github.com/windy1/zeroconf-rs" 10 | license-file = "../LICENSE" 11 | keywords = ["zeroconf", "mdns", "avahi", "bonjour", "dnssd"] 12 | categories = [ 13 | "api-bindings", 14 | "network-programming", 15 | "os", 16 | "os::linux-apis", 17 | "os::macos-apis", 18 | ] 19 | 20 | [lib] 21 | proc-macro = true 22 | 23 | [dependencies] 24 | syn = "2.0.37" 25 | quote = "1.0.33" 26 | -------------------------------------------------------------------------------- /zeroconf-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use crate::proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{self, DeriveInput, Ident}; 6 | 7 | #[proc_macro_derive(FromRaw)] 8 | pub fn from_raw_macro_derive(input: TokenStream) -> TokenStream { 9 | impl_from_raw(&syn::parse(input).expect("could not parse input")) 10 | } 11 | 12 | fn impl_from_raw(ast: &DeriveInput) -> TokenStream { 13 | let name = &ast.ident; 14 | let generics = &ast.generics; 15 | 16 | let gen = quote! { 17 | impl #generics crate::ffi::FromRaw<#name #generics> for #name #generics {} 18 | }; 19 | 20 | gen.into() 21 | } 22 | 23 | #[proc_macro_derive(CloneRaw)] 24 | pub fn clone_raw_macro_derive(input: TokenStream) -> TokenStream { 25 | impl_clone_raw(&syn::parse(input).expect("could not parse input")) 26 | } 27 | 28 | fn impl_clone_raw(ast: &DeriveInput) -> TokenStream { 29 | let name = &ast.ident; 30 | let generics = &ast.generics; 31 | 32 | let gen = quote! { 33 | impl #generics crate::ffi::CloneRaw<#name #generics> for #name #generics {} 34 | }; 35 | 36 | gen.into() 37 | } 38 | 39 | #[proc_macro_derive(AsRaw)] 40 | pub fn as_raw_macro_derive(input: TokenStream) -> TokenStream { 41 | impl_as_raw(&syn::parse(input).expect("could not parse input")) 42 | } 43 | 44 | fn impl_as_raw(ast: &DeriveInput) -> TokenStream { 45 | let name = &ast.ident; 46 | let generics = &ast.generics; 47 | 48 | let gen = quote! { 49 | impl #generics crate::ffi::AsRaw for #name #generics {} 50 | }; 51 | 52 | gen.into() 53 | } 54 | 55 | #[proc_macro_derive(BuilderDelegate)] 56 | pub fn builder_delegate_macro_derive(input: TokenStream) -> TokenStream { 57 | impl_builder_delegate(&syn::parse(input).expect("could not parse input")) 58 | } 59 | 60 | fn impl_builder_delegate(ast: &DeriveInput) -> TokenStream { 61 | let name = &ast.ident; 62 | 63 | let builder: Ident = 64 | syn::parse_str(&format!("{}Builder", name)).expect("could not parse builder name"); 65 | 66 | let generics = &ast.generics; 67 | 68 | let gen = quote! { 69 | impl #generics crate::prelude::BuilderDelegate<#builder #generics> for #name #generics {} 70 | }; 71 | 72 | gen.into() 73 | } 74 | -------------------------------------------------------------------------------- /zeroconf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zeroconf" 3 | version = "0.15.1" 4 | authors = ["Walker Crouse "] 5 | edition = "2018" 6 | description = "cross-platform library that wraps ZeroConf/mDNS implementations like Bonjour or Avahi" 7 | readme = "../README.md" 8 | homepage = "https://github.com/windy1/zeroconf-rs" 9 | repository = "https://github.com/windy1/zeroconf-rs" 10 | license-file = "../LICENSE" 11 | keywords = ["zeroconf", "mdns", "avahi", "bonjour", "dnssd"] 12 | categories = [ 13 | "api-bindings", 14 | "network-programming", 15 | "os::linux-apis", 16 | "os::macos-apis", 17 | "os::windows-apis", 18 | ] 19 | documentation = "https://docs.rs/zeroconf" 20 | 21 | [dependencies] 22 | serde = { version = "1.0.188", features = ["derive"], optional = true } 23 | derive-getters = "0.3.0" 24 | derive_builder = "0.9.0" 25 | derive-new = "0.5.9" 26 | log = "0.4.20" 27 | libc = "0.2.148" 28 | zeroconf-macros = { path = "../zeroconf-macros", version = "0.1.4" } 29 | 30 | [dev-dependencies] 31 | env_logger = "0.10.0" 32 | maplit = "1.0.2" 33 | serde_json = "1.0.107" 34 | clap = { version = "4.4.4", features = ["derive"] } 35 | 36 | [target.'cfg(unix)'.dependencies] 37 | avahi-sys = "0.10.1" 38 | 39 | [target.'cfg(target_vendor = "apple")'.dependencies] 40 | bonjour-sys = "0.3.0" 41 | 42 | [target.'cfg(target_vendor = "pc")'.dependencies] 43 | bonjour-sys = "0.3.0" 44 | 45 | [package.metadata.docs.rs] 46 | default-target = "x86_64-unknown-linux-gnu" 47 | targets = ["x86_64-apple-darwin", "x86_64-pc-windows-msvc"] 48 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/avahi_util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities related to Avahi 2 | 3 | use crate::ffi::c_str; 4 | use avahi_sys::{ 5 | avahi_address_snprint, avahi_alternative_service_name, avahi_strerror, AvahiAddress, 6 | AvahiClient, 7 | }; 8 | use libc::c_char; 9 | use std::ffi::CStr; 10 | 11 | use crate::{NetworkInterface, Result, ServiceType}; 12 | 13 | /// Converts the specified `*const AvahiAddress` to a `String`. 14 | /// 15 | /// The new `String` is constructed through allocating a new `CString`, passing it to 16 | /// `avahi_address_snprint` and then converting it to a Rust-type `String`. 17 | /// 18 | /// # Safety 19 | /// This function is unsafe because of internal Avahi calls and raw pointer dereference. 20 | pub unsafe fn avahi_address_to_string(addr: *const AvahiAddress) -> String { 21 | assert_not_null!(addr); 22 | 23 | let addr_str = c_string!(alloc(avahi_sys::AVAHI_ADDRESS_STR_MAX as usize)); 24 | 25 | avahi_address_snprint( 26 | addr_str.as_ptr() as *mut c_char, 27 | avahi_sys::AVAHI_ADDRESS_STR_MAX as usize, 28 | addr, 29 | ); 30 | 31 | String::from(c_str::to_str(&addr_str)) 32 | .trim_matches(char::from(0)) 33 | .to_string() 34 | } 35 | 36 | /// Returns the `&str` message associated with the specified error code. 37 | /// 38 | /// # Safety 39 | /// This function is unsafe because of internal Avahi calls. 40 | pub unsafe fn get_error<'a>(code: i32) -> &'a str { 41 | CStr::from_ptr(avahi_strerror(code)) 42 | .to_str() 43 | .expect("could not fetch Avahi error string") 44 | } 45 | 46 | /// Returns the last error message associated with the specified `*mut AvahiClient`. 47 | /// 48 | /// # Safety 49 | /// This function is unsafe because of internal Avahi calls. 50 | pub unsafe fn get_last_error<'a>(client: *mut AvahiClient) -> &'a str { 51 | get_error(avahi_sys::avahi_client_errno(client)) 52 | } 53 | 54 | /// Converts the specified [`NetworkInterface`] to the Avahi expected value. 55 | /// 56 | /// [`NetworkInterface`]: ../../enum.NetworkInterface.html 57 | pub fn interface_index(interface: NetworkInterface) -> i32 { 58 | match interface { 59 | NetworkInterface::Unspec => avahi_sys::AVAHI_IF_UNSPEC, 60 | NetworkInterface::AtIndex(i) => i as i32, 61 | } 62 | } 63 | 64 | /// Converts the specified Avahi interface index to a [`NetworkInterface`]. 65 | pub fn interface_from_index(index: i32) -> NetworkInterface { 66 | match index { 67 | avahi_sys::AVAHI_IF_UNSPEC => NetworkInterface::Unspec, 68 | _ => NetworkInterface::AtIndex(index as u32), 69 | } 70 | } 71 | 72 | /// Executes the specified closure and returns a formatted `Result` 73 | /// 74 | /// # Safety 75 | /// This function is unsafe because of the call to `get_error`. 76 | pub unsafe fn sys_exec i32>(func: F, message: &str) -> Result<()> { 77 | let err = func(); 78 | 79 | if err < 0 { 80 | Err(format!("{}: `{}`", message, get_error(err)).into()) 81 | } else { 82 | Ok(()) 83 | } 84 | } 85 | 86 | /// Formats the specified `ServiceType` as a `String` for use with Avahi 87 | pub fn format_service_type(service_type: &ServiceType) -> String { 88 | format!("_{}._{}", service_type.name(), service_type.protocol()) 89 | } 90 | 91 | /// Formats the specified `ServiceType` as a `String` for browsing Avahi services 92 | pub fn format_browser_type(service_type: &ServiceType) -> String { 93 | let kind = format_service_type(service_type); 94 | let sub_types = service_type.sub_types(); 95 | 96 | if sub_types.is_empty() { 97 | return kind; 98 | } 99 | 100 | if sub_types.len() > 1 { 101 | warn!("browsing by multiple sub-types is not supported on Avahi devices, using first sub-type only"); 102 | } 103 | 104 | format_sub_type(&sub_types[0], &kind) 105 | } 106 | 107 | /// Formats the specified `sub_type` string as a `String` for use with Avahi 108 | pub fn format_sub_type(sub_type: &str, kind: &str) -> String { 109 | format!( 110 | "{}{}._sub.{}", 111 | if sub_type.starts_with('_') { "" } else { "_" }, 112 | sub_type, 113 | kind 114 | ) 115 | } 116 | 117 | /// Returns an alternative service name for the specified `CStr` 118 | /// 119 | /// # Safety 120 | /// This function is unsafe because of the call to `avahi_alternative_service_name`. 121 | pub unsafe fn alternative_service_name(name: &CStr) -> &CStr { 122 | CStr::from_ptr(avahi_alternative_service_name(name.as_ptr())) 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::*; 128 | use avahi_sys::{ 129 | AvahiAddress__bindgen_ty_1, AvahiIPv4Address, AvahiIPv6Address, AVAHI_PROTO_INET, 130 | AVAHI_PROTO_INET6, 131 | }; 132 | 133 | #[test] 134 | fn sys_exec_returns_ok_for_success() { 135 | assert!(unsafe { sys_exec(|| 0, "test") }.is_ok()); 136 | } 137 | 138 | #[test] 139 | fn sys_exec_returns_error_for_failure() { 140 | assert_eq!( 141 | unsafe { sys_exec(|| avahi_sys::AVAHI_ERR_FAILURE, "uh oh spaghetti-o") }, 142 | Err("uh oh spaghetti-o: `Operation failed`".into()) 143 | ); 144 | } 145 | 146 | #[test] 147 | fn interface_index_returns_unspec_for_unspec() { 148 | assert_eq!( 149 | interface_index(NetworkInterface::Unspec), 150 | avahi_sys::AVAHI_IF_UNSPEC 151 | ); 152 | } 153 | 154 | #[test] 155 | fn interface_index_returns_index_for_index() { 156 | assert_eq!(interface_index(NetworkInterface::AtIndex(1)), 1); 157 | } 158 | 159 | #[test] 160 | fn interface_from_index_returns_unspec_for_avahi_unspec() { 161 | assert_eq!( 162 | interface_from_index(avahi_sys::AVAHI_IF_UNSPEC), 163 | NetworkInterface::Unspec 164 | ); 165 | } 166 | 167 | #[test] 168 | fn interface_from_index_returns_index_for_avahi_index() { 169 | assert_eq!(interface_from_index(1), NetworkInterface::AtIndex(1)); 170 | } 171 | 172 | #[test] 173 | fn format_service_type_returns_valid_string() { 174 | assert_eq!( 175 | format_service_type(&ServiceType::new("http", "tcp").unwrap()), 176 | "_http._tcp" 177 | ); 178 | } 179 | 180 | #[test] 181 | fn format_browser_type_returns_valid_string() { 182 | assert_eq!( 183 | format_browser_type(&ServiceType::new("http", "tcp").unwrap()), 184 | "_http._tcp" 185 | ); 186 | } 187 | 188 | #[test] 189 | fn format_browser_type_returns_string_with_sub_types() { 190 | assert_eq!( 191 | format_browser_type( 192 | &ServiceType::with_sub_types("http", "tcp", vec!["printer1", "printer2"]).unwrap() 193 | ), 194 | "_printer1._sub._http._tcp" 195 | ); 196 | } 197 | 198 | #[test] 199 | fn format_sub_type_returns_valid_string() { 200 | assert_eq!(format_sub_type("foo", "_http._tcp"), "_foo._sub._http._tcp"); 201 | } 202 | 203 | #[test] 204 | fn format_sub_type_strips_leading_underscore() { 205 | assert_eq!( 206 | format_sub_type("_foo", "_http._tcp"), 207 | "_foo._sub._http._tcp" 208 | ); 209 | } 210 | 211 | #[test] 212 | fn get_error_returns_valid_error_string() { 213 | assert_eq!( 214 | unsafe { get_error(avahi_sys::AVAHI_ERR_FAILURE) }, 215 | "Operation failed" 216 | ); 217 | } 218 | 219 | #[test] 220 | fn address_to_string_returns_correct_ipv4_string() { 221 | let ipv4_addr = AvahiAddress { 222 | proto: AVAHI_PROTO_INET, 223 | data: AvahiAddress__bindgen_ty_1 { 224 | ipv4: AvahiIPv4Address { 225 | address: 0x6464a8c0, // 192.168.100.100 226 | }, 227 | }, 228 | }; 229 | 230 | unsafe { 231 | assert_eq!(avahi_address_to_string(&ipv4_addr), "192.168.100.100"); 232 | } 233 | } 234 | 235 | #[test] 236 | fn address_to_string_returns_correct_ipv6_string() { 237 | let ipv6_addr = AvahiAddress { 238 | proto: AVAHI_PROTO_INET6, 239 | data: AvahiAddress__bindgen_ty_1 { 240 | ipv6: AvahiIPv6Address { 241 | address: [ 242 | 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 243 | 0x9a, 0xbc, 0xde, 0xf0, 244 | ], 245 | }, 246 | }, 247 | }; 248 | 249 | unsafe { 250 | assert_eq!( 251 | avahi_address_to_string(&ipv6_addr), 252 | "fe80::1234:5678:9abc:def0" 253 | ); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/browser.rs: -------------------------------------------------------------------------------- 1 | //! Avahi implementation for cross-platform browser 2 | 3 | use super::avahi_util; 4 | use super::client::{ManagedAvahiClient, ManagedAvahiClientParams}; 5 | use super::poll::ManagedAvahiSimplePoll; 6 | use super::raw_browser::{ManagedAvahiServiceBrowser, ManagedAvahiServiceBrowserParams}; 7 | use super::{ 8 | resolver::{ 9 | ManagedAvahiServiceResolver, ManagedAvahiServiceResolverParams, ServiceResolverSet, 10 | }, 11 | string_list::ManagedAvahiStringList, 12 | }; 13 | use crate::ffi::{c_str, AsRaw, FromRaw}; 14 | use crate::prelude::*; 15 | use crate::Result; 16 | use crate::{ 17 | BrowserEvent, EventLoop, NetworkInterface, ServiceBrowserCallback, ServiceDiscovery, 18 | ServiceRemoval, ServiceType, TxtRecord, 19 | }; 20 | use avahi_sys::{ 21 | AvahiAddress, AvahiBrowserEvent, AvahiClient, AvahiClientFlags, AvahiClientState, AvahiIfIndex, 22 | AvahiLookupResultFlags, AvahiProtocol, AvahiResolverEvent, AvahiServiceBrowser, 23 | AvahiServiceResolver, AvahiStringList, 24 | }; 25 | use libc::{c_char, c_void}; 26 | use std::any::Any; 27 | use std::ffi::CString; 28 | use std::str::FromStr; 29 | use std::sync::Arc; 30 | use std::{fmt, ptr}; 31 | 32 | #[derive(Debug)] 33 | pub struct AvahiMdnsBrowser { 34 | context: Box, 35 | client: Option>, 36 | poll: Option>, 37 | } 38 | 39 | impl TMdnsBrowser for AvahiMdnsBrowser { 40 | fn new(service_type: ServiceType) -> Self { 41 | Self { 42 | client: None, 43 | poll: None, 44 | context: Box::new(AvahiBrowserContext::new( 45 | c_string!(avahi_util::format_browser_type(&service_type)), 46 | avahi_sys::AVAHI_IF_UNSPEC, 47 | )), 48 | } 49 | } 50 | 51 | fn set_network_interface(&mut self, interface: NetworkInterface) { 52 | self.context.interface_index = avahi_util::interface_index(interface); 53 | } 54 | 55 | fn network_interface(&self) -> NetworkInterface { 56 | avahi_util::interface_from_index(self.context.interface_index) 57 | } 58 | 59 | fn set_service_callback(&mut self, service_callback: Box) { 60 | self.context.service_callback = Some(service_callback); 61 | } 62 | 63 | fn set_context(&mut self, context: Box) { 64 | self.context.user_context = Some(Arc::from(context)); 65 | } 66 | 67 | fn context(&self) -> Option<&dyn Any> { 68 | self.context.user_context.as_ref().map(|c| c.as_ref()) 69 | } 70 | 71 | fn browse_services(&mut self) -> Result { 72 | debug!("Browsing services: {:?}", self); 73 | 74 | self.poll = Some(Arc::new(unsafe { ManagedAvahiSimplePoll::new() }?)); 75 | 76 | let poll = self 77 | .poll 78 | .as_ref() 79 | .ok_or("could not get poll as ref")? 80 | .clone(); 81 | 82 | let client_params = ManagedAvahiClientParams::builder() 83 | .poll(poll) 84 | .flags(AvahiClientFlags(0)) 85 | .callback(Some(client_callback)) 86 | .userdata(self.context.as_raw()) 87 | .build()?; 88 | 89 | self.client = Some(Arc::new(unsafe { ManagedAvahiClient::new(client_params) }?)); 90 | 91 | self.context.client.clone_from(&self.client); 92 | 93 | unsafe { 94 | if let Err(e) = create_browser(&mut self.context) { 95 | self.context.invoke_callback(Err(e)); 96 | } 97 | } 98 | 99 | Ok(EventLoop::new( 100 | self.poll 101 | .as_ref() 102 | .ok_or("could not get poll as ref")? 103 | .clone(), 104 | )) 105 | } 106 | } 107 | 108 | #[derive(FromRaw, AsRaw)] 109 | struct AvahiBrowserContext { 110 | client: Option>, 111 | resolvers: ServiceResolverSet, 112 | service_callback: Option>, 113 | user_context: Option>, 114 | interface_index: AvahiIfIndex, 115 | kind: CString, 116 | browser: Option, 117 | } 118 | 119 | impl AvahiBrowserContext { 120 | fn new(kind: CString, interface_index: AvahiIfIndex) -> Self { 121 | Self { 122 | client: None, 123 | resolvers: ServiceResolverSet::default(), 124 | service_callback: None, 125 | user_context: None, 126 | interface_index, 127 | kind, 128 | browser: None, 129 | } 130 | } 131 | 132 | fn invoke_callback(&self, result: Result) { 133 | if let Some(f) = &self.service_callback { 134 | f(result, self.user_context.clone()); 135 | } else { 136 | warn!("attempted to invoke browser callback but none was set"); 137 | } 138 | } 139 | } 140 | 141 | impl fmt::Debug for AvahiBrowserContext { 142 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 143 | f.debug_struct("AvahiBrowserContext") 144 | .field("resolvers", &self.resolvers) 145 | .finish() 146 | } 147 | } 148 | 149 | unsafe extern "C" fn client_callback( 150 | client: *mut AvahiClient, 151 | state: AvahiClientState, 152 | userdata: *mut c_void, 153 | ) { 154 | let context = AvahiBrowserContext::from_raw(userdata); 155 | 156 | if state == avahi_sys::AvahiClientState_AVAHI_CLIENT_FAILURE { 157 | context.invoke_callback(Err(avahi_util::get_last_error(client).into())); 158 | } 159 | } 160 | 161 | unsafe fn create_browser(context: &mut AvahiBrowserContext) -> Result<()> { 162 | context.browser = Some(ManagedAvahiServiceBrowser::new( 163 | ManagedAvahiServiceBrowserParams::builder() 164 | .interface(context.interface_index) 165 | .protocol(avahi_sys::AVAHI_PROTO_UNSPEC) 166 | .kind(context.kind.as_ptr()) 167 | .domain(ptr::null_mut()) 168 | .flags(0) 169 | .callback(Some(browse_callback)) 170 | .userdata(context.as_raw()) 171 | .client(Arc::clone( 172 | context 173 | .client 174 | .as_ref() 175 | .ok_or("could not get client as ref")?, 176 | )) 177 | .build()?, 178 | )?); 179 | 180 | Ok(()) 181 | } 182 | 183 | unsafe extern "C" fn browse_callback( 184 | _browser: *mut AvahiServiceBrowser, 185 | interface: AvahiIfIndex, 186 | protocol: AvahiProtocol, 187 | event: AvahiBrowserEvent, 188 | name: *const c_char, 189 | kind: *const c_char, 190 | domain: *const c_char, 191 | _flags: AvahiLookupResultFlags, 192 | userdata: *mut c_void, 193 | ) { 194 | let context = AvahiBrowserContext::from_raw(userdata); 195 | 196 | match event { 197 | avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_NEW => { 198 | if let Err(e) = handle_browser_new(context, interface, protocol, name, kind, domain) { 199 | context.invoke_callback(Err(e)); 200 | } 201 | } 202 | avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_FAILURE => { 203 | context.invoke_callback(Err("browser failure".into())) 204 | } 205 | avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_REMOVE => { 206 | handle_browser_remove(context, name, kind, domain); 207 | } 208 | _ => {} 209 | }; 210 | } 211 | 212 | unsafe fn handle_browser_new( 213 | context: &mut AvahiBrowserContext, 214 | interface: AvahiIfIndex, 215 | protocol: AvahiProtocol, 216 | name: *const c_char, 217 | kind: *const c_char, 218 | domain: *const c_char, 219 | ) -> Result<()> { 220 | let raw_context = context.as_raw(); 221 | 222 | let client = context 223 | .client 224 | .as_ref() 225 | .ok_or("expected initialized client")?; 226 | 227 | context.resolvers.insert(ManagedAvahiServiceResolver::new( 228 | ManagedAvahiServiceResolverParams::builder() 229 | .client(client.clone()) 230 | .interface(interface) 231 | .protocol(protocol) 232 | .name(name) 233 | .kind(kind) 234 | .domain(domain) 235 | .aprotocol(avahi_sys::AVAHI_PROTO_UNSPEC) 236 | .flags(0) 237 | .callback(Some(resolve_callback)) 238 | .userdata(raw_context) 239 | .build()?, 240 | )?); 241 | 242 | Ok(()) 243 | } 244 | 245 | unsafe fn handle_browser_remove( 246 | ctx: &mut AvahiBrowserContext, 247 | name: *const c_char, 248 | regtype: *const c_char, 249 | domain: *const c_char, 250 | ) { 251 | let name = c_str::raw_to_str(name); 252 | let regtype = c_str::raw_to_str(regtype); 253 | let domain = c_str::raw_to_str(domain); 254 | 255 | ctx.invoke_callback(Ok(BrowserEvent::Remove( 256 | ServiceRemoval::builder() 257 | .name(name.to_string()) 258 | .kind(regtype.to_string()) 259 | .domain(domain.to_string()) 260 | .build() 261 | .expect("could not build ServiceRemoval"), 262 | ))); 263 | } 264 | 265 | unsafe extern "C" fn resolve_callback( 266 | resolver: *mut AvahiServiceResolver, 267 | _interface: AvahiIfIndex, 268 | _protocol: AvahiProtocol, 269 | event: AvahiResolverEvent, 270 | name: *const c_char, 271 | kind: *const c_char, 272 | domain: *const c_char, 273 | host_name: *const c_char, 274 | addr: *const AvahiAddress, 275 | port: u16, 276 | txt: *mut AvahiStringList, 277 | _flags: AvahiLookupResultFlags, 278 | userdata: *mut c_void, 279 | ) { 280 | let name = c_str::raw_to_str(name); 281 | let kind = c_str::raw_to_str(kind); 282 | let domain = c_str::raw_to_str(domain); 283 | 284 | let context = AvahiBrowserContext::from_raw(userdata); 285 | 286 | match event { 287 | avahi_sys::AvahiResolverEvent_AVAHI_RESOLVER_FAILURE => { 288 | context.invoke_callback(Err(format!( 289 | "failed to resolve service `{}` of type `{}` in domain `{}`", 290 | name, kind, domain 291 | ) 292 | .into())); 293 | } 294 | avahi_sys::AvahiResolverEvent_AVAHI_RESOLVER_FOUND => { 295 | let result = handle_resolver_found( 296 | context, 297 | c_str::raw_to_str(host_name), 298 | addr, 299 | name, 300 | kind, 301 | domain, 302 | port, 303 | txt, 304 | ); 305 | 306 | if let Err(e) = result { 307 | context.invoke_callback(Err(e)); 308 | } 309 | } 310 | _ => {} 311 | }; 312 | 313 | context.resolvers.remove_raw(resolver); 314 | } 315 | 316 | #[allow(clippy::too_many_arguments)] 317 | unsafe fn handle_resolver_found( 318 | context: &AvahiBrowserContext, 319 | host_name: &str, 320 | addr: *const AvahiAddress, 321 | name: &str, 322 | kind: &str, 323 | domain: &str, 324 | port: u16, 325 | txt: *mut AvahiStringList, 326 | ) -> Result<()> { 327 | let address = avahi_util::avahi_address_to_string(addr); 328 | 329 | let txt = if txt.is_null() { 330 | None 331 | } else { 332 | Some(TxtRecord::from(ManagedAvahiStringList::clone_raw(txt))) 333 | }; 334 | 335 | let result = ServiceDiscovery::builder() 336 | .name(name.to_string()) 337 | .service_type(ServiceType::from_str(kind)?) 338 | .domain(domain.to_string()) 339 | .host_name(host_name.to_string()) 340 | .address(address) 341 | .port(port) 342 | .txt(txt) 343 | .build()?; 344 | 345 | debug!("Service resolved: {:?}", result); 346 | 347 | context.invoke_callback(Ok(BrowserEvent::Add(result))); 348 | 349 | Ok(()) 350 | } 351 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/client.rs: -------------------------------------------------------------------------------- 1 | //! Rust friendly `AvahiClient` wrappers/helpers 2 | 3 | use std::sync::Arc; 4 | 5 | use super::{avahi_util, poll::ManagedAvahiSimplePoll}; 6 | use crate::ffi::c_str; 7 | use crate::Result; 8 | use avahi_sys::{ 9 | avahi_client_free, avahi_client_get_host_name, avahi_client_new, avahi_simple_poll_get, 10 | AvahiClient, AvahiClientCallback, AvahiClientFlags, 11 | }; 12 | use libc::{c_int, c_void}; 13 | 14 | /// Wraps the `AvahiClient` type from the raw Avahi bindings. 15 | /// 16 | /// This struct allocates a new `*mut AvahiClient` when `ManagedAvahiClient::new()` is invoked and 17 | /// calls the Avahi function responsible for freeing the client on `trait Drop`. 18 | #[derive(Debug)] 19 | pub struct ManagedAvahiClient { 20 | pub(crate) inner: *mut AvahiClient, 21 | _poll: Arc, 22 | } 23 | 24 | impl ManagedAvahiClient { 25 | /// Initializes the underlying `*mut AvahiClient` and verifies it was created; returning 26 | /// `Err(String)` if unsuccessful. 27 | /// 28 | /// # Safety 29 | /// This function is unsafe because of the raw pointer dereference. 30 | pub unsafe fn new( 31 | ManagedAvahiClientParams { 32 | poll, 33 | flags, 34 | callback, 35 | userdata, 36 | }: ManagedAvahiClientParams, 37 | ) -> Result { 38 | let mut err: c_int = 0; 39 | 40 | let inner = avahi_client_new( 41 | avahi_simple_poll_get(poll.inner()), 42 | flags, 43 | callback, 44 | userdata, 45 | &mut err, 46 | ); 47 | 48 | if inner.is_null() { 49 | return Err("could not initialize AvahiClient".into()); 50 | } 51 | 52 | match err { 53 | 0 => Ok(Self { inner, _poll: poll }), 54 | _ => Err(format!( 55 | "could not initialize AvahiClient: {}", 56 | avahi_util::get_error(err) 57 | ) 58 | .into()), 59 | } 60 | } 61 | 62 | /// Delegate function for [`avahi_client_get_host_name()`]. 63 | /// 64 | /// [`avahi_client_get_host_name()`]: https://avahi.org/doxygen/html/client_8h.html#a89378618c3c592a255551c308ba300bf 65 | /// 66 | /// # Safety 67 | /// This function is unsafe because of the raw pointer dereference. 68 | pub unsafe fn host_name<'a>(&self) -> Result<&'a str> { 69 | get_host_name(self.inner) 70 | } 71 | } 72 | 73 | impl Drop for ManagedAvahiClient { 74 | fn drop(&mut self) { 75 | unsafe { avahi_client_free(self.inner) }; 76 | } 77 | } 78 | 79 | unsafe impl Send for ManagedAvahiClient {} 80 | unsafe impl Sync for ManagedAvahiClient {} 81 | 82 | /// Holds parameters for initializing a new `ManagedAvahiClient` with `ManagedAvahiClient::new()`. 83 | /// 84 | /// See [`avahi_client_new()`] for more information about these parameters. 85 | /// 86 | /// [`avahi_client_new()`]: https://avahi.org/doxygen/html/client_8h.html#a07b2a33a3e7cbb18a0eb9d00eade6ae6 87 | #[derive(Builder, BuilderDelegate)] 88 | pub struct ManagedAvahiClientParams { 89 | poll: Arc, 90 | flags: AvahiClientFlags, 91 | callback: AvahiClientCallback, 92 | userdata: *mut c_void, 93 | } 94 | 95 | pub(super) unsafe fn get_host_name<'a>(client: *mut AvahiClient) -> Result<&'a str> { 96 | assert_not_null!(client); 97 | let host_name = avahi_client_get_host_name(client); 98 | 99 | if !host_name.is_null() { 100 | Ok(c_str::raw_to_str(host_name)) 101 | } else { 102 | Err("could not get host name from AvahiClient".into()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/entry_group.rs: -------------------------------------------------------------------------------- 1 | //! Rust friendly `AvahiEntryGroup` wrappers/helpers 2 | 3 | use std::sync::Arc; 4 | 5 | use super::{client::ManagedAvahiClient, string_list::ManagedAvahiStringList}; 6 | use crate::avahi::avahi_util; 7 | use crate::ffi::UnwrapMutOrNull; 8 | use crate::Result; 9 | use avahi_sys::{ 10 | avahi_client_errno, avahi_entry_group_add_service_strlst, 11 | avahi_entry_group_add_service_subtype, avahi_entry_group_commit, avahi_entry_group_free, 12 | avahi_entry_group_is_empty, avahi_entry_group_new, avahi_entry_group_reset, AvahiClient, 13 | AvahiEntryGroup, AvahiEntryGroupCallback, AvahiIfIndex, AvahiProtocol, AvahiPublishFlags, 14 | }; 15 | use libc::{c_char, c_void}; 16 | 17 | /// Wraps the `AvahiEntryGroup` type from the raw Avahi bindings. 18 | /// 19 | /// This struct allocates a new `*mut AvahiEntryGroup` when `ManagedAvahiEntryGroup::new()` is 20 | /// invoked and calls the Avahi function responsible for freeing the group on `trait Drop`. 21 | #[derive(Debug)] 22 | pub struct ManagedAvahiEntryGroup { 23 | inner: *mut AvahiEntryGroup, 24 | _client: Arc, 25 | } 26 | 27 | impl ManagedAvahiEntryGroup { 28 | /// Initializes the underlying `*mut AvahiEntryGroup` and verifies it was created; returning 29 | /// `Err(String)` if unsuccessful. 30 | /// 31 | /// # Safety 32 | /// This function is unsafe because of the raw pointer dereference. 33 | pub unsafe fn new( 34 | ManagedAvahiEntryGroupParams { 35 | client, 36 | callback, 37 | userdata, 38 | }: ManagedAvahiEntryGroupParams, 39 | ) -> Result { 40 | let inner = avahi_entry_group_new(client.inner, callback, userdata); 41 | 42 | if inner.is_null() { 43 | let err = avahi_util::get_error(avahi_client_errno(client.inner)); 44 | Err(format!("could not initialize AvahiEntryGroup: {}", err).into()) 45 | } else { 46 | Ok(Self { 47 | inner, 48 | _client: client, 49 | }) 50 | } 51 | } 52 | 53 | /// Delegate function for [`avahi_entry_group_is_empty()`]. 54 | /// 55 | /// [`avahi_entry_group_is_empty()`]: https://avahi.org/doxygen/html/publish_8h.html#af5a78ee1fda6678970536889d459d85c 56 | /// 57 | /// # Safety 58 | /// This function is unsafe because of the call to `avahi_entry_group_is_empty()`. 59 | pub unsafe fn is_empty(&self) -> bool { 60 | avahi_entry_group_is_empty(self.inner) != 0 61 | } 62 | 63 | /// Delegate function for [`avahi_entry_group_add_service()`]. 64 | /// 65 | /// Also propagates any error returned into a `Result`. 66 | /// 67 | /// [`avahi_entry_group_add_service()`]: https://avahi.org/doxygen/html/publish_8h.html#acb05a7d3d23a3b825ca77cb1c7d00ce4 68 | /// 69 | /// # Safety 70 | /// This function is unsafe because of the call to `avahi_entry_group_add_service_strlst()`. 71 | pub unsafe fn add_service( 72 | &mut self, 73 | AddServiceParams { 74 | interface, 75 | protocol, 76 | flags, 77 | name, 78 | kind, 79 | domain, 80 | host, 81 | port, 82 | txt, 83 | }: AddServiceParams, 84 | ) -> Result<()> { 85 | avahi_util::sys_exec( 86 | || { 87 | avahi_entry_group_add_service_strlst( 88 | self.inner, 89 | interface, 90 | protocol, 91 | flags, 92 | name, 93 | kind, 94 | domain, 95 | host, 96 | port, 97 | txt.map(|t| t.inner()).unwrap_mut_or_null(), 98 | ) 99 | }, 100 | "could not register service", 101 | ) 102 | } 103 | 104 | /// Delegate function for [`avahi_entry_group_add_service_subtype()`]. 105 | /// 106 | /// Also propagates any error returned into a `Result`. 107 | /// 108 | /// [`avahi_entry_group_add_service_subtype()`]: https://avahi.org/doxygen/html/publish_8h.html#a93841be69a152d3134b408c25bb4d5d5 109 | /// 110 | /// # Safety 111 | /// This function is unsafe because of the call to `avahi_entry_group_add_service_subtype()`. 112 | pub unsafe fn add_service_subtype( 113 | &mut self, 114 | AddServiceSubtypeParams { 115 | interface, 116 | protocol, 117 | flags, 118 | name, 119 | kind, 120 | domain, 121 | subtype, 122 | }: AddServiceSubtypeParams, 123 | ) -> Result<()> { 124 | avahi_util::sys_exec( 125 | || { 126 | avahi_entry_group_add_service_subtype( 127 | self.inner, interface, protocol, flags, name, kind, domain, subtype, 128 | ) 129 | }, 130 | "could not register service subtype", 131 | ) 132 | } 133 | 134 | /// Delegate function for [`avahi_entry_group_commit()`]. 135 | /// 136 | /// Also propagates any error returned into a `Result`. 137 | /// 138 | /// [`avahi_entry_group_commit()`]: https://avahi.org/doxygen/html/publish_8h.html#a2375338d23af4281399404758840a2de 139 | /// 140 | /// # Safety 141 | /// This function is unsafe because of the call to `avahi_entry_group_commit()`. 142 | pub unsafe fn commit(&mut self) -> Result<()> { 143 | avahi_util::sys_exec( 144 | || avahi_entry_group_commit(self.inner), 145 | "could not commit service", 146 | ) 147 | } 148 | 149 | /// Delegate function for [`avahi_entry_group_reset()`]. 150 | /// 151 | /// [`avahi_entry_group_reset()`]: https://avahi.org/doxygen/html/publish_8h.html#a1293bbccf878dbeb9916660022bc71b2 152 | /// 153 | /// # Safety 154 | /// This function is unsafe because of the call to `avahi_entry_group_reset()`. 155 | pub unsafe fn reset(&mut self) { 156 | avahi_entry_group_reset(self.inner); 157 | } 158 | 159 | /// Delegate function for [`avahi_entry_group_get_client()`]. 160 | /// 161 | /// # Safety 162 | /// This function is unsafe because it returns a raw pointer to 163 | /// the underlying `AvahiClient`. 164 | pub unsafe fn get_client(&self) -> *mut AvahiClient { 165 | avahi_sys::avahi_entry_group_get_client(self.inner) 166 | } 167 | } 168 | 169 | impl Drop for ManagedAvahiEntryGroup { 170 | fn drop(&mut self) { 171 | unsafe { avahi_entry_group_free(self.inner) }; 172 | } 173 | } 174 | 175 | /// Holds parameters for initializing a new `ManagedAvahiEntryGroup` with 176 | /// `ManagedAvahiEntryGroup::new()`. 177 | /// 178 | /// See [`avahi_entry_group_new()`] for more information about these parameters. 179 | /// 180 | /// [avahi_entry_group_new()]: https://avahi.org/doxygen/html/publish_8h.html#abb17598f2b6ec3c3f69defdd488d568c 181 | #[derive(Builder, BuilderDelegate)] 182 | pub struct ManagedAvahiEntryGroupParams { 183 | client: Arc, 184 | callback: AvahiEntryGroupCallback, 185 | userdata: *mut c_void, 186 | } 187 | 188 | /// Holds parameters for `ManagedAvahiEntryGroup::add_service()`. 189 | /// 190 | /// See [`avahi_entry_group_add_service()`] for more information about these parameters. 191 | /// 192 | /// [`avahi_entry_group_add_service()`]: https://avahi.org/doxygen/html/publish_8h.html#acb05a7d3d23a3b825ca77cb1c7d00ce4 193 | #[derive(Builder, BuilderDelegate)] 194 | pub struct AddServiceParams<'a> { 195 | interface: AvahiIfIndex, 196 | protocol: AvahiProtocol, 197 | flags: AvahiPublishFlags, 198 | name: *const c_char, 199 | kind: *const c_char, 200 | domain: *const c_char, 201 | host: *const c_char, 202 | port: u16, 203 | txt: Option<&'a ManagedAvahiStringList>, 204 | } 205 | 206 | /// Holds parameters for `ManagedAvahiEntryGroup::add_service_subtype()`. 207 | /// 208 | /// See [`avahi_entry_group_add_service_subtype()`] for more information about these parameters. 209 | /// 210 | /// [`avahi_entry_group_add_service_subtype()`]: https://www.avahi.org/doxygen/html/publish_8h.html#a93841be69a152d3134b408c25bb4d5d5 211 | #[derive(Builder, BuilderDelegate)] 212 | pub struct AddServiceSubtypeParams { 213 | interface: AvahiIfIndex, 214 | protocol: AvahiProtocol, 215 | flags: AvahiPublishFlags, 216 | name: *const c_char, 217 | kind: *const c_char, 218 | domain: *const c_char, 219 | subtype: *const c_char, 220 | } 221 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/event_loop.rs: -------------------------------------------------------------------------------- 1 | //! Event loop for running a `MdnsService` or `MdnsBrowser`. 2 | 3 | use super::poll::ManagedAvahiSimplePoll; 4 | use crate::event_loop::TEventLoop; 5 | use crate::Result; 6 | use std::sync::Arc; 7 | use std::time::Duration; 8 | 9 | #[derive(new)] 10 | pub struct AvahiEventLoop { 11 | poll: Arc, 12 | } 13 | 14 | impl TEventLoop for AvahiEventLoop { 15 | /// Polls for new events. 16 | /// 17 | /// Internally calls `ManagedAvahiSimplePoll::iterate(..)`. 18 | /// In systems where the C implementation of `poll(.., timeout)` 19 | /// does not respect the `timeout` parameter, the `timeout` passed 20 | /// here will have no effect -- ie will return immediately. 21 | fn poll(&self, timeout: Duration) -> Result<()> { 22 | unsafe { self.poll.iterate(timeout) } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Linux-specific ZeroConf bindings 2 | //! 3 | //! This module wraps the [Avahi] mDNS implementation which can be found in most major Linux 4 | //! distributions. It is a sufficient (and often more featured) replacement for Apple's [Bonjour]. 5 | //! 6 | //! [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) 7 | //! [Avahi]: https://en.wikipedia.org/wiki/Avahi_(software) 8 | 9 | pub mod avahi_util; 10 | pub mod browser; 11 | pub mod client; 12 | pub mod entry_group; 13 | pub mod event_loop; 14 | pub mod poll; 15 | pub mod raw_browser; 16 | pub mod resolver; 17 | pub mod service; 18 | pub mod string_list; 19 | pub mod txt_record; 20 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/poll.rs: -------------------------------------------------------------------------------- 1 | //! Rust friendly `AvahiSimplePoll` wrappers/helpers 2 | 3 | use crate::Result; 4 | use crate::{avahi::avahi_util, error::Error}; 5 | use avahi_sys::{ 6 | avahi_simple_poll_free, avahi_simple_poll_iterate, avahi_simple_poll_loop, 7 | avahi_simple_poll_new, AvahiSimplePoll, 8 | }; 9 | use std::{convert::TryInto, time::Duration}; 10 | 11 | /// Wraps the `AvahiSimplePoll` type from the raw Avahi bindings. 12 | /// 13 | /// This struct allocates a new `*mut AvahiSimplePoll` when `ManagedAvahiClient::new()` is invoked 14 | /// and calls the Avahi function responsible for freeing the poll on `trait Drop`. 15 | #[derive(Debug)] 16 | pub struct ManagedAvahiSimplePoll(*mut AvahiSimplePoll); 17 | 18 | impl ManagedAvahiSimplePoll { 19 | /// Initializes the underlying `*mut AvahiSimplePoll` and verifies it was created; returning 20 | /// `Err(String)` if unsuccessful 21 | /// 22 | /// # Safety 23 | /// This function is unsafe because of the raw pointer dereference. 24 | pub unsafe fn new() -> Result { 25 | let poll = avahi_simple_poll_new(); 26 | if poll.is_null() { 27 | Err("could not initialize AvahiSimplePoll".into()) 28 | } else { 29 | Ok(Self(poll)) 30 | } 31 | } 32 | 33 | /// Delegate function for [`avahi_simple_poll_loop()`]. 34 | /// 35 | /// [`avahi_simple_poll_loop()`]: https://avahi.org/doxygen/html/simple-watch_8h.html#a14b4cb29832e8c3de609d4c4e5611985 36 | /// 37 | /// # Safety 38 | /// This function is unsafe because of the call to `avahi_simple_poll_loop()`. 39 | pub unsafe fn start_loop(&self) -> Result<()> { 40 | avahi_util::sys_exec( 41 | || avahi_simple_poll_loop(self.0), 42 | "could not start AvahiSimplePoll", 43 | ) 44 | } 45 | 46 | /// Delegate function for [`avahi_simple_poll_iterate()`]. 47 | /// 48 | /// [`avahi_simple_poll_iterate()`]: https://avahi.org/doxygen/html/simple-watch_8h.html#ad5b7c9d3b7a6584d609241ee6f472a2e 49 | /// 50 | /// # Safety 51 | /// This function is unsafe because of the call to `avahi_simple_poll_iterate()`. 52 | pub unsafe fn iterate(&self, timeout: Duration) -> Result<()> { 53 | let sleep_time: i32 = timeout 54 | .as_millis() // `avahi_simple_poll_iterate()` expects `sleep_time` in msecs. 55 | .try_into() // `avahi_simple_poll_iterate()` expects `sleep_time` as an i32. 56 | .unwrap_or(i32::MAX); // if converting to an i32 overflows, just use the largest number we can. 57 | 58 | // Returns -1 on error, 0 on success and 1 if a quit request has been scheduled 59 | match avahi_simple_poll_iterate(self.0, sleep_time) { 60 | 0 | 1 => Ok(()), 61 | -1 => Err(Error::from( 62 | "avahi_simple_poll_iterate(..) threw an error result", 63 | )), 64 | _ => Err(Error::from( 65 | "avahi_simple_poll_iterate(..) returned an unknown result", 66 | )), 67 | } 68 | } 69 | 70 | pub(super) fn inner(&self) -> *mut AvahiSimplePoll { 71 | self.0 72 | } 73 | } 74 | 75 | impl Drop for ManagedAvahiSimplePoll { 76 | fn drop(&mut self) { 77 | unsafe { avahi_simple_poll_free(self.0) }; 78 | } 79 | } 80 | 81 | unsafe impl Send for ManagedAvahiSimplePoll {} 82 | unsafe impl Sync for ManagedAvahiSimplePoll {} 83 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/raw_browser.rs: -------------------------------------------------------------------------------- 1 | //! Rust friendly `AvahiServiceBrowser` wrappers/helpers 2 | 3 | use std::sync::Arc; 4 | 5 | use crate::Result; 6 | use avahi_sys::{ 7 | avahi_service_browser_free, avahi_service_browser_get_client, avahi_service_browser_new, 8 | AvahiClient, AvahiIfIndex, AvahiLookupFlags, AvahiProtocol, AvahiServiceBrowser, 9 | AvahiServiceBrowserCallback, 10 | }; 11 | use libc::{c_char, c_void}; 12 | 13 | use super::client::ManagedAvahiClient; 14 | 15 | /// Wraps the `AvahiServiceBrowser` type from the raw Avahi bindings. 16 | /// 17 | /// This struct allocates a new `*mut AvahiServiceBrowser` when `ManagedAvahiServiceBrowser::new()` 18 | /// is invoked and calls the Avahi function responsible for freeing the client on `trait Drop`. 19 | #[derive(Debug)] 20 | pub struct ManagedAvahiServiceBrowser { 21 | inner: *mut AvahiServiceBrowser, 22 | _client: Arc, 23 | } 24 | 25 | impl ManagedAvahiServiceBrowser { 26 | /// Initializes the underlying `*mut AvahiClient` and verifies it was created; returning 27 | /// `Err(String)` if unsuccessful. 28 | /// 29 | /// # Safety 30 | /// This function is unsafe because of the raw pointer dereference. 31 | pub unsafe fn new( 32 | ManagedAvahiServiceBrowserParams { 33 | client, 34 | interface, 35 | protocol, 36 | kind, 37 | domain, 38 | flags, 39 | callback, 40 | userdata, 41 | }: ManagedAvahiServiceBrowserParams, 42 | ) -> Result { 43 | let inner = avahi_service_browser_new( 44 | client.inner, 45 | interface, 46 | protocol, 47 | kind, 48 | domain, 49 | flags, 50 | callback, 51 | userdata, 52 | ); 53 | 54 | if inner.is_null() { 55 | Err("could not initialize Avahi service browser".into()) 56 | } else { 57 | Ok(Self { 58 | inner, 59 | _client: client, 60 | }) 61 | } 62 | } 63 | 64 | /// Returns the underlying `*mut AvahiServiceBrowser`. 65 | /// 66 | /// # Safety 67 | /// This function leaks the internal raw pointer, useful for accessing within callbacks where 68 | /// you are sure the pointer is still valid. 69 | pub unsafe fn get_client(&self) -> *mut AvahiClient { 70 | avahi_service_browser_get_client(self.inner) 71 | } 72 | } 73 | 74 | impl Drop for ManagedAvahiServiceBrowser { 75 | fn drop(&mut self) { 76 | unsafe { avahi_service_browser_free(self.inner) }; 77 | } 78 | } 79 | 80 | /// Holds parameters for initializing a new `ManagedAvahiServiceBrowser` with 81 | /// `ManagedAvahiServiceBrowser::new()`. 82 | /// 83 | /// See [`avahi_service_browser_new()`] for more information about these parameters. 84 | /// 85 | /// [`avahi_service_browser_new()`]: https://avahi.org/doxygen/html/lookup_8h.html#a52d55a5156a7943012d03e6700880d2b 86 | #[derive(Builder, BuilderDelegate)] 87 | pub struct ManagedAvahiServiceBrowserParams { 88 | client: Arc, 89 | interface: AvahiIfIndex, 90 | protocol: AvahiProtocol, 91 | kind: *const c_char, 92 | domain: *const c_char, 93 | flags: AvahiLookupFlags, 94 | callback: AvahiServiceBrowserCallback, 95 | userdata: *mut c_void, 96 | } 97 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/resolver.rs: -------------------------------------------------------------------------------- 1 | //! Rust friendly `AvahiServiceResolver` wrappers/helpers 2 | 3 | use crate::Result; 4 | use avahi_sys::{ 5 | avahi_service_resolver_free, avahi_service_resolver_new, AvahiIfIndex, AvahiLookupFlags, 6 | AvahiProtocol, AvahiServiceResolver, AvahiServiceResolverCallback, 7 | }; 8 | use libc::{c_char, c_void}; 9 | use std::{collections::HashMap, sync::Arc}; 10 | 11 | use super::client::ManagedAvahiClient; 12 | 13 | /// Wraps the `AvahiServiceResolver` type from the raw Avahi bindings. 14 | /// 15 | /// This struct allocates a new `*mut AvahiServiceResolver` when 16 | /// `ManagedAvahiServiceResolver::new()` is invoked and calls the Avahi function responsible for 17 | /// freeing the client on `trait Drop`. 18 | #[derive(Debug)] 19 | pub struct ManagedAvahiServiceResolver { 20 | inner: *mut AvahiServiceResolver, 21 | _client: Arc, 22 | } 23 | 24 | impl ManagedAvahiServiceResolver { 25 | /// Initializes the underlying `*mut AvahiServiceResolver` and verifies it was created; 26 | /// returning `Err(String)` if unsuccessful. 27 | /// 28 | /// # Safety 29 | /// This function is unsafe because of the raw pointer dereference. 30 | pub unsafe fn new( 31 | ManagedAvahiServiceResolverParams { 32 | client, 33 | interface, 34 | protocol, 35 | name, 36 | kind, 37 | domain, 38 | aprotocol, 39 | flags, 40 | callback, 41 | userdata, 42 | }: ManagedAvahiServiceResolverParams, 43 | ) -> Result { 44 | let inner = avahi_service_resolver_new( 45 | client.inner, 46 | interface, 47 | protocol, 48 | name, 49 | kind, 50 | domain, 51 | aprotocol, 52 | flags, 53 | callback, 54 | userdata, 55 | ); 56 | 57 | if inner.is_null() { 58 | Err("could not initialize AvahiServiceResolver".into()) 59 | } else { 60 | Ok(Self { 61 | inner, 62 | _client: client, 63 | }) 64 | } 65 | } 66 | } 67 | 68 | impl Drop for ManagedAvahiServiceResolver { 69 | fn drop(&mut self) { 70 | unsafe { avahi_service_resolver_free(self.inner) }; 71 | } 72 | } 73 | 74 | /// Holds parameters for initializing a new `ManagedAvahiServiceResolver` with 75 | /// `ManagedAvahiServiceResolver::new()`. 76 | /// 77 | /// See [`avahi_service_resolver_new()`] for more information about these parameters. 78 | /// 79 | /// [`avahi_service_resolver_new()`]: https://avahi.org/doxygen/html/lookup_8h.html#a904611a4134ceb5919f6bb637df84124 80 | #[derive(Builder, BuilderDelegate)] 81 | pub struct ManagedAvahiServiceResolverParams { 82 | client: Arc, 83 | interface: AvahiIfIndex, 84 | protocol: AvahiProtocol, 85 | name: *const c_char, 86 | kind: *const c_char, 87 | domain: *const c_char, 88 | aprotocol: AvahiProtocol, 89 | flags: AvahiLookupFlags, 90 | callback: AvahiServiceResolverCallback, 91 | userdata: *mut c_void, 92 | } 93 | 94 | #[derive(Default, Debug)] 95 | pub(crate) struct ServiceResolverSet { 96 | resolvers: HashMap<*mut AvahiServiceResolver, ManagedAvahiServiceResolver>, 97 | } 98 | 99 | impl ServiceResolverSet { 100 | pub fn insert(&mut self, resolver: ManagedAvahiServiceResolver) { 101 | self.resolvers.insert(resolver.inner, resolver); 102 | } 103 | 104 | pub fn remove_raw(&mut self, raw: *mut AvahiServiceResolver) { 105 | self.resolvers.remove(&raw); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/service.rs: -------------------------------------------------------------------------------- 1 | //! Avahi implementation for cross-platform service. 2 | 3 | use super::avahi_util; 4 | use super::client::{ManagedAvahiClient, ManagedAvahiClientParams}; 5 | use super::entry_group::{ 6 | AddServiceParams, AddServiceSubtypeParams, ManagedAvahiEntryGroup, ManagedAvahiEntryGroupParams, 7 | }; 8 | use super::poll::ManagedAvahiSimplePoll; 9 | use crate::ffi::{c_str, AsRaw, FromRaw, UnwrapOrNull}; 10 | use crate::prelude::*; 11 | use crate::{ 12 | EventLoop, NetworkInterface, Result, ServiceRegisteredCallback, ServiceRegistration, 13 | ServiceType, TxtRecord, 14 | }; 15 | use avahi_sys::{ 16 | AvahiClient, AvahiClientFlags, AvahiClientState, AvahiEntryGroup, AvahiEntryGroupState, 17 | AvahiIfIndex, 18 | }; 19 | use libc::c_void; 20 | use std::any::Any; 21 | use std::ffi::{CStr, CString}; 22 | use std::fmt::{self, Formatter}; 23 | use std::str::FromStr; 24 | use std::sync::Arc; 25 | 26 | #[derive(Debug)] 27 | pub struct AvahiMdnsService { 28 | // note: this declaration order is important, it ensures that each 29 | // component is dropped in the correct order 30 | context: Box, 31 | client: Option>, 32 | poll: Option>, 33 | } 34 | 35 | impl TMdnsService for AvahiMdnsService { 36 | fn new(service_type: ServiceType, port: u16) -> Self { 37 | let kind = avahi_util::format_service_type(&service_type); 38 | 39 | let sub_types = service_type 40 | .sub_types() 41 | .iter() 42 | .map(|sub_type| c_string!(avahi_util::format_sub_type(sub_type, &kind))) 43 | .collect::>(); 44 | 45 | Self { 46 | client: None, 47 | poll: None, 48 | context: Box::new(AvahiServiceContext::new(c_string!(kind), port, sub_types)), 49 | } 50 | } 51 | 52 | /// Sets the name to register this service under. If no name is set, the client's host name 53 | /// will be used instead. 54 | /// 55 | /// See: [`AvahiClient::host_name()`] 56 | /// 57 | /// [`AvahiClient::host_name()`]: client/struct.ManagedAvahiClient.html#method.host_name 58 | fn set_name(&mut self, name: &str) { 59 | self.context.name = c_string!(name).into() 60 | } 61 | 62 | fn name(&self) -> Option<&str> { 63 | self.context.name.as_ref().map(c_str::to_str) 64 | } 65 | 66 | fn set_network_interface(&mut self, interface: NetworkInterface) { 67 | self.context.interface_index = avahi_util::interface_index(interface) 68 | } 69 | 70 | fn network_interface(&self) -> NetworkInterface { 71 | avahi_util::interface_from_index(self.context.interface_index) 72 | } 73 | 74 | fn set_domain(&mut self, domain: &str) { 75 | self.context.domain = c_string!(domain).into() 76 | } 77 | 78 | fn domain(&self) -> Option<&str> { 79 | self.context.domain.as_ref().map(c_str::to_str) 80 | } 81 | 82 | fn set_host(&mut self, host: &str) { 83 | self.context.host = c_string!(host).into() 84 | } 85 | 86 | fn host(&self) -> Option<&str> { 87 | self.context.host.as_ref().map(c_str::to_str) 88 | } 89 | 90 | fn set_txt_record(&mut self, txt_record: TxtRecord) { 91 | self.context.txt_record = txt_record.into() 92 | } 93 | 94 | fn txt_record(&self) -> Option<&TxtRecord> { 95 | self.context.txt_record.as_ref() 96 | } 97 | 98 | fn set_registered_callback(&mut self, registered_callback: Box) { 99 | self.context.registered_callback = registered_callback.into() 100 | } 101 | 102 | fn set_context(&mut self, context: Box) { 103 | self.context.user_context = Some(Arc::from(context)) 104 | } 105 | 106 | fn context(&self) -> Option<&dyn Any> { 107 | self.context.user_context.as_ref().map(|c| c.as_ref()) 108 | } 109 | 110 | fn register(&mut self) -> Result { 111 | debug!("Registering service: {:?}", self); 112 | 113 | self.poll = Some(Arc::new(unsafe { ManagedAvahiSimplePoll::new() }?)); 114 | 115 | let poll = self 116 | .poll 117 | .as_ref() 118 | .ok_or("could not get poll as ref")? 119 | .clone(); 120 | 121 | let client_params = ManagedAvahiClientParams::builder() 122 | .poll(poll) 123 | .flags(AvahiClientFlags(0)) 124 | .callback(Some(client_callback)) 125 | .userdata(self.context.as_raw()) 126 | .build()?; 127 | 128 | self.client = Some(Arc::new(unsafe { ManagedAvahiClient::new(client_params) }?)); 129 | 130 | self.context.client.clone_from(&self.client); 131 | 132 | unsafe { 133 | if let Err(e) = create_service(&mut self.context) { 134 | self.context.invoke_callback(Err(e)) 135 | } 136 | } 137 | 138 | Ok(EventLoop::new( 139 | self.poll 140 | .as_ref() 141 | .ok_or("could not get poll as ref")? 142 | .clone(), 143 | )) 144 | } 145 | } 146 | 147 | #[derive(FromRaw, AsRaw)] 148 | struct AvahiServiceContext { 149 | client: Option>, 150 | name: Option, 151 | kind: CString, 152 | sub_types: Vec, 153 | port: u16, 154 | group: Option, 155 | txt_record: Option, 156 | interface_index: AvahiIfIndex, 157 | domain: Option, 158 | host: Option, 159 | registered_callback: Option>, 160 | user_context: Option>, 161 | } 162 | 163 | impl AvahiServiceContext { 164 | fn new(kind: CString, port: u16, sub_types: Vec) -> Self { 165 | Self { 166 | client: None, 167 | name: None, 168 | kind, 169 | port, 170 | sub_types, 171 | group: None, 172 | txt_record: None, 173 | interface_index: avahi_sys::AVAHI_IF_UNSPEC, 174 | domain: None, 175 | host: None, 176 | registered_callback: None, 177 | user_context: None, 178 | } 179 | } 180 | 181 | fn invoke_callback(&self, result: Result) { 182 | if let Some(f) = &self.registered_callback { 183 | f(result, self.user_context.clone()); 184 | } else { 185 | warn!("attempted to invoke service callback but none was set"); 186 | } 187 | } 188 | } 189 | 190 | impl fmt::Debug for AvahiServiceContext { 191 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 192 | f.debug_struct("AvahiServiceContext") 193 | .field("name", &self.name) 194 | .field("kind", &self.kind) 195 | .field("port", &self.port) 196 | .field("group", &self.group) 197 | .finish() 198 | } 199 | } 200 | 201 | unsafe extern "C" fn client_callback( 202 | client: *mut AvahiClient, 203 | state: AvahiClientState, 204 | userdata: *mut c_void, 205 | ) { 206 | let context = AvahiServiceContext::from_raw(userdata); 207 | 208 | match state { 209 | avahi_sys::AvahiServerState_AVAHI_SERVER_INVALID 210 | | avahi_sys::AvahiServerState_AVAHI_SERVER_COLLISION 211 | | avahi_sys::AvahiServerState_AVAHI_SERVER_FAILURE => { 212 | context.invoke_callback(Err(avahi_util::get_last_error(client).into())) 213 | } 214 | _ => {} 215 | } 216 | } 217 | 218 | unsafe fn create_service(context: &mut AvahiServiceContext) -> Result<()> { 219 | if context.name.is_none() { 220 | let host_name = context 221 | .client 222 | .as_ref() 223 | .ok_or("expected initialized client")? 224 | .host_name()?; 225 | 226 | context.name = Some(c_string!(host_name.to_string())); 227 | } 228 | 229 | if context.group.is_none() { 230 | debug!("Creating group"); 231 | 232 | context.group = Some(ManagedAvahiEntryGroup::new( 233 | ManagedAvahiEntryGroupParams::builder() 234 | .client(Arc::clone( 235 | context 236 | .client 237 | .as_ref() 238 | .ok_or("could not get client as ref")?, 239 | )) 240 | .callback(Some(entry_group_callback)) 241 | .userdata(context.as_raw()) 242 | .build()?, 243 | )?); 244 | } 245 | 246 | let group = context 247 | .group 248 | .as_mut() 249 | .ok_or("could not borrow group as mut")?; 250 | 251 | if !group.is_empty() { 252 | return Ok(()); 253 | } 254 | 255 | let name = context 256 | .name 257 | .as_ref() 258 | .ok_or("could not get name as ref")? 259 | .clone(); 260 | 261 | add_services(context, &name) 262 | } 263 | 264 | unsafe fn add_services(context: &mut AvahiServiceContext, name: &CStr) -> Result<()> { 265 | debug!("Adding service: {}", context.kind.to_string_lossy()); 266 | 267 | let group = context 268 | .group 269 | .as_mut() 270 | .ok_or("could not borrow group as mut")?; 271 | 272 | let params = AddServiceParams::builder() 273 | .interface(context.interface_index) 274 | .protocol(avahi_sys::AVAHI_PROTO_UNSPEC) 275 | .flags(0) 276 | .name(name.as_ptr()) 277 | .kind(context.kind.as_ptr()) 278 | .domain(context.domain.as_ref().map(|d| d.as_ptr()).unwrap_or_null()) 279 | .host(context.host.as_ref().map(|h| h.as_ptr()).unwrap_or_null()) 280 | .port(context.port) 281 | .txt(context.txt_record.as_ref().map(|t| t.inner())) 282 | .build()?; 283 | 284 | group.add_service(params)?; 285 | 286 | for sub_type in &context.sub_types { 287 | debug!("Adding service subtype: {}", sub_type.to_string_lossy()); 288 | 289 | let params = AddServiceSubtypeParams::builder() 290 | .interface(context.interface_index) 291 | .protocol(avahi_sys::AVAHI_PROTO_UNSPEC) 292 | .flags(0) 293 | .name(name.as_ptr()) 294 | .kind(context.kind.as_ptr()) 295 | .domain(context.domain.as_ref().map(|d| d.as_ptr()).unwrap_or_null()) 296 | .subtype(sub_type.as_ptr()) 297 | .build()?; 298 | 299 | group.add_service_subtype(params)?; 300 | } 301 | 302 | group.commit() 303 | } 304 | 305 | unsafe extern "C" fn entry_group_callback( 306 | _group: *mut AvahiEntryGroup, 307 | state: AvahiEntryGroupState, 308 | userdata: *mut c_void, 309 | ) { 310 | let context = AvahiServiceContext::from_raw(userdata); 311 | 312 | let client = context 313 | .client 314 | .as_ref() 315 | .expect("expected initialized client"); 316 | 317 | match state { 318 | avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_ESTABLISHED => { 319 | context.invoke_callback(handle_group_established(context)) 320 | } 321 | avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_FAILURE => { 322 | context.invoke_callback(Err(avahi_util::get_last_error(client.inner).into())) 323 | } 324 | avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_COLLISION => { 325 | let name = context 326 | .name 327 | .as_ref() 328 | .expect("expected initialized name") 329 | .clone(); 330 | 331 | let new_name = avahi_util::alternative_service_name(name.as_c_str()); 332 | let result = add_services(context, new_name); 333 | 334 | context.name = Some(new_name.into()); 335 | 336 | if let Err(e) = result { 337 | context.invoke_callback(Err(e)) 338 | } 339 | } 340 | _ => {} 341 | } 342 | } 343 | 344 | unsafe fn handle_group_established(context: &AvahiServiceContext) -> Result { 345 | debug!("Group established"); 346 | 347 | let name = c_str::copy_raw( 348 | context 349 | .name 350 | .as_ref() 351 | .ok_or("could not get name as ref")? 352 | .as_ptr(), 353 | ); 354 | 355 | Ok(ServiceRegistration::builder() 356 | .name(name) 357 | .service_type(ServiceType::from_str(&c_str::copy_raw( 358 | context.kind.as_ptr(), 359 | ))?) 360 | .domain("local".to_string()) 361 | .build()?) 362 | } 363 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/string_list.rs: -------------------------------------------------------------------------------- 1 | //! Low level interface for interacting with `AvahiStringList`. 2 | 3 | use crate::ffi::c_str; 4 | use avahi_sys::{ 5 | avahi_free, avahi_string_list_add_pair, avahi_string_list_copy, avahi_string_list_equal, 6 | avahi_string_list_find, avahi_string_list_free, avahi_string_list_get_next, 7 | avahi_string_list_get_pair, avahi_string_list_length, avahi_string_list_new, 8 | avahi_string_list_to_string, AvahiStringList, 9 | }; 10 | use libc::{c_char, c_void}; 11 | use std::marker::PhantomData; 12 | use std::ptr; 13 | 14 | /// Wraps the `AvahiStringList` pointer from the raw Avahi bindings. 15 | /// 16 | /// `zeroconf::TxtRecord` provides the cross-platform bindings for this functionality. 17 | #[derive(Debug)] 18 | pub struct ManagedAvahiStringList(*mut AvahiStringList); 19 | 20 | impl ManagedAvahiStringList { 21 | /// Creates a new empty TXT record 22 | /// 23 | /// # Safety 24 | /// This function is unsafe because of the call to `avahi_string_list_new()`. 25 | pub unsafe fn new() -> Self { 26 | Self(avahi_string_list_new(ptr::null())) 27 | } 28 | 29 | /// Delegate function for [`avahi_string_list_add_pair()`]. 30 | /// 31 | /// # Safety 32 | /// This function is unsafe because it provides no guarantees about the given pointers that are 33 | /// dereferenced. 34 | /// 35 | /// [`avahi_string_list_add_pair()`]: https://avahi.org/doxygen/html/strlst_8h.html#a72e1b0f724f0c29b5e3c8792f385223f 36 | pub unsafe fn add_pair(&mut self, key: *const c_char, value: *const c_char) { 37 | self.0 = avahi_string_list_add_pair(self.0, key, value); 38 | } 39 | 40 | /// Delegate function for [`avahi_string_list_find()`]. Returns a new `AvahiStringListNode`. 41 | /// 42 | /// # Safety 43 | /// This function is unsafe because it provides no guarantees about the given pointers that are 44 | /// dereferenced. 45 | /// 46 | /// [`avahi_string_list_find()`]: https://avahi.org/doxygen/html/strlst_8h.html#aafc54c009a2a1608b517c15a7cf29944 47 | pub unsafe fn find(&mut self, key: *const c_char) -> Option { 48 | let node = avahi_string_list_find(self.0, key); 49 | 50 | if !node.is_null() { 51 | Some(AvahiStringListNode::new(node)) 52 | } else { 53 | None 54 | } 55 | } 56 | 57 | /// Delegate function for [`avahi_string_list_length()`]. 58 | /// 59 | /// [`avahi_string_list_length()`]: https://avahi.org/doxygen/html/strlst_8h.html#a806c571b338e882390a180b1360c1456 60 | /// 61 | /// # Safety 62 | /// This function is unsafe because of the call to `avahi_string_list_length()`. 63 | pub unsafe fn length(&self) -> u32 { 64 | avahi_string_list_length(self.0) 65 | } 66 | 67 | /// Delegate function for [`avahi_string_list_to_string()`]. 68 | /// 69 | /// [`avahi_string_list_to_string()`]: https://avahi.org/doxygen/html/strlst_8h.html#a5c4b9ab709f22f7741c165ca3756a78b 70 | /// 71 | /// # Safety 72 | /// This function is unsafe because of the call to `avahi_string_list_to_string()`. 73 | pub unsafe fn to_string(&self) -> AvahiString { 74 | avahi_string_list_to_string(self.0).into() 75 | } 76 | 77 | /// Returns the first node in the list. 78 | pub fn head(&mut self) -> AvahiStringListNode { 79 | AvahiStringListNode::new(self.0) 80 | } 81 | 82 | /// Creates a copy of this `AvahiStringList`. 83 | /// 84 | /// # Safety 85 | /// This function is unsafe because of the call to `avahi_string_list_copy()`. 86 | pub unsafe fn clone(&self) -> Self { 87 | Self::clone_raw(self.0) 88 | } 89 | 90 | pub(super) unsafe fn clone_raw(raw: *mut AvahiStringList) -> Self { 91 | Self(avahi_string_list_copy(raw)) 92 | } 93 | 94 | pub(super) fn inner(&self) -> *mut AvahiStringList { 95 | self.0 96 | } 97 | } 98 | 99 | impl PartialEq for ManagedAvahiStringList { 100 | fn eq(&self, other: &Self) -> bool { 101 | unsafe { avahi_string_list_equal(self.0, other.0) == 1 } 102 | } 103 | } 104 | 105 | impl Eq for ManagedAvahiStringList {} 106 | 107 | impl Drop for ManagedAvahiStringList { 108 | fn drop(&mut self) { 109 | unsafe { avahi_string_list_free(self.0) }; 110 | } 111 | } 112 | 113 | unsafe impl Send for ManagedAvahiStringList {} 114 | unsafe impl Sync for ManagedAvahiStringList {} 115 | 116 | /// Represents a node or sub-list in an `AvahiStringList`. This struct is similar to it's parent, 117 | /// but it does not free the `AvahiStringList` once dropped and is bound to the lifetime of it's 118 | /// parent. 119 | #[derive(new, Getters)] 120 | pub struct AvahiStringListNode<'a> { 121 | list: *mut AvahiStringList, 122 | #[getter(skip)] 123 | phantom: PhantomData<&'a AvahiStringList>, 124 | } 125 | 126 | impl<'a> AvahiStringListNode<'a> { 127 | /// Returns the next node in the list, or `None` if last node. 128 | /// 129 | /// # Safety 130 | /// This function is unsafe because of the call to `avahi_string_list_get_next()`. 131 | pub unsafe fn next(self) -> Option> { 132 | let next = avahi_string_list_get_next(self.list); 133 | 134 | if next.is_null() { 135 | None 136 | } else { 137 | Some(AvahiStringListNode::new(next)) 138 | } 139 | } 140 | 141 | /// Returns the `AvahiPair` for this list. 142 | /// 143 | /// # Safety 144 | /// This function is unsafe because of the call to `avahi_string_list_get_pair()`. 145 | pub unsafe fn get_pair(&mut self) -> AvahiPair { 146 | let mut key: *mut c_char = ptr::null_mut(); 147 | let mut value: *mut c_char = ptr::null_mut(); 148 | let mut value_size: usize = 0; 149 | 150 | avahi_string_list_get_pair(self.list, &mut key, &mut value, &mut value_size); 151 | 152 | AvahiPair::new(key.into(), value.into(), value_size) 153 | } 154 | } 155 | 156 | /// Represents a key-value pair in an `AvahiStringList`. 157 | #[derive(new, Getters)] 158 | pub struct AvahiPair { 159 | key: AvahiString, 160 | value: AvahiString, 161 | value_size: usize, 162 | } 163 | 164 | /// Represents a string value returned by `AvahiStringList`. The underlying `*mut c_char` is freed 165 | /// using the appropriate Avahi function. 166 | #[derive(new)] 167 | pub struct AvahiString(*mut c_char); 168 | 169 | impl AvahiString { 170 | /// Returns this `AvahiStr` as a `&str` or `None` if null. 171 | /// 172 | /// # Safety 173 | /// This function is unsafe because of the call to `c_str::raw_to_str()`. 174 | pub unsafe fn as_str(&self) -> Option<&str> { 175 | if self.0.is_null() { 176 | None 177 | } else { 178 | Some(c_str::raw_to_str(self.0)) 179 | } 180 | } 181 | } 182 | 183 | impl From<*mut c_char> for AvahiString { 184 | fn from(s: *mut c_char) -> Self { 185 | Self::new(s) 186 | } 187 | } 188 | 189 | impl Drop for AvahiString { 190 | fn drop(&mut self) { 191 | if !self.0.is_null() { 192 | unsafe { avahi_free(self.0 as *mut c_void) } 193 | } 194 | } 195 | } 196 | 197 | #[cfg(test)] 198 | mod tests { 199 | use super::*; 200 | use std::collections::HashMap; 201 | 202 | #[test] 203 | fn add_get_pair_success() { 204 | crate::tests::setup(); 205 | 206 | let mut list = unsafe { ManagedAvahiStringList::new() }; 207 | let key1 = c_string!("foo"); 208 | let value1 = c_string!("bar"); 209 | 210 | unsafe { 211 | list.add_pair( 212 | key1.as_ptr() as *const c_char, 213 | value1.as_ptr() as *const c_char, 214 | ); 215 | 216 | let key2 = c_string!("hello"); 217 | let value2 = c_string!("world"); 218 | 219 | list.add_pair( 220 | key2.as_ptr() as *const c_char, 221 | value2.as_ptr() as *const c_char, 222 | ); 223 | 224 | let pair1 = list 225 | .find(key1.as_ptr() as *const c_char) 226 | .unwrap() 227 | .get_pair(); 228 | 229 | let pair2 = list 230 | .find(key2.as_ptr() as *const c_char) 231 | .unwrap() 232 | .get_pair(); 233 | 234 | assert_eq!(pair1.key().as_str().unwrap(), "foo"); 235 | assert_eq!(pair1.value().as_str().unwrap(), "bar"); 236 | assert_eq!(pair2.key().as_str().unwrap(), "hello"); 237 | assert_eq!(pair2.value().as_str().unwrap(), "world"); 238 | } 239 | } 240 | 241 | #[test] 242 | fn add_pair_replaces_success() { 243 | crate::tests::setup(); 244 | 245 | let mut list = unsafe { ManagedAvahiStringList::new() }; 246 | let key = c_string!("foo"); 247 | let value = c_string!("bar"); 248 | 249 | unsafe { 250 | list.add_pair( 251 | key.as_ptr() as *const c_char, 252 | value.as_ptr() as *const c_char, 253 | ); 254 | 255 | let pair = list.find(key.as_ptr() as *const c_char).unwrap().get_pair(); 256 | 257 | assert_eq!(pair.value().as_str().unwrap(), "bar"); 258 | 259 | let value = c_string!("baz"); 260 | 261 | list.add_pair( 262 | key.as_ptr() as *const c_char, 263 | value.as_ptr() as *const c_char, 264 | ); 265 | 266 | let pair = list.find(key.as_ptr() as *const c_char).unwrap().get_pair(); 267 | 268 | assert_eq!(pair.value().as_str().unwrap(), "baz"); 269 | } 270 | } 271 | 272 | #[test] 273 | fn length_success() { 274 | crate::tests::setup(); 275 | 276 | let mut list = unsafe { ManagedAvahiStringList::new() }; 277 | let key = c_string!("foo"); 278 | let value = c_string!("bar"); 279 | 280 | unsafe { 281 | list.add_pair( 282 | key.as_ptr() as *const c_char, 283 | value.as_ptr() as *const c_char, 284 | ); 285 | 286 | assert_eq!(list.length(), 1); 287 | } 288 | } 289 | 290 | #[test] 291 | fn to_string_success() { 292 | crate::tests::setup(); 293 | 294 | let mut list = unsafe { ManagedAvahiStringList::new() }; 295 | let key = c_string!("foo"); 296 | let value = c_string!("bar"); 297 | 298 | unsafe { 299 | list.add_pair( 300 | key.as_ptr() as *const c_char, 301 | value.as_ptr() as *const c_char, 302 | ); 303 | 304 | assert_eq!(list.to_string().as_str().unwrap(), "\"foo=bar\""); 305 | } 306 | } 307 | 308 | #[test] 309 | fn equals_success() { 310 | crate::tests::setup(); 311 | 312 | let mut list = unsafe { ManagedAvahiStringList::new() }; 313 | let key = c_string!("foo"); 314 | let value = c_string!("bar"); 315 | 316 | unsafe { 317 | list.add_pair( 318 | key.as_ptr() as *const c_char, 319 | value.as_ptr() as *const c_char, 320 | ); 321 | 322 | assert_eq!(list.clone(), list); 323 | } 324 | } 325 | 326 | #[test] 327 | fn iterate_success() { 328 | crate::tests::setup(); 329 | 330 | let mut list = unsafe { ManagedAvahiStringList::new() }; 331 | let key1 = c_string!("foo"); 332 | let value1 = c_string!("bar"); 333 | let key2 = c_string!("hello"); 334 | let value2 = c_string!("world"); 335 | 336 | unsafe { 337 | list.add_pair( 338 | key1.as_ptr() as *const c_char, 339 | value1.as_ptr() as *const c_char, 340 | ); 341 | 342 | list.add_pair( 343 | key2.as_ptr() as *const c_char, 344 | value2.as_ptr() as *const c_char, 345 | ); 346 | } 347 | 348 | let mut node = Some(list.head()); 349 | let mut map = HashMap::new(); 350 | 351 | while node.is_some() { 352 | let mut n = node.unwrap(); 353 | let pair = unsafe { n.get_pair() }; 354 | 355 | map.insert( 356 | unsafe { pair.key().as_str() }.unwrap().to_string(), 357 | unsafe { pair.value().as_str() }.unwrap().to_string(), 358 | ); 359 | 360 | node = unsafe { n.next() }; 361 | } 362 | 363 | let expected: HashMap = hashmap! { 364 | "foo".to_string() => "bar".to_string(), 365 | "hello".to_string() => "world".to_string() 366 | }; 367 | 368 | assert_eq!(map, expected); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /zeroconf/src/avahi/txt_record.rs: -------------------------------------------------------------------------------- 1 | //! Avahi implementation for cross-platform TXT record. 2 | 3 | use super::string_list::{AvahiStringListNode, ManagedAvahiStringList}; 4 | use crate::txt_record::TTxtRecord; 5 | use crate::Result; 6 | use libc::c_char; 7 | use std::cell::UnsafeCell; 8 | 9 | pub struct AvahiTxtRecord(UnsafeCell); 10 | 11 | impl TTxtRecord for AvahiTxtRecord { 12 | fn new() -> Self { 13 | Self(UnsafeCell::new(unsafe { ManagedAvahiStringList::new() })) 14 | } 15 | 16 | fn insert(&mut self, key: &str, value: &str) -> Result<()> { 17 | let c_key = c_string!(key); 18 | let c_value = c_string!(value); 19 | 20 | unsafe { 21 | self.inner_mut().add_pair( 22 | c_key.as_ptr() as *const c_char, 23 | c_value.as_ptr() as *const c_char, 24 | ); 25 | } 26 | Ok(()) 27 | } 28 | 29 | fn get(&self, key: &str) -> Option { 30 | let c_str = c_string!(key); 31 | unsafe { 32 | self.inner_mut() 33 | .find(c_str.as_ptr() as *const c_char)? 34 | .get_pair() 35 | .value() 36 | .as_str() 37 | .map(|s| s.to_string()) 38 | } 39 | } 40 | 41 | fn remove(&mut self, key: &str) -> Option { 42 | let mut list = unsafe { ManagedAvahiStringList::new() }; 43 | let mut map = self.to_map(); 44 | let prev = map.remove(key); 45 | 46 | for (key, value) in map { 47 | let c_key = c_string!(key); 48 | let c_value = c_string!(value); 49 | 50 | unsafe { 51 | list.add_pair( 52 | c_key.as_ptr() as *const c_char, 53 | c_value.as_ptr() as *const c_char, 54 | ); 55 | } 56 | } 57 | 58 | self.0 = UnsafeCell::new(list); 59 | 60 | prev 61 | } 62 | 63 | fn contains_key(&self, key: &str) -> bool { 64 | let c_str = c_string!(key); 65 | unsafe { 66 | self.inner_mut() 67 | .find(c_str.as_ptr() as *const c_char) 68 | .is_some() 69 | } 70 | } 71 | 72 | fn len(&self) -> usize { 73 | unsafe { self.inner().length() as usize } 74 | } 75 | 76 | fn iter<'a>(&'a self) -> Box + 'a> { 77 | Box::new(Iter::new(self.inner_mut().head())) 78 | } 79 | 80 | fn keys<'a>(&'a self) -> Box + 'a> { 81 | Box::new(Keys(Iter::new(self.inner_mut().head()))) 82 | } 83 | 84 | fn values<'a>(&'a self) -> Box + 'a> { 85 | Box::new(Values(Iter::new(self.inner_mut().head()))) 86 | } 87 | } 88 | 89 | impl AvahiTxtRecord { 90 | #[allow(clippy::mut_from_ref)] 91 | fn inner_mut(&self) -> &mut ManagedAvahiStringList { 92 | unsafe { &mut *self.0.get() } 93 | } 94 | 95 | pub(crate) fn inner(&self) -> &ManagedAvahiStringList { 96 | unsafe { &*self.0.get() } 97 | } 98 | } 99 | 100 | impl From for AvahiTxtRecord { 101 | fn from(list: ManagedAvahiStringList) -> Self { 102 | Self(UnsafeCell::new(list)) 103 | } 104 | } 105 | 106 | impl Clone for AvahiTxtRecord { 107 | fn clone(&self) -> Self { 108 | Self::from(unsafe { self.inner().clone() }) 109 | } 110 | } 111 | 112 | impl PartialEq for AvahiTxtRecord { 113 | fn eq(&self, other: &Self) -> bool { 114 | self.inner() == other.inner() 115 | } 116 | } 117 | 118 | pub struct Iter<'a> { 119 | node: Option>, 120 | } 121 | 122 | impl<'a> Iter<'a> { 123 | pub fn new(node: AvahiStringListNode<'a>) -> Self { 124 | Self { node: Some(node) } 125 | } 126 | } 127 | 128 | impl Iterator for Iter<'_> { 129 | type Item = (String, String); 130 | 131 | fn next(&mut self) -> Option { 132 | let mut n = self.node.take()?; 133 | 134 | if n.list().is_null() { 135 | return None; 136 | } 137 | 138 | let pair = unsafe { n.get_pair() }; 139 | self.node = unsafe { n.next() }; 140 | 141 | if let Some(key) = unsafe { pair.key().as_str() } { 142 | let key = key.to_string(); 143 | unsafe { pair.value().as_str() }.map(|value| (key, value.to_string())) 144 | } else { 145 | None 146 | } 147 | } 148 | } 149 | 150 | pub struct Keys<'a>(Iter<'a>); 151 | 152 | impl Iterator for Keys<'_> { 153 | type Item = String; 154 | 155 | fn next(&mut self) -> Option { 156 | self.0.next().map(|e| e.0) 157 | } 158 | } 159 | 160 | pub struct Values<'a>(Iter<'a>); 161 | 162 | impl Iterator for Values<'_> { 163 | type Item = String; 164 | 165 | fn next(&mut self) -> Option { 166 | self.0.next().map(|e| e.1) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/bonjour_util.rs: -------------------------------------------------------------------------------- 1 | //! Utilities related to Bonjour 2 | 3 | use std::{ffi::CString, str::FromStr}; 4 | 5 | use super::constants; 6 | use crate::{check_valid_characters, lstrip_underscore, NetworkInterface, Result, ServiceType}; 7 | use bonjour_sys::DNSServiceErrorType; 8 | 9 | /// Normalizes the specified domain `&str` to conform to a standard enforced by this crate. 10 | /// 11 | /// Bonjour suffixes domains with a final `'.'` character in some contexts but is not required by 12 | /// the standard. This function removes the final dot if present. 13 | pub fn normalize_domain(domain: &str) -> String { 14 | let end = domain 15 | .chars() 16 | .nth(domain.len() - 1) 17 | .expect("could not index domain string"); 18 | 19 | if end == '.' { 20 | String::from(&domain[..domain.len() - 1]) 21 | } else { 22 | String::from(domain) 23 | } 24 | } 25 | 26 | /// Converts the specified [`NetworkInterface`] to the Bonjour expected value. 27 | /// 28 | /// [`NetworkInterface`]: ../../enum.NetworkInterface.html 29 | pub fn interface_index(interface: NetworkInterface) -> u32 { 30 | match interface { 31 | NetworkInterface::Unspec => constants::BONJOUR_IF_UNSPEC, 32 | NetworkInterface::AtIndex(i) => i, 33 | } 34 | } 35 | 36 | /// Converts the specified Bonjour interface index to a [`NetworkInterface`]. 37 | pub fn interface_from_index(index: u32) -> NetworkInterface { 38 | match index { 39 | constants::BONJOUR_IF_UNSPEC => NetworkInterface::Unspec, 40 | _ => NetworkInterface::AtIndex(index), 41 | } 42 | } 43 | 44 | /// Executes the specified closure and returns a formatted `Result` 45 | pub fn sys_exec DNSServiceErrorType>(func: F, message: &str) -> Result<()> { 46 | let err = func(); 47 | 48 | if err < 0 { 49 | Err(format!("{} (code: {})", message, err).into()) 50 | } else { 51 | Ok(()) 52 | } 53 | } 54 | 55 | /// Formats the specified `ServiceType` as a `CString` for use with Bonjour 56 | pub fn format_regtype(service_type: &ServiceType) -> CString { 57 | let mut regtype = vec![format!( 58 | "_{}._{}", 59 | service_type.name(), 60 | service_type.protocol() 61 | )]; 62 | 63 | regtype.extend( 64 | service_type 65 | .sub_types() 66 | .iter() 67 | .map(|sub_type| format!("_{sub_type}")), 68 | ); 69 | 70 | c_string!(regtype.join(",")) 71 | } 72 | 73 | /// Parses the specified `&str` into a `ServiceType` 74 | pub fn parse_regtype(regtype: &str) -> Result { 75 | let types = regtype.split(',').collect::>(); 76 | let service_type = ServiceType::from_str(types[0])?; 77 | 78 | let sub_types = types[1..] 79 | .iter() 80 | .map(|s| check_valid_characters(lstrip_underscore(s))) 81 | .collect::>>()?; 82 | 83 | ServiceType::with_sub_types(service_type.name(), service_type.protocol(), sub_types) 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | use crate::ServiceType; 90 | 91 | #[test] 92 | fn parse_regtype_success() { 93 | assert_eq!( 94 | parse_regtype("_http._tcp,_printer1,_printer2").unwrap(), 95 | ServiceType::with_sub_types("http", "tcp", vec!["printer1", "printer2"]).unwrap() 96 | ); 97 | } 98 | 99 | #[test] 100 | fn parse_regtype_success_no_subtypes() { 101 | assert_eq!( 102 | parse_regtype("_http._tcp").unwrap(), 103 | ServiceType::new("http", "tcp").unwrap() 104 | ); 105 | } 106 | 107 | #[test] 108 | fn parse_regtype_failure_invalid_regtype() { 109 | assert_eq!( 110 | parse_regtype("foobar"), 111 | Err("invalid name and protocol".into()) 112 | ); 113 | } 114 | 115 | #[test] 116 | fn format_regtype_success() { 117 | assert_eq!( 118 | format_regtype( 119 | &ServiceType::with_sub_types("http", "tcp", vec!["printer1", "printer2"]).unwrap() 120 | ), 121 | c_string!("_http._tcp,_printer1,_printer2") 122 | ); 123 | } 124 | 125 | #[test] 126 | fn format_regtype_success_no_subtypes() { 127 | assert_eq!( 128 | format_regtype(&ServiceType::new("http", "tcp").unwrap()), 129 | c_string!("_http._tcp") 130 | ); 131 | } 132 | 133 | #[test] 134 | fn sys_exec_returns_error() { 135 | assert_eq!( 136 | sys_exec(|| -42, "uh oh spaghetti-o"), 137 | Err("uh oh spaghetti-o (code: -42)".into()) 138 | ); 139 | } 140 | 141 | #[test] 142 | fn sys_exec_returns_ok() { 143 | assert_eq!(sys_exec(|| 0, "success"), Ok(())); 144 | } 145 | 146 | #[test] 147 | fn network_interface_unspec_maps_to_bonjour_if_unspec() { 148 | assert_eq!(interface_index(NetworkInterface::Unspec), 0); 149 | } 150 | 151 | #[test] 152 | fn network_interface_at_index_maps_to_index() { 153 | assert_eq!(interface_index(NetworkInterface::AtIndex(42)), 42); 154 | } 155 | 156 | #[test] 157 | fn normalize_domain_removes_trailing_dot() { 158 | assert_eq!( 159 | normalize_domain("foo.bar.baz."), 160 | String::from("foo.bar.baz") 161 | ); 162 | } 163 | 164 | #[test] 165 | fn normalize_domain_does_not_remove_trailing_dot_if_not_present() { 166 | assert_eq!(normalize_domain("foo.bar.baz"), String::from("foo.bar.baz")); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/browser.rs: -------------------------------------------------------------------------------- 1 | //! Bonjour implementation for cross-platform browser 2 | 3 | use super::service_ref::{ 4 | BrowseServicesParams, GetAddressInfoParams, ManagedDNSServiceRef, ServiceResolveParams, 5 | }; 6 | use super::txt_record_ref::ManagedTXTRecordRef; 7 | use super::{bonjour_util, constants}; 8 | use crate::ffi::{c_str, AsRaw, FromRaw}; 9 | use crate::prelude::*; 10 | use crate::{BrowserEvent, ServiceBrowserCallback, ServiceDiscovery, ServiceRemoval}; 11 | use crate::{EventLoop, NetworkInterface, Result, ServiceType, TxtRecord}; 12 | #[cfg(target_vendor = "pc")] 13 | use bonjour_sys::sockaddr_in; 14 | use bonjour_sys::{DNSServiceErrorType, DNSServiceFlags, DNSServiceRef}; 15 | #[cfg(target_vendor = "apple")] 16 | use libc::sockaddr_in; 17 | use libc::{c_char, c_uchar, c_void}; 18 | use std::any::Any; 19 | use std::ffi::CString; 20 | use std::fmt::{self, Formatter}; 21 | use std::net::IpAddr; 22 | use std::ptr; 23 | use std::sync::{Arc, Mutex}; 24 | 25 | #[derive(Debug)] 26 | pub struct BonjourMdnsBrowser { 27 | service: Arc>, 28 | kind: CString, 29 | interface_index: u32, 30 | context: Box, 31 | } 32 | 33 | impl TMdnsBrowser for BonjourMdnsBrowser { 34 | fn new(service_type: ServiceType) -> Self { 35 | Self { 36 | service: Arc::default(), 37 | kind: bonjour_util::format_regtype(&service_type), 38 | interface_index: constants::BONJOUR_IF_UNSPEC, 39 | context: Box::default(), 40 | } 41 | } 42 | 43 | fn set_network_interface(&mut self, interface: NetworkInterface) { 44 | self.interface_index = bonjour_util::interface_index(interface); 45 | } 46 | 47 | fn network_interface(&self) -> NetworkInterface { 48 | bonjour_util::interface_from_index(self.interface_index) 49 | } 50 | 51 | fn set_service_callback(&mut self, service_discovered_callback: Box) { 52 | self.context.service_discovered_callback = Some(service_discovered_callback); 53 | } 54 | 55 | fn set_context(&mut self, context: Box) { 56 | self.context.user_context = Some(Arc::from(context)); 57 | } 58 | 59 | fn context(&self) -> Option<&dyn Any> { 60 | self.context.user_context.as_ref().map(|c| c.as_ref()) 61 | } 62 | 63 | fn browse_services(&mut self) -> Result { 64 | debug!("Browsing services: {:?}", self); 65 | 66 | let mut service_lock = self 67 | .service 68 | .lock() 69 | .expect("should have been able to obtain lock on service ref"); 70 | 71 | let browse_params = BrowseServicesParams::builder() 72 | .flags(0) 73 | .interface_index(self.interface_index) 74 | .regtype(self.kind.as_ptr()) 75 | .domain(ptr::null_mut()) 76 | .callback(Some(browse_callback)) 77 | .context(self.context.as_raw()) 78 | .build()?; 79 | 80 | unsafe { service_lock.browse_services(browse_params)? }; 81 | 82 | Ok(EventLoop::new(self.service.clone())) 83 | } 84 | } 85 | 86 | #[derive(Default, FromRaw, AsRaw)] 87 | struct BonjourBrowserContext { 88 | service_discovered_callback: Option>, 89 | resolved_name: Option, 90 | resolved_kind: Option, 91 | resolved_domain: Option, 92 | resolved_port: u16, 93 | resolved_txt: Option, 94 | user_context: Option>, 95 | } 96 | 97 | impl BonjourBrowserContext { 98 | fn invoke_callback(&self, result: Result) { 99 | if let Some(f) = &self.service_discovered_callback { 100 | f(result, self.user_context.clone()); 101 | } else { 102 | warn!("attempted to invoke callback but none was set"); 103 | } 104 | } 105 | } 106 | 107 | impl fmt::Debug for BonjourBrowserContext { 108 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 109 | f.debug_struct("BonjourResolverContext") 110 | .field("resolved_name", &self.resolved_name) 111 | .field("resolved_kind", &self.resolved_kind) 112 | .field("resolved_domain", &self.resolved_domain) 113 | .field("resolved_port", &self.resolved_port) 114 | .finish() 115 | } 116 | } 117 | 118 | unsafe extern "system" fn browse_callback( 119 | _sd_ref: DNSServiceRef, 120 | flags: DNSServiceFlags, 121 | interface_index: u32, 122 | error: DNSServiceErrorType, 123 | name: *const c_char, 124 | regtype: *const c_char, 125 | domain: *const c_char, 126 | context: *mut c_void, 127 | ) { 128 | let ctx = BonjourBrowserContext::from_raw(context); 129 | 130 | if error != 0 { 131 | ctx.invoke_callback(Err(format!( 132 | "browse_callback() reported error (code: {})", 133 | error 134 | ) 135 | .into())); 136 | return; 137 | } 138 | 139 | if flags & bonjour_sys::kDNSServiceFlagsAdd != 0 { 140 | if let Err(e) = handle_browse_add(ctx, name, regtype, domain, interface_index) { 141 | ctx.invoke_callback(Err(e)); 142 | } 143 | } else { 144 | handle_browse_remove(ctx, name, regtype, domain); 145 | } 146 | } 147 | 148 | unsafe fn handle_browse_add( 149 | ctx: &mut BonjourBrowserContext, 150 | name: *const c_char, 151 | regtype: *const c_char, 152 | domain: *const c_char, 153 | interface_index: u32, 154 | ) -> Result<()> { 155 | ctx.resolved_name = Some(c_str::copy_raw(name)); 156 | ctx.resolved_kind = Some(c_str::copy_raw(regtype)); 157 | ctx.resolved_domain = Some(c_str::copy_raw(domain)); 158 | 159 | ManagedDNSServiceRef::default().resolve_service( 160 | ServiceResolveParams::builder() 161 | .flags(bonjour_sys::kDNSServiceFlagsForceMulticast) 162 | .interface_index(interface_index) 163 | .name(name) 164 | .regtype(regtype) 165 | .domain(domain) 166 | .callback(Some(resolve_callback)) 167 | .context(ctx.as_raw()) 168 | .build()?, 169 | ) 170 | } 171 | 172 | unsafe fn handle_browse_remove( 173 | ctx: &mut BonjourBrowserContext, 174 | name: *const c_char, 175 | regtype: *const c_char, 176 | domain: *const c_char, 177 | ) { 178 | let name = c_str::raw_to_str(name); 179 | let regtype = c_str::raw_to_str(regtype); 180 | let domain = c_str::raw_to_str(domain); 181 | 182 | // Remove the "." suffix to be consistent with the Avahi implementation. 183 | let regtype = regtype.strip_suffix(".").unwrap_or(domain); 184 | let domain = domain.strip_suffix(".").unwrap_or(domain); 185 | 186 | ctx.invoke_callback(Ok(BrowserEvent::Remove( 187 | ServiceRemoval::builder() 188 | .name(name.to_string()) 189 | .kind(regtype.to_string()) 190 | .domain(domain.to_string()) 191 | .build() 192 | .expect("could not build ServiceRemoval"), 193 | ))); 194 | } 195 | 196 | unsafe extern "system" fn resolve_callback( 197 | _sd_ref: DNSServiceRef, 198 | _flags: DNSServiceFlags, 199 | interface_index: u32, 200 | error: DNSServiceErrorType, 201 | _fullname: *const c_char, 202 | host_target: *const c_char, 203 | port: u16, 204 | txt_len: u16, 205 | txt_record: *const c_uchar, 206 | context: *mut c_void, 207 | ) { 208 | let ctx = BonjourBrowserContext::from_raw(context); 209 | 210 | let result = handle_resolve( 211 | ctx, 212 | error, 213 | port, 214 | interface_index, 215 | host_target, 216 | txt_len, 217 | txt_record, 218 | ); 219 | 220 | if let Err(e) = result { 221 | ctx.invoke_callback(Err(e)); 222 | } 223 | } 224 | 225 | unsafe fn handle_resolve( 226 | ctx: &mut BonjourBrowserContext, 227 | error: DNSServiceErrorType, 228 | port: u16, 229 | interface_index: u32, 230 | host_target: *const c_char, 231 | txt_len: u16, 232 | txt_record: *const c_uchar, 233 | ) -> Result<()> { 234 | if error != 0 { 235 | return Err(format!("error reported by resolve_callback: (code: {})", error).into()); 236 | } 237 | 238 | ctx.resolved_port = port; 239 | 240 | ctx.resolved_txt = if txt_len > 1 { 241 | Some(TxtRecord::from(ManagedTXTRecordRef::clone_raw( 242 | txt_record, txt_len, 243 | )?)) 244 | } else { 245 | None 246 | }; 247 | 248 | ManagedDNSServiceRef::default().get_address_info( 249 | GetAddressInfoParams::builder() 250 | .flags(bonjour_sys::kDNSServiceFlagsForceMulticast) 251 | .interface_index(interface_index) 252 | .protocol(0) 253 | .hostname(host_target) 254 | .callback(Some(get_address_info_callback)) 255 | .context(ctx.as_raw()) 256 | .build()?, 257 | ) 258 | } 259 | 260 | unsafe extern "system" fn get_address_info_callback( 261 | _sd_ref: DNSServiceRef, 262 | _flags: DNSServiceFlags, 263 | _interface_index: u32, 264 | error: DNSServiceErrorType, 265 | hostname: *const c_char, 266 | address: *const bonjour_sys::sockaddr, 267 | _ttl: u32, 268 | context: *mut c_void, 269 | ) { 270 | let ctx = BonjourBrowserContext::from_raw(context); 271 | if let Err(e) = handle_get_address_info(ctx, error, address, hostname) { 272 | ctx.invoke_callback(Err(e)); 273 | } 274 | } 275 | 276 | unsafe fn handle_get_address_info( 277 | ctx: &mut BonjourBrowserContext, 278 | error: DNSServiceErrorType, 279 | address: *const bonjour_sys::sockaddr, 280 | hostname: *const c_char, 281 | ) -> Result<()> { 282 | // this callback runs multiple times for some reason 283 | if ctx.resolved_name.is_none() { 284 | return Ok(()); 285 | } 286 | 287 | if error != 0 { 288 | return Err(format!( 289 | "get_address_info_callback() reported error (code: {})", 290 | error 291 | ) 292 | .into()); 293 | } 294 | 295 | // on macOS the bytes are swapped for the port 296 | let port: u16 = ctx.resolved_port.to_be(); 297 | 298 | // on macOS the bytes are swapped for the ip 299 | #[cfg(target_vendor = "apple")] 300 | let ip = { 301 | let address = address as *const sockaddr_in; 302 | assert_not_null!(address); 303 | let s_addr = (*address).sin_addr.s_addr.to_le_bytes(); 304 | IpAddr::from(s_addr).to_string() 305 | }; 306 | 307 | #[cfg(target_vendor = "pc")] 308 | let ip = { 309 | let address = address as *const sockaddr_in; 310 | assert_not_null!(address); 311 | let s_un = (*address).sin_addr.S_un.S_un_b; 312 | let s_addr = [s_un.s_b1, s_un.s_b2, s_un.s_b3, s_un.s_b4]; 313 | IpAddr::from(s_addr).to_string() 314 | }; 315 | 316 | let hostname = c_str::copy_raw(hostname); 317 | 318 | let domain = bonjour_util::normalize_domain( 319 | &ctx.resolved_domain 320 | .take() 321 | .ok_or("could not get domain from BonjourBrowserContext")?, 322 | ); 323 | 324 | let kind = bonjour_util::normalize_domain( 325 | &ctx.resolved_kind 326 | .take() 327 | .ok_or("could not get kind from BonjourBrowserContext")?, 328 | ); 329 | 330 | let name = ctx 331 | .resolved_name 332 | .take() 333 | .ok_or("could not get name from BonjourBrowserContext")?; 334 | 335 | let result = ServiceDiscovery::builder() 336 | .name(name) 337 | .service_type(bonjour_util::parse_regtype(&kind)?) 338 | .domain(domain) 339 | .host_name(hostname) 340 | .address(ip) 341 | .port(port) 342 | .txt(ctx.resolved_txt.take()) 343 | .build() 344 | .expect("could not build ServiceResolution"); 345 | 346 | ctx.invoke_callback(Ok(BrowserEvent::Add(result))); 347 | 348 | Ok(()) 349 | } 350 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/constants.rs: -------------------------------------------------------------------------------- 1 | use bonjour_sys::DNSServiceFlags; 2 | 3 | pub const BONJOUR_IF_UNSPEC: u32 = 0; 4 | pub const BONJOUR_RENAME_FLAGS: DNSServiceFlags = 0; 5 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/event_loop.rs: -------------------------------------------------------------------------------- 1 | //! Event loop for running a `MdnsService` or `MdnsBrowser`. 2 | 3 | use super::service_ref::ManagedDNSServiceRef; 4 | use crate::event_loop::TEventLoop; 5 | use crate::{ffi, Result}; 6 | use std::sync::{Arc, Mutex}; 7 | use std::time::Duration; 8 | 9 | #[derive(new)] 10 | pub struct BonjourEventLoop { 11 | service: Arc>, 12 | } 13 | 14 | impl TEventLoop for BonjourEventLoop { 15 | /// Polls for new events. 16 | /// 17 | /// Prior to calling `ManagedDNSServiceRef::process_result()`, this function performs a unix 18 | /// `select()` on the underlying socket with the specified timeout. If the socket contains no 19 | /// new data, the blocking call is not made. 20 | fn poll(&self, timeout: Duration) -> Result<()> { 21 | let service = self 22 | .service 23 | .lock() 24 | .expect("should have been able to obtain lock on service ref"); 25 | 26 | let select = unsafe { ffi::bonjour::read_select(service.sock_fd(), timeout)? }; 27 | 28 | if select > 0 { 29 | unsafe { service.process_result() } 30 | } else { 31 | Ok(()) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/mod.rs: -------------------------------------------------------------------------------- 1 | //! macOS-specific ZeroConf bindings 2 | //! 3 | //! This module wraps the [Bonjour] mDNS implementation which is distributed with macOS. 4 | //! 5 | //! [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) 6 | 7 | pub(crate) mod constants; 8 | 9 | pub mod bonjour_util; 10 | pub mod browser; 11 | pub mod event_loop; 12 | pub mod service; 13 | pub mod service_ref; 14 | pub mod txt_record; 15 | pub mod txt_record_ref; 16 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/service.rs: -------------------------------------------------------------------------------- 1 | //! Bonjour implementation for cross-platform service. 2 | 3 | use super::service_ref::{ManagedDNSServiceRef, RegisterServiceParams}; 4 | use super::{bonjour_util, constants}; 5 | use crate::ffi::c_str::{self, AsCChars}; 6 | use crate::ffi::{AsRaw, FromRaw, UnwrapOrNull}; 7 | use crate::prelude::*; 8 | use crate::{ 9 | EventLoop, NetworkInterface, Result, ServiceRegisteredCallback, ServiceRegistration, 10 | ServiceType, TxtRecord, 11 | }; 12 | use bonjour_sys::{DNSServiceErrorType, DNSServiceFlags, DNSServiceRef}; 13 | use libc::{c_char, c_void}; 14 | use std::any::Any; 15 | use std::ffi::CString; 16 | use std::sync::{Arc, Mutex}; 17 | 18 | #[derive(Debug)] 19 | pub struct BonjourMdnsService { 20 | service: Arc>, 21 | kind: CString, 22 | port: u16, 23 | name: Option, 24 | domain: Option, 25 | host: Option, 26 | interface_index: u32, 27 | txt_record: Option, 28 | context: Box, 29 | } 30 | 31 | impl TMdnsService for BonjourMdnsService { 32 | fn new(service_type: ServiceType, port: u16) -> Self { 33 | Self { 34 | service: Arc::default(), 35 | kind: bonjour_util::format_regtype(&service_type), 36 | port, 37 | name: None, 38 | domain: None, 39 | host: None, 40 | interface_index: constants::BONJOUR_IF_UNSPEC, 41 | txt_record: None, 42 | context: Box::default(), 43 | } 44 | } 45 | 46 | /// Sets the name to register this service under. If no name is set, Bonjour will 47 | /// automatically assign one (usually to the name of the machine). 48 | fn set_name(&mut self, name: &str) { 49 | self.name = Some(c_string!(name)); 50 | } 51 | 52 | fn name(&self) -> Option<&str> { 53 | self.name.as_ref().map(c_str::to_str) 54 | } 55 | 56 | fn set_network_interface(&mut self, interface: NetworkInterface) { 57 | self.interface_index = bonjour_util::interface_index(interface); 58 | } 59 | 60 | fn network_interface(&self) -> NetworkInterface { 61 | bonjour_util::interface_from_index(self.interface_index) 62 | } 63 | 64 | fn set_domain(&mut self, domain: &str) { 65 | self.domain = Some(c_string!(domain)); 66 | } 67 | 68 | fn domain(&self) -> Option<&str> { 69 | self.domain.as_ref().map(c_str::to_str) 70 | } 71 | 72 | fn set_host(&mut self, host: &str) { 73 | self.host = Some(c_string!(host)); 74 | } 75 | 76 | fn host(&self) -> Option<&str> { 77 | self.host.as_ref().map(c_str::to_str) 78 | } 79 | 80 | fn set_txt_record(&mut self, txt_record: TxtRecord) { 81 | self.txt_record = Some(txt_record); 82 | } 83 | 84 | fn txt_record(&self) -> Option<&TxtRecord> { 85 | self.txt_record.as_ref() 86 | } 87 | 88 | fn set_registered_callback(&mut self, registered_callback: Box) { 89 | self.context.registered_callback = Some(registered_callback); 90 | } 91 | 92 | fn set_context(&mut self, context: Box) { 93 | self.context.user_context = Some(Arc::from(context)); 94 | } 95 | 96 | fn context(&self) -> Option<&dyn Any> { 97 | self.context.user_context.as_ref().map(|c| c.as_ref()) 98 | } 99 | 100 | fn register(&mut self) -> Result { 101 | debug!("Registering service: {:?}", self); 102 | 103 | let txt_len = self 104 | .txt_record 105 | .as_ref() 106 | .map(|t| unsafe { t.inner().get_length() }) 107 | .unwrap_or(0); 108 | 109 | let txt_record = self 110 | .txt_record 111 | .as_ref() 112 | .map(|t| unsafe { t.inner().get_bytes_ptr() }) 113 | .unwrap_or_null(); 114 | 115 | let mut service_lock = self 116 | .service 117 | .lock() 118 | .expect("should be able to obtain lock on service"); 119 | 120 | let register_params = RegisterServiceParams::builder() 121 | .flags(constants::BONJOUR_RENAME_FLAGS) 122 | .interface_index(self.interface_index) 123 | .name(self.name.as_ref().as_c_chars().unwrap_or_null()) 124 | .regtype(self.kind.as_ptr()) 125 | .domain(self.domain.as_ref().as_c_chars().unwrap_or_null()) 126 | .host(self.host.as_ref().as_c_chars().unwrap_or_null()) 127 | .port(self.port) 128 | .txt_len(txt_len) 129 | .txt_record(txt_record) 130 | .callback(Some(register_callback)) 131 | .context(self.context.as_raw()) 132 | .build()?; 133 | 134 | unsafe { service_lock.register_service(register_params)? }; 135 | 136 | Ok(EventLoop::new(self.service.clone())) 137 | } 138 | } 139 | 140 | #[derive(Default, FromRaw, AsRaw)] 141 | struct BonjourServiceContext { 142 | registered_callback: Option>, 143 | user_context: Option>, 144 | } 145 | 146 | // Necessary for BonjourMdnsService, cant be `derive`d because of registered_callback 147 | impl std::fmt::Debug for BonjourServiceContext { 148 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 149 | f.debug_struct("BonjourServiceContext") 150 | .field("user_context", &self.user_context) 151 | .finish() 152 | } 153 | } 154 | 155 | impl BonjourServiceContext { 156 | fn invoke_callback(&self, result: Result) { 157 | if let Some(f) = &self.registered_callback { 158 | f(result, self.user_context.clone()); 159 | } else { 160 | warn!("attempted to invoke callback but none was set"); 161 | } 162 | } 163 | } 164 | 165 | unsafe extern "system" fn register_callback( 166 | _sd_ref: DNSServiceRef, 167 | _flags: DNSServiceFlags, 168 | error: DNSServiceErrorType, 169 | name: *const c_char, 170 | regtype: *const c_char, 171 | domain: *const c_char, 172 | context: *mut c_void, 173 | ) { 174 | let context = BonjourServiceContext::from_raw(context); 175 | if let Err(e) = handle_register(context, error, domain, name, regtype) { 176 | context.invoke_callback(Err(e)); 177 | } 178 | } 179 | 180 | unsafe fn handle_register( 181 | context: &BonjourServiceContext, 182 | error: DNSServiceErrorType, 183 | domain: *const c_char, 184 | name: *const c_char, 185 | regtype: *const c_char, 186 | ) -> Result<()> { 187 | if error != 0 { 188 | return Err(format!("register_callback() reported error (code: {0})", error).into()); 189 | } 190 | 191 | let domain = bonjour_util::normalize_domain(c_str::raw_to_str(domain)); 192 | let kind = bonjour_util::normalize_domain(c_str::raw_to_str(regtype)); 193 | 194 | let result = ServiceRegistration::builder() 195 | .name(c_str::copy_raw(name)) 196 | .service_type(bonjour_util::parse_regtype(&kind)?) 197 | .domain(domain) 198 | .build() 199 | .expect("could not build ServiceRegistration"); 200 | 201 | context.invoke_callback(Ok(result)); 202 | 203 | Ok(()) 204 | } 205 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/service_ref.rs: -------------------------------------------------------------------------------- 1 | //! Low level interface for interacting with `DNSserviceRef` 2 | 3 | use crate::{bonjour::bonjour_util, Result}; 4 | use bonjour_sys::{ 5 | dnssd_sock_t, DNSServiceBrowse, DNSServiceBrowseReply, DNSServiceFlags, DNSServiceGetAddrInfo, 6 | DNSServiceGetAddrInfoReply, DNSServiceProcessResult, DNSServiceProtocol, DNSServiceRef, 7 | DNSServiceRefDeallocate, DNSServiceRefSockFD, DNSServiceRegister, DNSServiceRegisterReply, 8 | DNSServiceResolve, DNSServiceResolveReply, 9 | }; 10 | use libc::{c_char, c_void}; 11 | use std::ptr; 12 | 13 | /// Wraps the `DNSServiceRef` type from the raw Bonjour bindings. 14 | /// 15 | /// This struct allocates a new `DNSServiceRef` when any of the delegate functions is invoked and 16 | /// calls the Bonjour function responsible for freeing the client on `trait Drop`. 17 | /// 18 | /// # Note 19 | /// This wrapper is meant for one-off calls to underlying Bonjour functions. The behavior for 20 | /// using an already initialized `DNSServiceRef` in one of these functions is undefined. Therefore, 21 | /// it is preferable to only call one delegate function per-instance. 22 | #[derive(Debug)] 23 | pub struct ManagedDNSServiceRef(DNSServiceRef); 24 | 25 | impl ManagedDNSServiceRef { 26 | /// Constructs a new `ManagedDNSServiceRef`. 27 | pub fn new() -> Self { 28 | Self(ptr::null_mut()) 29 | } 30 | 31 | /// Delegate function for [`DNSServiceRegister`]. 32 | /// 33 | /// [`DNSServiceRegister`]: https://developer.apple.com/documentation/dnssd/1804733-dnsserviceregister?language=objc 34 | /// 35 | /// # Safety 36 | /// This function is unsafe because it calls a C function. 37 | pub unsafe fn register_service( 38 | &mut self, 39 | RegisterServiceParams { 40 | flags, 41 | interface_index, 42 | name, 43 | regtype, 44 | domain, 45 | host, 46 | port, 47 | txt_len, 48 | txt_record, 49 | callback, 50 | context, 51 | }: RegisterServiceParams, 52 | ) -> Result<()> { 53 | bonjour_util::sys_exec( 54 | || { 55 | DNSServiceRegister( 56 | &mut self.0 as *mut DNSServiceRef, 57 | flags, 58 | interface_index, 59 | name, 60 | regtype, 61 | domain, 62 | host, 63 | port.to_be(), 64 | txt_len, 65 | txt_record, 66 | callback, 67 | context, 68 | ) 69 | }, 70 | "could not register service", 71 | ) 72 | } 73 | 74 | /// Delegate function for [`DNSServiceBrowse`]. 75 | /// 76 | /// [`DNSServiceBrowse`]: https://developer.apple.com/documentation/dnssd/1804742-dnsservicebrowse?language=objc 77 | /// 78 | /// # Safety 79 | /// This function is unsafe because it calls a C function. 80 | pub unsafe fn browse_services( 81 | &mut self, 82 | BrowseServicesParams { 83 | flags, 84 | interface_index, 85 | regtype, 86 | domain, 87 | callback, 88 | context, 89 | }: BrowseServicesParams, 90 | ) -> Result<()> { 91 | bonjour_util::sys_exec( 92 | || { 93 | DNSServiceBrowse( 94 | &mut self.0 as *mut DNSServiceRef, 95 | flags, 96 | interface_index, 97 | regtype, 98 | domain, 99 | callback, 100 | context, 101 | ) 102 | }, 103 | "could not browse services", 104 | ) 105 | } 106 | 107 | /// Delegate function fro [`DNSServiceResolve`]. 108 | /// 109 | /// [`DNSServiceResolve`]: https://developer.apple.com/documentation/dnssd/1804744-dnsserviceresolve?language=objc 110 | /// 111 | /// # Safety 112 | /// This function is unsafe because it calls a C function. 113 | pub unsafe fn resolve_service( 114 | &mut self, 115 | ServiceResolveParams { 116 | flags, 117 | interface_index, 118 | name, 119 | regtype, 120 | domain, 121 | callback, 122 | context, 123 | }: ServiceResolveParams, 124 | ) -> Result<()> { 125 | bonjour_util::sys_exec( 126 | || { 127 | DNSServiceResolve( 128 | &mut self.0 as *mut DNSServiceRef, 129 | flags, 130 | interface_index, 131 | name, 132 | regtype, 133 | domain, 134 | callback, 135 | context, 136 | ) 137 | }, 138 | "DNSServiceResolve() reported error", 139 | )?; 140 | 141 | self.process_result() 142 | } 143 | 144 | /// Delegate function for [`DNSServiceGetAddrInfo`]. 145 | /// 146 | /// [`DNSServiceGetAddrInfo`]: https://developer.apple.com/documentation/dnssd/1804700-dnsservicegetaddrinfo?language=objc 147 | /// 148 | /// # Safety 149 | /// This function is unsafe because it calls a C function. 150 | pub unsafe fn get_address_info( 151 | &mut self, 152 | GetAddressInfoParams { 153 | flags, 154 | interface_index, 155 | protocol, 156 | hostname, 157 | callback, 158 | context, 159 | }: GetAddressInfoParams, 160 | ) -> Result<()> { 161 | bonjour_util::sys_exec( 162 | || { 163 | DNSServiceGetAddrInfo( 164 | &mut self.0 as *mut DNSServiceRef, 165 | flags, 166 | interface_index, 167 | protocol, 168 | hostname, 169 | callback, 170 | context, 171 | ) 172 | }, 173 | "DNSServiceGetAddrInfo() reported error", 174 | )?; 175 | 176 | self.process_result() 177 | } 178 | 179 | /// Delegate function for [`DNSServiceProcessResult`]. 180 | /// 181 | /// [`DNSServiceProcessResult`]: https://developer.apple.com/documentation/dnssd/1804696-dnsserviceprocessresult?language=objc 182 | /// 183 | /// # Safety 184 | /// This function is unsafe because it calls a C function. 185 | pub unsafe fn process_result(&self) -> Result<()> { 186 | bonjour_util::sys_exec( 187 | || DNSServiceProcessResult(self.0), 188 | "could not process service result", 189 | ) 190 | } 191 | 192 | /// Delegate function for [`DNSServiceRefSockFD`]. 193 | /// 194 | /// [`DNSServiceRefSockFD`]: https://developer.apple.com/documentation/dnssd/1804698-dnsservicerefsockfd?language=objc 195 | /// 196 | /// # Safety 197 | /// This function is unsafe because it calls a C function. 198 | pub unsafe fn sock_fd(&self) -> dnssd_sock_t { 199 | DNSServiceRefSockFD(self.0) 200 | } 201 | } 202 | 203 | impl Default for ManagedDNSServiceRef { 204 | fn default() -> Self { 205 | Self::new() 206 | } 207 | } 208 | 209 | impl Drop for ManagedDNSServiceRef { 210 | fn drop(&mut self) { 211 | unsafe { 212 | if !self.0.is_null() { 213 | DNSServiceRefDeallocate(self.0); 214 | } 215 | } 216 | } 217 | } 218 | 219 | unsafe impl Send for ManagedDNSServiceRef {} 220 | 221 | /// Holds parameters for `ManagedDNSServiceRef::register_service()`. 222 | #[derive(Builder, BuilderDelegate)] 223 | pub struct RegisterServiceParams { 224 | flags: DNSServiceFlags, 225 | interface_index: u32, 226 | name: *const c_char, 227 | regtype: *const c_char, 228 | domain: *const c_char, 229 | host: *const c_char, 230 | port: u16, 231 | txt_len: u16, 232 | txt_record: *const c_void, 233 | callback: DNSServiceRegisterReply, 234 | context: *mut c_void, 235 | } 236 | 237 | /// Holds parameters for `ManagedDNSServiceRef::browse_services()`. 238 | #[derive(Builder, BuilderDelegate)] 239 | pub struct BrowseServicesParams { 240 | flags: DNSServiceFlags, 241 | interface_index: u32, 242 | regtype: *const c_char, 243 | domain: *const c_char, 244 | callback: DNSServiceBrowseReply, 245 | context: *mut c_void, 246 | } 247 | 248 | /// Holds parameters for `ManagedDNSServiceRef::resolve_service()`. 249 | #[derive(Builder, BuilderDelegate)] 250 | pub struct ServiceResolveParams { 251 | flags: DNSServiceFlags, 252 | interface_index: u32, 253 | name: *const c_char, 254 | regtype: *const c_char, 255 | domain: *const c_char, 256 | callback: DNSServiceResolveReply, 257 | context: *mut c_void, 258 | } 259 | 260 | /// Holds parameters for `ManagedDNSServiceRef::get_address_info()`. 261 | #[derive(Builder, BuilderDelegate)] 262 | pub struct GetAddressInfoParams { 263 | flags: DNSServiceFlags, 264 | interface_index: u32, 265 | protocol: DNSServiceProtocol, 266 | hostname: *const c_char, 267 | callback: DNSServiceGetAddrInfoReply, 268 | context: *mut c_void, 269 | } 270 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/txt_record.rs: -------------------------------------------------------------------------------- 1 | //! Bonjour implementation for cross-platform TXT record. 2 | 3 | use super::txt_record_ref::ManagedTXTRecordRef; 4 | use crate::ffi::c_str; 5 | use crate::txt_record::TTxtRecord; 6 | use crate::Result; 7 | use libc::{c_char, c_void}; 8 | use std::ffi::CString; 9 | use std::{ptr, slice}; 10 | 11 | /// Interface for interfacing with Bonjour's TXT record capabilities. 12 | pub struct BonjourTxtRecord(ManagedTXTRecordRef); 13 | 14 | impl TTxtRecord for BonjourTxtRecord { 15 | fn new() -> Self { 16 | Self(unsafe { ManagedTXTRecordRef::new() }) 17 | } 18 | 19 | fn insert(&mut self, key: &str, value: &str) -> Result<()> { 20 | let key = c_string!(key); 21 | let value = c_string!(value); 22 | let value_size = value.as_bytes().len(); 23 | 24 | unsafe { 25 | self.0.set_value( 26 | key.as_ptr() as *const c_char, 27 | value_size as u8, 28 | value.as_ptr() as *const c_void, 29 | ) 30 | } 31 | } 32 | 33 | fn get(&self, key: &str) -> Option { 34 | let mut value_len: u8 = 0; 35 | 36 | let c_str = c_string!(key); 37 | 38 | let value_raw = unsafe { 39 | self.0 40 | .get_value_ptr(c_str.as_ptr() as *const c_char, &mut value_len) 41 | }; 42 | 43 | if value_raw.is_null() { 44 | None 45 | } else { 46 | unsafe { read_value(value_raw, value_len) }.into() 47 | } 48 | } 49 | 50 | fn remove(&mut self, key: &str) -> Option { 51 | let c_str = c_string!(key); 52 | let prev = self.get(key)?; 53 | 54 | unsafe { 55 | self.0 56 | .remove_value(c_str.as_ptr() as *const c_char) 57 | .expect("could not remove value") 58 | }; 59 | 60 | prev.into() 61 | } 62 | 63 | fn contains_key(&self, key: &str) -> bool { 64 | let c_str = c_string!(key); 65 | unsafe { self.0.contains_key(c_str.as_ptr() as *const c_char) } 66 | } 67 | 68 | fn len(&self) -> usize { 69 | unsafe { self.0.get_count() as usize } 70 | } 71 | 72 | fn iter<'a>(&'a self) -> Box + 'a> { 73 | Box::new(Iter::new(self)) 74 | } 75 | 76 | fn keys<'a>(&'a self) -> Box + 'a> { 77 | Box::new(Keys(Iter::new(self))) 78 | } 79 | 80 | fn values<'a>(&'a self) -> Box + 'a> { 81 | Box::new(Values(Iter::new(self))) 82 | } 83 | } 84 | 85 | impl Clone for BonjourTxtRecord { 86 | fn clone(&self) -> Self { 87 | Self(unsafe { self.0.clone() }) 88 | } 89 | } 90 | 91 | impl BonjourTxtRecord { 92 | pub(super) fn inner(&self) -> &ManagedTXTRecordRef { 93 | &self.0 94 | } 95 | } 96 | 97 | impl From for BonjourTxtRecord { 98 | fn from(txt: ManagedTXTRecordRef) -> Self { 99 | Self(txt) 100 | } 101 | } 102 | 103 | impl PartialEq for BonjourTxtRecord { 104 | fn eq(&self, other: &Self) -> bool { 105 | self.to_map() == other.to_map() 106 | } 107 | } 108 | 109 | /// An `Iterator` that allows iteration over a [`BonjourTxtRecord`] similar to a `HashMap`. 110 | #[derive(new)] 111 | pub struct Iter<'a> { 112 | record: &'a BonjourTxtRecord, 113 | #[new(default)] 114 | index: usize, 115 | } 116 | 117 | impl Iter<'_> { 118 | const KEY_LEN: u16 = 256; 119 | } 120 | 121 | impl Iterator for Iter<'_> { 122 | type Item = (String, String); 123 | 124 | fn next(&mut self) -> Option { 125 | if self.index == self.record.len() { 126 | return None; 127 | } 128 | 129 | let raw_key: CString = unsafe { c_string!(alloc(Iter::KEY_LEN as usize)) }; 130 | let mut value_len: u8 = 0; 131 | let mut value: *const c_void = ptr::null_mut(); 132 | 133 | unsafe { 134 | self.record 135 | .0 136 | .get_item_at_index( 137 | self.index as u16, 138 | Iter::KEY_LEN, 139 | raw_key.as_ptr() as *mut c_char, 140 | &mut value_len, 141 | &mut value, 142 | ) 143 | .expect("could not get item at index"); 144 | } 145 | 146 | assert_not_null!(value); 147 | 148 | let key = String::from(c_str::to_str(&raw_key)) 149 | .trim_matches(char::from(0)) 150 | .to_string(); 151 | 152 | let value = unsafe { read_value(value, value_len) }; 153 | 154 | self.index += 1; 155 | 156 | Some((key, value)) 157 | } 158 | } 159 | 160 | /// An `Iterator` that allows iteration over a [`BonjourTxtRecord`]'s keys. 161 | pub struct Keys<'a>(Iter<'a>); 162 | 163 | impl Iterator for Keys<'_> { 164 | type Item = String; 165 | 166 | fn next(&mut self) -> Option { 167 | self.0.next().map(|e| e.0) 168 | } 169 | } 170 | 171 | /// An `Iterator` that allows iteration over a [`BonjourTxtRecord`]'s values. 172 | pub struct Values<'a>(Iter<'a>); 173 | 174 | impl Iterator for Values<'_> { 175 | type Item = String; 176 | 177 | fn next(&mut self) -> Option { 178 | self.0.next().map(|e| e.1) 179 | } 180 | } 181 | 182 | unsafe fn read_value(value: *const c_void, value_len: u8) -> String { 183 | let value_len = value_len as usize; 184 | let value_raw = slice::from_raw_parts(value as *const u8, value_len); 185 | String::from_utf8(value_raw.to_vec()).expect("could not read value") 186 | } 187 | -------------------------------------------------------------------------------- /zeroconf/src/bonjour/txt_record_ref.rs: -------------------------------------------------------------------------------- 1 | //! Low level interface for interacting with `TXTRecordRef`. 2 | 3 | use crate::Result; 4 | use bonjour_sys::{ 5 | TXTRecordContainsKey, TXTRecordCreate, TXTRecordDeallocate, TXTRecordGetBytesPtr, 6 | TXTRecordGetCount, TXTRecordGetItemAtIndex, TXTRecordGetLength, TXTRecordGetValuePtr, 7 | TXTRecordRef, TXTRecordRemoveValue, TXTRecordSetValue, 8 | }; 9 | use libc::{c_char, c_uchar, c_void}; 10 | use std::ffi::CString; 11 | use std::{fmt, mem, ptr}; 12 | 13 | use super::bonjour_util; 14 | 15 | /// Wraps the `ManagedTXTRecordRef` type from the raw Bonjour bindings. 16 | /// 17 | /// `zeroconf::TxtRecord` provides the cross-platform bindings for this functionality. 18 | pub struct ManagedTXTRecordRef(TXTRecordRef); 19 | 20 | impl ManagedTXTRecordRef { 21 | /// Creates a new empty TXT record 22 | /// 23 | /// # Safety 24 | /// This function is unsafe because it calls a C function. 25 | pub unsafe fn new() -> Self { 26 | let mut record: TXTRecordRef = mem::zeroed(); 27 | TXTRecordCreate(&mut record, 0, ptr::null_mut()); 28 | Self(record) 29 | } 30 | 31 | /// Delegate function for [`TXTRecordGetBytes()`]. 32 | /// 33 | /// [`TXTRecordGetBytes()`]: https://developer.apple.com/documentation/dnssd/1804717-txtrecordgetbytesptr?language=objc 34 | /// 35 | /// # Safety 36 | /// This function is unsafe because it makes no guarantees about the raw pointer arguments 37 | pub unsafe fn get_bytes_ptr(&self) -> *const c_void { 38 | TXTRecordGetBytesPtr(&self.0) 39 | } 40 | 41 | /// Delegate function for [`TXTRecordGetLength()`]. 42 | /// 43 | /// [`TXTRecordGetLength()`]: https://developer.apple.com/documentation/dnssd/1804720-txtrecordgetlength?language=objc 44 | /// 45 | /// # Safety 46 | /// This function is unsafe because it makes no guarantees about the raw pointer arguments 47 | pub unsafe fn get_length(&self) -> u16 { 48 | TXTRecordGetLength(&self.0) 49 | } 50 | 51 | /// Delegate function for [`TXTRecordRemoveValue()`]. 52 | /// 53 | /// # Safety 54 | /// This function is unsafe because it makes no guarantees about `key` and `key` is 55 | /// dereferenced. `key` is expected to be a non-null `*const c_char`. 56 | /// 57 | /// [`TXTRecordRemoveValue()`]: https://developer.apple.com/documentation/dnssd/1804721-txtrecordremovevalue?language=objc 58 | pub unsafe fn remove_value(&mut self, key: *const c_char) -> Result<()> { 59 | bonjour_util::sys_exec( 60 | || TXTRecordRemoveValue(&mut self.0, key), 61 | "could not remove TXT record value", 62 | ) 63 | } 64 | 65 | /// Delegate function for [`TXTRecordSetValue`]. 66 | /// 67 | /// # Safety 68 | /// This function is unsafe because it makes no guarantees about it's rew pointer arguments 69 | /// that are dereferenced. 70 | /// 71 | /// [`TXTRecordSetValue`]: https://developer.apple.com/documentation/dnssd/1804723-txtrecordsetvalue?language=objc 72 | pub unsafe fn set_value( 73 | &mut self, 74 | key: *const c_char, 75 | value_size: u8, 76 | value: *const c_void, 77 | ) -> Result<()> { 78 | bonjour_util::sys_exec( 79 | || TXTRecordSetValue(&mut self.0, key, value_size, value), 80 | "could not set TXT record value", 81 | ) 82 | } 83 | 84 | /// Delegate function for [`TXTRecordContainsKey`]. 85 | /// 86 | /// # Safety 87 | /// This function is unsafe because it makes no guarantees about it's rew pointer arguments 88 | /// that are dereferenced. 89 | /// 90 | /// [`TXTRecordContainsKey`]: https://developer.apple.com/documentation/dnssd/1804705-txtrecordcontainskey?language=objc 91 | pub unsafe fn contains_key(&self, key: *const c_char) -> bool { 92 | TXTRecordContainsKey(self.get_length(), self.get_bytes_ptr(), key) == 1 93 | } 94 | 95 | /// Delegate function for [`TXTRecordGetCount`]. 96 | /// 97 | /// [`TXTRecordGetCount`]: https://developer.apple.com/documentation/dnssd/1804706-txtrecordgetcount?language=objc 98 | /// 99 | /// # Safety 100 | /// This function is unsafe because it makes no guarantees about it's raw pointer arguments 101 | pub unsafe fn get_count(&self) -> u16 { 102 | _get_count(self.get_length(), self.get_bytes_ptr()) 103 | } 104 | 105 | /// Delegate function for [`TXTRecordGetItemAtIndex`]. 106 | /// 107 | /// # Safety 108 | /// This function is unsafe because it makes no guarantees about it's raw pointer arguments 109 | /// that are dereferenced. 110 | /// 111 | /// [`TXTRecordGetItemAtIndex`]: https://developer.apple.com/documentation/dnssd/1804708-txtrecordgetitematindex?language=objc 112 | pub unsafe fn get_item_at_index( 113 | &self, 114 | item_index: u16, 115 | key_buf_len: u16, 116 | key: *mut c_char, 117 | value_len: *mut u8, 118 | value: *mut *const c_void, 119 | ) -> Result<()> { 120 | _get_item_at_index( 121 | self.get_length(), 122 | self.get_bytes_ptr(), 123 | item_index, 124 | key_buf_len, 125 | key, 126 | value_len, 127 | value, 128 | ) 129 | } 130 | 131 | /// Delegate function for [`TXTRecordGetValuePtr`]. 132 | /// 133 | /// # Safety 134 | /// This function is unsafe because it makes no guarantees about it's raw pointer arguments 135 | /// that are dereferenced. 136 | /// 137 | /// [`TXTRecordGetValuePtr`]: https://developer.apple.com/documentation/dnssd/1804709-txtrecordgetvalueptr?language=objc 138 | pub unsafe fn get_value_ptr(&self, key: *const c_char, value_len: *mut u8) -> *const c_void { 139 | TXTRecordGetValuePtr(self.get_length(), self.get_bytes_ptr(), key, value_len) 140 | } 141 | 142 | /// Returns a copy of the TXT record. 143 | /// 144 | /// # Safety 145 | /// This function is unsafe because it makes no guarantees about the raw pointer arguments 146 | pub unsafe fn clone(&self) -> Self { 147 | Self::clone_raw(self.get_bytes_ptr() as *const c_uchar, self.get_length()) 148 | .expect("could not clone TXT record") 149 | } 150 | 151 | pub(crate) unsafe fn clone_raw(raw: *const c_uchar, size: u16) -> Result { 152 | let chars = c_string!(alloc(size as usize)).into_raw() as *mut c_uchar; 153 | ptr::copy(raw, chars, size as usize); 154 | let chars = CString::from_raw(chars as *mut c_char); 155 | 156 | let mut record = Self::new(); 157 | 158 | for i in 0.._get_count(size, chars.as_ptr() as *const c_void) { 159 | let key = c_string!(alloc(256)); 160 | let mut value_len: u8 = 0; 161 | let mut value: *const c_void = ptr::null_mut(); 162 | 163 | _get_item_at_index( 164 | size, 165 | chars.as_ptr() as *const c_void, 166 | i, 167 | 256, 168 | key.as_ptr() as *mut c_char, 169 | &mut value_len, 170 | &mut value, 171 | )?; 172 | 173 | record.set_value(key.as_ptr() as *mut c_char, value_len, value)?; 174 | } 175 | 176 | Ok(record) 177 | } 178 | } 179 | 180 | impl Drop for ManagedTXTRecordRef { 181 | fn drop(&mut self) { 182 | unsafe { TXTRecordDeallocate(&mut self.0) }; 183 | } 184 | } 185 | 186 | impl fmt::Debug for ManagedTXTRecordRef { 187 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 188 | f.debug_struct("ManagedTXTRecordRef").finish() 189 | } 190 | } 191 | 192 | unsafe impl Send for ManagedTXTRecordRef {} 193 | 194 | unsafe fn _get_count(length: u16, data: *const c_void) -> u16 { 195 | TXTRecordGetCount(length, data) 196 | } 197 | 198 | unsafe fn _get_item_at_index( 199 | length: u16, 200 | data: *const c_void, 201 | item_index: u16, 202 | key_buf_len: u16, 203 | key: *mut c_char, 204 | value_len: *mut u8, 205 | value: *mut *const c_void, 206 | ) -> Result<()> { 207 | bonjour_util::sys_exec( 208 | || TXTRecordGetItemAtIndex(length, data, item_index, key_buf_len, key, value_len, value), 209 | "could get item at index for TXT record", 210 | ) 211 | } 212 | 213 | #[cfg(test)] 214 | mod tests { 215 | use super::*; 216 | use crate::ffi::c_str; 217 | 218 | #[test] 219 | fn set_value_success() { 220 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 221 | let key = c_string!("foo"); 222 | let value = c_string!("bar"); 223 | let value_size = mem::size_of_val(&value) as u8; 224 | 225 | unsafe { 226 | record 227 | .set_value( 228 | key.as_ptr() as *const c_char, 229 | value_size, 230 | value.as_ptr() as *const c_void, 231 | ) 232 | .unwrap(); 233 | } 234 | 235 | let mut value_len: u8 = 0; 236 | let result = unsafe { 237 | c_str::raw_to_str( 238 | record.get_value_ptr(key.as_ptr() as *const c_char, &mut value_len) 239 | as *const c_char, 240 | ) 241 | }; 242 | 243 | assert_eq!(result, "bar"); 244 | } 245 | 246 | #[test] 247 | fn set_value_null_success() { 248 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 249 | let key = c_string!("foo"); 250 | let value_size = 0; 251 | 252 | unsafe { 253 | record 254 | .set_value(key.as_ptr() as *const c_char, value_size, ptr::null()) 255 | .unwrap(); 256 | } 257 | 258 | let mut value_len: u8 = 0; 259 | let result = unsafe { record.get_value_ptr(key.as_ptr() as *const c_char, &mut value_len) }; 260 | 261 | assert!(result.is_null()); 262 | } 263 | 264 | #[test] 265 | fn remove_value_success() { 266 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 267 | let key = c_string!("foo"); 268 | let value = c_string!("bar"); 269 | let value_size = mem::size_of_val(&value) as u8; 270 | 271 | unsafe { 272 | record 273 | .set_value( 274 | key.as_ptr() as *const c_char, 275 | value_size, 276 | value.as_ptr() as *const c_void, 277 | ) 278 | .unwrap(); 279 | 280 | record.remove_value(key.as_ptr() as *const c_char).unwrap(); 281 | } 282 | 283 | let mut value_len: u8 = 0; 284 | let result = unsafe { record.get_value_ptr(key.as_ptr() as *const c_char, &mut value_len) }; 285 | 286 | assert!(result.is_null()); 287 | } 288 | 289 | #[test] 290 | #[should_panic] 291 | fn remove_value_missing_key_panics() { 292 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 293 | let key = c_string!("foo"); 294 | unsafe { 295 | record.remove_value(key.as_ptr() as *const c_char).unwrap(); 296 | } 297 | } 298 | 299 | #[test] 300 | fn contains_key_success() { 301 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 302 | let key = c_string!("foo"); 303 | let value = c_string!("bar"); 304 | let value_size = mem::size_of_val(&value) as u8; 305 | 306 | unsafe { 307 | record 308 | .set_value( 309 | key.as_ptr() as *const c_char, 310 | value_size, 311 | value.as_ptr() as *const c_void, 312 | ) 313 | .unwrap(); 314 | } 315 | 316 | let no_val = c_string!("baz"); 317 | 318 | unsafe { 319 | assert!(record.contains_key(key.as_ptr() as *const c_char)); 320 | assert!(!record.contains_key(no_val.as_ptr() as *const c_char)); 321 | } 322 | } 323 | 324 | #[test] 325 | fn get_count_success() { 326 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 327 | let key = c_string!("foo"); 328 | let value = c_string!("bar"); 329 | let value_size = mem::size_of_val(&value) as u8; 330 | 331 | unsafe { 332 | record 333 | .set_value( 334 | key.as_ptr() as *const c_char, 335 | value_size, 336 | value.as_ptr() as *const c_void, 337 | ) 338 | .unwrap(); 339 | } 340 | 341 | assert_eq!(unsafe { record.get_count() }, 1); 342 | } 343 | 344 | #[test] 345 | fn get_item_at_index() { 346 | let mut record = unsafe { ManagedTXTRecordRef::new() }; 347 | let key = c_string!("foo"); 348 | let value = c_string!("bar"); 349 | let value_size = mem::size_of_val(&value) as u8; 350 | 351 | unsafe { 352 | record 353 | .set_value( 354 | key.as_ptr() as *const c_char, 355 | value_size, 356 | value.as_ptr() as *const c_void, 357 | ) 358 | .unwrap(); 359 | } 360 | 361 | let key = unsafe { c_string!(alloc(256)) }; 362 | let mut value_len: u8 = 0; 363 | let mut value: *const c_void = ptr::null_mut(); 364 | 365 | unsafe { 366 | record 367 | .get_item_at_index( 368 | 0, 369 | 256, 370 | key.as_ptr() as *mut c_char, 371 | &mut value_len, 372 | &mut value, 373 | ) 374 | .unwrap(); 375 | 376 | let key = c_str::raw_to_str(key.as_ptr() as *const c_char); 377 | let value = c_str::raw_to_str(value as *const c_char); 378 | 379 | assert_eq!(key, "foo"); 380 | assert_eq!(value, "bar"); 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /zeroconf/src/browser.rs: -------------------------------------------------------------------------------- 1 | //! Trait definition for cross-platform browser 2 | 3 | use crate::{EventLoop, NetworkInterface, Result, ServiceType, TxtRecord}; 4 | use std::any::Any; 5 | use std::sync::Arc; 6 | 7 | /// Event from [`MdnsBrowser`] received by the `ServiceBrowserCallback`. 8 | /// 9 | /// [`MdnsBrowser`]: type.MdnsBrowser.html 10 | #[derive(Debug, Clone, PartialEq, Eq)] 11 | pub enum BrowserEvent { 12 | Add(ServiceDiscovery), 13 | Remove(ServiceRemoval), 14 | } 15 | 16 | /// Interface for interacting with underlying mDNS implementation service browsing capabilities. 17 | pub trait TMdnsBrowser { 18 | /// Creates a new `MdnsBrowser` that browses for the specified `kind` (e.g. `_http._tcp`) 19 | fn new(service_type: ServiceType) -> Self; 20 | 21 | /// Sets the network interface on which to browse for services on. 22 | /// 23 | /// Most applications will want to use the default value `NetworkInterface::Unspec` to browse 24 | /// on all available interfaces. 25 | fn set_network_interface(&mut self, interface: NetworkInterface); 26 | 27 | /// Returns the network interface on which to browse for services on. 28 | fn network_interface(&self) -> NetworkInterface; 29 | 30 | /// Sets the [`ServiceBrowserCallback`] that is invoked when the browser has discovered and 31 | /// resolved or removed a service. 32 | /// 33 | /// [`ServiceBrowserCallback`]: ../type.ServiceBrowserCallback.html 34 | fn set_service_callback(&mut self, service_callback: Box); 35 | 36 | /// Sets the optional user context to pass through to the callback. This is useful if you need 37 | /// to share state between pre and post-callback. The context type must implement `Any`. 38 | fn set_context(&mut self, context: Box); 39 | 40 | /// Returns the optional user context to pass through to the callback. 41 | fn context(&self) -> Option<&dyn Any>; 42 | 43 | /// Starts the browser. Returns an `EventLoop` which can be called to keep the browser alive. 44 | fn browse_services(&mut self) -> Result; 45 | } 46 | 47 | /// Callback invoked from [`MdnsBrowser`] once a service has been discovered and resolved or 48 | /// removed. 49 | /// 50 | /// # Arguments 51 | /// * `browser_event` - The event received from Zeroconf 52 | /// * `context` - The optional user context passed through 53 | /// 54 | /// [`MdnsBrowser`]: type.MdnsBrowser.html 55 | pub type ServiceBrowserCallback = dyn Fn(Result, Option>); 56 | 57 | /// Represents a service that has been discovered by a [`MdnsBrowser`]. 58 | /// 59 | /// [`MdnsBrowser`]: type.MdnsBrowser.html 60 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 61 | #[derive(Debug, Getters, Builder, BuilderDelegate, Clone, PartialEq, Eq)] 62 | pub struct ServiceDiscovery { 63 | name: String, 64 | service_type: ServiceType, 65 | domain: String, 66 | host_name: String, 67 | address: String, 68 | port: u16, 69 | txt: Option, 70 | } 71 | 72 | /// Represents a service that has been removed by a [`MdnsBrowser`]. 73 | /// 74 | /// [`MdnsBrowser`]: type.MdnsBrowser.html 75 | #[derive(Debug, Getters, Builder, BuilderDelegate, Clone, PartialEq, Eq)] 76 | pub struct ServiceRemoval { 77 | /// The "abc" part in "abc._http._udp.local" 78 | name: String, 79 | /// The "_http._udp" part in "abc._http._udp.local" 80 | kind: String, 81 | /// The "local" part in "abc._http._udp.local" 82 | domain: String, 83 | } 84 | -------------------------------------------------------------------------------- /zeroconf/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Utilities regarding error handling 2 | 3 | use std::fmt; 4 | 5 | /// For when something goes wrong when interfacing with mDNS implementations 6 | #[derive(new, Debug, Clone, PartialEq, Eq)] 7 | pub struct Error { 8 | description: String, 9 | } 10 | 11 | impl std::error::Error for Error {} 12 | 13 | impl fmt::Display for Error { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "{}", self.description) 16 | } 17 | } 18 | 19 | impl From<&str> for Error { 20 | fn from(s: &str) -> Self { 21 | Error::from(s.to_string()) 22 | } 23 | } 24 | 25 | impl From for Error { 26 | fn from(s: String) -> Self { 27 | Error::new(s) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /zeroconf/src/event_loop.rs: -------------------------------------------------------------------------------- 1 | //! Trait definition for cross-platform event loop 2 | 3 | use crate::Result; 4 | use std::time::Duration; 5 | 6 | /// A handle on the underlying implementation to poll the event loop. Typically, `poll()` 7 | /// is called in a loop to keep a `MdnsService` or `MdnsBrowser` running. 8 | pub trait TEventLoop { 9 | /// Polls for new events. 10 | fn poll(&self, timeout: Duration) -> Result<()>; 11 | } 12 | -------------------------------------------------------------------------------- /zeroconf/src/ffi/c_str.rs: -------------------------------------------------------------------------------- 1 | //! Utilities related to c-string handling 2 | 3 | use libc::c_char; 4 | use std::ffi::{CStr, CString}; 5 | 6 | /// Helper trait to map to `Option<*const c_char>`. 7 | #[cfg(not(target_os = "linux"))] 8 | pub trait AsCChars { 9 | /// Maps the type to a `Option<*const c_char>`. 10 | fn as_c_chars(&self) -> Option<*const c_char>; 11 | } 12 | 13 | #[cfg(not(target_os = "linux"))] 14 | impl AsCChars for Option<&CString> { 15 | fn as_c_chars(&self) -> Option<*const c_char> { 16 | self.map(|s| s.as_ptr() as *const c_char) 17 | } 18 | } 19 | 20 | /// Returns the specified `*const c_char` as a `&'a str`. Ownership is not taken. 21 | /// 22 | /// # Safety 23 | /// This function is unsafe due to a call to the unsafe function [`CStr::from_ptr()`]. 24 | /// 25 | /// [`CStr::from_ptr()`]: https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr 26 | pub unsafe fn raw_to_str<'a>(s: *const c_char) -> &'a str { 27 | assert_not_null!(s); 28 | CStr::from_ptr(s) 29 | .to_str() 30 | .expect("could not convert raw to str") 31 | } 32 | 33 | /// Copies the specified `*const c_char` into a `String`. 34 | /// 35 | /// # Safety 36 | /// This function is unsafe due to a call to the unsafe function [`raw_to_str()`]. 37 | /// 38 | /// [`raw_to_str()`]: fn.raw_to_str.html 39 | pub unsafe fn copy_raw(s: *const c_char) -> String { 40 | assert_not_null!(s); 41 | String::from(raw_to_str(s)) 42 | } 43 | 44 | /// Converts the specified [`CString`] to a `&str`. 45 | pub fn to_str(s: &CString) -> &str { 46 | s.to_str().expect("could not convert CString to str") 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | use libc::c_char; 53 | use std::ptr; 54 | 55 | #[test] 56 | fn raw_to_str_success() { 57 | let c_string = c_string!("foo"); 58 | unsafe { assert_eq!(raw_to_str(c_string.as_ptr() as *const c_char), "foo") }; 59 | } 60 | 61 | #[test] 62 | #[should_panic] 63 | fn raw_to_str_expects_non_null() { 64 | unsafe { raw_to_str(ptr::null() as *const c_char) }; 65 | } 66 | 67 | #[test] 68 | fn copy_raw_success() { 69 | let c_string = c_string!("foo"); 70 | let c_str = c_string.as_ptr() as *const c_char; 71 | unsafe { assert_eq!(copy_raw(c_str), "foo".to_string()) }; 72 | } 73 | 74 | #[test] 75 | #[should_panic] 76 | fn copy_raw_expects_non_null() { 77 | unsafe { copy_raw(ptr::null() as *const c_char) }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /zeroconf/src/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities related to FFI bindings 2 | 3 | use libc::c_void; 4 | use std::ptr; 5 | 6 | pub(crate) mod c_str; 7 | 8 | /// Helper trait to convert a raw `*mut c_void` to it's rust type 9 | pub trait FromRaw { 10 | /// Converts the specified `*mut c_void` to a `&'a mut T`. 11 | /// 12 | /// # Safety 13 | /// This function is unsafe due to the dereference of the specified raw pointer. 14 | unsafe fn from_raw<'a>(raw: *mut c_void) -> &'a mut T { 15 | assert_not_null!(raw); 16 | &mut *(raw as *mut T) 17 | } 18 | } 19 | 20 | /// Helper trait to convert self to a raw `*mut c_void` 21 | pub trait AsRaw { 22 | /// Converts self to a raw `*mut c_void` by cast. 23 | fn as_raw(&mut self) -> *mut c_void { 24 | self as *mut _ as *mut c_void 25 | } 26 | } 27 | 28 | /// Helper trait to unwrap a type to a `*const T` or a null-pointer if not present. 29 | pub trait UnwrapOrNull { 30 | /// Unwraps this type to `*const T` or `ptr::null()` if not present. 31 | fn unwrap_or_null(&self) -> *const T; 32 | } 33 | 34 | impl UnwrapOrNull for Option<*const T> { 35 | fn unwrap_or_null(&self) -> *const T { 36 | self.unwrap_or_else(ptr::null) 37 | } 38 | } 39 | 40 | /// Helper trait to unwrap a type to a `*mut T` or a null-pointer if not present. 41 | #[cfg(target_os = "linux")] 42 | pub trait UnwrapMutOrNull { 43 | /// Unwraps this type to `*mut T` or `ptr::null_mut()` if not present. 44 | fn unwrap_mut_or_null(&mut self) -> *mut T; 45 | } 46 | 47 | #[cfg(target_os = "linux")] 48 | impl UnwrapMutOrNull for Option<*mut T> { 49 | fn unwrap_mut_or_null(&mut self) -> *mut T { 50 | self.unwrap_or_else(ptr::null_mut) 51 | } 52 | } 53 | 54 | #[cfg(target_vendor = "apple")] 55 | pub(crate) mod bonjour { 56 | use crate::Result; 57 | use libc::{fd_set, suseconds_t, time_t, timeval}; 58 | use std::time::Duration; 59 | use std::{mem, ptr}; 60 | 61 | /// Performs a unix `select()` on the specified `sock_fd` and `timeout`. Returns the select result 62 | /// or `Err` if the result is negative. 63 | /// 64 | /// # Safety 65 | /// This function is unsafe because it directly interfaces with C-library system calls. 66 | pub unsafe fn read_select(sock_fd: i32, timeout: Duration) -> Result { 67 | let mut read_flags: fd_set = mem::zeroed(); 68 | 69 | libc::FD_ZERO(&mut read_flags); 70 | libc::FD_SET(sock_fd, &mut read_flags); 71 | 72 | let tv_sec = timeout.as_secs() as time_t; 73 | let tv_usec = timeout.subsec_micros() as suseconds_t; 74 | let mut timeout = timeval { tv_sec, tv_usec }; 75 | 76 | let result = libc::select( 77 | sock_fd + 1, 78 | &mut read_flags, 79 | ptr::null_mut(), 80 | ptr::null_mut(), 81 | &mut timeout, 82 | ); 83 | 84 | if result < 0 { 85 | Err("select(): returned error status".into()) 86 | } else { 87 | Ok(result as u32) 88 | } 89 | } 90 | } 91 | 92 | #[cfg(target_vendor = "pc")] 93 | pub(crate) mod bonjour { 94 | use crate::Result; 95 | use bonjour_sys::{dnssd_sock_t, fd_set, select, timeval}; 96 | #[cfg(target_vendor = "apple")] 97 | use std::mem; 98 | use std::ptr; 99 | use std::time::Duration; 100 | 101 | /// Performs a unix `select()` on the specified `sock_fd` and `timeout`. Returns the select result 102 | /// or `Err` if the result is negative. 103 | /// 104 | /// # Safety 105 | /// This function is unsafe because it directly interfaces with C-library system calls. 106 | pub unsafe fn read_select(sock_fd: dnssd_sock_t, timeout: Duration) -> Result { 107 | if timeout.as_secs() > i32::MAX as u64 { 108 | return Err( 109 | "Invalid timeout duration, as_secs() value exceeds ::libc::c_long. ".into(), 110 | ); 111 | } 112 | 113 | let timeout: timeval = timeval { 114 | tv_sec: timeout.as_secs() as ::libc::c_long, 115 | tv_usec: timeout.subsec_micros() as ::libc::c_long, 116 | }; 117 | 118 | let mut set: fd_set = fd_set { 119 | fd_count: 1, 120 | fd_array: [0; 64], 121 | }; 122 | set.fd_array[0] = sock_fd; 123 | 124 | let result = select(0, &mut set, ptr::null_mut(), &mut set, &timeout); 125 | 126 | if result < 0 { 127 | Err("select(): returned error status".into()) 128 | } else { 129 | Ok(result as u32) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /zeroconf/src/interface.rs: -------------------------------------------------------------------------------- 1 | /// Represents a network interface for mDNS services 2 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 3 | pub enum NetworkInterface { 4 | /// No interface specified, bind to all available interfaces 5 | Unspec, 6 | /// An interface at a specified index 7 | AtIndex(u32), 8 | } 9 | -------------------------------------------------------------------------------- /zeroconf/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `zeroconf` is a cross-platform library that wraps underlying [ZeroConf/mDNS] implementations 2 | //! such as [Bonjour] or [Avahi], providing an easy and idiomatic way to both register and 3 | //! browse services. 4 | //! 5 | //! This crate provides the cross-platform [`MdnsService`] and [`MdnsBrowser`] available for each 6 | //! supported platform as well as platform-specific modules for lower-level access to the mDNS 7 | //! implementation should that be necessary. 8 | //! 9 | //! Most users of this crate need only [`MdnsService`] and [`MdnsBrowser`]. 10 | //! 11 | //! # Examples 12 | //! 13 | //! ## Register a service 14 | //! 15 | //! When registering a service, you may optionally pass a "context" to pass state through the 16 | //! callback. The only requirement is that this context implements the [`Any`] trait, which most 17 | //! types will automatically. See [`MdnsService`] for more information about contexts. 18 | //! 19 | //! ```no_run 20 | //! #[macro_use] 21 | //! extern crate log; 22 | //! 23 | //! use clap::Parser; 24 | //! 25 | //! use std::any::Any; 26 | //! use std::sync::{Arc, Mutex}; 27 | //! use std::time::Duration; 28 | //! use zeroconf::prelude::*; 29 | //! use zeroconf::{MdnsService, ServiceRegistration, ServiceType, TxtRecord}; 30 | //! 31 | //! #[derive(Parser, Debug)] 32 | //! #[command(author, version, about)] 33 | //! struct Args { 34 | //! /// Name of the service type to register 35 | //! #[clap(short, long, default_value = "http")] 36 | //! name: String, 37 | //! 38 | //! /// Protocol of the service type to register 39 | //! #[clap(short, long, default_value = "tcp")] 40 | //! protocol: String, 41 | //! 42 | //! /// Sub-types of the service type to register 43 | //! #[clap(short, long)] 44 | //! sub_types: Vec, 45 | //! } 46 | //! 47 | //! #[derive(Default, Debug)] 48 | //! pub struct Context { 49 | //! service_name: String, 50 | //! } 51 | //! 52 | //! fn main() -> zeroconf::Result<()> { 53 | //! env_logger::init(); 54 | //! 55 | //! let Args { 56 | //! name, 57 | //! protocol, 58 | //! sub_types, 59 | //! } = Args::parse(); 60 | //! 61 | //! let sub_types = sub_types.iter().map(|s| s.as_str()).collect::>(); 62 | //! let service_type = ServiceType::with_sub_types(&name, &protocol, sub_types)?; 63 | //! let mut service = MdnsService::new(service_type, 8080); 64 | //! let mut txt_record = TxtRecord::new(); 65 | //! let context: Arc> = Arc::default(); 66 | //! 67 | //! txt_record.insert("foo", "bar")?; 68 | //! 69 | //! service.set_name("zeroconf_example_service"); 70 | //! service.set_registered_callback(Box::new(on_service_registered)); 71 | //! service.set_context(Box::new(context)); 72 | //! service.set_txt_record(txt_record); 73 | //! 74 | //! let event_loop = service.register()?; 75 | //! 76 | //! loop { 77 | //! // calling `poll()` will keep this service alive 78 | //! event_loop.poll(Duration::from_secs(0))?; 79 | //! } 80 | //! } 81 | //! 82 | //! fn on_service_registered( 83 | //! result: zeroconf::Result, 84 | //! context: Option>, 85 | //! ) { 86 | //! let service = result.expect("failed to register service"); 87 | //! 88 | //! info!("Service registered: {:?}", service); 89 | //! 90 | //! let context = context 91 | //! .as_ref() 92 | //! .expect("could not get context") 93 | //! .downcast_ref::>>() 94 | //! .expect("error down-casting context") 95 | //! .clone(); 96 | //! 97 | //! context 98 | //! .lock() 99 | //! .expect("failed to obtain context lock") 100 | //! .service_name = service.name().clone(); 101 | //! 102 | //! info!("Context: {:?}", context); 103 | //! 104 | //! // ... 105 | //! } 106 | //! ``` 107 | //! 108 | //! ## Browsing services 109 | //! ```no_run 110 | //! #[macro_use] 111 | //! extern crate log; 112 | //! 113 | //! use clap::Parser; 114 | //! 115 | //! use std::any::Any; 116 | //! use std::sync::Arc; 117 | //! use std::time::Duration; 118 | //! use zeroconf::prelude::*; 119 | //! use zeroconf::{BrowserEvent, MdnsBrowser, ServiceDiscovery, ServiceRemoval, ServiceType}; 120 | //! 121 | //! /// Example of a simple mDNS browser 122 | //! #[derive(Parser, Debug)] 123 | //! #[command(author, version, about)] 124 | //! struct Args { 125 | //! /// Name of the service type to browse 126 | //! #[clap(short, long, default_value = "http")] 127 | //! name: String, 128 | //! 129 | //! /// Protocol of the service type to browse 130 | //! #[clap(short, long, default_value = "tcp")] 131 | //! protocol: String, 132 | //! 133 | //! /// Sub-type of the service type to browse 134 | //! #[clap(short, long)] 135 | //! sub_type: Option, 136 | //! } 137 | //! 138 | //! fn main() -> zeroconf::Result<()> { 139 | //! env_logger::init(); 140 | //! 141 | //! let Args { 142 | //! name, 143 | //! protocol, 144 | //! sub_type, 145 | //! } = Args::parse(); 146 | //! 147 | //! let sub_types: Vec<&str> = match sub_type.as_ref() { 148 | //! Some(sub_type) => vec![sub_type], 149 | //! None => vec![], 150 | //! }; 151 | //! 152 | //! let service_type = 153 | //! ServiceType::with_sub_types(&name, &protocol, sub_types).expect("invalid service type"); 154 | //! 155 | //! let mut browser = MdnsBrowser::new(service_type); 156 | //! 157 | //! browser.set_service_callback(Box::new(on_service_event)); 158 | //! 159 | //! let event_loop = browser.browse_services()?; 160 | //! 161 | //! loop { 162 | //! // calling `poll()` will keep this browser alive 163 | //! event_loop.poll(Duration::from_secs(0))?; 164 | //! } 165 | //! } 166 | //! 167 | //! fn on_service_event( 168 | //! result: zeroconf::Result, 169 | //! _context: Option>, 170 | //! ) { 171 | //! info!( 172 | //! "Service event: {:?}", 173 | //! result.expect("service discovery failed") 174 | //! ); 175 | //! 176 | //! // ... 177 | //! } 178 | //! ``` 179 | //! 180 | //! [ZeroConf/mDNS]: https://en.wikipedia.org/wiki/Zero-configuration_networking 181 | //! [Bonjour]: https://en.wikipedia.org/wiki/Bonjour_(software) 182 | //! [Avahi]: https://en.wikipedia.org/wiki/Avahi_(software) 183 | //! [`MdnsService`]: type.MdnsService.html 184 | //! [`MdnsBrowser`]: type.MdnsBrowser.html 185 | //! [`Any`]: https://doc.rust-lang.org/std/any/trait.Any.html 186 | 187 | #![allow(clippy::needless_doctest_main)] 188 | #[macro_use] 189 | #[cfg(feature = "serde")] 190 | extern crate serde; 191 | #[macro_use] 192 | extern crate derive_builder; 193 | #[macro_use] 194 | extern crate zeroconf_macros; 195 | #[cfg(target_os = "linux")] 196 | extern crate avahi_sys; 197 | #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] 198 | extern crate bonjour_sys; 199 | #[macro_use] 200 | extern crate derive_getters; 201 | #[macro_use] 202 | extern crate log; 203 | #[macro_use] 204 | extern crate derive_new; 205 | 206 | #[macro_use] 207 | #[cfg(test)] 208 | #[allow(unused_imports)] 209 | extern crate maplit; 210 | 211 | #[macro_use] 212 | mod macros; 213 | mod ffi; 214 | mod interface; 215 | mod service_type; 216 | #[cfg(test)] 217 | mod tests; 218 | 219 | pub mod browser; 220 | pub mod error; 221 | pub mod event_loop; 222 | pub mod prelude; 223 | pub mod service; 224 | pub mod txt_record; 225 | 226 | #[cfg(target_os = "linux")] 227 | pub mod avahi; 228 | #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] 229 | pub mod bonjour; 230 | 231 | pub use browser::{BrowserEvent, ServiceBrowserCallback, ServiceDiscovery, ServiceRemoval}; 232 | pub use interface::*; 233 | pub use service::{ServiceRegisteredCallback, ServiceRegistration}; 234 | pub use service_type::*; 235 | 236 | /// Type alias for the platform-specific mDNS browser implementation 237 | #[cfg(target_os = "linux")] 238 | pub type MdnsBrowser = avahi::browser::AvahiMdnsBrowser; 239 | /// Type alias for the platform-specific mDNS browser implementation 240 | #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] 241 | pub type MdnsBrowser = bonjour::browser::BonjourMdnsBrowser; 242 | 243 | /// Type alias for the platform-specific mDNS service implementation 244 | #[cfg(target_os = "linux")] 245 | pub type MdnsService = avahi::service::AvahiMdnsService; 246 | /// Type alias for the platform-specific mDNS service implementation 247 | #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] 248 | pub type MdnsService = bonjour::service::BonjourMdnsService; 249 | 250 | /// Type alias for the platform-specific structure responsible for polling the mDNS event loop 251 | #[cfg(target_os = "linux")] 252 | pub type EventLoop = avahi::event_loop::AvahiEventLoop; 253 | /// Type alias for the platform-specific structure responsible for polling the mDNS event loop 254 | #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] 255 | pub type EventLoop = bonjour::event_loop::BonjourEventLoop; 256 | 257 | /// Type alias for the platform-specific structure responsible for storing and accessing TXT 258 | /// record data 259 | #[cfg(target_os = "linux")] 260 | pub type TxtRecord = avahi::txt_record::AvahiTxtRecord; 261 | /// Type alias for the platform-specific structure responsible for storing and accessing TXT 262 | /// record data 263 | #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] 264 | pub type TxtRecord = bonjour::txt_record::BonjourTxtRecord; 265 | 266 | /// Result type for this library 267 | pub type Result = std::result::Result; 268 | -------------------------------------------------------------------------------- /zeroconf/src/macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(useless_ptr_null_checks)] 2 | 3 | macro_rules! assert_not_null { 4 | ($ptr:expr) => { 5 | assert!(!$ptr.is_null(), "expected non-null value"); 6 | }; 7 | } 8 | 9 | macro_rules! c_string { 10 | (alloc($len:expr)) => { 11 | ::std::ffi::CString::from_vec_unchecked(vec![0; $len]) 12 | }; 13 | ($x:expr) => { 14 | ::std::ffi::CString::new($x).expect("could not create new CString") 15 | }; 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use libc::c_char; 21 | use std::ffi::CString; 22 | use std::ptr; 23 | 24 | #[test] 25 | fn assert_not_null_non_null_success() { 26 | let c_str = c_string!("foo"); 27 | assert_not_null!(c_str.as_ptr()); 28 | } 29 | 30 | #[test] 31 | #[should_panic] 32 | fn assert_not_null_null_panics() { 33 | assert_not_null!(ptr::null() as *const c_char); 34 | } 35 | 36 | #[test] 37 | fn c_string_success() { 38 | assert_eq!( 39 | c_string!("foo"), 40 | CString::new("foo").expect("could not create new CString") 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /zeroconf/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Crate prelude 2 | 3 | pub use crate::browser::TMdnsBrowser; 4 | pub use crate::event_loop::TEventLoop; 5 | pub use crate::service::TMdnsService; 6 | pub use crate::txt_record::TTxtRecord; 7 | 8 | /// Implements a `builder()` function for the specified type 9 | pub trait BuilderDelegate { 10 | /// Initializes a new default builder of type `T` 11 | fn builder() -> T { 12 | T::default() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /zeroconf/src/service.rs: -------------------------------------------------------------------------------- 1 | //! Trait definition for cross-platform service. 2 | 3 | use crate::{EventLoop, NetworkInterface, Result, ServiceType, TxtRecord}; 4 | use std::any::Any; 5 | use std::sync::Arc; 6 | 7 | /// Interface for interacting with underlying mDNS service implementation registration 8 | /// capabilities. 9 | pub trait TMdnsService { 10 | /// Creates a new `MdnsService` with the specified `ServiceType` (e.g. `_http._tcp`) and `port`. 11 | fn new(service_type: ServiceType, port: u16) -> Self; 12 | 13 | /// Sets the name to register this service under. 14 | fn set_name(&mut self, name: &str); 15 | 16 | /// Returns the name to register this service under. In some cases, the name of the service 17 | /// may be auto-assigned, in which case in may not be available until after registration. 18 | fn name(&self) -> Option<&str>; 19 | 20 | /// Sets the network interface to bind this service to. 21 | /// 22 | /// Most applications will want to use the default value `NetworkInterface::Unspec` to bind to 23 | /// all available interfaces. 24 | fn set_network_interface(&mut self, interface: NetworkInterface); 25 | 26 | /// Returns the network interface to bind this service to. 27 | fn network_interface(&self) -> NetworkInterface; 28 | 29 | /// Sets the domain on which to advertise the service. 30 | /// 31 | /// Most applications will want to use the default value of `ptr::null()` to register to the 32 | /// default domain. 33 | fn set_domain(&mut self, domain: &str); 34 | 35 | /// Returns the domain on which to advertise the service. 36 | fn domain(&self) -> Option<&str>; 37 | 38 | /// Sets the SRV target host name. 39 | /// 40 | /// Most applications will want to use the default value of `ptr::null()` to use the machine's 41 | /// default host name. 42 | fn set_host(&mut self, _host: &str); 43 | 44 | /// Returns the SRV target host name. 45 | fn host(&self) -> Option<&str>; 46 | 47 | /// Sets the optional `TxtRecord` to register this service with. 48 | fn set_txt_record(&mut self, txt_record: TxtRecord); 49 | 50 | /// Returns the optional `TxtRecord` to register this service with. 51 | fn txt_record(&self) -> Option<&TxtRecord>; 52 | 53 | /// Sets the [`ServiceRegisteredCallback`] that is invoked when the service has been 54 | /// registered. 55 | /// 56 | /// [`ServiceRegisteredCallback`]: ../type.ServiceRegisteredCallback.html 57 | fn set_registered_callback(&mut self, registered_callback: Box); 58 | 59 | /// Sets the optional user context to pass through to the callback. This is useful if you need 60 | /// to share state between pre and post-callback. The context type must implement `Any`. 61 | fn set_context(&mut self, context: Box); 62 | 63 | /// Returns the optional user context. 64 | fn context(&self) -> Option<&dyn Any>; 65 | 66 | /// Registers and start's the service. Returns an `EventLoop` which can be called to keep 67 | /// the service alive. 68 | fn register(&mut self) -> Result; 69 | } 70 | 71 | /// Callback invoked from [`MdnsService`] once it has successfully registered. 72 | /// 73 | /// # Arguments 74 | /// * `service` - The service information that was registered 75 | /// * `context` - The optional user context passed through 76 | /// 77 | /// [`MdnsService`]: type.MdnsService.html 78 | pub type ServiceRegisteredCallback = dyn Fn(Result, Option>); 79 | 80 | /// Represents a registration event for a [`MdnsService`]. 81 | /// 82 | /// [`MdnsService`]: type.MdnsService.html 83 | #[derive(Builder, BuilderDelegate, Debug, Getters, Clone, Default, PartialEq, Eq)] 84 | pub struct ServiceRegistration { 85 | name: String, 86 | service_type: ServiceType, 87 | domain: String, 88 | } 89 | -------------------------------------------------------------------------------- /zeroconf/src/service_type.rs: -------------------------------------------------------------------------------- 1 | //! Data type for constructing a service type 2 | 3 | use std::str::FromStr; 4 | 5 | use crate::{error::Error, Result}; 6 | 7 | /// Data type for constructing a service type to register as an mDNS service. 8 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 9 | #[derive(Default, Debug, Getters, Clone, PartialEq, Eq)] 10 | pub struct ServiceType { 11 | name: String, 12 | protocol: String, 13 | sub_types: Vec, 14 | } 15 | 16 | impl ServiceType { 17 | /// Creates a new `ServiceType` with the specified name (e.g. `http`) and protocol (e.g. `tcp`) 18 | pub fn new(name: &str, protocol: &str) -> Result { 19 | Ok(Self { 20 | name: check_valid_characters(name)?.to_string(), 21 | protocol: check_valid_characters(protocol)?.to_string(), 22 | sub_types: vec![], 23 | }) 24 | } 25 | 26 | /// Creates a new `ServiceType` with the specified name (e.g. `http`) and protocol (e.g. `tcp`) 27 | /// and sub-types. 28 | pub fn with_sub_types(name: &str, protocol: &str, sub_types: Vec<&str>) -> Result { 29 | Ok(Self { 30 | name: check_valid_characters(name)?.to_string(), 31 | protocol: check_valid_characters(protocol)?.to_string(), 32 | sub_types: sub_types 33 | .into_iter() 34 | .map(|s| check_valid_characters(s).map(|valid| valid.to_string())) 35 | .collect::>>()?, 36 | }) 37 | } 38 | } 39 | 40 | impl FromStr for ServiceType { 41 | type Err = Error; 42 | 43 | fn from_str(s: &str) -> Result { 44 | let parts = s.split('.').collect::>(); 45 | 46 | if parts.len() != 2 { 47 | return Err("invalid name and protocol".into()); 48 | } 49 | 50 | let name = lstrip_underscore(check_valid_characters(parts[0])?); 51 | let protocol = lstrip_underscore(check_valid_characters(parts[1])?); 52 | 53 | Self::new(name, protocol) 54 | } 55 | } 56 | 57 | pub fn check_valid_characters(part: &str) -> Result<&str> { 58 | if part.contains('.') { 59 | Err("invalid character: .".into()) 60 | } else if part.contains(',') { 61 | Err("invalid character: ,".into()) 62 | } else if part.is_empty() { 63 | Err("cannot be empty".into()) 64 | } else { 65 | Ok(part) 66 | } 67 | } 68 | 69 | pub fn lstrip_underscore(s: &str) -> &str { 70 | if let Some(stripped) = s.strip_prefix('_') { 71 | stripped 72 | } else { 73 | s 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::*; 80 | 81 | #[test] 82 | fn new_invalid() { 83 | ServiceType::new(".http", "tcp").expect_err("invalid character: ."); 84 | ServiceType::new("http", ".tcp").expect_err("invalid character: ."); 85 | ServiceType::new(",http", "tcp").expect_err("invalid character: ,"); 86 | ServiceType::new("http", ",tcp").expect_err("invalid character: ,"); 87 | ServiceType::new("", "tcp").expect_err("cannot be empty"); 88 | ServiceType::new("http", "").expect_err("cannot be empty"); 89 | } 90 | 91 | #[test] 92 | fn from_str_requires_two_parts() { 93 | ServiceType::from_str("_http").expect_err("invalid name and protocol"); 94 | ServiceType::from_str("_http._tcp._foo").expect_err("invalid name and protocol"); 95 | } 96 | 97 | #[test] 98 | fn from_str_success() { 99 | assert_eq!( 100 | ServiceType::from_str("_http._tcp").unwrap(), 101 | ServiceType::new("http", "tcp").unwrap() 102 | ); 103 | } 104 | 105 | #[test] 106 | fn check_valid_characters_returns_error_if_dot() { 107 | check_valid_characters("foo.bar").expect_err("invalid character: ."); 108 | } 109 | 110 | #[test] 111 | fn check_valid_characters_returns_error_if_comma() { 112 | check_valid_characters("foo,bar").expect_err("invalid character: ,"); 113 | } 114 | 115 | #[test] 116 | fn check_valid_characters_returns_error_if_empty() { 117 | check_valid_characters("").expect_err("cannot be empty"); 118 | } 119 | 120 | #[test] 121 | fn check_valid_characters_success() { 122 | assert_eq!(check_valid_characters("foo").unwrap(), "foo"); 123 | } 124 | 125 | #[test] 126 | fn lstrip_underscore_returns_stripped() { 127 | assert_eq!(lstrip_underscore("_foo"), "foo"); 128 | } 129 | 130 | #[test] 131 | fn lstrip_underscore_returns_original() { 132 | assert_eq!(lstrip_underscore("foo"), "foo"); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /zeroconf/src/tests/event_loop_test.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::{MdnsService, ServiceType, TxtRecord}; 3 | use std::time::{Duration, Instant}; 4 | 5 | const TEST_DURATION: Duration = Duration::from_secs(1); 6 | 7 | const FAST_SPIN_TIMEOUT: Duration = Duration::from_secs(0); 8 | const FAST_SPIN_MIN_ITERS: u32 = 10_000; 9 | 10 | const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(1); 11 | const LONG_POLL_MAX_ITERS: u32 = 100; 12 | 13 | #[test] 14 | fn event_loop_spins_fast() { 15 | super::setup(); 16 | 17 | static SERVICE_NAME: &str = "event_loop_test_service"; 18 | let mut service = MdnsService::new(ServiceType::new("http", "tcp").unwrap(), 8080); 19 | 20 | let mut txt = TxtRecord::new(); 21 | txt.insert("foo", "bar").unwrap(); 22 | 23 | service.set_name(SERVICE_NAME); 24 | service.set_txt_record(txt.clone()); 25 | service.set_registered_callback(Box::new(|_, _| { 26 | debug!("Service published"); 27 | })); 28 | 29 | let start = Instant::now(); 30 | let mut iterations = 0; 31 | let event_loop = service.register().unwrap(); 32 | loop { 33 | event_loop.poll(FAST_SPIN_TIMEOUT).unwrap(); 34 | 35 | if Instant::now().saturating_duration_since(start) >= TEST_DURATION { 36 | break; 37 | } 38 | iterations += 1; 39 | } 40 | 41 | println!( 42 | "service loop spun {} times in {} sec", 43 | iterations, 44 | TEST_DURATION.as_secs() 45 | ); 46 | 47 | assert!(iterations > FAST_SPIN_MIN_ITERS); 48 | } 49 | 50 | #[test] 51 | fn event_loop_long_polls() { 52 | super::setup(); 53 | 54 | static SERVICE_NAME: &str = "event_loop_test_service"; 55 | let mut service = MdnsService::new(ServiceType::new("http", "tcp").unwrap(), 8080); 56 | 57 | let mut txt = TxtRecord::new(); 58 | txt.insert("foo", "bar").unwrap(); 59 | 60 | service.set_name(SERVICE_NAME); 61 | service.set_txt_record(txt.clone()); 62 | service.set_registered_callback(Box::new(|_, _| { 63 | debug!("Service published"); 64 | })); 65 | 66 | let start = Instant::now(); 67 | let mut iterations = 0; 68 | let event_loop = service.register().unwrap(); 69 | loop { 70 | event_loop.poll(LONG_POLL_TIMEOUT).unwrap(); 71 | 72 | if Instant::now().saturating_duration_since(start) >= TEST_DURATION { 73 | break; 74 | } 75 | iterations += 1; 76 | } 77 | 78 | println!( 79 | "service loop spun {} times in {} sec", 80 | iterations, 81 | TEST_DURATION.as_secs() 82 | ); 83 | 84 | assert!(LONG_POLL_MAX_ITERS > iterations); 85 | } 86 | -------------------------------------------------------------------------------- /zeroconf/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | 3 | static INIT: Once = Once::new(); 4 | 5 | pub(crate) fn setup() { 6 | INIT.call_once(env_logger::init); 7 | } 8 | 9 | mod event_loop_test; 10 | mod service_test; 11 | -------------------------------------------------------------------------------- /zeroconf/src/tests/service_test.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::{BrowserEvent, MdnsBrowser, MdnsService, ServiceType, TxtRecord}; 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Duration; 5 | 6 | #[derive(Default, Debug)] 7 | struct Context { 8 | is_discovered: bool, 9 | timed_out: bool, 10 | txt: Option, 11 | } 12 | 13 | #[test] 14 | fn service_register_is_browsable() { 15 | super::setup(); 16 | 17 | const TOTAL_TEST_TIME_S: u64 = 30; 18 | static SERVICE_NAME: &str = "service_register_is_browsable"; 19 | 20 | let mut service = MdnsService::new( 21 | ServiceType::with_sub_types("http", "tcp", vec!["printer"]).unwrap(), 22 | 8080, 23 | ); 24 | 25 | let context: Arc> = Arc::default(); 26 | 27 | let mut txt = TxtRecord::new(); 28 | txt.insert("foo", "bar").unwrap(); 29 | 30 | service.set_name(SERVICE_NAME); 31 | service.set_context(Box::new(context.clone())); 32 | service.set_txt_record(txt.clone()); 33 | 34 | service.set_registered_callback(Box::new(|result, context| { 35 | assert!(result.is_ok()); 36 | 37 | let mut browser = 38 | MdnsBrowser::new(ServiceType::with_sub_types("http", "tcp", vec!["printer"]).unwrap()); 39 | 40 | let context = context 41 | .as_ref() 42 | .unwrap() 43 | .downcast_ref::>>() 44 | .unwrap() 45 | .clone(); 46 | 47 | browser.set_context(Box::new(context.clone())); 48 | 49 | browser.set_service_callback(Box::new(|event, context| match event.unwrap() { 50 | BrowserEvent::Add(service) => { 51 | if service.name() == SERVICE_NAME { 52 | let mut mtx = context 53 | .as_ref() 54 | .unwrap() 55 | .downcast_ref::>>() 56 | .unwrap() 57 | .lock() 58 | .unwrap(); 59 | 60 | mtx.txt.clone_from(service.txt()); 61 | mtx.is_discovered = true; 62 | 63 | debug!("Service discovered"); 64 | } 65 | } 66 | BrowserEvent::Remove(service) => { 67 | debug!( 68 | "Service removed: {}.{}.{}", 69 | service.name(), 70 | service.kind(), 71 | service.domain() 72 | ); 73 | } 74 | })); 75 | 76 | let event_loop = browser.browse_services().unwrap(); 77 | let browse_start = std::time::Instant::now(); 78 | 79 | loop { 80 | event_loop.poll(Duration::from_secs(0)).unwrap(); 81 | 82 | if context.lock().unwrap().is_discovered { 83 | break; 84 | } 85 | 86 | if browse_start.elapsed().as_secs() > TOTAL_TEST_TIME_S / 2 { 87 | context.lock().unwrap().timed_out = true; 88 | break; 89 | } 90 | } 91 | })); 92 | 93 | let event_loop = service.register().unwrap(); 94 | let publish_start = std::time::Instant::now(); 95 | 96 | loop { 97 | event_loop.poll(Duration::from_secs(0)).unwrap(); 98 | 99 | let mut mtx = context.lock().unwrap(); 100 | 101 | if mtx.is_discovered { 102 | assert_eq!(txt, mtx.txt.take().unwrap()); 103 | break; 104 | } 105 | 106 | if publish_start.elapsed().as_secs() > TOTAL_TEST_TIME_S { 107 | mtx.timed_out = true; 108 | break; 109 | } 110 | } 111 | 112 | assert!(!context.lock().unwrap().timed_out); 113 | } 114 | -------------------------------------------------------------------------------- /zeroconf/src/txt_record.rs: -------------------------------------------------------------------------------- 1 | //! TxtRecord utilities common to all platforms 2 | 3 | use crate::{Result, TxtRecord}; 4 | #[cfg(feature = "serde")] 5 | use serde::de::{MapAccess, Visitor}; 6 | #[cfg(feature = "serde")] 7 | use serde::ser::SerializeMap; 8 | #[cfg(feature = "serde")] 9 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 10 | use std::collections::HashMap; 11 | use std::fmt::{self, Debug}; 12 | #[cfg(feature = "serde")] 13 | use std::marker::PhantomData; 14 | 15 | /// Interface for interacting with underlying mDNS implementation TXT record capabilities 16 | pub trait TTxtRecord: Clone + PartialEq + Eq + Debug { 17 | /// Constructs a new TXT record 18 | fn new() -> Self; 19 | 20 | /// Inserts the specified value at the specified key. 21 | fn insert(&mut self, key: &str, value: &str) -> Result<()>; 22 | 23 | /// Returns the value at the specified key or `None` if no such key exists. 24 | /// 25 | /// This function returns an owned `String` because there are no guarantees that the 26 | /// implementation provides access to the underlying value pointer. 27 | fn get(&self, key: &str) -> Option; 28 | 29 | /// Removes the value at the specified key, returning the previous value if present. 30 | fn remove(&mut self, key: &str) -> Option; 31 | 32 | /// Returns true if the TXT record contains the specified key. 33 | fn contains_key(&self, key: &str) -> bool; 34 | 35 | /// Returns the amount of entries in the TXT record. 36 | fn len(&self) -> usize; 37 | 38 | /// Returns a new iterator for iterating over the record as you would a `HashMap`. 39 | fn iter<'a>(&'a self) -> Box + 'a>; 40 | 41 | /// Returns a new iterator over the records keys. 42 | fn keys<'a>(&'a self) -> Box + 'a>; 43 | 44 | /// Returns a new iterator over the records values. 45 | fn values<'a>(&'a self) -> Box + 'a>; 46 | 47 | /// Returns true if there are no entries in the record. 48 | fn is_empty(&self) -> bool { 49 | self.len() == 0 50 | } 51 | 52 | /// Returns a new `HashMap` with this record's keys and values. 53 | fn to_map(&self) -> HashMap { 54 | let mut m = HashMap::new(); 55 | for (key, value) in self.iter() { 56 | m.insert(key, value.to_string()); 57 | } 58 | m 59 | } 60 | } 61 | 62 | impl From> for TxtRecord { 63 | fn from(map: HashMap) -> TxtRecord { 64 | let mut record = TxtRecord::new(); 65 | for (key, value) in map { 66 | record 67 | .insert(&key, &value) 68 | .expect("could not insert key/value pair"); 69 | } 70 | record 71 | } 72 | } 73 | 74 | impl From> for TxtRecord { 75 | fn from(map: HashMap<&str, &str>) -> TxtRecord { 76 | map.iter() 77 | .map(|(k, v)| (k.to_string(), v.to_string())) 78 | .collect::>() 79 | .into() 80 | } 81 | } 82 | 83 | impl Eq for TxtRecord {} 84 | 85 | impl Default for TxtRecord { 86 | fn default() -> Self { 87 | Self::new() 88 | } 89 | } 90 | 91 | #[cfg(feature = "serde")] 92 | impl Serialize for TxtRecord { 93 | fn serialize(&self, serializer: S) -> std::result::Result 94 | where 95 | S: Serializer, 96 | { 97 | let mut map = serializer.serialize_map(Some(self.len()))?; 98 | for (key, value) in self.iter() { 99 | map.serialize_entry(&key, &value)?; 100 | } 101 | map.end() 102 | } 103 | } 104 | 105 | #[derive(new)] 106 | #[cfg(feature = "serde")] 107 | struct TxtRecordVisitor { 108 | marker: PhantomData TxtRecord>, 109 | } 110 | 111 | #[cfg(feature = "serde")] 112 | impl<'de> Visitor<'de> for TxtRecordVisitor { 113 | type Value = TxtRecord; 114 | 115 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 116 | formatter.write_str("map containing TXT record data") 117 | } 118 | 119 | fn visit_map(self, mut access: M) -> std::result::Result 120 | where 121 | M: MapAccess<'de>, 122 | { 123 | let mut map = TxtRecord::new(); 124 | 125 | while let Some((key, value)) = access.next_entry()? { 126 | map.insert(key, value) 127 | .expect("could not insert key/value pair"); 128 | } 129 | 130 | Ok(map) 131 | } 132 | } 133 | 134 | #[cfg(feature = "serde")] 135 | impl<'de> Deserialize<'de> for TxtRecord { 136 | fn deserialize(deserializer: D) -> std::result::Result 137 | where 138 | D: Deserializer<'de>, 139 | { 140 | deserializer.deserialize_map(TxtRecordVisitor::new()) 141 | } 142 | } 143 | 144 | impl Debug for TxtRecord { 145 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 146 | f.debug_struct("TxtRecord") 147 | .field("data", &self.to_map()) 148 | .finish() 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod tests { 154 | use super::*; 155 | use crate::TxtRecord; 156 | use std::collections::HashMap; 157 | 158 | #[test] 159 | fn insert_get_success() { 160 | crate::tests::setup(); 161 | let mut record = TxtRecord::new(); 162 | record.insert("foo", "bar").unwrap(); 163 | assert_eq!(record.get("foo").unwrap(), "bar"); 164 | assert_eq!(record.get("baz"), None); 165 | } 166 | 167 | #[test] 168 | fn get_miss_returns_none() { 169 | crate::tests::setup(); 170 | let record = TxtRecord::new(); 171 | assert_eq!(record.get("foo"), None); 172 | } 173 | 174 | #[test] 175 | fn remove_success() { 176 | crate::tests::setup(); 177 | let mut record = TxtRecord::new(); 178 | record.insert("foo", "bar").unwrap(); 179 | record.remove("foo").unwrap(); 180 | assert!(record.get("foo").is_none()); 181 | } 182 | 183 | #[test] 184 | fn remove_returns_previous_value() { 185 | crate::tests::setup(); 186 | let mut record = TxtRecord::new(); 187 | record.insert("foo", "bar").unwrap(); 188 | assert_eq!(record.remove("foo").unwrap(), "bar"); 189 | } 190 | 191 | #[test] 192 | fn remove_returns_none_if_missing() { 193 | crate::tests::setup(); 194 | let mut record = TxtRecord::new(); 195 | assert!(record.remove("foo").is_none()); 196 | } 197 | 198 | #[test] 199 | fn contains_key_success() { 200 | crate::tests::setup(); 201 | let mut record = TxtRecord::new(); 202 | record.insert("foo", "bar").unwrap(); 203 | assert!(record.contains_key("foo")); 204 | assert!(!record.contains_key("baz")); 205 | } 206 | 207 | #[test] 208 | fn len_success() { 209 | crate::tests::setup(); 210 | let mut record = TxtRecord::new(); 211 | record.insert("foo", "bar").unwrap(); 212 | assert_eq!(record.len(), 1); 213 | } 214 | 215 | #[test] 216 | fn len_returns_zero_if_empty() { 217 | crate::tests::setup(); 218 | let record = TxtRecord::new(); 219 | assert_eq!(record.len(), 0); 220 | } 221 | 222 | #[test] 223 | fn iter_success() { 224 | crate::tests::setup(); 225 | 226 | debug!("iter_success()"); 227 | 228 | let mut record = TxtRecord::new(); 229 | record.insert("foo", "bar").unwrap(); 230 | record.insert("baz", "qux").unwrap(); 231 | record.insert("hello", "world").unwrap(); 232 | 233 | for (key, value) in record.iter() { 234 | debug!("({:?}, {:?})", key, value); 235 | } 236 | } 237 | 238 | #[test] 239 | fn iter_works_if_empty() { 240 | crate::tests::setup(); 241 | 242 | let record = TxtRecord::new(); 243 | 244 | #[allow(clippy::never_loop)] 245 | for (key, value) in record.iter() { 246 | panic!("({:?}, {:?})", key, value); 247 | } 248 | } 249 | 250 | #[test] 251 | fn keys_success() { 252 | crate::tests::setup(); 253 | 254 | let mut record = TxtRecord::new(); 255 | record.insert("foo", "bar").unwrap(); 256 | record.insert("baz", "qux").unwrap(); 257 | record.insert("hello", "world").unwrap(); 258 | 259 | for key in record.keys() { 260 | debug!("{:?}", key); 261 | } 262 | } 263 | 264 | #[test] 265 | fn values_success() { 266 | crate::tests::setup(); 267 | 268 | let mut record = TxtRecord::new(); 269 | record.insert("foo", "bar").unwrap(); 270 | record.insert("baz", "qux").unwrap(); 271 | record.insert("hello", "world").unwrap(); 272 | 273 | for value in record.values() { 274 | debug!("{:?}", value); 275 | } 276 | } 277 | 278 | #[test] 279 | fn is_empty_success() { 280 | crate::tests::setup(); 281 | 282 | let mut record = TxtRecord::new(); 283 | assert!(record.is_empty()); 284 | 285 | record.insert("foo", "bar").unwrap(); 286 | assert!(!record.is_empty()); 287 | } 288 | 289 | #[test] 290 | fn from_hashmap_success() { 291 | crate::tests::setup(); 292 | 293 | let mut map = HashMap::new(); 294 | map.insert("foo", "bar"); 295 | 296 | let record: TxtRecord = map.into(); 297 | 298 | assert_eq!(record.get("foo").unwrap(), "bar"); 299 | } 300 | 301 | #[test] 302 | fn clone_success() { 303 | crate::tests::setup(); 304 | 305 | let mut record = TxtRecord::new(); 306 | record.insert("foo", "bar").unwrap(); 307 | 308 | assert_eq!(record.clone(), record); 309 | } 310 | 311 | #[test] 312 | #[cfg(feature = "serde")] 313 | fn serialize_success() { 314 | crate::tests::setup(); 315 | 316 | let mut txt = TxtRecord::new(); 317 | txt.insert("foo", "bar").unwrap(); 318 | 319 | let json = serde_json::to_string(&txt).unwrap(); 320 | let txt_de: TxtRecord = serde_json::from_str(&json).unwrap(); 321 | 322 | assert_eq!(txt, txt_de); 323 | } 324 | } 325 | --------------------------------------------------------------------------------