@@ -35,8 +35,7 @@ pub fn follow(
3535 // FilterState is built once and reused across all lines and files.
3636 const filter_state = formats .FilterState .init (args );
3737
38- // Allocate a single shared read buffer for the follow loop and for
39- // readLastNLines — avoids a second READ_BUF_SIZE allocation per file.
38+ // Allocate a single shared read buffer reused by readLastNLines and the follow loop.
4039 const read_buf = try allocator .alloc (u8 , READ_BUF_SIZE );
4140 defer allocator .free (read_buf );
4241
@@ -110,13 +109,9 @@ pub fn follow(
110109/// prints matching ones via `filter_state`, and returns the file position
111110/// after the last consumed byte.
112111///
113- /// Scans backwards through the file in SCAN_CHUNK steps until N newlines
114- /// are found or the beginning of the file is reached. This handles files of
115- /// any size correctly — the old single-chunk approach missed lines when the
116- /// last N lines spanned more than 8 KB.
117- ///
118- /// `read_buf` is a caller-owned scratch buffer of at least READ_BUF_SIZE bytes,
119- /// allowing the caller to reuse one allocation across multiple calls.
112+ /// Scans backwards in READ_BUF_SIZE windows until N newlines are found or
113+ /// the beginning of the file is reached. This handles files of any size —
114+ /// the old single 8 KB chunk missed lines when the last N lines exceeded that size.
120115pub fn readLastNLines (
121116 allocator : std.mem.Allocator ,
122117 file : * std.fs.File ,
@@ -127,47 +122,41 @@ pub fn readLastNLines(
127122) ! u64 {
128123 const n : usize = if (args .num_lines == 0 ) 10 else args .num_lines ;
129124
130- // Scan chunk size for the backwards pass — 64 KB matches READ_BUF_SIZE
131- // so one chunk is almost always enough for typical log lines.
132- const SCAN_CHUNK : u64 = 64 * 1024 ;
133-
134- const scan_buf = try allocator .alloc (u8 , SCAN_CHUNK );
125+ const scan_buf = try allocator .alloc (u8 , READ_BUF_SIZE );
135126 defer allocator .free (scan_buf );
136127
137- // Scan backwards in SCAN_CHUNK windows until we have found N newlines.
128+ // Scan backwards in READ_BUF_SIZE windows until N newlines are found .
138129 var newlines_found : usize = 0 ;
139- var read_from : u64 = 0 ; // absolute file offset to start forward reading from
130+ var read_from : u64 = 0 ;
140131 var scan_end : u64 = file_size ;
141132
142133 outer : while (scan_end > 0 ) {
143- const chunk_size : u64 = @min (scan_end , SCAN_CHUNK );
134+ const chunk_size : u64 = @min (scan_end , READ_BUF_SIZE );
144135 const chunk_start : u64 = scan_end - chunk_size ;
145136
146137 try file .seekTo (chunk_start );
147138 const bytes_read = try file .read (scan_buf [0.. chunk_size ]);
148139
149- // Walk backwards through this chunk counting newlines.
150140 var idx : usize = bytes_read ;
151141 while (idx > 0 ) {
152142 idx -= 1 ;
153143 if (scan_buf [idx ] == '\n ' ) {
154144 newlines_found += 1 ;
155145 if (newlines_found == n ) {
156- // The line after this newline is where we start reading.
157146 read_from = chunk_start + idx + 1 ;
158147 break :outer ;
159148 }
160149 }
161150 }
162151
163- if (chunk_start == 0 ) break ; // reached the beginning
152+ if (chunk_start == 0 ) break ;
164153 scan_end = chunk_start ;
165154 }
166155
167156 // read_from stays 0 if fewer than N newlines exist in the entire file.
168157 try file .seekTo (read_from );
169158
170- var carry = try std .ArrayList (u8 ). initCapacity ( allocator , 4096 ) ;
159+ var carry = std .ArrayList (u8 ){} ;
171160 defer carry .deinit (allocator );
172161 var pos : u64 = read_from ;
173162
@@ -194,9 +183,7 @@ fn readAvailable(
194183/// `filter_state.printIfMatch`. Partial trailing lines are saved in `carry`
195184/// and prepended on the next call. `position` is advanced by bytes read.
196185///
197- /// NOTE: empty lines (consecutive newlines) are silently skipped. This is
198- /// intentional for tail output but means blank separators between log entries
199- /// are not forwarded to the filter. Revisit if blank-line-aware formats are added.
186+ /// NOTE: empty lines (consecutive newlines) are silently skipped.
200187pub fn readToEOF (
201188 allocator : std.mem.Allocator ,
202189 file : * std.fs.File ,
@@ -212,10 +199,8 @@ pub fn readToEOF(
212199 position .* += n ;
213200 var slice = buf [0.. n ];
214201
215- // BUG FIX: the old code did `defer allocator.free(combined)` inside the
216- // `if` block, which freed `combined` before the line-scanning loop below
217- // could use `slice` — a use-after-free. The fix hoists the defer outside
218- // the `if` so `combined` lives for the full iteration of the outer loop.
202+ // Hoist combined lifetime outside the if-block to prevent use-after-free:
203+ // `defer allocator.free` inside the if would free before the scan loop runs.
219204 var combined : ? []u8 = null ;
220205 defer if (combined ) | c | allocator .free (c );
221206
@@ -233,7 +218,6 @@ pub fn readToEOF(
233218 start = nl + 1 ;
234219 }
235220
236- // Save the trailing partial line for the next iteration.
237221 if (start < slice .len ) {
238222 try carry .appendSlice (allocator , slice [start .. ]);
239223 }
0 commit comments