aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock691
-rw-r--r--Cargo.toml10
-rw-r--r--src/config.rs69
-rw-r--r--src/main.rs515
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"],
+ );
+ }
+}