Skip to main content

coreutils_rs/tac/
core.rs

1use std::io::{self, IoSlice, Write};
2
3/// Maximum number of iovecs per writev() call (Linux IOV_MAX is 1024).
4const IOV_BATCH: usize = 1024;
5
6/// Write all IoSlices to the writer, handling partial writes.
7/// For large numbers of slices, batches into IOV_BATCH-sized groups.
8fn write_all_slices(out: &mut impl Write, slices: &[IoSlice<'_>]) -> io::Result<()> {
9    // Small number of slices: use simple write_all for each
10    if slices.len() <= 4 {
11        for s in slices {
12            out.write_all(s)?;
13        }
14        return Ok(());
15    }
16
17    let mut offset = 0;
18    while offset < slices.len() {
19        let end = (offset + IOV_BATCH).min(slices.len());
20        let n = out.write_vectored(&slices[offset..end])?;
21        if n == 0 {
22            return Err(io::Error::new(
23                io::ErrorKind::WriteZero,
24                "failed to write any data",
25            ));
26        }
27        let mut remaining = n;
28        while offset < end && remaining >= slices[offset].len() {
29            remaining -= slices[offset].len();
30            offset += 1;
31        }
32        if remaining > 0 && offset < end {
33            out.write_all(&slices[offset][remaining..])?;
34            offset += 1;
35        }
36    }
37    Ok(())
38}
39
40/// Reverse records separated by a single byte.
41/// Uses backward SIMD scan (memrchr_iter) to process records from back to front,
42/// building batched IoSlice references for zero-copy writev output.
43/// O(IOV_BATCH) memory — no positions Vec allocation needed at all.
44/// For 100MB/2.5M lines: saves ~20MB positions Vec + ~40MB IoSlice Vec.
45pub fn tac_bytes(data: &[u8], separator: u8, before: bool, out: &mut impl Write) -> io::Result<()> {
46    if data.is_empty() {
47        return Ok(());
48    }
49
50    // For small data (< 256KB), use simple backward scan with direct writes.
51    // Avoids overhead of forward SIMD scan + position Vec + IoSlice Vec allocation.
52    if data.len() < 256 * 1024 {
53        return tac_bytes_small(data, separator, before, out);
54    }
55
56    let mut batch: Vec<IoSlice<'_>> = Vec::with_capacity(IOV_BATCH);
57
58    if !before {
59        // separator-after mode: records end with separator
60        let mut iter = memchr::memrchr_iter(separator, data);
61
62        let first_sep = match iter.next() {
63            Some(pos) => pos,
64            None => return out.write_all(data), // no separator found
65        };
66
67        // Trailing content without separator — output first
68        if first_sep + 1 < data.len() {
69            batch.push(IoSlice::new(&data[first_sep + 1..]));
70        }
71
72        let mut end = first_sep + 1; // exclusive end of current record
73
74        // Process remaining separators from back to front
75        for pos in iter {
76            batch.push(IoSlice::new(&data[pos + 1..end]));
77            end = pos + 1;
78            if batch.len() == IOV_BATCH {
79                write_all_slices(out, &batch)?;
80                batch.clear();
81            }
82        }
83
84        // First record (no separator before it)
85        if end > 0 {
86            batch.push(IoSlice::new(&data[0..end]));
87        }
88    } else {
89        // separator-before mode: records start with separator
90        let mut end = data.len();
91
92        for pos in memchr::memrchr_iter(separator, data) {
93            batch.push(IoSlice::new(&data[pos..end]));
94            end = pos;
95            if batch.len() == IOV_BATCH {
96                write_all_slices(out, &batch)?;
97                batch.clear();
98            }
99        }
100
101        // Leading content before first separator
102        if end > 0 {
103            batch.push(IoSlice::new(&data[0..end]));
104        }
105    }
106
107    if !batch.is_empty() {
108        write_all_slices(out, &batch)?;
109    }
110    Ok(())
111}
112
113/// Small-file path: backward SIMD scan, copy records into contiguous buffer, single write.
114/// Uses memrchr_iter for backward scanning (matches large-path approach).
115/// Avoids positions Vec + IoSlice Vec allocations and writev syscalls.
116/// Single write_all is faster than writev with many small segments for small data.
117fn tac_bytes_small(
118    data: &[u8],
119    separator: u8,
120    before: bool,
121    out: &mut impl Write,
122) -> io::Result<()> {
123    let mut outbuf = vec![0u8; data.len()];
124    let mut wp = 0;
125
126    if !before {
127        // separator-after mode: records end with separator
128        let mut iter = memchr::memrchr_iter(separator, data);
129
130        let first_sep = match iter.next() {
131            Some(pos) => pos,
132            None => return out.write_all(data),
133        };
134
135        // Trailing content without separator — output first
136        if first_sep + 1 < data.len() {
137            let trail = &data[first_sep + 1..];
138            outbuf[wp..wp + trail.len()].copy_from_slice(trail);
139            wp += trail.len();
140        }
141
142        let mut end = first_sep + 1;
143
144        for pos in iter {
145            let record = &data[pos + 1..end];
146            outbuf[wp..wp + record.len()].copy_from_slice(record);
147            wp += record.len();
148            end = pos + 1;
149        }
150
151        // First record
152        if end > 0 {
153            outbuf[wp..wp + end].copy_from_slice(&data[0..end]);
154            wp += end;
155        }
156    } else {
157        // separator-before mode: records start with separator
158        let mut end = data.len();
159
160        for pos in memchr::memrchr_iter(separator, data) {
161            let record = &data[pos..end];
162            outbuf[wp..wp + record.len()].copy_from_slice(record);
163            wp += record.len();
164            end = pos;
165        }
166
167        // Leading content before first separator
168        if end > 0 {
169            outbuf[wp..wp + end].copy_from_slice(&data[0..end]);
170            wp += end;
171        }
172    }
173
174    out.write_all(&outbuf[..wp])
175}
176
177/// Reverse records using a multi-byte string separator.
178/// Uses SIMD-accelerated memmem for substring search.
179pub fn tac_string_separator(
180    data: &[u8],
181    separator: &[u8],
182    before: bool,
183    out: &mut impl Write,
184) -> io::Result<()> {
185    if data.is_empty() {
186        return Ok(());
187    }
188
189    if separator.len() == 1 {
190        return tac_bytes(data, separator[0], before, out);
191    }
192
193    // Find all occurrences of the separator using SIMD-accelerated memmem
194    let estimated = (data.len() / separator.len().max(40)).max(64);
195    let mut positions: Vec<usize> = Vec::with_capacity(estimated);
196    for pos in memchr::memmem::find_iter(data, separator) {
197        positions.push(pos);
198    }
199
200    if positions.is_empty() {
201        out.write_all(data)?;
202        return Ok(());
203    }
204
205    let sep_len = separator.len();
206
207    // Small data: contiguous buffer + single write (avoids IoSlice/writev overhead)
208    if data.len() < 2 * 1024 * 1024 {
209        let mut outbuf = vec![0u8; data.len()];
210        let mut wp = 0;
211
212        if !before {
213            let last_end = positions.last().unwrap() + sep_len;
214
215            if last_end < data.len() {
216                let trail = &data[last_end..];
217                outbuf[wp..wp + trail.len()].copy_from_slice(trail);
218                wp += trail.len();
219            }
220
221            let mut i = positions.len();
222            while i > 0 {
223                i -= 1;
224                let sep_start = positions[i];
225                let rec_start = if i == 0 {
226                    0
227                } else {
228                    positions[i - 1] + sep_len
229                };
230                let record = &data[rec_start..sep_start + sep_len];
231                outbuf[wp..wp + record.len()].copy_from_slice(record);
232                wp += record.len();
233            }
234        } else {
235            let mut i = positions.len();
236            while i > 0 {
237                i -= 1;
238                let start = positions[i];
239                let end = if i + 1 < positions.len() {
240                    positions[i + 1]
241                } else {
242                    data.len()
243                };
244                let record = &data[start..end];
245                outbuf[wp..wp + record.len()].copy_from_slice(record);
246                wp += record.len();
247            }
248            if positions[0] > 0 {
249                outbuf[wp..wp + positions[0]].copy_from_slice(&data[..positions[0]]);
250                wp += positions[0];
251            }
252        }
253        return out.write_all(&outbuf[..wp]);
254    }
255
256    // Large data: batched IoSlice/writev for zero-copy output
257    let mut batch: Vec<IoSlice<'_>> = Vec::with_capacity(IOV_BATCH);
258
259    if !before {
260        let last_end = positions.last().unwrap() + sep_len;
261        let has_trailing_sep = last_end == data.len();
262
263        if !has_trailing_sep {
264            batch.push(IoSlice::new(&data[last_end..]));
265        }
266
267        let mut i = positions.len();
268        while i > 0 {
269            i -= 1;
270            let sep_start = positions[i];
271            let rec_start = if i == 0 {
272                0
273            } else {
274                positions[i - 1] + sep_len
275            };
276            batch.push(IoSlice::new(&data[rec_start..sep_start + sep_len]));
277            if batch.len() == IOV_BATCH {
278                write_all_slices(out, &batch)?;
279                batch.clear();
280            }
281        }
282    } else {
283        let mut i = positions.len();
284        while i > 0 {
285            i -= 1;
286            let start = positions[i];
287            let end = if i + 1 < positions.len() {
288                positions[i + 1]
289            } else {
290                data.len()
291            };
292            batch.push(IoSlice::new(&data[start..end]));
293            if batch.len() == IOV_BATCH {
294                write_all_slices(out, &batch)?;
295                batch.clear();
296            }
297        }
298
299        if positions[0] > 0 {
300            batch.push(IoSlice::new(&data[..positions[0]]));
301        }
302    }
303
304    if !batch.is_empty() {
305        write_all_slices(out, &batch)?;
306    }
307
308    Ok(())
309}
310
311/// Find regex matches using backward scanning, matching GNU tac's re_search behavior.
312/// GNU tac scans backward from the end, finding the rightmost starting position first.
313/// This produces different matches than forward scanning for patterns like [0-9]+.
314/// The matches are returned in left-to-right order.
315fn find_regex_matches_backward(data: &[u8], re: &regex::bytes::Regex) -> Vec<(usize, usize)> {
316    let mut matches = Vec::new();
317    let mut past_end = data.len();
318
319    while past_end > 0 {
320        let buf = &data[..past_end];
321        let mut found = false;
322
323        // Scan backward: try positions from past_end-1 down to 0
324        // We need the LAST match starting position in buf, so we try from the end
325        let mut pos = past_end;
326        while pos > 0 {
327            pos -= 1;
328            if let Some(m) = re.find_at(buf, pos) {
329                if m.start() == pos {
330                    // Match starts at exactly this position — this is the rightmost match start
331                    matches.push((m.start(), m.end()));
332                    past_end = m.start();
333                    found = true;
334                    break;
335                }
336                // Match starts later than pos — skip to before that match
337                // No point checking positions between pos and m.start() since
338                // find_at already told us the leftmost match from pos starts at m.start()
339                // But we need matches that START before m.start(), so continue decrementing
340            }
341            // If None, there's no match at pos or later, but there might be one earlier
342            // (find_at only searches forward from pos)
343        }
344
345        if !found {
346            break;
347        }
348    }
349
350    matches.reverse(); // Convert from backward order to left-to-right order
351    matches
352}
353
354/// Reverse records using a regex separator.
355/// Uses regex::bytes for direct byte-level matching (no UTF-8 conversion needed).
356/// NOTE: GNU tac uses POSIX Basic Regular Expressions (BRE), so we convert to ERE first.
357/// Uses backward scanning to match GNU tac's re_search behavior.
358pub fn tac_regex_separator(
359    data: &[u8],
360    pattern: &str,
361    before: bool,
362    out: &mut impl Write,
363) -> io::Result<()> {
364    if data.is_empty() {
365        return Ok(());
366    }
367
368    let re = match regex::bytes::Regex::new(pattern) {
369        Ok(r) => r,
370        Err(e) => {
371            return Err(io::Error::new(
372                io::ErrorKind::InvalidInput,
373                format!("invalid regex '{}': {}", pattern, e),
374            ));
375        }
376    };
377
378    // Use backward scanning to match GNU tac's re_search behavior
379    let matches = find_regex_matches_backward(data, &re);
380
381    if matches.is_empty() {
382        out.write_all(data)?;
383        return Ok(());
384    }
385
386    // Small data: contiguous buffer + single write (avoids IoSlice/writev overhead)
387    if data.len() < 2 * 1024 * 1024 {
388        let mut outbuf = vec![0u8; data.len()];
389        let mut wp = 0;
390
391        if !before {
392            let last_end = matches.last().unwrap().1;
393
394            if last_end < data.len() {
395                let trail = &data[last_end..];
396                outbuf[wp..wp + trail.len()].copy_from_slice(trail);
397                wp += trail.len();
398            }
399
400            let mut i = matches.len();
401            while i > 0 {
402                i -= 1;
403                let rec_start = if i == 0 { 0 } else { matches[i - 1].1 };
404                let record = &data[rec_start..matches[i].1];
405                outbuf[wp..wp + record.len()].copy_from_slice(record);
406                wp += record.len();
407            }
408        } else {
409            let mut i = matches.len();
410            while i > 0 {
411                i -= 1;
412                let start = matches[i].0;
413                let end = if i + 1 < matches.len() {
414                    matches[i + 1].0
415                } else {
416                    data.len()
417                };
418                let record = &data[start..end];
419                outbuf[wp..wp + record.len()].copy_from_slice(record);
420                wp += record.len();
421            }
422            if matches[0].0 > 0 {
423                outbuf[wp..wp + matches[0].0].copy_from_slice(&data[..matches[0].0]);
424                wp += matches[0].0;
425            }
426        }
427        return out.write_all(&outbuf[..wp]);
428    }
429
430    // Large data: batched IoSlice/writev for zero-copy output
431    let mut batch: Vec<IoSlice<'_>> = Vec::with_capacity(IOV_BATCH);
432
433    if !before {
434        let last_end = matches.last().unwrap().1;
435        let has_trailing_sep = last_end == data.len();
436
437        if !has_trailing_sep {
438            batch.push(IoSlice::new(&data[last_end..]));
439        }
440
441        let mut i = matches.len();
442        while i > 0 {
443            i -= 1;
444            let rec_start = if i == 0 { 0 } else { matches[i - 1].1 };
445            let rec_end = matches[i].1;
446            batch.push(IoSlice::new(&data[rec_start..rec_end]));
447            if batch.len() == IOV_BATCH {
448                write_all_slices(out, &batch)?;
449                batch.clear();
450            }
451        }
452    } else {
453        let mut i = matches.len();
454        while i > 0 {
455            i -= 1;
456            let start = matches[i].0;
457            let end = if i + 1 < matches.len() {
458                matches[i + 1].0
459            } else {
460                data.len()
461            };
462            batch.push(IoSlice::new(&data[start..end]));
463            if batch.len() == IOV_BATCH {
464                write_all_slices(out, &batch)?;
465                batch.clear();
466            }
467        }
468
469        if matches[0].0 > 0 {
470            batch.push(IoSlice::new(&data[..matches[0].0]));
471        }
472    }
473
474    if !batch.is_empty() {
475        write_all_slices(out, &batch)?;
476    }
477
478    Ok(())
479}