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, FILE_STYLES};
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        self.expand_symlink(&mut repr);
263        repr
264    }
265
266    fn expand_symlink(&self, repr: &mut String) {
267        if let FileKind::SymbolicLink(_) = self.file_kind {
268            match std::fs::read_link(&self.path) {
269                Ok(dest) if dest.exists() => {
270                    repr.push_str(&format!(" -> {dest}", dest = dest.display()))
271                }
272                _ => repr.push_str("  broken link"),
273            }
274        }
275    }
276
277    pub fn format_no_group(&self, owner_col_width: usize) -> String {
278        let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
279        let permissions = self
280            .permissions()
281            .unwrap_or_else(|_| Arc::from("?????????"));
282        format!(
283            "{dir_symbol}{permissions} {file_size} {owner:<owner_col_width$} {system_time}",
284            dir_symbol = self.dir_symbol(),
285            file_size = self.size_column,
286            system_time = self.system_time,
287        )
288    }
289
290    pub fn format_no_permissions(&self, owner_col_width: usize) -> String {
291        let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
292        format!(
293            "{file_size} {owner:<owner_col_width$} {system_time}",
294            file_size = self.size_column,
295            system_time = self.system_time,
296        )
297    }
298
299    pub fn format_no_owner(&self) -> String {
300        format!("{file_size}", file_size = self.size_column)
301    }
302
303    pub fn format_base(&self, owner_col_width: usize, group_col_width: usize) -> String {
304        let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
305        let group = format!("{group:.group_col_width$}", group = self.group,);
306        let permissions = self
307            .permissions()
308            .unwrap_or_else(|_| Arc::from("?????????"));
309        format!(
310            "{dir_symbol}{permissions} {file_size} {owner:<owner_col_width$} {group:<group_col_width$} {system_time}",
311            dir_symbol = self.dir_symbol(),
312            file_size = self.size_column,
313            system_time = self.system_time,
314        )
315    }
316    /// Format the metadata line, without the filename.
317    /// Owned & Group have fixed width of 6.
318    pub fn format_no_filename(&self) -> String {
319        self.format_base(6, 6)
320    }
321
322    pub fn dir_symbol(&self) -> char {
323        self.file_kind.dir_symbol()
324    }
325
326    /// True iff the file is hidden (aka starts with a '.').
327    pub fn is_hidden(&self) -> bool {
328        self.filename.starts_with('.')
329    }
330
331    pub fn is_dir(&self) -> bool {
332        matches!(self.file_kind, FileKind::Directory)
333    }
334
335    /// True iff the parent of the file is root.
336    /// It's also true for the root folder itself.
337    fn is_root_or_parent_is_root(&self) -> bool {
338        match self.path.as_ref().parent() {
339            None => true,
340            Some(parent) => parent == path::Path::new("/"),
341        }
342    }
343
344    /// Formated proper name.
345    /// "/ " for `.`
346    pub fn filename_without_dot_dotdot(&self) -> String {
347        let sep = if self.is_root_or_parent_is_root() {
348            ""
349        } else {
350            "/"
351        };
352        match self.filename.as_ref() {
353            "." => format!("{sep} "),
354            ".." => self.filename_without_dotdot(),
355            _ => format!("{sep}{name} ", name = self.filename,),
356        }
357    }
358
359    fn filename_without_dotdot(&self) -> String {
360        let Ok(filename) = extract_filename(&self.path) else {
361            return "/ ".to_string();
362        };
363        format!("/{filename} ")
364    }
365
366    #[inline]
367    pub fn style(&self) -> Style {
368        if matches!(self.file_kind, FileKind::NormalFile) {
369            return extension_color(&self.extension).into();
370        }
371        let styles = FILE_STYLES.get().expect("Colors should be set");
372        match self.file_kind {
373            FileKind::Directory => styles.directory,
374            FileKind::BlockDevice => styles.block,
375            FileKind::CharDevice => styles.char,
376            FileKind::Fifo => styles.fifo,
377            FileKind::Socket => styles.socket,
378            FileKind::SymbolicLink(true) => styles.symlink,
379            FileKind::SymbolicLink(false) => styles.broken,
380            _ => unreachable!("Should be done already"),
381        }
382    }
383}
384
385/// True if the file isn't hidden.
386pub fn is_not_hidden(entry: &DirEntry) -> Result<bool> {
387    let is_hidden = !entry
388        .file_name()
389        .to_str()
390        .context("Couldn't read filename")?
391        .starts_with('.');
392    Ok(is_hidden)
393}
394
395fn extract_filename(path: &path::Path) -> Result<Arc<str>> {
396    let s = path
397        .file_name()
398        .unwrap_or_default()
399        .to_str()
400        .context(format!("Couldn't read filename of {p}", p = path.display()))?;
401    Ok(Arc::from(s))
402}
403
404/// Returns the modified time.
405pub fn extract_datetime(time: std::time::SystemTime) -> Result<Arc<str>> {
406    let datetime: DateTime<Local> = time.into();
407    Ok(Arc::from(
408        format!("{}", datetime.format("%Y/%m/%d %T")).as_str(),
409    ))
410}
411
412/// Reads the owner name and returns it as a string.
413/// If it's not possible to get the owner name (happens if the owner exists on a remote machine but not on host),
414/// it returns the uid as a  `Result<String>`.
415fn extract_owner(metadata: &Metadata, users: &Users) -> Arc<str> {
416    match users.get_user_by_uid(metadata.uid()) {
417        Some(name) => Arc::from(name.as_str()),
418        None => Arc::from(format!("{}", metadata.uid()).as_str()),
419    }
420}
421
422/// Reads the group name and returns it as a string.
423/// If it's not possible to get the group name (happens if the group exists on a remote machine but not on host),
424/// it returns the gid as a  `Result<String>`.
425fn extract_group(metadata: &Metadata, users: &Users) -> Arc<str> {
426    match users.get_group_by_gid(metadata.gid()) {
427        Some(name) => Arc::from(name.as_str()),
428        None => Arc::from(format!("{}", metadata.gid()).as_str()),
429    }
430}
431
432/// Size of a file, number of entries of a directory
433fn true_size(path: &path::Path, metadata: &Metadata) -> u64 {
434    if path.is_dir() {
435        count_entries(path).unwrap_or_default()
436    } else {
437        extract_file_size(metadata)
438    }
439}
440
441/// Returns the file size.
442fn extract_file_size(metadata: &Metadata) -> u64 {
443    metadata.len()
444}
445
446/// Number of elements of a directory.
447///
448/// # Errors
449///
450/// Will fail if the provided path isn't a directory
451/// or doesn't exist.
452fn count_entries(path: &path::Path) -> Result<u64> {
453    Ok(std::fs::read_dir(path)?.count() as u64)
454}
455
456/// Extract the major & minor driver version of a special file.
457/// It's used for CharDevice & BlockDevice
458fn major_minor(metadata: &Metadata) -> (u8, u8) {
459    let device_ids = metadata.rdev().to_be_bytes();
460    let major = device_ids[6];
461    let minor = device_ids[7];
462    (major, minor)
463}
464
465/// Extract the optional extension from a filename.
466/// Returns empty &str aka "" if the file has no extension.
467pub fn extract_extension(path: &path::Path) -> &str {
468    path.extension()
469        .and_then(std::ffi::OsStr::to_str)
470        .unwrap_or_default()
471}
472
473/// true iff the path is a valid symlink (pointing to an existing file).
474fn is_valid_symlink(path: &path::Path) -> bool {
475    matches!(std::fs::read_link(path), Ok(dest) if dest.exists())
476}
477
478impl ToPath for FileInfo {
479    fn to_path(&self) -> &path::Path {
480        self.path.as_ref()
481    }
482}