#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionInfo {
management_version: Option<u32>,
openvpn_version_line: Option<String>,
raw_lines: Vec<String>,
}
impl VersionInfo {
pub fn parse(lines: &[String]) -> Self {
let mut management_version = None;
let mut openvpn_version_line = None;
for line in lines {
let lower = line.to_ascii_lowercase();
if management_version.is_none()
&& lower.starts_with("management")
&& lower.contains("version")
{
management_version = line
.rsplit(|chr: char| !chr.is_ascii_digit())
.find(|part| !part.is_empty())
.and_then(|part| {
part.parse()
.inspect_err(|error| {
tracing::warn!(%error, part, "non-numeric management version")
})
.ok()
});
} else if lower.starts_with("openvpn version") {
openvpn_version_line = Some(line.clone());
}
}
Self {
management_version,
openvpn_version_line,
raw_lines: lines.to_vec(),
}
}
pub fn management_version(&self) -> Option<u32> {
self.management_version
}
pub fn openvpn_version_line(&self) -> Option<&str> {
self.openvpn_version_line.as_deref()
}
pub fn raw_lines(&self) -> &[String] {
&self.raw_lines
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_typical_version_output() {
let lines = vec![
"OpenVPN Version: OpenVPN 2.6.9 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD]".to_string(),
"Management Interface Version: 5".to_string(),
];
let info = VersionInfo::parse(&lines);
assert_eq!(info.management_version(), Some(5));
assert!(info.openvpn_version_line().unwrap().contains("2.6.9"));
assert_eq!(info.raw_lines().len(), 2);
}
#[test]
fn parse_short_management_version_header() {
let lines = vec![
"OpenVPN Version: OpenVPN 2.6.16 x86_64-alpine-linux-musl [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD]".to_string(),
"Management Version: 5".to_string(),
];
let info = VersionInfo::parse(&lines);
assert_eq!(info.management_version(), Some(5));
assert!(info.openvpn_version_line().unwrap().contains("2.6.16"));
}
#[test]
fn parse_old_version_without_management_line() {
let lines = vec!["OpenVPN Version: OpenVPN 2.3.2 i686-pc-linux-gnu".to_string()];
let info = VersionInfo::parse(&lines);
assert_eq!(info.management_version(), None);
assert!(info.openvpn_version_line().is_some());
}
#[test]
fn parse_empty_response() {
let info = VersionInfo::parse(&[]);
assert_eq!(info.management_version(), None);
assert_eq!(info.openvpn_version_line(), None);
assert!(info.raw_lines().is_empty());
}
#[test]
fn parse_hypothetical_future_format() {
let lines = vec!["Management Protocol Version: 6".to_string()];
let info = VersionInfo::parse(&lines);
assert_eq!(info.management_version(), Some(6));
}
}