file.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. use std::ascii::AsciiExt;
  2. use std::env::current_dir;
  3. use std::fs;
  4. use std::io;
  5. use std::os::unix;
  6. use std::os::unix::fs::{MetadataExt, PermissionsExt};
  7. use std::path::{Component, Path, PathBuf};
  8. use unicode_width::UnicodeWidthStr;
  9. use dir::Dir;
  10. use options::TimeType;
  11. use feature::Attribute;
  12. use self::fields as f;
  13. /// A **File** is a wrapper around one of Rust's Path objects, along with
  14. /// associated data about the file.
  15. ///
  16. /// Each file is definitely going to have its filename displayed at least
  17. /// once, have its file extension extracted at least once, and have its stat
  18. /// information queried at least once, so it makes sense to do all this at the
  19. /// start and hold on to all the information.
  20. pub struct File<'dir> {
  21. pub name: String,
  22. pub dir: Option<&'dir Dir>,
  23. pub ext: Option<String>,
  24. pub path: PathBuf,
  25. pub stat: fs::Metadata,
  26. pub xattrs: Vec<Attribute>,
  27. pub this: Option<Dir>,
  28. }
  29. impl<'dir> File<'dir> {
  30. /// Create a new File object from the given Path, inside the given Dir, if
  31. /// appropriate. Paths specified directly on the command-line have no Dirs.
  32. ///
  33. /// This uses `symlink_metadata` instead of `metadata`, which doesn't
  34. /// follow symbolic links.
  35. pub fn from_path(path: &Path, parent: Option<&'dir Dir>, recurse: bool) -> io::Result<File<'dir>> {
  36. fs::symlink_metadata(path).map(|stat| File::with_stat(stat, path, parent, recurse))
  37. }
  38. /// Create a new File object from the given Stat result, and other data.
  39. pub fn with_stat(stat: fs::Metadata, path: &Path, parent: Option<&'dir Dir>, recurse: bool) -> File<'dir> {
  40. let filename = path_filename(path);
  41. // If we are recursing, then the `this` field contains a Dir object
  42. // that represents the current File as a directory, if it is a
  43. // directory. This is used for the --tree option.
  44. let this = if recurse && stat.is_dir() {
  45. Dir::readdir(path).ok()
  46. }
  47. else {
  48. None
  49. };
  50. File {
  51. path: path.to_path_buf(),
  52. dir: parent,
  53. stat: stat,
  54. ext: ext(&filename),
  55. xattrs: Attribute::llist(path).unwrap_or(Vec::new()),
  56. name: filename.to_string(),
  57. this: this,
  58. }
  59. }
  60. pub fn is_directory(&self) -> bool {
  61. self.stat.is_dir()
  62. }
  63. pub fn is_file(&self) -> bool {
  64. self.stat.is_file()
  65. }
  66. pub fn is_executable_file(&self) -> bool {
  67. let bit = unix::fs::USER_EXECUTE;
  68. self.is_file() && (self.stat.permissions().mode() & bit) == bit
  69. }
  70. pub fn is_link(&self) -> bool {
  71. self.stat.file_type().is_symlink()
  72. }
  73. pub fn is_pipe(&self) -> bool {
  74. false // TODO: Still waiting on this one...
  75. }
  76. /// Whether this file is a dotfile or not.
  77. pub fn is_dotfile(&self) -> bool {
  78. self.name.starts_with(".")
  79. }
  80. pub fn path_prefix(&self) -> String {
  81. let path_bytes: Vec<Component> = self.path.components().collect();
  82. let mut path_prefix = String::new();
  83. if !path_bytes.is_empty() {
  84. // Use init() to add all but the last component of the
  85. // path to the prefix. init() panics when given an
  86. // empty list, hence the check.
  87. for component in path_bytes.init().iter() {
  88. path_prefix.push_str(&*component.as_os_str().to_string_lossy());
  89. if component != &Component::RootDir {
  90. path_prefix.push_str("/");
  91. }
  92. }
  93. }
  94. path_prefix
  95. }
  96. /// The Unicode 'display width' of the filename.
  97. ///
  98. /// This is related to the number of graphemes in the string: most
  99. /// characters are 1 columns wide, but in some contexts, certain
  100. /// characters are actually 2 columns wide.
  101. pub fn file_name_width(&self) -> usize {
  102. UnicodeWidthStr::width(&self.name[..])
  103. }
  104. /// Assuming the current file is a symlink, follows the link and
  105. /// returns a File object from the path the link points to.
  106. ///
  107. /// If statting the file fails (usually because the file on the
  108. /// other end doesn't exist), returns the *filename* of the file
  109. /// that should be there.
  110. pub fn link_target(&self) -> Result<File, String> {
  111. let path = match fs::read_link(&self.path) {
  112. Ok(path) => path,
  113. Err(_) => return Err(self.name.clone()),
  114. };
  115. let target_path = match self.dir {
  116. Some(dir) => dir.join(&*path),
  117. None => path
  118. };
  119. let filename = path_filename(&target_path);
  120. // Use plain `metadata` instead of `symlink_metadata` - we *want* to follow links.
  121. if let Ok(stat) = fs::metadata(&target_path) {
  122. Ok(File {
  123. path: target_path.to_path_buf(),
  124. dir: self.dir,
  125. stat: stat,
  126. ext: ext(&filename),
  127. xattrs: Attribute::list(&target_path).unwrap_or(Vec::new()),
  128. name: filename.to_string(),
  129. this: None,
  130. })
  131. }
  132. else {
  133. Err(filename.to_string())
  134. }
  135. }
  136. /// This file's number of hard links as a coloured string.
  137. ///
  138. /// This is important, because a file with multiple links is uncommon,
  139. /// while you can come across directories and other types with multiple
  140. /// links much more often.
  141. pub fn links(&self) -> f::Links {
  142. let count = self.stat.as_raw().nlink();
  143. f::Links {
  144. count: count,
  145. multiple: self.is_file() && count > 1,
  146. }
  147. }
  148. pub fn inode(&self) -> f::Inode {
  149. f::Inode(self.stat.as_raw().ino())
  150. }
  151. pub fn blocks(&self) -> f::Blocks {
  152. if self.is_file() || self.is_link() {
  153. f::Blocks::Some(self.stat.as_raw().blocks())
  154. }
  155. else {
  156. f::Blocks::None
  157. }
  158. }
  159. pub fn user(&self) -> f::User {
  160. f::User(self.stat.as_raw().uid())
  161. }
  162. pub fn group(&self) -> f::Group {
  163. f::Group(self.stat.as_raw().gid())
  164. }
  165. /// This file's size, formatted using the given way, as a coloured string.
  166. ///
  167. /// For directories, no size is given. Although they do have a size on
  168. /// some filesystems, I've never looked at one of those numbers and gained
  169. /// any information from it, so by emitting "-" instead, the table is less
  170. /// cluttered with numbers.
  171. pub fn size(&self) -> f::Size {
  172. if self.is_directory() {
  173. f::Size::None
  174. }
  175. else {
  176. f::Size::Some(self.stat.len())
  177. }
  178. }
  179. pub fn timestamp(&self, time_type: TimeType) -> f::Time {
  180. let time_in_seconds = match time_type {
  181. TimeType::FileAccessed => self.stat.as_raw().atime(),
  182. TimeType::FileModified => self.stat.as_raw().mtime(),
  183. TimeType::FileCreated => self.stat.as_raw().ctime(),
  184. };
  185. f::Time(time_in_seconds)
  186. }
  187. /// This file's type, represented by a coloured character.
  188. ///
  189. /// Although the file type can usually be guessed from the colour of the
  190. /// file, `ls` puts this character there, so people will expect it.
  191. fn type_char(&self) -> f::Type {
  192. if self.is_file() {
  193. f::Type::File
  194. }
  195. else if self.is_directory() {
  196. f::Type::Directory
  197. }
  198. else if self.is_pipe() {
  199. f::Type::Pipe
  200. }
  201. else if self.is_link() {
  202. f::Type::Link
  203. }
  204. else {
  205. f::Type::Special
  206. }
  207. }
  208. pub fn permissions(&self) -> f::Permissions {
  209. let bits = self.stat.permissions().mode();
  210. let has_bit = |bit| { bits & bit == bit };
  211. f::Permissions {
  212. file_type: self.type_char(),
  213. user_read: has_bit(unix::fs::USER_READ),
  214. user_write: has_bit(unix::fs::USER_WRITE),
  215. user_execute: has_bit(unix::fs::USER_EXECUTE),
  216. group_read: has_bit(unix::fs::GROUP_READ),
  217. group_write: has_bit(unix::fs::GROUP_WRITE),
  218. group_execute: has_bit(unix::fs::GROUP_EXECUTE),
  219. other_read: has_bit(unix::fs::OTHER_READ),
  220. other_write: has_bit(unix::fs::OTHER_WRITE),
  221. other_execute: has_bit(unix::fs::OTHER_EXECUTE),
  222. attribute: !self.xattrs.is_empty()
  223. }
  224. }
  225. /// For this file, return a vector of alternate file paths that, if any of
  226. /// them exist, mean that *this* file should be coloured as `Compiled`.
  227. ///
  228. /// The point of this is to highlight compiled files such as `foo.o` when
  229. /// their source file `foo.c` exists in the same directory. It's too
  230. /// dangerous to highlight *all* compiled, so the paths in this vector
  231. /// are checked for existence first: for example, `foo.js` is perfectly
  232. /// valid without `foo.coffee`.
  233. pub fn get_source_files(&self) -> Vec<PathBuf> {
  234. if let Some(ref ext) = self.ext {
  235. match &ext[..] {
  236. "class" => vec![self.path.with_extension("java")], // Java
  237. "css" => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less
  238. "elc" => vec![self.path.with_extension("el")], // Emacs Lisp
  239. "hi" => vec![self.path.with_extension("hs")], // Haskell
  240. "js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript
  241. "o" => vec![self.path.with_extension("c"), self.path.with_extension("cpp")], // C, C++
  242. "pyc" => vec![self.path.with_extension("py")], // Python
  243. "aux" => vec![self.path.with_extension("tex")], // TeX: auxiliary file
  244. "bbl" => vec![self.path.with_extension("tex")], // BibTeX bibliography file
  245. "blg" => vec![self.path.with_extension("tex")], // BibTeX log file
  246. "lof" => vec![self.path.with_extension("tex")], // TeX list of figures
  247. "log" => vec![self.path.with_extension("tex")], // TeX log file
  248. "lot" => vec![self.path.with_extension("tex")], // TeX list of tables
  249. "toc" => vec![self.path.with_extension("tex")], // TeX table of contents
  250. _ => vec![], // No source files if none of the above
  251. }
  252. }
  253. else {
  254. vec![] // No source files if there's no extension, either!
  255. }
  256. }
  257. pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
  258. match self.ext {
  259. Some(ref ext) => choices.contains(&&ext[..]),
  260. None => false,
  261. }
  262. }
  263. pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
  264. choices.contains(&&self.name[..])
  265. }
  266. pub fn git_status(&self) -> f::Git {
  267. match self.dir {
  268. None => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
  269. Some(d) => {
  270. let cwd = match current_dir() {
  271. Err(_) => Path::new(".").join(&self.path),
  272. Ok(dir) => dir.join(&self.path),
  273. };
  274. d.git_status(&cwd, self.is_directory())
  275. },
  276. }
  277. }
  278. }
  279. /// Extract the filename to display from a path, converting it from UTF-8
  280. /// lossily, into a String.
  281. ///
  282. /// The filename to display is the last component of the path. However,
  283. /// the path has no components for `.`, `..`, and `/`, so in these
  284. /// cases, the entire path is used.
  285. fn path_filename(path: &Path) -> String {
  286. match path.iter().last() {
  287. Some(os_str) => os_str.to_string_lossy().to_string(),
  288. None => ".".to_string(), // can this even be reached?
  289. }
  290. }
  291. /// Extract an extension from a string, if one is present, in lowercase.
  292. ///
  293. /// The extension is the series of characters after the last dot. This
  294. /// deliberately counts dotfiles, so the ".git" folder has the extension "git".
  295. ///
  296. /// ASCII lowercasing is used because these extensions are only compared
  297. /// against a pre-compiled list of extensions which are known to only exist
  298. /// within ASCII, so it's alright.
  299. fn ext(name: &str) -> Option<String> {
  300. name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
  301. }
  302. pub mod fields {
  303. use std::os::unix::raw::{blkcnt_t, gid_t, ino_t, nlink_t, time_t, uid_t};
  304. pub enum Type {
  305. File, Directory, Pipe, Link, Special,
  306. }
  307. pub struct Permissions {
  308. pub file_type: Type,
  309. pub user_read: bool,
  310. pub user_write: bool,
  311. pub user_execute: bool,
  312. pub group_read: bool,
  313. pub group_write: bool,
  314. pub group_execute: bool,
  315. pub other_read: bool,
  316. pub other_write: bool,
  317. pub other_execute: bool,
  318. pub attribute: bool,
  319. }
  320. pub struct Links {
  321. pub count: nlink_t,
  322. pub multiple: bool,
  323. }
  324. pub struct Inode(pub ino_t);
  325. pub enum Blocks {
  326. Some(blkcnt_t),
  327. None,
  328. }
  329. pub struct User(pub uid_t);
  330. pub struct Group(pub gid_t);
  331. pub enum Size {
  332. Some(u64),
  333. None,
  334. }
  335. pub struct Time(pub time_t);
  336. pub enum GitStatus {
  337. NotModified,
  338. New,
  339. Modified,
  340. Deleted,
  341. Renamed,
  342. TypeChange,
  343. }
  344. pub struct Git {
  345. pub staged: GitStatus,
  346. pub unstaged: GitStatus,
  347. }
  348. impl Git {
  349. pub fn empty() -> Git {
  350. Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified }
  351. }
  352. }
  353. }
  354. #[cfg(broken_test)]
  355. pub mod test {
  356. pub use super::*;
  357. pub use column::{Cell, Column};
  358. pub use output::details::UserLocale;
  359. pub use users::{User, Group};
  360. pub use users::mock::MockUsers;
  361. pub use ansi_term::Style::Plain;
  362. pub use ansi_term::Colour::Yellow;
  363. #[test]
  364. fn extension() {
  365. assert_eq!(Some("dat".to_string()), super::ext("fester.dat"))
  366. }
  367. #[test]
  368. fn dotfile() {
  369. assert_eq!(Some("vimrc".to_string()), super::ext(".vimrc"))
  370. }
  371. #[test]
  372. fn no_extension() {
  373. assert_eq!(None, super::ext("jarlsberg"))
  374. }
  375. pub fn new_file(stat: io::FileStat, path: &'static str) -> File {
  376. File::with_stat(stat, &Path::new(path), None, false)
  377. }
  378. pub fn dummy_stat() -> io::FileStat {
  379. io::FileStat {
  380. size: 0,
  381. kind: io::FileType::RegularFile,
  382. created: 0,
  383. modified: 0,
  384. accessed: 0,
  385. perm: io::USER_READ,
  386. unstable: io::UnstableFileStat {
  387. inode: 0,
  388. device: 0,
  389. rdev: 0,
  390. nlink: 0,
  391. uid: 0,
  392. gid: 0,
  393. blksize: 0,
  394. blocks: 0,
  395. flags: 0,
  396. gen: 0,
  397. }
  398. }
  399. }
  400. pub fn dummy_locale() -> UserLocale {
  401. UserLocale::default()
  402. }
  403. mod users {
  404. use super::*;
  405. #[test]
  406. fn named() {
  407. let mut stat = dummy_stat();
  408. stat.unstable.uid = 1000;
  409. let file = new_file(stat, "/hi");
  410. let mut users = MockUsers::with_current_uid(1000);
  411. users.add_user(User { uid: 1000, name: "enoch".to_string(), primary_group: 100 });
  412. let cell = Cell::paint(Yellow.bold(), "enoch");
  413. assert_eq!(cell, file.display(&Column::User, &mut users, &dummy_locale()))
  414. }
  415. #[test]
  416. fn unnamed() {
  417. let mut stat = dummy_stat();
  418. stat.unstable.uid = 1000;
  419. let file = new_file(stat, "/hi");
  420. let mut users = MockUsers::with_current_uid(1000);
  421. let cell = Cell::paint(Yellow.bold(), "1000");
  422. assert_eq!(cell, file.display(&Column::User, &mut users, &dummy_locale()))
  423. }
  424. #[test]
  425. fn different_named() {
  426. let mut stat = dummy_stat();
  427. stat.unstable.uid = 1000;
  428. let file = new_file(stat, "/hi");
  429. let mut users = MockUsers::with_current_uid(3);
  430. users.add_user(User { uid: 1000, name: "enoch".to_string(), primary_group: 100 });
  431. let cell = Cell::paint(Plain, "enoch");
  432. assert_eq!(cell, file.display(&Column::User, &mut users, &dummy_locale()))
  433. }
  434. #[test]
  435. fn different_unnamed() {
  436. let mut stat = dummy_stat();
  437. stat.unstable.uid = 1000;
  438. let file = new_file(stat, "/hi");
  439. let mut users = MockUsers::with_current_uid(3);
  440. let cell = Cell::paint(Plain, "1000");
  441. assert_eq!(cell, file.display(&Column::User, &mut users, &dummy_locale()))
  442. }
  443. #[test]
  444. fn overflow() {
  445. let mut stat = dummy_stat();
  446. stat.unstable.uid = 2_147_483_648;
  447. let file = new_file(stat, "/hi");
  448. let mut users = MockUsers::with_current_uid(3);
  449. let cell = Cell::paint(Plain, "2147483648");
  450. assert_eq!(cell, file.display(&Column::User, &mut users, &dummy_locale()))
  451. }
  452. }
  453. mod groups {
  454. use super::*;
  455. #[test]
  456. fn named() {
  457. let mut stat = dummy_stat();
  458. stat.unstable.gid = 100;
  459. let file = new_file(stat, "/hi");
  460. let mut users = MockUsers::with_current_uid(3);
  461. users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![] });
  462. let cell = Cell::paint(Plain, "folk");
  463. assert_eq!(cell, file.display(&Column::Group, &mut users, &dummy_locale()))
  464. }
  465. #[test]
  466. fn unnamed() {
  467. let mut stat = dummy_stat();
  468. stat.unstable.gid = 100;
  469. let file = new_file(stat, "/hi");
  470. let mut users = MockUsers::with_current_uid(3);
  471. let cell = Cell::paint(Plain, "100");
  472. assert_eq!(cell, file.display(&Column::Group, &mut users, &dummy_locale()))
  473. }
  474. #[test]
  475. fn primary() {
  476. let mut stat = dummy_stat();
  477. stat.unstable.gid = 100;
  478. let file = new_file(stat, "/hi");
  479. let mut users = MockUsers::with_current_uid(3);
  480. users.add_user(User { uid: 3, name: "eve".to_string(), primary_group: 100 });
  481. users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![] });
  482. let cell = Cell::paint(Yellow.bold(), "folk");
  483. assert_eq!(cell, file.display(&Column::Group, &mut users, &dummy_locale()))
  484. }
  485. #[test]
  486. fn secondary() {
  487. let mut stat = dummy_stat();
  488. stat.unstable.gid = 100;
  489. let file = new_file(stat, "/hi");
  490. let mut users = MockUsers::with_current_uid(3);
  491. users.add_user(User { uid: 3, name: "eve".to_string(), primary_group: 12 });
  492. users.add_group(Group { gid: 100, name: "folk".to_string(), members: vec![ "eve".to_string() ] });
  493. let cell = Cell::paint(Yellow.bold(), "folk");
  494. assert_eq!(cell, file.display(&Column::Group, &mut users, &dummy_locale()))
  495. }
  496. #[test]
  497. fn overflow() {
  498. let mut stat = dummy_stat();
  499. stat.unstable.gid = 2_147_483_648;
  500. let file = new_file(stat, "/hi");
  501. let mut users = MockUsers::with_current_uid(3);
  502. let cell = Cell::paint(Plain, "2147483648");
  503. assert_eq!(cell, file.display(&Column::Group, &mut users, &dummy_locale()))
  504. }
  505. }
  506. }