1use std::io::{self, BufRead, IoSlice, Write};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4pub const DEFAULT_PAGE_LENGTH: usize = 66;
6pub const DEFAULT_PAGE_WIDTH: usize = 72;
8pub const HEADER_LINES: usize = 5;
10pub const FOOTER_LINES: usize = 5;
12
13#[derive(Clone)]
15pub struct PrConfig {
16 pub first_page: usize,
18 pub last_page: usize,
20 pub columns: usize,
22 pub across: bool,
24 pub show_control_chars: bool,
26 pub double_space: bool,
28 pub date_format: String,
30 pub expand_tabs: Option<(char, usize)>,
32 pub form_feed: bool,
34 pub header: Option<String>,
36 pub output_tabs: Option<(char, usize)>,
38 pub join_lines: bool,
40 pub page_length: usize,
42 pub merge: bool,
44 pub number_lines: Option<(char, usize)>,
46 pub first_line_number: usize,
48 pub indent: usize,
50 pub no_file_warnings: bool,
52 pub separator: Option<char>,
54 pub sep_string: Option<String>,
56 pub omit_header: bool,
58 pub omit_pagination: bool,
60 pub show_nonprinting: bool,
62 pub page_width: usize,
64 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
100fn 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 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
129fn 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 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 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 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 if seg_start < bytes.len() {
163 result.push_str(&line[seg_start..]);
164 }
165 result
166}
167
168#[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#[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
204fn 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
222fn 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
233fn has_explicit_separator(config: &PrConfig) -> bool {
235 config.sep_string.is_some() || config.separator.is_some()
236}
237
238const SPACES: [u8; 256] = [b' '; 256];
244
245#[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 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 return write_spaces(output, n);
273 }
274 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
290pub 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 let reader = io::Cursor::new(data);
306 return pr_file(reader, output, config, filename, file_date);
307 }
308
309 let is_simple = config.columns <= 1
313 && config.number_lines.is_none()
314 && config.indent == 0
315 && !config.truncate_lines
316 && !config.double_space
317 && !config.across
318 && memchr::memchr(b'\r', data).is_none()
319 && memchr::memchr(b'\x0c', data).is_none();
320
321 if is_simple {
322 if config.omit_pagination && config.first_page == 1 && config.last_page == 0 {
326 return output.write_all(data);
327 }
328 return pr_data_contiguous(data, output, config, filename, file_date);
329 }
330
331 if config.columns <= 1
333 && config.number_lines.is_some()
334 && config.indent == 0
335 && !config.truncate_lines
336 && !config.double_space
337 && memchr::memchr(b'\r', data).is_none()
338 && memchr::memchr(b'\x0c', data).is_none()
339 {
340 return pr_data_numbered(data, output, config, filename, file_date);
341 }
342
343 if config.columns > 1
346 && config.columns <= 32
347 && !config.across
348 && config.number_lines.is_none()
349 && config.indent == 0
350 && !config.double_space
351 && !config.join_lines
352 && data.len() <= u32::MAX as usize
353 && memchr::memchr(b'\r', data).is_none()
354 && memchr::memchr(b'\x0c', data).is_none()
355 {
356 return pr_data_multicolumn_fast(data, output, config, filename, file_date);
357 }
358
359 let mut lines: Vec<&[u8]> = Vec::with_capacity(data.len() / 40 + 64);
361 let mut start = 0;
362 for pos in memchr::memchr_iter(b'\n', data) {
363 let end = if pos > start && data[pos - 1] == b'\r' {
364 pos - 1
365 } else {
366 pos
367 };
368 lines.push(&data[start..end]);
369 start = pos + 1;
370 }
371 if start < data.len() {
373 let end = if data.last() == Some(&b'\r') {
374 data.len() - 1
375 } else {
376 data.len()
377 };
378 lines.push(&data[start..end]);
379 }
380
381 pr_lines_generic(&lines, output, config, filename, file_date)
382}
383
384fn pr_data_multicolumn_fast<W: Write>(
389 data: &[u8],
390 output: &mut W,
391 config: &PrConfig,
392 filename: &str,
393 file_date: Option<SystemTime>,
394) -> io::Result<()> {
395 let date = file_date.unwrap_or_else(SystemTime::now);
396 let header_str = config.header.as_deref().unwrap_or(filename);
397 let date_str = format_header_date(&date, &config.date_format);
398
399 let columns = config.columns.max(1);
400 let explicit_sep = has_explicit_separator(config);
401 let col_sep = get_column_separator(config);
402 let col_sep_bytes = col_sep.as_bytes();
403 let col_width = if explicit_sep {
404 if columns > 1 {
405 (config
406 .page_width
407 .saturating_sub(col_sep.len() * (columns - 1)))
408 / columns
409 } else {
410 config.page_width
411 }
412 } else {
413 config.page_width / columns
414 };
415 let content_width = if explicit_sep {
416 col_width
417 } else {
418 col_width.saturating_sub(1)
419 };
420
421 let suppress_header = !config.omit_header
422 && !config.omit_pagination
423 && config.page_length <= HEADER_LINES + FOOTER_LINES;
424 let body_lines_per_page = if config.omit_header || config.omit_pagination {
425 if config.page_length > 0 {
426 config.page_length
427 } else {
428 DEFAULT_PAGE_LENGTH
429 }
430 } else if suppress_header {
431 config.page_length
432 } else {
433 config.page_length - HEADER_LINES - FOOTER_LINES
434 };
435 let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
436 let lines_consumed_per_page = body_lines_per_page * columns;
437
438 let mut line_offsets: Vec<(u32, u32)> = Vec::with_capacity(data.len() / 40 + 64);
440 let mut start = 0usize;
441 for pos in memchr::memchr_iter(b'\n', data) {
442 line_offsets.push((start as u32, (pos - start) as u32));
443 start = pos + 1;
444 }
445 if start < data.len() {
446 line_offsets.push((start as u32, (data.len() - start) as u32));
447 }
448 let total_lines = line_offsets.len();
449
450 let out_cap = (data.len() + data.len() / 3 + 4096).min(64 * 1024 * 1024);
452 let mut out_buf: Vec<u8> = Vec::with_capacity(out_cap);
453
454 let mut line_idx = 0usize;
455 let mut page_num = 1usize;
456 let src = data.as_ptr();
457
458 while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
459 if total_lines == 0 {
460 if show_header
461 && page_num >= config.first_page
462 && (config.last_page == 0 || page_num <= config.last_page)
463 {
464 write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
465 write_footer(&mut out_buf, config)?;
466 }
467 break;
468 }
469
470 let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
471 let page_lines = &line_offsets[line_idx..page_end];
472 let n = page_lines.len();
473
474 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
475 {
476 if show_header {
477 write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
478 }
479
480 let base = n / columns;
482 let extra = n % columns;
483 let mut col_starts = [0u32; 33];
485 let max_cols = columns;
486 for col in 0..max_cols {
487 let col_lines = base + if col < extra { 1 } else { 0 };
488 col_starts[col + 1] = col_starts[col] + col_lines as u32;
489 }
490 let num_rows = if extra > 0 { base + 1 } else { base };
491
492 for row in 0..num_rows {
494 let mut last_data_col = 0;
496 for col in 0..max_cols {
497 let col_lines = (col_starts[col + 1] - col_starts[col]) as usize;
498 if row < col_lines {
499 last_data_col = col;
500 }
501 }
502
503 let mut abs_pos = 0usize;
504 for col in 0..max_cols {
505 let col_lines = (col_starts[col + 1] - col_starts[col]) as usize;
506 if row < col_lines {
507 let li = col_starts[col] as usize + row;
508 let (off, len) = page_lines[li];
509 let content_len = (len as usize).min(content_width);
510
511 if explicit_sep && col > 0 {
512 out_buf.extend_from_slice(col_sep_bytes);
513 abs_pos += col_sep_bytes.len();
514 }
515
516 if content_len > 0 {
525 let wp = out_buf.len();
526 out_buf.reserve(content_len);
527 unsafe {
528 std::ptr::copy_nonoverlapping(
529 src.add(off as usize),
530 out_buf.as_mut_ptr().add(wp),
531 content_len,
532 );
533 out_buf.set_len(wp + content_len);
534 }
535 }
536
537 if col == last_data_col && !explicit_sep {
539 while out_buf.last() == Some(&b' ') {
540 out_buf.pop();
541 }
542 }
543
544 abs_pos += content_len;
548
549 if col < last_data_col && !explicit_sep {
551 let target = (col + 1) * col_width;
552 if abs_pos < target {
553 write_column_padding_buf(&mut out_buf, abs_pos, target);
554 abs_pos = target;
555 }
556 }
557 } else if col <= last_data_col {
558 if explicit_sep {
560 if col > 0 {
561 out_buf.extend_from_slice(col_sep_bytes);
562 abs_pos += col_sep_bytes.len();
563 }
564 } else {
565 let target = (col + 1) * col_width;
566 write_column_padding_buf(&mut out_buf, abs_pos, target);
567 abs_pos = target;
568 }
569 }
570 }
571 out_buf.push(b'\n');
572 }
573
574 let body_lines_written = num_rows;
576 if show_header {
577 let pad = body_lines_per_page.saturating_sub(body_lines_written);
578 out_buf.resize(out_buf.len() + pad, b'\n');
579 write_footer(&mut out_buf, config)?;
580 }
581
582 if out_buf.len() >= 64 * 1024 * 1024 {
583 output.write_all(&out_buf)?;
584 out_buf.clear();
585 }
586 }
587
588 line_idx = page_end;
589 page_num += 1;
590 }
591
592 if !out_buf.is_empty() {
593 output.write_all(&out_buf)?;
594 }
595 Ok(())
596}
597
598#[inline]
601fn write_column_padding_buf(buf: &mut Vec<u8>, abs_pos: usize, target: usize) {
602 let n = target.saturating_sub(abs_pos);
603 if n == 0 {
604 return;
605 }
606 let next_tab = (abs_pos / 8 + 1) * 8;
607 if next_tab > target {
608 buf.resize(buf.len() + n, b' ');
610 return;
611 }
612 buf.push(b'\t');
613 let mut col = next_tab;
614 while col < target {
615 let nt = (col / 8 + 1) * 8;
616 if nt <= target {
617 buf.push(b'\t');
618 col = nt;
619 } else {
620 buf.resize(buf.len() + (target - col), b' ');
621 col = target;
622 }
623 }
624}
625
626fn pr_data_contiguous<W: Write>(
631 data: &[u8],
632 output: &mut W,
633 config: &PrConfig,
634 filename: &str,
635 file_date: Option<SystemTime>,
636) -> io::Result<()> {
637 let date = file_date.unwrap_or_else(SystemTime::now);
638 let header_str = config.header.as_deref().unwrap_or(filename);
639 let date_str = format_header_date(&date, &config.date_format);
640
641 let suppress_header = !config.omit_header
642 && !config.omit_pagination
643 && config.page_length <= HEADER_LINES + FOOTER_LINES;
644 let body_lines_per_page = if config.omit_header || config.omit_pagination {
645 if config.page_length > 0 {
646 config.page_length
647 } else {
648 DEFAULT_PAGE_LENGTH
649 }
650 } else if suppress_header {
651 config.page_length
652 } else {
653 config.page_length - HEADER_LINES - FOOTER_LINES
654 };
655 let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
656
657 if data.is_empty() {
658 if show_header {
659 let mut page_buf: Vec<u8> = Vec::with_capacity(256);
660 write_header(&mut page_buf, &date_str, header_str, 1, config)?;
661 write_footer(&mut page_buf, config)?;
662 output.write_all(&page_buf)?;
663 }
664 return Ok(());
665 }
666
667 let est_pages = data.len() / (body_lines_per_page * 40) + 2;
670 let mut page_bounds: Vec<usize> = Vec::with_capacity(est_pages + 1);
671 page_bounds.push(0);
672 let mut lines_count = 0usize;
673 for nl_pos in memchr::memchr_iter(b'\n', data) {
674 lines_count += 1;
675 if lines_count >= body_lines_per_page {
676 page_bounds.push(nl_pos + 1);
677 lines_count = 0;
678 }
679 }
680 if *page_bounds.last().unwrap() < data.len() {
681 page_bounds.push(data.len());
682 }
683 let total_pages = page_bounds.len() - 1;
684
685 let first_visible = config.first_page.max(1) - 1;
686 let last_visible = if config.last_page == 0 {
687 total_pages
688 } else {
689 config.last_page.min(total_pages)
690 };
691 if first_visible >= total_pages {
692 return Ok(());
693 }
694
695 if !show_header {
696 let start = page_bounds[first_visible];
698 let end = page_bounds[last_visible];
699 return output.write_all(&data[start..end]);
700 }
701
702 let visible_count = last_visible - first_visible;
704 let header_est = visible_count * 200;
705 let mut meta_buf: Vec<u8> = Vec::with_capacity(header_est);
706 let mut meta_ranges: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(visible_count);
708
709 for pi in first_visible..last_visible {
710 let page_num = pi + 1;
711 let body_start = page_bounds[pi];
712 let body_end = page_bounds[pi + 1];
713
714 let hdr_start = meta_buf.len();
715 write_header(&mut meta_buf, &date_str, header_str, page_num, config)?;
716 let hdr_end = meta_buf.len();
717
718 let ft_start = meta_buf.len();
719 let body_slice = &data[body_start..body_end];
720 let actual_lines = memchr::memchr_iter(b'\n', body_slice).count();
723 let has_unterminated = !body_slice.is_empty() && body_slice[body_slice.len() - 1] != b'\n';
724 if has_unterminated {
725 meta_buf.push(b'\n');
726 }
727 let effective_lines = actual_lines + has_unterminated as usize;
728 let pad = body_lines_per_page.saturating_sub(effective_lines);
729 meta_buf.resize(meta_buf.len() + pad, b'\n');
730 write_footer(&mut meta_buf, config)?;
731 let ft_end = meta_buf.len();
732
733 meta_ranges.push((hdr_start, hdr_end, ft_start, ft_end));
734 }
735
736 let mut slices: Vec<IoSlice<'_>> = Vec::with_capacity(visible_count * 3);
738 for (i, &(hs, he, fs, fe)) in meta_ranges.iter().enumerate() {
739 let pi = first_visible + i;
740 if he > hs {
741 slices.push(IoSlice::new(&meta_buf[hs..he]));
742 }
743 let body_start = page_bounds[pi];
744 let body_end = page_bounds[pi + 1];
745 if body_end > body_start {
746 slices.push(IoSlice::new(&data[body_start..body_end]));
747 }
748 if fe > fs {
749 slices.push(IoSlice::new(&meta_buf[fs..fe]));
750 }
751 }
752
753 write_all_ioslices(output, &slices)
754}
755
756fn write_all_ioslices<W: Write>(out: &mut W, slices: &[IoSlice<'_>]) -> io::Result<()> {
760 let written = out.write_vectored(slices)?;
762 if written == 0 && !slices.is_empty() {
763 return Err(io::Error::new(io::ErrorKind::WriteZero, "write zero"));
764 }
765 let total: usize = slices.iter().map(|s| s.len()).sum();
767 if written >= total {
768 return Ok(());
769 }
770 let mut bufs = slices.to_vec();
772 let mut remaining: &mut [IoSlice<'_>] = &mut bufs;
773 IoSlice::advance_slices(&mut remaining, written);
774 while !remaining.is_empty() {
775 let n = out.write_vectored(remaining)?;
776 if n == 0 {
777 return Err(io::Error::new(io::ErrorKind::WriteZero, "write zero"));
778 }
779 IoSlice::advance_slices(&mut remaining, n);
780 }
781 Ok(())
782}
783
784fn pr_data_numbered<W: Write>(
788 data: &[u8],
789 output: &mut W,
790 config: &PrConfig,
791 filename: &str,
792 file_date: Option<SystemTime>,
793) -> io::Result<()> {
794 let date = file_date.unwrap_or_else(SystemTime::now);
795 let header_str = config.header.as_deref().unwrap_or(filename);
796 let date_str = format_header_date(&date, &config.date_format);
797
798 let (sep_char, digits) = config.number_lines.unwrap_or(('\t', 5));
799 debug_assert!(sep_char.is_ascii(), "number separator must be ASCII");
800 let sep_byte = sep_char as u8;
801 let suppress_header = !config.omit_header
802 && !config.omit_pagination
803 && config.page_length <= HEADER_LINES + FOOTER_LINES;
804 let body_lines_per_page = if config.omit_header || config.omit_pagination {
805 if config.page_length > 0 {
806 config.page_length
807 } else {
808 DEFAULT_PAGE_LENGTH
809 }
810 } else if suppress_header {
811 config.page_length
812 } else {
813 config.page_length - HEADER_LINES - FOOTER_LINES
814 };
815 let show_header = !config.omit_header && !config.omit_pagination && !suppress_header;
816
817 let mut line_starts: Vec<usize> = Vec::with_capacity(data.len() / 40 + 64);
819 line_starts.push(0);
820 for pos in memchr::memchr_iter(b'\n', data) {
821 line_starts.push(pos + 1);
822 }
823 let total_lines = if !data.is_empty() && data[data.len() - 1] == b'\n' {
824 line_starts.len() - 1
825 } else {
826 line_starts.len()
827 };
828
829 let num_prefix_est = digits + 2; let out_cap =
833 (data.len() + total_lines * num_prefix_est + total_lines / 5 + 4096).min(64 * 1024 * 1024);
834 let mut out_buf: Vec<u8> = Vec::with_capacity(out_cap);
835
836 let mut line_number = config.first_line_number;
837 let mut page_num = 1usize;
838 let mut line_idx = 0;
839
840 while line_idx < total_lines {
841 let page_end = (line_idx + body_lines_per_page).min(total_lines);
842 let in_range = page_num >= config.first_page
843 && (config.last_page == 0 || page_num <= config.last_page);
844
845 if in_range {
846 if show_header {
847 write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
848 }
849
850 let src = data.as_ptr();
855 for li in line_idx..page_end {
856 let line_start = line_starts[li];
857 let line_end = if li + 1 < line_starts.len() {
858 let end = line_starts[li + 1] - 1;
859 if end > line_start && data[end - 1] == b'\r' {
860 end - 1
861 } else {
862 end
863 }
864 } else {
865 data.len()
866 };
867 let line_len = line_end - line_start;
868
869 let wp = out_buf.len();
870
871 let mut n = line_number;
872 let mut num_pos = 19usize;
873 let mut num_tmp = [0u8; 20];
874 loop {
875 num_tmp[num_pos] = b'0' + (n % 10) as u8;
876 n /= 10;
877 if n == 0 || num_pos == 0 {
878 break;
879 }
880 num_pos -= 1;
881 }
882 let num_digits = 20 - num_pos;
883 let padding = digits.saturating_sub(num_digits);
884 let actual_prefix = padding + num_digits + 1;
885
886 let needed = actual_prefix + line_len + 1;
887 if out_buf.len() + needed > out_buf.capacity() {
888 out_buf.reserve(needed);
889 }
890 let base = out_buf.as_mut_ptr();
891
892 unsafe {
893 let dst = base.add(wp);
894 std::ptr::write_bytes(dst, b' ', padding);
895 std::ptr::copy_nonoverlapping(
896 num_tmp.as_ptr().add(num_pos),
897 dst.add(padding),
898 num_digits,
899 );
900 *dst.add(padding + num_digits) = sep_byte;
901 if line_len > 0 {
902 std::ptr::copy_nonoverlapping(
903 src.add(line_start),
904 dst.add(actual_prefix),
905 line_len,
906 );
907 }
908 *dst.add(actual_prefix + line_len) = b'\n';
909 out_buf.set_len(wp + actual_prefix + line_len + 1);
910 }
911
912 line_number += 1;
913 }
914
915 if show_header {
916 let body_lines_written = page_end - line_idx;
917 let pad = body_lines_per_page.saturating_sub(body_lines_written);
918 out_buf.resize(out_buf.len() + pad, b'\n');
919 write_footer(&mut out_buf, config)?;
920 }
921
922 if out_buf.len() >= 64 * 1024 * 1024 {
924 output.write_all(&out_buf)?;
925 out_buf.clear();
926 }
927 } else {
928 line_number += page_end - line_idx;
929 }
930
931 line_idx = page_end;
932 page_num += 1;
933 }
934
935 if !out_buf.is_empty() {
936 output.write_all(&out_buf)?;
937 }
938 Ok(())
939}
940
941pub fn pr_file<R: BufRead, W: Write>(
943 input: R,
944 output: &mut W,
945 config: &PrConfig,
946 filename: &str,
947 file_date: Option<SystemTime>,
948) -> io::Result<()> {
949 let mut all_lines: Vec<String> = Vec::new();
951 for line_result in input.lines() {
952 let line = line_result?;
953 let mut line = line;
954
955 if let Some((tab_char, tab_width)) = config.expand_tabs {
957 line = expand_tabs_in_line(&line, tab_char, tab_width);
958 }
959
960 if config.show_control_chars || config.show_nonprinting {
962 line = process_control_chars(&line, config.show_control_chars, config.show_nonprinting);
963 }
964
965 all_lines.push(line);
966 }
967
968 let refs: Vec<&[u8]> = all_lines.iter().map(|s| s.as_bytes()).collect();
970 pr_lines_generic(&refs, output, config, filename, file_date)
971}
972
973fn pr_lines_generic<W: Write>(
975 all_lines: &[&[u8]],
976 output: &mut W,
977 config: &PrConfig,
978 filename: &str,
979 file_date: Option<SystemTime>,
980) -> io::Result<()> {
981 let date = file_date.unwrap_or_else(SystemTime::now);
982
983 let header_str = config.header.as_deref().unwrap_or(filename);
984 let date_str = format_header_date(&date, &config.date_format);
985
986 let suppress_header = !config.omit_header
990 && !config.omit_pagination
991 && config.page_length <= HEADER_LINES + FOOTER_LINES;
992 let suppressed_config;
995 let effective_config = if suppress_header {
996 suppressed_config = PrConfig {
997 omit_header: true,
998 ..config.clone()
999 };
1000 &suppressed_config
1001 } else {
1002 config
1003 };
1004 let body_lines_per_page = if config.omit_header || config.omit_pagination {
1005 if config.page_length > 0 {
1006 config.page_length
1007 } else {
1008 DEFAULT_PAGE_LENGTH
1009 }
1010 } else if suppress_header {
1011 config.page_length
1012 } else {
1013 config.page_length - HEADER_LINES - FOOTER_LINES
1014 };
1015
1016 let input_lines_per_page = if config.double_space {
1018 (body_lines_per_page + 1) / 2
1019 } else {
1020 body_lines_per_page
1021 };
1022
1023 let columns = config.columns.max(1);
1025
1026 let lines_consumed_per_page = if columns > 1 && !config.across {
1031 input_lines_per_page * columns
1032 } else {
1033 input_lines_per_page
1034 };
1035
1036 let total_lines = all_lines.len();
1038 let mut line_number = config.first_line_number;
1039 let mut page_num = 1usize;
1040 let mut line_idx = 0;
1041 let total_bytes: usize = all_lines.iter().map(|l| l.len() + 1).sum();
1042 let out_cap = (total_bytes + total_bytes / 5 + 4096).min(64 * 1024 * 1024);
1044 let mut out_buf: Vec<u8> = Vec::with_capacity(out_cap);
1045
1046 while line_idx < total_lines || (line_idx == 0 && total_lines == 0) {
1047 if total_lines == 0 && line_idx == 0 {
1048 if page_num >= config.first_page
1049 && (config.last_page == 0 || page_num <= config.last_page)
1050 {
1051 if !config.omit_header && !config.omit_pagination && !suppress_header {
1052 write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
1053 write_footer(&mut out_buf, config)?;
1054 }
1055 }
1056 break;
1057 }
1058
1059 let page_end = (line_idx + lines_consumed_per_page).min(total_lines);
1060
1061 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
1062 {
1063 if !config.omit_header && !config.omit_pagination && !suppress_header {
1064 write_header(&mut out_buf, &date_str, header_str, page_num, config)?;
1065 }
1066
1067 if columns > 1 {
1068 write_multicolumn_body(
1069 &mut out_buf,
1070 &all_lines[line_idx..page_end],
1071 effective_config,
1072 columns,
1073 &mut line_number,
1074 body_lines_per_page,
1075 )?;
1076 } else {
1077 write_single_column_body(
1078 &mut out_buf,
1079 &all_lines[line_idx..page_end],
1080 effective_config,
1081 &mut line_number,
1082 body_lines_per_page,
1083 )?;
1084 }
1085
1086 if !config.omit_header && !config.omit_pagination && !suppress_header {
1087 write_footer(&mut out_buf, config)?;
1088 }
1089
1090 if out_buf.len() >= 64 * 1024 * 1024 {
1092 output.write_all(&out_buf)?;
1093 out_buf.clear();
1094 }
1095 }
1096
1097 line_idx = page_end;
1098 page_num += 1;
1099
1100 if line_idx >= total_lines {
1101 break;
1102 }
1103 }
1104
1105 if !out_buf.is_empty() {
1106 output.write_all(&out_buf)?;
1107 }
1108 Ok(())
1109}
1110
1111pub fn pr_merge<W: Write>(
1113 inputs: &[Vec<String>],
1114 output: &mut W,
1115 config: &PrConfig,
1116 _filenames: &[&str],
1117 file_dates: &[SystemTime],
1118) -> io::Result<()> {
1119 let date = file_dates.first().copied().unwrap_or_else(SystemTime::now);
1120 let date_str = format_header_date(&date, &config.date_format);
1121 let header_str = config.header.as_deref().unwrap_or("");
1122
1123 let suppress_header = !config.omit_header
1124 && !config.omit_pagination
1125 && config.page_length <= HEADER_LINES + FOOTER_LINES;
1126 let body_lines_per_page = if config.omit_header || config.omit_pagination {
1127 if config.page_length > 0 {
1128 config.page_length
1129 } else {
1130 DEFAULT_PAGE_LENGTH
1131 }
1132 } else if suppress_header {
1133 config.page_length
1134 } else {
1135 config.page_length - HEADER_LINES - FOOTER_LINES
1136 };
1137
1138 let input_lines_per_page = if config.double_space {
1139 (body_lines_per_page + 1) / 2
1140 } else {
1141 body_lines_per_page
1142 };
1143
1144 let num_files = inputs.len();
1145 let explicit_sep = has_explicit_separator(config);
1146 let col_sep = get_column_separator(config);
1147 let col_width = if explicit_sep {
1148 if num_files > 1 {
1149 (config
1150 .page_width
1151 .saturating_sub(col_sep.len() * (num_files - 1)))
1152 / num_files
1153 } else {
1154 config.page_width
1155 }
1156 } else {
1157 config.page_width / num_files
1158 };
1159
1160 let max_lines = inputs.iter().map(|f| f.len()).max().unwrap_or(0);
1161 let mut page_num = 1usize;
1162 let mut line_idx = 0;
1163 let mut line_number = config.first_line_number;
1164
1165 let col_sep_bytes = col_sep.as_bytes();
1166 let mut page_buf: Vec<u8> = Vec::with_capacity(128 * 1024);
1167 let mut num_buf = [0u8; 32];
1168
1169 while line_idx < max_lines {
1170 let page_end = (line_idx + input_lines_per_page).min(max_lines);
1171
1172 if page_num >= config.first_page && (config.last_page == 0 || page_num <= config.last_page)
1173 {
1174 page_buf.clear();
1175
1176 if !config.omit_header && !config.omit_pagination && !suppress_header {
1177 write_header(&mut page_buf, &date_str, header_str, page_num, config)?;
1178 }
1179
1180 let indent_str = " ".repeat(config.indent);
1181 let mut body_lines_written = 0;
1182 for i in line_idx..page_end {
1183 if config.double_space && body_lines_written > 0 {
1184 page_buf.push(b'\n');
1185 body_lines_written += 1;
1186 }
1187
1188 page_buf.extend_from_slice(indent_str.as_bytes());
1189 let mut abs_pos = config.indent;
1190
1191 if let Some((sep, digits)) = config.number_lines {
1192 let num_str = format_line_number(line_number, sep, digits, &mut num_buf);
1193 page_buf.extend_from_slice(num_str);
1194 abs_pos += digits + 1;
1195 line_number += 1;
1196 }
1197
1198 for (fi, file_lines) in inputs.iter().enumerate() {
1199 let content = if i < file_lines.len() {
1200 file_lines[i].as_bytes()
1201 } else {
1202 b"" as &[u8]
1203 };
1204 let truncated = if !explicit_sep && content.len() > col_width.saturating_sub(1)
1205 {
1206 &content[..col_width.saturating_sub(1)]
1207 } else if explicit_sep && config.truncate_lines && content.len() > col_width {
1208 &content[..col_width]
1209 } else {
1210 content
1211 };
1212 if fi < num_files - 1 {
1213 if explicit_sep {
1214 if fi > 0 {
1215 page_buf.extend_from_slice(col_sep_bytes);
1216 }
1217 page_buf.extend_from_slice(truncated);
1218 abs_pos +=
1219 truncated.len() + if fi > 0 { col_sep_bytes.len() } else { 0 };
1220 } else {
1221 page_buf.extend_from_slice(truncated);
1222 abs_pos += truncated.len();
1223 let target = (fi + 1) * col_width + config.indent;
1224 write_column_padding(&mut page_buf, abs_pos, target)?;
1225 abs_pos = target;
1226 }
1227 } else {
1228 if explicit_sep && fi > 0 {
1229 page_buf.extend_from_slice(col_sep_bytes);
1230 }
1231 page_buf.extend_from_slice(truncated);
1232 }
1233 }
1234 page_buf.push(b'\n');
1235 body_lines_written += 1;
1236 }
1237
1238 while body_lines_written < body_lines_per_page {
1240 page_buf.push(b'\n');
1241 body_lines_written += 1;
1242 }
1243
1244 if !config.omit_header && !config.omit_pagination && !suppress_header {
1245 write_footer(&mut page_buf, config)?;
1246 }
1247
1248 output.write_all(&page_buf)?;
1249 }
1250
1251 line_idx = page_end;
1252 page_num += 1;
1253 }
1254
1255 Ok(())
1256}
1257
1258fn write_header<W: Write>(
1260 output: &mut W,
1261 date_str: &str,
1262 header: &str,
1263 page_num: usize,
1264 config: &PrConfig,
1265) -> io::Result<()> {
1266 output.write_all(b"\n\n")?;
1268
1269 let line_width = config.page_width;
1271
1272 let left = date_str;
1273 let center = header;
1274 let left_len = left.len();
1275 let center_len = center.len();
1276
1277 let mut page_buf = [0u8; 32];
1279 let page_str = format_page_number(page_num, &mut page_buf);
1280 let right_len = page_str.len();
1281
1282 if left_len + center_len + right_len + 2 >= line_width {
1284 output.write_all(left.as_bytes())?;
1285 output.write_all(b" ")?;
1286 output.write_all(center.as_bytes())?;
1287 output.write_all(b" ")?;
1288 output.write_all(page_str)?;
1289 output.write_all(b"\n")?;
1290 } else {
1291 let total_spaces = line_width - left_len - center_len - right_len;
1292 let left_spaces = total_spaces / 2;
1293 let right_spaces = total_spaces - left_spaces;
1294 output.write_all(left.as_bytes())?;
1295 write_spaces(output, left_spaces)?;
1296 output.write_all(center.as_bytes())?;
1297 write_spaces(output, right_spaces)?;
1298 output.write_all(page_str)?;
1299 output.write_all(b"\n")?;
1300 }
1301
1302 output.write_all(b"\n\n")?;
1304
1305 Ok(())
1306}
1307
1308#[inline]
1310fn format_page_number(page_num: usize, buf: &mut [u8; 32]) -> &[u8] {
1311 const PREFIX: &[u8] = b"Page ";
1312 let prefix_len = PREFIX.len();
1313 buf[..prefix_len].copy_from_slice(PREFIX);
1314 let mut num_buf = [0u8; 20];
1316 let mut n = page_num;
1317 let mut pos = 19;
1318 loop {
1319 num_buf[pos] = b'0' + (n % 10) as u8;
1320 n /= 10;
1321 if n == 0 {
1322 break;
1323 }
1324 pos -= 1;
1325 }
1326 let num_len = 20 - pos;
1327 buf[prefix_len..prefix_len + num_len].copy_from_slice(&num_buf[pos..20]);
1328 &buf[..prefix_len + num_len]
1329}
1330
1331fn write_footer<W: Write>(output: &mut W, config: &PrConfig) -> io::Result<()> {
1333 if config.form_feed {
1334 output.write_all(b"\x0c")?;
1335 } else {
1336 output.write_all(b"\n\n\n\n\n")?;
1337 }
1338 Ok(())
1339}
1340
1341fn write_single_column_body<W: Write>(
1343 output: &mut W,
1344 lines: &[&[u8]],
1345 config: &PrConfig,
1346 line_number: &mut usize,
1347 body_lines_per_page: usize,
1348) -> io::Result<()> {
1349 let indent_str = " ".repeat(config.indent);
1350 let content_width = if config.truncate_lines {
1351 compute_content_width(config)
1352 } else {
1353 0
1354 };
1355 let mut body_lines_written = 0;
1356 let mut num_buf = [0u8; 32];
1358
1359 for line in lines.iter() {
1360 output.write_all(indent_str.as_bytes())?;
1361
1362 if let Some((sep, digits)) = config.number_lines {
1363 let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1365 output.write_all(num_str)?;
1366 *line_number += 1;
1367 }
1368
1369 let content: &[u8] = if config.truncate_lines {
1370 if line.len() > content_width {
1371 &line[..content_width]
1372 } else {
1373 line
1374 }
1375 } else {
1376 line
1377 };
1378
1379 output.write_all(content)?;
1381 output.write_all(b"\n")?;
1382 body_lines_written += 1;
1383 if body_lines_written >= body_lines_per_page {
1384 break;
1385 }
1386
1387 if config.double_space {
1389 output.write_all(b"\n")?;
1390 body_lines_written += 1;
1391 if body_lines_written >= body_lines_per_page {
1392 break;
1393 }
1394 }
1395 }
1396
1397 if !config.omit_header && !config.omit_pagination {
1399 while body_lines_written < body_lines_per_page {
1400 output.write_all(b"\n")?;
1401 body_lines_written += 1;
1402 }
1403 }
1404
1405 Ok(())
1406}
1407
1408#[inline]
1411fn format_line_number(num: usize, sep: char, digits: usize, buf: &mut [u8; 32]) -> &[u8] {
1412 let mut n = num;
1414 let mut pos = 31;
1415 loop {
1416 buf[pos] = b'0' + (n % 10) as u8;
1417 n /= 10;
1418 if n == 0 || pos == 0 {
1419 break;
1420 }
1421 pos -= 1;
1422 }
1423 let num_digits = 32 - pos;
1424 let padding = if digits > num_digits {
1426 digits - num_digits
1427 } else {
1428 0
1429 };
1430 let total_len = padding + num_digits + sep.len_utf8();
1431 let start = 32 - num_digits;
1434 let sep_byte = sep as u8; let out_start = 32usize.saturating_sub(total_len);
1438 for i in out_start..out_start + padding {
1440 buf[i] = b' ';
1441 }
1442 if out_start + padding != start {
1444 let src = start;
1445 let dst = out_start + padding;
1446 for i in 0..num_digits {
1447 buf[dst + i] = buf[src + i];
1448 }
1449 }
1450 buf[out_start + padding + num_digits] = sep_byte;
1452 &buf[out_start..out_start + total_len]
1453}
1454
1455fn compute_content_width(config: &PrConfig) -> usize {
1457 let mut w = config.page_width;
1458 w = w.saturating_sub(config.indent);
1459 if let Some((_, digits)) = config.number_lines {
1460 w = w.saturating_sub(digits + 1); }
1462 w
1463}
1464
1465fn write_multicolumn_body<W: Write>(
1467 output: &mut W,
1468 lines: &[&[u8]],
1469 config: &PrConfig,
1470 columns: usize,
1471 line_number: &mut usize,
1472 body_lines_per_page: usize,
1473) -> io::Result<()> {
1474 let explicit_sep = has_explicit_separator(config);
1475 let col_sep = get_column_separator(config);
1476 let col_width = if explicit_sep {
1479 if columns > 1 {
1480 (config
1481 .page_width
1482 .saturating_sub(col_sep.len() * (columns - 1)))
1483 / columns
1484 } else {
1485 config.page_width
1486 }
1487 } else {
1488 config.page_width / columns
1489 };
1490 let do_truncate = !config.join_lines;
1493 let content_width = if explicit_sep {
1494 col_width
1495 } else {
1496 col_width.saturating_sub(1)
1497 };
1498
1499 let indent_str = " ".repeat(config.indent);
1500 let col_sep_bytes = col_sep.as_bytes();
1501 let mut body_lines_written = 0;
1502 let mut num_buf = [0u8; 32];
1503
1504 if config.across {
1505 let mut i = 0;
1507 while i < lines.len() {
1508 if config.double_space && body_lines_written > 0 {
1509 output.write_all(b"\n")?;
1510 body_lines_written += 1;
1511 if body_lines_written >= body_lines_per_page {
1512 break;
1513 }
1514 }
1515
1516 output.write_all(indent_str.as_bytes())?;
1517 let mut abs_pos = config.indent;
1518
1519 let mut last_data_col = 0;
1521 for col in 0..columns {
1522 let li = i + col;
1523 if li < lines.len() {
1524 last_data_col = col;
1525 }
1526 }
1527
1528 for col in 0..columns {
1529 let li = i + col;
1530 if li < lines.len() {
1531 if explicit_sep && col > 0 {
1532 output.write_all(col_sep_bytes)?;
1533 abs_pos += col_sep_bytes.len();
1534 }
1535 if let Some((sep, digits)) = config.number_lines {
1536 let num_str = format_line_number(*line_number, sep, digits, &mut num_buf);
1537 output.write_all(num_str)?;
1538 abs_pos += digits + 1;
1539 *line_number += 1;
1540 }
1541 let content: &[u8] = lines[li];
1542 let mut truncated = if do_truncate && content.len() > content_width {
1543 &content[..content_width]
1544 } else {
1545 content
1546 };
1547 if col == last_data_col && !explicit_sep {
1549 while truncated.last() == Some(&b' ') {
1550 truncated = &truncated[..truncated.len() - 1];
1551 }
1552 }
1553 output.write_all(truncated)?;
1554 abs_pos += truncated.len();
1555 if col < last_data_col && !explicit_sep {
1556 let target = (col + 1) * col_width + config.indent;
1557 write_column_padding(output, abs_pos, target)?;
1558 abs_pos = target;
1559 }
1560 }
1561 }
1562 output.write_all(b"\n")?;
1563 body_lines_written += 1;
1564 i += columns;
1565 }
1566 } else {
1567 let n = lines.len();
1571 let base = n / columns;
1572 let extra = n % columns;
1573
1574 let mut col_starts = vec![0usize; columns + 1];
1576 for col in 0..columns {
1577 let col_lines = base + if col < extra { 1 } else { 0 };
1578 col_starts[col + 1] = col_starts[col] + col_lines;
1579 }
1580
1581 let num_rows = if extra > 0 { base + 1 } else { base };
1583
1584 for row in 0..num_rows {
1585 if config.double_space && row > 0 {
1586 output.write_all(b"\n")?;
1587 body_lines_written += 1;
1588 if body_lines_written >= body_lines_per_page {
1589 break;
1590 }
1591 }
1592
1593 output.write_all(indent_str.as_bytes())?;
1594 let mut abs_pos = config.indent;
1595
1596 let mut last_data_col = 0;
1598 for col in 0..columns {
1599 let col_lines = col_starts[col + 1] - col_starts[col];
1600 if row < col_lines {
1601 last_data_col = col;
1602 }
1603 }
1604
1605 for col in 0..columns {
1606 let col_lines = col_starts[col + 1] - col_starts[col];
1607 let li = col_starts[col] + row;
1608 if row < col_lines {
1609 if explicit_sep && col > 0 {
1610 output.write_all(col_sep_bytes)?;
1611 abs_pos += col_sep_bytes.len();
1612 }
1613 if let Some((sep, digits)) = config.number_lines {
1614 let num = config.first_line_number + li;
1615 let num_str = format_line_number(num, sep, digits, &mut num_buf);
1616 output.write_all(num_str)?;
1617 abs_pos += digits + 1;
1618 }
1619 let content: &[u8] = lines[li];
1620 let mut truncated = if do_truncate && content.len() > content_width {
1621 &content[..content_width]
1622 } else {
1623 content
1624 };
1625 if col == last_data_col && !explicit_sep {
1627 while truncated.last() == Some(&b' ') {
1628 truncated = &truncated[..truncated.len() - 1];
1629 }
1630 }
1631 output.write_all(truncated)?;
1632 abs_pos += truncated.len();
1633 if col < last_data_col && !explicit_sep {
1634 let target = (col + 1) * col_width + config.indent;
1636 write_column_padding(output, abs_pos, target)?;
1637 abs_pos = target;
1638 }
1639 } else if col <= last_data_col {
1640 if explicit_sep {
1642 if col > 0 {
1643 output.write_all(col_sep_bytes)?;
1644 abs_pos += col_sep_bytes.len();
1645 }
1646 } else {
1648 let target = (col + 1) * col_width + config.indent;
1649 write_column_padding(output, abs_pos, target)?;
1650 abs_pos = target;
1651 }
1652 }
1653 }
1655 output.write_all(b"\n")?;
1656 body_lines_written += 1;
1657 }
1658 if config.number_lines.is_some() {
1660 *line_number += lines.len();
1661 }
1662 }
1663
1664 if !config.omit_header && !config.omit_pagination {
1666 while body_lines_written < body_lines_per_page {
1667 output.write_all(b"\n")?;
1668 body_lines_written += 1;
1669 }
1670 }
1671
1672 Ok(())
1673}