1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//  * This file is part of the uutils coreutils package.
//  *
//  * For the full copyright and license information, please view the LICENSE
//  * file that was distributed with this source code.

// spell-checker:ignore tailable seekable stdlib (stdlib)

use crate::args::Settings;
use crate::chunks::BytesChunkBuffer;
use crate::paths::{HeaderPrinter, PathExtTail};
use crate::text;
use std::collections::hash_map::Keys;
use std::collections::HashMap;
use std::fs::{File, Metadata};
use std::io::{stdout, BufRead, BufReader, BufWriter};
use std::path::{Path, PathBuf};
use uucore::error::UResult;

/// Data structure to keep a handle on files to follow.
/// `last` always holds the path/key of the last file that was printed from.
/// The keys of the HashMap can point to an existing file path (normal case),
/// or stdin ("-"), or to a non existing path (--retry).
/// For existing files, all keys in the HashMap are absolute Paths.
pub struct FileHandling {
    map: HashMap<PathBuf, PathData>,
    last: Option<PathBuf>,
    header_printer: HeaderPrinter,
}

impl FileHandling {
    pub fn from(settings: &Settings) -> Self {
        Self {
            map: HashMap::with_capacity(settings.inputs.len()),
            last: None,
            header_printer: HeaderPrinter::new(settings.verbose, false),
        }
    }

    /// Wrapper for HashMap::insert using Path::canonicalize
    pub fn insert(&mut self, k: &Path, v: PathData, update_last: bool) {
        let k = Self::canonicalize_path(k);
        if update_last {
            self.last = Some(k.to_owned());
        }
        let _ = self.map.insert(k, v);
    }

    /// Wrapper for HashMap::remove using Path::canonicalize
    pub fn remove(&mut self, k: &Path) -> PathData {
        self.map.remove(&Self::canonicalize_path(k)).unwrap()
    }

    /// Wrapper for HashMap::get using Path::canonicalize
    pub fn get(&self, k: &Path) -> &PathData {
        self.map.get(&Self::canonicalize_path(k)).unwrap()
    }

    /// Wrapper for HashMap::get_mut using Path::canonicalize
    pub fn get_mut(&mut self, k: &Path) -> &mut PathData {
        self.map.get_mut(&Self::canonicalize_path(k)).unwrap()
    }

    /// Canonicalize `path` if it is not already an absolute path
    fn canonicalize_path(path: &Path) -> PathBuf {
        if path.is_relative() && !path.is_stdin() {
            if let Ok(p) = path.canonicalize() {
                return p;
            }
        }
        path.to_owned()
    }

    pub fn get_mut_metadata(&mut self, path: &Path) -> Option<&Metadata> {
        self.get_mut(path).metadata.as_ref()
    }

    pub fn keys(&self) -> Keys<PathBuf, PathData> {
        self.map.keys()
    }

    pub fn contains_key(&self, k: &Path) -> bool {
        self.map.contains_key(k)
    }

    pub fn get_last(&self) -> Option<&PathBuf> {
        self.last.as_ref()
    }

    /// Return true if there is only stdin remaining
    pub fn only_stdin_remaining(&self) -> bool {
        self.map.len() == 1 && (self.map.contains_key(Path::new(text::DASH)))
    }

    /// Return true if there is at least one "tailable" path (or stdin) remaining
    pub fn files_remaining(&self) -> bool {
        for path in self.map.keys() {
            if path.is_tailable() || path.is_stdin() {
                return true;
            }
        }
        false
    }

    /// Returns true if there are no files remaining
    pub fn no_files_remaining(&self, settings: &Settings) -> bool {
        self.map.is_empty() || !self.files_remaining() && !settings.retry
    }

    /// Set `reader` to None to indicate that `path` is not an existing file anymore.
    pub fn reset_reader(&mut self, path: &Path) {
        self.get_mut(path).reader = None;
    }

    /// Reopen the file at the monitored `path`
    pub fn update_reader(&mut self, path: &Path) -> UResult<()> {
        /*
        BUG: If it's not necessary to reopen a file, GNU's tail calls seek to offset 0.
        However we can't call seek here because `BufRead` does not implement `Seek`.
        As a workaround we always reopen the file even though this might not always
        be necessary.
        */
        self.get_mut(path)
            .reader
            .replace(Box::new(BufReader::new(File::open(path)?)));
        Ok(())
    }

    /// Reload metadata from `path`, or `metadata`
    pub fn update_metadata(&mut self, path: &Path, metadata: Option<Metadata>) {
        self.get_mut(path).metadata = if metadata.is_some() {
            metadata
        } else {
            path.metadata().ok()
        };
    }

    /// Read new data from `path` and print it to stdout
    pub fn tail_file(&mut self, path: &Path, verbose: bool) -> UResult<bool> {
        let mut chunks = BytesChunkBuffer::new(u64::MAX);
        if let Some(reader) = self.get_mut(path).reader.as_mut() {
            chunks.fill(reader)?;
        }
        if chunks.has_data() {
            if self.needs_header(path, verbose) {
                let display_name = self.get(path).display_name.clone();
                self.header_printer.print(display_name.as_str());
            }

            let stdout = stdout();
            let writer = BufWriter::new(stdout.lock());
            chunks.print(writer)?;

            self.last.replace(path.to_owned());
            self.update_metadata(path, None);
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Decide if printing `path` needs a header based on when it was last printed
    pub fn needs_header(&self, path: &Path, verbose: bool) -> bool {
        if verbose {
            if let Some(ref last) = self.last {
                return !last.eq(&path);
            } else {
                return true;
            }
        }
        false
    }
}

/// Data structure to keep a handle on the BufReader, Metadata
/// and the display_name (header_name) of files that are being followed.
pub struct PathData {
    pub reader: Option<Box<dyn BufRead>>,
    pub metadata: Option<Metadata>,
    pub display_name: String,
}

impl PathData {
    pub fn new(
        reader: Option<Box<dyn BufRead>>,
        metadata: Option<Metadata>,
        display_name: &str,
    ) -> Self {
        Self {
            reader,
            metadata,
            display_name: display_name.to_owned(),
        }
    }
    pub fn from_other_with_path(data: Self, path: &Path) -> Self {
        // Remove old reader
        let old_reader = data.reader;
        let reader = if old_reader.is_some() {
            // Use old reader with the same file descriptor if there is one
            old_reader
        } else if let Ok(file) = File::open(path) {
            // Open new file tail from start
            Some(Box::new(BufReader::new(file)) as Box<dyn BufRead>)
        } else {
            // Probably file was renamed/moved or removed again
            None
        };

        Self::new(reader, path.metadata().ok(), data.display_name.as_str())
    }
}