use std::io;
use std::path::Path;
use grep_searcher::{
BinaryDetection, Searcher, SearcherBuilder, Sink, SinkContext, SinkFinish, SinkMatch,
};
pub struct SearchOpts {
pub case_insensitive: bool,
pub smart_case: bool,
pub word: bool,
pub fixed: bool,
pub invert: bool,
pub before: usize,
pub after: usize,
pub line_numbers: bool,
pub text: bool,
pub max_count: Option<u64>,
pub heap_limit: usize,
pub crlf: bool,
}
impl Default for SearchOpts {
fn default() -> Self {
Self {
case_insensitive: false,
smart_case: false,
word: false,
fixed: false,
invert: false,
before: 0,
after: 0,
line_numbers: true,
text: false,
max_count: None,
heap_limit: 16 * 1024 * 1024,
crlf: false,
}
}
}
pub fn build_matcher(
pats: &[String],
o: &SearchOpts,
) -> anyhow::Result<grep_regex::RegexMatcher> {
let mut b = grep_regex::RegexMatcherBuilder::new();
b.case_insensitive(o.case_insensitive)
.case_smart(o.smart_case)
.word(o.word)
.fixed_strings(o.fixed)
.multi_line(false)
.crlf(o.crlf);
Ok(b.build_many(pats)?)
}
pub fn build_searcher(o: &SearchOpts) -> Searcher {
let mut b = SearcherBuilder::new();
b.line_number(o.line_numbers)
.before_context(o.before)
.after_context(o.after)
.invert_match(o.invert)
.heap_limit(Some(o.heap_limit))
.binary_detection(if o.text {
BinaryDetection::none()
} else {
BinaryDetection::quit(b'\x00')
});
if o.crlf {
b.line_terminator(grep_matcher::LineTerminator::crlf());
}
b.build()
}
pub struct MaxCount<S> {
inner: S,
limit: Option<u64>,
seen: u64,
binary_at: Option<u64>,
}
impl<S: Sink> MaxCount<S> {
pub fn new(inner: S, limit: Option<u64>) -> Self {
Self { inner, limit, seen: 0, binary_at: None }
}
pub fn saw_match(&self) -> bool {
self.seen > 0
}
pub fn count_suppressed_by_binary(&self) -> bool {
self.seen > 0 && self.binary_at.is_some()
}
}
impl<S: Sink> Sink for MaxCount<S> {
type Error = S::Error;
fn matched(&mut self, s: &Searcher, m: &SinkMatch<'_>) -> Result<bool, S::Error> {
self.seen += 1;
if !self.inner.matched(s, m)? {
return Ok(false);
}
Ok(match self.limit {
Some(n) => self.seen < n,
None => true,
})
}
fn context(&mut self, s: &Searcher, c: &SinkContext<'_>) -> Result<bool, S::Error> {
self.inner.context(s, c)
}
fn context_break(&mut self, s: &Searcher) -> Result<bool, S::Error> {
self.inner.context_break(s)
}
fn binary_data(&mut self, s: &Searcher, off: u64) -> Result<bool, S::Error> {
self.inner.binary_data(s, off)
}
fn begin(&mut self, s: &Searcher) -> Result<bool, S::Error> {
self.seen = 0;
self.binary_at = None;
self.inner.begin(s)
}
fn finish(&mut self, s: &Searcher, f: &SinkFinish) -> Result<(), S::Error> {
self.binary_at = f.binary_byte_offset();
self.inner.finish(s, f)
}
}
pub fn display_path(archive: &Path, member: &str, sep: char) -> String {
if member.is_empty() {
archive.display().to_string()
} else {
format!("{}{}{}", archive.display(), sep, member)
}
}
pub struct SearchCtx<'a> {
pub matcher: &'a grep_regex::RegexMatcher,
pub sep: char,
pub globs: Option<&'a globset::GlobSet>,
pub max_count: Option<u64>,
pub drop_cache: bool,
pub count_mode: bool,
}
fn excluded_plain_file(member: &str, path: &Path, globs: Option<&globset::GlobSet>) -> bool {
member.is_empty() && globs.is_some_and(|set| !set.is_match(path))
}
pub struct MemberVerdict {
pub matched: bool,
pub count_suppressed: bool,
}
fn search_member(
printer: &mut crate::print::Printer<'_>,
m: &grep_regex::RegexMatcher,
s: &mut Searcher,
r: &mut dyn io::Read,
display: &str,
max_count: Option<u64>,
) -> io::Result<MemberVerdict> {
macro_rules! run {
($p:expr) => {{
let sink = $p.sink_with_path(m, display);
let mut capped = MaxCount::new(sink, max_count);
let res = s.search_reader(m, r, &mut capped);
let hit = capped.saw_match();
let suppressed = capped.count_suppressed_by_binary();
res.map_err(io::Error::other)?;
MemberVerdict { matched: hit, count_suppressed: suppressed }
}};
}
Ok(match printer {
crate::print::Printer::Standard(p) => run!(p),
crate::print::Printer::Summary(p) => run!(p),
crate::print::Printer::Json(p) => run!(p),
})
}
pub fn search_archive(
ctx: &SearchCtx<'_>,
path: &Path,
s: &mut Searcher,
printer: &mut crate::print::Printer<'_>,
strategy: crate::inflate::Strategy,
) -> crate::archive::Outcome {
let (m, sep, globs, max_count) = (ctx.matcher, ctx.sep, ctx.globs, ctx.max_count);
let rdr = match crate::archive::open_decoded_dropping(path, strategy, ctx.drop_cache) {
Ok(r) => r,
Err(e) => {
let mut o = crate::archive::Outcome::default();
o.errors.push(format!("{}: {e}", path.display()));
return o;
}
};
let mut matched = false;
let mut notes: Vec<String> = Vec::new();
let mut out = crate::archive::for_each_member(rdr, globs, |member, r| {
if excluded_plain_file(member, path, globs) {
return Ok(());
}
let display = display_path(path, member, sep);
let v = search_member(printer, m, s, r, &display, max_count)?;
matched |= v.matched;
if ctx.count_mode && v.count_suppressed {
notes.push(format!("{display}: binary data; count suppressed"));
}
Ok(())
});
out.matched = matched;
out.notes = notes;
out
}
pub fn list_members(
ctx: &SearchCtx<'_>,
path: &Path,
strategy: crate::inflate::Strategy,
out: &mut dyn io::Write,
) -> crate::archive::Outcome {
let (sep, globs) = (ctx.sep, ctx.globs);
let rdr = match crate::archive::open_decoded_dropping(path, strategy, ctx.drop_cache) {
Ok(r) => r,
Err(e) => {
let mut o = crate::archive::Outcome::default();
o.errors.push(format!("{}: {e}", path.display()));
return o;
}
};
let mut listed = false;
let mut outcome = crate::archive::for_each_member(rdr, globs, |member, _| {
if excluded_plain_file(member, path, globs) {
return Ok(());
}
listed = true;
writeln!(out, "{}", display_path(path, member, sep))
});
outcome.matched = listed;
outcome
}