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#[derive(Debug, Clone, Copy)]
18pub enum FileKind<Valid> {
19 NormalFile,
21 Directory,
23 BlockDevice,
25 CharDevice,
27 Fifo,
29 Socket,
31 SymbolicLink(Valid),
33}
34
35impl FileKind<Valid> {
36 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 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#[derive(Clone, Debug)]
118pub enum SizeColumn {
119 Size(u64),
121 EntryCount(u64),
123 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#[derive(Clone, Debug)]
156pub struct FileInfo {
157 pub path: Arc<path::Path>,
159 pub filename: Arc<str>,
161 pub size_column: SizeColumn,
164 pub true_size: u64,
166 pub owner: Arc<str>,
168 pub group: Arc<str>,
170 pub system_time: Arc<str>,
172 pub file_kind: FileKind<Valid>,
174 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 pub fn from_direntry(direntry: &DirEntry, users: &Users) -> Result<FileInfo> {
207 Self::new(&direntry.path(), users)
208 }
209
210 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 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
227 symlink_metadata(&self.path)
228 }
229
230 pub fn ino(&self) -> u64 {
234 self.metadata()
235 .map(|metadata| metadata.ino())
236 .unwrap_or_default()
237 }
238
239 pub fn permissions(&self) -> Result<Arc<str>> {
241 Ok(permission_mode_to_str(self.metadata()?.mode()))
242 }
243
244 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 #[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 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 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 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 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
385pub 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
404pub 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
412fn 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
422fn 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
432fn 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
441fn extract_file_size(metadata: &Metadata) -> u64 {
443 metadata.len()
444}
445
446fn count_entries(path: &path::Path) -> Result<u64> {
453 Ok(std::fs::read_dir(path)?.count() as u64)
454}
455
456fn 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
465pub fn extract_extension(path: &path::Path) -> &str {
468 path.extension()
469 .and_then(std::ffi::OsStr::to_str)
470 .unwrap_or_default()
471}
472
473fn 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}