Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/exa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,33 @@ use std::io::{stderr, Write, Result as IOResult};
use std::path::{Component, PathBuf};

use ansi_term::{ANSIStrings, Style};

use log::debug;

use crate::fs::{Dir, File};
use crate::fs::feature::ignore::IgnoreCache;
use crate::fs::feature::git::GitCache;
use crate::fs::get_mounts::get_mount_points;
use crate::options::{Options, Vars};
pub use crate::options::vars;
pub use crate::options::Misfire;
use crate::output::{escape, lines, grid, grid_details, details, View, Mode};

#[macro_use]
extern crate lazy_static;

mod fs;
mod info;
mod options;
mod output;
mod style;

lazy_static! {
// A global cache of mount points to enable lookups for each directory
static ref MOUNT_POINTS: Vec<(PathBuf, String, String)> = {
let mount_points = get_mount_points().unwrap();
mount_points
};
}

/// The main program wrapper.
pub struct Exa<'args, 'w, W: Write + 'w> {
Expand Down
2 changes: 1 addition & 1 deletion src/fs/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub type uid_t = u32;
/// Its ordering is used when sorting by type.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum Type {
Directory, File, Link, Pipe, Socket, CharDevice, BlockDevice, Special,
Directory, File, Link, Pipe, Socket, CharDevice, BlockDevice, Special, Subvolume,
}

impl Type {
Expand Down
72 changes: 71 additions & 1 deletion src/fs/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use std::io::Result as IOResult;
use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt};
use std::path::{Path, PathBuf};
use std::time::{UNIX_EPOCH, Duration};
use std::fs;

use log::{debug, error};

use crate::MOUNT_POINTS;
use crate::fs::dir::Dir;
use crate::fs::fields as f;

Expand Down Expand Up @@ -200,6 +202,71 @@ impl<'dir> File<'dir> {
self.metadata.file_type().is_socket()
}

// Whether this file is a btrfs subvolume
pub fn is_subvolume(&self) -> bool {
if self.is_directory() && (self.metadata.ino() == 2 || self.metadata.ino() == 256) {
// inode numbers look like the file is a subvolume, unwind its
// path to see whether it's on a btrfs volume
let absolute_path = fs::canonicalize(&self.path);
let absolute_path = match absolute_path {
Ok(buf) => buf,
Err(_) => {
return false;
},
};

let mut ancestors: Vec<PathBuf> = Vec::new();
for ancestor in absolute_path.ancestors() {
ancestors.push(ancestor.to_path_buf());
}
ancestors.reverse();
let mut is_on_btrfs = false;
// Start at / and work downwards
for ancestor in ancestors {
for mount_point in MOUNT_POINTS.iter() {
let mount_path = &mount_point.0;
let fs_type = &mount_point.1;
if ancestor.eq(mount_path) {
if "btrfs".eq(fs_type) {
is_on_btrfs = true;
} else {
is_on_btrfs = false;
}
}
}
}
return is_on_btrfs;
}
return false;
}

// Whether this file is a mount point
pub fn is_mount_point(&self) -> bool {
if self.is_directory() {
for mount_point in MOUNT_POINTS.iter() {
let mount_path = &mount_point.0;
if self.path.eq(mount_path) {
return true;
}
}
}
return false;
}

// The filesystem device and type for a mount point
pub fn mount_point_info(&self) -> (Option<PathBuf>, Option<String>, Option<String>) {
if self.is_mount_point() {
for mount_point in MOUNT_POINTS.iter() {
let mount_path = &mount_point.0;
let fs_type = &mount_point.1;
let fs_name = &mount_point.2;
if self.path.eq(mount_path) {
return (Some(mount_path.to_path_buf()), Some(fs_type.to_string()), Some(fs_name.to_string()));
}
}
}
return (None, None, None);
}

/// 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
Expand Down Expand Up @@ -249,7 +316,7 @@ impl<'dir> File<'dir> {
Ok(metadata) => {
let ext = File::ext(&path);
let name = File::filename(&path);
FileTarget::Ok(Box::new(File { parent_dir: None, path, ext, metadata, name, is_all_all: false }))
FileTarget::Ok(Box::new(File { parent_dir: None, path: absolute_path, ext, metadata, name, is_all_all: false }))
}
Err(e) => {
error!("Error following link {:?}: {:#?}", &path, e);
Expand Down Expand Up @@ -366,6 +433,9 @@ impl<'dir> File<'dir> {
if self.is_file() {
f::Type::File
}
else if self.is_subvolume() {
f::Type::Subvolume
}
else if self.is_directory() {
f::Type::Directory
}
Expand Down
54 changes: 54 additions & 0 deletions src/fs/get_mounts/bsd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Wrapper for the BSD getmntinfo() API which returns a list of mountpoints

use std::io::{Error, Result};
use std::ptr;
use std::slice;
use std::path::PathBuf;
use std::ffi::{CStr, OsStr};
use std::os::unix::ffi::OsStrExt;

use libc::{c_int, statfs};

pub static MNT_NOWAIT: i32 = 2;

extern "C" {
#[cfg_attr(target_os = "macos", link_name = "getmntinfo$INODE64")]
fn getmntinfo(mntbufp: *mut *mut statfs, flags: c_int) -> c_int;
}

pub fn get_mount_points() -> Result<Vec<(PathBuf,String)>> {
let mut raw_mounts_ptr: *mut statfs = ptr::null_mut();

let rc = unsafe { getmntinfo(&mut raw_mounts_ptr, MNT_NOWAIT) };

// getmntinfo() has non-obvious error handling behaviour: rc 0 indicates an
// error (presumably because any Unix system should have at least the root
// filesystem), requiring us to check errno for the actual error code. The
// man pages for FreeBSD and Darwin do not acknowledge the possibility of a
// negative return code so we'll simply panic if that happens.

if rc == 0 {
return Err(Error::last_os_error());
}

assert!(rc > 0, "getmntinfo() returned undocumented value: {}", rc);

assert!(
!raw_mounts_ptr.is_null(),
"getmntinfo() returned a null pointer to the list of mountpoints!"
);

let raw_mounts = unsafe { slice::from_raw_parts(raw_mounts_ptr, rc as usize) };

let mounts = raw_mounts
.iter()
.map(|m| unsafe {(
let bytes = CStr::from_ptr(&m.f_mntonname[0]).to_bytes();
PathBuf::from(OsStr::from_bytes(bytes).to_owned()),
let fstype = CStr::from_ptr(&m.f_fstypename[0]).to_str().unwrap().to_owned(),
let fsname = CStr::from_ptr(&m.f_mntfromname[0]).to_str().unwrap().to_owned()
}))
.collect();

Ok(mounts)
}
85 changes: 85 additions & 0 deletions src/fs/get_mounts/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Wrapper for the Linux getmntent() API which returns a list of mountpoints

use std::ffi::CStr;
use std::ffi::OsStr;
use std::io::Result;
use std::mem;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;

use libc::c_char;
use libc::c_int;
use libc::FILE;

#[repr(C)]
#[derive(Debug)]
struct mntent {
mnt_fsname: *mut c_char,
mnt_dir: *mut c_char,
mnt_type: *mut c_char,
mnt_opts: *mut c_char,
mnt_freq: c_int,
mnt_passno: c_int,
}

impl Default for mntent {
fn default() -> Self {
unsafe { mem::zeroed() }
}
}

extern "C" {
fn getmntent(fp: *mut FILE) -> *mut mntent;
fn setmntent(filename: *const c_char, _type: *const c_char) -> *mut FILE;
fn endmntent(fp: *mut FILE) -> c_int;
}

pub fn get_mount_points() -> Result<Vec<(PathBuf,String, String)>> {
let mut mount_points: Vec<(PathBuf,String, String)> = Vec::new();

// The Linux API is somewhat baroque: rather than exposing the kernel's view of the world
// you are expected to provide it with a mounts file which traditionally might have been
// something like /etc/mtab but should be /proc/self/mounts (n.b. /proc/mounts is just a
// symlink to /proc/self/mounts).
let mount_filename = "/proc/self/mounts\0";
let flags = "r\0";

let mount_file_handle = unsafe {
setmntent(
mount_filename.as_ptr() as *const _,
flags.as_ptr() as *const _,
)
};

assert!(
!mount_file_handle.is_null(),
"Attempting to read mounts from {} failed!",
&mount_filename[..mount_filename.len() - 1]
);

loop {
let mount_entry = unsafe { getmntent(mount_file_handle) };
if mount_entry.is_null() {
break;
}

let bytes = unsafe { CStr::from_ptr((*mount_entry).mnt_dir).to_bytes() };
let mount_point = PathBuf::from(OsStr::from_bytes(bytes).to_owned());
let str = unsafe { CStr::from_ptr((*mount_entry).mnt_type).to_str().unwrap() };
let fs_type = str.to_owned();
let str = unsafe { CStr::from_ptr((*mount_entry).mnt_fsname).to_str().unwrap() };
let fs_name = str.to_owned();
mount_points.push((mount_point, fs_type, fs_name));
}

let rc = unsafe { endmntent(mount_file_handle) };
// The documentation is strong enough about this that there's no plausible
// way to attempt handling endmntent() failures:
assert!(
rc == 1,
"endmntent() is always supposed to return 1 but returned {}",
rc
);

Ok(mount_points)
}
9 changes: 9 additions & 0 deletions src/fs/get_mounts/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use self::linux::get_mount_points;

#[cfg(all(unix, not(target_os = "linux")))]
mod bsd;
#[cfg(all(unix, not(target_os = "linux")))]
pub use self::bsd::get_mount_points;
1 change: 1 addition & 0 deletions src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pub mod feature;
pub mod fields;
pub mod filter;
pub mod dir_action;
pub mod get_mounts;
13 changes: 13 additions & 0 deletions src/output/file_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
bits.push(Style::default().paint(class));
}
}
else if self.file.is_mount_point() {
let mount_point_info = self.file.mount_point_info();
bits.push(Style::default().paint(" "));
bits.push(self.colours.normal_arrow().paint("->"));
bits.push(Style::default().paint(" "));

bits.push(Style::default().paint(mount_point_info.2.unwrap()));
bits.push(Style::default().paint(" ("));
bits.push(Style::default().paint(mount_point_info.1.unwrap()));
bits.push(Style::default().paint(")"));
}

bits.into()
}
Expand Down Expand Up @@ -253,6 +264,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {

fn kind_style(&self) -> Option<Style> {
Some(match self.file {
f if f.is_mount_point() => self.colours.mount_point(),
f if f.is_subvolume() => self.colours.subvolume(),
f if f.is_directory() => self.colours.directory(),
f if f.is_executable_file() => self.colours.executable_file(),
f if f.is_link() => self.colours.symlink(),
Expand Down
3 changes: 3 additions & 0 deletions src/output/render/filetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl f::Type {
f::Type::CharDevice => colours.char_device().paint("c"),
f::Type::Socket => colours.socket().paint("s"),
f::Type::Special => colours.special().paint("?"),
f::Type::Subvolume => colours.special().paint("^"),
}
}
}
Expand All @@ -28,4 +29,6 @@ pub trait Colours {
fn char_device(&self) -> Style;
fn socket(&self) -> Style;
fn special(&self) -> Style;
fn subvolume(&self) -> Style;
fn mount_point(&self) -> Style;
}
8 changes: 8 additions & 0 deletions src/style/colours.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub struct FileKinds {
pub socket: Style,
pub special: Style,
pub executable: Style,
pub subvolume: Style,
pub mount_point: Style,
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
Expand Down Expand Up @@ -125,6 +127,8 @@ impl Colours {
socket: Red.bold(),
special: Yellow.normal(),
executable: Green.bold(),
mount_point: Blue.bold().underline(),
subvolume: Blue.bold().blink(),
},

perms: Permissions {
Expand Down Expand Up @@ -280,6 +284,8 @@ impl Colours {
"cd" => self.filekinds.char_device = pair.to_style(), // CHR
"ln" => self.filekinds.symlink = pair.to_style(), // LINK
"or" => self.broken_symlink = pair.to_style(), // ORPHAN
"sv" => self.filekinds.subvolume = pair.to_style(), // SUBVOL
"mp" => self.filekinds.mount_point = pair.to_style(), // MNT
_ => return false,
// Codes we don’t do anything with:
// MULTIHARDLINK, DOOR, SETUID, SETGID, CAPABILITY,
Expand Down Expand Up @@ -383,6 +389,8 @@ impl render::FiletypeColours for Colours {
fn char_device(&self) -> Style { self.filekinds.char_device }
fn socket(&self) -> Style { self.filekinds.socket }
fn special(&self) -> Style { self.filekinds.special }
fn subvolume(&self) -> Style { self.filekinds.subvolume }
fn mount_point(&self) -> Style { self.filekinds.mount_point }
}

impl render::GitColours for Colours {
Expand Down