108 lines
3.7 KiB
Rust
108 lines
3.7 KiB
Rust
|
|
/// Attempt to find the sequence of `pattern` lines within `lines` beginning at or after `start`.
|
|||
|
|
/// Returns the starting index of the match or `None` if not found. Matches are attempted with
|
|||
|
|
/// decreasing strictness: exact match, then ignoring trailing whitespace, then ignoring leading
|
|||
|
|
/// and trailing whitespace. When `eof` is true, we first try starting at the end-of-file (so that
|
|||
|
|
/// patterns intended to match file endings are applied at the end), and fall back to searching
|
|||
|
|
/// from `start` if needed.
|
|||
|
|
///
|
|||
|
|
/// Special cases handled defensively:
|
|||
|
|
/// • Empty `pattern` → returns `Some(start)` (no-op match)
|
|||
|
|
/// • `pattern.len() > lines.len()` → returns `None` (cannot match, avoids
|
|||
|
|
/// out‑of‑bounds panic that occurred pre‑2025‑04‑12)
|
|||
|
|
pub(crate) fn seek_sequence(
|
|||
|
|
lines: &[String],
|
|||
|
|
pattern: &[String],
|
|||
|
|
start: usize,
|
|||
|
|
eof: bool,
|
|||
|
|
) -> Option<usize> {
|
|||
|
|
if pattern.is_empty() {
|
|||
|
|
return Some(start);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// When the pattern is longer than the available input there is no possible
|
|||
|
|
// match. Early‑return to avoid the out‑of‑bounds slice that would occur in
|
|||
|
|
// the search loops below (previously caused a panic when
|
|||
|
|
// `pattern.len() > lines.len()`).
|
|||
|
|
if pattern.len() > lines.len() {
|
|||
|
|
return None;
|
|||
|
|
}
|
|||
|
|
let search_start = if eof && lines.len() >= pattern.len() {
|
|||
|
|
lines.len() - pattern.len()
|
|||
|
|
} else {
|
|||
|
|
start
|
|||
|
|
};
|
|||
|
|
// Exact match first.
|
|||
|
|
for i in search_start..=lines.len().saturating_sub(pattern.len()) {
|
|||
|
|
if lines[i..i + pattern.len()] == *pattern {
|
|||
|
|
return Some(i);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Then rstrip match.
|
|||
|
|
for i in search_start..=lines.len().saturating_sub(pattern.len()) {
|
|||
|
|
let mut ok = true;
|
|||
|
|
for (p_idx, pat) in pattern.iter().enumerate() {
|
|||
|
|
if lines[i + p_idx].trim_end() != pat.trim_end() {
|
|||
|
|
ok = false;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if ok {
|
|||
|
|
return Some(i);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Finally, trim both sides to allow more lenience.
|
|||
|
|
for i in search_start..=lines.len().saturating_sub(pattern.len()) {
|
|||
|
|
let mut ok = true;
|
|||
|
|
for (p_idx, pat) in pattern.iter().enumerate() {
|
|||
|
|
if lines[i + p_idx].trim() != pat.trim() {
|
|||
|
|
ok = false;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if ok {
|
|||
|
|
return Some(i);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[cfg(test)]
|
|||
|
|
mod tests {
|
|||
|
|
use super::seek_sequence;
|
|||
|
|
|
|||
|
|
fn to_vec(strings: &[&str]) -> Vec<String> {
|
|||
|
|
strings.iter().map(|s| s.to_string()).collect()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_exact_match_finds_sequence() {
|
|||
|
|
let lines = to_vec(&["foo", "bar", "baz"]);
|
|||
|
|
let pattern = to_vec(&["bar", "baz"]);
|
|||
|
|
assert_eq!(seek_sequence(&lines, &pattern, 0, false), Some(1));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_rstrip_match_ignores_trailing_whitespace() {
|
|||
|
|
let lines = to_vec(&["foo ", "bar\t\t"]);
|
|||
|
|
// Pattern omits trailing whitespace.
|
|||
|
|
let pattern = to_vec(&["foo", "bar"]);
|
|||
|
|
assert_eq!(seek_sequence(&lines, &pattern, 0, false), Some(0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_trim_match_ignores_leading_and_trailing_whitespace() {
|
|||
|
|
let lines = to_vec(&[" foo ", " bar\t"]);
|
|||
|
|
// Pattern omits any additional whitespace.
|
|||
|
|
let pattern = to_vec(&["foo", "bar"]);
|
|||
|
|
assert_eq!(seek_sequence(&lines, &pattern, 0, false), Some(0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_pattern_longer_than_input_returns_none() {
|
|||
|
|
let lines = to_vec(&["just one line"]);
|
|||
|
|
let pattern = to_vec(&["too", "many", "lines"]);
|
|||
|
|
// Should not panic – must return None when pattern cannot possibly fit.
|
|||
|
|
assert_eq!(seek_sequence(&lines, &pattern, 0, false), None);
|
|||
|
|
}
|
|||
|
|
}
|