feat: add selinux contexts support

(exa PR) 855:  Add option to show security attribute and improve extended support
This commit is contained in:
Christina Sørensen 2023-07-29 11:49:36 +02:00 committed by GitHub
commit c04b41bde7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 319 additions and 105 deletions

View file

@ -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 files security context
- **-@**, **--extended**: list each files extended attributes and sizes
- **--changed**: use the changed timestamp field
- **--git**: list each files Git status, if tracked or ignored

View file

@ -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"

View file

@ -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'
}

View file

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

View file

@ -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 bufsize = lister.listxattr_first(&c_path);
match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
Ordering::Equal => return Ok(Vec::new()),
Ordering::Greater => {},
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 => (),
}
let mut buf = vec![0_u8; bufsize as usize];
let err = lister.listxattr_second(&c_path, &mut buf, bufsize);
Ok(vec![Attribute {
name: String::from(SELINUX_XATTR_NAME),
value: lister.translate_attribute_data(&buf_value),
}])
}
match err.cmp(&0) {
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);
let bufsize = match bufsize.cmp(&0) {
Ordering::Less => return Err(io::Error::last_os_error()),
// 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];
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]);
for attr_name in buf.split(|c| c == &0) {
if attr_name.is_empty() {
continue;
}
if size > 0 {
names.push(Attribute {
name: lister.translate_attribute_name(&buf[start..end]),
size: size as usize,
});
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,
)
}
}
}
}

View file

@ -260,6 +260,15 @@ impl Default for Git {
}
}
pub enum SecurityContextType<'a> {
SELinux(&'a str),
None
}
pub struct SecurityContext<'a> {
pub context: SecurityContextType<'a>,
}
#[allow(dead_code)]
#[derive(PartialEq, Copy, Clone)]
pub enum SubdirGitRepoStatus{

View file

@ -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> {
/// directorys 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 files 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 files 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 }
}
}

View file

@ -67,6 +67,7 @@ pub static GIT_REPOS: Arg = Arg { short: None, long: "git-repos",
pub static GIT_REPOS_NO_STAT: Arg = Arg { short: None, long: "git-repos-no-status", 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(&[
@ -82,5 +83,5 @@ pub static ALL_ARGS: Args = Args(&[
&BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
&NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
&GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, &EXTENDED, &OCTAL
&GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, &EXTENDED, &OCTAL, &SECURITY_CONTEXT
]);

View file

@ -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)

View file

@ -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)?,
})
}
}
@ -204,17 +206,18 @@ impl Columns {
let subdir_git_repos = matches.has(&flags::GIT_REPOS)?;
let subdir_git_repos_no_stat = !subdir_git_repos && matches.has(&flags::GIT_REPOS_NO_STAT)?;
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, subdir_git_repos, subdir_git_repos_no_stat,octal, permissions, filesize, user })
Ok(Self { time_types, inode, links, blocks, group, git, subdir_git_repos, subdir_git_repos_no_stat, octal, security_context, permissions, filesize, user })
}
}

View file

@ -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 files 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 *hasnt* been specified, then
// display the @, but dont 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 }
}

View file

@ -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,
}
}

View file

@ -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;

View file

@ -88,6 +88,7 @@ impl f::Permissions {
}
}
#[cfg(windows)]
impl f::Attributes {
pub fn render<C: Colours+FiletypeColours>(&self, colours: &C) -> Vec<ANSIString<'static>> {
let bit = |bit, chr: &'static str, style: Style| {

View file

@ -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;
}

View file

@ -46,6 +46,7 @@ pub struct Columns {
pub subdir_git_repos: bool,
pub subdir_git_repos_no_stat: bool,
pub octal: bool,
pub security_context: bool,
// Defaults to true:
pub permissions: bool,
@ -95,6 +96,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));
}
@ -149,6 +154,8 @@ pub enum Column {
SubdirGitRepoNoStatus,
#[cfg(unix)]
Octal,
#[cfg(unix)]
SecurityContext,
}
/// Each column can pick its own **Alignment**. Usually, numbers are
@ -208,6 +215,8 @@ impl Column {
Self::SubdirGitRepoNoStatus => "Repo",
#[cfg(unix)]
Self::Octal => "Octal",
#[cfg(unix)]
Self::SecurityContext => "Security Context",
}
}
}
@ -507,6 +516,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)
}

View file

@ -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(),

View file

@ -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,

View file

@ -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()