uu_tail/
paths.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore tailable seekable stdlib (stdlib)
7
8use crate::text;
9use std::ffi::OsStr;
10use std::fs::{File, Metadata};
11use std::io::{Seek, SeekFrom};
12#[cfg(unix)]
13use std::os::unix::fs::{FileTypeExt, MetadataExt};
14use std::path::{Path, PathBuf};
15use uucore::error::UResult;
16use uucore::translate;
17
18#[derive(Debug, Clone)]
19pub enum InputKind {
20    File(PathBuf),
21    Stdin,
22}
23
24#[cfg(unix)]
25impl From<&OsStr> for InputKind {
26    fn from(value: &OsStr) -> Self {
27        if value == OsStr::new("-") {
28            Self::Stdin
29        } else {
30            Self::File(PathBuf::from(value))
31        }
32    }
33}
34
35#[cfg(not(unix))]
36impl From<&OsStr> for InputKind {
37    fn from(value: &OsStr) -> Self {
38        if value == OsStr::new(text::DASH) {
39            Self::Stdin
40        } else {
41            Self::File(PathBuf::from(value))
42        }
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct Input {
48    kind: InputKind,
49    pub display_name: String,
50}
51
52impl Input {
53    pub fn from<T: AsRef<OsStr>>(string: T) -> Self {
54        let string = string.as_ref();
55
56        let kind = string.into();
57        let display_name = match kind {
58            InputKind::File(_) => string.to_string_lossy().to_string(),
59            InputKind::Stdin => translate!("tail-stdin-header"),
60        };
61
62        Self { kind, display_name }
63    }
64
65    pub fn kind(&self) -> &InputKind {
66        &self.kind
67    }
68
69    pub fn is_stdin(&self) -> bool {
70        match self.kind {
71            InputKind::File(_) => false,
72            InputKind::Stdin => true,
73        }
74    }
75
76    pub fn resolve(&self) -> Option<PathBuf> {
77        match &self.kind {
78            InputKind::File(path) if path != &PathBuf::from(text::DEV_STDIN) => {
79                path.canonicalize().ok()
80            }
81            InputKind::File(_) | InputKind::Stdin => {
82                // on macOS, /dev/fd isn't backed by /proc and canonicalize()
83                // on dev/fd/0 (or /dev/stdin) will fail (NotFound),
84                // so we treat stdin as a pipe here
85                // https://github.com/rust-lang/rust/issues/95239
86                #[cfg(target_os = "macos")]
87                {
88                    None
89                }
90                #[cfg(not(target_os = "macos"))]
91                {
92                    PathBuf::from(text::FD0).canonicalize().ok()
93                }
94            }
95        }
96    }
97
98    pub fn is_tailable(&self) -> bool {
99        match &self.kind {
100            InputKind::File(path) => path_is_tailable(path),
101            InputKind::Stdin => self.resolve().is_some_and(|path| path_is_tailable(&path)),
102        }
103    }
104}
105
106impl Default for Input {
107    fn default() -> Self {
108        Self {
109            kind: InputKind::Stdin,
110            display_name: translate!("tail-stdin-header"),
111        }
112    }
113}
114
115#[derive(Debug, Default, Clone, Copy)]
116pub struct HeaderPrinter {
117    verbose: bool,
118    first_header: bool,
119}
120
121impl HeaderPrinter {
122    pub fn new(verbose: bool, first_header: bool) -> Self {
123        Self {
124            verbose,
125            first_header,
126        }
127    }
128
129    pub fn print_input(&mut self, input: &Input) {
130        self.print(input.display_name.as_str());
131    }
132
133    pub fn print(&mut self, string: &str) {
134        if self.verbose {
135            println!(
136                "{}==> {string} <==",
137                if self.first_header { "" } else { "\n" },
138            );
139            self.first_header = false;
140        }
141    }
142}
143pub trait FileExtTail {
144    #[allow(clippy::wrong_self_convention)]
145    fn is_seekable(&mut self, current_offset: u64) -> bool;
146}
147
148impl FileExtTail for File {
149    /// Test if File is seekable.
150    /// Set the current position offset to `current_offset`.
151    fn is_seekable(&mut self, current_offset: u64) -> bool {
152        self.stream_position().is_ok()
153            && self.seek(SeekFrom::End(0)).is_ok()
154            && self.seek(SeekFrom::Start(current_offset)).is_ok()
155    }
156}
157
158pub trait MetadataExtTail {
159    fn is_tailable(&self) -> bool;
160    fn got_truncated(&self, other: &Metadata) -> UResult<bool>;
161    fn file_id_eq(&self, other: &Metadata) -> bool;
162}
163
164impl MetadataExtTail for Metadata {
165    fn is_tailable(&self) -> bool {
166        let ft = self.file_type();
167        #[cfg(unix)]
168        {
169            ft.is_file() || ft.is_char_device() || ft.is_fifo()
170        }
171        #[cfg(not(unix))]
172        {
173            ft.is_file()
174        }
175    }
176
177    /// Return true if the file was modified and is now shorter
178    fn got_truncated(&self, other: &Metadata) -> UResult<bool> {
179        Ok(other.len() < self.len() && other.modified()? != self.modified()?)
180    }
181
182    fn file_id_eq(&self, _other: &Metadata) -> bool {
183        #[cfg(unix)]
184        {
185            self.ino().eq(&_other.ino())
186        }
187        #[cfg(windows)]
188        {
189            // TODO: `file_index` requires unstable library feature `windows_by_handle`
190            // use std::os::windows::prelude::*;
191            // if let Some(self_id) = self.file_index() {
192            //     if let Some(other_id) = other.file_index() {
193            //     // TODO: not sure this is the equivalent of comparing inode numbers
194            //
195            //         return self_id.eq(&other_id);
196            //     }
197            // }
198            false
199        }
200    }
201}
202
203pub trait PathExtTail {
204    fn is_stdin(&self) -> bool;
205    fn is_orphan(&self) -> bool;
206    fn is_tailable(&self) -> bool;
207}
208
209impl PathExtTail for Path {
210    fn is_stdin(&self) -> bool {
211        self.eq(Self::new(text::DASH))
212            || self.eq(Self::new(text::DEV_STDIN))
213            || self.eq(Self::new(&translate!("tail-stdin-header")))
214    }
215
216    /// Return true if `path` does not have an existing parent directory
217    fn is_orphan(&self) -> bool {
218        !matches!(self.parent(), Some(parent) if parent.is_dir())
219    }
220
221    /// Return true if `path` is is a file type that can be tailed
222    fn is_tailable(&self) -> bool {
223        path_is_tailable(self)
224    }
225}
226
227pub fn path_is_tailable(path: &Path) -> bool {
228    path.is_file() || path.exists() && path.metadata().is_ok_and(|meta| meta.is_tailable())
229}
230
231#[inline]
232pub fn stdin_is_bad_fd() -> bool {
233    // FIXME : Rust's stdlib is reopening fds as /dev/null
234    // see also: https://github.com/uutils/coreutils/issues/2873
235    // (gnu/tests/tail-2/follow-stdin.sh fails because of this)
236    //#[cfg(unix)]
237    {
238        //platform::stdin_is_bad_fd()
239    }
240    //#[cfg(not(unix))]
241    false
242}