pub mod compression;
pub mod error;
pub mod position_tracker;
pub mod result;
pub mod source;
#[cfg(any(feature = "tar", feature = "zip"))]
pub mod archive;
#[cfg(any(
feature = "grep-pdf",
feature = "grep-docx",
feature = "grep-xlsx",
feature = "grep-epub",
feature = "grep-odt"
))]
pub mod document;
pub use compression::CompressionFormat;
pub use error::{GrepError, GrepResult};
pub use position_tracker::PositionTracker;
pub use result::{GrepMatchResult, MatchLocation, SourceId};
pub use source::GrepPath;
#[cfg(any(feature = "tar", feature = "zip"))]
pub use archive::ArchiveFormat;
#[cfg(any(
feature = "grep-pdf",
feature = "grep-docx",
feature = "grep-xlsx",
feature = "grep-epub",
feature = "grep-odt"
))]
pub use document::{DocumentExtractor, DocumentExtractorConfig, DocumentFormat};
use std::fs::File;
use std::io::{self, BufReader, Read};
use std::path::Path;
use compression::create_decompressor;
#[cfg(feature = "tar")]
use archive::tar::TarArchiveReader;
#[cfg(feature = "zip")]
use archive::zip::ZipArchiveReader;
#[derive(Debug, Clone)]
pub struct GrepConfig {
pub auto_decompress: bool,
pub max_file_size: Option<u64>,
pub skip_binary: bool,
pub include_hidden: bool,
pub default_archive_filter: Option<String>,
pub extract_documents: bool,
pub enable_ocr: bool,
pub ocr_language: String,
}
impl Default for GrepConfig {
fn default() -> Self {
Self {
auto_decompress: true,
max_file_size: Some(100 * 1024 * 1024), skip_binary: true,
include_hidden: false,
default_archive_filter: None,
#[cfg(any(
feature = "grep-pdf",
feature = "grep-docx",
feature = "grep-xlsx",
feature = "grep-epub",
feature = "grep-odt"
))]
extract_documents: true,
#[cfg(not(any(
feature = "grep-pdf",
feature = "grep-docx",
feature = "grep-xlsx",
feature = "grep-epub",
feature = "grep-odt"
)))]
extract_documents: false,
enable_ocr: false,
ocr_language: "eng".to_string(),
}
}
}
impl GrepConfig {
pub fn new() -> Self {
Self::default()
}
pub fn no_decompress(mut self) -> Self {
self.auto_decompress = false;
self
}
pub fn max_size(mut self, bytes: u64) -> Self {
self.max_file_size = Some(bytes);
self
}
pub fn no_size_limit(mut self) -> Self {
self.max_file_size = None;
self
}
pub fn skip_binary_files(mut self, skip: bool) -> Self {
self.skip_binary = skip;
self
}
pub fn include_hidden_files(mut self, include: bool) -> Self {
self.include_hidden = include;
self
}
pub fn default_filter(mut self, pattern: impl Into<String>) -> Self {
self.default_archive_filter = Some(pattern.into());
self
}
pub fn extract_documents(mut self, extract: bool) -> Self {
self.extract_documents = extract;
self
}
pub fn enable_ocr(mut self, enable: bool) -> Self {
self.enable_ocr = enable;
self
}
pub fn ocr_language(mut self, lang: impl Into<String>) -> Self {
self.ocr_language = lang.into();
self
}
pub fn with_ocr(self, language: impl Into<String>) -> Self {
self.extract_documents(true)
.enable_ocr(true)
.ocr_language(language)
}
}
#[derive(Debug, Clone)]
pub struct GrepSource {
config: GrepConfig,
}
impl GrepSource {
pub fn new(config: GrepConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(GrepConfig::default())
}
pub fn config(&self) -> &GrepConfig {
&self.config
}
pub fn open(&self, path: &GrepPath) -> GrepResult<GrepEntryIterator> {
let fs_path = &path.filesystem_path;
if !fs_path.exists() {
return Err(GrepError::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("file not found: {}", fs_path.display()),
)));
}
if let Some(max_size) = self.config.max_file_size {
let metadata = fs_path.metadata()?;
if metadata.len() > max_size {
return Err(GrepError::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"file too large: {} bytes (limit: {} bytes)",
metadata.len(),
max_size
),
)));
}
}
let compression = if self.config.auto_decompress {
path.compression
} else {
CompressionFormat::None
};
#[cfg(any(feature = "tar", feature = "zip"))]
let archive = if self.config.auto_decompress {
path.archive.clone()
} else {
ArchiveFormat::None
};
#[cfg(not(any(feature = "tar", feature = "zip")))]
let archive = ();
let filter = path
.archive_filter
.clone()
.or_else(|| self.config.default_archive_filter.clone());
#[cfg(any(feature = "tar", feature = "zip"))]
match archive {
ArchiveFormat::None => {
self.open_plain_file(fs_path, compression)
}
#[cfg(feature = "tar")]
ArchiveFormat::Tar {
compression: tar_compression,
} => self.open_tar_archive(fs_path, tar_compression, filter),
#[cfg(not(feature = "tar"))]
ArchiveFormat::Tar { .. } => Err(GrepError::FeatureNotEnabled {
feature: "tar".to_string(),
}),
#[cfg(feature = "zip")]
ArchiveFormat::Zip => self.open_zip_archive(fs_path, filter),
#[cfg(not(feature = "zip"))]
ArchiveFormat::Zip => Err(GrepError::FeatureNotEnabled {
feature: "zip".to_string(),
}),
}
#[cfg(not(any(feature = "tar", feature = "zip")))]
{
let _ = (archive, filter);
self.open_plain_file(fs_path, compression)
}
}
fn open_plain_file(
&self,
path: &Path,
compression: CompressionFormat,
) -> GrepResult<GrepEntryIterator> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let decompressed = create_decompressor(reader, compression)?;
let mut content = Vec::new();
let mut reader = decompressed;
reader.read_to_end(&mut content)?;
#[cfg(any(
feature = "grep-pdf",
feature = "grep-docx",
feature = "grep-xlsx",
feature = "grep-epub",
feature = "grep-odt"
))]
if self.config.extract_documents {
let doc_format = document::detect_document_format(&content, Some(path));
if doc_format != document::DocumentFormat::None {
let extractor_config = document::DocumentExtractorConfig {
enable_ocr: self.config.enable_ocr,
ocr_language: self.config.ocr_language.clone(),
max_size: self.config.max_file_size,
skip_on_error: false,
};
let extractor = document::DocumentExtractor::with_config(extractor_config);
let text = extractor.extract_format(&content, doc_format)?;
content = text.into_bytes();
}
}
if self.config.skip_binary && compression::detection::is_binary(&content) {
return Err(GrepError::BinaryFile(path.to_path_buf()));
}
let source_id = SourceId::file(path);
let entries = vec![GrepEntry { source_id, content }];
Ok(GrepEntryIterator {
inner: GrepEntryIteratorInner::Single(entries.into_iter()),
})
}
#[cfg(feature = "tar")]
fn open_tar_archive(
&self,
path: &Path,
compression: CompressionFormat,
filter: Option<String>,
) -> GrepResult<GrepEntryIterator> {
let mut reader = TarArchiveReader::open(path, compression)?;
if let Some(pattern) = filter {
reader = reader.with_pattern(&pattern)?;
}
let entries = reader.entries()?;
let collected: Vec<GrepResult<GrepEntry>> = entries
.map(|result| {
result.map(|(source_id, _meta, content)| GrepEntry { source_id, content })
})
.collect();
Ok(GrepEntryIterator {
inner: GrepEntryIteratorInner::Archive(collected.into_iter()),
})
}
#[cfg(feature = "zip")]
fn open_zip_archive(
&self,
path: &Path,
filter: Option<String>,
) -> GrepResult<GrepEntryIterator> {
let mut reader = ZipArchiveReader::open(path)?;
if let Some(pattern) = filter {
reader = reader.with_pattern(&pattern)?;
}
let entries = reader.entries()?;
let collected: Vec<GrepResult<GrepEntry>> = entries
.map(|result| {
result.map(|(source_id, _meta, content)| GrepEntry { source_id, content })
})
.collect();
Ok(GrepEntryIterator {
inner: GrepEntryIteratorInner::Archive(collected.into_iter()),
})
}
pub fn open_str(&self, path: &str) -> GrepResult<GrepEntryIterator> {
let grep_path = GrepPath::parse(path)?;
self.open(&grep_path)
}
}
impl Default for GrepSource {
fn default() -> Self {
Self::with_defaults()
}
}
#[derive(Debug, Clone)]
pub struct GrepEntry {
source_id: SourceId,
content: Vec<u8>,
}
impl GrepEntry {
pub fn source_id(&self) -> &SourceId {
&self.source_id
}
pub fn content(&self) -> &[u8] {
&self.content
}
pub fn content_str(&self) -> GrepResult<&str> {
std::str::from_utf8(&self.content).map_err(|e| GrepError::InvalidUtf8 {
file_path: self.source_id.display(),
message: e.to_string(),
})
}
pub fn into_content(self) -> Vec<u8> {
self.content
}
pub fn is_archive_entry(&self) -> bool {
self.source_id.is_archive_entry()
}
}
pub struct GrepEntryIterator {
inner: GrepEntryIteratorInner,
}
enum GrepEntryIteratorInner {
Single(std::vec::IntoIter<GrepEntry>),
Archive(std::vec::IntoIter<GrepResult<GrepEntry>>),
}
impl Iterator for GrepEntryIterator {
type Item = GrepResult<GrepEntry>;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
GrepEntryIteratorInner::Single(iter) => iter.next().map(Ok),
GrepEntryIteratorInner::Archive(iter) => iter.next(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grep_config_default() {
let config = GrepConfig::default();
assert!(config.auto_decompress);
assert!(config.skip_binary);
assert_eq!(config.max_file_size, Some(100 * 1024 * 1024));
}
#[test]
fn test_grep_config_builder() {
let config = GrepConfig::new()
.no_decompress()
.no_size_limit()
.skip_binary_files(false)
.include_hidden_files(true);
assert!(!config.auto_decompress);
assert!(config.max_file_size.is_none());
assert!(!config.skip_binary);
assert!(config.include_hidden);
}
#[test]
fn test_grep_entry() {
let entry = GrepEntry {
source_id: SourceId::file("test.txt"),
content: b"Hello, World!".to_vec(),
};
assert_eq!(entry.content(), b"Hello, World!");
assert_eq!(entry.content_str().expect("valid utf8"), "Hello, World!");
assert!(!entry.is_archive_entry());
}
#[test]
fn test_grep_entry_archive() {
let entry = GrepEntry {
source_id: SourceId::archive_entry("archive.tar.gz", "dir/file.txt"),
content: b"test content".to_vec(),
};
assert!(entry.is_archive_entry());
assert_eq!(entry.source_id().display(), "archive.tar.gz:dir/file.txt");
}
}