Skip to main content

coreutils_rs/cat/
core.rs

1use std::io::{self, Read, Write};
2use std::path::Path;
3
4use crate::common::io::{read_file_direct, read_stdin};
5
6/// Errors specific to the plain-file fast path on Linux.
7/// Separates directory/same-file detection from I/O errors so callers
8/// can emit GNU-compatible diagnostics without redundant syscalls.
9#[cfg(target_os = "linux")]
10pub enum CatPlainError {
11    /// The path is a directory
12    IsDirectory,
13    /// Input file is the same as stdout (e.g. `cat file >> file`)
14    InputIsOutput,
15    /// Regular I/O error
16    Io(io::Error),
17}
18
19#[cfg(target_os = "linux")]
20impl From<io::Error> for CatPlainError {
21    fn from(e: io::Error) -> Self {
22        CatPlainError::Io(e)
23    }
24}
25
26/// Configuration for cat
27#[derive(Clone, Debug, Default)]
28pub struct CatConfig {
29    pub number: bool,
30    pub number_nonblank: bool,
31    pub show_ends: bool,
32    pub show_tabs: bool,
33    pub show_nonprinting: bool,
34    pub squeeze_blank: bool,
35}
36
37impl CatConfig {
38    /// Returns true if no special processing is needed (plain cat)
39    pub fn is_plain(&self) -> bool {
40        !self.number
41            && !self.number_nonblank
42            && !self.show_ends
43            && !self.show_tabs
44            && !self.show_nonprinting
45            && !self.squeeze_blank
46    }
47}
48
49/// Zero-copy file→stdout on Linux using splice, copy_file_range, or fast read/write.
50/// Also performs directory check and input==output detection using the fstat results
51/// to avoid redundant syscalls in the caller.
52///
53/// Returns:
54///   Ok(true)  — file was fully handled
55///   Ok(false) — caller should fall back to generic path
56///   Err(CatPlainError::IsDirectory)    — path is a directory
57///   Err(CatPlainError::InputIsOutput)  — input file is the same as stdout
58///   Err(CatPlainError::Io(e))          — I/O error
59#[cfg(target_os = "linux")]
60pub fn cat_plain_file_linux(path: &Path) -> Result<bool, CatPlainError> {
61    use std::os::unix::fs::OpenOptionsExt;
62    use std::os::unix::io::AsRawFd;
63
64    let file = std::fs::OpenOptions::new()
65        .read(true)
66        .custom_flags(libc::O_NOATIME)
67        .open(path)
68        .or_else(|_| std::fs::File::open(path))?;
69
70    let in_fd = file.as_raw_fd();
71
72    // Single fstat(file_fd) — replaces both stat(path) calls in the caller
73    let mut in_stat: libc::stat = unsafe { std::mem::zeroed() };
74    if unsafe { libc::fstat(in_fd, &mut in_stat) } != 0 {
75        return Err(io::Error::last_os_error().into());
76    }
77
78    let in_mode = in_stat.st_mode & libc::S_IFMT;
79
80    // Directory check (replaces stat(path).is_dir() in caller)
81    if in_mode == libc::S_IFDIR {
82        return Err(CatPlainError::IsDirectory);
83    }
84
85    // Single fstat(stdout) — replaces the fstat(1) in caller AND the one we did below
86    let stdout = io::stdout();
87    let out_fd = stdout.as_raw_fd();
88    let mut out_stat: libc::stat = unsafe { std::mem::zeroed() };
89    if unsafe { libc::fstat(out_fd, &mut out_stat) } != 0 {
90        return Err(io::Error::last_os_error().into());
91    }
92
93    // Same-file detection (replaces dev/ino comparison in caller)
94    if in_stat.st_dev == out_stat.st_dev && in_stat.st_ino == out_stat.st_ino {
95        return Err(CatPlainError::InputIsOutput);
96    }
97
98    let file_size = in_stat.st_size as usize;
99
100    if file_size == 0 {
101        // May be a virtual file (e.g. /proc/*) with size 0 but actual content
102        if in_mode != libc::S_IFREG {
103            return Ok(false); // let generic path handle devices/special files
104        }
105        // Try reading — virtual files report size 0
106        let mut buf = [0u8; 65536];
107        let mut out = stdout.lock();
108        loop {
109            let n = match nix_read(in_fd, &mut buf) {
110                Ok(0) => break,
111                Ok(n) => n,
112                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
113                Err(_) => break,
114            };
115            out.write_all(&buf[..n])?;
116        }
117        return Ok(true);
118    }
119
120    // Hint kernel for sequential access
121    unsafe {
122        libc::posix_fadvise(in_fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
123    }
124
125    let stdout_mode = out_stat.st_mode & libc::S_IFMT;
126
127    if stdout_mode == libc::S_IFIFO {
128        // stdout is a pipe → splice (zero-copy file→pipe)
129        let mut remaining = file_size;
130        while remaining > 0 {
131            let chunk = remaining.min(1024 * 1024 * 1024);
132            let ret = unsafe {
133                libc::splice(
134                    in_fd,
135                    std::ptr::null_mut(),
136                    out_fd,
137                    std::ptr::null_mut(),
138                    chunk,
139                    libc::SPLICE_F_MOVE,
140                )
141            };
142            if ret > 0 {
143                remaining -= ret as usize;
144            } else if ret == 0 {
145                break;
146            } else {
147                let err = io::Error::last_os_error();
148                if err.kind() == io::ErrorKind::Interrupted {
149                    continue;
150                }
151                // splice not supported — fall through to read/write
152                return Ok(cat_readwrite(in_fd, stdout.lock())?);
153            }
154        }
155        return Ok(true);
156    }
157
158    if stdout_mode == libc::S_IFREG {
159        // stdout is a regular file → copy_file_range (zero-copy in-kernel)
160        // Use NULL offsets so the kernel uses and updates the fd positions directly.
161        // This is critical for multi-file cat: explicit offsets don't update the fd
162        // position, causing the second file to overwrite the first.
163        let mut remaining = file_size;
164        while remaining > 0 {
165            let chunk = remaining.min(0x7ffff000);
166            let ret = unsafe {
167                libc::copy_file_range(
168                    in_fd,
169                    std::ptr::null_mut(),
170                    out_fd,
171                    std::ptr::null_mut(),
172                    chunk,
173                    0,
174                )
175            };
176            if ret > 0 {
177                remaining -= ret as usize;
178            } else if ret == 0 {
179                break;
180            } else {
181                let err = io::Error::last_os_error();
182                if err.kind() == io::ErrorKind::Interrupted {
183                    continue;
184                }
185                // copy_file_range not supported — fall through to read/write
186                return Ok(cat_readwrite(in_fd, stdout.lock())?);
187            }
188        }
189        return Ok(true);
190    }
191
192    // stdout is /dev/null, socket, or other — use fast read/write loop
193    Ok(cat_readwrite(in_fd, stdout.lock())?)
194}
195
196/// Fast read/write loop using raw fds and a 256KB page-aligned buffer.
197#[cfg(target_os = "linux")]
198fn cat_readwrite(in_fd: i32, mut out: impl Write) -> io::Result<bool> {
199    // 256KB matches GNU cat's empirically-optimal buffer size
200    let mut buf = vec![0u8; 256 * 1024];
201    loop {
202        let n = match nix_read(in_fd, &mut buf) {
203            Ok(0) => break,
204            Ok(n) => n,
205            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
206            Err(e) => return Err(e),
207        };
208        out.write_all(&buf[..n])?;
209    }
210    Ok(true)
211}
212
213/// Wrapper around libc::read returning io::Result
214#[cfg(target_os = "linux")]
215fn nix_read(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
216    let ret = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
217    if ret >= 0 {
218        Ok(ret as usize)
219    } else {
220        Err(io::Error::last_os_error())
221    }
222}
223
224/// Plain cat for a single file — tries zero-copy, then falls back to read/write loop.
225/// Note: On Linux, callers that need directory/same-file detection should call
226/// cat_plain_file_linux directly to avoid redundant syscalls.
227pub fn cat_plain_file(path: &Path, out: &mut impl Write) -> io::Result<bool> {
228    #[cfg(target_os = "linux")]
229    {
230        match cat_plain_file_linux(path) {
231            Ok(true) => return Ok(true),
232            Ok(false) => {}
233            Err(CatPlainError::Io(e)) if e.kind() == io::ErrorKind::BrokenPipe => {
234                return Err(e);
235            }
236            Err(_) => {} // fall through to generic path (includes IsDirectory, InputIsOutput)
237        }
238    }
239
240    // Fallback: read file + write (non-Linux or special files)
241    let data = read_file_direct(path)?;
242    if !data.is_empty() {
243        out.write_all(&data)?;
244    }
245    Ok(true)
246}
247
248/// Plain cat for stdin — try splice on Linux, otherwise bulk read+write
249pub fn cat_plain_stdin(out: &mut impl Write) -> io::Result<()> {
250    #[cfg(target_os = "linux")]
251    {
252        // Try splice stdin→stdout if both are pipes
253        let stdin_fd = 0i32;
254        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
255        if unsafe { libc::fstat(1, &mut stat) } == 0
256            && (stat.st_mode & libc::S_IFMT) == libc::S_IFIFO
257        {
258            // stdout is a pipe, try splice from stdin
259            loop {
260                let ret = unsafe {
261                    libc::splice(
262                        stdin_fd,
263                        std::ptr::null_mut(),
264                        1,
265                        std::ptr::null_mut(),
266                        1024 * 1024 * 1024,
267                        libc::SPLICE_F_MOVE,
268                    )
269                };
270                if ret > 0 {
271                    continue;
272                } else if ret == 0 {
273                    return Ok(());
274                } else {
275                    let err = io::Error::last_os_error();
276                    if err.kind() == io::ErrorKind::Interrupted {
277                        continue;
278                    }
279                    // splice not supported, fall through to read+write
280                    break;
281                }
282            }
283        }
284    }
285
286    // Fallback: read+write loop (256KB matches GNU cat's optimal buffer)
287    let stdin = io::stdin();
288    let mut reader = stdin.lock();
289    let mut buf = [0u8; 262144]; // 256KB buffer
290    loop {
291        let n = match reader.read(&mut buf) {
292            Ok(0) => break,
293            Ok(n) => n,
294            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
295            Err(e) => return Err(e),
296        };
297        out.write_all(&buf[..n])?;
298    }
299    Ok(())
300}
301
302/// Build the 256-byte lookup table for non-printing character display.
303/// Returns (table, needs_expansion) where needs_expansion[b] is true if
304/// the byte maps to more than one output byte.
305fn _build_nonprinting_table(show_tabs: bool) -> ([u8; 256], [bool; 256]) {
306    let mut table = [0u8; 256];
307    let mut multi = [false; 256];
308
309    for i in 0..256u16 {
310        let b = i as u8;
311        match b {
312            b'\n' => {
313                table[i as usize] = b'\n';
314            }
315            b'\t' => {
316                if show_tabs {
317                    table[i as usize] = b'I';
318                    multi[i as usize] = true;
319                } else {
320                    table[i as usize] = b'\t';
321                }
322            }
323            0..=8 | 10..=31 => {
324                // Control chars: ^@ through ^_
325                table[i as usize] = b + 64;
326                multi[i as usize] = true;
327            }
328            32..=126 => {
329                table[i as usize] = b;
330            }
331            127 => {
332                // DEL: ^?
333                table[i as usize] = b'?';
334                multi[i as usize] = true;
335            }
336            128..=159 => {
337                // M-^@ through M-^_
338                table[i as usize] = b - 128 + 64;
339                multi[i as usize] = true;
340            }
341            160..=254 => {
342                // M-space through M-~
343                table[i as usize] = b - 128;
344                multi[i as usize] = true;
345            }
346            255 => {
347                // M-^?
348                table[i as usize] = b'?';
349                multi[i as usize] = true;
350            }
351        }
352    }
353
354    (table, multi)
355}
356
357/// Write a non-printing byte in cat -v notation
358#[inline]
359fn write_nonprinting(b: u8, show_tabs: bool, out: &mut Vec<u8>) {
360    match b {
361        b'\t' if !show_tabs => out.push(b'\t'),
362        b'\n' => out.push(b'\n'),
363        0..=8 | 10..=31 => {
364            out.push(b'^');
365            out.push(b + 64);
366        }
367        9 => {
368            // show_tabs must be true here
369            out.push(b'^');
370            out.push(b'I');
371        }
372        32..=126 => out.push(b),
373        127 => {
374            out.push(b'^');
375            out.push(b'?');
376        }
377        128..=159 => {
378            out.push(b'M');
379            out.push(b'-');
380            out.push(b'^');
381            out.push(b - 128 + 64);
382        }
383        160..=254 => {
384            out.push(b'M');
385            out.push(b'-');
386            out.push(b - 128);
387        }
388        255 => {
389            out.push(b'M');
390            out.push(b'-');
391            out.push(b'^');
392            out.push(b'?');
393        }
394    }
395}
396
397/// Fast path for cat -A (show-all) without line numbering or squeeze.
398/// Uses an internal buffer with bulk memcpy of printable ASCII runs.
399fn cat_show_all_fast(
400    data: &[u8],
401    show_tabs: bool,
402    show_ends: bool,
403    out: &mut impl Write,
404) -> io::Result<()> {
405    // Internal buffer — flush every 256KB to keep memory bounded
406    const BUF_SIZE: usize = 256 * 1024;
407    // Worst case expansion: every byte → 4 chars (M-^X), so reserve proportionally
408    let cap = data.len().min(BUF_SIZE) + data.len().min(BUF_SIZE) / 2;
409    let mut buf = Vec::with_capacity(cap);
410    let mut pos = 0;
411
412    while pos < data.len() {
413        // Find the next byte that needs transformation (outside 32..=126)
414        let start = pos;
415        while pos < data.len() && data[pos].wrapping_sub(32) <= 94 {
416            pos += 1;
417        }
418        // Bulk copy printable ASCII run via memcpy
419        if pos > start {
420            buf.extend_from_slice(&data[start..pos]);
421        }
422        if pos >= data.len() {
423            break;
424        }
425        // Handle the special byte
426        let b = data[pos];
427        pos += 1;
428        match b {
429            b'\n' => {
430                if show_ends {
431                    buf.extend_from_slice(b"$\n");
432                } else {
433                    buf.push(b'\n');
434                }
435            }
436            b'\t' if show_tabs => buf.extend_from_slice(b"^I"),
437            b'\t' => buf.push(b'\t'),
438            0..=8 | 10..=31 => {
439                buf.push(b'^');
440                buf.push(b + 64);
441            }
442            127 => buf.extend_from_slice(b"^?"),
443            128..=159 => {
444                buf.push(b'M');
445                buf.push(b'-');
446                buf.push(b'^');
447                buf.push(b - 128 + 64);
448            }
449            160..=254 => {
450                buf.push(b'M');
451                buf.push(b'-');
452                buf.push(b - 128);
453            }
454            255 => buf.extend_from_slice(b"M-^?"),
455            _ => unreachable!(),
456        }
457
458        // Flush when buffer is large enough
459        if buf.len() >= BUF_SIZE {
460            out.write_all(&buf)?;
461            buf.clear();
462        }
463    }
464
465    if !buf.is_empty() {
466        out.write_all(&buf)?;
467    }
468    Ok(())
469}
470
471/// Write right-aligned line number (6-char field) + tab directly into buffer.
472/// Uses pre-computed digit tables to avoid itoa overhead per line.
473/// Returns the number of bytes written (always 7 for numbers up to 999999).
474#[inline(always)]
475unsafe fn write_line_number_raw(dst: *mut u8, num: u64) -> usize {
476    // GNU cat format: "%6d\t" — right-aligned in 6-char field + tab
477    if num <= 999999 {
478        // Fast path: fits in 6 digits (covers 99.99% of cases)
479        // Pre-compute all 6 digits at once using division
480        let mut n = num as u32;
481        let d5 = n / 100000;
482        n -= d5 * 100000;
483        let d4 = n / 10000;
484        n -= d4 * 10000;
485        let d3 = n / 1000;
486        n -= d3 * 1000;
487        let d2 = n / 100;
488        n -= d2 * 100;
489        let d1 = n / 10;
490        let d0 = n - d1 * 10;
491
492        // Determine leading spaces
493        let width = if num >= 100000 {
494            6
495        } else if num >= 10000 {
496            5
497        } else if num >= 1000 {
498            4
499        } else if num >= 100 {
500            3
501        } else if num >= 10 {
502            2
503        } else {
504            1
505        };
506        let pad = 6 - width;
507
508        // Write padding spaces
509        unsafe {
510            for i in 0..pad {
511                *dst.add(i) = b' ';
512            }
513        }
514
515        // Write digits (only the significant ones)
516        let digits = [
517            d5 as u8 + b'0',
518            d4 as u8 + b'0',
519            d3 as u8 + b'0',
520            d2 as u8 + b'0',
521            d1 as u8 + b'0',
522            d0 as u8 + b'0',
523        ];
524        unsafe {
525            std::ptr::copy_nonoverlapping(digits[6 - width..].as_ptr(), dst.add(pad), width);
526            *dst.add(6) = b'\t';
527        }
528        7
529    } else {
530        // Slow path: number > 999999 (use itoa)
531        let mut buf = itoa::Buffer::new();
532        let s = buf.format(num);
533        let pad = if s.len() < 6 { 6 - s.len() } else { 0 };
534        unsafe {
535            for i in 0..pad {
536                *dst.add(i) = b' ';
537            }
538            std::ptr::copy_nonoverlapping(s.as_ptr(), dst.add(pad), s.len());
539            *dst.add(pad + s.len()) = b'\t';
540        }
541        pad + s.len() + 1
542    }
543}
544
545/// Streaming cat -n/-b from a raw fd. Reads in 4MB chunks, finds the last
546/// newline in each chunk to split on line boundaries, and processes complete
547/// lines via cat_number_all_fast or cat_number_nonblank_fast. Partial line
548/// data at the end of a chunk is carried to the next read.
549/// This avoids loading the entire file into memory — peak RSS is ~12MB
550/// (4MB read buf + ~8MB output buf) instead of file_size * 2.
551#[cfg(target_os = "linux")]
552fn cat_stream_numbered(
553    fd: i32,
554    line_num: &mut u64,
555    nonblank: bool,
556    out: &mut impl Write,
557) -> io::Result<bool> {
558    const READ_BUF: usize = 4 * 1024 * 1024;
559    let mut buf = vec![0u8; READ_BUF];
560    let mut carry: Vec<u8> = Vec::new();
561
562    loop {
563        let n = loop {
564            let ret = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
565            if ret >= 0 {
566                break ret as usize;
567            }
568            let err = io::Error::last_os_error();
569            if err.kind() != io::ErrorKind::Interrupted {
570                return Err(err);
571            }
572        };
573        if n == 0 {
574            // EOF — process any remaining carry data
575            if !carry.is_empty() {
576                if nonblank {
577                    cat_number_nonblank_fast(&carry, line_num, out)?;
578                } else {
579                    cat_number_all_fast(&carry, line_num, out)?;
580                }
581            }
582            return Ok(true);
583        }
584
585        let chunk = &buf[..n];
586
587        if carry.is_empty() {
588            // No carry — find last newline to split on line boundary
589            match memchr::memrchr(b'\n', chunk) {
590                Some(last_nl) => {
591                    let complete = &chunk[..last_nl + 1];
592                    if nonblank {
593                        cat_number_nonblank_fast(complete, line_num, out)?;
594                    } else {
595                        cat_number_all_fast(complete, line_num, out)?;
596                    }
597                    // Save partial line after last newline
598                    if last_nl + 1 < n {
599                        carry.extend_from_slice(&chunk[last_nl + 1..]);
600                    }
601                }
602                None => {
603                    // No newline in entire chunk — save as carry
604                    carry.extend_from_slice(chunk);
605                }
606            }
607        } else {
608            // Have carry data — prepend to this chunk's first line
609            match memchr::memchr(b'\n', chunk) {
610                Some(first_nl) => {
611                    // Complete the carried line
612                    carry.extend_from_slice(&chunk[..first_nl + 1]);
613                    if nonblank {
614                        cat_number_nonblank_fast(&carry, line_num, out)?;
615                    } else {
616                        cat_number_all_fast(&carry, line_num, out)?;
617                    }
618                    carry.clear();
619
620                    // Process remaining complete lines
621                    let rest = &chunk[first_nl + 1..];
622                    if !rest.is_empty() {
623                        match memchr::memrchr(b'\n', rest) {
624                            Some(last_nl) => {
625                                let complete = &rest[..last_nl + 1];
626                                if nonblank {
627                                    cat_number_nonblank_fast(complete, line_num, out)?;
628                                } else {
629                                    cat_number_all_fast(complete, line_num, out)?;
630                                }
631                                if last_nl + 1 < rest.len() {
632                                    carry.extend_from_slice(&rest[last_nl + 1..]);
633                                }
634                            }
635                            None => {
636                                carry.extend_from_slice(rest);
637                            }
638                        }
639                    }
640                }
641                None => {
642                    // No newline — append to carry
643                    carry.extend_from_slice(chunk);
644                }
645            }
646        }
647    }
648}
649
650/// Ultra-fast path for cat -n (number all lines, no other options).
651/// Uses pre-formatted numbers with raw buffer writes to minimize per-line overhead.
652/// Single pass through data with memchr_iter for batched SIMD newline scanning.
653/// Pre-allocates generously (2x input) to avoid any capacity checks or reallocation.
654fn cat_number_all_fast(data: &[u8], line_num: &mut u64, out: &mut impl Write) -> io::Result<()> {
655    if data.is_empty() {
656        return Ok(());
657    }
658
659    // Pre-allocate ~2x input (each line gets ~8 byte number prefix).
660    // Average line ~80 chars → 8/80 = 10% overhead. 2x is very conservative.
661    // Use min with 64MB to avoid excessive allocation for huge files.
662    let alloc = (data.len() * 2 + 256).min(64 * 1024 * 1024);
663    let mut output: Vec<u8> = Vec::with_capacity(alloc);
664    let mut out_ptr = output.as_mut_ptr();
665    let mut out_pos: usize = 0;
666
667    let mut num = *line_num;
668    let mut pos: usize = 0;
669
670    for nl_pos in memchr::memchr_iter(b'\n', data) {
671        // Ensure capacity for number prefix + line content
672        let line_len = nl_pos + 1 - pos;
673        let needed = out_pos + line_len + 22; // max: 20 digits + 1 tab + 1 safety
674        if needed > output.capacity() {
675            unsafe { output.set_len(out_pos) };
676            output.reserve(needed.saturating_sub(output.len()));
677            out_ptr = output.as_mut_ptr();
678        }
679
680        // Write line number directly to output buffer
681        unsafe {
682            out_pos += write_line_number_raw(out_ptr.add(out_pos), num);
683        }
684        num += 1;
685
686        // Copy line content including newline
687        unsafe {
688            std::ptr::copy_nonoverlapping(data.as_ptr().add(pos), out_ptr.add(out_pos), line_len);
689        }
690        out_pos += line_len;
691        pos = nl_pos + 1;
692
693        // Flush periodically for very large files (keep working set in cache)
694        if out_pos >= 8 * 1024 * 1024 {
695            unsafe { output.set_len(out_pos) };
696            out.write_all(&output)?;
697            output.clear();
698            out_pos = 0;
699            out_ptr = output.as_mut_ptr();
700        }
701    }
702
703    // Handle final line without trailing newline
704    if pos < data.len() {
705        let remaining = data.len() - pos;
706        let needed = out_pos + remaining + 22;
707        if needed > output.capacity() {
708            unsafe { output.set_len(out_pos) };
709            output.reserve(needed.saturating_sub(output.len()));
710            out_ptr = output.as_mut_ptr();
711        }
712        unsafe {
713            out_pos += write_line_number_raw(out_ptr.add(out_pos), num);
714        }
715        num += 1;
716        unsafe {
717            std::ptr::copy_nonoverlapping(data.as_ptr().add(pos), out_ptr.add(out_pos), remaining);
718        }
719        out_pos += remaining;
720    }
721
722    *line_num = num;
723
724    unsafe { output.set_len(out_pos) };
725    if !output.is_empty() {
726        out.write_all(&output)?;
727    }
728
729    Ok(())
730}
731
732/// Ultra-fast path for cat -b (number non-blank lines, no other options).
733fn cat_number_nonblank_fast(
734    data: &[u8],
735    line_num: &mut u64,
736    out: &mut impl Write,
737) -> io::Result<()> {
738    if data.is_empty() {
739        return Ok(());
740    }
741
742    let alloc = (data.len() * 2 + 256).min(64 * 1024 * 1024);
743    let mut output: Vec<u8> = Vec::with_capacity(alloc);
744    let mut out_ptr = output.as_mut_ptr();
745    let mut out_pos: usize = 0;
746
747    let mut num = *line_num;
748    let mut pos: usize = 0;
749
750    for nl_pos in memchr::memchr_iter(b'\n', data) {
751        let line_len = nl_pos + 1 - pos;
752        let needed = out_pos + line_len + 22;
753        if needed > output.capacity() {
754            unsafe { output.set_len(out_pos) };
755            output.reserve(needed.saturating_sub(output.len()));
756            out_ptr = output.as_mut_ptr();
757        }
758
759        let is_blank = nl_pos == pos;
760        if !is_blank {
761            unsafe {
762                out_pos += write_line_number_raw(out_ptr.add(out_pos), num);
763            }
764            num += 1;
765        }
766
767        unsafe {
768            std::ptr::copy_nonoverlapping(data.as_ptr().add(pos), out_ptr.add(out_pos), line_len);
769        }
770        out_pos += line_len;
771        pos = nl_pos + 1;
772
773        if out_pos >= 8 * 1024 * 1024 {
774            unsafe { output.set_len(out_pos) };
775            out.write_all(&output)?;
776            output.clear();
777            out_pos = 0;
778            out_ptr = output.as_mut_ptr();
779        }
780    }
781
782    if pos < data.len() {
783        let remaining = data.len() - pos;
784        let needed = out_pos + remaining + 22;
785        if needed > output.capacity() {
786            unsafe { output.set_len(out_pos) };
787            output.reserve(needed.saturating_sub(output.len()));
788            out_ptr = output.as_mut_ptr();
789        }
790        unsafe {
791            out_pos += write_line_number_raw(out_ptr.add(out_pos), num);
792        }
793        num += 1;
794        unsafe {
795            std::ptr::copy_nonoverlapping(data.as_ptr().add(pos), out_ptr.add(out_pos), remaining);
796        }
797        out_pos += remaining;
798    }
799
800    *line_num = num;
801
802    unsafe { output.set_len(out_pos) };
803    if !output.is_empty() {
804        out.write_all(&output)?;
805    }
806
807    Ok(())
808}
809
810/// Cat with options (numbering, show-ends, show-tabs, show-nonprinting, squeeze)
811pub fn cat_with_options(
812    data: &[u8],
813    config: &CatConfig,
814    line_num: &mut u64,
815    pending_cr: &mut bool,
816    out: &mut impl Write,
817) -> io::Result<()> {
818    if data.is_empty() {
819        return Ok(());
820    }
821
822    // Fast path: show-all without numbering or squeeze
823    if config.show_nonprinting && !config.number && !config.number_nonblank && !config.squeeze_blank
824    {
825        return cat_show_all_fast(data, config.show_tabs, config.show_ends, out);
826    }
827
828    // Fast path: -n only (number all lines, no other processing)
829    if config.number
830        && !config.number_nonblank
831        && !config.show_ends
832        && !config.show_tabs
833        && !config.show_nonprinting
834        && !config.squeeze_blank
835        && !*pending_cr
836    {
837        return cat_number_all_fast(data, line_num, out);
838    }
839
840    // Fast path: -b only (number non-blank lines, no other processing)
841    if config.number_nonblank
842        && !config.number
843        && !config.show_ends
844        && !config.show_tabs
845        && !config.show_nonprinting
846        && !config.squeeze_blank
847        && !*pending_cr
848    {
849        return cat_number_nonblank_fast(data, line_num, out);
850    }
851
852    // Pre-allocate output buffer (worst case: every byte expands to 4 chars for M-^X)
853    // In practice, most files are mostly printable, so 1.1x is a good estimate
854    let estimated = data.len() + data.len() / 10 + 1024;
855    let mut buf = Vec::with_capacity(estimated.min(16 * 1024 * 1024));
856
857    let mut prev_blank = false;
858    let mut pos = 0;
859    let mut itoa_buf = itoa::Buffer::new();
860
861    // Handle pending CR from previous file (only relevant for show_ends without show_nonprinting)
862    if *pending_cr {
863        *pending_cr = false;
864        if config.show_ends
865            && !(config.show_nonprinting || config.show_tabs)
866            && !data.is_empty()
867            && data[0] == b'\n'
868        {
869            // CR from previous file + this LF = CRLF line ending → ^M$\n
870            buf.extend_from_slice(b"^M$\n");
871            pos = 1;
872        } else {
873            // CR not followed by LF, emit literally
874            buf.push(b'\r');
875        }
876    }
877
878    while pos < data.len() {
879        // Find end of this line
880        let line_end = memchr::memchr(b'\n', &data[pos..])
881            .map(|p| pos + p + 1)
882            .unwrap_or(data.len());
883
884        let line = &data[pos..line_end];
885        let is_blank = line == b"\n" || line.is_empty();
886
887        // Squeeze blank lines
888        if config.squeeze_blank && is_blank && prev_blank {
889            pos = line_end;
890            continue;
891        }
892        prev_blank = is_blank;
893
894        // Line numbering - use itoa for fast integer formatting
895        if config.number_nonblank {
896            if !is_blank {
897                let s = itoa_buf.format(*line_num);
898                // Right-align in 6-char field
899                let pad = if s.len() < 6 { 6 - s.len() } else { 0 };
900                buf.extend(std::iter::repeat_n(b' ', pad));
901                buf.extend_from_slice(s.as_bytes());
902                buf.push(b'\t');
903                *line_num += 1;
904            }
905        } else if config.number {
906            let s = itoa_buf.format(*line_num);
907            let pad = if s.len() < 6 { 6 - s.len() } else { 0 };
908            buf.extend(std::iter::repeat_n(b' ', pad));
909            buf.extend_from_slice(s.as_bytes());
910            buf.push(b'\t');
911            *line_num += 1;
912        }
913
914        // Process line content
915        if config.show_nonprinting || config.show_tabs {
916            let content_end = if line.last() == Some(&b'\n') {
917                line.len() - 1
918            } else {
919                line.len()
920            };
921
922            for &b in &line[..content_end] {
923                if config.show_nonprinting {
924                    write_nonprinting(b, config.show_tabs, &mut buf);
925                } else if config.show_tabs && b == b'\t' {
926                    buf.extend_from_slice(b"^I");
927                } else {
928                    buf.push(b);
929                }
930            }
931
932            if config.show_ends && line.last() == Some(&b'\n') {
933                buf.push(b'$');
934            }
935            if line.last() == Some(&b'\n') {
936                buf.push(b'\n');
937            }
938        } else {
939            // No character transformation needed
940            if config.show_ends {
941                let has_newline = line.last() == Some(&b'\n');
942                let content_end = if has_newline {
943                    line.len() - 1
944                } else {
945                    line.len()
946                };
947                // GNU cat -E: only \r immediately before \n is shown as ^M.
948                // Other \r bytes are passed through as literal CR (0x0d).
949                let content = &line[..content_end];
950                if has_newline && !content.is_empty() && content[content.len() - 1] == b'\r' {
951                    // Content ends with \r (which is right before \n) → show as ^M$
952                    buf.extend_from_slice(&content[..content.len() - 1]);
953                    buf.extend_from_slice(b"^M");
954                } else if !has_newline && !content.is_empty() && content[content.len() - 1] == b'\r'
955                {
956                    // Trailing CR at end of data without following LF — hold as pending.
957                    // It might pair with next file's LF to form CRLF line ending.
958                    buf.extend_from_slice(&content[..content.len() - 1]);
959                    *pending_cr = true;
960                } else {
961                    buf.extend_from_slice(content);
962                }
963                if has_newline {
964                    buf.push(b'$');
965                    buf.push(b'\n');
966                }
967            } else {
968                buf.extend_from_slice(line);
969            }
970        }
971
972        // Flush buffer periodically to avoid excessive memory use
973        if buf.len() >= 8 * 1024 * 1024 {
974            out.write_all(&buf)?;
975            buf.clear();
976        }
977
978        pos = line_end;
979    }
980
981    if !buf.is_empty() {
982        out.write_all(&buf)?;
983    }
984
985    Ok(())
986}
987
988/// Process a single file for cat
989pub fn cat_file(
990    filename: &str,
991    config: &CatConfig,
992    line_num: &mut u64,
993    pending_cr: &mut bool,
994    out: &mut impl Write,
995    tool_name: &str,
996) -> io::Result<bool> {
997    if filename == "-" {
998        if config.is_plain() {
999            match cat_plain_stdin(out) {
1000                Ok(()) => return Ok(true),
1001                Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
1002                    std::process::exit(0);
1003                }
1004                Err(e) => {
1005                    eprintln!(
1006                        "{}: standard input: {}",
1007                        tool_name,
1008                        crate::common::io_error_msg(&e)
1009                    );
1010                    return Ok(false);
1011                }
1012            }
1013        }
1014        match read_stdin() {
1015            Ok(data) => {
1016                cat_with_options(&data, config, line_num, pending_cr, out)?;
1017                Ok(true)
1018            }
1019            Err(e) => {
1020                eprintln!(
1021                    "{}: standard input: {}",
1022                    tool_name,
1023                    crate::common::io_error_msg(&e)
1024                );
1025                Ok(false)
1026            }
1027        }
1028    } else {
1029        let path = Path::new(filename);
1030
1031        if config.is_plain() {
1032            // On Linux, cat_plain_file_linux handles directory check and same-file
1033            // detection inside its own fstat calls, avoiding redundant syscalls.
1034            #[cfg(target_os = "linux")]
1035            {
1036                match cat_plain_file_linux(path) {
1037                    Ok(true) => return Ok(true),
1038                    Ok(false) => {
1039                        // Fallback to generic path — still need dir/same-file checks
1040                        // since the generic path doesn't do them.
1041                    }
1042                    Err(CatPlainError::IsDirectory) => {
1043                        eprintln!("{}: {}: Is a directory", tool_name, filename);
1044                        return Ok(false);
1045                    }
1046                    Err(CatPlainError::InputIsOutput) => {
1047                        eprintln!("{}: {}: input file is output file", tool_name, filename);
1048                        return Ok(false);
1049                    }
1050                    Err(CatPlainError::Io(e)) if e.kind() == io::ErrorKind::BrokenPipe => {
1051                        std::process::exit(0);
1052                    }
1053                    Err(CatPlainError::Io(e)) => {
1054                        eprintln!(
1055                            "{}: {}: {}",
1056                            tool_name,
1057                            filename,
1058                            crate::common::io_error_msg(&e)
1059                        );
1060                        return Ok(false);
1061                    }
1062                }
1063            }
1064
1065            // Non-Linux plain path or Linux fallback (Ok(false) from above)
1066            // Need to do directory/same-file checks here for the generic fallback.
1067            // Hoist single metadata call for both directory check and same-file detection.
1068            if let Ok(file_meta) = std::fs::metadata(path) {
1069                if file_meta.is_dir() {
1070                    eprintln!("{}: {}: Is a directory", tool_name, filename);
1071                    return Ok(false);
1072                }
1073
1074                #[cfg(unix)]
1075                {
1076                    use std::os::unix::fs::MetadataExt;
1077                    let mut stdout_stat: libc::stat = unsafe { std::mem::zeroed() };
1078                    if unsafe { libc::fstat(1, &mut stdout_stat) } == 0
1079                        && file_meta.dev() == stdout_stat.st_dev as u64
1080                        && file_meta.ino() == stdout_stat.st_ino as u64
1081                    {
1082                        eprintln!("{}: {}: input file is output file", tool_name, filename);
1083                        return Ok(false);
1084                    }
1085                }
1086            }
1087
1088            // Generic fallback: read file + write
1089            match read_file_direct(path) {
1090                Ok(data) => {
1091                    if !data.is_empty() {
1092                        out.write_all(&data)?;
1093                    }
1094                    return Ok(true);
1095                }
1096                Err(e) => {
1097                    eprintln!(
1098                        "{}: {}: {}",
1099                        tool_name,
1100                        filename,
1101                        crate::common::io_error_msg(&e)
1102                    );
1103                    return Ok(false);
1104                }
1105            }
1106        }
1107
1108        // Non-plain path: need directory check and same-file detection before processing
1109        // On Linux, use fstat on opened fd to avoid redundant metadata calls, then
1110        // stream cat -n/-b directly from fd for zero-copy I/O.
1111        #[cfg(target_os = "linux")]
1112        {
1113            use std::os::unix::io::AsRawFd;
1114            if let Ok(file) = crate::common::io::open_noatime(path) {
1115                let fd = file.as_raw_fd();
1116                let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1117                if unsafe { libc::fstat(fd, &mut stat) } == 0 {
1118                    // Directory check
1119                    if (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR {
1120                        eprintln!("{}: {}: Is a directory", tool_name, filename);
1121                        return Ok(false);
1122                    }
1123                    // Same-file detection
1124                    let mut stdout_stat: libc::stat = unsafe { std::mem::zeroed() };
1125                    if unsafe { libc::fstat(1, &mut stdout_stat) } == 0
1126                        && stat.st_dev == stdout_stat.st_dev
1127                        && stat.st_ino == stdout_stat.st_ino
1128                    {
1129                        eprintln!("{}: {}: input file is output file", tool_name, filename);
1130                        return Ok(false);
1131                    }
1132                }
1133
1134                // Streaming path for cat -n (number all lines only)
1135                if config.number
1136                    && !config.number_nonblank
1137                    && !config.show_ends
1138                    && !config.show_tabs
1139                    && !config.show_nonprinting
1140                    && !config.squeeze_blank
1141                    && !*pending_cr
1142                {
1143                    unsafe {
1144                        libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
1145                    }
1146                    return cat_stream_numbered(fd, line_num, false, out);
1147                }
1148
1149                // Streaming path for cat -b (number non-blank only)
1150                if config.number_nonblank
1151                    && !config.number
1152                    && !config.show_ends
1153                    && !config.show_tabs
1154                    && !config.show_nonprinting
1155                    && !config.squeeze_blank
1156                    && !*pending_cr
1157                {
1158                    unsafe {
1159                        libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL);
1160                    }
1161                    return cat_stream_numbered(fd, line_num, true, out);
1162                }
1163
1164                // Other options: read entire file via fd, then process
1165                // Reuse stat from fstat above (already populated)
1166                let size = if stat.st_size > 0 {
1167                    stat.st_size as usize
1168                } else {
1169                    0
1170                };
1171                let mut data = Vec::with_capacity(size);
1172                use std::io::Read;
1173                if (&file).read_to_end(&mut data).is_ok() {
1174                    cat_with_options(&data, config, line_num, pending_cr, out)?;
1175                    return Ok(true);
1176                }
1177                // read failed, fall through to read_file_direct
1178            }
1179        }
1180
1181        // Non-Linux path or Linux fallback
1182        // Check if it's a directory
1183        match std::fs::metadata(path) {
1184            Ok(meta) if meta.is_dir() => {
1185                eprintln!("{}: {}: Is a directory", tool_name, filename);
1186                return Ok(false);
1187            }
1188            _ => {}
1189        }
1190
1191        #[cfg(unix)]
1192        {
1193            use std::os::unix::fs::MetadataExt;
1194            if let Ok(file_meta) = std::fs::metadata(path) {
1195                let mut stdout_stat: libc::stat = unsafe { std::mem::zeroed() };
1196                if unsafe { libc::fstat(1, &mut stdout_stat) } == 0
1197                    && file_meta.dev() == stdout_stat.st_dev as u64
1198                    && file_meta.ino() == stdout_stat.st_ino as u64
1199                {
1200                    eprintln!("{}: {}: input file is output file", tool_name, filename);
1201                    return Ok(false);
1202                }
1203            }
1204        }
1205
1206        match read_file_direct(path) {
1207            Ok(data) => {
1208                cat_with_options(&data, config, line_num, pending_cr, out)?;
1209                Ok(true)
1210            }
1211            Err(e) => {
1212                eprintln!(
1213                    "{}: {}: {}",
1214                    tool_name,
1215                    filename,
1216                    crate::common::io_error_msg(&e)
1217                );
1218                Ok(false)
1219            }
1220        }
1221    }
1222}