file.rs 16.6 KB
Newer Older
B
Ben S 已提交
1 2
//! Files, and methods and fields to access their metadata.

3
use std::fs;
4
use std::io::Error as IOError;
B
Benjamin Sago 已提交
5
use std::io::Result as IOResult;
B
Benjamin Sago 已提交
6
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
7
use std::path::{Path, PathBuf};
B
Ben S 已提交
8

B
Benjamin Sago 已提交
9
use fs::dir::Dir;
B
Benjamin Sago 已提交
10
use fs::fields as f;
B
Ben S 已提交
11

B
Benjamin Sago 已提交
12

B
Ben S 已提交
13 14 15 16
/// A **File** is a wrapper around one of Rust's Path objects, along with
/// associated data about the file.
///
/// Each file is definitely going to have its filename displayed at least
B
Ben S 已提交
17
/// once, have its file extension extracted at least once, and have its metadata
B
Ben S 已提交
18 19
/// information queried at least once, so it makes sense to do all this at the
/// start and hold on to all the information.
B
Ben S 已提交
20
pub struct File<'dir> {
B
Ben S 已提交
21

22 23 24 25 26
    /// The filename portion of this file's path, including the extension.
    ///
    /// This is used to compare against certain filenames (such as checking if
    /// it’s “Makefile” or something) and to highlight only the filename in
    /// colour when displaying the path.
B
Ben S 已提交
27
    pub name: String,
B
Ben S 已提交
28

29 30 31
    /// The file’s name’s extension, if present, extracted from the name.
    ///
    /// This is queried many times over, so it’s worth caching it.
B
Ben S 已提交
32
    pub ext: Option<String>,
B
Ben S 已提交
33

34 35 36 37 38
    /// The path that begat this file.
    ///
    /// Even though the file's name is extracted, the path needs to be kept
    /// around, as certain operations involve looking up the file's absolute
    /// location (such as the Git status, or searching for compiled files).
B
Ben S 已提交
39
    pub path: PathBuf,
B
Ben S 已提交
40

41 42 43 44 45
    /// A cached `metadata` call for this file.
    ///
    /// This too is queried multiple times, and is *not* cached by the OS, as
    /// it could easily change between invocations - but exa is so short-lived
    /// it's better to just cache it.
B
Ben S 已提交
46
    pub metadata: fs::Metadata,
B
Ben S 已提交
47 48 49 50 51 52 53 54 55

    /// A reference to the directory that contains this file, if present.
    ///
    /// Filenames that get passed in on the command-line directly will have no
    /// parent directory reference - although they technically have one on the
    /// filesystem, we'll never need to look at it, so it'll be `None`.
    /// However, *directories* that get passed in will produce files that
    /// contain a reference to it, which is used in certain operations (such
    /// as looking up a file's Git status).
56
    pub parent_dir: Option<&'dir Dir>,
B
Benjamin Sago 已提交
57
}
B
Ben S 已提交
58

B
Benjamin Sago 已提交
59
impl<'dir> File<'dir> {
B
Benjamin Sago 已提交
60 61 62 63 64 65 66 67
    pub fn new<PD, FN>(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult<File<'dir>>
    where PD: Into<Option<&'dir Dir>>,
          FN: Into<Option<String>>
    {
        let parent_dir = parent_dir.into();
        let metadata   = fs::symlink_metadata(&path)?;
        let name       = filename.into().unwrap_or_else(|| File::filename(&path));
        let ext        = File::ext(&path);
B
Benjamin Sago 已提交
68

B
Benjamin Sago 已提交
69
        Ok(File { path, parent_dir, metadata, ext, name })
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    }

    /// A file’s name is derived from its string. This needs to handle directories
    /// such as `/` or `..`, which have no `file_name` component. So instead, just
    /// use the last component as the name.
    pub fn filename(path: &Path) -> String {
        match path.components().next_back() {
            Some(back) => back.as_os_str().to_string_lossy().to_string(),
            None       => path.display().to_string(),  // use the path as fallback
        }
    }

    /// Extract an extension from a file path, if one is present, in lowercase.
    ///
    /// The extension is the series of characters after the last dot. This
    /// deliberately counts dotfiles, so the ".git" folder has the extension "git".
    ///
    /// ASCII lowercasing is used because these extensions are only compared
    /// against a pre-compiled list of extensions which are known to only exist
    /// within ASCII, so it's alright.
    fn ext(path: &Path) -> Option<String> {
        use std::ascii::AsciiExt;

        let name = match path.file_name() {
            Some(f) => f.to_string_lossy().to_string(),
            None => return None,
        };

        name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase())
B
Ben S 已提交
99 100
    }

B
Ben S 已提交
101
    /// Whether this file is a directory on the filesystem.
102
    pub fn is_directory(&self) -> bool {
B
Ben S 已提交
103
        self.metadata.is_dir()
104 105
    }

B
Ben S 已提交
106 107 108 109 110 111
    /// If this file is a directory on the filesystem, then clone its
    /// `PathBuf` for use in one of our own `Dir` objects, and read a list of
    /// its contents.
    ///
    /// Returns an IO error upon failure, but this shouldn't be used to check
    /// if a `File` is a directory or not! For that, just use `is_directory()`.
B
Benjamin Sago 已提交
112 113
    pub fn to_dir(&self, scan_for_git: bool) -> IOResult<Dir> {
        Dir::read_dir(self.path.clone(), scan_for_git)
114 115
    }

B
Ben S 已提交
116 117
    /// Whether this file is a regular file on the filesystem - that is, not a
    /// directory, a link, or anything else treated specially.
118
    pub fn is_file(&self) -> bool {
B
Ben S 已提交
119
        self.metadata.is_file()
120 121
    }

B
Ben S 已提交
122 123 124
    /// Whether this file is both a regular file *and* executable for the
    /// current user. Executable files have different semantics than
    /// executable directories, and so should be highlighted differently.
B
Ben S 已提交
125
    pub fn is_executable_file(&self) -> bool {
126
        let bit = modes::USER_EXECUTE;
B
Ben S 已提交
127
        self.is_file() && (self.metadata.permissions().mode() & bit) == bit
B
Ben S 已提交
128 129
    }

B
Ben S 已提交
130
    /// Whether this file is a symlink on the filesystem.
131
    pub fn is_link(&self) -> bool {
B
Ben S 已提交
132
        self.metadata.file_type().is_symlink()
133
    }
134

B
Benjamin Sago 已提交
135 136
    /// Whether this file is a named pipe on the filesystem.
    pub fn is_pipe(&self) -> bool {
137
        self.metadata.file_type().is_fifo()
B
Ben S 已提交
138 139
    }

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    /// Whether this file is a char device on the filesystem.
    pub fn is_char_device(&self) -> bool {
        self.metadata.file_type().is_char_device()
    }

    /// Whether this file is a block device on the filesystem.
    pub fn is_block_device(&self) -> bool {
        self.metadata.file_type().is_block_device()
    }

    /// Whether this file is a socket on the filesystem.
    pub fn is_socket(&self) -> bool {
        self.metadata.file_type().is_socket()
    }


156 157 158 159 160 161 162
    /// Re-prefixes the path pointed to by this file, if it's a symlink, to
    /// make it an absolute path that can be accessed from whichever
    /// directory exa is being run from.
    fn reorient_target_path(&self, path: &Path) -> PathBuf {
        if path.is_absolute() {
            path.to_path_buf()
        }
163
        else if let Some(dir) = self.parent_dir {
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
            dir.join(&*path)
        }
        else if let Some(parent) = self.path.parent() {
            parent.join(&*path)
        }
        else {
            self.path.join(&*path)
        }
    }

    /// Again assuming this file is a symlink, follows that link and returns
    /// the result of following it.
    ///
    /// For a working symlink that the user is allowed to follow,
    /// this will be the `File` object at the other end, which can then have
    /// its name, colour, and other details read.
180
    ///
181 182 183
    /// For a broken symlink, returns where the file *would* be, if it
    /// existed. If this file cannot be read at all, returns the error that
    /// we got when we tried to read it.
184
    pub fn link_target(&self) -> FileTarget<'dir> {
185 186 187 188 189

        // We need to be careful to treat the path actually pointed to by
        // this file -- which could be absolute or relative -- to the path
        // we actually look up and turn into a `File` -- which needs to be
        // absolute to be accessible from any directory.
190 191 192
        let path = match fs::read_link(&self.path) {
            Ok(p)   => p,
            Err(e)  => return FileTarget::Err(e),
B
Ben S 已提交
193 194
        };

195
        let absolute_path = self.reorient_target_path(&path);
B
Ben S 已提交
196

197 198
        // Use plain `metadata` instead of `symlink_metadata` - we *want* to
        // follow links.
199 200 201 202
        if let Ok(metadata) = fs::metadata(&absolute_path) {
            let ext  = File::ext(&path);
            let name = File::filename(&path);
            FileTarget::Ok(File { parent_dir: None, path, ext, metadata, name })
203 204
        }
        else {
205
            FileTarget::Broken(path)
B
Ben S 已提交
206 207 208
        }
    }

B
Ben S 已提交
209
    /// This file's number of hard links.
B
Ben S 已提交
210
    ///
B
Ben S 已提交
211 212 213 214 215
    /// It also reports whether this is both a regular file, and a file with
    /// multiple links. This is important, because a file with multiple links
    /// is uncommon, while you can come across directories and other types
    /// with multiple links much more often. Thus, it should get highlighted
    /// more attentively.
B
Ben S 已提交
216
    pub fn links(&self) -> f::Links {
217
        let count = self.metadata.nlink();
B
Ben S 已提交
218

B
Ben S 已提交
219
        f::Links {
B
Ben S 已提交
220 221 222
            count: count,
            multiple: self.is_file() && count > 1,
        }
B
Ben S 已提交
223 224
    }

B
Ben S 已提交
225
    /// This file's inode.
B
Ben S 已提交
226
    pub fn inode(&self) -> f::Inode {
227
        f::Inode(self.metadata.ino())
B
Ben S 已提交
228 229
    }

B
Ben S 已提交
230 231 232
    /// This file's number of filesystem blocks.
    ///
    /// (Not the size of each block, which we don't actually report on)
B
Ben S 已提交
233
    pub fn blocks(&self) -> f::Blocks {
234
        if self.is_file() || self.is_link() {
235
            f::Blocks::Some(self.metadata.blocks())
B
Ben S 已提交
236 237
        }
        else {
B
Ben S 已提交
238
            f::Blocks::None
B
Ben S 已提交
239 240 241
        }
    }

B
Ben S 已提交
242
    /// The ID of the user that own this file.
B
Ben S 已提交
243
    pub fn user(&self) -> f::User {
244
        f::User(self.metadata.uid())
B
Ben S 已提交
245 246
    }

B
Ben S 已提交
247
    /// The ID of the group that owns this file.
B
Ben S 已提交
248
    pub fn group(&self) -> f::Group {
249
        f::Group(self.metadata.gid())
B
Ben S 已提交
250 251
    }

B
Benjamin Sago 已提交
252
    /// This file’s size, if it’s a regular file.
B
Ben S 已提交
253 254
    ///
    /// For directories, no size is given. Although they do have a size on
B
Benjamin Sago 已提交
255 256 257 258 259
    /// some filesystems, I’ve never looked at one of those numbers and gained
    /// any information from it. So it’s going to be hidden instead.
    ///
    /// Block and character devices return their device IDs, because they
    /// usually just have a file size of zero.
B
Ben S 已提交
260
    pub fn size(&self) -> f::Size {
B
Ben S 已提交
261
        if self.is_directory() {
B
Ben S 已提交
262
            f::Size::None
B
Ben S 已提交
263
        }
264 265
        else if self.is_char_device() || self.is_block_device() {
            let dev = self.metadata.rdev();
B
Benjamin Sago 已提交
266
            f::Size::DeviceIDs(f::DeviceIDs {
267 268
                major: (dev / 256) as u8,
                minor: (dev % 256) as u8,
B
Benjamin Sago 已提交
269
            })
270
        }
B
Ben S 已提交
271
        else {
B
Ben S 已提交
272
            f::Size::Some(self.metadata.len())
273
        }
B
Ben S 已提交
274 275
    }

B
Ben S 已提交
276 277 278 279 280 281 282
    pub fn modified_time(&self) -> f::Time {
        f::Time(self.metadata.mtime())
    }

    pub fn created_time(&self) -> f::Time {
        f::Time(self.metadata.ctime())
    }
B
Ben S 已提交
283

B
Ben S 已提交
284
    pub fn accessed_time(&self) -> f::Time {
285
        f::Time(self.metadata.atime())
286 287
    }

B
Ben S 已提交
288
    /// This file's 'type'.
B
Ben S 已提交
289
    ///
B
Ben S 已提交
290
    /// This is used in the leftmost column of the permissions column.
B
Ben S 已提交
291 292
    /// Although the file type can usually be guessed from the colour of the
    /// file, `ls` puts this character there, so people will expect it.
293
    pub fn type_char(&self) -> f::Type {
294
        if self.is_file() {
B
Ben S 已提交
295
            f::Type::File
296 297
        }
        else if self.is_directory() {
B
Ben S 已提交
298
            f::Type::Directory
299 300
        }
        else if self.is_pipe() {
B
Ben S 已提交
301
            f::Type::Pipe
302 303
        }
        else if self.is_link() {
B
Ben S 已提交
304
            f::Type::Link
305
        }
306 307 308 309 310 311 312 313 314
        else if self.is_char_device() {
            f::Type::CharDevice
        }
        else if self.is_block_device() {
            f::Type::BlockDevice
        }
        else if self.is_socket() {
            f::Type::Socket
        }
315
        else {
B
Ben S 已提交
316
            f::Type::Special
B
Ben S 已提交
317 318 319
        }
    }

320
    /// This file’s permissions, with flags for each bit.
B
Ben S 已提交
321
    pub fn permissions(&self) -> f::Permissions {
B
Benjamin Sago 已提交
322
        let bits = self.metadata.mode();
B
Ben S 已提交
323 324
        let has_bit = |bit| { bits & bit == bit };

B
Ben S 已提交
325
        f::Permissions {
326 327 328
            user_read:      has_bit(modes::USER_READ),
            user_write:     has_bit(modes::USER_WRITE),
            user_execute:   has_bit(modes::USER_EXECUTE),
B
Benjamin Sago 已提交
329

330 331 332
            group_read:     has_bit(modes::GROUP_READ),
            group_write:    has_bit(modes::GROUP_WRITE),
            group_execute:  has_bit(modes::GROUP_EXECUTE),
B
Benjamin Sago 已提交
333

334 335 336
            other_read:     has_bit(modes::OTHER_READ),
            other_write:    has_bit(modes::OTHER_WRITE),
            other_execute:  has_bit(modes::OTHER_EXECUTE),
B
Benjamin Sago 已提交
337 338 339 340

            sticky:         has_bit(modes::STICKY),
            setgid:         has_bit(modes::SETGID),
            setuid:         has_bit(modes::SETUID),
341 342
        }
    }
B
Ben S 已提交
343

B
Ben S 已提交
344 345 346
    /// Whether this file's extension is any of the strings that get passed in.
    ///
    /// This will always return `false` if the file has no extension.
B
Ben S 已提交
347 348 349 350 351 352 353
    pub fn extension_is_one_of(&self, choices: &[&str]) -> bool {
        match self.ext {
            Some(ref ext)  => choices.contains(&&ext[..]),
            None           => false,
        }
    }

B
Ben S 已提交
354 355
    /// Whether this file's name, including extension, is any of the strings
    /// that get passed in.
B
Ben S 已提交
356 357 358 359
    pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
        choices.contains(&&self.name[..])
    }

B
Ben S 已提交
360 361 362 363 364 365
    /// This file's Git status as two flags: one for staged changes, and the
    /// other for unstaged changes.
    ///
    /// This requires looking at the `git` field of this file's parent
    /// directory, so will not work if this file has just been passed in on
    /// the command line.
B
Ben S 已提交
366
    pub fn git_status(&self) -> f::Git {
B
Benjamin Sago 已提交
367 368
        use std::env::current_dir;

369
        match self.parent_dir {
B
Ben S 已提交
370
            None    => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
B
Benjamin Sago 已提交
371 372 373
            Some(d) => {
                let cwd = match current_dir() {
                    Err(_)  => Path::new(".").join(&self.path),
374
                    Ok(dir) => dir.join(&self.path),
B
Benjamin Sago 已提交
375 376
                };

B
Ben S 已提交
377
                d.git_status(&cwd, self.is_directory())
B
Benjamin Sago 已提交
378
            },
B
Ben S 已提交
379
        }
B
Ben S 已提交
380
    }
B
Ben S 已提交
381 382
}

383

B
Ben S 已提交
384 385
impl<'a> AsRef<File<'a>> for File<'a> {
    fn as_ref(&self) -> &File<'a> {
D
Daniel Lockyer 已提交
386
        self
B
Ben S 已提交
387 388 389
    }
}

390

391 392 393 394 395 396 397 398 399 400 401
/// The result of following a symlink.
pub enum FileTarget<'dir> {

    /// The symlink pointed at a file that exists.
    Ok(File<'dir>),

    /// The symlink pointed at a file that does not exist. Holds the path
    /// where the file would be, if it existed.
    Broken(PathBuf),

    /// There was an IO error when following the link. This can happen if the
B
Benjamin Sago 已提交
402
    /// file isn’t a link to begin with, but also if, say, we don’t have
403 404
    /// permission to follow it.
    Err(IOError),
405 406 407 408

    // Err is its own variant, instead of having the whole thing be inside an
    // `IOResult`, because being unable to follow a symlink is not a serious
    // error -- we just display the error message and move on.
409 410
}

411
impl<'dir> FileTarget<'dir> {
B
Benjamin Sago 已提交
412 413 414

    /// Whether this link doesn’t lead to a file, for whatever reason. This
    /// gets used to determine how to highlight the link in grid views.
415
    pub fn is_broken(&self) -> bool {
B
Benjamin Sago 已提交
416
        match *self {
417 418
            FileTarget::Ok(_)                           => false,
            FileTarget::Broken(_) | FileTarget::Err(_)  => true,
419 420 421 422
        }
    }
}

423

424 425 426 427 428 429 430 431 432 433 434 435
/// More readable aliases for the permission bits exposed by libc.
#[allow(trivial_numeric_casts)]
mod modes {
    use libc;

    pub type Mode = u32;
    // The `libc::mode_t` type’s actual type varies, but the value returned
    // from `metadata.permissions().mode()` is always `u32`.

    pub const USER_READ: Mode     = libc::S_IRUSR as Mode;
    pub const USER_WRITE: Mode    = libc::S_IWUSR as Mode;
    pub const USER_EXECUTE: Mode  = libc::S_IXUSR as Mode;
B
Benjamin Sago 已提交
436

437 438 439
    pub const GROUP_READ: Mode    = libc::S_IRGRP as Mode;
    pub const GROUP_WRITE: Mode   = libc::S_IWGRP as Mode;
    pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode;
B
Benjamin Sago 已提交
440

441 442 443
    pub const OTHER_READ: Mode    = libc::S_IROTH as Mode;
    pub const OTHER_WRITE: Mode   = libc::S_IWOTH as Mode;
    pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode;
B
Benjamin Sago 已提交
444 445 446 447

    pub const STICKY: Mode        = libc::S_ISVTX as Mode;
    pub const SETGID: Mode        = libc::S_ISGID as Mode;
    pub const SETUID: Mode        = libc::S_ISUID as Mode;
448 449 450
}


451
#[cfg(test)]
452 453
mod ext_test {
    use super::File;
454
    use std::path::Path;
B
Benjamin Sago 已提交
455

B
Benjamin Sago 已提交
456 457
    #[test]
    fn extension() {
458
        assert_eq!(Some("dat".to_string()), File::ext(Path::new("fester.dat")))
B
Benjamin Sago 已提交
459 460 461 462
    }

    #[test]
    fn dotfile() {
463
        assert_eq!(Some("vimrc".to_string()), File::ext(Path::new(".vimrc")))
B
Benjamin Sago 已提交
464 465 466 467
    }

    #[test]
    fn no_extension() {
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
        assert_eq!(None, File::ext(Path::new("jarlsberg")))
    }
}


#[cfg(test)]
mod filename_test {
    use super::File;
    use std::path::Path;

    #[test]
    fn file() {
        assert_eq!("fester.dat", File::filename(Path::new("fester.dat")))
    }

    #[test]
    fn no_path() {
        assert_eq!("foo.wha", File::filename(Path::new("/var/cache/foo.wha")))
    }

    #[test]
    fn here() {
        assert_eq!(".", File::filename(Path::new(".")))
    }

    #[test]
    fn there() {
        assert_eq!("..", File::filename(Path::new("..")))
    }

    #[test]
    fn everywhere() {
        assert_eq!("..", File::filename(Path::new("./..")))
    }

    #[test]
    fn topmost() {
        assert_eq!("/", File::filename(Path::new("/")))
506
    }
B
Benjamin Sago 已提交
507
}