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}