openvpn_mgmt_codec/
version_info.rs1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct VersionInfo {
35 management_version: Option<u32>,
37 openvpn_version_line: Option<String>,
39 raw_lines: Vec<String>,
41}
42
43impl VersionInfo {
44 pub fn parse(lines: &[String]) -> Self {
49 let mut management_version = None;
50 let mut openvpn_version_line = None;
51
52 for line in lines {
53 let lower = line.to_ascii_lowercase();
54
55 if management_version.is_none()
62 && lower.starts_with("management")
63 && lower.contains("version")
64 {
65 management_version = line
66 .rsplit(|c: char| !c.is_ascii_digit())
67 .find(|s| !s.is_empty())
68 .and_then(|s| s.parse().ok());
69 } else if lower.starts_with("openvpn version") {
70 openvpn_version_line = Some(line.clone());
71 }
72 }
73
74 Self {
75 management_version,
76 openvpn_version_line,
77 raw_lines: lines.to_vec(),
78 }
79 }
80
81 pub fn management_version(&self) -> Option<u32> {
87 self.management_version
88 }
89
90 pub fn openvpn_version_line(&self) -> Option<&str> {
92 self.openvpn_version_line.as_deref()
93 }
94
95 pub fn raw_lines(&self) -> &[String] {
97 &self.raw_lines
98 }
99}
100
101impl fmt::Display for VersionInfo {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 if let Some(line) = &self.openvpn_version_line {
104 write!(f, "{line}")?;
105 if let Some(v) = self.management_version {
106 write!(f, " (management v{v})")?;
107 }
108 } else if let Some(v) = self.management_version {
109 write!(f, "Management Interface Version: {v}")?;
110 } else {
111 f.write_str("(unknown version)")?;
112 }
113 Ok(())
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn parse_typical_version_output() {
123 let lines = vec![
124 "OpenVPN Version: OpenVPN 2.6.9 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD]".to_string(),
125 "Management Interface Version: 5".to_string(),
126 ];
127 let info = VersionInfo::parse(&lines);
128 assert_eq!(info.management_version(), Some(5));
129 assert!(info.openvpn_version_line().unwrap().contains("2.6.9"));
130 assert_eq!(info.raw_lines().len(), 2);
131 }
132
133 #[test]
134 fn parse_short_management_version_header() {
135 let lines = vec![
137 "OpenVPN Version: OpenVPN 2.6.16 x86_64-alpine-linux-musl [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD]".to_string(),
138 "Management Version: 5".to_string(),
139 ];
140 let info = VersionInfo::parse(&lines);
141 assert_eq!(info.management_version(), Some(5));
142 assert!(info.openvpn_version_line().unwrap().contains("2.6.16"));
143 }
144
145 #[test]
146 fn parse_old_version_without_management_line() {
147 let lines = vec!["OpenVPN Version: OpenVPN 2.3.2 i686-pc-linux-gnu".to_string()];
148 let info = VersionInfo::parse(&lines);
149 assert_eq!(info.management_version(), None);
150 assert!(info.openvpn_version_line().is_some());
151 }
152
153 #[test]
154 fn parse_empty_response() {
155 let info = VersionInfo::parse(&[]);
156 assert_eq!(info.management_version(), None);
157 assert_eq!(info.openvpn_version_line(), None);
158 assert!(info.raw_lines().is_empty());
159 }
160
161 #[test]
162 fn parse_hypothetical_future_format() {
163 let lines = vec!["Management Protocol Version: 6".to_string()];
166 let info = VersionInfo::parse(&lines);
167 assert_eq!(info.management_version(), Some(6));
168 }
169
170 #[test]
171 fn display_typical() {
172 let lines = vec![
173 "OpenVPN Version: OpenVPN 2.6.9".to_string(),
174 "Management Interface Version: 5".to_string(),
175 ];
176 let info = VersionInfo::parse(&lines);
177 let s = info.to_string();
178 assert!(s.contains("2.6.9"));
179 assert!(s.contains("management v5"));
180 }
181
182 #[test]
183 fn display_unknown() {
184 let info = VersionInfo::parse(&[]);
185 assert_eq!(info.to_string(), "(unknown version)");
186 }
187}