clamav_client/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4#[cfg(feature = "tokio")]
5/// Use the feature flag "tokio" or "tokio-stream" to enable this module
6pub mod tokio;
7
8#[cfg(feature = "async-std")]
9/// Use the feature flag "async-std" to enable this module
10pub mod async_std;
11
12#[cfg(feature = "smol")]
13/// Use the feature flag "smol" to enable this module
14pub mod smol;
15
16use std::{
17    fs::File,
18    io::{self, Error, Read, Write},
19    net::{TcpStream, ToSocketAddrs},
20    path::Path,
21    str::{self, Utf8Error},
22};
23
24#[cfg(unix)]
25use std::os::unix::net::UnixStream;
26
27/// Custom result type
28pub type IoResult = Result<Vec<u8>, Error>;
29
30/// Custom result type
31pub type Utf8Result = Result<bool, Utf8Error>;
32
33/// Default chunk size in bytes for reading data during scanning
34const DEFAULT_CHUNK_SIZE: usize = 4096;
35
36/// ClamAV commands
37const PING: &[u8; 6] = b"zPING\0";
38const RELOAD: &[u8; 8] = b"zRELOAD\0";
39const VERSION: &[u8; 9] = b"zVERSION\0";
40const SHUTDOWN: &[u8; 10] = b"zSHUTDOWN\0";
41const INSTREAM: &[u8; 10] = b"zINSTREAM\0";
42const END_OF_STREAM: &[u8; 4] = &[0, 0, 0, 0];
43
44/// ClamAV's response to a PING request
45pub const PONG: &[u8; 5] = b"PONG\0";
46
47/// ClamAV's response to a RELOAD request
48pub const RELOADING: &[u8; 10] = b"RELOADING\0";
49
50fn send_command<RW: Read + Write>(
51    mut stream: RW,
52    command: &[u8],
53    expected_response_length: Option<usize>,
54) -> IoResult {
55    stream.write_all(command)?;
56    stream.flush()?;
57
58    let mut response = match expected_response_length {
59        Some(len) => Vec::with_capacity(len),
60        None => Vec::new(),
61    };
62
63    stream.read_to_end(&mut response)?;
64    Ok(response)
65}
66
67fn scan<R: Read, RW: Read + Write>(
68    mut input: R,
69    chunk_size: Option<usize>,
70    mut stream: RW,
71) -> IoResult {
72    stream.write_all(INSTREAM)?;
73
74    let chunk_size = chunk_size
75        .unwrap_or(DEFAULT_CHUNK_SIZE)
76        .min(u32::MAX as usize);
77    let mut buffer = vec![0; chunk_size];
78    loop {
79        let len = input.read(&mut buffer[..])?;
80        if len != 0 {
81            stream.write_all(&(len as u32).to_be_bytes())?;
82            stream.write_all(&buffer[..len])?;
83        } else {
84            stream.write_all(END_OF_STREAM)?;
85            stream.flush()?;
86            break;
87        }
88    }
89
90    let mut response = Vec::new();
91    stream.read_to_end(&mut response)?;
92    Ok(response)
93}
94
95/// Checks whether the ClamAV response indicates that the scanned content is
96/// clean or contains a virus
97///
98/// # Example
99///
100/// ```
101/// let clamd_tcp = clamav_client::Tcp{ host_address: "localhost:3310" };
102/// let response = clamav_client::scan_buffer(b"clean data", clamd_tcp, None).unwrap();
103/// let data_clean = clamav_client::clean(&response).unwrap();
104/// # assert_eq!(data_clean, true);
105/// ```
106///
107/// # Returns
108///
109/// An [`Utf8Result`] containing the scan result as [`bool`]
110///
111pub fn clean(response: &[u8]) -> Utf8Result {
112    let response = str::from_utf8(response)?;
113    Ok(response.contains("OK") && !response.contains("FOUND"))
114}
115
116/// Use a TCP connection to communicate with a ClamAV server
117#[derive(Copy, Clone)]
118pub struct Tcp<A: ToSocketAddrs> {
119    /// The address (host and port) of the ClamAV server
120    pub host_address: A,
121}
122
123/// Use a Unix socket connection to communicate with a ClamAV server
124#[derive(Copy, Clone)]
125#[cfg(unix)]
126pub struct Socket<P: AsRef<Path>> {
127    /// The socket file path of the ClamAV server
128    pub socket_path: P,
129}
130
131/// The communication protocol to use
132pub trait TransportProtocol {
133    /// Bidirectional stream
134    type Stream: Read + Write;
135
136    /// Converts the protocol instance into the corresponding stream
137    fn connect(&self) -> io::Result<Self::Stream>;
138}
139
140impl<A: ToSocketAddrs> TransportProtocol for Tcp<A> {
141    type Stream = TcpStream;
142
143    fn connect(&self) -> io::Result<Self::Stream> {
144        TcpStream::connect(&self.host_address)
145    }
146}
147
148#[cfg(unix)]
149impl<P: AsRef<Path>> TransportProtocol for Socket<P> {
150    type Stream = UnixStream;
151
152    fn connect(&self) -> io::Result<Self::Stream> {
153        UnixStream::connect(&self.socket_path)
154    }
155}
156
157impl<T: TransportProtocol> TransportProtocol for &T {
158    type Stream = T::Stream;
159
160    fn connect(&self) -> io::Result<Self::Stream> {
161        TransportProtocol::connect(*self)
162    }
163}
164
165/// Sends a ping request to ClamAV
166///
167/// This function establishes a connection to a ClamAV server and sends the PING
168/// command to it. If the server is available, it responds with [`PONG`].
169///
170/// # Arguments
171///
172/// * `connection`: The connection type to use - either TCP or a Unix socket connection
173///
174/// # Returns
175///
176/// An [`IoResult`] containing the server's response as a vector of bytes
177///
178/// # Example
179///
180/// ```
181/// let clamd_tcp = clamav_client::Tcp{ host_address: "localhost:3310" };
182/// let clamd_available = match clamav_client::ping(clamd_tcp) {
183///     Ok(ping_response) => ping_response == clamav_client::PONG,
184///     Err(_) => false,
185/// };
186/// # assert!(clamd_available);
187/// ```
188///
189pub fn ping<T: TransportProtocol>(connection: T) -> IoResult {
190    let stream = connection.connect()?;
191    send_command(stream, PING, Some(PONG.len()))
192}
193
194/// Reloads the virus databases
195///
196/// This function establishes a connection to a ClamAV server and sends the
197/// RELOAD command to it. If the server is available, it responds with
198/// [`RELOADING`].
199///
200/// # Arguments
201///
202/// * `connection`: The connection type to use - either TCP or a Unix socket connection
203///
204/// # Returns
205///
206/// An [`IoResult`] containing the server's response as a vector of bytes
207///
208/// # Example
209///
210/// ```
211/// let clamd_tcp = clamav_client::Tcp{ host_address: "localhost:3310" };
212/// let response = clamav_client::reload(clamd_tcp).unwrap();
213/// # assert!(response == clamav_client::RELOADING);
214/// ```
215///
216pub fn reload<T: TransportProtocol>(connection: T) -> IoResult {
217    let stream = connection.connect()?;
218    send_command(stream, RELOAD, Some(RELOADING.len()))
219}
220
221/// Gets the version number from ClamAV
222///
223/// This function establishes a connection to a ClamAV server and sends the
224/// VERSION command to it. If the server is available, it responds with its
225/// version number.
226///
227/// # Arguments
228///
229/// * `connection`: The connection type to use - either TCP or a Unix socket connection
230///
231/// # Returns
232///
233/// An [`IoResult`] containing the server's response as a vector of bytes
234///
235/// # Example
236///
237/// ```
238/// let clamd_tcp = clamav_client::Tcp{ host_address: "localhost:3310" };
239/// let version = clamav_client::get_version(clamd_tcp).unwrap();
240/// # assert!(version.starts_with(b"ClamAV"));
241/// ```
242///
243pub fn get_version<T: TransportProtocol>(connection: T) -> IoResult {
244    let stream = connection.connect()?;
245    send_command(stream, VERSION, None)
246}
247
248/// Scans a file for viruses
249///
250/// This function reads data from a file located at the specified `file_path`
251/// and streams it to a ClamAV server for scanning.
252///
253/// # Arguments
254///
255/// * `file_path`: The path to the file to be scanned
256/// * `connection`: The connection type to use - either TCP or a Unix socket connection
257/// * `chunk_size`: An optional chunk size for reading data. If [`None`], a default chunk size is used
258///
259/// # Returns
260///
261/// An [`IoResult`] containing the server's response as a vector of bytes
262///
263pub fn scan_file<P: AsRef<Path>, T: TransportProtocol>(
264    file_path: P,
265    connection: T,
266    chunk_size: Option<usize>,
267) -> IoResult {
268    let file = File::open(file_path)?;
269    let stream = connection.connect()?;
270    scan(file, chunk_size, stream)
271}
272
273/// Scans a data buffer for viruses
274///
275/// This function streams the provided `buffer` data to a ClamAV server for
276/// scanning.
277///
278/// # Arguments
279///
280/// * `buffer`: The data to be scanned
281/// * `connection`: The connection type to use - either TCP or a Unix socket connection
282/// * `chunk_size`: An optional chunk size for reading data. If [`None`], a default chunk size is used
283///
284/// # Returns
285///
286/// An [`IoResult`] containing the server's response as a vector of bytes
287///
288pub fn scan_buffer<T: TransportProtocol>(
289    buffer: &[u8],
290    connection: T,
291    chunk_size: Option<usize>,
292) -> IoResult {
293    let stream = connection.connect()?;
294    scan(buffer, chunk_size, stream)
295}
296
297/// Shuts down a ClamAV server
298///
299/// This function establishes a connection to a ClamAV server and sends the
300/// SHUTDOWN command to it. If the server is available, it will perform a clean
301/// exit and shut itself down. The response will be empty.
302///
303/// # Arguments
304///
305/// * `connection`: The connection type to use - either TCP or a Unix socket connection
306///
307/// # Returns
308///
309/// An [`IoResult`] containing the server's response
310///
311pub fn shutdown<T: TransportProtocol>(connection: T) -> IoResult {
312    let stream = connection.connect()?;
313    send_command(stream, SHUTDOWN, None)
314}