Skip to main content

fm/modes/fs/
fileinfo.rs

1use std::fs::{symlink_metadata, DirEntry, Metadata};
2use std::os::unix::fs::{FileTypeExt, MetadataExt};
3use std::path;
4use std::sync::Arc;
5
6use anyhow::{Context, Result};
7use chrono::offset::Local;
8use chrono::DateTime;
9use ratatui::style::Style;
10
11use crate::config::{extension_color, FileStyle};
12use crate::modes::{human_size, permission_mode_to_str, ToPath, Users};
13
14type Valid = bool;
15
16/// Different kind of files
17#[derive(Debug, Clone, Copy)]
18pub enum FileKind<Valid> {
19    /// Classic files.
20    NormalFile,
21    /// Folder
22    Directory,
23    /// Block devices like /sda1
24    BlockDevice,
25    /// Char devices like /dev/null
26    CharDevice,
27    /// Named pipes
28    Fifo,
29    /// File socket
30    Socket,
31    /// symlink
32    SymbolicLink(Valid),
33}
34
35impl FileKind<Valid> {
36    /// Returns a new `FileKind` depending on metadata.
37    /// Only linux files have some of those metadata
38    /// since we rely on `std::fs::MetadataExt`.
39    pub fn new(meta: &Metadata, filepath: &path::Path) -> Self {
40        if meta.file_type().is_dir() {
41            Self::Directory
42        } else if meta.file_type().is_block_device() {
43            Self::BlockDevice
44        } else if meta.file_type().is_socket() {
45            Self::Socket
46        } else if meta.file_type().is_char_device() {
47            Self::CharDevice
48        } else if meta.file_type().is_fifo() {
49            Self::Fifo
50        } else if meta.file_type().is_symlink() {
51            Self::SymbolicLink(is_valid_symlink(filepath))
52        } else {
53            Self::NormalFile
54        }
55    }
56    /// Returns the expected first symbol from `ln -l` line.
57    /// d for directory, s for socket, . for file, c for char device,
58    /// b for block, l for links.
59    pub fn dir_symbol(&self) -> char {
60        match self {
61            Self::Fifo => 'p',
62            Self::Socket => 's',
63            Self::Directory => 'd',
64            Self::NormalFile => '.',
65            Self::CharDevice => 'c',
66            Self::BlockDevice => 'b',
67            Self::SymbolicLink(_) => 'l',
68        }
69    }
70
71    fn sortable_char(&self) -> char {
72        match self {
73            Self::Directory => 'a',
74            Self::NormalFile => 'b',
75            Self::SymbolicLink(_) => 'c',
76            Self::BlockDevice => 'd',
77            Self::CharDevice => 'e',
78            Self::Socket => 'f',
79            Self::Fifo => 'g',
80        }
81    }
82
83    pub fn long_description(&self) -> &'static str {
84        match self {
85            Self::Fifo => "fifo",
86            Self::Socket => "socket",
87            Self::Directory => "directory",
88            Self::NormalFile => "normal file",
89            Self::CharDevice => "char device",
90            Self::BlockDevice => "block device",
91            Self::SymbolicLink(_) => "symbolic link",
92        }
93    }
94
95    #[rustfmt::skip]
96    pub fn size_description(&self) -> &'static str {
97        match self {
98            Self::Fifo              => "Size:       ",
99            Self::Socket            => "Size:       ",
100            Self::Directory         => "Elements:   ",
101            Self::NormalFile        => "Size:       ",
102            Self::CharDevice        => "Major,Minor:",
103            Self::BlockDevice       => "Major,Minor:",
104            Self::SymbolicLink(_)   => "Size:       ",
105        }
106    }
107
108    pub fn is_normal_file(&self) -> bool {
109        matches!(self, Self::NormalFile)
110    }
111}
112
113/// Different kind of display for the size column.
114/// ls -lh display a human readable size for normal files,
115/// nothing should be displayed for a directory,
116/// Major & Minor driver versions are used for CharDevice & BlockDevice
117#[derive(Clone, Debug)]
118pub enum SizeColumn {
119    /// Used for normal files. It's the size in bytes.
120    Size(u64),
121    /// Used for directories, nothing is displayed
122    EntryCount(u64),
123    /// Use for CharDevice and BlockDevice.
124    /// It's the major & minor driver versions.
125    MajorMinor((u8, u8)),
126}
127
128impl std::fmt::Display for SizeColumn {
129    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
130        match self {
131            Self::Size(bytes) => write!(f, "   {hs}", hs = human_size(*bytes)),
132            Self::EntryCount(count) => write!(f, "{hs:>6} ", hs = count),
133            Self::MajorMinor((major, minor)) => write!(f, "{major:>3},{minor:<3}"),
134        }
135    }
136}
137
138impl SizeColumn {
139    fn new(size: u64, metadata: &Metadata, file_kind: &FileKind<Valid>) -> Self {
140        match file_kind {
141            FileKind::Directory => Self::EntryCount(size),
142            FileKind::CharDevice | FileKind::BlockDevice => Self::MajorMinor(major_minor(metadata)),
143            _ => Self::Size(size),
144        }
145    }
146
147    pub fn trimed(&self) -> String {
148        format!("{self}").trim().to_owned()
149    }
150}
151
152/// Infos about a file
153/// We read and keep tracks every displayable information about
154/// a file.
155#[derive(Clone, Debug)]
156pub struct FileInfo {
157    /// Full path of the file
158    pub path: Arc<path::Path>,
159    /// Filename
160    pub filename: Arc<str>,
161    /// File size as a `String`, already human formated.
162    /// For char devices and block devices we display major & minor like ls.
163    pub size_column: SizeColumn,
164    /// True size of a file, not formated
165    pub true_size: u64,
166    /// Owner name of the file.
167    pub owner: Arc<str>,
168    /// Group name of the file.
169    pub group: Arc<str>,
170    /// System time of last modification
171    pub system_time: Arc<str>,
172    /// What kind of file is this ?
173    pub file_kind: FileKind<Valid>,
174    /// Extension of the file. `""` for a directory.
175    pub extension: Arc<str>,
176}
177
178impl FileInfo {
179    pub fn new(path: &path::Path, users: &Users) -> Result<Self> {
180        let filename = extract_filename(path)?;
181        let metadata = symlink_metadata(path)?;
182        let true_size = true_size(path, &metadata);
183        let path = Arc::from(path);
184        let owner = extract_owner(&metadata, users);
185        let group = extract_group(&metadata, users);
186        let system_time = extract_datetime(metadata.modified()?)?;
187        let file_kind = FileKind::new(&metadata, &path);
188        let size_column = SizeColumn::new(true_size, &metadata, &file_kind);
189        let extension = extract_extension(&path).into();
190
191        Ok(FileInfo {
192            path,
193            filename,
194            size_column,
195            true_size,
196            owner,
197            group,
198            system_time,
199            file_kind,
200            extension,
201        })
202    }
203
204    /// Reads every information about a file from its metadata and returs
205    /// a new `FileInfo` object if we can create one.
206    pub fn from_direntry(direntry: &DirEntry, users: &Users) -> Result<FileInfo> {
207        Self::new(&direntry.path(), users)
208    }
209
210    /// Creates a fileinfo from a path and a filename.
211    /// The filename is used when we create the fileinfo for "." and ".." in every folder.
212    pub fn from_path_with_name(path: &path::Path, filename: &str, users: &Users) -> Result<Self> {
213        let mut file_info = Self::new(path, users)?;
214        file_info.filename = Arc::from(filename);
215        Ok(file_info)
216    }
217
218    /// Symlink metadata of the file.
219    /// Doesn't follow the symlinks.
220    /// Correspond to `lstat` function on Linux.
221    /// See [`std::fs::symlink_metadata`].
222    ///
223    /// # Errors
224    ///
225    /// Could return an error if the file doesn't exist or if the user can't stat it.
226    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
227        symlink_metadata(&self.path)
228    }
229
230    /// Returns the Inode number.
231    ///
232    /// Returns 0 if the metadata can't be read.
233    pub fn ino(&self) -> u64 {
234        self.metadata()
235            .map(|metadata| metadata.ino())
236            .unwrap_or_default()
237    }
238
239    /// String representation of file permissions
240    pub fn permissions(&self) -> Result<Arc<str>> {
241        Ok(permission_mode_to_str(self.metadata()?.mode()))
242    }
243
244    /// A formated filename where the "kind" of file
245    /// (directory, char device, block devive, fifo, socket, normal)
246    /// is prepend to the name, allowing a "sort by kind" method.
247    pub fn kind_format(&self) -> String {
248        format!(
249            "{c}{filename}",
250            c = self.file_kind.sortable_char(),
251            filename = self.filename
252        )
253    }
254    /// Format the file line.
255    /// Since files can have different owners in the same directory, we need to
256    /// know the maximum size of owner column for formatting purpose.
257    #[inline]
258    pub fn format_metadata(&self, owner_col_width: usize, group_col_width: usize) -> String {
259        let mut repr = self.format_base(owner_col_width, group_col_width);
260        repr.push(' ');
261        repr.push_str(&self.filename);
262        if let FileKind::SymbolicLink(_) = self.file_kind {
263            self.expand_symlink(&mut repr);
264        }
265        repr
266    }
267
268    /// Append the destination link to the content.
269    /// If the link is broken, it appends "broken link"
270    ///
271    /// # Warning
272    ///
273    /// This method assumes the current file to be a _symlink_ and shouldn't be called otherwise.
274    /// It's the responsability of the **caller** to ensure it's not called for other file kinds.
275    pub fn expand_symlink(&self, repr: &mut String) {
276        match std::fs::read_link(&self.path) {
277            Ok(dest) if dest.exists() => {
278                repr.push_str(&format!(" -> {dest}", dest = dest.display()))
279            }
280            _ => {
281                repr.push_str("  broken link");
282            }
283        }
284    }
285
286    pub fn format_no_group(&self, owner_col_width: usize) -> String {
287        let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
288        let permissions = self
289            .permissions()
290            .unwrap_or_else(|_| Arc::from("?????????"));
291        format!(
292            "{dir_symbol}{permissions} {file_size} {owner:<owner_col_width$} {system_time}",
293            dir_symbol = self.dir_symbol(),
294            file_size = self.size_column,
295            system_time = self.system_time,
296        )
297    }
298
299    pub fn format_no_permissions(&self, owner_col_width: usize) -> String {
300        let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
301        format!(
302            "{file_size} {owner:<owner_col_width$} {system_time}",
303            file_size = self.size_column,
304            system_time = self.system_time,
305        )
306    }
307
308    pub fn format_no_owner(&self) -> String {
309        format!("{file_size}", file_size = self.size_column)
310    }
311
312    pub fn format_base(&self, owner_col_width: usize, group_col_width: usize) -> String {
313        let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
314        let group = format!("{group:.group_col_width$}", group = self.group,);
315        let permissions = self
316            .permissions()
317            .unwrap_or_else(|_| Arc::from("?????????"));
318        format!(
319            "{dir_symbol}{permissions} {file_size} {owner:<owner_col_width$} {group:<group_col_width$} {system_time}",
320            dir_symbol = self.dir_symbol(),
321            file_size = self.size_column,
322            system_time = self.system_time,
323        )
324    }
325    /// Format the metadata line, without the filename.
326    /// Owned & Group have fixed width of 6.
327    pub fn format_no_filename(&self) -> String {
328        self.format_base(6, 6)
329    }
330
331    pub fn dir_symbol(&self) -> char {
332        self.file_kind.dir_symbol()
333    }
334
335    /// True iff the file is hidden (aka starts with a '.').
336    pub fn is_hidden(&self) -> bool {
337        self.filename.starts_with('.')
338    }
339
340    pub fn is_dir(&self) -> bool {
341        matches!(self.file_kind, FileKind::Directory)
342    }
343
344    pub fn is_symlink(&self) -> bool {
345        matches!(self.file_kind, FileKind::SymbolicLink(_))
346    }
347
348    /// True iff the parent of the file is root.
349    /// It's also true for the root folder itself.
350    fn is_root_or_parent_is_root(&self) -> bool {
351        match self.path.as_ref().parent() {
352            None => true,
353            Some(parent) => parent == path::Path::new("/"),
354        }
355    }
356
357    /// Formated proper name.
358    /// "/ " for `.`
359    pub fn filename_without_dot_dotdot(&self) -> String {
360        let sep = if self.is_root_or_parent_is_root() {
361            ""
362        } else {
363            "/"
364        };
365        match self.filename.as_ref() {
366            "." => format!("{sep} "),
367            ".." => self.filename_without_dotdot(),
368            _ => format!("{sep}{name} ", name = self.filename,),
369        }
370    }
371
372    fn filename_without_dotdot(&self) -> String {
373        let Ok(filename) = extract_filename(&self.path) else {
374            return "/ ".to_string();
375        };
376        format!("/{filename} ")
377    }
378
379    #[inline]
380    pub fn style(&self, styles: &'static FileStyle) -> Style {
381        if matches!(self.file_kind, FileKind::NormalFile) {
382            return extension_color(&self.extension).into();
383        }
384        match self.file_kind {
385            FileKind::Directory => styles.directory,
386            FileKind::BlockDevice => styles.block,
387            FileKind::CharDevice => styles.char,
388            FileKind::Fifo => styles.fifo,
389            FileKind::Socket => styles.socket,
390            FileKind::SymbolicLink(true) => styles.symlink,
391            FileKind::SymbolicLink(false) => styles.broken,
392            _ => unreachable!("Should be done already"),
393        }
394    }
395}
396
397/// True if the file isn't hidden.
398pub fn is_not_hidden(entry: &DirEntry) -> Result<bool> {
399    let is_hidden = !entry
400        .file_name()
401        .to_str()
402        .context("Couldn't read filename")?
403        .starts_with('.');
404    Ok(is_hidden)
405}
406
407fn extract_filename(path: &path::Path) -> Result<Arc<str>> {
408    let s = path
409        .file_name()
410        .unwrap_or_default()
411        .to_str()
412        .context(format!("Couldn't read filename of {p}", p = path.display()))?;
413    Ok(Arc::from(s))
414}
415
416/// Returns the modified time.
417pub fn extract_datetime(time: std::time::SystemTime) -> Result<Arc<str>> {
418    let datetime: DateTime<Local> = time.into();
419    Ok(Arc::from(
420        format!("{}", datetime.format("%Y/%m/%d %T")).as_str(),
421    ))
422}
423
424/// Reads the owner name and returns it as a string.
425/// If it's not possible to get the owner name (happens if the owner exists on a remote machine but not on host),
426/// it returns the uid as a  `Result<String>`.
427fn extract_owner(metadata: &Metadata, users: &Users) -> Arc<str> {
428    match users.get_user_by_uid(metadata.uid()) {
429        Some(name) => Arc::from(name.as_str()),
430        None => Arc::from(format!("{}", metadata.uid()).as_str()),
431    }
432}
433
434/// Reads the group name and returns it as a string.
435/// If it's not possible to get the group name (happens if the group exists on a remote machine but not on host),
436/// it returns the gid as a  `Result<String>`.
437fn extract_group(metadata: &Metadata, users: &Users) -> Arc<str> {
438    match users.get_group_by_gid(metadata.gid()) {
439        Some(name) => Arc::from(name.as_str()),
440        None => Arc::from(format!("{}", metadata.gid()).as_str()),
441    }
442}
443
444/// Size of a file, number of entries of a directory
445fn true_size(path: &path::Path, metadata: &Metadata) -> u64 {
446    if path.is_dir() {
447        count_entries(path).unwrap_or_default()
448    } else {
449        extract_file_size(metadata)
450    }
451}
452
453/// Returns the file size.
454fn extract_file_size(metadata: &Metadata) -> u64 {
455    metadata.len()
456}
457
458/// Number of elements of a directory.
459///
460/// # Errors
461///
462/// Will fail if the provided path isn't a directory
463/// or doesn't exist.
464fn count_entries(path: &path::Path) -> Result<u64> {
465    Ok(std::fs::read_dir(path)?.count() as u64)
466}
467
468/// Extract the major & minor driver version of a special file.
469/// It's used for CharDevice & BlockDevice
470fn major_minor(metadata: &Metadata) -> (u8, u8) {
471    let device_ids = metadata.rdev().to_be_bytes();
472    let major = device_ids[6];
473    let minor = device_ids[7];
474    (major, minor)
475}
476
477/// Extract the optional extension from a filename.
478/// Returns empty &str aka "" if the file has no extension.
479pub fn extract_extension(path: &path::Path) -> &str {
480    path.extension()
481        .and_then(std::ffi::OsStr::to_str)
482        .unwrap_or_default()
483}
484
485/// true iff the path is a valid symlink (pointing to an existing file).
486fn is_valid_symlink(path: &path::Path) -> bool {
487    matches!(std::fs::read_link(path), Ok(dest) if dest.exists())
488}
489
490impl ToPath for FileInfo {
491    fn to_path(&self) -> &path::Path {
492        self.path.as_ref()
493    }
494}