Compare commits

...

10 commits

Author SHA1 Message Date
b79a6af150 Update dependencies; remove usage of flatten() 2024-01-06 23:08:24 -05:00
292cdecd87 Release 0.6.1 2023-08-14 23:43:38 -04:00
Hubert Pawlak
8712a6087b
fix: Prevent panic caused by subtracting with overflow (#36)
This bug occurred if the string was empty (e.g. from a lost connection)
2023-08-14 09:31:16 -04:00
8ef85f3b28 Release 0.6.0; edition 2021 2023-07-01 15:57:28 -04:00
0b69b3fc47 Release 0.5.3 2022-08-20 11:23:45 -04:00
3e70c36b85 make proto module private for now 2022-08-20 11:22:57 -04:00
Rajasekharan Vengalil
821414d9cd
Add support for running commands (#33) 2022-08-08 10:25:47 -04:00
35d40d3111 Add all remaining errors 2021-08-04 02:53:00 -04:00
1842ebef15 Add util for joining and splitting words 2021-08-04 02:30:19 -04:00
Aram Peres
1e3481e18d
Start work on generic protocol impl for #28 (#29) 2021-08-04 02:11:44 -04:00
18 changed files with 1887 additions and 138 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
/.idea
/.vscode

336
Cargo.lock generated
View file

@ -3,19 +3,34 @@
version = 3
[[package]]
name = "ansi_term"
version = "0.11.0"
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.42"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "atty"
@ -23,46 +38,58 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "bitflags"
version = "1.2.1"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.7.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytes"
version = "1.0.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cc"
version = "1.0.69"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
@ -72,9 +99,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "2.33.3"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
@ -85,6 +112,12 @@ dependencies = [
"vec_map",
]
[[package]]
name = "gimli"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -95,108 +128,103 @@ dependencies = [
]
[[package]]
name = "js-sys"
version = "0.3.51"
name = "hermit-abi"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "js-sys"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.98"
version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
[[package]]
name = "log"
version = "0.4.14"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.4.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.7.13"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
"wasi",
"windows-sys",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.3",
"libc",
]
[[package]]
name = "once_cell"
version = "1.8.0"
name = "object"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "proc-macro2"
version = "1.0.28"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [
"unicode-xid",
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.9"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -218,7 +246,7 @@ dependencies = [
[[package]]
name = "rups"
version = "0.5.2"
version = "0.6.1"
dependencies = [
"rustls",
"shell-words",
@ -230,13 +258,19 @@ dependencies = [
[[package]]
name = "rupsc"
version = "0.5.2"
version = "0.6.1"
dependencies = [
"anyhow",
"clap",
"rups",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustls"
version = "0.19.1"
@ -262,9 +296,19 @@ dependencies = [
[[package]]
name = "shell-words"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "socket2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "spin"
@ -280,13 +324,13 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "syn"
version = "1.0.74"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
"unicode-ident",
]
[[package]]
@ -300,26 +344,26 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.9.0"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c"
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
dependencies = [
"autocfg",
"backtrace",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"pin-project-lite",
"socket2",
"tokio-macros",
"winapi",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "1.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@ -338,16 +382,16 @@ dependencies = [
]
[[package]]
name = "unicode-width"
version = "0.1.8"
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-xid"
version = "0.2.2"
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "untrusted"
@ -362,10 +406,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@ -373,13 +423,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.74"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
@ -388,9 +438,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.74"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -398,9 +448,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.74"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
@ -411,15 +461,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.74"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "web-sys"
version = "0.3.51"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -465,3 +515,69 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

View file

@ -53,8 +53,7 @@ fn main() -> nut_client::Result<()> {
let host = env::var("NUT_HOST").unwrap_or_else(|_| "localhost".into());
let port = env::var("NUT_PORT")
.ok()
.map(|s| s.parse::<u16>().ok())
.flatten()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(3493);
let username = env::var("NUT_USER").ok();

View file

@ -1,8 +1,8 @@
[package]
name = "rups"
version = "0.5.2"
version = "0.6.1"
authors = ["Aram Peres <aram.peres@wavy.fm>"]
edition = "2018"
edition = "2021"
description = "Network UPS Tools (NUT) client library"
categories = ["network-programming"]
keywords = ["ups", "nut", "tokio", "async"]
@ -26,6 +26,7 @@ default = []
ssl = ["rustls", "rustls/dangerous_configuration", "webpki", "webpki-roots"]
async = ["tokio"]
async-ssl = ["async", "tokio-rustls", "ssl"]
write = []
# a feature gate for examples
async-rt = ["async", "tokio/rt-multi-thread", "tokio/macros"]

View file

@ -9,8 +9,7 @@ async fn main() -> rups::Result<()> {
let host = env::var("NUT_HOST").unwrap_or_else(|_| "localhost".into());
let port = env::var("NUT_PORT")
.ok()
.map(|s| s.parse::<u16>().ok())
.flatten()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(3493);
let username = env::var("NUT_USER").ok();

View file

@ -8,8 +8,7 @@ fn main() -> rups::Result<()> {
let host = env::var("NUT_HOST").unwrap_or_else(|_| "localhost".into());
let port = env::var("NUT_PORT")
.ok()
.map(|s| s.parse::<u16>().ok())
.flatten()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(3493);
let username = env::var("NUT_USER").ok();

View file

@ -138,7 +138,7 @@ impl TcpConnection {
if debug {
eprint!("DEBUG <- {}", raw);
}
raw = raw[..raw.len() - 1].to_string(); // Strip off \n
raw = raw.trim_end_matches('\n').to_string(); // Strip off \n
// Parse args by splitting whitespace, minding quotes for args with multiple words
let args = shell_words::split(&raw)

View file

@ -18,6 +18,9 @@ pub enum Command<'a> {
NetworkVersion,
/// Queries the server version.
Version,
#[cfg(feature = "write")]
/// Run a command. Allow for on additional optional param.
Run(&'a str, Option<&'a str>),
/// Gracefully shuts down the connection.
Logout,
}
@ -33,6 +36,8 @@ impl<'a> Command<'a> {
Self::StartTLS => "STARTTLS",
Self::NetworkVersion => "NETVER",
Self::Version => "VER",
#[cfg(feature = "write")]
Self::Run(_, _) => "INSTCMD",
Self::Logout => "LOGOUT",
}
}
@ -44,6 +49,10 @@ impl<'a> Command<'a> {
Self::SetUsername(username) => vec![username],
Self::SetPassword(password) => vec![password],
Self::List(query) => query.to_vec(),
#[cfg(feature = "write")]
Self::Run(cmd, param) => param
.map(|param| vec![*cmd, param])
.unwrap_or_else(|| vec![cmd]),
_ => Vec::new(),
}
}
@ -826,7 +835,7 @@ implement_simple_commands! {
pub fn get_network_version() -> String {
(
{ Command::NetworkVersion },
{ |row: String| Ok(row) },
{ Ok },
)
}
@ -834,7 +843,7 @@ implement_simple_commands! {
pub fn get_server_version() -> String {
(
{ Command::Version },
{ |row: String| Ok(row) },
{ Ok },
)
}
}
@ -855,3 +864,31 @@ implement_action_commands! {
Command::Logout
}
}
#[cfg(feature = "write")]
impl crate::blocking::Connection {
/// Runs a command on the UPS.
pub fn run_command(&mut self, cmd: &str, param: Option<&str>) -> crate::Result<()> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::Run(cmd, param))?;
conn.read_response()?.expect_ok()?;
Ok(())
}
}
}
}
#[cfg(all(feature = "write", feature = "async"))]
impl crate::tokio::Connection {
/// Runs a command on the UPS.
pub async fn run_command(&mut self, cmd: &str, param: Option<&str>) -> crate::Result<()> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::Run(cmd, param)).await?;
conn.read_response().await?.expect_ok()?;
Ok(())
}
}
}
}

View file

@ -1,3 +1,4 @@
use crate::proto::ClientSentences;
use core::fmt;
use std::io;
@ -8,6 +9,48 @@ pub enum NutError {
AccessDenied,
/// Occurs when the specified UPS device does not exist.
UnknownUps,
/// The specified UPS doesn't support the variable in the request.
VarNotSupported,
/// The specified UPS doesn't support the instant command in the request.
CmdNotSupported,
/// The client sent an argument to a command which is not recognized or is otherwise invalid in this context.
InvalidArgument,
/// Server failed to deliver the instant command request to the driver. No further information is available to the client.
InstCmdFailed,
/// Server failed to deliver the set request to the driver.
SetFailed,
/// The requested variable in a SET command is not writable.
ReadOnly,
/// The requested value in a SET command is too long.
TooLong,
/// The server does not support the requested feature.
FeatureNotSupported,
/// TLS/SSL mode is already enabled on this connection, so the server can't start it again.
AlreadySslMode,
/// The server can't perform the requested command, since the driver for that UPS is not connected.
DriverNotConnected,
/// Server is connected to the driver for the UPS, but that driver isn't providing regular updates
/// or has specifically marked the data as stale.
DataStale,
/// The client already sent LOGIN for a UPS and can't do it again.
/// There is presently a limit of one LOGIN record per connection.
AlreadyLoggedIn,
/// The client sent an invalid PASSWORD - perhaps an empty one.
InvalidPassword,
/// The client already set a PASSWORD and can't set another.
AlreadySetPassword,
/// The client sent an invalid USERNAME.
InvalidUsername,
/// The client has already set a USERNAME, and can't set another.
AlreadySetUsername,
/// The requested command requires a username for authentication, but the client hasn't set one.
UsernameRequired,
/// The requested command requires a password for authentication, but the client hasn't set one.
PasswordRequired,
/// The server doesn't recognize the requested command.
UnknownCommand,
/// The value specified in the request is not valid.
InvalidValue,
/// Occurs when the response type or content wasn't expected at the current stage.
UnexpectedResponse,
/// Occurs when the response type is not recognized by the client.
@ -26,8 +69,28 @@ pub enum NutError {
impl fmt::Display for NutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AccessDenied => write!(f, "Authentication failed"),
Self::UnknownUps => write!(f, "Unknown UPS device name"),
Self::AccessDenied => write!(f, "Access denied"),
Self::UnknownUps => write!(f, "Unknown UPS device"),
Self::VarNotSupported => write!(f, "Variable not supported"),
Self::CmdNotSupported => write!(f, "Command not supported"),
Self::InvalidArgument => write!(f, "Invalid argument"),
Self::InstCmdFailed => write!(f, "Instant command failed"),
Self::SetFailed => write!(f, "Failed to set variable"),
Self::ReadOnly => write!(f, "Cannot set read-only variable"),
Self::TooLong => write!(f, "Value is too long"),
Self::FeatureNotSupported => write!(f, "Feature is not supported by server"),
Self::AlreadySslMode => write!(f, "Connection is already in TLS/SSL"),
Self::DriverNotConnected => write!(f, "Driver is not connected"),
Self::DataStale => write!(f, "Data is stale"),
Self::AlreadyLoggedIn => write!(f, "Connection is already authenticated"),
Self::InvalidPassword => write!(f, "Invalid password"),
Self::AlreadySetPassword => write!(f, "Password can only be set once"),
Self::InvalidUsername => write!(f, "Invalid username"),
Self::AlreadySetUsername => write!(f, "Username can only be set once"),
Self::UsernameRequired => write!(f, "Username required"),
Self::PasswordRequired => write!(f, "Password required"),
Self::UnknownCommand => write!(f, "Unknown command"),
Self::InvalidValue => write!(f, "Invalid value"),
Self::UnexpectedResponse => write!(f, "Unexpected server response content"),
Self::UnknownResponseType(ty) => write!(f, "Unknown response type: {}", ty),
Self::SslNotSupported => write!(f, "SSL not supported by server or transport"),
@ -41,6 +104,42 @@ impl fmt::Display for NutError {
}
}
impl<T: AsRef<ClientSentences>> From<T> for NutError {
fn from(sentence: T) -> Self {
if let ClientSentences::RespondErr { message, .. } = sentence.as_ref() {
match message.as_str() {
"ACCESS-DENIED" => Self::AccessDenied,
"UNKNOWN-UPS" => Self::UnknownUps,
"VAR-NOT-SUPPORTED" => Self::VarNotSupported,
"CMD-NOT-SUPPORTED" => Self::CmdNotSupported,
"INVALID-ARGUMENT" => Self::InvalidArgument,
"INSTCMD-FAILED" => Self::InstCmdFailed,
"SET-FAILED" => Self::SetFailed,
"READONLY" => Self::ReadOnly,
"TOO-LONG" => Self::TooLong,
"FEATURE-NOT-SUPPORTED" => Self::FeatureNotSupported,
"FEATURE-NOT-CONFIGURED" => Self::FeatureNotConfigured,
"ALREADY-SSL-MODE" => Self::AlreadySslMode,
"DRIVER-NOT-CONNECTED" => Self::DriverNotConnected,
"DATA-STALE" => Self::DataStale,
"ALREADY-LOGGED-IN" => Self::AlreadyLoggedIn,
"INVALID-PASSWORD" => Self::InvalidPassword,
"ALREADY-SET-PASSWORD" => Self::AlreadySetPassword,
"INVALID-USERNAME" => Self::InvalidUsername,
"ALREADY-SET-USERNAME" => Self::AlreadySetUsername,
"USERNAME-REQUIRED" => Self::UsernameRequired,
"PASSWORD-REQUIRED" => Self::PasswordRequired,
"UNKNOWN-COMMAND" => Self::UnknownCommand,
"INVALID-VALUE" => Self::InvalidValue,
_ => Self::Generic(message.to_string()),
}
} else {
// This is not supposed to happen...
panic!("Cannot convert {:?} into NutError", sentence.as_ref());
}
}
}
impl NutError {
/// Constructs a generic rups error.
pub fn generic<T: ToString>(message: T) -> Self {

View file

@ -12,6 +12,12 @@ pub use var::*;
/// Blocking client implementation for NUT.
pub mod blocking;
/// NUT protocol implementation (v1.2).
///
/// Reference: <https://networkupstools.org/docs/developer-guide.chunked/ar01s09.html>
#[allow(dead_code)]
#[macro_use]
pub(crate) mod proto;
/// Async client implementation for NUT, using Tokio.
#[cfg(feature = "async")]
pub mod tokio;

666
rups/src/proto/client.rs Normal file
View file

@ -0,0 +1,666 @@
use crate::proto::impl_sentences;
impl_sentences! {
/// A generic successful response with no additional data.
GenericOk (
{
0: Ok,
1: EOL,
},
{}
),
/// Forced shut down (FSD) successful.
FsdOk (
{
0: Ok,
1: FsdSet,
2: EOL,
},
{}
),
/// Server acknowledges TLS upgrade.
StartTLSOk (
{
0: Ok,
1: StartTLS,
2: EOL,
},
{}
),
/// Server confirms logout.
LogoutOk (
{
0: Ok,
1: Goodbye,
},
{}
),
/// Server returns an error.
RespondErr (
{
0: Err,
1: Arg,
},
{
/// The error code.
1: message,
},
{
/// Extra information about the error.
2...: extras
}
),
/// Server responds with the number of prior logins to the given `ups_name` device.
RespondNumLogins (
{
0: NumLogins,
1: Arg,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The number of logins to the UPS device.
2: num_logins,
}
),
/// Server responds with the description of the UPS device.
RespondUpsDesc (
{
0: UpsDesc,
1: Arg,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The description of the UPS device.
2: description,
}
),
/// Server responds with the value of the given `var_name` variable for the UPS device.
RespondVar (
{
0: Var,
1: Arg,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the variable.
2: var_name,
/// The current value of the variable.
3: value,
}
),
/// Server responds with the type of the given `var_name` variable for the UPS device.
RespondType (
{
0: Type,
1: Arg,
2: Arg,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the variable.
2: var_name,
},
{
/// The variable definition (RW, ENUN, STRING...)
3...: var_types
}
),
/// Server responds with the description of the given `var_name` variable for the UPS device.
RespondDesc (
{
0: Desc,
1: Arg,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the variable.
2: var_name,
/// The description of the variable.
3: description,
}
),
/// Server responds with the description of the given `cmd_name` command for the UPS device.
RespondCmdDesc (
{
0: CmdDesc,
1: Arg,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the command.
2: cmd_name,
/// The description of the command.
3: description,
}
),
/// Server responds with the name and description of a UPS device.
RespondUps (
{
0: Ups,
1: Arg,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the command.
2: description,
}
),
/// Server responds with the name and description of a mutable variable.
RespondRw (
{
0: Rw,
1: Arg,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the variable.
2: var_name,
/// The current value of the variable.
3: value,
}
),
/// Server responds with the name of a command.
RespondCmd (
{
0: Cmd,
1: Arg,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the command.
2: cmd_name,
}
),
/// Server responds with a possible value of an enumerable variable.
RespondEnum (
{
0: Enum,
1: Arg,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the variable.
2: var_name,
/// A possible value of the variable.
3: enum_value,
}
),
/// Server responds with a possible range of an numeric variable.
RespondRange (
{
0: Range,
1: Arg,
2: Arg,
3: Arg,
4: Arg,
5: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the variable.
2: var_name,
/// The minimum value of the range.
3: min_value,
/// The maximum value of the range.
4: max_value,
}
),
/// Server responds with a client connected to a UPS device.
RespondClient (
{
0: Client,
1: Arg,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The IP address of the client.
2: client_ip,
}
),
/// Server begins returning a list of UPS devices.
BeginListUps (
{
0: Begin,
1: List,
2: Ups,
3: EOL,
},
{}
),
/// Server ends returning a list of UPS devices.
EndListUps (
{
0: End,
1: List,
2: Ups,
3: EOL,
},
{}
),
/// Server begins returning a list of variables for a UPS device.
BeginListVar (
{
0: Begin,
1: List,
2: Var,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server ends returning a list of variables for a UPS device.
EndListVar (
{
0: End,
1: List,
2: Var,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server begins returning a list of mutable variables for a UPS device.
BeginListRw (
{
0: Begin,
1: List,
2: Rw,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server ends returning a list of mutable variables for a UPS device.
EndListRw (
{
0: End,
1: List,
2: Rw,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server begins returning a list of commands for a UPS device.
BeginListCmd (
{
0: Begin,
1: List,
2: Cmd,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server ends returning a list of commands for a UPS device.
EndListCmd (
{
0: End,
1: List,
2: Cmd,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server begins returning a list of possible values for an enumerable variable.
BeginListEnum (
{
0: Begin,
1: List,
2: Enum,
3: Arg,
4: Arg,
5: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
/// The name of the variable.
4: var_name,
}
),
/// Server ends returning a list of possible values for an enumerable variable.
EndListEnum (
{
0: End,
1: List,
2: Enum,
3: Arg,
4: Arg,
5: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
/// The name of the variable.
4: var_name,
}
),
/// Server begins returning a list of possible ranges for an enumerable variable.
BeginListRange (
{
0: Begin,
1: List,
2: Range,
3: Arg,
4: Arg,
5: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
/// The name of the variable.
4: var_name,
}
),
/// Server ends returning a list of possible ranges for an enumerable variable.
EndListRange (
{
0: End,
1: List,
2: Range,
3: Arg,
4: Arg,
5: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
/// The name of the variable.
4: var_name,
}
),
/// Server begins returning a list of clients for a UPS device.
BeginListClient (
{
0: Begin,
1: List,
2: Client,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
/// Server ends returning a list of clients for a UPS device.
EndListClient (
{
0: End,
1: List,
2: Client,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
3: ups_name,
}
),
}
#[cfg(test)]
mod tests {
use crate::proto::test_encode_decode;
use super::Sentences;
#[test]
fn test_encode_decode() {
test_encode_decode!(
["OK"] <=>
Sentences::GenericOk {}
);
test_encode_decode!(
["OK", "FSD-SET"] <=>
Sentences::FsdOk {}
);
test_encode_decode!(
["OK", "STARTTLS"] <=>
Sentences::StartTLSOk {}
);
test_encode_decode!(
["OK", "Goodbye"] <=>
Sentences::LogoutOk {}
);
test_encode_decode!(
["ERR", "ACCESS-DENIED"] <=>
Sentences::RespondErr {
message: "ACCESS-DENIED".into(),
extras: vec![],
}
);
test_encode_decode!(
["ERR", "ACCESS-DENIED", "extra1", "extra2"] <=>
Sentences::RespondErr {
message: "ACCESS-DENIED".into(),
extras: vec!["extra1".into(), "extra2".into()],
}
);
test_encode_decode!(
["NUMLOGINS", "nutdev", "42"] <=>
Sentences::RespondNumLogins {
ups_name: "nutdev".into(),
num_logins: "42".into(),
}
);
test_encode_decode!(
["UPSDESC", "nutdev", "Development box"] <=>
Sentences::RespondUpsDesc {
ups_name: "nutdev".into(),
description: "Development box".into(),
}
);
test_encode_decode!(
["VAR", "nutdev", "ups.status", "OL"] <=>
Sentences::RespondVar {
ups_name: "nutdev".into(),
var_name: "ups.status".into(),
value: "OL".into(),
}
);
test_encode_decode!(
["TYPE", "nutdev", "input.transfer.low", "ENUM", "RW"] <=>
Sentences::RespondType {
ups_name: "nutdev".into(),
var_name: "input.transfer.low".into(),
var_types: vec!["ENUM".into(), "RW".into()],
}
);
test_encode_decode!(
["DESC", "nutdev", "ups.status", "UPS status"] <=>
Sentences::RespondDesc {
ups_name: "nutdev".into(),
var_name: "ups.status".into(),
description: "UPS status".into(),
}
);
test_encode_decode!(
["CMDDESC", "nutdev", "load.on", "Turn on the load immediately"] <=>
Sentences::RespondCmdDesc {
ups_name: "nutdev".into(),
cmd_name: "load.on".into(),
description: "Turn on the load immediately".into(),
}
);
test_encode_decode!(
["UPS", "nutdev", "Development box"] <=>
Sentences::RespondUps {
ups_name: "nutdev".into(),
description: "Development box".into(),
}
);
test_encode_decode!(
["RW", "nutdev", "ups.mfr", "APC"] <=>
Sentences::RespondRw {
ups_name: "nutdev".into(),
var_name: "ups.mfr".into(),
value: "APC".into(),
}
);
test_encode_decode!(
["CMD", "nutdev", "do.something"] <=>
Sentences::RespondCmd {
ups_name: "nutdev".into(),
cmd_name: "do.something".into(),
}
);
test_encode_decode!(
["ENUM", "nutdev", "input.transfer.low", "103"] <=>
Sentences::RespondEnum {
ups_name: "nutdev".into(),
var_name: "input.transfer.low".into(),
enum_value: "103".into(),
}
);
test_encode_decode!(
["RANGE", "nutdev", "input.transfer.low", "90", "100"] <=>
Sentences::RespondRange {
ups_name: "nutdev".into(),
var_name: "input.transfer.low".into(),
min_value: "90".into(),
max_value: "100".into(),
}
);
test_encode_decode!(
["CLIENT", "nutdev", "127.0.0.1"] <=>
Sentences::RespondClient {
ups_name: "nutdev".into(),
client_ip: "127.0.0.1".into(),
}
);
test_encode_decode!(
["BEGIN", "LIST", "VAR", "nutdev"] <=>
Sentences::BeginListVar {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["END", "LIST", "VAR", "nutdev"] <=>
Sentences::EndListVar {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["BEGIN", "LIST", "RW", "nutdev"] <=>
Sentences::BeginListRw {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["END", "LIST", "RW", "nutdev"] <=>
Sentences::EndListRw {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["BEGIN", "LIST", "CMD", "nutdev"] <=>
Sentences::BeginListCmd {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["END", "LIST", "CMD", "nutdev"] <=>
Sentences::EndListCmd {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["BEGIN", "LIST", "ENUM", "nutdev", "test.var"] <=>
Sentences::BeginListEnum {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["END", "LIST", "ENUM", "nutdev", "test.var"] <=>
Sentences::EndListEnum {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["BEGIN", "LIST", "RANGE", "nutdev", "test.var"] <=>
Sentences::BeginListRange {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["END", "LIST", "RANGE", "nutdev", "test.var"] <=>
Sentences::EndListRange {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["BEGIN", "LIST", "CLIENT", "nutdev"] <=>
Sentences::BeginListClient {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["END", "LIST", "CLIENT", "nutdev"] <=>
Sentences::EndListClient {
ups_name: "nutdev".into(),
}
);
}
}

315
rups/src/proto/mod.rs Normal file
View file

@ -0,0 +1,315 @@
/// Client-bound protocol implementation.
///
/// "Client-bound" implies commands RECEIVED and DECODED by the client. The server implementation
/// must use the same messages to ENCODE and SEND.
pub mod client;
/// Server-bound protocol implementation.
///
/// "Server-bound" implies commands RECEIVED and DECODED by the server. The client implementation
/// must use the same messages to ENCODE and SEND.
pub mod server;
/// Utilities for encoding and decoding packets.
pub mod util;
pub use client::Sentences as ClientSentences;
pub use server::Sentences as ServerSentences;
/// Macro that implements the list of "words" in the NUT network protocol.
macro_rules! impl_words {
(
$(
$(#[$attr:meta])+
$name:ident($word:tt),
)*
) => {
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum Word {
/// A string argument.
Arg,
/// End-of-line.
EOL,
$(
/// Protocol word.
$(#[$attr])*
#[allow(dead_code)]
$name,
)*
}
impl Word {
/// Matches a raw string into the corresponding word.
/// Passing `None` will always return `EOL`. Passing an unrecognized
/// string returns `None`.
pub(crate) fn decode(raw: Option<&str>) -> Option<Self> {
if let Some(raw) = raw {
match raw {
$($word => Some(Self::$name),)*
_ => None
}
} else {
Some(Self::EOL)
}
}
/// Decodes a sequence of words.
/// Unrecognized words will be `None`
/// Returns a `Vec` of the same length as the given slice.
pub(crate) fn decode_words<T: AsRef<str>>(raw: &[T]) -> Vec<Option<Self>> {
let mut words = Vec::new();
for r in raw.iter() {
words.push(Self::decode(Some(r.as_ref())));
}
words.push(Some(Self::EOL));
words
}
/// Encodes a `Word` into a string.
/// This function cannot encode `Arg` or `EOL` (either returns `None`).
pub(crate) fn encode(&self) -> Option<&str> {
match self {
Self::Arg | Self::EOL => None,
$(Self::$name => Some($word),)*
}
}
/// Whether the `Word` matches another.
pub(crate) fn matches(&self, other: Option<&Option<Self>>) -> bool {
if let Some(other) = other {
if self == &Word::Arg {
true
} else if let Some(other) = other {
self == other
} else {
self == &Word::EOL
}
} else {
false
}
}
/// Whether the `Word` matches all words in the vec, starting at the given index.
pub(crate) fn matches_until_end(&self, start: usize, others: &[Option<Self>]) -> bool {
for i in start..others.len() {
if i == others.len() {
return others[i] == Some(Self::EOL);
}
if !self.matches(others.get(i)) {
return false;
}
}
true
}
}
};
}
/// Implements the list of sentences, which are combinations
/// of words that form commands (serverbound) and responses (clientbound).
macro_rules! impl_sentences {
(
$(
$(#[$attr:meta])+
$name:ident(
{
$($wordidx:tt: $word:ident,)*
},
{
$(
$(#[$argattr:meta])+
$argidx:tt: $arg:ident,
)*
}
$(
,{
$(#[$varargattr:meta])+
$varargidx:tt...: $vararg:ident
}
)?
),
)*
) => {
/// Protocol sentences.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Sentences {
$(
$(#[$attr])*
$name {
$(
$(#[$argattr])*
$arg: String,
)*
$(
$(#[$varargattr])*
$vararg: Vec<String>,
)*
},
)*
}
impl Sentences {
/// Decodes a sentence. Returns `None` if the pattern cannot be recognized.
pub(crate) fn decode(raw: Vec<String>) -> Option<Sentences> {
use super::{Word::*, *};
use Sentences::*;
let words = Word::decode_words(raw.as_slice());
$(
if true
$(&& $word.matches(words.get($wordidx)))*
$(&& Arg.matches_until_end($varargidx, &words))*
{
return Some($name {
$($arg: raw[$argidx].to_owned(),)*
$($vararg: raw[$varargidx..].to_owned(),)*
})
}
)*
None
}
/// Encodes the sentence.
pub(crate) fn encode(&self) -> Vec<&str> {
use super::Word::*;
match self {
$(
Self::$name {
$($arg,)*
$($vararg,)*
} => {
#[allow(unused_mut)]
let mut words = vec![
$(
$word.encode(),
)*
];
$(
words[$argidx] = Some($arg);
)*
$(
for vararg in $vararg {
words.push(Some(vararg));
}
)*
words
.into_iter()
.flatten()
.collect()
}
)*
}
}
}
};
}
/// Macro that asserts the encoding and decoding of a valid sentence.
///
/// The two arguments, separated by `<=>`, are:
/// 1. the encoded sentence, e.g. `["GET", "VAR", "nutdev", "test.var"]`
/// 2. the decoded sentence
///
/// ```
/// # #[macro_use] extern crate rups;
/// # fn main() {
/// # #[cfg(test)]
/// test_encode_decode!(
/// ["GET", "VAR", "nutdev", "test.var"] <=>
/// Sentences::QueryVar {
/// ups_name: "nutdev".into(),
/// var_name: "test.var".into(),
/// }
/// );
/// # }
/// ```
#[allow(unused_macros)]
macro_rules! test_encode_decode {
([$($word:expr$(,)?)+] <=> $expected:expr) => {
assert_eq!(
Sentences::decode(vec![
$(String::from($word),)*
]),
Some($expected)
);
assert_eq!(
vec![
$(String::from($word),)*
],
$expected.encode()
);
};
}
impl_words! {
/// Begins a `LIST`.
Begin("BEGIN"),
/// Describes a client connected to a UPS.
Client("CLIENT"),
/// Represents an executable command.
Cmd("CMD"),
/// Describes a command (`CMD`).
CmdDesc("CMDDESC"),
/// Describes a variable (`VAR` or `RW`).
Desc("DESC"),
/// Ends a block of sentences.
End("END"),
/// An enumerable type.
Enum("ENUM"),
/// An error response.
Err("ERR"),
/// Executes a forced shut down (FSD).
Fsd("FSD"),
/// Server confirms forced shut down (FSD).
FsdSet("FSD-SET"),
/// Serverbound query.
Get("GET"),
/// Server confirms logout (this is lower-case on purpose).
Goodbye("Goodbye"),
/// Client requesting a list of commands supported by the server.
Help("HELP"),
/// Executes an instant command.
InstCmd("INSTCMD"),
/// Queries or describes a list.
List("LIST"),
/// Client requests login to a UPS device.
Login("LOGIN"),
/// Client logs out.
Logout("LOGOUT"),
/// Client verifying it has master-level access to the UPS device.
Master("MASTER"),
/// Client requests the network version.
NetworkVersion("NETVER"),
/// Represents the amount of logins to a UPS device.
NumLogins("NUMLOGINS"),
/// Clientbound response for a good outcome.
Ok("OK"),
/// Client setting password.
Password("PASSWORD"),
/// Represents a range of numerical values.
Range("RANGE"),
/// Represents a mutable variable.
Rw("RW"),
/// Client requests to set the value of a mutable variable.
Set("SET"),
/// Client requests the connection be upgraded to TLS.
StartTLS("STARTTLS"),
/// Represents the type of a variable.
Type("TYPE"),
/// Represents a UPS device.
Ups("UPS"),
/// Represents the description of a UPS device.
UpsDesc("UPSDESC"),
/// Client setting username.
Username("USERNAME"),
/// Represents a variable.
Var("VAR"),
/// Client requests the server version.
Version("VERSION"),
}
pub(crate) use impl_sentences;
#[cfg(test)]
pub(crate) use test_encode_decode;

472
rups/src/proto/server.rs Normal file
View file

@ -0,0 +1,472 @@
use crate::proto::impl_sentences;
impl_sentences! {
/// Client requests the number of prior logins to the given `ups_name` device.
QueryNumLogins (
{
0: Get,
1: NumLogins,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
}
),
/// Client requests the description of the given `ups_name` device.
QueryUpsDesc (
{
0: Get,
1: UpsDesc,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
}
),
/// Client requests the value of the given `var_name` variable in the given `ups_name` device.
QueryVar (
{
0: Get,
1: Var,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the variable.
3: var_name,
}
),
/// Client requests the type of the given `var_name` variable in the given `ups_name` device.
QueryType (
{
0: Get,
1: Type,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the variable.
3: var_name,
}
),
/// Client requests the description of the given `var_name` variable in the given `ups_name` device.
QueryDesc (
{
0: Get,
1: Desc,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the variable.
3: var_name,
}
),
/// Client requests the description of the given `cmd_name` command in the given `ups_name` device.
QueryCmdDesc (
{
0: Get,
1: CmdDesc,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the command.
3: cmd_name,
}
),
/// Client requests the list of variables for the given `ups_name` device.
QueryListVar (
{
0: List,
1: Var,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
}
),
/// Client requests the list of mutable variables for the given `ups_name` device.
QueryListRw (
{
0: List,
1: Rw,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
}
),
/// Client requests the list of commands for the given `ups_name` device.
QueryListCmd (
{
0: List,
1: Cmd,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
}
),
/// Client requests the list of possible values of the enumerable variable `var_name`
/// for the given `ups_name` device.
QueryListEnum (
{
0: List,
1: Enum,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the variable.
3: var_name,
}
),
/// Client requests the list of possible ranges of the numerical variable `var_name`
/// for the given `ups_name` device.
QueryListRange (
{
0: List,
1: Range,
2: Arg,
3: Arg,
4: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the variable.
3: var_name,
}
),
/// Client requests the list of clients connected to the given `ups_name` device.
QueryListClient (
{
0: List,
1: Client,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
}
),
/// Client requests to set the value `value` of the `var_name` variable on the `ups_name` device.
ExecSetVar (
{
0: Set,
1: Var,
2: Arg,
3: Arg,
4: Arg,
5: EOL,
},
{
/// The name of the UPS device.
2: ups_name,
/// The name of the variable.
3: var_name,
/// The new value of the variable.
4: value,
}
),
/// Client requests the execution of an instant command `cmd_name` on the `ups_name` device.
ExecInstCmd (
{
0: InstCmd,
1: Arg,
2: Arg,
3: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
/// The name of the command.
2: cmd_name,
}
),
/// Client logs-out of the current UPS device.
ExecLogout (
{
0: Logout,
1: EOL,
},
{}
),
/// Client logs-into the given `ups_name` device.
ExecLogin (
{
0: Login,
1: Arg,
2: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
}
),
/// Client asserts master-level access to the `ups_name` device.
ExecMaster (
{
0: Master,
1: Arg,
2: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
}
),
/// Client requests the forced shut-down of the `ups_name` device.
ExecForcedShutDown (
{
0: Fsd,
1: Arg,
2: EOL,
},
{
/// The name of the UPS device.
1: ups_name,
}
),
/// Client sets the password on the connection.
SetPassword (
{
0: Password,
1: Arg,
2: EOL,
},
{
/// The password to set.
1: password,
}
),
/// Client sets the username on the connection.
SetUsername (
{
0: Username,
1: Arg,
2: EOL,
},
{
/// The username to set.
1: username,
}
),
/// Client requests the connection be upgraded to TLS.
ExecStartTLS (
{
0: StartTLS,
1: EOL,
},
{}
),
/// Client requests the list of commands supported by the server.
QueryHelp (
{
0: Help,
1: EOL,
},
{}
),
/// Client requests the server version.
QueryVersion (
{
0: Version,
1: EOL,
},
{}
),
/// Client requests the network version.
QueryNetworkVersion (
{
0: NetworkVersion,
1: EOL,
},
{}
),
}
#[cfg(test)]
mod tests {
use super::Sentences;
use crate::proto::test_encode_decode;
#[test]
fn test_encode_decode() {
test_encode_decode!(
["GET", "NUMLOGINS", "nutdev"] <=>
Sentences::QueryNumLogins {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["GET", "NUMLOGINS", "nutdev"] <=>
Sentences::QueryNumLogins {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["GET", "UPSDESC", "nutdev"] <=>
Sentences::QueryUpsDesc {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["GET", "VAR", "nutdev", "test.var"] <=>
Sentences::QueryVar {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["GET", "TYPE", "nutdev", "test.var"] <=>
Sentences::QueryType {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["GET", "DESC", "nutdev", "test.var"] <=>
Sentences::QueryDesc {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["GET", "CMDDESC", "nutdev", "test.cmd"] <=>
Sentences::QueryCmdDesc {
ups_name: "nutdev".into(),
cmd_name: "test.cmd".into(),
}
);
test_encode_decode!(
["LIST", "VAR", "nutdev"] <=>
Sentences::QueryListVar {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["LIST", "RW", "nutdev"] <=>
Sentences::QueryListRw {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["LIST", "CMD", "nutdev"] <=>
Sentences::QueryListCmd {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["LIST", "ENUM", "nutdev", "test.var"] <=>
Sentences::QueryListEnum {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["LIST", "RANGE", "nutdev", "test.var"] <=>
Sentences::QueryListRange {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
}
);
test_encode_decode!(
["LIST", "CLIENT", "nutdev"] <=>
Sentences::QueryListClient {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["SET", "VAR", "nutdev", "test.var", "something"] <=>
Sentences::ExecSetVar {
ups_name: "nutdev".into(),
var_name: "test.var".into(),
value: "something".into(),
}
);
test_encode_decode!(
["INSTCMD", "nutdev", "test.cmd"] <=>
Sentences::ExecInstCmd {
ups_name: "nutdev".into(),
cmd_name: "test.cmd".into(),
}
);
test_encode_decode!(
["LOGOUT"] <=>
Sentences::ExecLogout {}
);
test_encode_decode!(
["LOGIN", "nutdev"] <=>
Sentences::ExecLogin {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["MASTER", "nutdev"] <=>
Sentences::ExecMaster {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["FSD", "nutdev"] <=>
Sentences::ExecForcedShutDown {
ups_name: "nutdev".into(),
}
);
test_encode_decode!(
["PASSWORD", "topsecret"] <=>
Sentences::SetPassword {
password: "topsecret".into(),
}
);
test_encode_decode!(
["USERNAME", "john"] <=>
Sentences::SetUsername {
username: "john".into(),
}
);
test_encode_decode!(
["STARTTLS"] <=>
Sentences::ExecStartTLS {}
);
test_encode_decode!(
["HELP"] <=>
Sentences::QueryHelp {}
);
test_encode_decode!(
["VERSION"] <=>
Sentences::QueryVersion {}
);
test_encode_decode!(
["NETVER"] <=>
Sentences::QueryNetworkVersion {}
);
}
}

41
rups/src/proto/util.rs Normal file
View file

@ -0,0 +1,41 @@
/// Splits a sentence (line) into a `Vec<String>`, minding quotation marks
/// for words with spaces in them.
///
/// Returns `None` if the sentence cannot be split safely (usually unbalanced quotation marks).
pub fn split_sentence<T: AsRef<str>>(sentence: T) -> Option<Vec<String>> {
shell_words::split(sentence.as_ref()).ok()
}
/// Joins a collection of words (`&str`) into one sentence string,
/// adding quotation marks for words with spaces in them.
pub fn join_sentence<I, S>(words: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
shell_words::join(words)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split() {
assert_eq!(
split_sentence("AbC dEf GHi"),
Some(vec!["AbC".into(), "dEf".into(), "GHi".into()])
);
assert_eq!(
split_sentence("\"AbC dEf\" GHi"),
Some(vec!["AbC dEf".into(), "GHi".into()])
);
assert_eq!(split_sentence("\"AbC dEf GHi"), None);
}
#[test]
fn test_join() {
assert_eq!(join_sentence(vec!["AbC", "dEf", "GHi"]), "AbC dEf GHi",);
assert_eq!(join_sentence(vec!["AbC dEf", "GHi"]), "'AbC dEf' GHi",);
assert_eq!(join_sentence(vec!["\"AbC dEf", "GHi"]), "'\"AbC dEf' GHi",);
}
}

View file

@ -144,7 +144,7 @@ impl TcpConnection {
if debug {
eprint!("DEBUG <- {}", raw);
}
raw = raw[..raw.len() - 1].to_string(); // Strip off \n
raw = raw.trim_end_matches('\n').to_string(); // Strip off \n
// Parse args by splitting whitespace, minding quotes for args with multiple words
let args = shell_words::split(&raw)

View file

@ -5,7 +5,7 @@ use std::time::Duration;
/// Well-known variable keys for NUT UPS devices.
///
/// List retrieved from: https://networkupstools.org/docs/user-manual.chunked/apcs01.html
/// List retrieved from: <https://networkupstools.org/docs/user-manual.chunked/apcs01.html>
pub mod key {
/// Device model.
pub const DEVICE_MODEL: &str = "device.model";
@ -31,7 +31,7 @@ pub mod key {
/// Well-known variables for NUT UPS devices.
///
/// List retrieved from: https://networkupstools.org/docs/user-manual.chunked/apcs01.html
/// List retrieved from: <https://networkupstools.org/docs/user-manual.chunked/apcs01.html>
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Variable {
/// Device model.
@ -196,10 +196,8 @@ impl TryFrom<&str> for VariableType {
other => {
if other.starts_with("STRING:") {
let size = other
.splitn(2, ':')
.nth(1)
.map(|s| s.parse().ok())
.flatten()
.split_once(':')
.and_then(|(_, s)| s.parse().ok())
.ok_or_else(|| crate::ClientError::generic("Invalid STRING definition"))?;
Ok(Self::String(size))
} else {

View file

@ -1,8 +1,8 @@
[package]
name = "rupsc"
version = "0.5.2"
version = "0.6.1"
authors = ["Aram Peres <aram.peres@wavy.fm>"]
edition = "2018"
edition = "2021"
description = "A demo program to display UPS variables"
categories = ["network-programming"]
keywords = ["ups", "nut"]
@ -17,6 +17,6 @@ clap = "2.33.3"
anyhow = "1"
[dependencies.rups]
version = "0.5.2"
version = "0.6.1"
path = "../rups"
features = ["ssl"]

View file

@ -1,8 +1,8 @@
///! # rupsc
///! A demo program to display UPS variables.
///! This a Rust clone of [upsc](https://github.com/networkupstools/nut/blob/master/clients/upsc.c).
///!
///! P.S.: pronounced "r-oopsie".
//! # rupsc
//! A demo program to display UPS variables.
//! This a Rust clone of [upsc](https://github.com/networkupstools/nut/blob/master/clients/upsc.c).
//!
//! P.S.: pronounced "r-oopsie".
use core::convert::TryInto;
use anyhow::Context;