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    // Read backward in chunks to find N lines from the end
221    const CHUNK: u64 = 65536;
222    let mut chunks: Vec<Vec<u8>> = Vec::new();
223    let mut pos = file_size;
224    let mut count = 0u64;
225    let mut found_start = false;
226
227    let mut reader = file;
228
229    while pos > 0 {
230        let read_start = if pos > CHUNK { pos - CHUNK } else { 0 };
231        let read_len = (pos - read_start) as usize;
232
233        reader.seek(io::SeekFrom::Start(read_start))?;
234        let mut buf = vec![0u8; read_len];
235        reader.read_exact(&mut buf)?;
236
237        // Count delimiters backward in this chunk
238        let search_end = if pos == file_size && !buf.is_empty() && buf[buf.len() - 1] == delimiter {
239            buf.len() - 1
240        } else {
241            buf.len()
242        };
243
244        for i in (0..search_end).rev() {
245            if buf[i] == delimiter {
246                count += 1;
247                if count == n {
248                    // Found the start point: write from buf[i+1..] + all subsequent chunks
249                    out.write_all(&buf[i + 1..])?;
250                    for chunk in chunks.iter().rev() {
251                        out.write_all(chunk)?;
252                    }
253                    found_start = true;
254                    break;
255                }
256            }
257        }
258
259        if found_start {
260            break;
261        }
262
263        chunks.push(buf);
264        pos = read_start;
265    }
266
267    if !found_start {
268        // Fewer than N lines — output entire file
269        for chunk in chunks.iter().rev() {
270            out.write_all(chunk)?;
271        }
272    }
273
274    Ok(true)
275}
276
277/// Streaming tail -n +N for regular files: skip N-1 lines from start.
278fn tail_lines_from_streaming_file(
279    path: &Path,
280    n: u64,
281    delimiter: u8,
282    out: &mut impl Write,
283) -> io::Result<bool> {
284    #[cfg(target_os = "linux")]
285    let file = {
286        use std::os::unix::fs::OpenOptionsExt;
287        std::fs::OpenOptions::new()
288            .read(true)
289            .custom_flags(libc::O_NOATIME)
290            .open(path)
291            .or_else(|_| std::fs::File::open(path))?
292    };
293    #[cfg(not(target_os = "linux"))]
294    let file = std::fs::File::open(path)?;
295
296    if n <= 1 {
297        // Output entire file via sendfile
298        #[cfg(target_os = "linux")]
299        {
300            use std::os::unix::io::AsRawFd;
301            let in_fd = file.as_raw_fd();
302            let stdout = io::stdout();
303            let out_fd = stdout.as_raw_fd();
304            let file_size = file.metadata()?.len() as usize;
305            return sendfile_to_stdout_raw(in_fd, file_size, out_fd);
306        }
307        #[cfg(not(target_os = "linux"))]
308        {
309            let mut reader = io::BufReader::with_capacity(65536, file);
310            let mut buf = [0u8; 65536];
311            loop {
312                let n = match reader.read(&mut buf) {
313                    Ok(0) => break,
314                    Ok(n) => n,
315                    Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
316                    Err(e) => return Err(e),
317                };
318                out.write_all(&buf[..n])?;
319            }
320            return Ok(true);
321        }
322    }
323
324    let skip = n - 1;
325    let mut reader = io::BufReader::with_capacity(65536, file);
326    let mut buf = [0u8; 65536];
327    let mut count = 0u64;
328    let mut skipping = true;
329
330    loop {
331        let bytes_read = match reader.read(&mut buf) {
332            Ok(0) => break,
333            Ok(n) => n,
334            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
335            Err(e) => return Err(e),
336        };
337
338        let chunk = &buf[..bytes_read];
339
340        if skipping {
341            for pos in memchr_iter(delimiter, chunk) {
342                count += 1;
343                if count == skip {
344                    // Found the start — output rest of this chunk and stop skipping
345                    let start = pos + 1;
346                    if start < chunk.len() {
347                        out.write_all(&chunk[start..])?;
348                    }
349                    skipping = false;
350                    break;
351                }
352            }
353        } else {
354            out.write_all(chunk)?;
355        }
356    }
357
358    Ok(true)
359}
360
361/// Raw sendfile helper
362#[cfg(target_os = "linux")]
363fn sendfile_to_stdout_raw(in_fd: i32, file_size: usize, out_fd: i32) -> io::Result<bool> {
364    let mut offset: libc::off_t = 0;
365    let mut remaining = file_size;
366    while remaining > 0 {
367        let chunk = remaining.min(0x7ffff000);
368        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
369        if ret > 0 {
370            remaining -= ret as usize;
371        } else if ret == 0 {
372            break;
373        } else {
374            let err = io::Error::last_os_error();
375            if err.kind() == io::ErrorKind::Interrupted {
376                continue;
377            }
378            return Err(err);
379        }
380    }
381    Ok(true)
382}
383
384/// Process a single file/stdin for tail
385pub fn tail_file(
386    filename: &str,
387    config: &TailConfig,
388    out: &mut impl Write,
389    tool_name: &str,
390) -> io::Result<bool> {
391    let delimiter = if config.zero_terminated { b'\0' } else { b'\n' };
392
393    if filename != "-" {
394        let path = Path::new(filename);
395
396        match &config.mode {
397            TailMode::Lines(n) => {
398                // Streaming backward read: only reads enough to find N lines
399                match tail_lines_streaming_file(path, *n, delimiter, out) {
400                    Ok(true) => return Ok(true),
401                    Err(e) => {
402                        eprintln!(
403                            "{}: cannot open '{}' for reading: {}",
404                            tool_name,
405                            filename,
406                            crate::common::io_error_msg(&e)
407                        );
408                        return Ok(false);
409                    }
410                    _ => {}
411                }
412            }
413            TailMode::LinesFrom(n) => {
414                // Streaming forward: skip N-1 lines, output rest
415                match tail_lines_from_streaming_file(path, *n, delimiter, out) {
416                    Ok(true) => return Ok(true),
417                    Err(e) => {
418                        eprintln!(
419                            "{}: cannot open '{}' for reading: {}",
420                            tool_name,
421                            filename,
422                            crate::common::io_error_msg(&e)
423                        );
424                        return Ok(false);
425                    }
426                    _ => {}
427                }
428            }
429            TailMode::Bytes(_n) => {
430                #[cfg(target_os = "linux")]
431                {
432                    use std::os::unix::io::AsRawFd;
433                    let stdout = io::stdout();
434                    let out_fd = stdout.as_raw_fd();
435                    if let Ok(true) = sendfile_tail_bytes(path, *_n, out_fd) {
436                        return Ok(true);
437                    }
438                }
439            }
440            TailMode::BytesFrom(_n) => {
441                #[cfg(target_os = "linux")]
442                {
443                    use std::os::unix::io::AsRawFd;
444                    let stdout = io::stdout();
445                    let out_fd = stdout.as_raw_fd();
446                    if let Ok(true) = sendfile_tail_bytes_from(path, *_n, out_fd) {
447                        return Ok(true);
448                    }
449                }
450            }
451        }
452    }
453
454    // Slow path: read entire input (stdin or fallback)
455    let data: FileData = if filename == "-" {
456        match read_stdin() {
457            Ok(d) => FileData::Owned(d),
458            Err(e) => {
459                eprintln!(
460                    "{}: standard input: {}",
461                    tool_name,
462                    crate::common::io_error_msg(&e)
463                );
464                return Ok(false);
465            }
466        }
467    } else {
468        match read_file(Path::new(filename)) {
469            Ok(d) => d,
470            Err(e) => {
471                eprintln!(
472                    "{}: cannot open '{}' for reading: {}",
473                    tool_name,
474                    filename,
475                    crate::common::io_error_msg(&e)
476                );
477                return Ok(false);
478            }
479        }
480    };
481
482    match &config.mode {
483        TailMode::Lines(n) => tail_lines(&data, *n, delimiter, out)?,
484        TailMode::LinesFrom(n) => tail_lines_from(&data, *n, delimiter, out)?,
485        TailMode::Bytes(n) => tail_bytes(&data, *n, out)?,
486        TailMode::BytesFrom(n) => tail_bytes_from(&data, *n, out)?,
487    }
488
489    Ok(true)
490}
491
492/// sendfile from byte N onward (1-indexed)
493#[cfg(target_os = "linux")]
494fn sendfile_tail_bytes_from(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
495    use std::os::unix::fs::OpenOptionsExt;
496
497    let file = std::fs::OpenOptions::new()
498        .read(true)
499        .custom_flags(libc::O_NOATIME)
500        .open(path)
501        .or_else(|_| std::fs::File::open(path))?;
502
503    let metadata = file.metadata()?;
504    let file_size = metadata.len();
505
506    if file_size == 0 {
507        return Ok(true);
508    }
509
510    let start = if n <= 1 { 0 } else { (n - 1).min(file_size) };
511
512    if start >= file_size {
513        return Ok(true);
514    }
515
516    use std::os::unix::io::AsRawFd;
517    let in_fd = file.as_raw_fd();
518    let mut offset: libc::off_t = start as libc::off_t;
519    let mut remaining = (file_size - start) as usize;
520
521    while remaining > 0 {
522        let chunk = remaining.min(0x7ffff000);
523        let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
524        if ret > 0 {
525            remaining -= ret as usize;
526        } else if ret == 0 {
527            break;
528        } else {
529            let err = io::Error::last_os_error();
530            if err.kind() == io::ErrorKind::Interrupted {
531                continue;
532            }
533            return Err(err);
534        }
535    }
536
537    Ok(true)
538}
539
540/// Follow a file for new data (basic implementation)
541#[cfg(target_os = "linux")]
542pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
543    use std::thread;
544    use std::time::Duration;
545
546    let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
547    let path = Path::new(filename);
548
549    let mut last_size = match std::fs::metadata(path) {
550        Ok(m) => m.len(),
551        Err(_) => 0,
552    };
553
554    loop {
555        // Check PID if set
556        if let Some(pid) = config.pid {
557            if unsafe { libc::kill(pid as i32, 0) } != 0 {
558                break;
559            }
560        }
561
562        thread::sleep(sleep_duration);
563
564        let current_size = match std::fs::metadata(path) {
565            Ok(m) => m.len(),
566            Err(_) => {
567                if config.retry {
568                    continue;
569                }
570                break;
571            }
572        };
573
574        if current_size > last_size {
575            // Read new data
576            let file = std::fs::File::open(path)?;
577            use std::os::unix::io::AsRawFd;
578            let in_fd = file.as_raw_fd();
579            let stdout = io::stdout();
580            let out_fd = stdout.as_raw_fd();
581            let mut offset = last_size as libc::off_t;
582            let mut remaining = (current_size - last_size) as usize;
583
584            while remaining > 0 {
585                let chunk = remaining.min(0x7ffff000);
586                let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
587                if ret > 0 {
588                    remaining -= ret as usize;
589                } else if ret == 0 {
590                    break;
591                } else {
592                    let err = io::Error::last_os_error();
593                    if err.kind() == io::ErrorKind::Interrupted {
594                        continue;
595                    }
596                    return Err(err);
597                }
598            }
599            let _ = out.flush();
600            last_size = current_size;
601        } else if current_size < last_size {
602            // File was truncated
603            last_size = current_size;
604        }
605    }
606
607    Ok(())
608}
609
610#[cfg(not(target_os = "linux"))]
611pub fn follow_file(filename: &str, config: &TailConfig, out: &mut impl Write) -> io::Result<()> {
612    use std::io::{Read, Seek};
613    use std::thread;
614    use std::time::Duration;
615
616    let sleep_duration = Duration::from_secs_f64(config.sleep_interval);
617    let path = Path::new(filename);
618
619    let mut last_size = match std::fs::metadata(path) {
620        Ok(m) => m.len(),
621        Err(_) => 0,
622    };
623
624    loop {
625        thread::sleep(sleep_duration);
626
627        let current_size = match std::fs::metadata(path) {
628            Ok(m) => m.len(),
629            Err(_) => {
630                if config.retry {
631                    continue;
632                }
633                break;
634            }
635        };
636
637        if current_size > last_size {
638            let mut file = std::fs::File::open(path)?;
639            file.seek(io::SeekFrom::Start(last_size))?;
640            let mut buf = vec![0u8; (current_size - last_size) as usize];
641            file.read_exact(&mut buf)?;
642            out.write_all(&buf)?;
643            out.flush()?;
644            last_size = current_size;
645        } else if current_size < last_size {
646            last_size = current_size;
647        }
648    }
649
650    Ok(())
651}