use std::{path::PathBuf, time::Duration};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProtocolConfig {
#[serde(with = "humantime_serde", default = "default_duration::<20>")]
pub t3: Duration,
#[serde(with = "humantime_serde", default = "default_duration::<10>")]
pub t2: Duration,
#[serde(with = "humantime_serde", default = "default_duration::<15>")]
pub t1: Duration,
#[serde(with = "humantime_serde", default = "default_duration::<10>")]
pub t0: Duration,
#[serde(default = "default_number::<12>")]
pub k: u16,
#[serde(default = "default_number::<8>")]
pub w: u16,
#[serde(default = "default_max_pending_outgoing_asdu")]
pub max_pending_outgoing_asdu: u32,
pub originator_address: u8,
}
impl ProtocolConfig {
#[must_use]
pub fn max_pending_outgoing_asdu_limit(&self) -> Option<usize> {
(self.max_pending_outgoing_asdu != 0).then_some(self.max_pending_outgoing_asdu as usize)
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.k == 0 {
return KZeroError.fail();
}
if self.w == 0 {
return WZeroError.fail();
}
let max_w = (u32::from(self.k) * 2) / 3;
if u32::from(self.w) > max_w {
return WExceedsLimitError { k: self.k, w: self.w, max_w: max_w as u16 }.fail();
}
Ok(())
}
}
#[derive(Debug, snafu::Snafu)]
#[snafu(visibility(pub), context(suffix(Error)))]
pub enum ConfigError {
#[snafu(display("Protocol k must be > 0"))]
KZero,
#[snafu(display("Protocol w must be > 0"))]
WZero,
#[snafu(display(
"Protocol w ({w}) exceeds spec-mandated upper bound ⌊2k/3⌋ = {max_w} for k = {k}"
))]
WExceedsLimit { k: u16, w: u16, max_w: u16 },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TlsClientConfig {
#[serde(default)]
pub client_key: Option<PathBuf>,
#[serde(default)]
pub client_certificate: Option<PathBuf>,
#[serde(default)]
pub server_certificate: Option<PathBuf>,
#[serde(default)]
pub danger_disable_tls_verify: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ClientConfig {
pub address: String,
pub port: u16,
#[serde(default)]
pub protocol: ProtocolConfig,
#[serde(default)]
pub tls: Option<TlsClientConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServerConfig {
pub address: String,
pub port: u16,
#[serde(default)]
pub protocol: ProtocolConfig,
#[serde(default)]
pub tls: Option<TlsServerConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TlsServerConfig {
pub server_certificate: PathBuf,
pub server_key: PathBuf,
}
impl Default for ProtocolConfig {
fn default() -> Self {
Self {
t3: Duration::from_secs(20),
t2: Duration::from_secs(10),
t1: Duration::from_secs(15),
t0: Duration::from_secs(10),
k: 12,
w: 8,
max_pending_outgoing_asdu: 1024,
originator_address: 1,
}
}
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
address: "127.0.0.1".to_owned(),
port: 2404,
protocol: ProtocolConfig::default(),
tls: None,
}
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
address: "127.0.0.1".to_owned(),
port: 2404,
protocol: ProtocolConfig::default(),
tls: None,
}
}
}
const fn default_number<const N: u16>() -> u16 {
N
}
const fn default_max_pending_outgoing_asdu() -> u32 {
1024
}
const fn default_duration<const N: u64>() -> Duration {
Duration::from_secs(N)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_t1_matches_iec_104_spec() {
assert_eq!(ProtocolConfig::default().t1, Duration::from_secs(15));
}
#[test]
fn default_protocol_config_is_valid() {
ProtocolConfig::default().validate().expect("default config must validate");
}
#[test]
fn validate_accepts_w_at_two_thirds_k() {
let cfg = ProtocolConfig { k: 12, w: 8, ..ProtocolConfig::default() };
cfg.validate().expect("w == 2k/3 is allowed");
}
#[test]
fn validate_rejects_w_above_two_thirds_k() {
let cfg = ProtocolConfig { k: 12, w: 9, ..ProtocolConfig::default() };
let err = cfg.validate().expect_err("w > 2k/3 must be rejected");
assert!(matches!(err, ConfigError::WExceedsLimit { .. }), "got: {err:?}");
}
#[test]
fn validate_rejects_zero_k() {
let cfg = ProtocolConfig { k: 0, w: 0, ..ProtocolConfig::default() };
assert!(matches!(cfg.validate(), Err(ConfigError::KZero)));
}
#[test]
fn validate_rejects_zero_w() {
let cfg = ProtocolConfig { k: 12, w: 0, ..ProtocolConfig::default() };
assert!(matches!(cfg.validate(), Err(ConfigError::WZero)));
}
}