Skip to main content

coreutils_rs/tail/
core.rs

1use std::io::{self, Read, Seek, Write};
2use std::path::Path;
3
4use memchr::{memchr_iter, memrchr_iter};
5
6use crate::common::io::{FileData, read_file, read_stdin};
7
8/// Open a file with O_NOATIME on Linux, falling back if not permitted.
9#[cfg(target_os = "linux")]
10fn open_noatime(path: &Path) -> io::Result<std::fs::File> {
11    use std::os::unix::fs::OpenOptionsExt;
12    std::fs::OpenOptions::new()
13        .read(true)
14        .custom_flags(libc::O_NOATIME)
15        .open(path)
16        .or_else(|_| std::fs::File::open(path))
17}
18
19/// Public wrapper for the binary fast path to open files with O_NOATIME.
20#[cfg(target_os = "linux")]
21pub fn open_file_noatime(path: &Path) -> io::Result<std::fs::File> {
22    open_noatime(path)
23}
24
25/// Raw write(2) to a file descriptor, retrying on EINTR.
26#[cfg(target_os = "linux")]
27fn write_all_fd(fd: i32, mut data: &[u8]) -> io::Result<()> {
28    while !data.is_empty() {
29        let ret = unsafe { libc::write(fd, data.as_ptr() as *const libc::c_void, data.len()) };
30        if ret > 0 {
31            data = &data[ret as usize..];
32        } else if ret == 0 {
33            return Err(io::Error::new(io::ErrorKind::WriteZero, "write returned 0"));
34        } else {
35            let err = io::Error::last_os_error();
36            if err.kind() == io::ErrorKind::Interrupted {
37                continue;
38            }
39            return Err(err);
40        }
41    }
42    Ok(())
43}
44
45/// Scan backward from EOF to find the byte offset where the last N delimited
46/// lines begin. Returns 0 when the file has fewer than N lines (output all).
47/// Platform-agnostic -- tested on all CI targets.
48///
49/// Uses a stack buffer for files <= 8KB (covers most small-file benchmarks)
50/// and falls back to a heap-allocated 256KB buffer for larger files.
51fn find_tail_start_byte(
52    reader: &mut (impl Read + Seek),
53    file_size: u64,
54    n: u64,
55    delimiter: u8,
56) -> io::Result<u64> {
57    // For small files, use a stack buffer to avoid heap allocation entirely.
58    if file_size <= 8192 {
59        let mut stack_buf = [0u8; 8192];
60        return find_tail_start_byte_inner(reader, file_size, n, delimiter, &mut stack_buf);
61    }
62
63    const CHUNK: usize = 262144;
64    let mut buf = vec![0u8; CHUNK];
65    find_tail_start_byte_inner(reader, file_size, n, delimiter, &mut buf)
66}
67
68/// Inner implementation of backward scanning, parameterized over buffer.
69fn find_tail_start_byte_inner(
70    reader: &mut (impl Read + Seek),
71    file_size: u64,
72    n: u64,
73    delimiter: u8,
74    buf: &mut [u8],
75) -> io::Result<u64> {
76    let chunk_size = buf.len() as u64;
77    let mut pos = file_size;
78    let mut count = 0u64;
79
80    while pos > 0 {
81        let read_start = if pos > chunk_size {
82            pos - chunk_size
83        } else {
84            0
85        };
86        let read_len = (pos - read_start) as usize;
87
88        reader.seek(io::SeekFrom::Start(read_start))?;
89        reader.read_exact(&mut buf[..read_len])?;
90
91        // Skip trailing delimiter (don't count the file's final newline)
92        let search_end = if pos == file_size && read_len > 0 && buf[read_len - 1] == delimiter {
93            read_len - 1
94        } else {
95            read_len
96        };
97
98        for rpos in memrchr_iter(delimiter, &buf[..search_end]) {
99            count += 1;
100            if count == n {
101                return Ok(read_start + rpos as u64 + 1);
102            }
103        }
104
105        pos = read_start;
106    }
107
108    Ok(0)
109}
110
111/// Mode for tail operation
112#[derive(Clone, Debug)]
113pub enum TailMode {
114    /// Last N lines (default: 10)
115    Lines(u64),
116    /// Starting from line N (1-indexed)
117    LinesFrom(u64),
118    /// Last N bytes
119    Bytes(u64),
120    /// Starting from byte N (1-indexed)
121    BytesFrom(u64),
122}
123
124/// Follow mode
125#[derive(Clone, Debug, PartialEq)]
126pub enum FollowMode {
127    None,
128    Descriptor,
129    Name,
130}
131
132/// Configuration for tail
133#[derive(Clone, Debug)]
134pub struct TailConfig {
135    pub mode: TailMode,
136    pub follow: FollowMode,
137    pub retry: bool,
138    pub pid: Option<u32>,
139    pub sleep_interval: f64,
140    pub max_unchanged_stats: u64,
141    pub zero_terminated: bool,
142}
143
144impl Default for TailConfig {
145    fn default() -> Self {
146        Self {
147            mode: TailMode::Lines(10),
148            follow: FollowMode::None,
149            retry: false,
150            pid: None,
151            sleep_interval: 1.0,
152            max_unchanged_stats: 5,
153            zero_terminated: false,
154        }
155    }
156}
157
158/// Parse a numeric argument with optional suffix, same as head
159pub fn parse_size(s: &str) -> Result<u64, String> {
160    crate::head::parse_size(s)
161}
162
163/// Output last N lines from data using backward SIMD scanning
164pub fn tail_lines(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
165    if n == 0 || data.is_empty() {
166        return Ok(());
167    }
168
169    // Use memrchr for backward scanning - SIMD accelerated
170    let mut count = 0u64;
171
172    // Check if data ends with delimiter - if so, skip the trailing one
173    let search_end = if !data.is_empty() && data[data.len() - 1] == delimiter {
174        data.len() - 1
175    } else {
176        data.len()
177    };
178
179    for pos in memrchr_iter(delimiter, &data[..search_end]) {
180        count += 1;
181        if count == n {
182            return out.write_all(&data[pos + 1..]);
183        }
184    }
185
186    // Fewer than N lines — output everything
187    out.write_all(data)
188}
189
190/// Output from line N onward (1-indexed)
191pub fn tail_lines_from(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
192    if data.is_empty() {
193        return Ok(());
194    }
195
196    if n <= 1 {
197        return out.write_all(data);
198    }
199
200    // Skip first (n-1) lines
201    let skip = n - 1;
202    let mut count = 0u64;
203
204    for pos in memchr_iter(delimiter, data) {
205        count += 1;
206        if count == skip {
207            let start = pos + 1;
208            if start < data.len() {
209                return out.write_all(&data[start..]);
210            }
211            return Ok(());
212        }
213    }
214
215    // Fewer than N lines — output nothing
216    Ok(())
217}
218
219/// Output last N bytes from data
220pub fn tail_bytes(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
221    if n == 0 || data.is_empty() {
222        return Ok(());
223    }
224
225    let n = n.min(data.len() as u64) as usize;
226    out.write_all(&data[data.len() - n..])
227}
228
229/// Output from byte N onward (1-indexed)
230pub fn tail_bytes_from(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
231    if data.is_empty() {
232        return Ok(());
233    }
234
235    if n <= 1 {
236        return out.write_all(data);
237    }
238
239    let start = ((n - 1) as usize).min(data.len());
240    if start < data.len() {
241        out.write_all(&data[start..])
242    } else {
243        Ok(())
244    }
245}
246
247/// Use sendfile for zero-copy byte output on Linux (last N bytes).
248/// Falls back to read+write if sendfile fails (e.g., stdout is a terminal).
249#[cfg(target_os = "linux")]
250pub fn sendfile_tail_bytes(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
251    let file = open_noatime(path)?;
252
253    let metadata = file.metadata()?;
254    let file_size = metadata.len();
255
256    if file_size == 0 {
257        return Ok(true);
258    }
259
260    let n = n.min(file_size);
261    let start = file_size - n;
262
263    use std::os::unix::io::AsRawFd;
264    let in_fd = file.as_raw_fd();
265    let _ = unsafe {
266        libc::posix_fadvise(
267            in_fd,
268            start as libc::off_t,
269            n as libc::off_t,
270            libc::POSIX_FADV_SEQUENTIAL,
271        )
272    };
273    let mut offset: libc::off_t = start as libc::off_t;
274    let mut remaining = n;
275    let total = n;
276
277    while remaining > 0 {
278        let chunk = remaining.min(0x7fff_f000) as usize;
279        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
280        if ret > 0 {
281            remaining -= ret as u64;
282        } else if ret == 0 {
283            break;
284        } else {
285            let err = io::Error::last_os_error();
286            if err.kind() == io::ErrorKind::Interrupted {
287                continue;
288            }
289            // sendfile fails with EINVAL for terminal fds; fall back to read+write
290            if err.raw_os_error() == Some(libc::EINVAL) && remaining == total {
291                let mut file = file;
292                file.seek(io::SeekFrom::Start(start))?;
293                let mut buf = [0u8; 65536];
294                let mut left = n as usize;
295                while left > 0 {
296                    let to_read = left.min(buf.len());
297                    let nr = match file.read(&mut buf[..to_read]) {
298                        Ok(0) => break,
299                        Ok(nr) => nr,
300                        Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
301                        Err(e) => return Err(e),
302                    };
303                    write_all_fd(out_fd, &buf[..nr])?;
304                    left -= nr;
305                }
306                return Ok(true);
307            }
308            return Err(err);
309        }
310    }
311
312    Ok(true)
313}
314
315/// Streaming tail -n N via sendfile on Linux. Caller opens the file so that
316/// open errors can be reported as "cannot open" and I/O errors as "error reading".
317#[cfg(target_os = "linux")]
318fn sendfile_tail_lines(
319    file: std::fs::File,
320    file_size: u64,
321    n: u64,
322    delimiter: u8,
323    out_fd: i32,
324) -> io::Result<bool> {
325    use std::os::unix::io::AsRawFd;
326
327    if n == 0 || file_size == 0 {
328        return Ok(true);
329    }
330
331    let in_fd = file.as_raw_fd();
332
333    // Disable forward readahead — we scan backward from EOF
334    let _ = unsafe { libc::posix_fadvise(in_fd, 0, 0, libc::POSIX_FADV_RANDOM) };
335
336    let mut reader = file;
337    let start_byte = find_tail_start_byte(&mut reader, file_size, n, delimiter)?;
338
339    // Enable forward readahead from the output start point
340    let remaining = file_size - start_byte;
341    let _ = unsafe {
342        libc::posix_fadvise(
343            in_fd,
344            start_byte as libc::off_t,
345            remaining as libc::off_t,
346            libc::POSIX_FADV_SEQUENTIAL,
347        )
348    };
349
350    // Try zero-copy output via sendfile first
351    let mut offset = start_byte as libc::off_t;
352    let mut left = remaining;
353    while left > 0 {
354        let chunk = left.min(0x7fff_f000) as usize;
355        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
356        if ret > 0 {
357            left -= ret as u64;
358        } else if ret == 0 {
359            break;
360        } else {
361            let err = io::Error::last_os_error();
362            if err.kind() == io::ErrorKind::Interrupted {
363                continue;
364            }
365            // sendfile fails with EINVAL for terminal fds; fall back to read+write
366            if err.raw_os_error() == Some(libc::EINVAL) && left == remaining {
367                reader.seek(io::SeekFrom::Start(start_byte))?;
368                let mut buf = [0u8; 65536];
369                loop {
370                    let n = match reader.read(&mut buf) {
371                        Ok(0) => break,
372                        Ok(n) => n,
373                        Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
374                        Err(e) => return Err(e),
375                    };
376                    write_all_fd(out_fd, &buf[..n])?;
377                }
378                return Ok(true);
379            }
380            return Err(err);
381        }
382    }
383
384    Ok(true)
385}
386
387/// Ultra-fast direct tail -n N for a single file. Bypasses BufWriter entirely.
388/// For small files, reads into a stack buffer and uses memrchr to find the
389/// last N lines, then writes via raw write(2). For larger files, uses the
390/// seek+sendfile path with automatic terminal fallback.
391#[cfg(target_os = "linux")]
392pub fn tail_file_direct(filename: &str, n: u64, delimiter: u8) -> io::Result<bool> {
393    use std::os::unix::fs::OpenOptionsExt;
394
395    if n == 0 {
396        return Ok(true);
397    }
398
399    let path = Path::new(filename);
400
401    let file = std::fs::OpenOptions::new()
402        .read(true)
403        .custom_flags(libc::O_NOATIME)
404        .open(path)
405        .or_else(|_| std::fs::File::open(path));
406    let mut file = match file {
407        Ok(f) => f,
408        Err(e) => {
409            eprintln!(
410                "tail: cannot open '{}' for reading: {}",
411                filename,
412                crate::common::io_error_msg(&e)
413            );
414            return Ok(false);
415        }
416    };
417
418    let file_size = file.metadata()?.len();
419    if file_size == 0 {
420        return Ok(true);
421    }
422
423    // For small files (<=64KB), read entirely into a stack buffer and scan backward.
424    // This avoids seek syscalls and works on any stdout (terminal, pipe, file).
425    if file_size <= 65536 {
426        let mut buf = [0u8; 65536];
427        let buf = &mut buf[..file_size as usize];
428        let mut total_read = 0;
429        while total_read < buf.len() {
430            match file.read(&mut buf[total_read..]) {
431                Ok(0) => break,
432                Ok(nr) => total_read += nr,
433                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
434                Err(e) => return Err(e),
435            }
436        }
437        let data = &buf[..total_read];
438        // Find start of last N lines using memrchr
439        let mut count = 0u64;
440        let search_end = if !data.is_empty() && data[data.len() - 1] == delimiter {
441            data.len() - 1
442        } else {
443            data.len()
444        };
445        for rpos in memrchr_iter(delimiter, &data[..search_end]) {
446            count += 1;
447            if count == n {
448                write_all_fd(1, &data[rpos + 1..])?;
449                return Ok(true);
450            }
451        }
452        // Fewer than N lines -- output everything
453        write_all_fd(1, data)?;
454        return Ok(true);
455    }
456
457    // For larger files, use the seek+sendfile path
458    use std::os::unix::io::AsRawFd;
459    let in_fd = file.as_raw_fd();
460    let _ = unsafe { libc::posix_fadvise(in_fd, 0, 0, libc::POSIX_FADV_RANDOM) };
461
462    let start_byte = find_tail_start_byte(&mut file, file_size, n, delimiter)?;
463    let remaining = file_size - start_byte;
464
465    let _ = unsafe {
466        libc::posix_fadvise(
467            in_fd,
468            start_byte as libc::off_t,
469            remaining as libc::off_t,
470            libc::POSIX_FADV_SEQUENTIAL,
471        )
472    };
473
474    // Try sendfile, fall back to read+write for terminals
475    let mut sf_offset = start_byte as libc::off_t;
476    let mut sf_left = remaining;
477    let sf_total = remaining;
478    while sf_left > 0 {
479        let chunk = sf_left.min(0x7fff_f000) as usize;
480        let ret = unsafe { libc::sendfile(1, in_fd, &mut sf_offset, chunk) };
481        if ret > 0 {
482            sf_left -= ret as u64;
483        } else if ret == 0 {
484            break;
485        } else {
486            let err = io::Error::last_os_error();
487            if err.kind() == io::ErrorKind::Interrupted {
488                continue;
489            }
490            if err.raw_os_error() == Some(libc::EINVAL) && sf_left == sf_total {
491                file.seek(io::SeekFrom::Start(start_byte))?;
492                let mut rbuf = [0u8; 65536];
493                loop {
494                    let nr = match file.read(&mut rbuf) {
495                        Ok(0) => break,
496                        Ok(nr) => nr,
497                        Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
498                        Err(e) => return Err(e),
499                    };
500                    write_all_fd(1, &rbuf[..nr])?;
501                }
502                return Ok(true);
503            }
504            return Err(err);
505        }
506    }
507
508    Ok(true)
509}
510
511/// Ultra-fast direct tail -c N for a single file. Bypasses BufWriter entirely.
512#[cfg(target_os = "linux")]
513pub fn tail_file_bytes_direct(filename: &str, n: u64) -> io::Result<bool> {
514    use std::os::unix::fs::OpenOptionsExt;
515
516    if n == 0 {
517        return Ok(true);
518    }
519
520    let path = Path::new(filename);
521
522    let file = std::fs::OpenOptions::new()
523        .read(true)
524        .custom_flags(libc::O_NOATIME)
525        .open(path)
526        .or_else(|_| std::fs::File::open(path));
527    let mut file = match file {
528        Ok(f) => f,
529        Err(e) => {
530            eprintln!(
531                "tail: cannot open '{}' for reading: {}",
532                filename,
533                crate::common::io_error_msg(&e)
534            );
535            return Ok(false);
536        }
537    };
538
539    let file_size = file.metadata()?.len();
540    if file_size == 0 {
541        return Ok(true);
542    }
543
544    let n = n.min(file_size);
545    let start = file_size - n;
546
547    // For small outputs, read directly and write via raw fd
548    if n <= 65536 {
549        file.seek(io::SeekFrom::Start(start))?;
550        let mut buf = [0u8; 65536];
551        let buf = &mut buf[..n as usize];
552        let mut total_read = 0;
553        while total_read < buf.len() {
554            match file.read(&mut buf[total_read..]) {
555                Ok(0) => break,
556                Ok(nr) => total_read += nr,
557                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
558                Err(e) => return Err(e),
559            }
560        }
561        write_all_fd(1, &buf[..total_read])?;
562        return Ok(true);
563    }
564
565    // For larger reads, try sendfile with terminal fallback
566    use std::os::unix::io::AsRawFd;
567    let in_fd = file.as_raw_fd();
568    let _ = unsafe {
569        libc::posix_fadvise(
570            in_fd,
571            start as libc::off_t,
572            n as libc::off_t,
573            libc::POSIX_FADV_SEQUENTIAL,
574        )
575    };
576
577    let mut sf_offset = start as libc::off_t;
578    let mut sf_remaining = n;
579    let sf_total = n;
580    while sf_remaining > 0 {
581        let chunk = sf_remaining.min(0x7fff_f000) as usize;
582        let ret = unsafe { libc::sendfile(1, in_fd, &mut sf_offset, chunk) };
583        if ret > 0 {
584            sf_remaining -= ret as u64;
585        } else if ret == 0 {
586            break;
587        } else {
588            let err = io::Error::last_os_error();
589            if err.kind() == io::ErrorKind::Interrupted {
590                continue;
591            }
592            if err.raw_os_error() == Some(libc::EINVAL) && sf_remaining == sf_total {
593                file.seek(io::SeekFrom::Start(start))?;
594                let mut rbuf = [0u8; 65536];
595                let mut left = n as usize;
596                while left > 0 {
597                    let to_read = left.min(rbuf.len());
598                    let nr = match file.read(&mut rbuf[..to_read]) {
599                        Ok(0) => break,
600                        Ok(nr) => nr,
601                        Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
602                        Err(e) => return Err(e),
603                    };
604                    write_all_fd(1, &rbuf[..nr])?;
605                    left -= nr;
606                }
607                return Ok(true);
608            }
609            return Err(err);
610        }
611    }
612
613    Ok(true)
614}
615
616/// Streaming tail -n N for regular files: read backward from EOF, then
617/// seek forward and copy. Caller opens the file. Used on non-Linux platforms.
618#[cfg(not(target_os = "linux"))]
619fn tail_lines_streaming_file(
620    mut file: std::fs::File,
621    file_size: u64,
622    n: u64,
623    delimiter: u8,
624    out: &mut impl Write,
625) -> io::Result<bool> {
626    if n == 0 || file_size == 0 {
627        return Ok(true);
628    }
629
630    let start_byte = find_tail_start_byte(&mut file, file_size, n, delimiter)?;
631    file.seek(io::SeekFrom::Start(start_byte))?;
632    io::copy(&mut file, out)?;
633
634    Ok(true)
635}
636
637/// Streaming tail -n +N for regular files: skip N-1 lines from start.
638/// Caller opens the file.
639///
640/// **Precondition**: On Linux, the `n <= 1` path uses `sendfile` which writes
641/// directly to stdout (bypassing `out`). The caller MUST `out.flush()` before
642/// calling this function to avoid interleaved output.
643fn tail_lines_from_streaming_file(
644    file: std::fs::File,
645    n: u64,
646    delimiter: u8,
647    out: &mut impl Write,
648) -> io::Result<bool> {
649    if n <= 1 {
650        // Output entire file via sendfile
651        #[cfg(target_os = "linux")]
652        {
653            use std::os::unix::io::AsRawFd;
654            let in_fd = file.as_raw_fd();
655            let stdout = io::stdout();
656            let out_fd = stdout.as_raw_fd();
657            let file_size = file.metadata()?.len();
658            return sendfile_to_stdout_raw(in_fd, file_size, out_fd);
659        }
660        #[cfg(not(target_os = "linux"))]
661        {
662            let mut reader = io::BufReader::with_capacity(1024 * 1024, file);
663            let mut buf = [0u8; 262144];
664            loop {
665                let n = match reader.read(&mut buf) {
666                    Ok(0) => break,
667                    Ok(n) => n,
668                    Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
669                    Err(e) => return Err(e),
670                };
671                out.write_all(&buf[..n])?;
672            }
673            return Ok(true);
674        }
675    }
676
677    let skip = n - 1;
678    let mut reader = io::BufReader::with_capacity(1024 * 1024, file);
679    let mut buf = [0u8; 262144];
680    let mut count = 0u64;
681    let mut skipping = true;
682
683    loop {
684        let bytes_read = match reader.read(&mut buf) {
685            Ok(0) => break,
686            Ok(n) => n,
687            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
688            Err(e) => return Err(e),
689        };
690
691        let chunk = &buf[..bytes_read];
692
693        if skipping {
694            for pos in memchr_iter(delimiter, chunk) {
695                count += 1;
696                if count == skip {
697                    // Found the start — output rest of this chunk and stop skipping
698                    let start = pos + 1;
699                    if start < chunk.len() {
700                        out.write_all(&chunk[start..])?;
701                    }
702                    skipping = false;
703                    break;
704                }
705            }
706        } else {
707            out.write_all(chunk)?;
708        }
709    }
710
711    Ok(true)
712}
713
714/// Raw sendfile helper
715#[cfg(target_os = "linux")]
716fn sendfile_to_stdout_raw(in_fd: i32, file_size: u64, out_fd: i32) -> io::Result<bool> {
717    let mut offset: libc::off_t = 0;
718    let mut remaining = file_size;
719    while remaining > 0 {
720        let chunk = remaining.min(0x7fff_f000) as usize;
721        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
722        if ret > 0 {
723            remaining -= ret as u64;
724        } else if ret == 0 {
725            break;
726        } else {
727            let err = io::Error::last_os_error();
728            if err.kind() == io::ErrorKind::Interrupted {
729                continue;
730            }
731            return Err(err);
732        }
733    }
734    Ok(true)
735}
736
737/// Process a single file/stdin for tail.
738///
739/// On Linux, the sendfile fast paths bypass `out` and write directly to stdout
740/// (fd 1). Callers MUST ensure `out` wraps stdout when these paths are active.
741/// The `out.flush()` call drains any buffered data before sendfile takes over.
742pub fn tail_file(
743    filename: &str,
744    config: &TailConfig,
745    out: &mut impl Write,
746    tool_name: &str,
747) -> io::Result<bool> {
748    let delimiter = if config.zero_terminated { b'\0' } else { b'\n' };
749
750    if filename != "-" {
751        let path = Path::new(filename);
752
753        match &config.mode {
754            TailMode::Lines(n) => {
755                // Open the file first so open errors get the right message
756                #[cfg(target_os = "linux")]
757                let file = match open_noatime(path) {
758                    Ok(f) => f,
759                    Err(e) => {
760                        eprintln!(
761                            "{}: cannot open '{}' for reading: {}",
762                            tool_name,
763                            filename,
764                            crate::common::io_error_msg(&e)
765                        );
766                        return Ok(false);
767                    }
768                };
769                #[cfg(not(target_os = "linux"))]
770                let file = match std::fs::File::open(path) {
771                    Ok(f) => f,
772                    Err(e) => {
773                        eprintln!(
774                            "{}: cannot open '{}' for reading: {}",
775                            tool_name,
776                            filename,
777                            crate::common::io_error_msg(&e)
778                        );
779                        return Ok(false);
780                    }
781                };
782                let file_size = match file.metadata() {
783                    Ok(m) => m.len(),
784                    Err(e) => {
785                        eprintln!(
786                            "{}: error reading '{}': {}",
787                            tool_name,
788                            filename,
789                            crate::common::io_error_msg(&e)
790                        );
791                        return Ok(false);
792                    }
793                };
794                #[cfg(target_os = "linux")]
795                {
796                    use std::os::unix::io::AsRawFd;
797                    out.flush()?;
798                    let stdout = io::stdout();
799                    let out_fd = stdout.as_raw_fd();
800                    match sendfile_tail_lines(file, file_size, *n, delimiter, out_fd) {
801                        Ok(_) => return Ok(true),
802                        Err(e) => {
803                            eprintln!(
804                                "{}: error reading '{}': {}",
805                                tool_name,
806                                filename,
807                                crate::common::io_error_msg(&e)
808                            );
809                            return Ok(false);
810                        }
811                    }
812                }
813                #[cfg(not(target_os = "linux"))]
814                {
815                    match tail_lines_streaming_file(file, file_size, *n, delimiter, out) {
816                        Ok(_) => return Ok(true),
817                        Err(e) => {
818                            eprintln!(
819                                "{}: error reading '{}': {}",
820                                tool_name,
821                                filename,
822                                crate::common::io_error_msg(&e)
823                            );
824                            return Ok(false);
825                        }
826                    }
827                }
828            }
829            TailMode::LinesFrom(n) => {
830                out.flush()?;
831                #[cfg(target_os = "linux")]
832                let file = match open_noatime(path) {
833                    Ok(f) => f,
834                    Err(e) => {
835                        eprintln!(
836                            "{}: cannot open '{}' for reading: {}",
837                            tool_name,
838                            filename,
839                            crate::common::io_error_msg(&e)
840                        );
841                        return Ok(false);
842                    }
843                };
844                #[cfg(not(target_os = "linux"))]
845                let file = match std::fs::File::open(path) {
846                    Ok(f) => f,
847                    Err(e) => {
848                        eprintln!(
849                            "{}: cannot open '{}' for reading: {}",
850                            tool_name,
851                            filename,
852                            crate::common::io_error_msg(&e)
853                        );
854                        return Ok(false);
855                    }
856                };
857                match tail_lines_from_streaming_file(file, *n, delimiter, out) {
858                    Ok(_) => return Ok(true),
859                    Err(e) => {
860                        eprintln!(
861                            "{}: error reading '{}': {}",
862                            tool_name,
863                            filename,
864                            crate::common::io_error_msg(&e)
865                        );
866                        return Ok(false);
867                    }
868                }
869            }
870            TailMode::Bytes(_n) => {
871                #[cfg(target_os = "linux")]
872                {
873                    use std::os::unix::io::AsRawFd;
874                    out.flush()?;
875                    let stdout = io::stdout();
876                    let out_fd = stdout.as_raw_fd();
877                    match sendfile_tail_bytes(path, *_n, out_fd) {
878                        Ok(true) => return Ok(true),
879                        Ok(false) => {}
880                        Err(e) => {
881                            eprintln!(
882                                "{}: error reading '{}': {}",
883                                tool_name,
884                                filename,
885                                crate::common::io_error_msg(&e)
886                            );
887                            return Ok(false);
888                        }
889                    }
890                }
891            }
892            TailMode::BytesFrom(_n) => {
893                #[cfg(target_os = "linux")]
894                {
895                    use std::os::unix::io::AsRawFd;
896                    out.flush()?;
897                    let stdout = io::stdout();
898                    let out_fd = stdout.as_raw_fd();
899                    match sendfile_tail_bytes_from(path, *_n, out_fd) {
900                        Ok(true) => return Ok(true),
901                        Ok(false) => {}
902                        Err(e) => {
903                            eprintln!(
904                                "{}: error reading '{}': {}",
905                                tool_name,
906                                filename,
907                                crate::common::io_error_msg(&e)
908                            );
909                            return Ok(false);
910                        }
911                    }
912                }
913            }
914        }
915    }
916
917    // Slow path: read entire input (stdin or fallback)
918    let data: FileData = if filename == "-" {
919        match read_stdin() {
920            Ok(d) => FileData::Owned(d),
921            Err(e) => {
922                eprintln!(
923                    "{}: standard input: {}",
924                    tool_name,
925                    crate::common::io_error_msg(&e)
926                );
927                return Ok(false);
928            }
929        }
930    } else {
931        match read_file(Path::new(filename)) {
932            Ok(d) => d,
933            Err(e) => {
934                eprintln!(
935                    "{}: cannot open '{}' for reading: {}",
936                    tool_name,
937                    filename,
938                    crate::common::io_error_msg(&e)
939                );
940                return Ok(false);
941            }
942        }
943    };
944
945    match &config.mode {
946        TailMode::Lines(n) => tail_lines(&data, *n, delimiter, out)?,
947        TailMode::LinesFrom(n) => tail_lines_from(&data, *n, delimiter, out)?,
948        TailMode::Bytes(n) => tail_bytes(&data, *n, out)?,
949        TailMode::BytesFrom(n) => tail_bytes_from(&data, *n, out)?,
950    }
951
952    Ok(true)
953}
954
955/// sendfile from byte N onward (1-indexed).
956/// Falls back to read+write if sendfile fails (e.g., stdout is a terminal).
957#[cfg(target_os = "linux")]
958fn sendfile_tail_bytes_from(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
959    let file = open_noatime(path)?;
960
961    let metadata = file.metadata()?;
962    let file_size = metadata.len();
963
964    if file_size == 0 {
965        return Ok(true);
966    }
967
968    let start = if n <= 1 { 0 } else { (n - 1).min(file_size) };
969
970    if start >= file_size {
971        return Ok(true);
972    }
973
974    use std::os::unix::io::AsRawFd;
975    let in_fd = file.as_raw_fd();
976    let output_len = file_size - start;
977    let _ = unsafe {
978        libc::posix_fadvise(
979            in_fd,
980            start as libc::off_t,
981            output_len as libc::off_t,
982            libc::POSIX_FADV_SEQUENTIAL,
983        )
984    };
985    let mut offset: libc::off_t = start as libc::off_t;
986    let mut remaining = output_len;
987    let total = output_len;
988
989    while remaining > 0 {
990        let chunk = remaining.min(0x7fff_f000) as usize;
991        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
992        if ret > 0 {
993            remaining -= ret as u64;
994        } else if ret == 0 {
995            break;
996        } else {
997            let err = io::Error::last_os_error();
998            if err.kind() == io::ErrorKind::Interrupted {
999                continue;
1000            }
1001            // sendfile fails with EINVAL for terminal fds; fall back to read+write
1002            if err.raw_os_error() == Some(libc::EINVAL) && remaining == total {
1003                let mut file = file;
1004                file.seek(io::SeekFrom::Start(start))?;
1005                let mut buf = [0u8; 65536];
1006                loop {
1007                    let nr = match file.read(&mut buf) {
1008                        Ok(0) => break,
1009                        Ok(nr) => nr,
1010                        Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
1011                        Err(e) => return Err(e),
1012                    };
1013                    write_all_fd(out_fd, &buf[..nr])?;
1014                }
1015                return Ok(true);
1016            }
1017            return Err(err);
1018        }
1019    }
1020
1021    Ok(true)
1022}
1023
1024/// Follow a file for new data (basic implementation)
1025#[cfg(target_os = "linux")]
1026pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
1027    use std::thread;
1028    use std::time::Duration;
1029
1030    let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
1031    let path = Path::new(filename);
1032
1033    let mut last_size = match std::fs::metadata(path) {
1034        Ok(m) => m.len(),
1035        Err(_) => 0,
1036    };
1037
1038    loop {
1039        // Check PID if set
1040        if let Some(pid) = config.pid {
1041            if unsafe { libc::kill(pid as i32, 0) } != 0 {
1042                break;
1043            }
1044        }
1045
1046        thread::sleep(sleep_duration);
1047
1048        let current_size = match std::fs::metadata(path) {
1049            Ok(m) => m.len(),
1050            Err(_) => {
1051                if config.retry {
1052                    continue;
1053                }
1054                break;
1055            }
1056        };
1057
1058        if current_size > last_size {
1059            // Read new data
1060            let file = std::fs::File::open(path)?;
1061            use std::os::unix::io::AsRawFd;
1062            let in_fd = file.as_raw_fd();
1063            let stdout = io::stdout();
1064            let out_fd = stdout.as_raw_fd();
1065            let mut offset = last_size as libc::off_t;
1066            let mut remaining = current_size - last_size; // u64, safe on 32-bit
1067
1068            while remaining > 0 {
1069                let chunk = remaining.min(0x7fff_f000) as usize;
1070                let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
1071                if ret > 0 {
1072                    remaining -= ret as u64;
1073                } else if ret == 0 {
1074                    break;
1075                } else {
1076                    let err = io::Error::last_os_error();
1077                    if err.kind() == io::ErrorKind::Interrupted {
1078                        continue;
1079                    }
1080                    return Err(err);
1081                }
1082            }
1083            let _ = out.flush();
1084            last_size = current_size;
1085        } else if current_size < last_size {
1086            // File was truncated
1087            last_size = current_size;
1088        }
1089    }
1090
1091    Ok(())
1092}
1093
1094#[cfg(not(target_os = "linux"))]
1095pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
1096    use std::io::{Read, Seek};
1097    use std::thread;
1098    use std::time::Duration;
1099
1100    let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
1101    let path = Path::new(filename);
1102
1103    let mut last_size = match std::fs::metadata(path) {
1104        Ok(m) => m.len(),
1105        Err(_) => 0,
1106    };
1107
1108    loop {
1109        thread::sleep(sleep_duration);
1110
1111        let current_size = match std::fs::metadata(path) {
1112            Ok(m) => m.len(),
1113            Err(_) => {
1114                if config.retry {
1115                    continue;
1116                }
1117                break;
1118            }
1119        };
1120
1121        if current_size > last_size {
1122            let mut file = std::fs::File::open(path)?;
1123            file.seek(io::SeekFrom::Start(last_size))?;
1124            let mut buf = vec![0u8; (current_size - last_size) as usize];
1125            file.read_exact(&mut buf)?;
1126            out.write_all(&buf)?;
1127            out.flush()?;
1128            last_size = current_size;
1129        } else if current_size < last_size {
1130            last_size = current_size;
1131        }
1132    }
1133
1134    Ok(())
1135}