Repository: lsd-rs/lsd (language: rust)

Issue:
Show SELinux contexts
It would be great to be able to view SELinux context with lsd.

Just copying ls's -Z argument would be great. Like so:
```
$ ls -Z ~
unconfined_u:object_r:user_home_t:s0 code	  unconfined_u:object_r:user_home_t:s0 Nextcloud
...

$ ls -Zl ~
drwxr-xr-x. 30 mars mars unconfined_u:object_r:user_home_t:s0  4096 May  2 16:46 Nextcloud
...
```

Thanks for a great application! I didn't know i needed before i tried it!


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b9cb76dc..14c5b4b7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Add support for `--no-sort` `-U` from [MichaelAug](https://github.com/MichaelAug)
 - Add `--group-directories-first` as an alias for `--group-dirs=first` to improve compatibility with `coreutils/ls`
 - Add `--permission` flag to choose permission formatting (rwx, octal) from [meain](https://github.com/meain)
+- Display MAC contexts and MAC and ACL indicators from [mmatous](https://github.com/mmatous)
 ### Fixed
 - Support non-bold bright colors [#248](https://github.com/Peltoche/lsd/issues/248) from [meain](https://github.com/meain)
 - Don't automatically dereference symlinks in tree/recursive [#637](https://github.com/Peltoche/lsd/issues/637) from [meain](https://github.com/meain)
diff --git a/Cargo.lock b/Cargo.lock
index befe269f1..366d5d09a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -364,9 +364,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
 
 [[package]]
 name = "lock_api"
-version = "0.4.6"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
+checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
 dependencies = [
  "scopeguard",
 ]
@@ -417,6 +417,7 @@ dependencies = [
  "version_check",
  "wild",
  "winapi",
+ "xattr",
  "xdg",
  "yaml-rust",
 ]
@@ -740,9 +741,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook"
-version = "0.3.13"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
+checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -969,6 +970,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "xattr"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "xdg"
 version = "2.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 156cfac7c..da52ce0dc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,7 @@ serde_yaml = "0.8"
 
 [target.'cfg(unix)'.dependencies]
 users = "0.11.*"
+xattr = "0.2.*"
 
 [target.'cfg(windows)'.dependencies]
 winapi = {version = "0.3.*", features = ["aclapi", "accctrl", "winnt", "winerror", "securitybaseapi", "winbase"]}
diff --git a/README.md b/README.md
index 8711f5188..37732ea3a 100644
--- a/README.md
+++ b/README.md
@@ -252,6 +252,8 @@ permission:
   exec-sticky: 5
   no-access: 245
   octal: 6
+  acl: dark_cyan
+  context: cyan
 date:
   hour-old: 40
   day-old: 42
@@ -348,7 +350,7 @@ Workaround for Konsole: ㅤEdit the config file (or [create it](#config-file-loc
 icons:
     separator: " ㅤ"
 ```
-  
+
 
 This is a known issue in a few terminal emulator. Try using a different terminal emulator like. [Alacritty](https://github.com/alacritty/alacritty) and [Kitty](https://github.com/kovidgoyal/kitty) are really good alternatives. You might also want to check if your font is responsible for causing this.
 To verify this, try running lsd with icons disabled and if it still does not have the first character, then this is an lsd bug:
diff --git a/doc/lsd.md b/doc/lsd.md
index 00ccedaf2..3cbc0a1d2 100644
--- a/doc/lsd.md
+++ b/doc/lsd.md
@@ -128,6 +128,9 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich
 `-U`, `--no-sort`
 : Do not sort. List entries in directory order
 
+`-Z` `--context`
+: Display SELinux or SMACK security context
+
 # ARGS
 
 `<FILE>...`
diff --git a/src/app.rs b/src/app.rs
index 4d96eac32..1ae0edd35 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -266,6 +266,7 @@ pub fn build() -> App<'static, 'static> {
                     "permission",
                     "user",
                     "group",
+                    "context",
                     "size",
                     "date",
                     "name",
@@ -309,6 +310,14 @@ pub fn build() -> App<'static, 'static> {
                 .multiple(true)
                 .help("When showing file information for a symbolic link, show information for the file the link references rather than for the link itself"),
         )
+        .arg(
+            Arg::with_name("context")
+                .short("Z")
+                .long("context")
+                .required(false)
+                .takes_value(false)
+                .help("Print security context (label) of each file"),
+        )
 }
 
 fn validate_date_argument(arg: String) -> Result<(), String> {
diff --git a/src/color.rs b/src/color.rs
index 1dd36b373..6d8639fe1 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -36,6 +36,8 @@ pub enum Elem {
     ExecSticky,
     NoAccess,
     Octal,
+    Acl,
+    Context,
 
     /// Last Time Modified
     DayOld,
@@ -104,6 +106,8 @@ impl Elem {
             Elem::ExecSticky => theme.permission.exec_sticky,
             Elem::NoAccess => theme.permission.no_access,
             Elem::Octal => theme.permission.octal,
+            Elem::Acl => theme.permission.acl,
+            Elem::Context => theme.permission.context,
 
             Elem::DayOld => theme.date.day_old,
             Elem::HourOld => theme.date.hour_old,
@@ -343,6 +347,8 @@ mod elem {
                 exec_sticky: Color::Magenta,
                 no_access: Color::AnsiValue(245), // Grey
                 octal: Color::AnsiValue(6),
+                acl: Color::DarkCyan,
+                context: Color::Cyan,
             },
             file_type: theme::FileType {
                 file: theme::File {
diff --git a/src/color/theme.rs b/src/color/theme.rs
index 9554ddc39..a63fd67dc 100644
--- a/src/color/theme.rs
+++ b/src/color/theme.rs
@@ -39,6 +39,8 @@ pub struct Permission {
     pub exec_sticky: Color,
     pub no_access: Color,
     pub octal: Color,
+    pub acl: Color,
+    pub context: Color,
 }
 
 #[derive(Debug, Deserialize, PartialEq)]
@@ -134,6 +136,8 @@ impl Default for Permission {
             exec_sticky: Color::AnsiValue(5),
             no_access: Color::AnsiValue(245), // Grey
             octal: Color::AnsiValue(6),
+            acl: Color::DarkCyan,
+            context: Color::Cyan,
         }
     }
 }
diff --git a/src/config_file.rs b/src/config_file.rs
index aa551bb1c..8c9eb08eb 100644
--- a/src/config_file.rs
+++ b/src/config_file.rs
@@ -197,7 +197,7 @@ classic: false
 # == Blocks ==
 # This specifies the columns and their order when using the long and the tree
 # layout.
-# Possible values: permission, user, group, size, size_value, date, name, inode
+# Possible values: permission, user, group, context, size, size_value, date, name, inode
 blocks:
   - permission
   - user
diff --git a/src/display.rs b/src/display.rs
index d828db32f..76e317f7f 100644
--- a/src/display.rs
+++ b/src/display.rs
@@ -271,10 +271,12 @@ fn get_output<'a>(
                 block_vec.extend(vec![
                     meta.file_type.render(colors),
                     meta.permissions.render(colors, flags),
+                    meta.access_control.render_method(colors),
                 ]);
             }
             Block::User => block_vec.push(meta.owner.render_user(colors)),
             Block::Group => block_vec.push(meta.owner.render_group(colors)),
+            Block::Context => block_vec.push(meta.access_control.render_context(colors)),
             Block::Size => {
                 let pad = if Layout::Tree == flags.layout && 0 == tree.0 && 0 == i {
                     None
diff --git a/src/flags/blocks.rs b/src/flags/blocks.rs
index 0732dc32c..3d33cac85 100644
--- a/src/flags/blocks.rs
+++ b/src/flags/blocks.rs
@@ -45,6 +45,12 @@ impl Blocks {
             result = value;
         }
 
+        if matches.is_present("context") {
+            if let Ok(blocks) = result.as_mut() {
+                blocks.optional_insert_context();
+            }
+        }
+
         if matches.is_present("inode") {
             if let Ok(blocks) = result.as_mut() {
                 blocks.optional_prepend_inode();
@@ -144,6 +150,23 @@ impl Blocks {
             self.prepend_inode()
         }
     }
+
+    /// Tnserts a [Block] of variant [INode](Block::Context), if `self` does not already contain a
+    /// [Block] of that variant. The positioning will be best-effort approximation of coreutils
+    /// ls position for a security context
+    fn optional_insert_context(&mut self) {
+        if self.0.contains(&Block::Context) {
+            return;
+        }
+        let mut pos = self.0.iter().position(|elem| *elem == Block::Group);
+        if pos.is_none() {
+            pos = self.0.iter().position(|elem| *elem == Block::User);
+        }
+        match pos {
+            Some(pos) => self.0.insert(pos + 1, Block::Context),
+            None => self.0.insert(0, Block::Context),
+        }
+    }
 }
 
 /// The default value for `Blocks` contains a [Vec] of [Name](Block::Name).
@@ -159,6 +182,7 @@ pub enum Block {
     Permission,
     User,
     Group,
+    Context,
     Size,
     SizeValue,
     Date,
@@ -175,6 +199,7 @@ impl TryFrom<&str> for Block {
             "permission" => Ok(Self::Permission),
             "user" => Ok(Self::User),
             "group" => Ok(Self::Group),
+            "context" => Ok(Self::Context),
             "size" => Ok(Self::Size),
             "size_value" => Ok(Self::SizeValue),
             "date" => Ok(Self::Date),
@@ -446,6 +471,46 @@ mod test_blocks {
         let blocks = Blocks(vec![Block::Permission, Block::Date]);
         assert_eq!(Some(blocks), Blocks::from_config(&c));
     }
+
+    #[test]
+    fn test_context_not_present_on_cli() {
+        let argv = vec!["lsd", "--long"];
+        let matches = app::build().get_matches_from_safe(argv).unwrap();
+        let parsed_blocks = Blocks::configure_from(&matches, &Config::with_none()).unwrap();
+        let it = parsed_blocks.0.iter();
+        assert_eq!(it.filter(|&x| *x == Block::Context).count(), 0);
+    }
+
+    #[test]
+    fn test_context_present_if_context_on() {
+        let argv = vec!["lsd", "--context"];
+        let matches = app::build().get_matches_from_safe(argv).unwrap();
+        let parsed_blocks = Blocks::configure_from(&matches, &Config::with_none()).unwrap();
+        let it = parsed_blocks.0.iter();
+        assert_eq!(it.filter(|&x| *x == Block::Context).count(), 1);
+    }
+
+    #[test]
+    fn test_only_one_context_no_other_blocks_affected() {
+        let argv = vec![
+            "lsd",
+            "--context",
+            "--blocks",
+            "name,date,size,context,group,user,permission",
+        ];
+        let matches = app::build().get_matches_from_safe(argv).unwrap();
+        let test_blocks = Blocks(vec![
+            Block::Name,
+            Block::Date,
+            Block::Size,
+            Block::Context,
+            Block::Group,
+            Block::User,
+            Block::Permission,
+        ]);
+        let parsed_blocks = Blocks::from_arg_matches(&matches).unwrap().unwrap();
+        assert_eq!(test_blocks, parsed_blocks);
+    }
 }
 
 #[cfg(test)]
@@ -506,4 +571,9 @@ mod test_block {
     fn test_links() {
         assert_eq!(Ok(Block::Links), Block::try_from("links"));
     }
+
+    #[test]
+    fn test_context() {
+        assert_eq!(Ok(Block::Context), Block::try_from("context"));
+    }
 }
diff --git a/src/flags/layout.rs b/src/flags/layout.rs
index 0b917bbf6..1e0da4974 100644
--- a/src/flags/layout.rs
+++ b/src/flags/layout.rs
@@ -30,6 +30,7 @@ impl Configurable<Layout> for Layout {
         } else if matches.is_present("long")
             || matches.is_present("oneline")
             || matches.is_present("inode")
+            || matches.is_present("context")
             || matches!(matches.values_of("blocks"), Some(values) if values.len() > 1)
         // TODO: handle this differently
         {
diff --git a/src/meta/access_control.rs b/src/meta/access_control.rs
new file mode 100644
index 000000000..4695df107
--- /dev/null
+++ b/src/meta/access_control.rs
@@ -0,0 +1,152 @@
+use crate::color::{ColoredString, Colors, Elem};
+use std::path::Path;
+
+#[derive(Clone, Debug)]
+pub struct AccessControl {
+    has_acl: bool,
+    selinux_context: String,
+    smack_context: String,
+}
+
+impl AccessControl {
+    #[cfg(not(unix))]
+    pub fn for_path(_: &Path) -> Self {
+        Self::from_data(false, &[], &[])
+    }
+
+    #[cfg(unix)]
+    pub fn for_path(path: &Path) -> Self {
+        let has_acl = !xattr::get(path, Method::Acl.name())
+            .unwrap_or_default()
+            .unwrap_or_default()
+            .is_empty();
+        let selinux_context = xattr::get(path, Method::Selinux.name())
+            .unwrap_or_default()
+            .unwrap_or_default();
+        let smack_context = xattr::get(path, Method::Smack.name())
+            .unwrap_or_default()
+            .unwrap_or_default();
+
+        Self::from_data(has_acl, &selinux_context, &smack_context)
+    }
+
+    fn from_data(has_acl: bool, selinux_context: &[u8], smack_context: &[u8]) -> Self {
+        let selinux_context = String::from_utf8_lossy(selinux_context).to_string();
+        let smack_context = String::from_utf8_lossy(smack_context).to_string();
+        Self {
+            has_acl,
+            selinux_context,
+            smack_context,
+        }
+    }
+
+    pub fn render_method(&self, colors: &Colors) -> ColoredString {
+        if self.has_acl {
+            colors.colorize(String::from("+"), &Elem::Acl)
+        } else if !self.selinux_context.is_empty() || !self.smack_context.is_empty() {
+            colors.colorize(String::from("."), &Elem::Context)
+        } else {
+            colors.colorize(String::from(""), &Elem::Acl)
+        }
+    }
+
+    pub fn render_context(&self, colors: &Colors) -> ColoredString {
+        let mut context = self.selinux_context.clone();
+        if !self.smack_context.is_empty() {
+            if !context.is_empty() {
+                context += "+";
+            }
+            context += &self.smack_context;
+        }
+        if context.is_empty() {
+            context += "?";
+        }
+        colors.colorize(context, &Elem::Context)
+    }
+}
+
+#[cfg(unix)]
+enum Method {
+    Acl,
+    Selinux,
+    Smack,
+}
+
+#[cfg(unix)]
+impl Method {
+    fn name(&self) -> &'static str {
+        match self {
+            Method::Acl => "system.posix_acl_access",
+            Method::Selinux => "security.selinux",
+            Method::Smack => "security.SMACK64",
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::AccessControl;
+    use crate::color::{Colors, ThemeOption};
+    use crossterm::style::{Color, Stylize};
+
+    #[test]
+    fn test_acl_only_indicator() {
+        // actual file would collide with proper AC data, no permission to scrub those
+        let access_control = AccessControl::from_data(true, &[], &[]);
+
+        assert_eq!(
+            String::from("+").with(Color::DarkCyan),
+            access_control.render_method(&Colors::new(ThemeOption::Default))
+        );
+    }
+
+    #[test]
+    fn test_smack_only_indicator() {
+        let access_control = AccessControl::from_data(false, &[], &[b'a']);
+
+        assert_eq!(
+            String::from(".").with(Color::Cyan),
+            access_control.render_method(&Colors::new(ThemeOption::Default))
+        );
+    }
+
+    #[test]
+    fn test_acl_and_selinux_indicator() {
+        let access_control = AccessControl::from_data(true, &[b'a'], &[]);
+
+        assert_eq!(
+            String::from("+").with(Color::DarkCyan),
+            access_control.render_method(&Colors::new(ThemeOption::Default))
+        );
+    }
+
+    #[test]
+    fn test_selinux_context() {
+        let access_control = AccessControl::from_data(false, &[b'a'], &[]);
+
+        assert_eq!(
+            String::from("a").with(Color::Cyan),
+            access_control.render_context(&Colors::new(ThemeOption::Default))
+        );
+    }
+
+    #[test]
+    fn test_selinux_and_smack_context() {
+        let access_control = AccessControl::from_data(false, &[b'a'], &[b'b']);
+
+        assert_eq!(
+            String::from("a+b").with(Color::Cyan),
+            access_control.render_context(&Colors::new(ThemeOption::Default))
+        );
+    }
+
+    #[test]
+    fn test_no_context() {
+        let access_control = AccessControl::from_data(false, &[], &[]);
+
+        assert_eq!(
+            String::from("?").with(Color::Cyan),
+            access_control.render_context(&Colors::new(ThemeOption::Default))
+        );
+    }
+}
diff --git a/src/meta/mod.rs b/src/meta/mod.rs
index ad531ea11..9df37f5b3 100644
--- a/src/meta/mod.rs
+++ b/src/meta/mod.rs
@@ -1,3 +1,4 @@
+mod access_control;
 mod date;
 mod filetype;
 mod indicator;
@@ -12,6 +13,7 @@ mod symlink;
 #[cfg(windows)]
 mod windows_utils;
 
+pub use self::access_control::AccessControl;
 pub use self::date::Date;
 pub use self::filetype::FileType;
 pub use self::indicator::Indicator;
@@ -44,6 +46,7 @@ pub struct Meta {
     pub inode: INode,
     pub links: Links,
     pub content: Option<Vec<Meta>>,
+    pub access_control: AccessControl,
 }
 
 impl Meta {
@@ -231,6 +234,7 @@ impl Meta {
         #[cfg(windows)]
         let (owner, permissions) = windows_utils::get_file_data(path)?;
 
+        let access_control = AccessControl::for_path(path);
         let file_type = FileType::new(&metadata, symlink_meta.as_ref(), &permissions);
         let name = Name::new(path, file_type);
         let inode = INode::from(&metadata);
@@ -249,6 +253,7 @@ impl Meta {
             name,
             file_type,
             content: None,
+            access_control,
         })
     }
 }


Test patch: