1use 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 #[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 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 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 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 fn is_orphan(&self) -> bool {
218 !matches!(self.parent(), Some(parent) if parent.is_dir())
219 }
220
221 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 {
238 }
240 false
242}