clam_client/
response.rs

1//! The `response` module is responsibe for parsing the responses issued to use
2//! by ClamAV. To do so, it relies on two external crates, namely, `nom` and `chrono`.
3//!
4//! All structs and enums derive `Debug` for ease of client send debugging and development.
5
6use chrono::{DateTime, TimeZone, Utc};
7use client::ClamResult;
8use error::ClamError;
9use std::str::FromStr;
10
11/// `ClamStats` provides all of the metrics that Clam provides via the `STATS` command
12/// as at version 0.100. 
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[derive(Debug, PartialEq, PartialOrd)]
15pub struct ClamStats {
16    /// The number of `pools` available to ClamAV
17    pub pools: u64,
18    /// The state of the responding Clam Daemon
19    pub state: String,
20    /// The numbe of active threads owned by the Clam Daemon
21    pub threads_live: u64,
22    /// The number of idle threads owned by the Clam Daemon
23    pub threads_idle: u64,
24    /// The maximum number of threads the Clam Daemon can spawn
25    pub threads_max: u64,
26    /// The timeout (seconds) before a thread is determined to be idle
27    pub threads_idle_timeout_secs: u64,
28    /// The number of items in the queue awaiting processing
29    pub queue: u64,
30    /// Total memory allocated to the heap
31    pub mem_heap: String,
32    /// Ammount of mmap'd memory used
33    pub mem_mmap: String,
34    /// Total memory used by the daemon
35    pub mem_used: String,
36    /// Total memory available to the daemon not in use
37    pub mem_free: String,
38    /// Total memory re
39    pub mem_releasable: String,
40    /// Total number of pools in use by the daemon
41    pub pools_used: String,
42    /// Total number of pools available to the daemon
43    pub pools_total: String,
44}
45
46/// `ClamVersion` provdes all of the Clam meta-information provided by the `VERSION` command
47#[derive(Debug, PartialEq, PartialOrd)]
48pub struct ClamVersion {
49    /// The name and version number of the responding daemon
50    pub version_tag: String,
51    /// The build number of the responding daemon
52    pub build_number: u64,
53    /// The release date for the responding daemon
54    pub release_date: DateTime<Utc>,
55}
56
57/// `ClamScanResult` Provides a `match` 'friendly' interface for receiving the result of a scan.
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59#[derive(Debug, Clone, PartialEq, PartialOrd)]
60pub enum ClamScanResult {
61    /// An `Ok` response means that Clam found no virus in the given file/directory.
62    Ok,
63    /// A `Found` response means that Clam did find one or more virus('s) in the given file/directory,
64    /// the first value of `Found` is the location where the virus was found, and the second value is
65    /// the name of the virus detected.
66    ///
67    /// *Note*: When performing a stream scan, the location is redundant, and will always be `instream`.
68    Found(String, String),
69    /// An `Error` response means that Clam encountered an error whilst processing the request,
70    /// for example, if the given file/directory couldn't be found.
71    Error(String),
72}
73
74impl ClamScanResult {
75    /// `ClamScanResult::parse` takes a Clam scan result string and parses into into a `Vec<ClamScanResult`.
76    /// A vec must be used because Clam may scan multiple files in one request, or may encounter
77    /// multuple errors.
78    ///
79    /// *Note*: If performing a stream scan, the result will be converted to a single `ClamScanResult` by
80    /// the caller.
81    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    /// `ClamVersion::parse` takes a string returned from the Clam `VERSION` command and parses it
109    /// into a strongly typed struct assuming it retains a standard format of
110    /// `version tag/build no/publish datetime`
111    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    /// `ClamStats::parse` takes a statistics output of the Clam `STATS` command and uses
142    /// nom to parse that into a strongly typed struct.
143    ///
144    /// Given that this is likely to be the most volatile area of returned data, it is likely
145    /// that this will fail across different versions. This parses the data expected as of
146    /// version 0.100.0. If it cannot parse the data, then the result is returned in its
147    /// raw form insude `ClamError::InvalidData`.
148    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}