file.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. //! Files, and methods and fields to access their metadata.
  2. use std::io;
  3. #[cfg(unix)]
  4. use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
  5. use std::path::{Path, PathBuf};
  6. use std::time::{Duration, SystemTime, UNIX_EPOCH};
  7. use log::*;
  8. use crate::fs::dir::Dir;
  9. use crate::fs::fields as f;
  10. /// A **File** is a wrapper around one of Rust’s `PathBuf` values, along with
  11. /// associated data about the file.
  12. ///
  13. /// Each file is definitely going to have its filename displayed at least
  14. /// once, have its file extension extracted at least once, and have its metadata
  15. /// information queried at least once, so it makes sense to do all this at the
  16. /// start and hold on to all the information.
  17. pub struct File<'dir> {
  18. /// The filename portion of this file’s path, including the extension.
  19. ///
  20. /// This is used to compare against certain filenames (such as checking if
  21. /// it’s “Makefile” or something) and to highlight only the filename in
  22. /// colour when displaying the path.
  23. pub name: String,
  24. /// The file’s name’s extension, if present, extracted from the name.
  25. ///
  26. /// This is queried many times over, so it’s worth caching it.
  27. pub ext: Option<String>,
  28. /// The path that begat this file.
  29. ///
  30. /// Even though the file’s name is extracted, the path needs to be kept
  31. /// around, as certain operations involve looking up the file’s absolute
  32. /// location (such as searching for compiled files) or using its original
  33. /// path (following a symlink).
  34. pub path: PathBuf,
  35. /// A cached `metadata` (`stat`) call for this file.
  36. ///
  37. /// This too is queried multiple times, and is *not* cached by the OS, as
  38. /// it could easily change between invocations — but exa is so short-lived
  39. /// it’s better to just cache it.
  40. pub metadata: std::fs::Metadata,
  41. /// A reference to the directory that contains this file, if any.
  42. ///
  43. /// Filenames that get passed in on the command-line directly will have no
  44. /// parent directory reference — although they technically have one on the
  45. /// filesystem, we’ll never need to look at it, so it’ll be `None`.
  46. /// However, *directories* that get passed in will produce files that
  47. /// contain a reference to it, which is used in certain operations (such
  48. /// as looking up compiled files).
  49. pub parent_dir: Option<&'dir Dir>,
  50. /// Whether this is one of the two `--all all` directories, `.` and `..`.
  51. ///
  52. /// Unlike all other entries, these are not returned as part of the
  53. /// directory’s children, and are in fact added specifically by exa; this
  54. /// means that they should be skipped when recursing.
  55. pub is_all_all: bool,
  56. }
  57. impl<'dir> File<'dir> {
  58. pub fn from_args<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> io::Result<File<'dir>>
  59. where PD: Into<Option<&'dir Dir>>,
  60. FN: Into<Option<String>>
  61. {
  62. let parent_dir = parent_dir.into();
  63. let name = filename.into().unwrap_or_else(|| File::filename(&path));
  64. let ext = File::ext(&path);
  65. debug!("Statting file {:?}", &path);
  66. let metadata = std::fs::symlink_metadata(&path)?;
  67. let is_all_all = false;
  68. Ok(File { path, parent_dir, metadata, ext, name, is_all_all })
  69. }
  70. pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
  71. let path = parent_dir.path.to_path_buf();
  72. let ext = File::ext(&path);
  73. debug!("Statting file {:?}", &path);
  74. let metadata = std::fs::symlink_metadata(&path)?;
  75. let is_all_all = true;
  76. let parent_dir = Some(parent_dir);
  77. Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all })
  78. }
  79. pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
  80. let ext = File::ext(&path);
  81. debug!("Statting file {:?}", &path);
  82. let metadata = std::fs::symlink_metadata(&path)?;
  83. let is_all_all = true;
  84. let parent_dir = Some(parent_dir);
  85. Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all })
  86. }
  87. /// A file’s name is derived from its string. This needs to handle directories
  88. /// such as `/` or `..`, which have no `file_name` component. So instead, just
  89. /// use the last component as the name.
  90. pub fn filename(path: &Path) -> String {
  91. if let Some(back) = path.components().next_back() {
  92. back.as_os_str().to_string_lossy().to_string()
  93. }
  94. else {
  95. // use the path as fallback
  96. error!("Path {:?} has no last component", path);
  97. path.display().to_string()
  98. }
  99. }
  100. /// Extract an extension from a file path, if one is present, in lowercase.
  101. ///
  102. /// The extension is the series of characters after the last dot. This
  103. /// deliberately counts dotfiles, so the “.git” folder has the extension “git”.
  104. ///
  105. /// ASCII lowercasing is used because these extensions are only compared
  106. /// against a pre-compiled list of extensions which are known to only exist
  107. /// within ASCII, so it’s alright.
  108. fn ext(path: &Path) -> Option<String> {
  109. let name = path.file_name().map(|f| f.to_string_lossy().to_string())?;
  110. name.rfind('.')
  111. .map(|p| name[p + 1 ..]
  112. .to_ascii_lowercase())
  113. }
  114. /// Whether this file is a directory on the filesystem.
  115. pub fn is_directory(&self) -> bool {
  116. self.metadata.is_dir()
  117. }
  118. /// Whether this file is a directory, or a symlink pointing to a directory.
  119. pub fn points_to_directory(&self) -> bool {
  120. if self.is_directory() {
  121. return true;
  122. }
  123. if self.is_link() {
  124. let target = self.link_target();
  125. if let FileTarget::Ok(target) = target {
  126. return target.points_to_directory();
  127. }
  128. }
  129. false
  130. }
  131. /// If this file is a directory on the filesystem, then clone its
  132. /// `PathBuf` for use in one of our own `Dir` values, and read a list of
  133. /// its contents.
  134. ///
  135. /// Returns an IO error upon failure, but this shouldn’t be used to check
  136. /// if a `File` is a directory or not! For that, just use `is_directory()`.
  137. pub fn to_dir(&self) -> io::Result<Dir> {
  138. Dir::read_dir(self.path.clone())
  139. }
  140. /// Whether this file is a regular file on the filesystem — that is, not a
  141. /// directory, a link, or anything else treated specially.
  142. pub fn is_file(&self) -> bool {
  143. self.metadata.is_file()
  144. }
  145. /// Whether this file is both a regular file *and* executable for the
  146. /// current user. An executable file has a different purpose from an
  147. /// executable directory, so they should be highlighted differently.
  148. #[cfg(unix)]
  149. pub fn is_executable_file(&self) -> bool {
  150. let bit = modes::USER_EXECUTE;
  151. self.is_file() && (self.metadata.permissions().mode() & bit) == bit
  152. }
  153. /// Whether this file is a symlink on the filesystem.
  154. pub fn is_link(&self) -> bool {
  155. self.metadata.file_type().is_symlink()
  156. }
  157. /// Whether this file is a named pipe on the filesystem.
  158. #[cfg(unix)]
  159. pub fn is_pipe(&self) -> bool {
  160. self.metadata.file_type().is_fifo()
  161. }
  162. /// Whether this file is a char device on the filesystem.
  163. #[cfg(unix)]
  164. pub fn is_char_device(&self) -> bool {
  165. self.metadata.file_type().is_char_device()
  166. }
  167. /// Whether this file is a block device on the filesystem.
  168. #[cfg(unix)]
  169. pub fn is_block_device(&self) -> bool {
  170. self.metadata.file_type().is_block_device()
  171. }
  172. /// Whether this file is a socket on the filesystem.
  173. #[cfg(unix)]
  174. pub fn is_socket(&self) -> bool {
  175. self.metadata.file_type().is_socket()
  176. }
  177. /// Re-prefixes the path pointed to by this file, if it’s a symlink, to
  178. /// make it an absolute path that can be accessed from whichever
  179. /// directory exa is being run from.
  180. fn reorient_target_path(&self, path: &Path) -> PathBuf {
  181. if path.is_absolute() {
  182. path.to_path_buf()
  183. }
  184. else if let Some(dir) = self.parent_dir {
  185. dir.join(&*path)
  186. }
  187. else if let Some(parent) = self.path.parent() {
  188. parent.join(&*path)
  189. }
  190. else {
  191. self.path.join(&*path)
  192. }
  193. }
  194. /// Again assuming this file is a symlink, follows that link and returns
  195. /// the result of following it.
  196. ///
  197. /// For a working symlink that the user is allowed to follow,
  198. /// this will be the `File` object at the other end, which can then have
  199. /// its name, colour, and other details read.
  200. ///
  201. /// For a broken symlink, returns where the file *would* be, if it
  202. /// existed. If this file cannot be read at all, returns the error that
  203. /// we got when we tried to read it.
  204. pub fn link_target(&self) -> FileTarget<'dir> {
  205. // We need to be careful to treat the path actually pointed to by
  206. // this file — which could be absolute or relative — to the path
  207. // we actually look up and turn into a `File` — which needs to be
  208. // absolute to be accessible from any directory.
  209. debug!("Reading link {:?}", &self.path);
  210. let path = match std::fs::read_link(&self.path) {
  211. Ok(p) => p,
  212. Err(e) => return FileTarget::Err(e),
  213. };
  214. let absolute_path = self.reorient_target_path(&path);
  215. // Use plain `metadata` instead of `symlink_metadata` - we *want* to
  216. // follow links.
  217. match std::fs::metadata(&absolute_path) {
  218. Ok(metadata) => {
  219. let ext = File::ext(&path);
  220. let name = File::filename(&path);
  221. let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false };
  222. FileTarget::Ok(Box::new(file))
  223. }
  224. Err(e) => {
  225. error!("Error following link {:?}: {:#?}", &path, e);
  226. FileTarget::Broken(path)
  227. }
  228. }
  229. }
  230. /// This file’s number of hard links.
  231. ///
  232. /// It also reports whether this is both a regular file, and a file with
  233. /// multiple links. This is important, because a file with multiple links
  234. /// is uncommon, while you come across directories and other types
  235. /// with multiple links much more often. Thus, it should get highlighted
  236. /// more attentively.
  237. #[cfg(unix)]
  238. pub fn links(&self) -> f::Links {
  239. let count = self.metadata.nlink();
  240. f::Links {
  241. count,
  242. multiple: self.is_file() && count > 1,
  243. }
  244. }
  245. /// This file’s inode.
  246. #[cfg(unix)]
  247. pub fn inode(&self) -> f::Inode {
  248. f::Inode(self.metadata.ino())
  249. }
  250. /// This file’s number of filesystem blocks.
  251. ///
  252. /// (Not the size of each block, which we don’t actually report on)
  253. #[cfg(unix)]
  254. pub fn blocks(&self) -> f::Blocks {
  255. if self.is_file() || self.is_link() {
  256. f::Blocks::Some(self.metadata.blocks())
  257. }
  258. else {
  259. f::Blocks::None
  260. }
  261. }
  262. /// The ID of the user that own this file.
  263. #[cfg(unix)]
  264. pub fn user(&self) -> f::User {
  265. f::User(self.metadata.uid())
  266. }
  267. /// The ID of the group that owns this file.
  268. #[cfg(unix)]
  269. pub fn group(&self) -> f::Group {
  270. f::Group(self.metadata.gid())
  271. }
  272. /// This file’s size, if it’s a regular file.
  273. ///
  274. /// For directories, no size is given. Although they do have a size on
  275. /// some filesystems, I’ve never looked at one of those numbers and gained
  276. /// any information from it. So it’s going to be hidden instead.
  277. ///
  278. /// Block and character devices return their device IDs, because they
  279. /// usually just have a file size of zero.
  280. pub fn size(&self) -> f::Size {
  281. if self.is_directory() {
  282. f::Size::None
  283. }
  284. #[cfg(unix)]
  285. else if self.is_char_device() || self.is_block_device() {
  286. let dev = self.metadata.rdev();
  287. f::Size::DeviceIDs(f::DeviceIDs {
  288. major: (dev / 256) as u8,
  289. minor: (dev % 256) as u8,
  290. })
  291. }
  292. else {
  293. f::Size::Some(self.metadata.len())
  294. }
  295. }
  296. /// This file’s last modified timestamp, if available on this platform.
  297. pub fn modified_time(&self) -> Option<SystemTime> {
  298. self.metadata.modified().ok()
  299. }
  300. /// This file’s last changed timestamp, if available on this platform.
  301. #[cfg(unix)]
  302. pub fn changed_time(&self) -> Option<SystemTime> {
  303. let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec());
  304. if sec < 0 {
  305. if nanosec > 0 {
  306. sec += 1;
  307. nanosec -= 1_000_000_000;
  308. }
  309. let duration = Duration::new(sec.abs() as u64, nanosec.abs() as u32);
  310. Some(UNIX_EPOCH - duration)
  311. }
  312. else {
  313. let duration = Duration::new(sec as u64, nanosec as u32);
  314. Some(UNIX_EPOCH + duration)
  315. }
  316. }
  317. /// This file’s last accessed timestamp, if available on this platform.
  318. pub fn accessed_time(&self) -> Option<SystemTime> {
  319. self.metadata.accessed().ok()
  320. }
  321. /// This file’s created timestamp, if available on this platform.
  322. pub fn created_time(&self) -> Option<SystemTime> {
  323. self.metadata.created().ok()
  324. }
  325. /// This file’s ‘type’.
  326. ///
  327. /// This is used a the leftmost character of the permissions column.
  328. /// The file type can usually be guessed from the colour of the file, but
  329. /// ls puts this character there.
  330. #[cfg(windows)]
  331. pub fn type_char(&self) -> f::Type {
  332. if self.is_file() {
  333. f::Type::File
  334. }
  335. else if self.is_directory() {
  336. f::Type::Directory
  337. }
  338. else {
  339. f::Type::Special
  340. }
  341. }
  342. #[cfg(unix)]
  343. pub fn type_char(&self) -> f::Type {
  344. if self.is_file() {
  345. f::Type::File
  346. }
  347. else if self.is_directory() {
  348. f::Type::Directory
  349. }
  350. else if self.is_pipe() {
  351. f::Type::Pipe
  352. }
  353. else if self.is_link() {
  354. f::Type::Link
  355. }
  356. else if self.is_char_device() {
  357. f::Type::CharDevice
  358. }
  359. else if self.is_block_device() {
  360. f::Type::BlockDevice
  361. }
  362. else if self.is_socket() {
  363. f::Type::Socket
  364. }
  365. else {
  366. f::Type::Special
  367. }
  368. }
  369. /// This file’s permissions, with flags for each bit.
  370. #[cfg(unix)]
  371. pub fn permissions(&self) -> f::Permissions {
  372. let bits = self.metadata.mode();
  373. let has_bit = |bit| bits & bit == bit;
  374. f::Permissions {
  375. user_read: has_bit(modes::USER_READ),
  376. user_write: has_bit(modes::USER_WRITE),
  377. user_execute: has_bit(modes::USER_EXECUTE),
  378. group_read: has_bit(modes::GROUP_READ),
  379. group_write: has_bit(modes::GROUP_WRITE),
  380. group_execute: has_bit(modes::GROUP_EXECUTE),
  381. other_read: has_bit(modes::OTHER_READ),
  382. other_write: has_bit(modes::OTHER_WRITE),
  383. other_execute: has_bit(modes::OTHER_EXECUTE),
  384. sticky: has_bit(modes::STICKY),
  385. setgid: has_bit(modes::SETGID),
  386. setuid: has_bit(modes::SETUID),
  387. }
  388. }
  389. /// Whether this file’s extension is any of the strings that get passed in.
  390. ///
  391. /// This will always return `false` if the file has no extension.
  392. pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
  393. match &self.ext {
  394. Some(ext) => choices.contains(&&ext[..]),
  395. None => false,
  396. }
  397. }
  398. /// Whether this file’s name, including extension, is any of the strings
  399. /// that get passed in.
  400. pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
  401. choices.contains(&&self.name[..])
  402. }
  403. }
  404. impl<'a> AsRef<File<'a>> for File<'a> {
  405. fn as_ref(&self) -> &File<'a> {
  406. self
  407. }
  408. }
  409. /// The result of following a symlink.
  410. pub enum FileTarget<'dir> {
  411. /// The symlink pointed at a file that exists.
  412. Ok(Box<File<'dir>>),
  413. /// The symlink pointed at a file that does not exist. Holds the path
  414. /// where the file would be, if it existed.
  415. Broken(PathBuf),
  416. /// There was an IO error when following the link. This can happen if the
  417. /// file isn’t a link to begin with, but also if, say, we don’t have
  418. /// permission to follow it.
  419. Err(io::Error),
  420. // Err is its own variant, instead of having the whole thing be inside an
  421. // `io::Result`, because being unable to follow a symlink is not a serious
  422. // error — we just display the error message and move on.
  423. }
  424. impl<'dir> FileTarget<'dir> {
  425. /// Whether this link doesn’t lead to a file, for whatever reason. This
  426. /// gets used to determine how to highlight the link in grid views.
  427. pub fn is_broken(&self) -> bool {
  428. matches!(self, Self::Broken(_) | Self::Err(_))
  429. }
  430. }
  431. /// More readable aliases for the permission bits exposed by libc.
  432. #[allow(trivial_numeric_casts)]
  433. #[cfg(unix)]
  434. mod modes {
  435. // The `libc::mode_t` type’s actual type varies, but the value returned
  436. // from `metadata.permissions().mode()` is always `u32`.
  437. pub type Mode = u32;
  438. pub const USER_READ: Mode = libc::S_IRUSR as Mode;
  439. pub const USER_WRITE: Mode = libc::S_IWUSR as Mode;
  440. pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode;
  441. pub const GROUP_READ: Mode = libc::S_IRGRP as Mode;
  442. pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode;
  443. pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
  444. pub const OTHER_READ: Mode = libc::S_IROTH as Mode;
  445. pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode;
  446. pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
  447. pub const STICKY: Mode = libc::S_ISVTX as Mode;
  448. pub const SETGID: Mode = libc::S_ISGID as Mode;
  449. pub const SETUID: Mode = libc::S_ISUID as Mode;
  450. }
  451. #[cfg(test)]
  452. mod ext_test {
  453. use super::File;
  454. use std::path::Path;
  455. #[test]
  456. fn extension() {
  457. assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat")))
  458. }
  459. #[test]
  460. fn dotfile() {
  461. assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")))
  462. }
  463. #[test]
  464. fn no_extension() {
  465. assert_eq!(None, File::ext(Path::new("jarlsberg")))
  466. }
  467. }
  468. #[cfg(test)]
  469. mod filename_test {
  470. use super::File;
  471. use std::path::Path;
  472. #[test]
  473. fn file() {
  474. assert_eq!("fester.dat", File::filename(Path::new("fester.dat")))
  475. }
  476. #[test]
  477. fn no_path() {
  478. assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha")))
  479. }
  480. #[test]
  481. fn here() {
  482. assert_eq!(".", File::filename(Path::new(".")))
  483. }
  484. #[test]
  485. fn there() {
  486. assert_eq!("..", File::filename(Path::new("..")))
  487. }
  488. #[test]
  489. fn everywhere() {
  490. assert_eq!("..", File::filename(Path::new("./..")))
  491. }
  492. #[test]
  493. fn topmost() {
  494. #[cfg(unix)]
  495. assert_eq!("/", File::filename(Path::new("/")));
  496. #[cfg(windows)]
  497. assert_eq!("C:\\", File::filename(Path::new("C:\\")));
  498. }
  499. }