1use chrono::{DateTime, TimeZone, Utc};
7use client::ClamResult;
8use error::ClamError;
9use std::str::FromStr;
10
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[derive(Debug, PartialEq, PartialOrd)]
15pub struct ClamStats {
16 pub pools: u64,
18 pub state: String,
20 pub threads_live: u64,
22 pub threads_idle: u64,
24 pub threads_max: u64,
26 pub threads_idle_timeout_secs: u64,
28 pub queue: u64,
30 pub mem_heap: String,
32 pub mem_mmap: String,
34 pub mem_used: String,
36 pub mem_free: String,
38 pub mem_releasable: String,
40 pub pools_used: String,
42 pub pools_total: String,
44}
45
46#[derive(Debug, PartialEq, PartialOrd)]
48pub struct ClamVersion {
49 pub version_tag: String,
51 pub build_number: u64,
53 pub release_date: DateTime<Utc>,
55}
56
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59#[derive(Debug, Clone, PartialEq, PartialOrd)]
60pub enum ClamScanResult {
61 Ok,
63 Found(String, String),
69 Error(String),
72}
73
74impl ClamScanResult {
75 pub fn parse<T: AsRef<str>>(s_string: T) -> Vec<ClamScanResult> {
82 s_string
83 .as_ref()
84 .split('\0')
85 .filter(|s| s != &"")
86 .map(|s| {
87 if s.ends_with("OK") {
88 return ClamScanResult::Ok;
89 }
90
91 if s.contains("FOUND") {
92 let mut split = s.split_whitespace();
93 let path: String = split.next().unwrap().trim_right_matches(':').to_owned();
94 let virus = split
95 .take_while(|s| !s.starts_with("FOUND"))
96 .collect::<String>();
97
98 return ClamScanResult::Found(path, virus);
99 }
100
101 ClamScanResult::Error(s.to_owned())
102 })
103 .collect::<Vec<ClamScanResult>>()
104 }
105}
106
107impl ClamVersion {
108 pub fn parse(v_string: String) -> ClamResult<Self> {
112 let parts: Vec<String> = v_string
113 .trim_right_matches('\0')
114 .split('/')
115 .map(|s| s.to_owned())
116 .collect();
117
118 if parts.len() != 3 {
119 return Err(ClamError::InvalidData(v_string));
120 }
121
122 let bn = match parts[1].parse() {
123 Ok(v) => v,
124 Err(e) => return Err(ClamError::IntParseError(e)),
125 };
126
127 let dt = match Utc.datetime_from_str(&parts[2], "%a %b %e %T %Y") {
128 Ok(v) => v,
129 Err(e) => return Err(ClamError::DateParseError(e)),
130 };
131
132 Ok(ClamVersion {
133 version_tag: parts[0].to_owned(),
134 build_number: bn,
135 release_date: dt,
136 })
137 }
138}
139
140impl ClamStats {
141 pub fn parse(s_string: &str) -> ClamResult<Self> {
149 match parse_stats(s_string) {
150 Ok(v) => Ok(v.1),
151 Err(_) => Err(ClamError::InvalidData(s_string.to_owned())),
152 }
153 }
154}
155
156named!(parse_stats<&str, ClamStats>,
157 do_parse!(
158 tag!("POOLS: ") >>
159 pools: map_res!(take_until_and_consume!("\n\nSTATE: "), u64::from_str) >>
160 state: map_res!(take_until_and_consume!("\nTHREADS: live "), FromStr::from_str) >>
161 threads_live: map_res!(take_until_and_consume!(" idle "), u64::from_str) >>
162 threads_idle: map_res!(take_until_and_consume!(" max "), u64::from_str) >>
163 threads_max: map_res!(take_until_and_consume!(" idle-timeout "), u64::from_str) >>
164 threads_idle_timeout_secs: map_res!(take_until_and_consume!("\nQUEUE: "), u64::from_str) >>
165 queue: map_res!(take_until_and_consume!(" items\n"), u64::from_str) >>
166 take_until_and_consume!("heap ") >>
167 mem_heap: map_res!(take_until_and_consume!(" mmap "), FromStr::from_str) >>
168 mem_mmap: map_res!(take_until_and_consume!(" used "), FromStr::from_str) >>
169 mem_used: map_res!(take_until_and_consume!(" free "), FromStr::from_str) >>
170 mem_free: map_res!(take_until_and_consume!(" releasable "), FromStr::from_str) >>
171 mem_releasable: map_res!(take_until_and_consume!(" pools "), FromStr::from_str) >>
172 take_until_and_consume!("pools_used ") >>
173 pools_used: map_res!(take_until_and_consume!(" pools_total "), FromStr::from_str) >>
174 pools_total: map_res!(take_until!("\n"), FromStr::from_str) >>
175 (
176 ClamStats {
177 pools,
178 state,
179 threads_live,
180 threads_idle,
181 threads_max,
182 threads_idle_timeout_secs,
183 queue,
184 mem_heap,
185 mem_mmap,
186 mem_used,
187 mem_free,
188 mem_releasable,
189 pools_used,
190 pools_total
191 }
192 )
193 )
194);
195
196#[cfg(test)]
197mod tests {
198 use chrono::prelude::*;
199 use response;
200
201 static VERSION_STRING: &'static str = "ClamAV 0.100.0/24802/Wed Aug 1 08:43:37 2018\0";
202 static STATS_STRING: &'static str = "POOLS: 1\n\nSTATE: VALID PRIMARY\nTHREADS: live 1 idle 0 max 12 idle-timeout 30\nQUEUE: 0 items\n\tSTATS 0.000394\n\nMEMSTATS: heap 9.082M mmap 0.000M used 6.902M free 2.184M releasable 0.129M pools 1 pools_used 565.979M pools_total 565.999M\nEND\0";
203
204 #[test]
205 fn test_version_parse_version_tag() {
206 let raw = VERSION_STRING.to_owned();
207 let parsed = response::ClamVersion::parse(raw).unwrap();
208 assert_eq!(parsed.version_tag, "ClamAV 0.100.0".to_string());
209 }
210
211 #[test]
212 fn test_version_parse_build_number() {
213 let raw = VERSION_STRING.to_owned();
214 let parsed = response::ClamVersion::parse(raw).unwrap();
215 assert_eq!(parsed.build_number, 24802);
216 }
217
218 #[test]
219 fn test_version_parse_publish_dt() {
220 let raw = VERSION_STRING.to_owned();
221 let parsed = response::ClamVersion::parse(raw).unwrap();
222 assert_eq!(
223 parsed.release_date,
224 Utc.datetime_from_str("Wed Aug 1 08:43:37 2018", "%a %b %e %T %Y")
225 .unwrap()
226 );
227 }
228
229 #[test]
230 fn test_result_parse_ok() {
231 let raw = "/some/file: OK\0";
232 let parsed = response::ClamScanResult::parse(raw);
233 assert_eq!(parsed[0], response::ClamScanResult::Ok);
234 }
235
236 #[test]
237 fn test_result_parse_found() {
238 let raw = "/some/file: SOME_BAD-Virus FOUND\0";
239 let parsed = response::ClamScanResult::parse(raw);
240 assert_eq!(
241 parsed[0],
242 response::ClamScanResult::Found("/some/file".to_string(), "SOME_BAD-Virus".to_string())
243 );
244 }
245
246 #[test]
247 fn test_result_parse_multi_found() {
248 let raw = "/some/file: SOME_BAD-Virus FOUND\0/some/other_file: SOME_V*BAD-Virus FOUND\0";
249 let parsed = response::ClamScanResult::parse(raw);
250 assert_eq!(
251 parsed[0],
252 response::ClamScanResult::Found("/some/file".to_string(), "SOME_BAD-Virus".to_string())
253 );
254 assert_eq!(
255 parsed[1],
256 response::ClamScanResult::Found(
257 "/some/other_file".to_string(),
258 "SOME_V*BAD-Virus".to_string()
259 )
260 );
261 }
262
263 #[test]
264 fn test_result_parse_error() {
265 let raw = "/some/file: lstat() failed or some other random error\0";
266 let parsed = response::ClamScanResult::parse(raw);
267 assert_eq!(
268 parsed[0],
269 response::ClamScanResult::Error(
270 "/some/file: lstat() failed or some other random error".to_string()
271 )
272 );
273 }
274
275 #[test]
276 fn test_stats_parse_pools() {
277 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
278 assert_eq!(parsed.pools, 1);
279 }
280
281 #[test]
282 fn test_stats_parse_state() {
283 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
284 assert_eq!(parsed.state, "VALID PRIMARY".to_string());
285 }
286
287 #[test]
288 fn test_stats_parse_live_threads() {
289 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
290 assert_eq!(parsed.threads_live, 1);
291 }
292
293 #[test]
294 fn test_stats_parse_idle_threads() {
295 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
296 assert_eq!(parsed.threads_idle, 0);
297 }
298
299 #[test]
300 fn test_stats_parse_max_threads() {
301 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
302 assert_eq!(parsed.threads_max, 12);
303 }
304
305 #[test]
306 fn test_stats_parse_threads_timeout() {
307 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
308 assert_eq!(parsed.threads_idle_timeout_secs, 30);
309 }
310
311 #[test]
312 fn test_stats_parse_queue() {
313 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
314 assert_eq!(parsed.queue, 0);
315 }
316
317 #[test]
318 fn test_stats_parse_mem_heap() {
319 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
320 assert_eq!(parsed.mem_heap, "9.082M".to_string());
321 }
322
323 #[test]
324 fn test_stats_parse_mem_mmap() {
325 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
326 assert_eq!(parsed.mem_mmap, "0.000M".to_string());
327 }
328
329 #[test]
330 fn test_stats_parse_mem_used() {
331 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
332 assert_eq!(parsed.mem_used, "6.902M".to_string());
333 }
334
335 #[test]
336 fn test_stats_parse_mem_free() {
337 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
338 assert_eq!(parsed.mem_free, "2.184M".to_string());
339 }
340
341 #[test]
342 fn test_stats_parse_mem_releaseable() {
343 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
344 assert_eq!(parsed.mem_releasable, "0.129M".to_string());
345 }
346
347 #[test]
348 fn test_stats_parse_pools_used() {
349 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
350 assert_eq!(parsed.pools_used, "565.979M".to_string());
351 }
352
353 #[test]
354 fn test_stats_parse_pools_total() {
355 let parsed = response::ClamStats::parse(STATS_STRING).unwrap();
356 assert_eq!(parsed.pools_total, "565.999M".to_string());
357 }
358}