From 404a68a41fb1350ffa6e68f4abe8f72d0081274d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 28 Jun 2026 13:12:54 +0200 Subject: [PATCH] grep: -B retains exactly N context lines, not next power of two --- src/context_buffer.rs | 11 ++++++++--- tests/test_grep.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/context_buffer.rs b/src/context_buffer.rs index 76ea5db..290df88 100644 --- a/src/context_buffer.rs +++ b/src/context_buffer.rs @@ -42,13 +42,17 @@ impl BufferedLine { /// provide a more optimized `ContextBuffer` when `mmap()` is available. pub struct ContextBuffer { slots: Vec, + /// Number of lines to retain — the requested context size. The slot count is + /// rounded up to a power of two for cheap `& mask` indexing, so it must not be + /// used as the retention limit. + capacity: usize, head: usize, len: usize, } impl ContextBuffer { pub fn new(capacity: usize) -> Self { - let len = if capacity == 0 { + let slots = if capacity == 0 { 0 } else { capacity.next_power_of_two() @@ -60,8 +64,9 @@ impl ContextBuffer { line_number: 0, byte_offset: 0, }; - len + slots ], + capacity, head: 0, len: 0, } @@ -90,7 +95,7 @@ impl ContextBuffer { slot.byte_offset = byte_offset; self.head = self.head.wrapping_add(1); - self.len = (self.len + 1).min(self.slots.len()); + self.len = (self.len + 1).min(self.capacity); } pub fn drain_iter(&mut self) -> impl Iterator { diff --git a/tests/test_grep.rs b/tests/test_grep.rs index d612a40..6203012 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -909,6 +909,45 @@ fn after_before_combined_context() { .stdout_only("b\nMATCH\nc\n"); } +#[test] +fn before_context_count_is_exact_not_rounded() { + // `-B N` must retain exactly N preceding lines. The ring buffer rounds its + // slot count up to a power of two, so a non-power-of-two N (3, 5, 6, 7) must + // not leak extra context lines. + let input = "01\n02\n03\n04\n05\n06\n07\n08\n09\n10\nMM\n"; + + let expected = [ + ("0", "MM\n"), + ("1", "10\nMM\n"), + ("2", "09\n10\nMM\n"), + ("3", "08\n09\n10\nMM\n"), + ("4", "07\n08\n09\n10\nMM\n"), + ("5", "06\n07\n08\n09\n10\nMM\n"), + ("6", "05\n06\n07\n08\n09\n10\nMM\n"), + ("7", "04\n05\n06\n07\n08\n09\n10\nMM\n"), + ]; + for (n, want) in expected { + let (_s, mut c) = ucmd(); + c.args(&["-B", n, "MM"]) + .pipe_in(input) + .succeeds() + .stdout_only(want); + } + + // A count larger than the available lines just caps at what is there. + let (_s, mut c) = ucmd(); + c.args(&["-B", "1000000", "MM"]) + .pipe_in(input) + .succeeds() + .stdout_only(input); + + // Non-numeric, negative, and fractional counts are rejected with exit code 2. + for bad in ["abc", "-1", "2.5"] { + let (_s, mut c) = ucmd(); + c.args(&["-B", bad, "MM"]).pipe_in(input).fails_with_code(2); + } +} + #[test] fn num_shorthand_is_context() { // `-2` is shorthand for `-C 2`.