mirror of
https://github.com/aramperes/nut-rs.git
synced 2025-09-09 13:38:30 -04:00
337 lines
10 KiB
Rust
337 lines
10 KiB
Rust
/// 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
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// A NUT protocol sentence that can be encoded and decoded from a Vector of strings.
|
|
pub trait Sentence: Eq + Sized + Into<crate::Result<Self>> {
|
|
/// Decodes a sentence. Returns `None` if the pattern cannot be recognized.
|
|
fn decode(raw: Vec<String>) -> Option<Self>;
|
|
|
|
/// Encodes the sentence.
|
|
fn encode(&self) -> Vec<&str>;
|
|
|
|
/// Returns an error if the sentence does not match what was expected.
|
|
fn matching<F: FnOnce(&Self) -> bool>(self, matcher: F) -> crate::Result<Self> {
|
|
if matcher(&self) {
|
|
Ok(self)
|
|
} else {
|
|
Err(Error::Nut(NutError::UnexpectedResponse))
|
|
}
|
|
}
|
|
|
|
/// Returns an error if the sentence is not equal to what was expected.
|
|
fn exactly(self, expected: Self) -> crate::Result<Self> {
|
|
if expected == self {
|
|
Ok(self)
|
|
} else {
|
|
Err(Error::Nut(NutError::UnexpectedResponse))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 crate::proto::Sentence for Sentences {
|
|
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
|
|
}
|
|
|
|
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
|
|
///
|
|
/// ```
|
|
/// 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"),
|
|
}
|
|
|
|
use crate::{Error, NutError};
|
|
pub(crate) use impl_sentences;
|
|
#[cfg(test)]
|
|
pub(crate) use test_encode_decode;
|