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 plain spaces for column padding by default
263    let n = target_abs_pos.saturating_sub(abs_pos);
264    write_spaces(output, n)
265}
266
267/// Paginate raw byte data — fast path that avoids per-line String allocation.
268/// When no tab expansion or control char processing is needed, lines are
269/// extracted as byte slices directly from the input buffer (zero-copy).
270pub fn pr_data<W: Write>(
271    data: &[u8],
272    output: &mut W,
273    config: &PrConfig,
274    filename: &str,
275    file_date: Option<SystemTime>,
276) -> io::Result<()> {
277    let needs_transform =
278        config.expand_tabs.is_some() || config.show_control_chars || config.show_nonprinting;
279
280    if needs_transform {
281        // Fall back to the String-based path for transforms
282        let reader = io::Cursor::new(data);
283        return pr_file(reader, output, config, filename, file_date);
284    }
285
286    // Ultra-fast path: single column, no per-line transforms → contiguous chunk writes
287    // Instead of splitting into individual lines and writing each one, we index newline
288    // positions and write entire page bodies as contiguous slices of the original data.
289    let is_simple = config.columns <= 1
290        && config.number_lines.is_none()
291        && config.indent == 0
292        && !config.truncate_lines
293        && !config.double_space
294        && !config.across
295        && memchr::memchr(b'\r', data).is_none()
296        && memchr::memchr(b'\x0c', data).is_none();
297
298    if is_simple {
299        // Passthrough: -T (omit_pagination) with no transforms and no page range → output == input.
300        // -t (omit_header) alone does NOT qualify because page bodies are still separated
301        // by blank lines; only -T eliminates all inter-page structure.
302        if config.omit_pagination && config.first_page == 1 && config.last_page == 0 {
303            return output.write_all(data);
304        }
305        return pr_data_contiguous(data, output, config, filename, file_date);
306    }
307
308    // Fast path: single column with numbering only (no indent, no truncate, no double-space)
309    if config.columns <= 1
310        && config.number_lines.is_some()
311        && config.indent == 0
312        && !config.truncate_lines
313        && !config.double_space
314        && memchr::memchr(b'\r', data).is_none()
315        && memchr::memchr(b'\x0c', data).is_none()
316    {
317        return pr_data_numbered(data, output, config, filename, file_date);
318    }
319
320    // Normal path: split into line byte slices using SIMD memchr
321    let mut lines: Vec<&[u8]> = Vec::with_capacity(data.len() / 40 + 64);
322    let mut start = 0;
323    for pos in memchr::memchr_iter(b'\n', data) {
324        let end = if pos > start && data[pos - 1] == b'\r' {
325            pos - 1
326        } else {
327            pos
328        };
329        lines.push(&data[start..end]);
330        start = pos + 1;
331    }
332    // Handle last line without trailing newline
333    if start < data.len() {
334        let end = if data.last() == Some(&b'\r') {
335            data.len() - 1
336        } else {
337            data.len()
338        };
339        lines.push(&data[start..end]);
340    }
341
342    pr_lines_generic(&lines, output, config, filename, file_date)
343}
344
345/// Ultra-fast contiguous-write paginator for single-column, no-transform mode.
346/// Streams through data using memchr_iter without building a Vec<usize> of newline positions.
347/// Pre-computes the header prefix (date + filename) once, appending only the page number per page.
348fn pr_data_contiguous<W: Write>(
349    data: &[u8],
350    output: &mut W,
351    config: &PrConfig,
352    filename: &str,
353    file_date: Option<SystemTime>,
354) -> io::Result<()> {
355    let date = file_date.unwrap_or_else(SystemTime::now);
356    let header_str = config.header.as_deref().unwrap_or(filename);
357    let date_str = format_header_date(&date, &config.date_format);
358
359    let suppress_header = !config.omit_header
360        && !config.omit_pagination
361        && config.page_length <= HEADER_LINES + FOOTER_LINES;
362    let body_lines_per_page = if config.omit_header || config.omit_pagination {
363        if config.page_length > 0 {
364            config.page_length
365        } else {
366            DEFAULT_PAGE_LENGTH
367        }
368    } else if suppress_header {
369        config.page_length
370    } else {
371        config.page_length - HEADER_LINES - FOOTER_LINES
372    };
373    let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
374
375    if data.is_empty() {
376        if show_header {
377            let mut page_buf: Vec<u8> = Vec::with_capacity(256);
378            write_header(&mut page_buf, &date_str, header_str, 1, config)?;
379            write_footer(&mut page_buf, config)?;
380            output.write_all(&page_buf)?;
381        }
382        return Ok(());
383    }
384
385    let footer: &[u8] = if show_header {
386        if config.form_feed {
387            b"\x0c"
388        } else {
389            b"\n\n\n\n\n"
390        }
391    } else {
392        b""
393    };
394
395    // Stream through data: skip body_lines_per_page newlines at a time
396    let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
397    let mut page_num = 1usize;
398    let mut byte_pos = 0usize;
399    loop {
400        if byte_pos >= data.len() {
401            break;
402        }
403
404        // Find the end of this page: skip body_lines_per_page newlines
405        let page_start = byte_pos;
406        let mut lines_found = 0usize;
407        let remaining = &data[byte_pos..];
408        let mut page_end = data.len();
409
410        for nl_off in memchr::memchr_iter(b'\n', remaining) {
411            lines_found += 1;
412            if lines_found >= body_lines_per_page {
413                page_end = byte_pos + nl_off + 1;
414                break;
415            }
416        }
417
418        let in_range = page_num >= config.first_page
419            && (config.last_page == 0 || page_num <= config.last_page);
420
421        if in_range {
422            page_buf.clear();
423
424            if show_header {
425                write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
426            }
427
428            // Write body: contiguous slice of original data
429            page_buf.extend_from_slice(&data[page_start..page_end]);
430
431            // Ensure last line ends with newline
432            if page_buf.last() != Some(&b'\n') {
433                page_buf.push(b'\n');
434            }
435
436            // Pad remaining body lines
437            if show_header || (!config.omit_header && !config.omit_pagination) {
438                let pad_lines = body_lines_per_page.saturating_sub(lines_found);
439                page_buf.resize(page_buf.len() + pad_lines, b'\n');
440            }
441
442            page_buf.extend_from_slice(footer);
443
444            output.write_all(&page_buf)?;
445        }
446
447        byte_pos = page_end;
448        page_num += 1;
449
450        // If we didn't find enough lines, we've consumed all data
451        if lines_found < body_lines_per_page {
452            break;
453        }
454    }
455
456    Ok(())
457}
458
459/// Fast numbered single-column paginator.
460/// Uses unsafe pointer arithmetic to format numbered lines directly into
461/// a pre-allocated buffer, avoiding per-line write_all overhead.
462fn pr_data_numbered<W: Write>(
463    data: &[u8],
464    output: &mut W,
465    config: &PrConfig,
466    filename: &str,
467    file_date: Option<SystemTime>,
468) -> io::Result<()> {
469    let date = file_date.unwrap_or_else(SystemTime::now);
470    let header_str = config.header.as_deref().unwrap_or(filename);
471    let date_str = format_header_date(&date, &config.date_format);
472
473    let (sep_char, digits) = config.number_lines.unwrap_or(('\t', 5));
474    debug_assert!(sep_char.is_ascii(), "number separator must be ASCII");
475    let sep_byte = sep_char as u8;
476    let suppress_header = !config.omit_header
477        && !config.omit_pagination
478        && config.page_length <= HEADER_LINES + FOOTER_LINES;
479    let body_lines_per_page = if config.omit_header || config.omit_pagination {
480        if config.page_length > 0 {
481            config.page_length
482        } else {
483            DEFAULT_PAGE_LENGTH
484        }
485    } else if suppress_header {
486        config.page_length
487    } else {
488        config.page_length - HEADER_LINES - FOOTER_LINES
489    };
490    let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
491
492    // Pre-allocate output buffer: ~128KB for a page
493    const BUF_SIZE: usize = 128 * 1024;
494    let mut page_buf: Vec<u8> = Vec::with_capacity(BUF_SIZE + 4096);
495
496    let mut line_number = config.first_line_number;
497    let mut page_num = 1usize;
498
499    // Pre-split lines using SIMD memchr for fast iteration
500    let mut line_starts: Vec<usize> = Vec::with_capacity(data.len() / 40 + 64);
501    line_starts.push(0);
502    for pos in memchr::memchr_iter(b'\n', data) {
503        line_starts.push(pos + 1);
504    }
505    let total_lines = if !data.is_empty() && data[data.len() - 1] == b'\n' {
506        line_starts.len() - 1
507    } else {
508        line_starts.len()
509    };
510
511    let mut line_idx = 0;
512
513    while line_idx < total_lines {
514        let page_end = (line_idx + body_lines_per_page).min(total_lines);
515        let in_range = page_num >= config.first_page
516            && (config.last_page == 0 || page_num <= config.last_page);
517
518        if in_range {
519            page_buf.clear();
520
521            if show_header {
522                write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
523            }
524
525            // Write numbered lines using unsafe pointer arithmetic.
526            // SAFETY: `src` points into `data` which is a &[u8] borrowed for the
527            // function's lifetime. `page_buf` may reallocate but `src` is independent.
528            let src = data.as_ptr();
529            for li in line_idx..page_end {
530                let line_start = line_starts[li];
531                let line_end = if li + 1 < line_starts.len() {
532                    // strip trailing \n (and \r\n)
533                    let end = line_starts[li + 1] - 1;
534                    if end > line_start && data[end - 1] == b'\r' {
535                        end - 1
536                    } else {
537                        end
538                    }
539                } else {
540                    data.len()
541                };
542                let line_len = line_end - line_start;
543
544                let wp = page_buf.len();
545
546                // Format line number with right-aligned padding
547                let mut n = line_number;
548                let mut num_pos = 19usize;
549                let mut num_tmp = [0u8; 20];
550                loop {
551                    num_tmp[num_pos] = b'0' + (n % 10) as u8;
552                    n /= 10;
553                    if n == 0 || num_pos == 0 {
554                        break;
555                    }
556                    num_pos -= 1;
557                }
558                let num_digits = 20 - num_pos;
559                let padding = digits.saturating_sub(num_digits);
560                // Actual prefix width: when number overflows configured width,
561                // use num_digits instead of digits to avoid buffer overwrite
562                let actual_prefix = padding + num_digits + 1; // padding + digits + separator
563
564                // Ensure capacity with actual prefix size
565                let needed = actual_prefix + line_len + 1;
566                if page_buf.len() + needed > page_buf.capacity() {
567                    page_buf.reserve(needed);
568                }
569                let base = page_buf.as_mut_ptr();
570
571                unsafe {
572                    let dst = base.add(wp);
573                    // Write padding spaces
574                    std::ptr::write_bytes(dst, b' ', padding);
575                    // Write number digits
576                    std::ptr::copy_nonoverlapping(
577                        num_tmp.as_ptr().add(num_pos),
578                        dst.add(padding),
579                        num_digits,
580                    );
581                    // Write separator
582                    *dst.add(padding + num_digits) = sep_byte;
583                    // Write line content
584                    if line_len > 0 {
585                        std::ptr::copy_nonoverlapping(
586                            src.add(line_start),
587                            dst.add(actual_prefix),
588                            line_len,
589                        );
590                    }
591                    // Write newline
592                    *dst.add(actual_prefix + line_len) = b'\n';
593                    page_buf.set_len(wp + actual_prefix + line_len + 1);
594                }
595
596                line_number += 1;
597            }
598
599            // Pad remaining body lines
600            if show_header {
601                let body_lines_written = page_end - line_idx;
602                let pad = body_lines_per_page.saturating_sub(body_lines_written);
603                page_buf.resize(page_buf.len() + pad, b'\n');
604            }
605
606            // Footer
607            if show_header {
608                write_footer(&mut page_buf, config)?;
609            }
610
611            output.write_all(&page_buf)?;
612        } else {
613            // Skip page but still advance line number
614            line_number += page_end - line_idx;
615        }
616
617        line_idx = page_end;
618        page_num += 1;
619    }
620
621    Ok(())
622}
623
624/// Paginate a single file and write output.
625pub fn pr_file<R: BufRead, W: Write>(
626    input: R,
627    output: &mut W,
628    config: &PrConfig,
629    filename: &str,
630    file_date: Option<SystemTime>,
631) -> io::Result<()> {
632    // Read all lines with transforms applied
633    let mut all_lines: Vec<String> = Vec::new();
634    for line_result in input.lines() {
635        let line = line_result?;
636        let mut line = line;
637
638        // Expand tabs if requested
639        if let Some((tab_char, tab_width)) = config.expand_tabs {
640            line = expand_tabs_in_line(&line, tab_char, tab_width);
641        }
642
643        // Process control characters (skip when not needed to avoid copying)
644        if config.show_control_chars || config.show_nonprinting {
645            line = process_control_chars(&line, config.show_control_chars, config.show_nonprinting);
646        }
647
648        all_lines.push(line);
649    }
650
651    // Convert to &[u8] slices for the byte-based paginator
652    let refs: Vec<&[u8]> = all_lines.iter().map(|s| s.as_bytes()).collect();
653    pr_lines_generic(&refs, output, config, filename, file_date)
654}
655
656/// Core paginator that works on a slice of byte slices (zero-copy).
657fn pr_lines_generic<W: Write>(
658    all_lines: &[&[u8]],
659    output: &mut W,
660    config: &PrConfig,
661    filename: &str,
662    file_date: Option<SystemTime>,
663) -> io::Result<()> {
664    let date = file_date.unwrap_or_else(SystemTime::now);
665
666    let header_str = config.header.as_deref().unwrap_or(filename);
667    let date_str = format_header_date(&date, &config.date_format);
668
669    // Calculate body lines per page
670    // When page_length is too small for header+footer, GNU pr suppresses
671    // headers/footers and uses page_length as the body size.
672    let suppress_header = !config.omit_header
673        && !config.omit_pagination
674        && config.page_length <= HEADER_LINES + FOOTER_LINES;
675    // When suppress_header is active, create a config view with omit_header set
676    // so that sub-functions skip padding to body_lines_per_page.
677    let suppressed_config;
678    let effective_config = if suppress_header {
679        suppressed_config = PrConfig {
680            omit_header: true,
681            ..config.clone()
682        };
683        &suppressed_config
684    } else {
685        config
686    };
687    let body_lines_per_page = if config.omit_header || config.omit_pagination {
688        if config.page_length > 0 {
689            config.page_length
690        } else {
691            DEFAULT_PAGE_LENGTH
692        }
693    } else if suppress_header {
694        config.page_length
695    } else {
696        config.page_length - HEADER_LINES - FOOTER_LINES
697    };
698
699    // Account for double spacing: each input line takes 2 output lines
700    let input_lines_per_page = if config.double_space {
701        (body_lines_per_page + 1) / 2
702    } else {
703        body_lines_per_page
704    };
705
706    // Handle multi-column mode
707    let columns = config.columns.max(1);
708
709    // GNU pr in multi-column down mode: each page has body_lines_per_page rows,
710    // each row shows one value from each column. So up to
711    // input_lines_per_page * columns input lines can be consumed per page.
712    // actual_lines_per_column = ceil(page_lines / columns) for each page.
713    let lines_consumed_per_page = if columns > 1 && !config.across {
714        input_lines_per_page * columns
715    } else {
716        input_lines_per_page
717    };
718
719    // Split into pages
720    let total_lines = all_lines.len();
721    let mut line_number = config.first_line_number;
722    let mut page_num = 1usize;
723    let mut line_idx = 0;
724    // Page-level output buffer: batch many small writes into one large write_all
725    let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
726
727    while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
728        // For empty input, output one empty page (matching GNU behavior)
729        if total_lines == 0 && line_idx == 0 {
730            if page_num >= config.first_page
731                && (config.last_page == 0 || page_num <= config.last_page)
732            {
733                if !config.omit_header && !config.omit_pagination && !suppress_header {
734                    write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
735                    write_footer(&mut page_buf, config)?;
736                    output.write_all(&page_buf)?;
737                }
738            }
739            break;
740        }
741
742        let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
743
744        if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
745        {
746            page_buf.clear();
747
748            // Write header to page buffer
749            if !config.omit_header && !config.omit_pagination && !suppress_header {
750                write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
751            }
752
753            // Write body to page buffer
754            if columns > 1 {
755                write_multicolumn_body(
756                    &mut page_buf,
757                    &all_lines[line_idx..page_end],
758                    effective_config,
759                    columns,
760                    &mut line_number,
761                    body_lines_per_page,
762                )?;
763            } else {
764                write_single_column_body(
765                    &mut page_buf,
766                    &all_lines[line_idx..page_end],
767                    effective_config,
768                    &mut line_number,
769                    body_lines_per_page,
770                )?;
771            }
772
773            // Write footer to page buffer
774            if !config.omit_header && !config.omit_pagination && !suppress_header {
775                write_footer(&mut page_buf, config)?;
776            }
777
778            // Flush entire page to output in one call
779            output.write_all(&page_buf)?;
780        }
781
782        line_idx = page_end;
783        page_num += 1;
784
785        // Break if we've consumed all lines
786        if line_idx >= total_lines {
787            break;
788        }
789    }
790
791    Ok(())
792}
793
794/// Paginate multiple files merged side by side (-m mode).
795pub fn pr_merge<W: Write>(
796    inputs: &[Vec<String>],
797    output: &mut W,
798    config: &PrConfig,
799    _filenames: &[&str],
800    file_dates: &[SystemTime],
801) -> io::Result<()> {
802    let date = file_dates.first().copied().unwrap_or_else(SystemTime::now);
803    let date_str = format_header_date(&date, &config.date_format);
804    let header_str = config.header.as_deref().unwrap_or("");
805
806    let suppress_header = !config.omit_header
807        && !config.omit_pagination
808        && config.page_length <= HEADER_LINES + FOOTER_LINES;
809    let body_lines_per_page = if config.omit_header || config.omit_pagination {
810        if config.page_length > 0 {
811            config.page_length
812        } else {
813            DEFAULT_PAGE_LENGTH
814        }
815    } else if suppress_header {
816        config.page_length
817    } else {
818        config.page_length - HEADER_LINES - FOOTER_LINES
819    };
820
821    let input_lines_per_page = if config.double_space {
822        (body_lines_per_page + 1) / 2
823    } else {
824        body_lines_per_page
825    };
826
827    let num_files = inputs.len();
828    let explicit_sep = has_explicit_separator(config);
829    let col_sep = get_column_separator(config);
830    let col_width = if explicit_sep {
831        if num_files > 1 {
832            (config
833                .page_width
834                .saturating_sub(col_sep.len() * (num_files - 1)))
835                / num_files
836        } else {
837            config.page_width
838        }
839    } else {
840        config.page_width / num_files
841    };
842
843    let max_lines = inputs.iter().map(|f| f.len()).max().unwrap_or(0);
844    let mut page_num = 1usize;
845    let mut line_idx = 0;
846    let mut line_number = config.first_line_number;
847
848    let col_sep_bytes = col_sep.as_bytes();
849    let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
850    let mut num_buf = [0u8; 32];
851
852    while line_idx < max_lines {
853        let page_end = (line_idx + input_lines_per_page).min(max_lines);
854
855        if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
856        {
857            page_buf.clear();
858
859            if !config.omit_header && !config.omit_pagination && !suppress_header {
860                write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
861            }
862
863            let indent_str = " ".repeat(config.indent);
864            let mut body_lines_written = 0;
865            for i in line_idx..page_end {
866                if config.double_space && body_lines_written > 0 {
867                    page_buf.push(b'\n');
868                    body_lines_written += 1;
869                }
870
871                page_buf.extend_from_slice(indent_str.as_bytes());
872                let mut abs_pos = config.indent;
873
874                if let Some((sep, digits)) = config.number_lines {
875                    let num_str = format_line_number(line_number, sep, digits, &mut num_buf);
876                    page_buf.extend_from_slice(num_str);
877                    abs_pos += digits + 1;
878                    line_number += 1;
879                }
880
881                for (fi, file_lines) in inputs.iter().enumerate() {
882                    let content = if i < file_lines.len() {
883                        file_lines[i].as_bytes()
884                    } else {
885                        b"" as &[u8]
886                    };
887                    let truncated = if !explicit_sep && content.len() > col_width.saturating_sub(1)
888                    {
889                        &content[..col_width.saturating_sub(1)]
890                    } else if explicit_sep && config.truncate_lines && content.len() > col_width {
891                        &content[..col_width]
892                    } else {
893                        content
894                    };
895                    if fi < num_files - 1 {
896                        if explicit_sep {
897                            if fi > 0 {
898                                page_buf.extend_from_slice(col_sep_bytes);
899                            }
900                            page_buf.extend_from_slice(truncated);
901                            abs_pos +=
902                                truncated.len() + if fi > 0 { col_sep_bytes.len() } else { 0 };
903                        } else {
904                            page_buf.extend_from_slice(truncated);
905                            abs_pos += truncated.len();
906                            let target = (fi + 1) * col_width + config.indent;
907                            write_column_padding(&mut page_buf, abs_pos, target)?;
908                            abs_pos = target;
909                        }
910                    } else {
911                        if explicit_sep && fi > 0 {
912                            page_buf.extend_from_slice(col_sep_bytes);
913                        }
914                        page_buf.extend_from_slice(truncated);
915                    }
916                }
917                page_buf.push(b'\n');
918                body_lines_written += 1;
919            }
920
921            // Pad remaining body lines
922            while body_lines_written < body_lines_per_page {
923                page_buf.push(b'\n');
924                body_lines_written += 1;
925            }
926
927            if !config.omit_header && !config.omit_pagination && !suppress_header {
928                write_footer(&mut page_buf, config)?;
929            }
930
931            output.write_all(&page_buf)?;
932        }
933
934        line_idx = page_end;
935        page_num += 1;
936    }
937
938    Ok(())
939}
940
941/// Write page header: 2 blank lines, date/header/page line, 2 blank lines.
942fn write_header<W: Write>(
943    output: &mut W,
944    date_str: &str,
945    header: &str,
946    page_num: usize,
947    config: &PrConfig,
948) -> io::Result<()> {
949    // 2 blank lines
950    output.write_all(b"\n\n")?;
951
952    // Header line: date is left-aligned, header is centered, Page N is right-aligned.
953    let line_width = config.page_width;
954
955    let left = date_str;
956    let center = header;
957    let left_len = left.len();
958    let center_len = center.len();
959
960    // Format "Page N" without allocation for small page numbers
961    let mut page_buf = [0u8; 32];
962    let page_str = format_page_number(page_num, &mut page_buf);
963    let right_len = page_str.len();
964
965    // GNU pr centers the header title within the line.
966    if left_len + center_len + right_len + 2 >= line_width {
967        output.write_all(left.as_bytes())?;
968        output.write_all(b" ")?;
969        output.write_all(center.as_bytes())?;
970        output.write_all(b" ")?;
971        output.write_all(page_str)?;
972        output.write_all(b"\n")?;
973    } else {
974        let total_spaces = line_width - left_len - center_len - right_len;
975        let left_spaces = total_spaces / 2;
976        let right_spaces = total_spaces - left_spaces;
977        output.write_all(left.as_bytes())?;
978        write_spaces(output, left_spaces)?;
979        output.write_all(center.as_bytes())?;
980        write_spaces(output, right_spaces)?;
981        output.write_all(page_str)?;
982        output.write_all(b"\n")?;
983    }
984
985    // 2 blank lines
986    output.write_all(b"\n\n")?;
987
988    Ok(())
989}
990
991/// Format "Page N" into a stack buffer, returning the used slice.
992#[inline]
993fn format_page_number(page_num: usize, buf: &mut [u8; 32]) -> &[u8] {
994    const PREFIX: &[u8] = b"Page ";
995    let prefix_len = PREFIX.len();
996    buf[..prefix_len].copy_from_slice(PREFIX);
997    // Format number into a separate stack buffer to avoid overlapping borrow
998    let mut num_buf = [0u8; 20];
999    let mut n = page_num;
1000    let mut pos = 19;
1001    loop {
1002        num_buf[pos] = b'0' + (n % 10) as u8;
1003        n /= 10;
1004        if n == 0 {
1005            break;
1006        }
1007        pos -= 1;
1008    }
1009    let num_len = 20 - pos;
1010    buf[prefix_len..prefix_len + num_len].copy_from_slice(&num_buf[pos..20]);
1011    &buf[..prefix_len + num_len]
1012}
1013
1014/// Write page footer: 5 blank lines (or form feed).
1015fn write_footer<W: Write>(output: &mut W, config: &PrConfig) -> io::Result<()> {
1016    if config.form_feed {
1017        output.write_all(b"\x0c")?;
1018    } else {
1019        output.write_all(b"\n\n\n\n\n")?;
1020    }
1021    Ok(())
1022}
1023
1024/// Write body for single column mode.
1025fn write_single_column_body<W: Write>(
1026    output: &mut W,
1027    lines: &[&[u8]],
1028    config: &PrConfig,
1029    line_number: &mut usize,
1030    body_lines_per_page: usize,
1031) -> io::Result<()> {
1032    let indent_str = " ".repeat(config.indent);
1033    let content_width = if config.truncate_lines {
1034        compute_content_width(config)
1035    } else {
1036        0
1037    };
1038    let mut body_lines_written = 0;
1039    // Pre-allocate line number buffer to avoid per-line write! formatting
1040    let mut num_buf = [0u8; 32];
1041
1042    for line in lines.iter() {
1043        output.write_all(indent_str.as_bytes())?;
1044
1045        if let Some((sep, digits)) = config.number_lines {
1046            // Format line number directly into buffer, avoiding write! overhead
1047            let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1048            output.write_all(num_str)?;
1049            *line_number += 1;
1050        }
1051
1052        let content: &[u8] = if config.truncate_lines {
1053            if line.len() > content_width {
1054                &line[..content_width]
1055            } else {
1056                line
1057            }
1058        } else {
1059            line
1060        };
1061
1062        // Direct write_all of byte slice — no format dispatch or UTF-8 overhead
1063        output.write_all(content)?;
1064        output.write_all(b"\n")?;
1065        body_lines_written += 1;
1066        if body_lines_written >= body_lines_per_page {
1067            break;
1068        }
1069
1070        // Double-space: write blank line AFTER each content line
1071        if config.double_space {
1072            output.write_all(b"\n")?;
1073            body_lines_written += 1;
1074            if body_lines_written >= body_lines_per_page {
1075                break;
1076            }
1077        }
1078    }
1079
1080    // Pad remaining body lines if not omitting headers
1081    if !config.omit_header && !config.omit_pagination {
1082        while body_lines_written < body_lines_per_page {
1083            output.write_all(b"\n")?;
1084            body_lines_written += 1;
1085        }
1086    }
1087
1088    Ok(())
1089}
1090
1091/// Format a line number with right-aligned padding and separator into a stack buffer.
1092/// Returns the formatted slice. Avoids write!() per-line overhead.
1093#[inline]
1094fn format_line_number(num: usize, sep: char, digits: usize, buf: &mut [u8; 32]) -> &[u8] {
1095    // Format the number
1096    let mut n = num;
1097    let mut pos = 31;
1098    loop {
1099        buf[pos] = b'0' + (n % 10) as u8;
1100        n /= 10;
1101        if n == 0 || pos == 0 {
1102            break;
1103        }
1104        pos -= 1;
1105    }
1106    let num_digits = 32 - pos;
1107    // Build the output: spaces for padding + number + separator
1108    let padding = if digits > num_digits {
1109        digits - num_digits
1110    } else {
1111        0
1112    };
1113    let total_len = padding + num_digits + sep.len_utf8();
1114    // We need a separate output buffer since we're using buf for the number
1115    // Just use the write_all approach with two calls for simplicity
1116    let start = 32 - num_digits;
1117    // Return just the number portion; caller handles padding via spaces
1118    // Actually, let's format properly into a contiguous buffer
1119    let sep_byte = sep as u8; // ASCII separator assumed
1120    let out_start = 32usize.saturating_sub(total_len);
1121    // Fill padding
1122    for i in out_start..out_start + padding {
1123        buf[i] = b' ';
1124    }
1125    // Number is already at positions [start..32], shift if needed
1126    if out_start + padding != start {
1127        let src = start;
1128        let dst = out_start + padding;
1129        for i in 0..num_digits {
1130            buf[dst + i] = buf[src + i];
1131        }
1132    }
1133    // Add separator
1134    buf[out_start + padding + num_digits] = sep_byte;
1135    &buf[out_start..out_start + total_len]
1136}
1137
1138/// Compute available content width after accounting for numbering and indent.
1139fn compute_content_width(config: &PrConfig) -> usize {
1140    let mut w = config.page_width;
1141    w = w.saturating_sub(config.indent);
1142    if let Some((_, digits)) = config.number_lines {
1143        w = w.saturating_sub(digits + 1); // digits + separator
1144    }
1145    w
1146}
1147
1148/// Write body for multi-column mode.
1149fn write_multicolumn_body<W: Write>(
1150    output: &mut W,
1151    lines: &[&[u8]],
1152    config: &PrConfig,
1153    columns: usize,
1154    line_number: &mut usize,
1155    body_lines_per_page: usize,
1156) -> io::Result<()> {
1157    let explicit_sep = has_explicit_separator(config);
1158    let col_sep = get_column_separator(config);
1159    // When no explicit separator, GNU pr uses the full page_width / columns as column width
1160    // and pads with tabs. When separator is explicit, use sep width in calculation.
1161    let col_width = if explicit_sep {
1162        if columns > 1 {
1163            (config
1164                .page_width
1165                .saturating_sub(col_sep.len() * (columns - 1)))
1166                / columns
1167        } else {
1168            config.page_width
1169        }
1170    } else {
1171        config.page_width / columns
1172    };
1173    // GNU pr truncates lines in multi-column mode by default, unless -J (join_lines) is set.
1174    // For non-explicit separator, truncate to col_width - 1 to leave room for padding.
1175    let do_truncate = !config.join_lines;
1176    let content_width = if explicit_sep {
1177        col_width
1178    } else {
1179        col_width.saturating_sub(1)
1180    };
1181
1182    let indent_str = " ".repeat(config.indent);
1183    let col_sep_bytes = col_sep.as_bytes();
1184    let mut body_lines_written = 0;
1185    let mut num_buf = [0u8; 32];
1186
1187    if config.across {
1188        // Print columns across: line 0 fills col0, line 1 fills col1, etc.
1189        let mut i = 0;
1190        while i < lines.len() {
1191            if config.double_space && body_lines_written > 0 {
1192                output.write_all(b"\n")?;
1193                body_lines_written += 1;
1194                if body_lines_written >= body_lines_per_page {
1195                    break;
1196                }
1197            }
1198
1199            output.write_all(indent_str.as_bytes())?;
1200            let mut abs_pos = config.indent;
1201
1202            // Find the last column with data on this row
1203            let mut last_data_col = 0;
1204            for col in 0..columns {
1205                let li = i + col;
1206                if li < lines.len() {
1207                    last_data_col = col;
1208                }
1209            }
1210
1211            for col in 0..columns {
1212                let li = i + col;
1213                if li < lines.len() {
1214                    if explicit_sep && col > 0 {
1215                        output.write_all(col_sep_bytes)?;
1216                        abs_pos += col_sep_bytes.len();
1217                    }
1218                    if let Some((sep, digits)) = config.number_lines {
1219                        let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1220                        output.write_all(num_str)?;
1221                        abs_pos += digits + 1;
1222                        *line_number += 1;
1223                    }
1224                    let content: &[u8] = lines[li];
1225                    let mut truncated = if do_truncate && content.len() > content_width {
1226                        &content[..content_width]
1227                    } else {
1228                        content
1229                    };
1230                    // GNU pr strips trailing spaces from the last column
1231                    if col == last_data_col && !explicit_sep {
1232                        while truncated.last() == Some(&b' ') {
1233                            truncated = &truncated[..truncated.len() - 1];
1234                        }
1235                    }
1236                    output.write_all(truncated)?;
1237                    abs_pos += truncated.len();
1238                    if col < last_data_col && !explicit_sep {
1239                        let target = (col + 1) * col_width + config.indent;
1240                        write_column_padding(output, abs_pos, target)?;
1241                        abs_pos = target;
1242                    }
1243                }
1244            }
1245            output.write_all(b"\n")?;
1246            body_lines_written += 1;
1247            i += columns;
1248        }
1249    } else {
1250        // Print columns down: distribute lines across columns.
1251        // GNU pr distributes evenly: base = lines/cols, extra = lines%cols.
1252        // First 'extra' columns get base+1 lines, rest get base lines.
1253        let n = lines.len();
1254        let base = n / columns;
1255        let extra = n % columns;
1256
1257        // Compute start offset of each column
1258        let mut col_starts = vec![0usize; columns + 1];
1259        for col in 0..columns {
1260            let col_lines = base + if col < extra { 1 } else { 0 };
1261            col_starts[col + 1] = col_starts[col] + col_lines;
1262        }
1263
1264        // Number of rows = max lines in any column
1265        let num_rows = if extra > 0 { base + 1 } else { base };
1266
1267        for row in 0..num_rows {
1268            if config.double_space && row > 0 {
1269                output.write_all(b"\n")?;
1270                body_lines_written += 1;
1271                if body_lines_written >= body_lines_per_page {
1272                    break;
1273                }
1274            }
1275
1276            output.write_all(indent_str.as_bytes())?;
1277            let mut abs_pos = config.indent;
1278
1279            // Find the last column with data for this row
1280            let mut last_data_col = 0;
1281            for col in 0..columns {
1282                let col_lines = col_starts[col + 1] - col_starts[col];
1283                if row < col_lines {
1284                    last_data_col = col;
1285                }
1286            }
1287
1288            for col in 0..columns {
1289                let col_lines = col_starts[col + 1] - col_starts[col];
1290                let li = col_starts[col] + row;
1291                if row < col_lines {
1292                    if explicit_sep && col > 0 {
1293                        output.write_all(col_sep_bytes)?;
1294                        abs_pos += col_sep_bytes.len();
1295                    }
1296                    if let Some((sep, digits)) = config.number_lines {
1297                        let num = config.first_line_number + li;
1298                        let num_str = format_line_number(num, sep, digits, &mut num_buf);
1299                        output.write_all(num_str)?;
1300                        abs_pos += digits + 1;
1301                    }
1302                    let content: &[u8] = lines[li];
1303                    let mut truncated = if do_truncate && content.len() > content_width {
1304                        &content[..content_width]
1305                    } else {
1306                        content
1307                    };
1308                    // GNU pr strips trailing spaces from the last column
1309                    if col == last_data_col && !explicit_sep {
1310                        while truncated.last() == Some(&b' ') {
1311                            truncated = &truncated[..truncated.len() - 1];
1312                        }
1313                    }
1314                    output.write_all(truncated)?;
1315                    abs_pos += truncated.len();
1316                    if col < last_data_col && !explicit_sep {
1317                        // Not the last column with data: pad to next column boundary
1318                        let target = (col + 1) * col_width + config.indent;
1319                        write_column_padding(output, abs_pos, target)?;
1320                        abs_pos = target;
1321                    }
1322                } else if col <= last_data_col {
1323                    // Empty column before the last data column: pad to next boundary
1324                    if explicit_sep {
1325                        if col > 0 {
1326                            output.write_all(col_sep_bytes)?;
1327                            abs_pos += col_sep_bytes.len();
1328                        }
1329                        // For explicit separator, just write separator, no padding
1330                    } else {
1331                        let target = (col + 1) * col_width + config.indent;
1332                        write_column_padding(output, abs_pos, target)?;
1333                        abs_pos = target;
1334                    }
1335                }
1336                // Empty columns after last data column: skip entirely
1337            }
1338            output.write_all(b"\n")?;
1339            body_lines_written += 1;
1340        }
1341        // Update line_number for the lines we processed
1342        if config.number_lines.is_some() {
1343            *line_number += lines.len();
1344        }
1345    }
1346
1347    // Pad remaining body lines
1348    if !config.omit_header && !config.omit_pagination {
1349        while body_lines_written < body_lines_per_page {
1350            output.write_all(b"\n")?;
1351            body_lines_written += 1;
1352        }
1353    }
1354
1355    Ok(())
1356}