use crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(windows, allow(dead_code))] pub enum FileType { BlockDevice, CharDevice, Directory { uid: bool }, File { uid: bool, exec: bool }, SymLink { is_dir: bool }, Pipe, Socket, Special, } impl FileType { #[cfg(windows)] const EXECUTABLE_EXTENSIONS: &'static [&'static str] = &["exe", "msi", "bat", "ps1"]; #[cfg(unix)] pub fn new( meta: &Metadata, symlink_meta: Option<&Metadata>, permissions: &crate::meta::Permissions, ) -> Self { use std::os::unix::fs::FileTypeExt; let file_type = meta.file_type(); if file_type.is_file() { FileType::File { exec: permissions.is_executable(), uid: permissions.setuid, } } else if file_type.is_dir() { FileType::Directory { uid: permissions.setuid, } } else if file_type.is_fifo() { FileType::Pipe } else if file_type.is_symlink() { FileType::SymLink { // if broken, defaults to false is_dir: symlink_meta.map(|m| m.is_dir()).unwrap_or_default(), } } else if file_type.is_char_device() { FileType::CharDevice } else if file_type.is_block_device() { FileType::BlockDevice } else if file_type.is_socket() { FileType::Socket } else { FileType::Special } } #[cfg(windows)] pub fn new(meta: &Metadata, symlink_meta: Option<&Metadata>, path: &std::path::Path) -> Self { let file_type = meta.file_type(); if file_type.is_file() { let exec = path .extension() .map(|ext| { Self::EXECUTABLE_EXTENSIONS .iter() .map(std::ffi::OsStr::new) .any(|exec_ext| ext == exec_ext) }) .unwrap_or(false); FileType::File { exec, uid: false } } else if file_type.is_dir() { FileType::Directory { uid: false } } else if file_type.is_symlink() { FileType::SymLink { // if broken, defaults to false is_dir: symlink_meta.map(|m| m.is_dir()).unwrap_or_default(), } } else { FileType::Special } } pub fn is_dirlike(self) -> bool { matches!( self, FileType::Directory { .. } | FileType::SymLink { is_dir: true } ) } } impl FileType { pub fn render(self, colors: &Colors) -> ColoredString { match self { FileType::File { exec, .. } => colors.colorize('.', &Elem::File { exec, uid: false }), FileType::Directory { .. } => colors.colorize('d', &Elem::Dir { uid: false }), FileType::Pipe => colors.colorize('|', &Elem::Pipe), FileType::SymLink { .. } => colors.colorize('l', &Elem::SymLink), FileType::BlockDevice => colors.colorize('b', &Elem::BlockDevice), FileType::CharDevice => colors.colorize('c', &Elem::CharDevice), FileType::Socket => colors.colorize('s', &Elem::Socket), FileType::Special => colors.colorize('?', &Elem::Special), } } } #[cfg(test)] mod test { use super::FileType; use crate::color::{Colors, ThemeOption}; #[cfg(unix)] use crate::flags::PermissionFlag; #[cfg(unix)] use crate::meta::permissions_or_attributes::PermissionsOrAttributes; #[cfg(unix)] use crate::meta::Permissions; use crossterm::style::{Color, Stylize}; use std::fs::File; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(unix)] use std::os::unix::net::UnixListener; #[cfg(unix)] use std::process::Command; use tempfile::tempdir; #[test] #[cfg(unix)] // Windows uses different default permissions fn test_file_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( ".".to_string().with(Color::AnsiValue(184)), file_type.render(&colors) ); } #[test] fn test_dir_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); #[cfg(not(windows))] let meta = crate::meta::Meta::from_path(tmp_dir.path(), false, PermissionFlag::Rwx) .expect("failed to get tempdir path"); let metadata = tmp_dir.path().metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); #[cfg(not(windows))] let file_type = match meta.permissions_or_attributes { Some(PermissionsOrAttributes::Permissions(permissions)) => { FileType::new(&metadata, None, &permissions) } _ => panic!("unexpected"), }; #[cfg(windows)] let file_type = FileType::new(&metadata, None, tmp_dir.path()); assert_eq!( "d".to_string().with(Color::AnsiValue(33)), file_type.render(&colors) ); } #[test] #[cfg(unix)] // Symlink support is *hard* on Windows fn test_symlink_type_file() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.tmp"); File::create(&file_path).expect("failed to create file"); // Create the symlink let symlink_path = tmp_dir.path().join("target.tmp"); symlink(&file_path, &symlink_path).expect("failed to create symlink"); let meta = symlink_path .symlink_metadata() .expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, Some(&meta), &Permissions::from(&meta)); assert_eq!( "l".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(unix)] fn test_symlink_type_dir() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create directory let dir_path = tmp_dir.path().join("dir.d"); std::fs::create_dir(&dir_path).expect("failed to create dir"); // Create symlink let symlink_path = tmp_dir.path().join("target.d"); symlink(&dir_path, &symlink_path).expect("failed to create symlink"); let meta = symlink_path .symlink_metadata() .expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, Some(&meta), &Permissions::from(&meta)); assert_eq!( "l".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(unix)] // Windows pipes aren't like Unix pipes fn test_pipe_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the pipe; let pipe_path = tmp_dir.path().join("pipe.tmp"); let success = Command::new("mkfifo") .arg(&pipe_path) .status() .expect("failed to exec mkfifo") .success(); assert!(success, "failed to exec mkfifo"); let meta = pipe_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( "|".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(feature = "sudo")] fn test_char_device_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the char device; let char_device_path = tmp_dir.path().join("char-device.tmp"); let success = Command::new("sudo") .arg("mknod") .arg(&char_device_path) .arg("c") .arg("89") .arg("1") .status() .expect("failed to exec mknod") .success(); assert!(success, "failed to exec mknod"); let meta = char_device_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( "c".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[test] #[cfg(unix)] // Sockets don't work the same way on Windows fn test_socket_type() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the socket; let socket_path = tmp_dir.path().join("socket.tmp"); UnixListener::bind(&socket_path).expect("failed to create the socket"); let meta = socket_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &Permissions::from(&meta)); assert_eq!( "s".to_string().with(Color::AnsiValue(44)), file_type.render(&colors) ); } #[cfg(windows)] #[test] fn test_file_executable() { let tmp_dir = tempdir().expect("failed to create temp dir"); for ext in FileType::EXECUTABLE_EXTENSIONS { // Create the file; let file_path = tmp_dir.path().join(format!("file.{ext}")); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &file_path); assert_eq!( ".".to_string().with(Color::AnsiValue(40)), file_type.render(&colors) ); } } #[cfg(windows)] #[test] fn test_file_not_executable() { let tmp_dir = tempdir().expect("failed to create temp dir"); // Create the file; let file_path = tmp_dir.path().join("file.txt"); File::create(&file_path).expect("failed to create file"); let meta = file_path.metadata().expect("failed to get metas"); let colors = Colors::new(ThemeOption::NoLscolors); let file_type = FileType::new(&meta, None, &file_path); assert_eq!( ".".to_string().with(Color::AnsiValue(184)), file_type.render(&colors) ); } }