file.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. //! Files, and methods and fields to access their metadata.
  2. use std::ascii::AsciiExt;
  3. use std::env::current_dir;
  4. use std::fs;
  5. use std::io::Error as IOError;
  6. use std::io::Result as IOResult;
  7. use std::os::unix::fs::{MetadataExt, PermissionsExt};
  8. use std::path::{Path, PathBuf};
  9. use fs::dir::Dir;
  10. use fs::fields as f;
  11. #[cfg(any(target_os = "macos", target_os = "linux"))]
  12. use std::os::unix::fs::FileTypeExt;
  13. /// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
  14. /// which is currently unstable and lacks vision for stabilization,
  15. /// see https://github.com/rust-lang/rust/issues/27712
  16. #[allow(dead_code, non_camel_case_types)]
  17. mod modes {
  18. pub type mode_t = u32;
  19. pub const USER_READ: mode_t = 0o400;
  20. pub const USER_WRITE: mode_t = 0o200;
  21. pub const USER_EXECUTE: mode_t = 0o100;
  22. pub const USER_RWX: mode_t = 0o700;
  23. pub const GROUP_READ: mode_t = 0o040;
  24. pub const GROUP_WRITE: mode_t = 0o020;
  25. pub const GROUP_EXECUTE: mode_t = 0o010;
  26. pub const GROUP_RWX: mode_t = 0o070;
  27. pub const OTHER_READ: mode_t = 0o004;
  28. pub const OTHER_WRITE: mode_t = 0o002;
  29. pub const OTHER_EXECUTE: mode_t = 0o001;
  30. pub const OTHER_RWX: mode_t = 0o007;
  31. pub const ALL_READ: mode_t = 0o444;
  32. pub const ALL_WRITE: mode_t = 0o222;
  33. pub const ALL_EXECUTE: mode_t = 0o111;
  34. pub const ALL_RWX: mode_t = 0o777;
  35. pub const SETUID: mode_t = 0o4000;
  36. pub const SETGID: mode_t = 0o2000;
  37. pub const STICKY_BIT: mode_t = 0o1000;
  38. }
  39. /// A **File** is a wrapper around one of Rust's Path objects, along with
  40. /// associated data about the file.
  41. ///
  42. /// Each file is definitely going to have its filename displayed at least
  43. /// once, have its file extension extracted at least once, and have its metadata
  44. /// information queried at least once, so it makes sense to do all this at the
  45. /// start and hold on to all the information.
  46. pub struct File<'dir> {
  47. /// The filename portion of this file's path, including the extension.
  48. ///
  49. /// This is used to compare against certain filenames (such as checking if
  50. /// it’s “Makefile” or something) and to highlight only the filename in
  51. /// colour when displaying the path.
  52. pub name: String,
  53. /// The file’s name’s extension, if present, extracted from the name.
  54. ///
  55. /// This is queried many times over, so it’s worth caching it.
  56. pub ext: Option<String>,
  57. /// The path that begat this file.
  58. ///
  59. /// Even though the file's name is extracted, the path needs to be kept
  60. /// around, as certain operations involve looking up the file's absolute
  61. /// location (such as the Git status, or searching for compiled files).
  62. pub path: PathBuf,
  63. /// A cached `metadata` call for this file.
  64. ///
  65. /// This too is queried multiple times, and is *not* cached by the OS, as
  66. /// it could easily change between invocations - but exa is so short-lived
  67. /// it's better to just cache it.
  68. pub metadata: fs::Metadata,
  69. /// A reference to the directory that contains this file, if present.
  70. ///
  71. /// Filenames that get passed in on the command-line directly will have no
  72. /// parent directory reference - although they technically have one on the
  73. /// filesystem, we'll never need to look at it, so it'll be `None`.
  74. /// However, *directories* that get passed in will produce files that
  75. /// contain a reference to it, which is used in certain operations (such
  76. /// as looking up a file's Git status).
  77. pub dir: Option<&'dir Dir>,
  78. }
  79. impl<'dir> File<'dir> {
  80. /// Create a new `File` object from the given `Path`, inside the given
  81. /// `Dir`, if appropriate.
  82. ///
  83. /// This uses `symlink_metadata` instead of `metadata`, which doesn't
  84. /// follow symbolic links.
  85. pub fn from_path(path: &Path, parent: Option<&'dir Dir>) -> IOResult<File<'dir>> {
  86. fs::symlink_metadata(path).map(|metadata| File::with_metadata(metadata, path, parent))
  87. }
  88. /// Create a new File object from the given metadata result, and other data.
  89. pub fn with_metadata(metadata: fs::Metadata, path: &Path, parent: Option<&'dir Dir>) -> File<'dir> {
  90. let filename = match path.file_name() {
  91. Some(name) => name.to_string_lossy().to_string(),
  92. None => String::new(),
  93. };
  94. File {
  95. path: path.to_path_buf(),
  96. dir: parent,
  97. metadata: metadata,
  98. ext: ext(path),
  99. name: filename,
  100. }
  101. }
  102. /// Whether this file is a directory on the filesystem.
  103. pub fn is_directory(&self) -> bool {
  104. self.metadata.is_dir()
  105. }
  106. /// If this file is a directory on the filesystem, then clone its
  107. /// `PathBuf` for use in one of our own `Dir` objects, and read a list of
  108. /// its contents.
  109. ///
  110. /// Returns an IO error upon failure, but this shouldn't be used to check
  111. /// if a `File` is a directory or not! For that, just use `is_directory()`.
  112. pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> {
  113. Dir::read_dir(&*self.path, scan_for_git)
  114. }
  115. /// Whether this file is a regular file on the filesystem - that is, not a
  116. /// directory, a link, or anything else treated specially.
  117. pub fn is_file(&self) -> bool {
  118. self.metadata.is_file()
  119. }
  120. /// Whether this file is both a regular file *and* executable for the
  121. /// current user. Executable files have different semantics than
  122. /// executable directories, and so should be highlighted differently.
  123. pub fn is_executable_file(&self) -> bool {
  124. let bit = modes::USER_EXECUTE;
  125. self.is_file() && (self.metadata.permissions().mode() & bit) == bit
  126. }
  127. /// Whether this file is a symlink on the filesystem.
  128. pub fn is_link(&self) -> bool {
  129. self.metadata.file_type().is_symlink()
  130. }
  131. /// Whether this file is a dotfile, based on its name. In Unix, file names
  132. /// beginning with a dot represent system or configuration files, and
  133. /// should be hidden by default.
  134. pub fn is_dotfile(&self) -> bool {
  135. self.name.starts_with('.')
  136. }
  137. /// Assuming the current file is a symlink, follows the link and
  138. /// returns a File object from the path the link points to.
  139. ///
  140. /// If statting the file fails (usually because the file on the
  141. /// other end doesn't exist), returns the path to the file
  142. /// that should be there.
  143. pub fn link_target(&self) -> FileTarget<'dir> {
  144. let path = match fs::read_link(&self.path) {
  145. Ok(path) => path,
  146. Err(e) => return FileTarget::Err(e),
  147. };
  148. let target_path = match self.dir {
  149. Some(dir) => dir.join(&*path),
  150. None => path
  151. };
  152. let filename = match target_path.file_name() {
  153. Some(name) => name.to_string_lossy().to_string(),
  154. None => String::new(),
  155. };
  156. // Use plain `metadata` instead of `symlink_metadata` - we *want* to follow links.
  157. if let Ok(metadata) = fs::metadata(&target_path) {
  158. FileTarget::Ok(File {
  159. path: target_path.to_path_buf(),
  160. dir: self.dir,
  161. metadata: metadata,
  162. ext: ext(&target_path),
  163. name: filename,
  164. })
  165. }
  166. else {
  167. FileTarget::Broken(target_path)
  168. }
  169. }
  170. /// This file's number of hard links.
  171. ///
  172. /// It also reports whether this is both a regular file, and a file with
  173. /// multiple links. This is important, because a file with multiple links
  174. /// is uncommon, while you can come across directories and other types
  175. /// with multiple links much more often. Thus, it should get highlighted
  176. /// more attentively.
  177. pub fn links(&self) -> f::Links {
  178. let count = self.metadata.nlink();
  179. f::Links {
  180. count: count,
  181. multiple: self.is_file() && count > 1,
  182. }
  183. }
  184. /// This file's inode.
  185. pub fn inode(&self) -> f::Inode {
  186. f::Inode(self.metadata.ino())
  187. }
  188. /// This file's number of filesystem blocks.
  189. ///
  190. /// (Not the size of each block, which we don't actually report on)
  191. pub fn blocks(&self) -> f::Blocks {
  192. if self.is_file() || self.is_link() {
  193. f::Blocks::Some(self.metadata.blocks())
  194. }
  195. else {
  196. f::Blocks::None
  197. }
  198. }
  199. /// The ID of the user that own this file.
  200. pub fn user(&self) -> f::User {
  201. f::User(self.metadata.uid())
  202. }
  203. /// The ID of the group that owns this file.
  204. pub fn group(&self) -> f::Group {
  205. f::Group(self.metadata.gid())
  206. }
  207. /// This file's size, if it's a regular file.
  208. ///
  209. /// For directories, no size is given. Although they do have a size on
  210. /// some filesystems, I've never looked at one of those numbers and gained
  211. /// any information from it. So it's going to be hidden instead.
  212. pub fn size(&self) -> f::Size {
  213. if self.is_directory() {
  214. f::Size::None
  215. }
  216. else {
  217. f::Size::Some(self.metadata.len())
  218. }
  219. }
  220. pub fn modified_time(&self) -> f::Time {
  221. f::Time(self.metadata.mtime())
  222. }
  223. pub fn created_time(&self) -> f::Time {
  224. f::Time(self.metadata.ctime())
  225. }
  226. pub fn accessed_time(&self) -> f::Time {
  227. f::Time(self.metadata.mtime())
  228. }
  229. /// This file's 'type'.
  230. ///
  231. /// This is used in the leftmost column of the permissions column.
  232. /// Although the file type can usually be guessed from the colour of the
  233. /// file, `ls` puts this character there, so people will expect it.
  234. pub fn type_char(&self) -> f::Type {
  235. if self.is_file() {
  236. f::Type::File
  237. }
  238. else if self.is_directory() {
  239. f::Type::Directory
  240. }
  241. else if self.is_pipe() {
  242. f::Type::Pipe
  243. }
  244. else if self.is_link() {
  245. f::Type::Link
  246. }
  247. else if self.is_char_device() {
  248. f::Type::CharDevice
  249. }
  250. else if self.is_block_device() {
  251. f::Type::BlockDevice
  252. }
  253. else if self.is_socket() {
  254. f::Type::Socket
  255. }
  256. else {
  257. f::Type::Special
  258. }
  259. }
  260. /// This file's permissions, with flags for each bit.
  261. ///
  262. /// The extended-attribute '@' character that you see in here is in fact
  263. /// added in later, to avoid querying the extended attributes more than
  264. /// once. (Yes, it's a little hacky.)
  265. pub fn permissions(&self) -> f::Permissions {
  266. let bits = self.metadata.permissions().mode();
  267. let has_bit = |bit| { bits & bit == bit };
  268. f::Permissions {
  269. user_read: has_bit(modes::USER_READ),
  270. user_write: has_bit(modes::USER_WRITE),
  271. user_execute: has_bit(modes::USER_EXECUTE),
  272. group_read: has_bit(modes::GROUP_READ),
  273. group_write: has_bit(modes::GROUP_WRITE),
  274. group_execute: has_bit(modes::GROUP_EXECUTE),
  275. other_read: has_bit(modes::OTHER_READ),
  276. other_write: has_bit(modes::OTHER_WRITE),
  277. other_execute: has_bit(modes::OTHER_EXECUTE),
  278. }
  279. }
  280. /// Whether this file's extension is any of the strings that get passed in.
  281. ///
  282. /// This will always return `false` if the file has no extension.
  283. pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
  284. match self.ext {
  285. Some(ref ext) => choices.contains(&&ext[..]),
  286. None => false,
  287. }
  288. }
  289. /// Whether this file's name, including extension, is any of the strings
  290. /// that get passed in.
  291. pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
  292. choices.contains(&&self.name[..])
  293. }
  294. /// This file's Git status as two flags: one for staged changes, and the
  295. /// other for unstaged changes.
  296. ///
  297. /// This requires looking at the `git` field of this file's parent
  298. /// directory, so will not work if this file has just been passed in on
  299. /// the command line.
  300. pub fn git_status(&self) -> f::Git {
  301. match self.dir {
  302. None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
  303. Some(d) => {
  304. let cwd = match current_dir() {
  305. Err(_) => Path::new(".").join(&self.path),
  306. Ok(dir) => dir.join(&self.path),
  307. };
  308. d.git_status(&cwd, self.is_directory())
  309. },
  310. }
  311. }
  312. }
  313. #[cfg(any(target_os = "macos", target_os = "linux"))]
  314. impl<'dir> File<'dir> {
  315. /// Whether this file is a named pipe on the filesystem.
  316. pub fn is_pipe(&self) -> bool {
  317. self.metadata.file_type().is_fifo()
  318. }
  319. /// Whether this file is a char device on the filesystem.
  320. pub fn is_char_device(&self) -> bool {
  321. self.metadata.file_type().is_char_device()
  322. }
  323. /// Whether this file is a block device on the filesystem.
  324. pub fn is_block_device(&self) -> bool {
  325. self.metadata.file_type().is_block_device()
  326. }
  327. /// Whether this file is a socket on the filesystem.
  328. pub fn is_socket(&self) -> bool {
  329. self.metadata.file_type().is_socket()
  330. }
  331. }
  332. #[cfg(not(any(target_os = "macos", target_os = "linux")))]
  333. impl<'dir> File<'dir> {
  334. /// Whether this file is a named pipe on the filesystem.
  335. pub fn is_pipe(&self) -> bool {
  336. false
  337. }
  338. /// Whether this file is a char device on the filesystem.
  339. pub fn is_char_device(&self) -> bool {
  340. false
  341. }
  342. /// Whether this file is a block device on the filesystem.
  343. pub fn is_block_device(&self) -> bool {
  344. false
  345. }
  346. /// Whether this file is a socket on the filesystem.
  347. pub fn is_socket(&self) -> bool {
  348. false
  349. }
  350. }
  351. impl<'a> AsRef<File<'a>> for File<'a> {
  352. fn as_ref(&self) -> &File<'a> {
  353. self
  354. }
  355. }
  356. /// Extract an extension from a file path, if one is present, in lowercase.
  357. ///
  358. /// The extension is the series of characters after the last dot. This
  359. /// deliberately counts dotfiles, so the ".git" folder has the extension "git".
  360. ///
  361. /// ASCII lowercasing is used because these extensions are only compared
  362. /// against a pre-compiled list of extensions which are known to only exist
  363. /// within ASCII, so it's alright.
  364. fn ext(path: &Path) -> Option<String> {
  365. let name = match path.file_name() {
  366. Some(f) => f.to_string_lossy().to_string(),
  367. None => return None,
  368. };
  369. name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
  370. }
  371. /// The result of following a symlink.
  372. pub enum FileTarget<'dir> {
  373. /// The symlink pointed at a file that exists.
  374. Ok(File<'dir>),
  375. /// The symlink pointed at a file that does not exist. Holds the path
  376. /// where the file would be, if it existed.
  377. Broken(PathBuf),
  378. /// There was an IO error when following the link. This can happen if the
  379. /// file isn't a link to begin with, but also if, say, we don't have
  380. /// permission to follow it.
  381. Err(IOError),
  382. }
  383. #[cfg(test)]
  384. mod test {
  385. use super::ext;
  386. use std::path::Path;
  387. #[test]
  388. fn extension() {
  389. assert_eq!(Some("dat".to_string()), ext(Path::new("fester.dat")))
  390. }
  391. #[test]
  392. fn dotfile() {
  393. assert_eq!(Some("vimrc".to_string()), ext(Path::new(".vimrc")))
  394. }
  395. #[test]
  396. fn no_extension() {
  397. assert_eq!(None, ext(Path::new("jarlsberg")))
  398. }
  399. }