vault_audit_tools/utils/
reader.rs

1//! Smart file reader with automatic decompression support.
2//!
3//! Provides transparent decompression for .gz and .zst files,
4//! allowing analysis of compressed audit logs without manual extraction.
5//!
6//! # Supported Formats
7//!
8//! - Plain text files
9//! - Gzip compressed files (.gz)
10//! - Zstandard compressed files (.zst)
11//!
12//! # Examples
13//!
14//! ```no_run
15//! use vault_audit_tools::utils::reader::open_file;
16//! use std::io::{BufRead, BufReader};
17//!
18//! // Automatically handles .gz, .zst, or plain text
19//! let reader = open_file("audit.log.gz").unwrap();
20//! let buf_reader = BufReader::new(reader);
21//!
22//! for line in buf_reader.lines() {
23//!     let line = line.unwrap();
24//!     // Process line...
25//! }
26//! ```
27
28use anyhow::{Context, Result};
29use flate2::read::GzDecoder;
30use std::fs::File;
31use std::io::Read;
32use std::path::Path;
33
34/// Opens a file with automatic decompression based on extension.
35///
36/// Detects file type by extension:
37/// - `.gz` → Gzip decompression
38/// - `.zst` → Zstandard decompression
39/// - Otherwise → Plain file
40///
41/// # Arguments
42///
43/// * `path` - Path to the file (compressed or uncompressed)
44///
45/// # Returns
46///
47/// A `Read` trait object that transparently handles decompression
48///
49/// # Examples
50///
51/// ```no_run
52/// use vault_audit_tools::utils::reader::open_file;
53/// use std::io::Read;
54///
55/// let mut reader = open_file("audit.log.gz").unwrap();
56/// let mut contents = String::new();
57/// reader.read_to_string(&mut contents).unwrap();
58/// ```
59pub fn open_file(path: impl AsRef<Path>) -> Result<Box<dyn Read + Send>> {
60    let path = path.as_ref();
61    let file =
62        File::open(path).with_context(|| format!("Failed to open file: {}", path.display()))?;
63
64    let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
65
66    match extension {
67        "gz" => {
68            let decoder = GzDecoder::new(file);
69            Ok(Box::new(decoder))
70        }
71        "zst" => {
72            let decoder = zstd::Decoder::new(file).with_context(|| {
73                format!("Failed to create zstd decoder for: {}", path.display())
74            })?;
75            Ok(Box::new(decoder))
76        }
77        _ => Ok(Box::new(file)),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::io::{BufRead, BufReader, Write};
85    use tempfile::NamedTempFile;
86
87    #[test]
88    fn test_plain_file() {
89        let mut temp = NamedTempFile::new().unwrap();
90        writeln!(temp, "test line 1").unwrap();
91        writeln!(temp, "test line 2").unwrap();
92        temp.flush().unwrap();
93
94        let reader = open_file(temp.path()).unwrap();
95        let buf_reader = BufReader::new(reader);
96        let lines: Vec<String> = buf_reader.lines().collect::<Result<_, _>>().unwrap();
97
98        assert_eq!(lines.len(), 2);
99        assert_eq!(lines[0], "test line 1");
100        assert_eq!(lines[1], "test line 2");
101    }
102
103    #[test]
104    fn test_gzip_file() {
105        use flate2::write::GzEncoder;
106        use flate2::Compression;
107
108        let mut temp = NamedTempFile::with_suffix(".gz").unwrap();
109        {
110            let mut encoder = GzEncoder::new(&mut temp, Compression::default());
111            writeln!(encoder, "compressed line 1").unwrap();
112            writeln!(encoder, "compressed line 2").unwrap();
113            encoder.finish().unwrap();
114        }
115        temp.flush().unwrap();
116
117        let reader = open_file(temp.path()).unwrap();
118        let buf_reader = BufReader::new(reader);
119        let lines: Vec<String> = buf_reader.lines().collect::<Result<_, _>>().unwrap();
120
121        assert_eq!(lines.len(), 2);
122        assert_eq!(lines[0], "compressed line 1");
123        assert_eq!(lines[1], "compressed line 2");
124    }
125
126    #[test]
127    fn test_zstd_file() {
128        let mut temp = NamedTempFile::with_suffix(".zst").unwrap();
129        {
130            let mut encoder = zstd::Encoder::new(&mut temp, 3).unwrap();
131            writeln!(encoder, "zstd line 1").unwrap();
132            writeln!(encoder, "zstd line 2").unwrap();
133            encoder.finish().unwrap();
134        }
135        temp.flush().unwrap();
136
137        let reader = open_file(temp.path()).unwrap();
138        let buf_reader = BufReader::new(reader);
139        let lines: Vec<String> = buf_reader.lines().collect::<Result<_, _>>().unwrap();
140
141        assert_eq!(lines.len(), 2);
142        assert_eq!(lines[0], "zstd line 1");
143        assert_eq!(lines[1], "zstd line 2");
144    }
145}