Parcourir la source

Add option to show security attribute and improve extended support

Add a command line option -Z/--context to show the security context of
objects, similar to ls(1).

Show the actual extended attribute values on -@/--extended, instead of
just their length.

In case of a symbolic link, show the extended attributes of the symbolic
link itself, not the target. This matches the behavior of ls(1) and is
more intuitive.

Closes: #254
Christian Göttsche il y a 4 ans
Parent
commit
afeac47c13

+ 1 - 0
README.md

@@ -74,6 +74,7 @@ These options are available when running with `--long` (`-l`):
 - **-t**, **--time=(field)**: which timestamp field to use
 - **-u**, **--accessed**: use the accessed timestamp field
 - **-U**, **--created**: use the created timestamp field
+- **-Z**, **--context**: list each file’s security context
 - **-@**, **--extended**: list each file’s extended attributes and sizes
 - **--changed**: use the changed timestamp field
 - **--git**: list each file’s Git status, if tracked or ignored

+ 1 - 0
completions/fish/exa.fish

@@ -89,3 +89,4 @@ complete -c exa        -l 'no-time'        -d "Suppress the time field"
 # Optional extras
 complete -c exa -l 'git' -d "List each file's Git status, if tracked"
 complete -c exa -s '@' -l 'extended' -d "List each file's extended attributes and sizes"
+complete -c exa -s 'Z' -l 'context' -d "List each file's security context"

+ 1 - 0
completions/zsh/_exa

@@ -53,6 +53,7 @@ __exa() {
         {-U,--created}"[Use the created timestamp field]" \
         --git"[List each file's Git status, if tracked]" \
         {-@,--extended}"[List each file's extended attributes and sizes]" \
+        {-Z,--context}"[List each file's security context]" \
         '*:filename:_files'
 }
 

+ 3 - 0
man/exa.1.md

@@ -180,6 +180,9 @@ These options are available when running with `--long` (`-l`):
 `-@`, `--extended`
 : List each file’s extended attributes and sizes.
 
+`-Z`, `--context`
+: List each file's security context.
+
 `--git`  [if exa was built with git support]
 : List each file’s Git status, if tracked.
 

+ 121 - 51
src/fs/feature/xattr.rs

@@ -3,6 +3,7 @@
 #![allow(trivial_casts)]  // for ARM
 
 use std::cmp::Ordering;
+use std::ffi::CString;
 use std::io;
 use std::path::Path;
 
@@ -50,58 +51,98 @@ pub enum FollowSymlinks {
 #[derive(Debug, Clone)]
 pub struct Attribute {
     pub name: String,
-    pub size: usize,
+    pub value: String,
 }
 
 
 #[cfg(any(target_os = "macos", target_os = "linux"))]
-pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
-    use std::ffi::CString;
+fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Result<Vec<Attribute>> {
+    const SELINUX_XATTR_NAME: &str = "security.selinux";
+    const ENODATA: i32 = 61;
 
-    let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) {
-        Some(cstring) => cstring,
-        None => {
-            return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?"));
-        }
+    let c_attr_name = CString::new(SELINUX_XATTR_NAME).map_err(|e| {
+        io::Error::new(io::ErrorKind::Other, e)
+    })?;
+    let size = lister.getxattr_first(c_path, &c_attr_name);
+
+    let size = match size.cmp(&0) {
+        Ordering::Less => {
+            let e = io::Error::last_os_error();
+
+            if e.kind() == io::ErrorKind::Other && e.raw_os_error() == Some(ENODATA) {
+                return Ok(Vec::new())
+            }
+
+            return Err(e)
+        },
+        Ordering::Equal => return Err(io::Error::from(io::ErrorKind::InvalidData)),
+        Ordering::Greater => size as usize,
     };
 
+    let mut buf_value = vec![0_u8; size];
+    let size = lister.getxattr_second(c_path, &c_attr_name, &mut buf_value, size);
+
+    match size.cmp(&0) {
+        Ordering::Less => return Err(io::Error::last_os_error()),
+        Ordering::Equal => return Err(io::Error::from(io::ErrorKind::InvalidData)),
+        Ordering::Greater => (),
+    }
+
+    Ok(vec![Attribute {
+        name:  String::from(SELINUX_XATTR_NAME),
+        value: lister.translate_attribute_data(&buf_value),
+    }])
+}
+
+pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result<Vec<Attribute>> {
+    let c_path = CString::new(path.to_str().ok_or(io::Error::new(io::ErrorKind::Other, "Error: path not convertible to string"))?).map_err(|e| {
+        io::Error::new(io::ErrorKind::Other, e)
+    })?;
+
     let bufsize = lister.listxattr_first(&c_path);
-    match bufsize.cmp(&0) {
+    let bufsize = match bufsize.cmp(&0) {
         Ordering::Less     => return Err(io::Error::last_os_error()),
-        Ordering::Equal    => return Ok(Vec::new()),
-        Ordering::Greater  => {},
-    }
+        // Some filesystems, like sysfs, return nothing on listxattr, even though the security
+        // attribute is set.
+        Ordering::Equal    => return get_secattr(lister, &c_path),
+        Ordering::Greater  => bufsize as usize,
+    };
 
-    let mut buf = vec![0_u8; bufsize as usize];
-    let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
+    let mut buf = vec![0_u8; bufsize];
 
-    match err.cmp(&0) {
+    match lister.listxattr_second(&c_path, &mut buf, bufsize).cmp(&0) {
         Ordering::Less     => return Err(io::Error::last_os_error()),
         Ordering::Equal    => return Ok(Vec::new()),
         Ordering::Greater  => {},
     }
 
     let mut names = Vec::new();
-    if err > 0 {
-        // End indices of the attribute names
-        // the buffer contains 0-terminated c-strings
-        let idx = buf.iter().enumerate().filter_map(|(i, v)|
-            if *v == 0 { Some(i) } else { None }
-        );
-        let mut start = 0;
-
-        for end in idx {
-            let c_end = end + 1; // end of the c-string (including 0)
-            let size = lister.getxattr(&c_path, &buf[start..c_end]);
-
-            if size > 0 {
-                names.push(Attribute {
-                    name: lister.translate_attribute_name(&buf[start..end]),
-                    size: size as usize,
-                });
+
+    for attr_name in buf.split(|c| c == &0) {
+        if attr_name.is_empty() {
+            continue;
+        }
+
+        let c_attr_name = CString::new(attr_name).map_err(|e| {
+            io::Error::new(io::ErrorKind::Other, e)
+        })?;
+        let size = lister.getxattr_first(&c_path, &c_attr_name);
+
+        if size > 0 {
+            let mut buf_value = vec![0_u8; size as usize];
+            if lister.getxattr_second(&c_path, &c_attr_name, &mut buf_value, size as usize) < 0 {
+                return Err(io::Error::last_os_error());
             }
 
-            start = c_end;
+            names.push(Attribute {
+                name:  lister.translate_attribute_data(attr_name),
+                value: lister.translate_attribute_data(&buf_value),
+            });
+        } else {
+            names.push(Attribute {
+                name:  lister.translate_attribute_data(attr_name),
+                value: String::new(),
+            });
         }
     }
 
@@ -148,8 +189,8 @@ mod lister {
             Self { c_flags }
         }
 
-        pub fn translate_attribute_name(&self, input: &[u8]) -> String {
-            unsafe { std::str::from_utf8_unchecked(input).into() }
+        pub fn translate_attribute_data(&self, input: &[u8]) -> String {
+            unsafe { std::str::from_utf8_unchecked(input).trim_end_matches('\0').into() }
         }
 
         pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
@@ -163,22 +204,22 @@ mod lister {
             }
         }
 
-        pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
+        pub fn listxattr_second(&self, c_path: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
             unsafe {
                 listxattr(
                     c_path.as_ptr(),
-                    buf.as_mut_ptr().cast::<c_char>(),
-                    bufsize as size_t,
+                    buf.as_mut_ptr().cast(),
+                    bufsize,
                     self.c_flags,
                 )
             }
         }
 
-        pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
+        pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t {
             unsafe {
                 getxattr(
                     c_path.as_ptr(),
-                    buf.as_ptr().cast::<c_char>(),
+                    c_name.as_ptr().cast(),
                     ptr::null_mut(),
                     0,
                     0,
@@ -186,6 +227,19 @@ mod lister {
                 )
             }
         }
+
+        pub fn getxattr_second(&self, c_path: &CString, c_name: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
+            unsafe {
+                getxattr(
+                    c_path.as_ptr(),
+                    c_name.as_ptr().cast(),
+                    buf.as_mut_ptr().cast::<libc::c_void>(),
+                    bufsize,
+                    0,
+                    self.c_flags,
+                )
+            }
+        }
     }
 }
 
@@ -234,8 +288,8 @@ mod lister {
             Lister { follow_symlinks }
         }
 
-        pub fn translate_attribute_name(&self, input: &[u8]) -> String {
-            String::from_utf8_lossy(input).into_owned()
+        pub fn translate_attribute_data(&self, input: &[u8]) -> String {
+            String::from_utf8_lossy(input).trim_end_matches('\0').into()
         }
 
         pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
@@ -246,14 +300,14 @@ mod lister {
 
             unsafe {
                 listxattr(
-                    c_path.as_ptr().cast(),
+                    c_path.as_ptr(),
                     ptr::null_mut(),
                     0,
                 )
             }
         }
 
-        pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec<u8>, bufsize: ssize_t) -> ssize_t {
+        pub fn listxattr_second(&self, c_path: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
             let listxattr = match self.follow_symlinks {
                 FollowSymlinks::Yes  => listxattr,
                 FollowSymlinks::No   => llistxattr,
@@ -261,27 +315,43 @@ mod lister {
 
             unsafe {
                 listxattr(
-                    c_path.as_ptr().cast(),
+                    c_path.as_ptr(),
                     buf.as_mut_ptr().cast(),
-                    bufsize as size_t,
+                    bufsize,
                 )
             }
         }
 
-        pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t {
+        pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t {
             let getxattr = match self.follow_symlinks {
-                FollowSymlinks::Yes  => getxattr,
-                FollowSymlinks::No   => lgetxattr,
+                FollowSymlinks::Yes => getxattr,
+                FollowSymlinks::No  => lgetxattr,
             };
 
             unsafe {
                 getxattr(
-                    c_path.as_ptr().cast(),
-                    buf.as_ptr().cast(),
+                    c_path.as_ptr(),
+                    c_name.as_ptr().cast(),
                     ptr::null_mut(),
                     0,
                 )
             }
         }
+
+        pub fn getxattr_second(&self, c_path: &CString, c_name: &CString, buf: &mut [u8], bufsize: size_t) -> ssize_t {
+            let getxattr = match self.follow_symlinks {
+                FollowSymlinks::Yes => getxattr,
+                FollowSymlinks::No  => lgetxattr,
+            };
+
+            unsafe {
+                getxattr(
+                    c_path.as_ptr(),
+                    c_name.as_ptr().cast(),
+                    buf.as_mut_ptr().cast::<libc::c_void>(),
+                    bufsize,
+                )
+            }
+        }
     }
 }

+ 9 - 0
src/fs/fields.rs

@@ -259,3 +259,12 @@ impl Default for Git {
         }
     }
 }
+
+pub enum SecurityContextType<'a> {
+    SELinux(&'a str),
+    None
+}
+
+pub struct SecurityContext<'a> {
+    pub context: SecurityContextType<'a>,
+}

+ 38 - 4
src/fs/file.rs

@@ -11,6 +11,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
 use log::*;
 
 use crate::fs::dir::Dir;
+use crate::fs::feature::xattr;
+use crate::fs::feature::xattr::{FileAttributes, Attribute};
 use crate::fs::fields as f;
 
 
@@ -66,6 +68,9 @@ pub struct File<'dir> {
     /// directory’s children, and are in fact added specifically by exa; this
     /// means that they should be skipped when recursing.
     pub is_all_all: bool,
+
+    /// The extended attributes of this file.
+    pub extended_attributes: Vec<Attribute>,
 }
 
 impl<'dir> File<'dir> {
@@ -80,8 +85,9 @@ impl<'dir> File<'dir> {
         debug!("Statting file {:?}", &path);
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = false;
+        let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { name, ext, path, metadata, parent_dir, is_all_all })
+        Ok(File { name, ext, path, metadata, parent_dir, is_all_all, extended_attributes })
     }
 
     pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -92,8 +98,9 @@ impl<'dir> File<'dir> {
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = true;
         let parent_dir = Some(parent_dir);
+        let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all })
+        Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all, extended_attributes })
     }
 
     pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result<File<'dir>> {
@@ -103,8 +110,9 @@ impl<'dir> File<'dir> {
         let metadata   = std::fs::symlink_metadata(&path)?;
         let is_all_all = true;
         let parent_dir = Some(parent_dir);
+        let extended_attributes = File::gather_extended_attributes(&path);
 
-        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all })
+        Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all, extended_attributes })
     }
 
     /// A file’s name is derived from its string. This needs to handle directories
@@ -137,6 +145,21 @@ impl<'dir> File<'dir> {
             .to_ascii_lowercase())
     }
 
+    /// Read the extended attributes of a file path.
+    fn gather_extended_attributes(path: &Path) -> Vec<Attribute> {
+        if xattr::ENABLED {
+            match path.symlink_attributes() {
+                Ok(xattrs) => xattrs,
+                Err(e) => {
+                    error!("Error looking up extended attributes for {}: {}", path.display(), e);
+                    Vec::new()
+                }
+            }
+        } else {
+            Vec::new()
+        }
+    }
+
     /// Whether this file is a directory on the filesystem.
     pub fn is_directory(&self) -> bool {
         self.metadata.is_dir()
@@ -261,7 +284,8 @@ impl<'dir> File<'dir> {
             Ok(metadata) => {
                 let ext  = File::ext(&path);
                 let name = File::filename(&path);
-                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false };
+                let extended_attributes = File::gather_extended_attributes(&absolute_path);
+                let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false, extended_attributes };
                 FileTarget::Ok(Box::new(file))
             }
             Err(e) => {
@@ -501,6 +525,16 @@ impl<'dir> File<'dir> {
     pub fn name_is_one_of(&self, choices: &[&str]) -> bool {
         choices.contains(&&self.name[..])
     }
+
+    /// This file’s security context field.
+    pub fn security_context(&self) -> f::SecurityContext<'_> {
+        let context = match &self.extended_attributes.iter().find(|a| a.name == "security.selinux") {
+            Some(attr) => f::SecurityContextType::SELinux(&attr.value),
+            None       => f::SecurityContextType::None
+        };
+
+        f::SecurityContext { context }
+    }
 }
 
 

+ 5 - 4
src/options/flags.rs

@@ -62,9 +62,10 @@ pub static NO_TIME: Arg = Arg { short: None, long: "no-time", takes_value: Takes
 pub static NO_ICONS: Arg = Arg { short: None, long: "no-icons", takes_value: TakesValue::Forbidden };
 
 // optional feature options
-pub static GIT:       Arg = Arg { short: None,       long: "git",               takes_value: TakesValue::Forbidden };
-pub static EXTENDED:  Arg = Arg { short: Some(b'@'), long: "extended",          takes_value: TakesValue::Forbidden };
-pub static OCTAL:     Arg = Arg { short: None,       long: "octal-permissions", takes_value: TakesValue::Forbidden };
+pub static GIT:              Arg = Arg { short: None,       long: "git",               takes_value: TakesValue::Forbidden };
+pub static EXTENDED:         Arg = Arg { short: Some(b'@'), long: "extended",          takes_value: TakesValue::Forbidden };
+pub static OCTAL:            Arg = Arg { short: None,       long: "octal-permissions", takes_value: TakesValue::Forbidden };
+pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context",           takes_value: TakesValue::Forbidden };
 
 
 pub static ALL_ARGS: Args = Args(&[
@@ -80,5 +81,5 @@ pub static ALL_ARGS: Args = Args(&[
     &BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
 
-    &GIT, &EXTENDED, &OCTAL
+    &GIT, &EXTENDED, &OCTAL, &SECURITY_CONTEXT
 ]);

+ 2 - 0
src/options/help.rs

@@ -64,6 +64,7 @@ LONG VIEW OPTIONS
 static GIT_FILTER_HELP: &str = "  --git-ignore               ignore files mentioned in '.gitignore'";
 static GIT_VIEW_HELP:   &str = "  --git                list each file's Git status, if tracked or ignored";
 static EXTENDED_HELP:   &str = "  -@, --extended       list each file's extended attributes and sizes";
+static SECATTR_HELP:    &str = "  -Z, --context        list each file's security context";
 
 
 /// All the information needed to display the help text, which depends
@@ -110,6 +111,7 @@ impl fmt::Display for HelpString {
 
         if xattr::ENABLED {
             write!(f, "\n{}", EXTENDED_HELP)?;
+            write!(f, "\n{}", SECATTR_HELP)?;
         }
 
         writeln!(f)

+ 9 - 6
src/options/view.rs

@@ -117,6 +117,7 @@ impl details::Options {
             table: None,
             header: false,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
+            secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
         };
 
         Ok(details)
@@ -136,6 +137,7 @@ impl details::Options {
             table: Some(TableOptions::deduce(matches, vars)?),
             header: matches.has(&flags::HEADER)?,
             xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?,
+            secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?,
         })
     }
 }
@@ -201,17 +203,18 @@ impl Columns {
         let time_types = TimeTypes::deduce(matches)?;
         let git = matches.has(&flags::GIT)?;
 
-        let blocks = matches.has(&flags::BLOCKS)?;
-        let group  = matches.has(&flags::GROUP)?;
-        let inode  = matches.has(&flags::INODE)?;
-        let links  = matches.has(&flags::LINKS)?;
-        let octal  = matches.has(&flags::OCTAL)?;
+        let blocks           = matches.has(&flags::BLOCKS)?;
+        let group            = matches.has(&flags::GROUP)?;
+        let inode            = matches.has(&flags::INODE)?;
+        let links            = matches.has(&flags::LINKS)?;
+        let octal            = matches.has(&flags::OCTAL)?;
+        let security_context = xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?;
 
         let permissions = ! matches.has(&flags::NO_PERMISSIONS)?;
         let filesize =    ! matches.has(&flags::NO_FILESIZE)?;
         let user =        ! matches.has(&flags::NO_USER)?;
 
-        Ok(Self { time_types, inode, links, blocks, group, git, octal, permissions, filesize, user })
+        Ok(Self { time_types, inode, links, blocks, group, git, octal, security_context, permissions, filesize, user })
     }
 }
 

+ 28 - 28
src/output/details.rs

@@ -71,7 +71,8 @@ use scoped_threadpool::Pool;
 use crate::fs::{Dir, File};
 use crate::fs::dir_action::RecurseOptions;
 use crate::fs::feature::git::GitCache;
-use crate::fs::feature::xattr::{Attribute, FileAttributes};
+use crate::fs::feature::xattr::Attribute;
+use crate::fs::fields::SecurityContextType;
 use crate::fs::filter::FileFilter;
 use crate::output::cell::TextCell;
 use crate::output::file_name::Options as FileStyle;
@@ -105,6 +106,9 @@ pub struct Options {
 
     /// Whether to show each file’s extended attributes.
     pub xattr: bool,
+
+    /// Whether to show each file's security attribute.
+    pub secattr: bool,
 }
 
 
@@ -132,7 +136,7 @@ pub struct Render<'a> {
 
 struct Egg<'a> {
     table_row: Option<TableRow>,
-    xattrs:    Vec<Attribute>,
+    xattrs:    &'a [Attribute],
     errors:    Vec<(io::Error, Option<PathBuf>)>,
     dir:       Option<Dir>,
     file:      &'a File<'a>,
@@ -189,11 +193,22 @@ impl<'a> Render<'a> {
         Ok(())
     }
 
+    /// Whether to show the extended attribute hint
+    pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
+        // Do not show the hint '@' if the only extended attribute is the security
+        // attribute and the security attribute column is active.
+        let xattr_count = file.extended_attributes.len();
+        let selinux_ctx_shown = self.opts.secattr && match file.security_context().context {
+            SecurityContextType::SELinux(_) => true,
+            SecurityContextType::None       => false,
+        };
+        xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
+    }
+
     /// Adds files to the table, possibly recursively. This is easily
     /// parallelisable, and uses a pool of threads.
     fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) {
         use std::sync::{Arc, Mutex};
-        use log::*;
         use crate::fs::feature::xattr;
 
         let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
@@ -207,7 +222,6 @@ impl<'a> Render<'a> {
 
                 scoped.execute(move || {
                     let mut errors = Vec::new();
-                    let mut xattrs = Vec::new();
 
                     // There are three “levels” of extended attribute support:
                     //
@@ -216,7 +230,7 @@ impl<'a> Render<'a> {
                     // 2. If the feature is enabled and the --extended flag
                     //    has been specified, then display an @ in the
                     //    permissions column for files with attributes, the
-                    //    names of all attributes and their lengths, and any
+                    //    names of all attributes and their values, and any
                     //    errors encountered when getting them.
                     // 3. If the --extended flag *hasn’t* been specified, then
                     //    display the @, but don’t display anything else.
@@ -231,28 +245,14 @@ impl<'a> Render<'a> {
                     // printed unless the user passes --extended to signify
                     // that they want to see them.
 
-                    if xattr::ENABLED {
-                        match file.path.attributes() {
-                            Ok(xs) => {
-                                xattrs.extend(xs);
-                            }
-                            Err(e) => {
-                                if self.opts.xattr {
-                                    errors.push((e, None));
-                                }
-                                else {
-                                    error!("Error looking up xattr for {:?}: {:#?}", file.path, e);
-                                }
-                            }
-                        }
-                    }
+                    let xattrs: &[Attribute] = if xattr::ENABLED && self.opts.xattr {
+                        &file.extended_attributes
+                    } else {
+                        &[]
+                    };
 
                     let table_row = table.as_ref()
-                                         .map(|t| t.row_for_file(file, ! xattrs.is_empty()));
-
-                    if ! self.opts.xattr {
-                        xattrs.clear();
-                    }
+                                         .map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
 
                     let mut dir = None;
                     if let Some(r) = self.recurse {
@@ -315,7 +315,7 @@ impl<'a> Render<'a> {
 
                 if ! files.is_empty() {
                     for xattr in egg.xattrs {
-                        rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false)));
+                        rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), false)));
                     }
 
                     for (error, path) in errors {
@@ -330,7 +330,7 @@ impl<'a> Render<'a> {
             let count = egg.xattrs.len();
             for (index, xattr) in egg.xattrs.into_iter().enumerate() {
                 let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
-                let r = self.render_xattr(&xattr, params);
+                let r = self.render_xattr(xattr, params);
                 rows.push(r);
             }
 
@@ -367,7 +367,7 @@ impl<'a> Render<'a> {
     }
 
     fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
-        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
+        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{}=\"{}\"", xattr.name, xattr.value));
         Row { cells: None, name, tree }
     }
 

+ 1 - 10
src/output/grid_details.rs

@@ -7,7 +7,6 @@ use term_grid as grid;
 
 use crate::fs::{Dir, File};
 use crate::fs::feature::git::GitCache;
-use crate::fs::feature::xattr::FileAttributes;
 use crate::fs::filter::FileFilter;
 use crate::output::cell::TextCell;
 use crate::output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
@@ -150,7 +149,7 @@ impl<'a> Render<'a> {
         let (first_table, _) = self.make_table(options, &drender);
 
         let rows = self.files.iter()
-                       .map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
+                       .map(|file| first_table.row_for_file(file, drender.show_xattr_hint(file)))
                        .collect::<Vec<_>>();
 
         let file_names = self.files.iter()
@@ -299,11 +298,3 @@ fn divide_rounding_up(a: usize, b: usize) -> usize {
 
     result
 }
-
-
-fn file_has_xattrs(file: &File<'_>) -> bool {
-    match file.path.attributes() {
-        Ok(attrs)  => ! attrs.is_empty(),
-        Err(_)     => false,
-    }
-}

+ 3 - 0
src/output/render/mod.rs

@@ -35,3 +35,6 @@ pub use self::users::Colours as UserColours;
 
 mod octal;
 // octal uses just one colour
+
+mod securityctx;
+pub use self::securityctx::Colours as SecurityCtxColours;

+ 45 - 0
src/output/render/securityctx.rs

@@ -0,0 +1,45 @@
+use ansi_term::Style;
+
+use crate::fs::fields as f;
+use crate::output::cell::{TextCell, DisplayWidth};
+
+
+impl f::SecurityContext<'_> {
+    pub fn render<C: Colours>(&self, colours: &C) -> TextCell {
+        match &self.context {
+            f::SecurityContextType::None => {
+                TextCell::paint_str(colours.none(), "?")
+            }
+            f::SecurityContextType::SELinux(context) => {
+                let mut chars = Vec::with_capacity(7);
+
+                for (i, part) in context.split(':').enumerate() {
+                    let partcolour = match i {
+                        0 => colours.selinux_user(),
+                        1 => colours.selinux_role(),
+                        2 => colours.selinux_type(),
+                        _ => colours.selinux_range()
+                    };
+                    if i > 0 {
+                        chars.push(colours.selinux_colon().paint(":"));
+                    }
+                    chars.push(partcolour.paint(String::from(part)));
+                }
+
+                TextCell {
+                    contents: chars.into(),
+                    width: DisplayWidth::from(context.len())
+                }
+            }
+        }
+    }
+}
+
+pub trait Colours {
+    fn none(&self)          -> Style;
+    fn selinux_colon(&self) -> Style;
+    fn selinux_user(&self)  -> Style;
+    fn selinux_role(&self)  -> Style;
+    fn selinux_type(&self)  -> Style;
+    fn selinux_range(&self) -> Style;
+}

+ 13 - 0
src/output/table.rs

@@ -44,6 +44,7 @@ pub struct Columns {
     pub group: bool,
     pub git: bool,
     pub octal: bool,
+    pub security_context: bool,
 
     // Defaults to true:
     pub permissions: bool,
@@ -93,6 +94,10 @@ impl Columns {
             columns.push(Column::Group);
         }
 
+        if self.security_context {
+            columns.push(Column::SecurityContext);
+        }
+
         if self.time_types.modified {
             columns.push(Column::Timestamp(TimeType::Modified));
         }
@@ -137,6 +142,8 @@ pub enum Column {
     GitStatus,
     #[cfg(unix)]
     Octal,
+    #[cfg(unix)]
+    SecurityContext,
 }
 
 /// Each column can pick its own **Alignment**. Usually, numbers are
@@ -194,6 +201,8 @@ impl Column {
             Self::GitStatus     => "Git",
             #[cfg(unix)]
             Self::Octal         => "Octal",
+            #[cfg(unix)]
+            Self::SecurityContext => "Security Context",
         }
     }
 }
@@ -493,6 +502,10 @@ impl<'a, 'f> Table<'a> {
             Column::Group => {
                 file.group().render(self.theme, &*self.env.lock_users(), self.user_format)
             }
+            #[cfg(unix)]
+            Column::SecurityContext => {
+                file.security_context().render(self.theme)
+            }
             Column::GitStatus => {
                 self.git_status(file).render(self.theme)
             }

+ 11 - 0
src/theme/default_theme.rs

@@ -66,6 +66,17 @@ impl UiStyles {
                 conflicted:  Red.normal(),
             },
 
+            security_context: SecurityContext {
+                none:       Style::default(),
+                selinux: SELinuxContext {
+                    colon: Style::default().dimmed(),
+                    user:  Blue.normal(),
+                    role:  Green.normal(),
+                    typ:   Yellow.normal(),
+                    range: Cyan.normal(),
+                },
+            },
+
             punctuation:  Fixed(244).normal(),
             date:         Blue.normal(),
             inode:        Purple.normal(),

+ 9 - 0
src/theme/mod.rs

@@ -308,6 +308,15 @@ impl FileNameColours for Theme {
     }
 }
 
+impl render::SecurityCtxColours for Theme {
+    fn none(&self)          -> Style { self.ui.security_context.none }
+    fn selinux_colon(&self) -> Style { self.ui.security_context.selinux.colon }
+    fn selinux_user(&self)  -> Style { self.ui.security_context.selinux.user }
+    fn selinux_role(&self)  -> Style { self.ui.security_context.selinux.role }
+    fn selinux_type(&self)  -> Style { self.ui.security_context.selinux.typ }
+    fn selinux_range(&self) -> Style { self.ui.security_context.selinux.range }
+}
+
 
 /// Some of the styles are **overlays**: although they have the same attribute
 /// set as regular styles (foreground and background colours, bold, underline,

+ 22 - 6
src/theme/ui_styles.rs

@@ -7,12 +7,13 @@ use crate::theme::lsc::Pair;
 pub struct UiStyles {
     pub colourful: bool,
 
-    pub filekinds:  FileKinds,
-    pub perms:      Permissions,
-    pub size:       Size,
-    pub users:      Users,
-    pub links:      Links,
-    pub git:        Git,
+    pub filekinds:        FileKinds,
+    pub perms:            Permissions,
+    pub size:             Size,
+    pub users:            Users,
+    pub links:            Links,
+    pub git:              Git,
+    pub security_context: SecurityContext,
 
     pub punctuation:  Style,
     pub date:         Style,
@@ -104,6 +105,21 @@ pub struct Git {
     pub conflicted: Style,
 }
 
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct SELinuxContext {
+    pub colon: Style,
+    pub user:  Style,
+    pub role:  Style,
+    pub typ:   Style,
+    pub range: Style,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct SecurityContext {
+    pub none:    Style,
+    pub selinux: SELinuxContext,
+}
+
 impl UiStyles {
     pub fn plain() -> Self {
         Self::default()