file.rs 15 KB

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