use std::path::Path;
use memchr::{memchr, memchr_iter, memmem, memrchr};
use regex::bytes::Regex;
use crate::index::is_binary;
use crate::SearchMatch;
pub fn verify_literal(
pattern: &str,
path: &Path,
content: &[u8],
skip_line_content: bool,
) -> Vec<SearchMatch> {
if is_binary(content) {
return Vec::new(); }
let finder = memmem::Finder::new(pattern.as_bytes());
let mut matches = Vec::new();
let mut last_line_start = usize::MAX;
let mut current_line_num = 1;
let mut last_newline_counted_up_to = 0;
let mut current_line_end = 0;
for match_start in finder.find_iter(content) {
if match_start < current_line_end {
continue;
}
let from = last_newline_counted_up_to;
let line_start = match memrchr(b'\n', &content[from..match_start]) {
Some(pos) => from + pos + 1,
None => from,
};
if line_start == last_line_start {
continue;
}
let next_newline = memchr(b'\n', &content[match_start..]);
let line_end = match next_newline {
Some(pos) => match_start + pos,
None => content.len(),
};
let line_content_end = if line_end > line_start && content[line_end - 1] == b'\r' {
line_end - 1
} else {
line_end
};
if line_start > last_newline_counted_up_to {
let newline_count =
memchr_iter(b'\n', &content[last_newline_counted_up_to..line_start]).count();
current_line_num += newline_count as u32;
last_newline_counted_up_to = line_start;
}
matches.push(SearchMatch {
path: path.to_path_buf(),
line_number: current_line_num,
line_content: if skip_line_content {
Vec::new()
} else {
content[line_start..line_content_end].to_vec()
},
byte_offset: match_start as u64,
submatch_start: match_start - line_start,
submatch_end: (match_start + pattern.len()).min(line_content_end) - line_start,
});
last_line_start = line_start;
current_line_end = line_end;
}
matches
}
pub fn verify_regex(
re: &Regex,
path: &Path,
content: &[u8],
skip_line_content: bool,
) -> Vec<SearchMatch> {
if is_binary(content) {
return Vec::new(); }
let mut matches = Vec::new();
let mut last_line_start = usize::MAX;
let mut current_line_num = 1;
let mut last_newline_counted_up_to = 0;
let mut current_line_end = 0;
for m in re.find_iter(content) {
let match_start = m.start();
let match_end = m.end();
if match_start < current_line_end {
continue;
}
let from = last_newline_counted_up_to;
let line_start = match memrchr(b'\n', &content[from..match_start]) {
Some(pos) => from + pos + 1,
None => from,
};
let next_newline = memchr(b'\n', &content[match_start..]);
let line_end = match next_newline {
Some(pos) => match_start + pos,
None => content.len(),
};
let line_content_end = if line_end > line_start && content[line_end - 1] == b'\r' {
line_end - 1
} else {
line_end
};
if match_end > line_end {
continue;
}
if line_start == last_line_start {
continue;
}
if line_start > last_newline_counted_up_to {
let newline_count =
memchr_iter(b'\n', &content[last_newline_counted_up_to..line_start]).count();
current_line_num += newline_count as u32;
last_newline_counted_up_to = line_start;
}
matches.push(SearchMatch {
path: path.to_path_buf(),
line_number: current_line_num,
line_content: if skip_line_content {
Vec::new()
} else {
content[line_start..line_content_end].to_vec()
},
byte_offset: match_start as u64,
submatch_start: match_start - line_start,
submatch_end: match_end.min(line_content_end) - line_start,
});
last_line_start = line_start;
current_line_end = line_end;
}
matches
}
pub fn verify_empty(path: &Path, content: &[u8], skip_line_content: bool) -> Vec<SearchMatch> {
if is_binary(content) {
return Vec::new();
}
let mut matches = Vec::new();
let mut line_start = 0;
let mut line_num = 1;
for pos in memchr_iter(b'\n', content) {
let line_end = pos;
let line_content_end = if line_end > line_start && content[line_end - 1] == b'\r' {
line_end - 1
} else {
line_end
};
matches.push(SearchMatch {
path: path.to_path_buf(),
line_number: line_num,
line_content: if skip_line_content {
Vec::new()
} else {
content[line_start..line_content_end].to_vec()
},
byte_offset: line_start as u64,
submatch_start: 0,
submatch_end: 0,
});
line_start = pos + 1;
line_num += 1;
}
if line_start <= content.len() {
let line_end = content.len();
let line_content_end = if line_end > line_start && content[line_end - 1] == b'\r' {
line_end - 1
} else {
line_end
};
matches.push(SearchMatch {
path: path.to_path_buf(),
line_number: line_num,
line_content: if skip_line_content {
Vec::new()
} else {
content[line_start..line_content_end].to_vec()
},
byte_offset: line_start as u64,
submatch_start: 0,
submatch_end: 0,
});
}
matches
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal_reports_match_start_offset() {
let matches = verify_literal(
"needle",
Path::new("file.txt"),
b"prefix needle suffix\n",
false,
);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].byte_offset, 7);
assert_eq!(matches[0].submatch_start, 7);
assert_eq!(matches[0].submatch_end, 13);
}
#[test]
fn regex_reports_match_start_offset() {
let re = Regex::new("needle").unwrap();
let matches = verify_regex(&re, Path::new("file.txt"), b"prefix needle suffix\n", false);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].byte_offset, 7);
assert_eq!(matches[0].submatch_start, 7);
assert_eq!(matches[0].submatch_end, 13);
}
#[test]
fn literal_pattern_ending_in_cr_clamps_submatch_end() {
let matches = verify_literal("abc\r", Path::new("f"), b"abc\r\n", false);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].line_content, b"abc");
assert!(
matches[0].submatch_end <= matches[0].line_content.len(),
"submatch_end {} must not exceed line_content len {}",
matches[0].submatch_end,
matches[0].line_content.len()
);
let _ = &matches[0].line_content[matches[0].submatch_start..matches[0].submatch_end];
}
#[test]
fn crlf_offsets_include_line_break_bytes_before_match() {
let matches = verify_literal(
"needle",
Path::new("file.txt"),
b"one\r\ntwo needle\r\n",
false,
);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].line_number, 2);
assert_eq!(matches[0].byte_offset, 9);
assert_eq!(matches[0].line_content, b"two needle");
}
#[test]
fn literal_many_matches_across_and_clustered_on_lines() {
let mut content = Vec::new();
for _ in 0..500 {
content.extend_from_slice(b"nomatch here\n");
}
content.extend_from_slice(b"aa needle bb needle\n"); content.extend_from_slice(b"cc needle\n"); content.extend_from_slice(b"dd needle ee\n");
let matches = verify_literal("needle", Path::new("f"), &content, false);
assert_eq!(matches.len(), 3, "one match reported per line");
assert_eq!(matches[0].line_number, 501);
assert_eq!(matches[0].line_content, b"aa needle bb needle");
assert_eq!(matches[0].submatch_start, 3);
assert_eq!(matches[1].line_number, 502);
assert_eq!(matches[1].submatch_start, 3);
assert_eq!(matches[2].line_number, 503);
assert_eq!(matches[2].submatch_start, 3);
}
#[test]
fn regex_line_numbers_correct_with_gaps() {
let re = Regex::new("needle").unwrap();
let content = b"a\nb\nc needle\nd\ne needle\n";
let matches = verify_regex(&re, Path::new("f"), content, false);
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].line_number, 3);
assert_eq!(matches[1].line_number, 5);
}
#[test]
fn skip_line_content_leaves_content_empty_but_keeps_offsets() {
let lit = verify_literal("needle", Path::new("f"), b"a\nx needle y\n", true);
assert_eq!(lit.len(), 1);
assert!(lit[0].line_content.is_empty(), "content skipped");
assert_eq!(lit[0].line_number, 2);
assert_eq!(lit[0].submatch_start, 2);
assert_eq!(lit[0].submatch_end, 8);
let re = Regex::new("needle").unwrap();
let rgx = verify_regex(&re, Path::new("f"), b"a\nx needle y\n", true);
assert_eq!(rgx.len(), 1);
assert!(rgx[0].line_content.is_empty());
assert_eq!(rgx[0].line_number, 2);
assert_eq!(rgx[0].submatch_start, 2);
}
#[test]
fn regex_matches_invalid_utf8_line_bytes() {
let re = Regex::new(r"(?-u)\xFF").unwrap();
let matches = verify_regex(&re, Path::new("file.bin"), b"prefix\xFFsuffix\n", false);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].line_content, b"prefix\xFFsuffix");
assert_eq!(matches[0].submatch_start, 6);
assert_eq!(matches[0].submatch_end, 7);
}
#[test]
fn regex_pattern_ending_in_cr_clamps_submatch_end() {
let re = Regex::new("abc\r").unwrap();
let matches = verify_regex(&re, Path::new("f"), b"abc\r\n", false);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].line_content, b"abc");
assert!(
matches[0].submatch_end <= matches[0].line_content.len(),
"submatch_end {} must not exceed line_content len {}",
matches[0].submatch_end,
matches[0].line_content.len()
);
let _ = &matches[0].line_content[matches[0].submatch_start..matches[0].submatch_end];
}
#[test]
fn empty_pattern_matches_all_lines() {
let content = b"line one\nline two\r\nline three";
let matches = verify_empty(Path::new("f"), content, false);
assert_eq!(matches.len(), 3);
assert_eq!(matches[0].line_number, 1);
assert_eq!(matches[0].line_content, b"line one");
assert_eq!(matches[1].line_number, 2);
assert_eq!(matches[1].line_content, b"line two");
assert_eq!(matches[2].line_number, 3);
assert_eq!(matches[2].line_content, b"line three");
}
}