Skip to main content

coreutils_rs/pr/
core.rs

1use std::io::{self, BufRead, Write};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4/// Default page length in lines.
5pub const DEFAULT_PAGE_LENGTH: usize = 66;
6/// Default page width in columns.
7pub const DEFAULT_PAGE_WIDTH: usize = 72;
8/// Number of header lines (2 blank + 1 header + 2 blank).
9pub const HEADER_LINES: usize = 5;
10/// Number of footer lines (5 blank).
11pub const FOOTER_LINES: usize = 5;
12
13/// Configuration for the pr command.
14#[derive(Clone)]
15pub struct PrConfig {
16    /// First page to print (1-indexed).
17    pub first_page: usize,
18    /// Last page to print (0 = no limit).
19    pub last_page: usize,
20    /// Number of columns.
21    pub columns: usize,
22    /// Print columns across rather than down.
23    pub across: bool,
24    /// Show control characters in hat notation (^X).
25    pub show_control_chars: bool,
26    /// Double-space output.
27    pub double_space: bool,
28    /// Date format string for header.
29    pub date_format: String,
30    /// Expand input tabs to spaces (char, width).
31    pub expand_tabs: Option<(char, usize)>,
32    /// Use form feeds instead of newlines for page breaks.
33    pub form_feed: bool,
34    /// Custom header string (replaces filename).
35    pub header: Option<String>,
36    /// Replace spaces with tabs in output (char, width).
37    pub output_tabs: Option<(char, usize)>,
38    /// Join lines (do not truncate lines when using columns).
39    pub join_lines: bool,
40    /// Page length in lines (including header/footer).
41    pub page_length: usize,
42    /// Merge multiple files side by side.
43    pub merge: bool,
44    /// Number lines: (separator_char, digits).
45    pub number_lines: Option<(char, usize)>,
46    /// First line number.
47    pub first_line_number: usize,
48    /// Indent (offset) each line by this many spaces.
49    pub indent: usize,
50    /// Suppress file-not-found warnings.
51    pub no_file_warnings: bool,
52    /// Column separator character.
53    pub separator: Option<char>,
54    /// Column separator string.
55    pub sep_string: Option<String>,
56    /// Omit header and trailer.
57    pub omit_header: bool,
58    /// Omit header, trailer, and form feeds.
59    pub omit_pagination: bool,
60    /// Show nonprinting characters.
61    pub show_nonprinting: bool,
62    /// Page width.
63    pub page_width: usize,
64    /// Truncate lines to page width (-W).
65    pub truncate_lines: bool,
66}
67
68impl Default for PrConfig {
69    fn default() -> Self {
70        Self {
71            first_page: 1,
72            last_page: 0,
73            columns: 1,
74            across: false,
75            show_control_chars: false,
76            double_space: false,
77            date_format: "%Y-%m-%d %H:%M".to_string(),
78            expand_tabs: None,
79            form_feed: false,
80            header: None,
81            output_tabs: None,
82            join_lines: false,
83            page_length: DEFAULT_PAGE_LENGTH,
84            merge: false,
85            number_lines: None,
86            first_line_number: 1,
87            indent: 0,
88            no_file_warnings: false,
89            separator: None,
90            sep_string: None,
91            omit_header: false,
92            omit_pagination: false,
93            show_nonprinting: false,
94            page_width: DEFAULT_PAGE_WIDTH,
95            truncate_lines: false,
96        }
97    }
98}
99
100/// Format a SystemTime as a date string using libc strftime.
101fn format_header_date(time: &SystemTime, format: &str) -> String {
102    let secs = time
103        .duration_since(UNIX_EPOCH)
104        .unwrap_or_default()
105        .as_secs() as i64;
106    let mut tm: libc::tm = unsafe { std::mem::zeroed() };
107    unsafe {
108        libc::localtime_r(&secs, &mut tm);
109    }
110
111    // Use strftime via libc
112    let c_format = std::ffi::CString::new(format).unwrap_or_default();
113    let mut buf = vec![0u8; 256];
114    let len = unsafe {
115        libc::strftime(
116            buf.as_mut_ptr() as *mut libc::c_char,
117            buf.len(),
118            c_format.as_ptr(),
119            &tm,
120        )
121    };
122    if len == 0 {
123        return String::new();
124    }
125    buf.truncate(len);
126    String::from_utf8_lossy(&buf).into_owned()
127}
128
129/// Expand tabs in a line to spaces.
130fn expand_tabs_in_line(line: &str, tab_char: char, tab_width: usize) -> String {
131    if tab_width == 0 {
132        return line.replace(tab_char, "");
133    }
134    // Pre-allocate with extra capacity for tab expansion
135    let mut result = String::with_capacity(line.len() + line.len() / 4);
136    let tab_byte = tab_char as u8;
137    let bytes = line.as_bytes();
138    let mut col = 0;
139    let mut seg_start = 0;
140
141    for (i, &b) in bytes.iter().enumerate() {
142        if b == tab_byte {
143            // Copy segment before tab
144            if i > seg_start {
145                result.push_str(&line[seg_start..i]);
146                col += i - seg_start;
147            }
148            let spaces = tab_width - (col % tab_width);
149            // Batch push spaces
150            let space_buf = "                                ";
151            let mut remaining = spaces;
152            while remaining > 0 {
153                let chunk = remaining.min(space_buf.len());
154                result.push_str(&space_buf[..chunk]);
155                remaining -= chunk;
156            }
157            col += spaces;
158            seg_start = i + 1;
159        }
160    }
161    // Copy remaining segment after last tab
162    if seg_start < bytes.len() {
163        result.push_str(&line[seg_start..]);
164    }
165    result
166}
167
168/// Push hat notation (^X) for a control character into a String, avoiding allocation.
169#[inline]
170fn push_hat_notation(result: &mut String, ch: char) {
171    let b = ch as u32;
172    if b < 32 {
173        result.push('^');
174        result.push((b as u8 + b'@') as char);
175    } else if b == 127 {
176        result.push_str("^?");
177    } else {
178        result.push(ch);
179    }
180}
181
182/// Push nonprinting notation (like cat -v) for a character into a String.
183#[inline]
184fn push_nonprinting(result: &mut String, ch: char) {
185    let b = ch as u32;
186    if b < 32 && b != 9 && b != 10 {
187        result.push('^');
188        result.push((b as u8 + b'@') as char);
189    } else if b == 127 {
190        result.push_str("^?");
191    } else if b >= 128 && b < 160 {
192        result.push_str("M-^");
193        result.push((b as u8 - 128 + b'@') as char);
194    } else if b >= 160 && b < 255 {
195        result.push_str("M-");
196        result.push((b as u8 - 128) as char);
197    } else if b == 255 {
198        result.push_str("M-^?");
199    } else {
200        result.push(ch);
201    }
202}
203
204/// Process a line for control char display.
205fn process_control_chars(line: &str, show_control: bool, show_nonprinting: bool) -> String {
206    if !show_control && !show_nonprinting {
207        return line.to_string();
208    }
209    let mut result = String::with_capacity(line.len() + line.len() / 4);
210    for ch in line.chars() {
211        if show_nonprinting {
212            push_nonprinting(&mut result, ch);
213        } else if show_control {
214            push_hat_notation(&mut result, ch);
215        } else {
216            result.push(ch);
217        }
218    }
219    result
220}
221
222/// Get the column separator to use.
223fn get_column_separator(config: &PrConfig) -> String {
224    if let Some(ref s) = config.sep_string {
225        s.clone()
226    } else if let Some(c) = config.separator {
227        c.to_string()
228    } else {
229        " ".to_string()
230    }
231}
232
233/// Check if the user has explicitly set a column separator.
234fn has_explicit_separator(config: &PrConfig) -> bool {
235    config.sep_string.is_some() || config.separator.is_some()
236}
237
238/// Write tab-based padding from an absolute position on the line to a target absolute position.
239/// GNU pr pads columns using tab characters (8-space tab stops) to reach the column boundary.
240/// `abs_pos` is the current absolute position on the line.
241/// `target_abs_pos` is the target absolute position.
242/// Static spaces buffer for padding without allocation.
243const SPACES: [u8; 256] = [b' '; 256];
244
245/// Write `n` spaces to output using the static SPACES buffer.
246#[inline]
247fn write_spaces<W: Write>(output: &mut W, n: usize) -> io::Result<()> {
248    let mut remaining = n;
249    while remaining > 0 {
250        let chunk = remaining.min(SPACES.len());
251        output.write_all(&SPACES[..chunk])?;
252        remaining -= chunk;
253    }
254    Ok(())
255}
256
257fn write_column_padding<W: Write>(
258    output: &mut W,
259    abs_pos: usize,
260    target_abs_pos: usize,
261) -> io::Result<()> {
262    // GNU pr uses tabs (8-stop) + trailing spaces to reach column boundary.
263    // When the gap can be filled entirely with spaces fewer than one tab
264    // stop away, just emit spaces (matching GNU's encoder).
265    let n = target_abs_pos.saturating_sub(abs_pos);
266    if n == 0 {
267        return Ok(());
268    }
269    let next_tab = (abs_pos / 8 + 1) * 8;
270    if next_tab > target_abs_pos {
271        // Gap doesn't reach next tab stop → spaces only
272        return write_spaces(output, n);
273    }
274    // Tab to the first tab stop, then continue
275    output.write_all(b"\t")?;
276    let mut col = next_tab;
277    while col < target_abs_pos {
278        let nt = (col / 8 + 1) * 8;
279        if nt <= target_abs_pos {
280            output.write_all(b"\t")?;
281            col = nt;
282        } else {
283            write_spaces(output, target_abs_pos - col)?;
284            col = target_abs_pos;
285        }
286    }
287    Ok(())
288}
289
290/// Paginate raw byte data — fast path that avoids per-line String allocation.
291/// When no tab expansion or control char processing is needed, lines are
292/// extracted as byte slices directly from the input buffer (zero-copy).
293pub fn pr_data<W: Write>(
294    data: &[u8],
295    output: &mut W,
296    config: &PrConfig,
297    filename: &str,
298    file_date: Option<SystemTime>,
299) -> io::Result<()> {
300    let needs_transform =
301        config.expand_tabs.is_some() || config.show_control_chars || config.show_nonprinting;
302
303    if needs_transform {
304        // Fall back to the String-based path for transforms
305        let reader = io::Cursor::new(data);
306        return pr_file(reader, output, config, filename, file_date);
307    }
308
309    // Single SIMD pass to detect CR/FF (rare in normal files); cache result
310    // for all fast-path guards below.
311    let has_cr_or_ff = memchr::memchr2(b'\r', b'\x0c', data).is_some();
312
313    // Ultra-fast path: single column, no per-line transforms → contiguous chunk writes
314    let is_simple = config.columns <= 1
315        && config.number_lines.is_none()
316        && config.indent == 0
317        && !config.truncate_lines
318        && !config.double_space
319        && !config.across
320        && !has_cr_or_ff;
321
322    if is_simple {
323        // Passthrough: -T (omit_pagination) with no transforms and no page range → output == input.
324        if config.omit_pagination && config.first_page == 1 && config.last_page == 0 {
325            return output.write_all(data);
326        }
327        return pr_data_contiguous(data, output, config, filename, file_date);
328    }
329
330    // Fast path: single column with numbering only (no indent, no truncate, no double-space)
331    if config.columns <= 1
332        && config.number_lines.is_some()
333        && config.indent == 0
334        && !config.truncate_lines
335        && !config.double_space
336        && !has_cr_or_ff
337    {
338        return pr_data_numbered(data, output, config, filename, file_date);
339    }
340
341    // Fast path: multi-column down mode without numbering, no transforms.
342    if config.columns > 1
343        && config.columns <= 32
344        && !config.across
345        && config.number_lines.is_none()
346        && config.indent == 0
347        && !config.double_space
348        && !config.join_lines
349        && data.len() <= u32::MAX as usize
350        && !has_cr_or_ff
351    {
352        return pr_data_multicolumn_fast(data, output, config, filename, file_date);
353    }
354
355    // Normal path: split into line byte slices using SIMD memchr
356    let mut lines: Vec<&[u8]> = Vec::with_capacity(data.len() / 40 + 64);
357    let mut start = 0;
358    for pos in memchr::memchr_iter(b'\n', data) {
359        let end = if pos > start && data[pos - 1] == b'\r' {
360            pos - 1
361        } else {
362            pos
363        };
364        lines.push(&data[start..end]);
365        start = pos + 1;
366    }
367    // Handle last line without trailing newline
368    if start < data.len() {
369        let end = if data.last() == Some(&b'\r') {
370            data.len() - 1
371        } else {
372            data.len()
373        };
374        lines.push(&data[start..end]);
375    }
376
377    pr_lines_generic(&lines, output, config, filename, file_date)
378}
379
380/// Fast multi-column "down" paginator using unsafe pointer arithmetic.
381/// Pre-splits lines via SIMD memchr, pre-computes column padding, and builds
382/// output directly into a Vec<u8> buffer with bulk copies. Avoids the many
383/// small extend_from_slice calls of the generic multicolumn path.
384fn pr_data_multicolumn_fast<W: Write>(
385    data: &[u8],
386    output: &mut W,
387    config: &PrConfig,
388    filename: &str,
389    file_date: Option<SystemTime>,
390) -> io::Result<()> {
391    let date = file_date.unwrap_or_else(SystemTime::now);
392    let header_str = config.header.as_deref().unwrap_or(filename);
393    let date_str = format_header_date(&date, &config.date_format);
394
395    let columns = config.columns.max(1);
396    let explicit_sep = has_explicit_separator(config);
397    let col_sep = get_column_separator(config);
398    let col_sep_bytes = col_sep.as_bytes();
399    let col_width = if explicit_sep {
400        if columns > 1 {
401            (config
402                .page_width
403                .saturating_sub(col_sep.len() * (columns - 1)))
404                / columns
405        } else {
406            config.page_width
407        }
408    } else {
409        config.page_width / columns
410    };
411    let content_width = if explicit_sep {
412        col_width
413    } else {
414        col_width.saturating_sub(1)
415    };
416
417    let suppress_header = !config.omit_header
418        && !config.omit_pagination
419        && config.page_length <= HEADER_LINES + FOOTER_LINES;
420    let body_lines_per_page = if config.omit_header || config.omit_pagination {
421        if config.page_length > 0 {
422            config.page_length
423        } else {
424            DEFAULT_PAGE_LENGTH
425        }
426    } else if suppress_header {
427        config.page_length
428    } else {
429        config.page_length - HEADER_LINES - FOOTER_LINES
430    };
431    let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
432    let lines_consumed_per_page = body_lines_per_page * columns;
433
434    // Pre-split lines using SIMD memchr — store (start, len) as u32 pairs for compactness
435    let mut line_offsets: Vec<(u32, u32)> = Vec::with_capacity(data.len() / 40 + 64);
436    let mut start = 0usize;
437    for pos in memchr::memchr_iter(b'\n', data) {
438        line_offsets.push((start as u32, (pos - start) as u32));
439        start = pos + 1;
440    }
441    if start < data.len() {
442        line_offsets.push((start as u32, (data.len() - start) as u32));
443    }
444    let total_lines = line_offsets.len();
445
446    // Single output buffer
447    let out_cap = (data.len() + data.len() / 3 + 4096).min(64 * 1024 * 1024);
448    let mut out_buf: Vec<u8> = Vec::with_capacity(out_cap);
449
450    let mut line_idx = 0usize;
451    let mut page_num = 1usize;
452    let src = data.as_ptr();
453
454    while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
455        if total_lines == 0 {
456            if show_header
457                && page_num >= config.first_page
458                && (config.last_page == 0 || page_num <= config.last_page)
459            {
460                write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
461                write_footer(&mut out_buf, config)?;
462            }
463            break;
464        }
465
466        let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
467        let page_lines = &line_offsets[line_idx..page_end];
468        let n = page_lines.len();
469
470        if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
471        {
472            if show_header {
473                write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
474            }
475
476            // Compute column layout: "down" mode distributes lines across columns
477            let base = n / columns;
478            let extra = n % columns;
479            // Guard ensures columns <= 32, so col_starts[columns] is always in bounds.
480            let mut col_starts = [0u32; 33];
481            let max_cols = columns;
482            for col in 0..max_cols {
483                let col_lines = base + if col < extra { 1 } else { 0 };
484                col_starts[col + 1] = col_starts[col] + col_lines as u32;
485            }
486            let num_rows = if extra > 0 { base + 1 } else { base };
487
488            // Build rows directly
489            for row in 0..num_rows {
490                // Find last column with data for this row
491                let mut last_data_col = 0;
492                for col in 0..max_cols {
493                    let col_lines = (col_starts[col + 1] - col_starts[col]) as usize;
494                    if row < col_lines {
495                        last_data_col = col;
496                    }
497                }
498
499                let mut abs_pos = 0usize;
500                for col in 0..max_cols {
501                    let col_lines = (col_starts[col + 1] - col_starts[col]) as usize;
502                    if row < col_lines {
503                        let li = col_starts[col] as usize + row;
504                        let (off, len) = page_lines[li];
505                        let content_len = (len as usize).min(content_width);
506
507                        if explicit_sep && col > 0 {
508                            out_buf.extend_from_slice(col_sep_bytes);
509                            abs_pos += col_sep_bytes.len();
510                        }
511
512                        // Copy line content directly from source.
513                        // SAFETY: `src` is `data.as_ptr()` and `out_buf` is a separate
514                        // allocation, so reallocating `out_buf` cannot invalidate `src`.
515                        // `off` and `len` come from `line_offsets` built by iterating
516                        // `memchr_iter` over `data`, guaranteeing `off + len <= data.len()`.
517                        // `content_len = len.min(content_width) <= len`, so
518                        // `off as usize + content_len <= data.len()`. The u32→usize cast
519                        // is lossless because the guard ensures `data.len() <= u32::MAX`.
520                        if content_len > 0 {
521                            let wp = out_buf.len();
522                            out_buf.reserve(content_len);
523                            unsafe {
524                                std::ptr::copy_nonoverlapping(
525                                    src.add(off as usize),
526                                    out_buf.as_mut_ptr().add(wp),
527                                    content_len,
528                                );
529                                out_buf.set_len(wp + content_len);
530                            }
531                        }
532
533                        // Strip trailing spaces from last data column (GNU compat)
534                        if col == last_data_col && !explicit_sep {
535                            while out_buf.last() == Some(&b' ') {
536                                out_buf.pop();
537                            }
538                        }
539
540                        // abs_pos tracks the column alignment position. The trailing
541                        // space strip only fires on last_data_col, after which no more
542                        // padding is needed, so the divergence is harmless.
543                        abs_pos += content_len;
544
545                        // Pad to next column boundary
546                        if col < last_data_col && !explicit_sep {
547                            let target = (col + 1) * col_width;
548                            if abs_pos < target {
549                                write_column_padding_buf(&mut out_buf, abs_pos, target);
550                                abs_pos = target;
551                            }
552                        }
553                    } else if col <= last_data_col {
554                        // Empty column before last data column
555                        if explicit_sep {
556                            if col > 0 {
557                                out_buf.extend_from_slice(col_sep_bytes);
558                                abs_pos += col_sep_bytes.len();
559                            }
560                        } else {
561                            let target = (col + 1) * col_width;
562                            write_column_padding_buf(&mut out_buf, abs_pos, target);
563                            abs_pos = target;
564                        }
565                    }
566                }
567                out_buf.push(b'\n');
568            }
569
570            // Pad remaining body lines
571            let body_lines_written = num_rows;
572            if show_header {
573                let pad = body_lines_per_page.saturating_sub(body_lines_written);
574                out_buf.resize(out_buf.len() + pad, b'\n');
575                write_footer(&mut out_buf, config)?;
576            }
577
578            if out_buf.len() >= 64 * 1024 * 1024 {
579                output.write_all(&out_buf)?;
580                out_buf.clear();
581            }
582        }
583
584        line_idx = page_end;
585        page_num += 1;
586    }
587
588    if !out_buf.is_empty() {
589        output.write_all(&out_buf)?;
590    }
591    Ok(())
592}
593
594/// Write column padding (tabs + spaces) directly into a Vec<u8> buffer.
595/// Avoids the Write trait overhead of write_column_padding.
596#[inline]
597fn write_column_padding_buf(buf: &mut Vec<u8>, abs_pos: usize, target: usize) {
598    let n = target.saturating_sub(abs_pos);
599    if n == 0 {
600        return;
601    }
602    let next_tab = (abs_pos / 8 + 1) * 8;
603    if next_tab > target {
604        // Gap doesn't reach next tab stop — spaces only
605        buf.resize(buf.len() + n, b' ');
606        return;
607    }
608    buf.push(b'\t');
609    let mut col = next_tab;
610    while col < target {
611        let nt = (col / 8 + 1) * 8;
612        if nt <= target {
613            buf.push(b'\t');
614            col = nt;
615        } else {
616            buf.resize(buf.len() + (target - col), b' ');
617            col = target;
618        }
619    }
620}
621
622/// Count newlines in a u64 word.
623/// Uses direct byte comparison — LLVM optimizes this to SIMD on x86-64.
624/// The classic SWAR subtract-and-mask formula has a borrow-propagation bug:
625/// when byte[i]=0x0A and byte[i+1]=0x0B, the borrow from the zero-detection
626/// subtraction falsely sets the indicator for byte[i+1], overcounting.
627#[inline(always)]
628fn count_newlines_u64(word: u64) -> u32 {
629    let b = word.to_ne_bytes();
630    (b[0] == b'\n') as u32
631        + (b[1] == b'\n') as u32
632        + (b[2] == b'\n') as u32
633        + (b[3] == b'\n') as u32
634        + (b[4] == b'\n') as u32
635        + (b[5] == b'\n') as u32
636        + (b[6] == b'\n') as u32
637        + (b[7] == b'\n') as u32
638}
639
640/// Ultra-fast contiguous-write paginator for single-column, no-transform mode.
641/// Two-pass approach: find page boundaries via SWAR+memchr, then build a single
642/// output buffer interleaving metadata (headers/footers) with body data copied
643/// from the original mmap via extend_from_slice. Flushed at a 4MB threshold.
644fn pr_data_contiguous<W: Write>(
645    data: &[u8],
646    output: &mut W,
647    config: &PrConfig,
648    filename: &str,
649    file_date: Option<SystemTime>,
650) -> io::Result<()> {
651    let date = file_date.unwrap_or_else(SystemTime::now);
652    let header_str = config.header.as_deref().unwrap_or(filename);
653    let date_str = format_header_date(&date, &config.date_format);
654
655    let suppress_header = !config.omit_header
656        && !config.omit_pagination
657        && config.page_length <= HEADER_LINES + FOOTER_LINES;
658    let body_lines_per_page = if config.omit_header || config.omit_pagination {
659        if config.page_length > 0 {
660            config.page_length
661        } else {
662            DEFAULT_PAGE_LENGTH
663        }
664    } else if suppress_header {
665        config.page_length
666    } else {
667        config.page_length - HEADER_LINES - FOOTER_LINES
668    };
669    let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
670
671    if data.is_empty() {
672        if show_header {
673            let mut page_buf: Vec<u8> = Vec::with_capacity(256);
674            write_header(&mut page_buf, &date_str, header_str, 1, config)?;
675            write_footer(&mut page_buf, config)?;
676            output.write_all(&page_buf)?;
677        }
678        return Ok(());
679    }
680
681    // Pass 1: find page boundaries using SWAR block counting.
682    // Process 64-byte blocks: count newlines with SWAR (8 u64 words per block),
683    // skip blocks that don't contain a page boundary, and only do precise
684    // memchr_iter scanning in blocks that cross a boundary. This reduces
685    // per-newline branch overhead from 2M iterations to ~35K.
686    let est_pages = data.len() / (body_lines_per_page * 40) + 2;
687    let mut page_bounds: Vec<usize> = Vec::with_capacity(est_pages + 1);
688    let mut page_line_counts: Vec<usize> = Vec::with_capacity(est_pages);
689    page_bounds.push(0);
690    let mut lines_count = 0usize;
691    let block_size = 64usize;
692    let mut block_start = 0usize;
693    let ptr = data.as_ptr();
694    while block_start + block_size <= data.len() {
695        let mut block_nl = 0u32;
696        unsafe {
697            for i in 0..8 {
698                let word = std::ptr::read_unaligned(ptr.add(block_start + i * 8) as *const u64);
699                block_nl += count_newlines_u64(word);
700            }
701        }
702        let block_nl = block_nl as usize;
703        if lines_count + block_nl < body_lines_per_page {
704            lines_count += block_nl;
705            block_start += block_size;
706        } else {
707            for nl_pos in memchr::memchr_iter(b'\n', &data[block_start..block_start + block_size]) {
708                lines_count += 1;
709                if lines_count >= body_lines_per_page {
710                    page_bounds.push(block_start + nl_pos + 1);
711                    page_line_counts.push(lines_count);
712                    lines_count = 0;
713                }
714            }
715            block_start += block_size;
716        }
717    }
718    // Handle remaining bytes after last full block
719    for nl_pos in memchr::memchr_iter(b'\n', &data[block_start..]) {
720        lines_count += 1;
721        if lines_count >= body_lines_per_page {
722            page_bounds.push(block_start + nl_pos + 1);
723            page_line_counts.push(lines_count);
724            lines_count = 0;
725        }
726    }
727    if *page_bounds.last().unwrap() < data.len() {
728        page_bounds.push(data.len());
729        page_line_counts.push(lines_count);
730    }
731    let total_pages = page_bounds.len() - 1;
732
733    let first_visible = config.first_page.max(1) - 1;
734    let last_visible = if config.last_page == 0 {
735        total_pages
736    } else {
737        config.last_page.min(total_pages)
738    };
739    if first_visible >= total_pages {
740        return Ok(());
741    }
742
743    if !show_header {
744        // No headers/footers — write visible body pages contiguously
745        let start = page_bounds[first_visible];
746        let end = page_bounds[last_visible];
747        return output.write_all(&data[start..end]);
748    }
749
750    // Pass 2: build output in a single buffer with 4MB flush threshold.
751    // Headers/footers are generated inline; body data is copied from the mmap.
752    // A single large write_all uses fewer syscalls than batched writev, which
753    // is critical for file output (where writev overhead dominates).
754    let footer_bytes: &[u8] = if config.form_feed {
755        b"\x0c"
756    } else {
757        b"\n\n\n\n\n"
758    };
759    let date_bytes = date_str.as_bytes();
760    let header_bytes = header_str.as_bytes();
761    let line_width = config.page_width;
762    let left_len = date_bytes.len();
763    let center_len = header_bytes.len();
764    let page_prefix = b"Page ";
765    let flush_threshold = 4 * 1024 * 1024;
766    let mut out_buf: Vec<u8> = Vec::with_capacity(flush_threshold + 256 * 1024);
767    let mut num_tmp = [0u8; 20];
768
769    for pi in first_visible..last_visible {
770        let page_num = pi + 1;
771        let body_start = page_bounds[pi];
772        let body_end = page_bounds[pi + 1];
773
774        // Format page number into num_tmp (right-aligned, digits end at index 19).
775        let mut n = page_num;
776        let mut pos = 19usize;
777        loop {
778            num_tmp[pos] = b'0' + (n % 10) as u8;
779            n /= 10;
780            if n == 0 {
781                break;
782            }
783            pos -= 1;
784        }
785        let num_digits = 20 - pos;
786        let right_len = page_prefix.len() + num_digits;
787
788        // Check overflow with the actual page number width each iteration,
789        // since the digit count can grow as page numbers increase.
790        let overflow = left_len + center_len + right_len + 2 >= line_width;
791
792        if overflow {
793            out_buf.extend_from_slice(b"\n\n");
794            out_buf.extend_from_slice(date_bytes);
795            out_buf.push(b' ');
796            out_buf.extend_from_slice(header_bytes);
797            out_buf.push(b' ');
798            out_buf.extend_from_slice(page_prefix);
799            out_buf.extend_from_slice(&num_tmp[pos..20]);
800            out_buf.extend_from_slice(b"\n\n\n");
801        } else {
802            // Safety: overflow check above guarantees
803            // line_width > left_len + center_len + right_len, so this
804            // subtraction cannot underflow.
805            let total_spaces = line_width - left_len - center_len - right_len;
806            let left_spaces = total_spaces / 2;
807            let right_spaces = total_spaces - left_spaces;
808            let hdr_len = 2
809                + left_len
810                + left_spaces
811                + center_len
812                + right_spaces
813                + page_prefix.len()
814                + num_digits
815                + 3;
816            out_buf.reserve(hdr_len);
817            let wp = out_buf.len();
818            unsafe {
819                let dst = out_buf.as_mut_ptr().add(wp);
820                *dst = b'\n';
821                *dst.add(1) = b'\n';
822                let mut off = 2;
823                std::ptr::copy_nonoverlapping(date_bytes.as_ptr(), dst.add(off), left_len);
824                off += left_len;
825                std::ptr::write_bytes(dst.add(off), b' ', left_spaces);
826                off += left_spaces;
827                std::ptr::copy_nonoverlapping(header_bytes.as_ptr(), dst.add(off), center_len);
828                off += center_len;
829                std::ptr::write_bytes(dst.add(off), b' ', right_spaces);
830                off += right_spaces;
831                std::ptr::copy_nonoverlapping(
832                    page_prefix.as_ptr(),
833                    dst.add(off),
834                    page_prefix.len(),
835                );
836                off += page_prefix.len();
837                std::ptr::copy_nonoverlapping(num_tmp.as_ptr().add(pos), dst.add(off), num_digits);
838                off += num_digits;
839                *dst.add(off) = b'\n';
840                *dst.add(off + 1) = b'\n';
841                *dst.add(off + 2) = b'\n';
842                off += 3;
843                out_buf.set_len(wp + off);
844            }
845        }
846
847        out_buf.extend_from_slice(&data[body_start..body_end]);
848
849        let actual_lines = page_line_counts[pi];
850        let has_unterminated = body_end > body_start && data[body_end - 1] != b'\n';
851        if has_unterminated {
852            out_buf.push(b'\n');
853        }
854        let effective_lines = actual_lines + has_unterminated as usize;
855        let pad = body_lines_per_page.saturating_sub(effective_lines);
856        out_buf.resize(out_buf.len() + pad, b'\n');
857        out_buf.extend_from_slice(footer_bytes);
858
859        if out_buf.len() >= flush_threshold {
860            output.write_all(&out_buf)?;
861            out_buf.clear();
862        }
863    }
864
865    if !out_buf.is_empty() {
866        output.write_all(&out_buf)?;
867    }
868    Ok(())
869}
870
871/// Fast numbered single-column paginator.
872/// Uses unsafe pointer arithmetic to format numbered lines directly into
873/// a pre-allocated buffer, avoiding per-line write_all overhead.
874fn pr_data_numbered<W: Write>(
875    data: &[u8],
876    output: &mut W,
877    config: &PrConfig,
878    filename: &str,
879    file_date: Option<SystemTime>,
880) -> io::Result<()> {
881    let date = file_date.unwrap_or_else(SystemTime::now);
882    let header_str = config.header.as_deref().unwrap_or(filename);
883    let date_str = format_header_date(&date, &config.date_format);
884
885    let (sep_char, digits) = config.number_lines.unwrap_or(('\t', 5));
886    debug_assert!(sep_char.is_ascii(), "number separator must be ASCII");
887    let sep_byte = sep_char as u8;
888    let suppress_header = !config.omit_header
889        && !config.omit_pagination
890        && config.page_length <= HEADER_LINES + FOOTER_LINES;
891    let body_lines_per_page = if config.omit_header || config.omit_pagination {
892        if config.page_length > 0 {
893            config.page_length
894        } else {
895            DEFAULT_PAGE_LENGTH
896        }
897    } else if suppress_header {
898        config.page_length
899    } else {
900        config.page_length - HEADER_LINES - FOOTER_LINES
901    };
902    let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
903
904    // Pre-split lines using SIMD memchr for fast iteration
905    let mut line_starts: Vec<usize> = Vec::with_capacity(data.len() / 40 + 64);
906    line_starts.push(0);
907    for pos in memchr::memchr_iter(b'\n', data) {
908        line_starts.push(pos + 1);
909    }
910    let total_lines = if !data.is_empty() && data[data.len() - 1] == b'\n' {
911        line_starts.len() - 1
912    } else {
913        line_starts.len()
914    };
915
916    // Single output buffer — avoids per-page write_all syscalls.
917    // Initial capacity capped at 64MB; Vec grows on demand for larger inputs.
918    let num_prefix_est = digits + 2; // padding + digits + separator
919    let out_cap =
920        (data.len() + total_lines * num_prefix_est + total_lines / 5 + 4096).min(64 * 1024 * 1024);
921    let mut out_buf: Vec<u8> = Vec::with_capacity(out_cap);
922
923    let mut line_number = config.first_line_number;
924    let mut page_num = 1usize;
925    let mut line_idx = 0;
926
927    while line_idx < total_lines {
928        let page_end = (line_idx + body_lines_per_page).min(total_lines);
929        let in_range = page_num >= config.first_page
930            && (config.last_page == 0 || page_num <= config.last_page);
931
932        if in_range {
933            if show_header {
934                write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
935            }
936
937            // Write numbered lines using unsafe pointer arithmetic.
938            // SAFETY: `src` points into `data` (a borrowed &[u8]) and is
939            // independent of `out_buf`. Reallocations of `out_buf` cannot
940            // invalidate `src` because they are separate allocations.
941            let src = data.as_ptr();
942            for li in line_idx..page_end {
943                let line_start = line_starts[li];
944                let line_end = if li + 1 < line_starts.len() {
945                    let end = line_starts[li + 1] - 1;
946                    if end > line_start && data[end - 1] == b'\r' {
947                        end - 1
948                    } else {
949                        end
950                    }
951                } else {
952                    data.len()
953                };
954                let line_len = line_end - line_start;
955
956                let wp = out_buf.len();
957
958                let mut n = line_number;
959                let mut num_pos = 19usize;
960                let mut num_tmp = [0u8; 20];
961                loop {
962                    num_tmp[num_pos] = b'0' + (n % 10) as u8;
963                    n /= 10;
964                    if n == 0 || num_pos == 0 {
965                        break;
966                    }
967                    num_pos -= 1;
968                }
969                let num_digits = 20 - num_pos;
970                let padding = digits.saturating_sub(num_digits);
971                let actual_prefix = padding + num_digits + 1;
972
973                let needed = actual_prefix + line_len + 1;
974                if out_buf.len() + needed > out_buf.capacity() {
975                    out_buf.reserve(needed);
976                }
977                let base = out_buf.as_mut_ptr();
978
979                unsafe {
980                    let dst = base.add(wp);
981                    std::ptr::write_bytes(dst, b' ', padding);
982                    std::ptr::copy_nonoverlapping(
983                        num_tmp.as_ptr().add(num_pos),
984                        dst.add(padding),
985                        num_digits,
986                    );
987                    *dst.add(padding + num_digits) = sep_byte;
988                    if line_len > 0 {
989                        std::ptr::copy_nonoverlapping(
990                            src.add(line_start),
991                            dst.add(actual_prefix),
992                            line_len,
993                        );
994                    }
995                    *dst.add(actual_prefix + line_len) = b'\n';
996                    out_buf.set_len(wp + actual_prefix + line_len + 1);
997                }
998
999                line_number += 1;
1000            }
1001
1002            if show_header {
1003                let body_lines_written = page_end - line_idx;
1004                let pad = body_lines_per_page.saturating_sub(body_lines_written);
1005                out_buf.resize(out_buf.len() + pad, b'\n');
1006                write_footer(&mut out_buf, config)?;
1007            }
1008
1009            // Flush to writer when buffer exceeds 64MB to bound memory usage.
1010            if out_buf.len() >= 64 * 1024 * 1024 {
1011                output.write_all(&out_buf)?;
1012                out_buf.clear();
1013            }
1014        } else {
1015            line_number += page_end - line_idx;
1016        }
1017
1018        line_idx = page_end;
1019        page_num += 1;
1020    }
1021
1022    if !out_buf.is_empty() {
1023        output.write_all(&out_buf)?;
1024    }
1025    Ok(())
1026}
1027
1028/// Paginate a single file and write output.
1029pub fn pr_file<R: BufRead, W: Write>(
1030    input: R,
1031    output: &mut W,
1032    config: &PrConfig,
1033    filename: &str,
1034    file_date: Option<SystemTime>,
1035) -> io::Result<()> {
1036    // Read all lines with transforms applied
1037    let mut all_lines: Vec<String> = Vec::new();
1038    for line_result in input.lines() {
1039        let line = line_result?;
1040        let mut line = line;
1041
1042        // Expand tabs if requested
1043        if let Some((tab_char, tab_width)) = config.expand_tabs {
1044            line = expand_tabs_in_line(&line, tab_char, tab_width);
1045        }
1046
1047        // Process control characters (skip when not needed to avoid copying)
1048        if config.show_control_chars || config.show_nonprinting {
1049            line = process_control_chars(&line, config.show_control_chars, config.show_nonprinting);
1050        }
1051
1052        all_lines.push(line);
1053    }
1054
1055    // Convert to &[u8] slices for the byte-based paginator
1056    let refs: Vec<&[u8]> = all_lines.iter().map(|s| s.as_bytes()).collect();
1057    pr_lines_generic(&refs, output, config, filename, file_date)
1058}
1059
1060/// Core paginator that works on a slice of byte slices (zero-copy).
1061fn pr_lines_generic<W: Write>(
1062    all_lines: &[&[u8]],
1063    output: &mut W,
1064    config: &PrConfig,
1065    filename: &str,
1066    file_date: Option<SystemTime>,
1067) -> io::Result<()> {
1068    let date = file_date.unwrap_or_else(SystemTime::now);
1069
1070    let header_str = config.header.as_deref().unwrap_or(filename);
1071    let date_str = format_header_date(&date, &config.date_format);
1072
1073    // Calculate body lines per page
1074    // When page_length is too small for header+footer, GNU pr suppresses
1075    // headers/footers and uses page_length as the body size.
1076    let suppress_header = !config.omit_header
1077        && !config.omit_pagination
1078        && config.page_length <= HEADER_LINES + FOOTER_LINES;
1079    // When suppress_header is active, create a config view with omit_header set
1080    // so that sub-functions skip padding to body_lines_per_page.
1081    let suppressed_config;
1082    let effective_config = if suppress_header {
1083        suppressed_config = PrConfig {
1084            omit_header: true,
1085            ..config.clone()
1086        };
1087        &suppressed_config
1088    } else {
1089        config
1090    };
1091    let body_lines_per_page = if config.omit_header || config.omit_pagination {
1092        if config.page_length > 0 {
1093            config.page_length
1094        } else {
1095            DEFAULT_PAGE_LENGTH
1096        }
1097    } else if suppress_header {
1098        config.page_length
1099    } else {
1100        config.page_length - HEADER_LINES - FOOTER_LINES
1101    };
1102
1103    // Account for double spacing: each input line takes 2 output lines
1104    let input_lines_per_page = if config.double_space {
1105        (body_lines_per_page + 1) / 2
1106    } else {
1107        body_lines_per_page
1108    };
1109
1110    // Handle multi-column mode
1111    let columns = config.columns.max(1);
1112
1113    // GNU pr in multi-column down mode: each page has body_lines_per_page rows,
1114    // each row shows one value from each column. So up to
1115    // input_lines_per_page * columns input lines can be consumed per page.
1116    // actual_lines_per_column = ceil(page_lines / columns) for each page.
1117    let lines_consumed_per_page = if columns > 1 && !config.across {
1118        input_lines_per_page * columns
1119    } else {
1120        input_lines_per_page
1121    };
1122
1123    // Single output buffer for all pages — one write_all at the end
1124    let total_lines = all_lines.len();
1125    let mut line_number = config.first_line_number;
1126    let mut page_num = 1usize;
1127    let mut line_idx = 0;
1128    let total_bytes: usize = all_lines.iter().map(|l| l.len() + 1).sum();
1129    // Initial capacity capped at 64MB; Vec grows on demand for larger inputs.
1130    let out_cap = (total_bytes + total_bytes / 5 + 4096).min(64 * 1024 * 1024);
1131    let mut out_buf: Vec<u8> = Vec::with_capacity(out_cap);
1132
1133    while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
1134        if total_lines == 0 && line_idx == 0 {
1135            if page_num >= config.first_page
1136                && (config.last_page == 0 || page_num <= config.last_page)
1137            {
1138                if !config.omit_header && !config.omit_pagination && !suppress_header {
1139                    write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
1140                    write_footer(&mut out_buf, config)?;
1141                }
1142            }
1143            break;
1144        }
1145
1146        let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
1147
1148        if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
1149        {
1150            if !config.omit_header && !config.omit_pagination && !suppress_header {
1151                write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
1152            }
1153
1154            if columns > 1 {
1155                write_multicolumn_body(
1156                    &mut out_buf,
1157                    &all_lines[line_idx..page_end],
1158                    effective_config,
1159                    columns,
1160                    &mut line_number,
1161                    body_lines_per_page,
1162                )?;
1163            } else {
1164                write_single_column_body(
1165                    &mut out_buf,
1166                    &all_lines[line_idx..page_end],
1167                    effective_config,
1168                    &mut line_number,
1169                    body_lines_per_page,
1170                )?;
1171            }
1172
1173            if !config.omit_header && !config.omit_pagination && !suppress_header {
1174                write_footer(&mut out_buf, config)?;
1175            }
1176
1177            // Flush to writer when buffer exceeds 64MB to bound memory usage.
1178            if out_buf.len() >= 64 * 1024 * 1024 {
1179                output.write_all(&out_buf)?;
1180                out_buf.clear();
1181            }
1182        }
1183
1184        line_idx = page_end;
1185        page_num += 1;
1186
1187        if line_idx >= total_lines {
1188            break;
1189        }
1190    }
1191
1192    if !out_buf.is_empty() {
1193        output.write_all(&out_buf)?;
1194    }
1195    Ok(())
1196}
1197
1198/// Paginate multiple files merged side by side (-m mode).
1199pub fn pr_merge<W: Write>(
1200    inputs: &[Vec<String>],
1201    output: &mut W,
1202    config: &PrConfig,
1203    _filenames: &[&str],
1204    file_dates: &[SystemTime],
1205) -> io::Result<()> {
1206    let date = file_dates.first().copied().unwrap_or_else(SystemTime::now);
1207    let date_str = format_header_date(&date, &config.date_format);
1208    let header_str = config.header.as_deref().unwrap_or("");
1209
1210    let suppress_header = !config.omit_header
1211        && !config.omit_pagination
1212        && config.page_length <= HEADER_LINES + FOOTER_LINES;
1213    let body_lines_per_page = if config.omit_header || config.omit_pagination {
1214        if config.page_length > 0 {
1215            config.page_length
1216        } else {
1217            DEFAULT_PAGE_LENGTH
1218        }
1219    } else if suppress_header {
1220        config.page_length
1221    } else {
1222        config.page_length - HEADER_LINES - FOOTER_LINES
1223    };
1224
1225    let input_lines_per_page = if config.double_space {
1226        (body_lines_per_page + 1) / 2
1227    } else {
1228        body_lines_per_page
1229    };
1230
1231    let num_files = inputs.len();
1232    let explicit_sep = has_explicit_separator(config);
1233    let col_sep = get_column_separator(config);
1234    let col_width = if explicit_sep {
1235        if num_files > 1 {
1236            (config
1237                .page_width
1238                .saturating_sub(col_sep.len() * (num_files - 1)))
1239                / num_files
1240        } else {
1241            config.page_width
1242        }
1243    } else {
1244        config.page_width / num_files
1245    };
1246
1247    let max_lines = inputs.iter().map(|f| f.len()).max().unwrap_or(0);
1248    let mut page_num = 1usize;
1249    let mut line_idx = 0;
1250    let mut line_number = config.first_line_number;
1251
1252    let col_sep_bytes = col_sep.as_bytes();
1253    let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
1254    let mut num_buf = [0u8; 32];
1255
1256    while line_idx < max_lines {
1257        let page_end = (line_idx + input_lines_per_page).min(max_lines);
1258
1259        if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
1260        {
1261            page_buf.clear();
1262
1263            if !config.omit_header && !config.omit_pagination && !suppress_header {
1264                write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
1265            }
1266
1267            let indent_str = " ".repeat(config.indent);
1268            let mut body_lines_written = 0;
1269            for i in line_idx..page_end {
1270                if config.double_space && body_lines_written > 0 {
1271                    page_buf.push(b'\n');
1272                    body_lines_written += 1;
1273                }
1274
1275                page_buf.extend_from_slice(indent_str.as_bytes());
1276                let mut abs_pos = config.indent;
1277
1278                if let Some((sep, digits)) = config.number_lines {
1279                    let num_str = format_line_number(line_number, sep, digits, &mut num_buf);
1280                    page_buf.extend_from_slice(num_str);
1281                    abs_pos += digits + 1;
1282                    line_number += 1;
1283                }
1284
1285                for (fi, file_lines) in inputs.iter().enumerate() {
1286                    let content = if i < file_lines.len() {
1287                        file_lines[i].as_bytes()
1288                    } else {
1289                        b"" as &[u8]
1290                    };
1291                    let truncated = if !explicit_sep && content.len() > col_width.saturating_sub(1)
1292                    {
1293                        &content[..col_width.saturating_sub(1)]
1294                    } else if explicit_sep && config.truncate_lines && content.len() > col_width {
1295                        &content[..col_width]
1296                    } else {
1297                        content
1298                    };
1299                    if fi < num_files - 1 {
1300                        if explicit_sep {
1301                            if fi > 0 {
1302                                page_buf.extend_from_slice(col_sep_bytes);
1303                            }
1304                            page_buf.extend_from_slice(truncated);
1305                            abs_pos +=
1306                                truncated.len() + if fi > 0 { col_sep_bytes.len() } else { 0 };
1307                        } else {
1308                            page_buf.extend_from_slice(truncated);
1309                            abs_pos += truncated.len();
1310                            let target = (fi + 1) * col_width + config.indent;
1311                            write_column_padding(&mut page_buf, abs_pos, target)?;
1312                            abs_pos = target;
1313                        }
1314                    } else {
1315                        if explicit_sep && fi > 0 {
1316                            page_buf.extend_from_slice(col_sep_bytes);
1317                        }
1318                        page_buf.extend_from_slice(truncated);
1319                    }
1320                }
1321                page_buf.push(b'\n');
1322                body_lines_written += 1;
1323            }
1324
1325            // Pad remaining body lines
1326            while body_lines_written < body_lines_per_page {
1327                page_buf.push(b'\n');
1328                body_lines_written += 1;
1329            }
1330
1331            if !config.omit_header && !config.omit_pagination && !suppress_header {
1332                write_footer(&mut page_buf, config)?;
1333            }
1334
1335            output.write_all(&page_buf)?;
1336        }
1337
1338        line_idx = page_end;
1339        page_num += 1;
1340    }
1341
1342    Ok(())
1343}
1344
1345/// Write page header: 2 blank lines, date/header/page line, 2 blank lines.
1346fn write_header<W: Write>(
1347    output: &mut W,
1348    date_str: &str,
1349    header: &str,
1350    page_num: usize,
1351    config: &PrConfig,
1352) -> io::Result<()> {
1353    // 2 blank lines
1354    output.write_all(b"\n\n")?;
1355
1356    // Header line: date is left-aligned, header is centered, Page N is right-aligned.
1357    let line_width = config.page_width;
1358
1359    let left = date_str;
1360    let center = header;
1361    let left_len = left.len();
1362    let center_len = center.len();
1363
1364    // Format "Page N" without allocation for small page numbers
1365    let mut page_buf = [0u8; 32];
1366    let page_str = format_page_number(page_num, &mut page_buf);
1367    let right_len = page_str.len();
1368
1369    // GNU pr centers the header title within the line.
1370    if left_len + center_len + right_len + 2 >= line_width {
1371        output.write_all(left.as_bytes())?;
1372        output.write_all(b" ")?;
1373        output.write_all(center.as_bytes())?;
1374        output.write_all(b" ")?;
1375        output.write_all(page_str)?;
1376        output.write_all(b"\n")?;
1377    } else {
1378        let total_spaces = line_width - left_len - center_len - right_len;
1379        let left_spaces = total_spaces / 2;
1380        let right_spaces = total_spaces - left_spaces;
1381        output.write_all(left.as_bytes())?;
1382        write_spaces(output, left_spaces)?;
1383        output.write_all(center.as_bytes())?;
1384        write_spaces(output, right_spaces)?;
1385        output.write_all(page_str)?;
1386        output.write_all(b"\n")?;
1387    }
1388
1389    // 2 blank lines
1390    output.write_all(b"\n\n")?;
1391
1392    Ok(())
1393}
1394
1395/// Format "Page N" into a stack buffer, returning the used slice.
1396#[inline]
1397fn format_page_number(page_num: usize, buf: &mut [u8; 32]) -> &[u8] {
1398    const PREFIX: &[u8] = b"Page ";
1399    let prefix_len = PREFIX.len();
1400    buf[..prefix_len].copy_from_slice(PREFIX);
1401    // Format number into a separate stack buffer to avoid overlapping borrow
1402    let mut num_buf = [0u8; 20];
1403    let mut n = page_num;
1404    let mut pos = 19;
1405    loop {
1406        num_buf[pos] = b'0' + (n % 10) as u8;
1407        n /= 10;
1408        if n == 0 {
1409            break;
1410        }
1411        pos -= 1;
1412    }
1413    let num_len = 20 - pos;
1414    buf[prefix_len..prefix_len + num_len].copy_from_slice(&num_buf[pos..20]);
1415    &buf[..prefix_len + num_len]
1416}
1417
1418/// Write page footer: 5 blank lines (or form feed).
1419fn write_footer<W: Write>(output: &mut W, config: &PrConfig) -> io::Result<()> {
1420    if config.form_feed {
1421        output.write_all(b"\x0c")?;
1422    } else {
1423        output.write_all(b"\n\n\n\n\n")?;
1424    }
1425    Ok(())
1426}
1427
1428/// Write body for single column mode.
1429fn write_single_column_body<W: Write>(
1430    output: &mut W,
1431    lines: &[&[u8]],
1432    config: &PrConfig,
1433    line_number: &mut usize,
1434    body_lines_per_page: usize,
1435) -> io::Result<()> {
1436    let indent_str = " ".repeat(config.indent);
1437    let content_width = if config.truncate_lines {
1438        compute_content_width(config)
1439    } else {
1440        0
1441    };
1442    let mut body_lines_written = 0;
1443    // Pre-allocate line number buffer to avoid per-line write! formatting
1444    let mut num_buf = [0u8; 32];
1445
1446    for line in lines.iter() {
1447        output.write_all(indent_str.as_bytes())?;
1448
1449        if let Some((sep, digits)) = config.number_lines {
1450            // Format line number directly into buffer, avoiding write! overhead
1451            let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1452            output.write_all(num_str)?;
1453            *line_number += 1;
1454        }
1455
1456        let content: &[u8] = if config.truncate_lines {
1457            if line.len() > content_width {
1458                &line[..content_width]
1459            } else {
1460                line
1461            }
1462        } else {
1463            line
1464        };
1465
1466        // Direct write_all of byte slice — no format dispatch or UTF-8 overhead
1467        output.write_all(content)?;
1468        output.write_all(b"\n")?;
1469        body_lines_written += 1;
1470        if body_lines_written >= body_lines_per_page {
1471            break;
1472        }
1473
1474        // Double-space: write blank line AFTER each content line
1475        if config.double_space {
1476            output.write_all(b"\n")?;
1477            body_lines_written += 1;
1478            if body_lines_written >= body_lines_per_page {
1479                break;
1480            }
1481        }
1482    }
1483
1484    // Pad remaining body lines if not omitting headers
1485    if !config.omit_header && !config.omit_pagination {
1486        while body_lines_written < body_lines_per_page {
1487            output.write_all(b"\n")?;
1488            body_lines_written += 1;
1489        }
1490    }
1491
1492    Ok(())
1493}
1494
1495/// Format a line number with right-aligned padding and separator into a stack buffer.
1496/// Returns the formatted slice. Avoids write!() per-line overhead.
1497#[inline]
1498fn format_line_number(num: usize, sep: char, digits: usize, buf: &mut [u8; 32]) -> &[u8] {
1499    // Format the number
1500    let mut n = num;
1501    let mut pos = 31;
1502    loop {
1503        buf[pos] = b'0' + (n % 10) as u8;
1504        n /= 10;
1505        if n == 0 || pos == 0 {
1506            break;
1507        }
1508        pos -= 1;
1509    }
1510    let num_digits = 32 - pos;
1511    // Build the output: spaces for padding + number + separator
1512    let padding = if digits > num_digits {
1513        digits - num_digits
1514    } else {
1515        0
1516    };
1517    let total_len = padding + num_digits + sep.len_utf8();
1518    // We need a separate output buffer since we're using buf for the number
1519    // Just use the write_all approach with two calls for simplicity
1520    let start = 32 - num_digits;
1521    // Return just the number portion; caller handles padding via spaces
1522    // Actually, let's format properly into a contiguous buffer
1523    let sep_byte = sep as u8; // ASCII separator assumed
1524    let out_start = 32usize.saturating_sub(total_len);
1525    // Fill padding
1526    for i in out_start..out_start + padding {
1527        buf[i] = b' ';
1528    }
1529    // Number is already at positions [start..32], shift if needed
1530    if out_start + padding != start {
1531        let src = start;
1532        let dst = out_start + padding;
1533        for i in 0..num_digits {
1534            buf[dst + i] = buf[src + i];
1535        }
1536    }
1537    // Add separator
1538    buf[out_start + padding + num_digits] = sep_byte;
1539    &buf[out_start..out_start + total_len]
1540}
1541
1542/// Compute available content width after accounting for numbering and indent.
1543fn compute_content_width(config: &PrConfig) -> usize {
1544    let mut w = config.page_width;
1545    w = w.saturating_sub(config.indent);
1546    if let Some((_, digits)) = config.number_lines {
1547        w = w.saturating_sub(digits + 1); // digits + separator
1548    }
1549    w
1550}
1551
1552/// Write body for multi-column mode.
1553fn write_multicolumn_body<W: Write>(
1554    output: &mut W,
1555    lines: &[&[u8]],
1556    config: &PrConfig,
1557    columns: usize,
1558    line_number: &mut usize,
1559    body_lines_per_page: usize,
1560) -> io::Result<()> {
1561    let explicit_sep = has_explicit_separator(config);
1562    let col_sep = get_column_separator(config);
1563    // When no explicit separator, GNU pr uses the full page_width / columns as column width
1564    // and pads with tabs. When separator is explicit, use sep width in calculation.
1565    let col_width = if explicit_sep {
1566        if columns > 1 {
1567            (config
1568                .page_width
1569                .saturating_sub(col_sep.len() * (columns - 1)))
1570                / columns
1571        } else {
1572            config.page_width
1573        }
1574    } else {
1575        config.page_width / columns
1576    };
1577    // GNU pr truncates lines in multi-column mode by default, unless -J (join_lines) is set.
1578    // For non-explicit separator, truncate to col_width - 1 to leave room for padding.
1579    let do_truncate = !config.join_lines;
1580    let content_width = if explicit_sep {
1581        col_width
1582    } else {
1583        col_width.saturating_sub(1)
1584    };
1585
1586    let indent_str = " ".repeat(config.indent);
1587    let col_sep_bytes = col_sep.as_bytes();
1588    let mut body_lines_written = 0;
1589    let mut num_buf = [0u8; 32];
1590
1591    if config.across {
1592        // Print columns across: line 0 fills col0, line 1 fills col1, etc.
1593        let mut i = 0;
1594        while i < lines.len() {
1595            if config.double_space && body_lines_written > 0 {
1596                output.write_all(b"\n")?;
1597                body_lines_written += 1;
1598                if body_lines_written >= body_lines_per_page {
1599                    break;
1600                }
1601            }
1602
1603            output.write_all(indent_str.as_bytes())?;
1604            let mut abs_pos = config.indent;
1605
1606            // Find the last column with data on this row
1607            let mut last_data_col = 0;
1608            for col in 0..columns {
1609                let li = i + col;
1610                if li < lines.len() {
1611                    last_data_col = col;
1612                }
1613            }
1614
1615            for col in 0..columns {
1616                let li = i + col;
1617                if li < lines.len() {
1618                    if explicit_sep && col > 0 {
1619                        output.write_all(col_sep_bytes)?;
1620                        abs_pos += col_sep_bytes.len();
1621                    }
1622                    if let Some((sep, digits)) = config.number_lines {
1623                        let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1624                        output.write_all(num_str)?;
1625                        abs_pos += digits + 1;
1626                        *line_number += 1;
1627                    }
1628                    let content: &[u8] = lines[li];
1629                    let mut truncated = if do_truncate && content.len() > content_width {
1630                        &content[..content_width]
1631                    } else {
1632                        content
1633                    };
1634                    // GNU pr strips trailing spaces from the last column
1635                    if col == last_data_col && !explicit_sep {
1636                        while truncated.last() == Some(&b' ') {
1637                            truncated = &truncated[..truncated.len() - 1];
1638                        }
1639                    }
1640                    output.write_all(truncated)?;
1641                    abs_pos += truncated.len();
1642                    if col < last_data_col && !explicit_sep {
1643                        let target = (col + 1) * col_width + config.indent;
1644                        write_column_padding(output, abs_pos, target)?;
1645                        abs_pos = target;
1646                    }
1647                }
1648            }
1649            output.write_all(b"\n")?;
1650            body_lines_written += 1;
1651            i += columns;
1652        }
1653    } else {
1654        // Print columns down: distribute lines across columns.
1655        // GNU pr distributes evenly: base = lines/cols, extra = lines%cols.
1656        // First 'extra' columns get base+1 lines, rest get base lines.
1657        let n = lines.len();
1658        let base = n / columns;
1659        let extra = n % columns;
1660
1661        // Compute start offset of each column
1662        let mut col_starts = vec![0usize; columns + 1];
1663        for col in 0..columns {
1664            let col_lines = base + if col < extra { 1 } else { 0 };
1665            col_starts[col + 1] = col_starts[col] + col_lines;
1666        }
1667
1668        // Number of rows = max lines in any column
1669        let num_rows = if extra > 0 { base + 1 } else { base };
1670
1671        for row in 0..num_rows {
1672            if config.double_space && row > 0 {
1673                output.write_all(b"\n")?;
1674                body_lines_written += 1;
1675                if body_lines_written >= body_lines_per_page {
1676                    break;
1677                }
1678            }
1679
1680            output.write_all(indent_str.as_bytes())?;
1681            let mut abs_pos = config.indent;
1682
1683            // Find the last column with data for this row
1684            let mut last_data_col = 0;
1685            for col in 0..columns {
1686                let col_lines = col_starts[col + 1] - col_starts[col];
1687                if row < col_lines {
1688                    last_data_col = col;
1689                }
1690            }
1691
1692            for col in 0..columns {
1693                let col_lines = col_starts[col + 1] - col_starts[col];
1694                let li = col_starts[col] + row;
1695                if row < col_lines {
1696                    if explicit_sep && col > 0 {
1697                        output.write_all(col_sep_bytes)?;
1698                        abs_pos += col_sep_bytes.len();
1699                    }
1700                    if let Some((sep, digits)) = config.number_lines {
1701                        let num = config.first_line_number + li;
1702                        let num_str = format_line_number(num, sep, digits, &mut num_buf);
1703                        output.write_all(num_str)?;
1704                        abs_pos += digits + 1;
1705                    }
1706                    let content: &[u8] = lines[li];
1707                    let mut truncated = if do_truncate && content.len() > content_width {
1708                        &content[..content_width]
1709                    } else {
1710                        content
1711                    };
1712                    // GNU pr strips trailing spaces from the last column
1713                    if col == last_data_col && !explicit_sep {
1714                        while truncated.last() == Some(&b' ') {
1715                            truncated = &truncated[..truncated.len() - 1];
1716                        }
1717                    }
1718                    output.write_all(truncated)?;
1719                    abs_pos += truncated.len();
1720                    if col < last_data_col && !explicit_sep {
1721                        // Not the last column with data: pad to next column boundary
1722                        let target = (col + 1) * col_width + config.indent;
1723                        write_column_padding(output, abs_pos, target)?;
1724                        abs_pos = target;
1725                    }
1726                } else if col <= last_data_col {
1727                    // Empty column before the last data column: pad to next boundary
1728                    if explicit_sep {
1729                        if col > 0 {
1730                            output.write_all(col_sep_bytes)?;
1731                            abs_pos += col_sep_bytes.len();
1732                        }
1733                        // For explicit separator, just write separator, no padding
1734                    } else {
1735                        let target = (col + 1) * col_width + config.indent;
1736                        write_column_padding(output, abs_pos, target)?;
1737                        abs_pos = target;
1738                    }
1739                }
1740                // Empty columns after last data column: skip entirely
1741            }
1742            output.write_all(b"\n")?;
1743            body_lines_written += 1;
1744        }
1745        // Update line_number for the lines we processed
1746        if config.number_lines.is_some() {
1747            *line_number += lines.len();
1748        }
1749    }
1750
1751    // Pad remaining body lines
1752    if !config.omit_header && !config.omit_pagination {
1753        while body_lines_written < body_lines_per_page {
1754            output.write_all(b"\n")?;
1755            body_lines_written += 1;
1756        }
1757    }
1758
1759    Ok(())
1760}