diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 691 | ||||
-rw-r--r-- | Cargo.toml | 10 | ||||
-rw-r--r-- | src/config.rs | 69 | ||||
-rw-r--r-- | src/main.rs | 515 |
5 files changed, 1286 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7acfd90 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,691 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mqttt" +version = "0.1.0" +dependencies = [ + "anyhow", + "rumqttc", + "serde", + "toml", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rumqttc" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1568e15fab2d546f940ed3a21f48bbbd1c494c90c99c4481339364a497f94a9" +dependencies = [ + "bytes", + "flume", + "futures-util", + "log", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki", + "thiserror", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..de0c66e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mqttt" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.98" +rumqttc = "0.24.0" +serde = { version = "1.0.219", features = ["derive"] } +toml = { version = "0.8.22", default-features = false, features = ["parse"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9000f01 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,69 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + process, + time::Duration, +}; + +use rumqttc::{Client, Connection, MqttOptions}; +use serde::Deserialize; + +use crate::PROGRAM; + +#[derive(Deserialize)] +pub struct Credentials { + pub username: String, + pub password: String, +} + +fn default_root() -> PathBuf { + option_env!("DEFAULT_ROOT") + .unwrap_or(&format!("/var/run/{PROGRAM}")) + .into() +} + +fn default_host() -> String { + "localhost".to_string() +} + +fn default_port() -> u16 { + 1883 +} + +fn default_id() -> String { + PROGRAM.to_string() +} + +#[derive(Deserialize)] +pub struct Config { + #[serde(default = "default_root")] + pub root: PathBuf, + #[serde(default = "default_host")] + pub host: String, + #[serde(default = "default_port")] + pub port: u16, + pub credentials: Option<Credentials>, + #[serde(default = "default_id")] + pub id: String, +} + +impl Config { + pub fn mqtt_client(&self) -> (Client, Connection) { + let client_id = format!("{}_{}", self.id, process::id()); + let mut options = MqttOptions::new(client_id, &self.host, self.port); + if let Some(credentials) = &self.credentials { + options.set_credentials(&credentials.username, &credentials.password); + } + options.set_keep_alive(Duration::from_secs(5)); + Client::new(options, 10) + } +} + +pub fn load<P: AsRef<Path>>(path: P) -> anyhow::Result<Config> { + let config = match fs::read_to_string(&path) { + Ok(s) => Ok(s), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(String::new()), + Err(e) => Err(e), + }?; + Ok(toml::from_str(&config)?) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6ccafba --- /dev/null +++ b/src/main.rs @@ -0,0 +1,515 @@ +use std::{ + collections::HashMap, + fs, + io::Write, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::Context; +use rumqttc::{Event::Incoming, Packet::Publish, QoS}; + +mod config; + +const PROGRAM: &str = "mqttt"; + +struct Node { + children: HashMap<String, Node>, + executables: Box<[PathBuf]>, +} + +fn is_executable<P: AsRef<Path>>(path: P) -> std::io::Result<bool> { + Ok(path.as_ref().metadata()?.permissions().mode() & 0o111 != 0) +} + +impl Node { + fn build<P: AsRef<Path>>(path: P) -> std::io::Result<Node> { + let mut children = HashMap::new(); + let mut executables = Vec::new(); + + for entry in fs::read_dir(&path)? { + let entry = entry?; + let path = entry.path(); + let name = match entry.file_name().into_string() { + Ok(name) => name, + Err(_) => { + eprintln!("warning: Path '{path:?}' is not valid UTF-8. Skipping..."); + continue; + } + }; + + if path.is_dir() { + let child = Node::build(&path)?; + children.insert(name, child); + } else if path.is_file() && is_executable(&path)? { + executables.push(path); + } + } + + Ok(Node { + children, + executables: executables.into_boxed_slice(), + }) + } + + fn traverse<F: FnMut(&str, &Node)>(&self, prefix: &str, f: &mut F) { + f(prefix, &self); + for (name, child) in &self.children { + let sep = if prefix != "" { "/" } else { "" }; + let name = if name != "#empty" { &name } else { "" }; + let path = format!("{prefix}{sep}{name}"); + child.traverse(&path, f); + } + } + + fn publish<F: FnMut(&Node)>(&self, path: &str, f: &mut F) { + let path = if path == "" { None } else { Some(path) }; + let is_sys = path.is_some_and(|p| p.starts_with("$")); + self.publish_impl(path, f, is_sys); + } + + fn publish_impl<F: FnMut(&Node)>(&self, path: Option<&str>, f: &mut F, is_sys: bool) { + let Some(path) = path else { + f(&self); + if let Some(child) = self.children.get("#") { + f(&child); + } + return; + }; + let (front, rest) = match path.split_once('/') { + Some((front, rest)) => (front, Some(rest)), + None => (path, None), + }; + if let Some(child) = self.children.get(front) { + child.publish_impl(rest, f, false); + } + if !is_sys { + if let Some(child) = self.children.get("+") { + child.publish_impl(rest, f, false); + } + if let Some(child) = self.children.get("#") { + child.publish_impl(None, f, false); + } + } + } +} + +fn main() -> anyhow::Result<()> { + let mut conf_path: PathBuf = option_env!("SYSCONFDIR").unwrap_or("/usr/local/etc").into(); + conf_path.push(format!("{PROGRAM}.toml")); + let conf = config::load(&conf_path) + .with_context(|| format!("Failed to load config: {:?}", &conf_path))?; + let root = Node::build(&conf.root).context("Failed to build tree")?; + let (client, mut connection) = conf.mqtt_client(); + root.traverse("", &mut |path, node| { + if node.executables.len() != 0 { + if let Err(e) = client.subscribe(path, QoS::AtMostOnce) { + eprintln!("warning: Failed to subscribe {path}: {e:?}"); + } else { + println!("Subscribed to {path}"); + } + } + }); + for notification in connection.iter() { + match notification? { + Incoming(Publish(p)) => root.publish(&p.topic, &mut |node| { + for e in &node.executables { + let mut proc = Command::new(e) + .args([&p.topic]) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + let stdin = proc.stdin.as_mut().unwrap(); + stdin.write_all(&p.payload).unwrap(); + println!("{}", proc.wait().unwrap()); + } + }), + _ => (), + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::Node; + use std::collections::HashMap; + use std::path::PathBuf; + + fn node(id_str: Option<&str>, children_data: Vec<(&str, Node)>) -> Node { + let executables = id_str + .map(|s| vec![PathBuf::from(s)].into_boxed_slice()) + .unwrap_or_default(); + + let children: HashMap<String, Node> = children_data + .into_iter() + .map(|(name, child_node)| (name.to_string(), child_node)) + .collect(); + + Node { + children, + executables, + } + } + + fn assert_publish_ids(root_node: &Node, publish_path: &str, expected_ids_str: &[&str]) { + let mut actual_ids: Vec<PathBuf> = Vec::new(); + root_node.publish(publish_path, &mut |n| { + if let Some(id) = n.executables.get(0) { + actual_ids.push(id.clone()); + } + }); + + assert_eq!( + actual_ids.len(), + expected_ids_str.len(), + "Path '{}': Number of called IDs ({}) does not match expected ({}). Actual: {:?}, Expected: {:?}", + publish_path, + actual_ids.len(), + expected_ids_str.len(), + actual_ids, + expected_ids_str + ); + for (i, expected_id_str) in expected_ids_str.iter().enumerate() { + assert_eq!( + actual_ids[i], + PathBuf::from(expected_id_str), + "Path '{}': Called ID at index {} does not match. Actual: {:?}, Expected: {:?}", + publish_path, + i, + actual_ids[i], + expected_id_str + ); + } + } + + #[test] + fn topic_single_segment_exact() { + assert_publish_ids( + &node(None, vec![("a", node(Some("a_id"), vec![]))]), + "a", + &["a_id"], + ); + } + + #[test] + fn topic_multi_segment_exact() { + assert_publish_ids( + &node( + None, + vec![("a", node(None, vec![("b", node(Some("b_id"), vec![]))]))], + ), + "a/b", + &["b_id"], + ); + } + + #[test] + fn topic_single_segment_plus_wildcard() { + assert_publish_ids( + &node(None, vec![("+", node(Some("plus_id"), vec![]))]), + "unknown", + &["plus_id"], + ); + } + + #[test] + fn topic_single_segment_hash_literal_wildcard() { + assert_publish_ids( + &node(None, vec![("#", node(Some("hash_literal_id"), vec![]))]), + "unknown", + &["hash_literal_id"], + ); + } + + #[test] + fn topic_multi_segment_hash_literal_wildcard() { + assert_publish_ids( + &node(None, vec![("#", node(Some("hash_literal_id"), vec![]))]), + "unknown/unknown", + &["hash_literal_id"], + ); + } + + #[test] + fn topic_single_segment_all_match_types() { + assert_publish_ids( + &node( + None, + vec![ + ("data", node(Some("data_id"), vec![])), + ("+", node(Some("plus_id"), vec![])), + ("#", node(Some("hash_literal_id"), vec![])), + ], + ), + "data", + &["data_id", "plus_id", "hash_literal_id"], + ); + } + + #[test] + fn topic_multi_segment_all_match_types() { + assert_publish_ids( + &node( + None, + vec![ + ( + "data", + node( + None, + vec![ + ("data", node(Some("data_id"), vec![])), + ("+", node(Some("plus_id"), vec![])), + ], + ), + ), + ("#", node(Some("hash_literal_id"), vec![])), + ], + ), + "data/data", + &["data_id", "plus_id", "hash_literal_id"], + ); + } + + #[test] + fn topic_path_ends_triggers_base_case_hash_wildcard_child() { + let root = node( + None, + vec![( + "a", + node(Some("a_id"), vec![("#", node(Some("a_hash_id"), vec![]))]), + )], + ); + assert_publish_ids(&root, "a", &["a_id", "a_hash_id"]); + } + + #[test] + fn topic_no_match_for_segment() { + let root = node(None, vec![("known", node(Some("known_id"), vec![]))]); + assert_publish_ids(&root, "unknown", &[]); + } + + #[test] + fn topic_path_deeper_than_tree() { + let root = node( + None, + vec![("a", node(None, vec![("b", node(Some("b_id"), vec![]))]))], + ); + assert_publish_ids(&root, "a/b/c", &[]); + } + + #[test] + fn topic_trailing_slash_maps_to_empty_key_child() { + let root = node( + None, + vec![( + "a", + node(None, vec![("", node(Some("a_empty_key_id"), vec![]))]), + )], + ); + assert_publish_ids(&root, "a/", &["a_empty_key_id"]); + } + + #[test] + fn topic_multi_trailing_slash_maps_to_empty_key_child() { + let root = node( + None, + vec![( + "a", + node( + None, + vec![( + "b", + node(None, vec![("", node(Some("b_empty_key_id"), vec![]))]), + )], + ), + )], + ); + assert_publish_ids(&root, "a/b/", &["b_empty_key_id"]); + } + + #[test] + fn topic_trailing_slash_plus_wildcard_for_empty_key() { + // a/ -> a/(+ -> "") + let root = node( + None, + vec![( + "a", + node( + None, + vec![("+", node(Some("a_plus_for_empty_key"), vec![]))], + ), + )], + ); + assert_publish_ids(&root, "a/", &["a_plus_for_empty_key"]); + } + + #[test] + fn topic_a_double_slash_b() { + let root = node( + None, + vec![( + "a", + node( + None, + vec![("", node(None, vec![("b", node(Some("b_id"), vec![]))]))], + ), + )], + ); + assert_publish_ids(&root, "a//b", &["b_id"]); + } + + #[test] + fn topic_a_triple_slash_b() { + let root = node( + None, + vec![( + "a", + node( + None, + vec![( + "", + node( + None, + vec![("", node(None, vec![("b", node(Some("b_id"), vec![]))]))], + ), + )], + ), + )], + ); + assert_publish_ids(&root, "a///b", &["b_id"]); + } + + #[test] + fn topic_root_single_slash() { + let root = node( + None, + vec![( + "", + node(None, vec![("", node(Some("via_single_slash"), vec![]))]), + )], + ); + assert_publish_ids(&root, "/", &["via_single_slash"]); + } + + #[test] + fn topic_root_double_slash() { + let root = node( + None, + vec![( + "", + node( + None, + vec![( + "", + node(None, vec![("", node(Some("via_double_slash"), vec![]))]), + )], + ), + )], + ); + assert_publish_ids(&root, "//", &["via_double_slash"]); + } + + #[test] + fn topic_leading_double_slash_a() { + let root = node( + None, + vec![( + "", + node( + None, + vec![("", node(None, vec![("a", node(Some("a_id"), vec![]))]))], + ), + )], + ); + assert_publish_ids(&root, "//a", &["a_id"]); + } + + #[test] + fn topic_trailing_double_slash_a() { + let root = node( + None, + vec![( + "a", + node( + None, + vec![("", node(None, vec![("", node(Some("empty_id"), vec![]))]))], + ), + )], + ); + assert_publish_ids(&root, "a//", &["empty_id"]); + } + + #[test] + fn topic_a_double_slash_b_with_plus_wildcards() { + let root = node( + None, + vec![( + "a", + node( + None, + vec![("+", node(None, vec![("b", node(Some("b_id"), vec![]))]))], + ), + )], + ); + assert_publish_ids(&root, "a//b", &["b_id"]); + } + + #[test] + fn topic_a_trailing_slash_with_base_case_hash_on_empty_key_node() { + let root = node( + None, + vec![( + "a", + node( + None, + vec![( + "", + node( + Some("a_empty_key_id"), + vec![("#", node(Some("a_empty_key_hash_id"), vec![]))], + ), + )], + ), + )], + ); + assert_publish_ids(&root, "a/", &["a_empty_key_id", "a_empty_key_hash_id"]); + } + + #[test] + fn sys_topic_only_prefixed() { + assert_publish_ids( + &node( + None, + vec![ + ( + "$SYS", + node( + None, + vec![ + ("foo", node(Some("sys_foo_id"), vec![])), + ("#", node(Some("sys_hash_id"), vec![])), + ("+", node(Some("sys_plus_id"), vec![])), + ], + ), + ), + ("#", node(Some("hash_id"), vec![])), + ( + "+", + node( + None, + vec![ + ("foo", node(Some("plus_food_id"), vec![])), + ("#", node(Some("plus_hash_id"), vec![])), + ("+", node(Some("plus_plus_id"), vec![])), + ], + ), + ), + ], + ), + "$SYS/foo", + &["sys_foo_id", "sys_plus_id", "sys_hash_id"], + ); + } +} |