openvpn_mgmt_codec/
parsed_response.rs1use crate::version_info::VersionInfo;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct LoadStats {
26 pub nclients: u64,
28 pub bytesin: u64,
30 pub bytesout: u64,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
36pub enum ParseResponseError {
37 #[error("missing 'pid=' prefix in: {0:?}")]
39 MissingPidPrefix(String),
40
41 #[error("missing 'hold=' prefix in: {0:?}")]
43 MissingHoldPrefix(String),
44
45 #[error("invalid hold value: {0:?}")]
47 InvalidHoldValue(String),
48
49 #[error("invalid integer for field {field:?}: {value:?}")]
51 InvalidInteger {
52 field: &'static str,
54 value: String,
56 },
57
58 #[error("missing field {0:?} in load-stats payload")]
60 MissingField(&'static str),
61
62 #[error("unexpected field {0:?} in load-stats payload")]
64 UnexpectedField(String),
65}
66
67pub fn parse_pid(payload: &str) -> Result<u32, ParseResponseError> {
77 let val = payload
78 .strip_prefix("pid=")
79 .ok_or_else(|| ParseResponseError::MissingPidPrefix(payload.to_string()))?;
80 val.parse().map_err(|_| ParseResponseError::InvalidInteger {
81 field: "pid",
82 value: val.to_string(),
83 })
84}
85
86pub fn parse_load_stats(payload: &str) -> Result<LoadStats, ParseResponseError> {
98 let mut nclients = None;
99 let mut bytesin = None;
100 let mut bytesout = None;
101
102 for part in payload.split(',') {
103 if let Some((key, val)) = part.split_once('=') {
104 let parsed = |field| {
105 val.parse().map_err(|_| ParseResponseError::InvalidInteger {
106 field,
107 value: val.to_string(),
108 })
109 };
110 match key {
111 "nclients" => nclients = Some(parsed("nclients")?),
112 "bytesin" => bytesin = Some(parsed("bytesin")?),
113 "bytesout" => bytesout = Some(parsed("bytesout")?),
114 other => return Err(ParseResponseError::UnexpectedField(other.to_string())),
115 }
116 }
117 }
118
119 Ok(LoadStats {
120 nclients: nclients.ok_or(ParseResponseError::MissingField("nclients"))?,
121 bytesin: bytesin.ok_or(ParseResponseError::MissingField("bytesin"))?,
122 bytesout: bytesout.ok_or(ParseResponseError::MissingField("bytesout"))?,
123 })
124}
125
126pub fn parse_hold(payload: &str) -> Result<bool, ParseResponseError> {
137 let val = payload
138 .strip_prefix("hold=")
139 .ok_or_else(|| ParseResponseError::MissingHoldPrefix(payload.to_string()))?;
140 match val {
141 "1" => Ok(true),
142 "0" => Ok(false),
143 _ => Err(ParseResponseError::InvalidHoldValue(val.to_string())),
144 }
145}
146
147pub fn parse_version(lines: &[String]) -> VersionInfo {
163 VersionInfo::parse(lines)
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
173 fn pid_normal() {
174 assert_eq!(parse_pid("pid=42"), Ok(42));
175 }
176
177 #[test]
178 fn pid_zero() {
179 assert_eq!(parse_pid("pid=0"), Ok(0));
180 }
181
182 #[test]
183 fn pid_missing_prefix() {
184 assert!(parse_pid("42").is_err());
185 }
186
187 #[test]
188 fn pid_not_a_number() {
189 assert!(parse_pid("pid=abc").is_err());
190 }
191
192 #[test]
195 fn load_stats_normal() {
196 let s = parse_load_stats("nclients=10,bytesin=123456,bytesout=789012").unwrap();
197 assert_eq!(s.nclients, 10);
198 assert_eq!(s.bytesin, 123456);
199 assert_eq!(s.bytesout, 789012);
200 }
201
202 #[test]
203 fn load_stats_reordered() {
204 let s = parse_load_stats("bytesout=1,nclients=2,bytesin=3").unwrap();
205 assert_eq!(s.nclients, 2);
206 assert_eq!(s.bytesin, 3);
207 assert_eq!(s.bytesout, 1);
208 }
209
210 #[test]
211 fn load_stats_missing_field() {
212 let err = parse_load_stats("nclients=1,bytesin=2").unwrap_err();
213 assert!(matches!(err, ParseResponseError::MissingField("bytesout")));
214 }
215
216 #[test]
217 fn load_stats_non_numeric_value() {
218 let err = parse_load_stats("nclients=abc,bytesin=2,bytesout=3").unwrap_err();
219 assert!(matches!(
220 err,
221 ParseResponseError::InvalidInteger {
222 field: "nclients",
223 ..
224 }
225 ));
226 }
227
228 #[test]
229 fn load_stats_unexpected_field() {
230 let err = parse_load_stats("nclients=1,bytesin=2,bytesout=3,extra=99").unwrap_err();
231 assert!(matches!(err, ParseResponseError::UnexpectedField(f) if f == "extra"));
232 }
233
234 #[test]
237 fn hold_active() {
238 assert_eq!(parse_hold("hold=1"), Ok(true));
239 }
240
241 #[test]
242 fn hold_inactive() {
243 assert_eq!(parse_hold("hold=0"), Ok(false));
244 }
245
246 #[test]
247 fn hold_missing_prefix() {
248 assert!(parse_hold("garbage").is_err());
249 }
250
251 #[test]
252 fn hold_invalid_value() {
253 assert!(parse_hold("hold=maybe").is_err());
254 }
255
256 #[test]
259 fn version_roundtrip() {
260 let lines = vec![
261 "OpenVPN Version: OpenVPN 2.5.0".to_string(),
262 "Management Interface Version: 4".to_string(),
263 ];
264 let info = parse_version(&lines);
265 assert_eq!(info.management_version(), Some(4));
266 }
267}