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/// Mode for tail operation
9#[derive(Clone, Debug)]
10pub enum TailMode {
11    /// Last N lines (default: 10)
12    Lines(u64),
13    /// Starting from line N (1-indexed)
14    LinesFrom(u64),
15    /// Last N bytes
16    Bytes(u64),
17    /// Starting from byte N (1-indexed)
18    BytesFrom(u64),
19}
20
21/// Follow mode
22#[derive(Clone, Debug, PartialEq)]
23pub enum FollowMode {
24    None,
25    Descriptor,
26    Name,
27}
28
29/// Configuration for tail
30#[derive(Clone, Debug)]
31pub struct TailConfig {
32    pub mode: TailMode,
33    pub follow: FollowMode,
34    pub retry: bool,
35    pub pid: Option<u32>,
36    pub sleep_interval: f64,
37    pub max_unchanged_stats: u64,
38    pub zero_terminated: bool,
39}
40
41impl Default for TailConfig {
42    fn default() -> Self {
43        Self {
44            mode: TailMode::Lines(10),
45            follow: FollowMode::None,
46            retry: false,
47            pid: None,
48            sleep_interval: 1.0,
49            max_unchanged_stats: 5,
50            zero_terminated: false,
51        }
52    }
53}
54
55/// Parse a numeric argument with optional suffix, same as head
56pub fn parse_size(s: &str) -> Result<u64, String> {
57    crate::head::parse_size(s)
58}
59
60/// Output last N lines from data using backward SIMD scanning
61pub fn tail_lines(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
62    if n == 0 || data.is_empty() {
63        return Ok(());
64    }
65
66    // Use memrchr for backward scanning - SIMD accelerated
67    let mut count = 0u64;
68
69    // Check if data ends with delimiter - if so, skip the trailing one
70    let search_end = if !data.is_empty() && data[data.len() - 1] == delimiter {
71        data.len() - 1
72    } else {
73        data.len()
74    };
75
76    for pos in memrchr_iter(delimiter, &data[..search_end]) {
77        count += 1;
78        if count == n {
79            return out.write_all(&data[pos + 1..]);
80        }
81    }
82
83    // Fewer than N lines — output everything
84    out.write_all(data)
85}
86
87/// Output from line N onward (1-indexed)
88pub fn tail_lines_from(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
89    if data.is_empty() {
90        return Ok(());
91    }
92
93    if n <= 1 {
94        return out.write_all(data);
95    }
96
97    // Skip first (n-1) lines
98    let skip = n - 1;
99    let mut count = 0u64;
100
101    for pos in memchr_iter(delimiter, data) {
102        count += 1;
103        if count == skip {
104            let start = pos + 1;
105            if start < data.len() {
106                return out.write_all(&data[start..]);
107            }
108            return Ok(());
109        }
110    }
111
112    // Fewer than N lines — output nothing
113    Ok(())
114}
115
116/// Output last N bytes from data
117pub fn tail_bytes(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
118    if n == 0 || data.is_empty() {
119        return Ok(());
120    }
121
122    let n = n.min(data.len() as u64) as usize;
123    out.write_all(&data[data.len() - n..])
124}
125
126/// Output from byte N onward (1-indexed)
127pub fn tail_bytes_from(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
128    if data.is_empty() {
129        return Ok(());
130    }
131
132    if n <= 1 {
133        return out.write_all(data);
134    }
135
136    let start = ((n - 1) as usize).min(data.len());
137    if start < data.len() {
138        out.write_all(&data[start..])
139    } else {
140        Ok(())
141    }
142}
143
144/// Use sendfile for zero-copy byte output on Linux (last N bytes)
145#[cfg(target_os = "linux")]
146pub fn sendfile_tail_bytes(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
147    use std::os::unix::fs::OpenOptionsExt;
148
149    let file = std::fs::OpenOptions::new()
150        .read(true)
151        .custom_flags(libc::O_NOATIME)
152        .open(path)
153        .or_else(|_| std::fs::File::open(path))?;
154
155    let metadata = file.metadata()?;
156    let file_size = metadata.len();
157
158    if file_size == 0 {
159        return Ok(true);
160    }
161
162    let n = n.min(file_size);
163    let start = file_size - n;
164
165    use std::os::unix::io::AsRawFd;
166    let in_fd = file.as_raw_fd();
167    let mut offset: libc::off_t = start as libc::off_t;
168    let mut remaining = n as usize;
169
170    while remaining > 0 {
171        let chunk = remaining.min(0x7ffff000);
172        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
173        if ret > 0 {
174            remaining -= ret as usize;
175        } else if ret == 0 {
176            break;
177        } else {
178            let err = io::Error::last_os_error();
179            if err.kind() == io::ErrorKind::Interrupted {
180                continue;
181            }
182            return Err(err);
183        }
184    }
185
186    Ok(true)
187}
188
189/// Streaming tail -n N for regular files: read backward from EOF.
190/// Only reads enough of the file to find N lines, avoids mmapping entire file.
191fn tail_lines_streaming_file(
192    path: &Path,
193    n: u64,
194    delimiter: u8,
195    out: &mut impl Write,
196) -> io::Result<bool> {
197    if n == 0 {
198        return Ok(true);
199    }
200
201    #[cfg(target_os = "linux")]
202    let file = {
203        use std::os::unix::fs::OpenOptionsExt;
204        std::fs::OpenOptions::new()
205            .read(true)
206            .custom_flags(libc::O_NOATIME)
207            .open(path)
208            .or_else(|_| std::fs::File::open(path))?
209    };
210    #[cfg(not(target_os = "linux"))]
211    let file = std::fs::File::open(path)?;
212
213    let metadata = file.metadata()?;
214    let file_size = metadata.len();
215
216    if file_size == 0 {
217        return Ok(true);
218    }
219
220    // Try mmap for backward scanning — avoids heap allocations entirely
221    // and lets the kernel page in only the portion we scan.
222    #[cfg(target_os = "linux")]
223    {
224        use std::os::unix::io::AsRawFd;
225        let fd = file.as_raw_fd();
226        let ptr = unsafe {
227            libc::mmap(
228                std::ptr::null_mut(),
229                file_size as libc::size_t,
230                libc::PROT_READ,
231                libc::MAP_PRIVATE | libc::MAP_NORESERVE,
232                fd,
233                0,
234            )
235        };
236        if ptr != libc::MAP_FAILED {
237            // Advise sequential read from the end
238            let _ = unsafe { libc::madvise(ptr, file_size as libc::size_t, libc::MADV_SEQUENTIAL) };
239            let data = unsafe { std::slice::from_raw_parts(ptr as *const u8, file_size as usize) };
240            let result = tail_lines(data, n, delimiter, out);
241            unsafe {
242                libc::munmap(ptr, file_size as libc::size_t);
243            }
244            return result.map(|_| true);
245        }
246    }
247
248    // Fallback: read backward in chunks to find N lines from the end
249    const CHUNK: u64 = 262144;
250    let mut chunks: Vec<Vec<u8>> = Vec::new();
251    let mut pos = file_size;
252    let mut count = 0u64;
253    let mut found_start = false;
254
255    let mut reader = file;
256
257    while pos > 0 {
258        let read_start = if pos > CHUNK { pos - CHUNK } else { 0 };
259        let read_len = (pos - read_start) as usize;
260
261        reader.seek(io::SeekFrom::Start(read_start))?;
262        let mut buf = vec![0u8; read_len];
263        reader.read_exact(&mut buf)?;
264
265        // Count delimiters backward in this chunk using SIMD
266        let search_end = if pos == file_size && !buf.is_empty() && buf[buf.len() - 1] == delimiter {
267            buf.len() - 1
268        } else {
269            buf.len()
270        };
271
272        for rpos in memrchr_iter(delimiter, &buf[..search_end]) {
273            count += 1;
274            if count == n {
275                out.write_all(&buf[rpos + 1..])?;
276                for chunk in chunks.iter().rev() {
277                    out.write_all(chunk)?;
278                }
279                found_start = true;
280                break;
281            }
282        }
283
284        if found_start {
285            break;
286        }
287
288        chunks.push(buf);
289        pos = read_start;
290    }
291
292    if !found_start {
293        // Fewer than N lines — output entire file
294        for chunk in chunks.iter().rev() {
295            out.write_all(chunk)?;
296        }
297    }
298
299    Ok(true)
300}
301
302/// Streaming tail -n +N for regular files: skip N-1 lines from start.
303fn tail_lines_from_streaming_file(
304    path: &Path,
305    n: u64,
306    delimiter: u8,
307    out: &mut impl Write,
308) -> io::Result<bool> {
309    #[cfg(target_os = "linux")]
310    let file = {
311        use std::os::unix::fs::OpenOptionsExt;
312        std::fs::OpenOptions::new()
313            .read(true)
314            .custom_flags(libc::O_NOATIME)
315            .open(path)
316            .or_else(|_| std::fs::File::open(path))?
317    };
318    #[cfg(not(target_os = "linux"))]
319    let file = std::fs::File::open(path)?;
320
321    if n <= 1 {
322        // Output entire file via sendfile
323        #[cfg(target_os = "linux")]
324        {
325            use std::os::unix::io::AsRawFd;
326            let in_fd = file.as_raw_fd();
327            let stdout = io::stdout();
328            let out_fd = stdout.as_raw_fd();
329            let file_size = file.metadata()?.len() as usize;
330            return sendfile_to_stdout_raw(in_fd, file_size, out_fd);
331        }
332        #[cfg(not(target_os = "linux"))]
333        {
334            let mut reader = io::BufReader::with_capacity(1024 * 1024, file);
335            let mut buf = [0u8; 262144];
336            loop {
337                let n = match reader.read(&mut buf) {
338                    Ok(0) => break,
339                    Ok(n) => n,
340                    Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
341                    Err(e) => return Err(e),
342                };
343                out.write_all(&buf[..n])?;
344            }
345            return Ok(true);
346        }
347    }
348
349    let skip = n - 1;
350    let mut reader = io::BufReader::with_capacity(1024 * 1024, file);
351    let mut buf = [0u8; 262144];
352    let mut count = 0u64;
353    let mut skipping = true;
354
355    loop {
356        let bytes_read = match reader.read(&mut buf) {
357            Ok(0) => break,
358            Ok(n) => n,
359            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
360            Err(e) => return Err(e),
361        };
362
363        let chunk = &buf[..bytes_read];
364
365        if skipping {
366            for pos in memchr_iter(delimiter, chunk) {
367                count += 1;
368                if count == skip {
369                    // Found the start — output rest of this chunk and stop skipping
370                    let start = pos + 1;
371                    if start < chunk.len() {
372                        out.write_all(&chunk[start..])?;
373                    }
374                    skipping = false;
375                    break;
376                }
377            }
378        } else {
379            out.write_all(chunk)?;
380        }
381    }
382
383    Ok(true)
384}
385
386/// Raw sendfile helper
387#[cfg(target_os = "linux")]
388fn sendfile_to_stdout_raw(in_fd: i32, file_size: usize, out_fd: i32) -> io::Result<bool> {
389    let mut offset: libc::off_t = 0;
390    let mut remaining = file_size;
391    while remaining > 0 {
392        let chunk = remaining.min(0x7ffff000);
393        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
394        if ret > 0 {
395            remaining -= ret as usize;
396        } else if ret == 0 {
397            break;
398        } else {
399            let err = io::Error::last_os_error();
400            if err.kind() == io::ErrorKind::Interrupted {
401                continue;
402            }
403            return Err(err);
404        }
405    }
406    Ok(true)
407}
408
409/// Process a single file/stdin for tail
410pub fn tail_file(
411    filename: &str,
412    config: &TailConfig,
413    out: &mut impl Write,
414    tool_name: &str,
415) -> io::Result<bool> {
416    let delimiter = if config.zero_terminated { b'\0' } else { b'\n' };
417
418    if filename != "-" {
419        let path = Path::new(filename);
420
421        match &config.mode {
422            TailMode::Lines(n) => {
423                // Streaming backward read: only reads enough to find N lines
424                match tail_lines_streaming_file(path, *n, delimiter, out) {
425                    Ok(true) => return Ok(true),
426                    Err(e) => {
427                        eprintln!(
428                            "{}: cannot open '{}' for reading: {}",
429                            tool_name,
430                            filename,
431                            crate::common::io_error_msg(&e)
432                        );
433                        return Ok(false);
434                    }
435                    _ => {}
436                }
437            }
438            TailMode::LinesFrom(n) => {
439                // Streaming forward: skip N-1 lines, output rest
440                match tail_lines_from_streaming_file(path, *n, delimiter, out) {
441                    Ok(true) => return Ok(true),
442                    Err(e) => {
443                        eprintln!(
444                            "{}: cannot open '{}' for reading: {}",
445                            tool_name,
446                            filename,
447                            crate::common::io_error_msg(&e)
448                        );
449                        return Ok(false);
450                    }
451                    _ => {}
452                }
453            }
454            TailMode::Bytes(_n) => {
455                #[cfg(target_os = "linux")]
456                {
457                    use std::os::unix::io::AsRawFd;
458                    let stdout = io::stdout();
459                    let out_fd = stdout.as_raw_fd();
460                    if let Ok(true) = sendfile_tail_bytes(path, *_n, out_fd) {
461                        return Ok(true);
462                    }
463                }
464            }
465            TailMode::BytesFrom(_n) => {
466                #[cfg(target_os = "linux")]
467                {
468                    use std::os::unix::io::AsRawFd;
469                    let stdout = io::stdout();
470                    let out_fd = stdout.as_raw_fd();
471                    if let Ok(true) = sendfile_tail_bytes_from(path, *_n, out_fd) {
472                        return Ok(true);
473                    }
474                }
475            }
476        }
477    }
478
479    // Slow path: read entire input (stdin or fallback)
480    let data: FileData = if filename == "-" {
481        match read_stdin() {
482            Ok(d) => FileData::Owned(d),
483            Err(e) => {
484                eprintln!(
485                    "{}: standard input: {}",
486                    tool_name,
487                    crate::common::io_error_msg(&e)
488                );
489                return Ok(false);
490            }
491        }
492    } else {
493        match read_file(Path::new(filename)) {
494            Ok(d) => d,
495            Err(e) => {
496                eprintln!(
497                    "{}: cannot open '{}' for reading: {}",
498                    tool_name,
499                    filename,
500                    crate::common::io_error_msg(&e)
501                );
502                return Ok(false);
503            }
504        }
505    };
506
507    match &config.mode {
508        TailMode::Lines(n) => tail_lines(&data, *n, delimiter, out)?,
509        TailMode::LinesFrom(n) => tail_lines_from(&data, *n, delimiter, out)?,
510        TailMode::Bytes(n) => tail_bytes(&data, *n, out)?,
511        TailMode::BytesFrom(n) => tail_bytes_from(&data, *n, out)?,
512    }
513
514    Ok(true)
515}
516
517/// sendfile from byte N onward (1-indexed)
518#[cfg(target_os = "linux")]
519fn sendfile_tail_bytes_from(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
520    use std::os::unix::fs::OpenOptionsExt;
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
528    let metadata = file.metadata()?;
529    let file_size = metadata.len();
530
531    if file_size == 0 {
532        return Ok(true);
533    }
534
535    let start = if n <= 1 { 0 } else { (n - 1).min(file_size) };
536
537    if start >= file_size {
538        return Ok(true);
539    }
540
541    use std::os::unix::io::AsRawFd;
542    let in_fd = file.as_raw_fd();
543    let mut offset: libc::off_t = start as libc::off_t;
544    let mut remaining = (file_size - start) as usize;
545
546    while remaining > 0 {
547        let chunk = remaining.min(0x7ffff000);
548        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
549        if ret > 0 {
550            remaining -= ret as usize;
551        } else if ret == 0 {
552            break;
553        } else {
554            let err = io::Error::last_os_error();
555            if err.kind() == io::ErrorKind::Interrupted {
556                continue;
557            }
558            return Err(err);
559        }
560    }
561
562    Ok(true)
563}
564
565/// Follow a file for new data (basic implementation)
566#[cfg(target_os = "linux")]
567pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
568    use std::thread;
569    use std::time::Duration;
570
571    let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
572    let path = Path::new(filename);
573
574    let mut last_size = match std::fs::metadata(path) {
575        Ok(m) => m.len(),
576        Err(_) => 0,
577    };
578
579    loop {
580        // Check PID if set
581        if let Some(pid) = config.pid {
582            if unsafe { libc::kill(pid as i32, 0) } != 0 {
583                break;
584            }
585        }
586
587        thread::sleep(sleep_duration);
588
589        let current_size = match std::fs::metadata(path) {
590            Ok(m) => m.len(),
591            Err(_) => {
592                if config.retry {
593                    continue;
594                }
595                break;
596            }
597        };
598
599        if current_size > last_size {
600            // Read new data
601            let file = std::fs::File::open(path)?;
602            use std::os::unix::io::AsRawFd;
603            let in_fd = file.as_raw_fd();
604            let stdout = io::stdout();
605            let out_fd = stdout.as_raw_fd();
606            let mut offset = last_size as libc::off_t;
607            let mut remaining = (current_size - last_size) as usize;
608
609            while remaining > 0 {
610                let chunk = remaining.min(0x7ffff000);
611                let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
612                if ret > 0 {
613                    remaining -= ret as usize;
614                } else if ret == 0 {
615                    break;
616                } else {
617                    let err = io::Error::last_os_error();
618                    if err.kind() == io::ErrorKind::Interrupted {
619                        continue;
620                    }
621                    return Err(err);
622                }
623            }
624            let _ = out.flush();
625            last_size = current_size;
626        } else if current_size < last_size {
627            // File was truncated
628            last_size = current_size;
629        }
630    }
631
632    Ok(())
633}
634
635#[cfg(not(target_os = "linux"))]
636pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
637    use std::io::{Read, Seek};
638    use std::thread;
639    use std::time::Duration;
640
641    let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
642    let path = Path::new(filename);
643
644    let mut last_size = match std::fs::metadata(path) {
645        Ok(m) => m.len(),
646        Err(_) => 0,
647    };
648
649    loop {
650        thread::sleep(sleep_duration);
651
652        let current_size = match std::fs::metadata(path) {
653            Ok(m) => m.len(),
654            Err(_) => {
655                if config.retry {
656                    continue;
657                }
658                break;
659            }
660        };
661
662        if current_size > last_size {
663            let mut file = std::fs::File::open(path)?;
664            file.seek(io::SeekFrom::Start(last_size))?;
665            let mut buf = vec![0u8; (current_size - last_size) as usize];
666            file.read_exact(&mut buf)?;
667            out.write_all(&buf)?;
668            out.flush()?;
669            last_size = current_size;
670        } else if current_size < last_size {
671            last_size = current_size;
672        }
673    }
674
675    Ok(())
676}