Procházet zdrojové kódy

feat(xattr): Handle formatting and display of binary extended attributes.

Fixes issue 425
TODO: Add FreeBSD support
TODO: Add environment variable for maximum length for hex display of
binary values.  Currently hard coded to 16.
Robert Minsk před 2 roky
rodič
revize
475ed40af9
5 změnil soubory, kde provedl 518 přidání a 273 odebrání
  1. 92 4
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 405 263
      src/fs/feature/xattr.rs
  4. 19 2
      src/fs/file.rs
  5. 1 4
      src/output/details.rs

+ 92 - 4
Cargo.lock

@@ -111,6 +111,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "base64"
+version = "0.21.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -331,6 +337,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "deranged"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
+dependencies = [
+ "powerfmt",
+]
+
 [[package]]
 name = "dunce"
 version = "1.0.4"
@@ -389,6 +404,7 @@ dependencies = [
  "palette",
  "percent-encoding",
  "phf",
+ "plist",
  "proc-mounts",
  "scoped_threadpool",
  "terminal_size",
@@ -463,9 +479,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
 
 [[package]]
 name = "hashbrown"
-version = "0.14.0"
+version = "0.14.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
 
 [[package]]
 name = "hermit-abi"
@@ -525,9 +541,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.0.0"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
 dependencies = [
  "equivalent",
  "hashbrown",
@@ -608,6 +624,15 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.11"
@@ -817,6 +842,19 @@ version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
 
+[[package]]
+name = "plist"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
+dependencies = [
+ "base64",
+ "indexmap",
+ "line-wrap",
+ "quick-xml",
+ "time",
+]
+
 [[package]]
 name = "plotters"
 version = "0.3.5"
@@ -845,6 +883,12 @@ dependencies = [
  "plotters-backend",
 ]
 
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.66"
@@ -863,6 +907,15 @@ dependencies = [
  "partition-identity",
 ]
 
+[[package]]
+name = "quick-xml"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.33"
@@ -972,6 +1025,12 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -1136,6 +1195,35 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
+dependencies = [
+ "deranged",
+ "itoa",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
+dependencies = [
+ "time-core",
+]
+
 [[package]]
 name = "timeago"
 version = "0.4.2"

+ 1 - 0
Cargo.toml

@@ -84,6 +84,7 @@ palette = { version = "0.7.3", default-features = false, features = ["std"] }
 once_cell = "1.18.0"
 percent-encoding = "2.3.0"
 phf = { version = "0.11.2", features = ["macros"] }
+plist = { version = "1.6.0", default-features = false }
 scoped_threadpool = "0.1"
 uutils_term_grid = "0.3"
 terminal_size = "0.3.0"

+ 405 - 263
src/fs/feature/xattr.rs

@@ -1,33 +1,41 @@
-//! Extended attribute support for Darwin and Linux systems.
+//! Extended attribute support for `NetBSD`, `Darwin`, and `Linux` systems.
 
 #![allow(trivial_casts)] // for ARM
 
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-use std::cmp::Ordering;
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-use std::ffi::CString;
+use std::fmt::{Display, Formatter};
 use std::io;
 use std::path::Path;
+use std::str;
 
-pub const ENABLED: bool = cfg!(any(target_os = "macos", target_os = "linux"));
+pub const ENABLED: bool = cfg!(any(
+    target_os = "macos",
+    target_os = "linux",
+    target_os = "netbsd"
+));
+
+#[derive(Debug)]
+pub struct Attribute {
+    pub name: String,
+    pub value: Option<Vec<u8>>,
+}
 
 pub trait FileAttributes {
     fn attributes(&self) -> io::Result<Vec<Attribute>>;
     fn symlink_attributes(&self) -> io::Result<Vec<Attribute>>;
 }
 
-#[cfg(any(target_os = "macos", target_os = "linux"))]
+#[cfg(any(target_os = "macos", target_os = "linux", target_os = "netbsd"))]
 impl FileAttributes for Path {
     fn attributes(&self) -> io::Result<Vec<Attribute>> {
-        list_attrs(&lister::Lister::new(FollowSymlinks::Yes), self)
+        extended_attrs::attributes(self, true)
     }
 
     fn symlink_attributes(&self) -> io::Result<Vec<Attribute>> {
-        list_attrs(&lister::Lister::new(FollowSymlinks::No), self)
+        extended_attrs::attributes(self, false)
     }
 }
 
-#[cfg(not(any(target_os = "macos", target_os = "linux")))]
+#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "netbsd")))]
 impl FileAttributes for Path {
     fn attributes(&self) -> io::Result<Vec<Attribute>> {
         Ok(Vec::new())
@@ -38,309 +46,443 @@ impl FileAttributes for Path {
     }
 }
 
-/// Attributes which can be passed to `Attribute::list_with_flags`
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-#[derive(Copy, Clone)]
-pub enum FollowSymlinks {
-    Yes,
-    No,
-}
-
-/// Extended attribute
-#[derive(Debug, Clone)]
-pub struct Attribute {
-    pub name: String,
-    pub value: String,
-}
-
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-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_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());
+#[cfg(any(target_os = "macos", target_os = "linux", target_os = "netbsd"))]
+mod extended_attrs {
+    use super::Attribute;
+    use libc::{c_char, c_void, size_t, ssize_t, ENODATA, ERANGE};
+    use std::ffi::{CStr, CString, OsStr, OsString};
+    use std::io;
+    use std::os::unix::ffi::OsStrExt;
+    use std::path::Path;
+    use std::ptr::null_mut;
+
+    #[cfg(target_os = "macos")]
+    mod os {
+        use libc::{
+            c_char, c_int, c_void, getxattr, listxattr, size_t, ssize_t, XATTR_NOFOLLOW,
+            XATTR_SHOWCOMPRESSION,
+        };
+
+        // Options to use for MacOS versions of getxattr and listxattr
+        fn get_options(follow_symlinks: bool) -> c_int {
+            if follow_symlinks {
+                XATTR_SHOWCOMPRESSION
+            } else {
+                XATTR_NOFOLLOW | XATTR_SHOWCOMPRESSION
             }
-
-            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),
-    }])
-}
-
-#[cfg(any(target_os = "macos", target_os = "linux"))]
-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();
-
-    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());
-            }
-
-            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(),
-            });
-        }
-    }
-
-    Ok(names)
-}
-
-#[cfg(target_os = "macos")]
-mod lister {
-    use super::FollowSymlinks;
-    use libc::{c_char, c_int, c_void, size_t, ssize_t};
-    use std::ffi::CString;
-    use std::ptr;
-
-    extern "C" {
-        fn listxattr(
+        // Wrapper around listxattr that handles symbolic links
+        pub(super) fn list_xattr(
+            follow_symlinks: bool,
             path: *const c_char,
             namebuf: *mut c_char,
             size: size_t,
-            options: c_int,
-        ) -> ssize_t;
+        ) -> ssize_t {
+            // SAFETY: Calling C function
+            unsafe { listxattr(path, namebuf, size, get_options(follow_symlinks)) }
+        }
 
-        fn getxattr(
+        // Wrapper around getxattr that handles symbolic links
+        pub(super) fn get_xattr(
+            follow_symlinks: bool,
             path: *const c_char,
             name: *const c_char,
             value: *mut c_void,
             size: size_t,
-            position: u32,
-            options: c_int,
-        ) -> ssize_t;
-    }
-
-    pub struct Lister {
-        c_flags: c_int,
-    }
-
-    impl Lister {
-        pub fn new(do_follow: FollowSymlinks) -> Self {
-            let c_flags: c_int = match do_follow {
-                FollowSymlinks::Yes => 0x0001,
-                FollowSymlinks::No => 0x0000,
-            };
-
-            Self { c_flags }
-        }
-
-        pub fn translate_attribute_data(&self, input: &[u8]) -> String {
-            unsafe {
-                std::str::from_utf8_unchecked(input)
-                    .trim_end_matches('\0')
-                    .into()
-            }
+        ) -> ssize_t {
+            // SAFETY: Calling C function
+            unsafe { getxattr(path, name, value, size, 0, get_options(follow_symlinks)) }
         }
+    }
 
-        pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
-            unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags) }
+    #[cfg(any(target_os = "linux", target_os = "netbsd"))]
+    mod os {
+        use libc::{c_char, c_void, size_t, ssize_t};
+
+        #[cfg(target_os = "linux")]
+        use libc::{getxattr, lgetattr, listxattr, llistxattr};
+
+        #[cfg(target_os = "netbsd")]
+        extern "C" {
+            fn getxattr(
+                path: *const c_char,
+                name: *const c_char,
+                value: *mut c_void,
+                size: size_t,
+            ) -> ssize_t;
+            fn lgetxattr(
+                path: *const c_char,
+                name: *const c_char,
+                value: *mut c_void,
+                size: size_t,
+            ) -> ssize_t;
+            fn listxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
+            fn llistxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
         }
 
-        pub fn listxattr_second(
-            &self,
-            c_path: &CString,
-            buf: &mut [u8],
-            bufsize: size_t,
+        // Wrapper around listxattr and llistattr for handling symbolic links
+        pub(super) fn list_xattr(
+            follow_symlinks: bool,
+            path: *const c_char,
+            namebuf: *mut c_char,
+            size: size_t,
         ) -> ssize_t {
-            unsafe {
-                listxattr(
-                    c_path.as_ptr(),
-                    buf.as_mut_ptr().cast(),
-                    bufsize,
-                    self.c_flags,
-                )
-            }
-        }
-
-        pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t {
-            unsafe {
-                getxattr(
-                    c_path.as_ptr(),
-                    c_name.as_ptr().cast(),
-                    ptr::null_mut(),
-                    0,
-                    0,
-                    self.c_flags,
-                )
+            if follow_symlinks {
+                // SAFETY: Calling C function
+                unsafe { listxattr(path, namebuf, size) }
+            } else {
+                // SAFETY: Calling C function
+                unsafe { llistxattr(path, namebuf, size) }
             }
         }
 
-        pub fn getxattr_second(
-            &self,
-            c_path: &CString,
-            c_name: &CString,
-            buf: &mut [u8],
-            bufsize: size_t,
+        // Wrapper around getxattr and lgetxattr for handling symbolic links
+        pub(super) fn get_xattr(
+            follow_symlinks: bool,
+            path: *const c_char,
+            name: *const c_char,
+            value: *mut c_void,
+            size: 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,
-                )
+            if follow_symlinks {
+                // SAFETY: Calling C function
+                unsafe { getxattr(path, name, value, size) }
+            } else {
+                // SAFETY: Calling C function
+                unsafe { lgetxattr(path, name, value, size) }
             }
         }
     }
-}
 
-#[cfg(target_os = "linux")]
-mod lister {
-    use super::FollowSymlinks;
-    use libc::{c_char, c_void, size_t, ssize_t};
-    use std::ffi::CString;
-    use std::ptr;
+    // Split attribute name list.  Each attribute name is null terminated in the
+    // list.
+    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "netbsd"))]
+    fn split_attribute_list(buffer: &[u8]) -> Vec<OsString> {
+        buffer[..buffer.len() - 1] // Skip trailing null
+            .split(|&c| c == 0)
+            .filter(|&s| !s.is_empty())
+            .map(OsStr::from_bytes)
+            .map(std::borrow::ToOwned::to_owned)
+            .collect()
+    }
 
-    extern "C" {
-        fn listxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
+    // Calling getxattr and listxattr is a two part process.  The first call
+    // a null ptr for buffer and a zero buffer size is passed and the function
+    // returns the needed buffer size.  The second call the buffer ptr and the
+    // buffer size is passed and the buffer is filled.  Care must be taken if
+    // the buffer size changes between the first and second call.
+    fn get_loop<F: Fn(*mut u8, usize) -> ssize_t>(f: F) -> io::Result<Option<Vec<u8>>> {
+        let mut buffer: Vec<u8> = Vec::new();
+        loop {
+            let buffer_size = match f(null_mut(), 0) {
+                -1 => return Err(io::Error::last_os_error()),
+                0 => return Ok(None),
+                size => size as size_t,
+            };
 
-        fn llistxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t;
+            buffer.resize(buffer_size, 0);
+
+            return match f(buffer.as_mut_ptr(), buffer_size) {
+                -1 => {
+                    let last_os_error = io::Error::last_os_error();
+                    if last_os_error.raw_os_error() == Some(ERANGE) {
+                        // Passed buffer was to small so retry again.
+                        continue;
+                    }
+                    Err(last_os_error)
+                }
+                0 => Ok(None),
+                len => {
+                    // Just in case the size shrunk
+                    buffer.truncate(len as usize);
+                    Ok(Some(buffer))
+                }
+            };
+        }
+    }
 
-        fn getxattr(
+    // Get a list of all attribute names on `path`
+    fn list_attributes(
+        path: &CStr,
+        follow_symlinks: bool,
+        lister: fn(
+            follow_symlinks: bool,
             path: *const c_char,
-            name: *const c_char,
-            value: *mut c_void,
+            namebuf: *mut c_char,
             size: size_t,
-        ) -> ssize_t;
+        ) -> ssize_t,
+    ) -> io::Result<Vec<OsString>> {
+        Ok(
+            get_loop(|buf, size| lister(follow_symlinks, path.as_ptr(), buf.cast(), size))?
+                .map_or_else(Vec::new, |buffer| split_attribute_list(&buffer)),
+        )
+    }
 
-        fn lgetxattr(
+    // Get the attribute value `name` on `path`
+    fn get_attribute(
+        path: &CStr,
+        name: &CStr,
+        follow_symlinks: bool,
+        getter: fn(
+            follow_symlinks: bool,
             path: *const c_char,
             name: *const c_char,
             value: *mut c_void,
             size: size_t,
-        ) -> ssize_t;
+        ) -> ssize_t,
+    ) -> io::Result<Option<Vec<u8>>> {
+        get_loop(|buf, size| {
+            getter(
+                follow_symlinks,
+                path.as_ptr(),
+                name.as_ptr(),
+                buf.cast(),
+                size,
+            )
+        })
+        .or_else(|err| {
+            if err.raw_os_error() == Some(ENODATA) {
+                // This handles the case when the named attribute is not on the
+                // path.  This is for mainly handling the special case for the
+                // security.selinux attribute mentioned below.  This can
+                // also happen when an attribute is deleted between listing
+                // the attributes and getting its value.
+                Ok(None)
+            } else {
+                Err(err)
+            }
+        })
     }
 
-    pub struct Lister {
-        follow_symlinks: FollowSymlinks,
+    // Specially handle security.linux for filesystem that do not list attributes.
+    #[cfg(target_os = "linux")]
+    fn get_selinux_attribute(path: &CStr, follow_symlinks: bool) -> io::Result<Vec<Attribute>> {
+        const SELINUX_XATTR_NAME: &str = "security.selinux";
+        let name = CString::new(SELINUX_XATTR_NAME).unwrap();
+
+        get_attribute(path, &name, follow_symlinks, os::get_xattr).map(|value| {
+            if value.is_some() {
+                vec![Attribute {
+                    name: String::from(SELINUX_XATTR_NAME),
+                    value,
+                }]
+            } else {
+                Vec::new()
+            }
+        })
     }
 
-    impl Lister {
-        pub fn new(follow_symlinks: FollowSymlinks) -> Lister {
-            Lister { follow_symlinks }
+    // Get a vector of all attribute names and values on `path`
+    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "netbsd"))]
+    pub fn attributes(path: &Path, follow_symlinks: bool) -> io::Result<Vec<Attribute>> {
+        let path = CString::new(path.as_os_str().as_bytes())
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+        let attr_names = list_attributes(&path, follow_symlinks, os::list_xattr)?;
+
+        #[cfg(target_os = "linux")]
+        if attr_names.is_empty() {
+            // Some filesystems, like sysfs, return nothing on listxattr, even though the security
+            // attribute is set.
+            return get_selinux_attribute(&c_path, follow_symlinks);
         }
 
-        pub fn translate_attribute_data(&self, input: &[u8]) -> String {
-            String::from_utf8_lossy(input).trim_end_matches('\0').into()
+        let mut attrs = Vec::with_capacity(attr_names.len());
+        for attr_name in attr_names {
+            if let Some(name) = attr_name.to_str() {
+                let attr_name =
+                    CString::new(name).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+                let value = get_attribute(&path, &attr_name, follow_symlinks, os::get_xattr)?;
+                attrs.push(Attribute {
+                    name: name.to_string(),
+                    value,
+                });
+            }
         }
 
-        pub fn listxattr_first(&self, c_path: &CString) -> ssize_t {
-            let listxattr = match self.follow_symlinks {
-                FollowSymlinks::Yes => listxattr,
-                FollowSymlinks::No => llistxattr,
-            };
+        Ok(attrs)
+    }
+}
 
-            unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0) }
+const ATTRIBUTE_VALUE_MAX_HEX_LENGTH: usize = 16;
+
+// Display for an attribute.  Attribute values that have a custom display are
+// enclosed in curley brackets.
+impl Display for Attribute {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!("{}: ", self.name))?;
+        if let Some(value) = custom_attr_display(self) {
+            f.write_fmt(format_args!("<{value}>"))
+        } else {
+            match &self.value {
+                None => f.write_str("<empty>"),
+                Some(value) => {
+                    if let Some(val) = custom_value_display(value) {
+                        f.write_fmt(format_args!("<{val}>"))
+                    } else if let Ok(v) = str::from_utf8(value) {
+                        f.write_fmt(format_args!("{:?}", v.trim_end_matches(char::from(0))))
+                    } else if value.len() <= ATTRIBUTE_VALUE_MAX_HEX_LENGTH {
+                        f.write_fmt(format_args!("{value:02x?}"))
+                    } else {
+                        f.write_fmt(format_args!("<length {}>", value.len()))
+                    }
+                }
+            }
         }
+    }
+}
 
-        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,
-            };
+struct AttributeDisplay {
+    pub attribute: &'static str,
+    pub display: fn(&Attribute) -> Option<String>,
+}
 
-            unsafe { listxattr(c_path.as_ptr(), buf.as_mut_ptr().cast(), bufsize) }
-        }
+// Check for a custom display by attribute name and call the display function
+fn custom_attr_display(attribute: &Attribute) -> Option<String> {
+    let name = attribute.name.as_str();
+    // Strip off MacOS Metadata Persistence Flags
+    // See https://eclecticlight.co/2020/11/02/controlling-metadata-tricks-with-persistence/
+    #[cfg(target_os = "macos")]
+    let name = name.rsplit_once('#').map_or(name, |n| n.0);
+
+    ATTRIBUTE_DISPLAYS
+        .iter()
+        .find(|c| c.attribute == name)
+        .and_then(|c| (c.display)(attribute))
+}
 
-        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,
-            };
+#[cfg(target_os = "macos")]
+const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[
+    AttributeDisplay {
+        attribute: "com.apple.lastuseddate",
+        display: display_lastuseddate,
+    },
+    AttributeDisplay {
+        attribute: "com.apple.macl",
+        display: display_macl,
+    },
+];
+
+#[cfg(not(target_os = "macos"))]
+const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[];
+
+// com.apple.lastuseddate is two 64-bit values representing the seconds and nano seconds
+// from January 1, 1970
+#[cfg(target_os = "macos")]
+fn display_lastuseddate(attribute: &Attribute) -> Option<String> {
+    use chrono::{Local, SecondsFormat, TimeZone};
+
+    attribute
+        .value
+        .as_ref()
+        .filter(|value| value.len() == 16)
+        .and_then(|value| {
+            let sec = i64::from_le_bytes(value[0..8].try_into().unwrap());
+            let n_sec = i64::from_le_bytes(value[8..].try_into().unwrap());
+            Local
+                .timestamp_opt(sec, n_sec as u32)
+                .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Nanos, true))
+                .single()
+        })
+}
 
-            unsafe { getxattr(c_path.as_ptr(), c_name.as_ptr().cast(), ptr::null_mut(), 0) }
+// com.apple.macl is a two byte flag followed by a uuid for the application
+#[cfg(target_os = "macos")]
+fn format_macl(value: &[u8]) -> String {
+    const HEX: [u8; 16] = [
+        b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e',
+        b'f',
+    ];
+    const GROUPS: [(usize, usize, u8); 6] = [
+        (0, 4, b';'),
+        (5, 13, b'-'),
+        (14, 18, b'-'),
+        (19, 23, b'-'),
+        (24, 28, b'-'),
+        (29, 41, 0),
+    ];
+
+    let mut dst = [0; 41];
+    let mut i = 0;
+
+    for (start, end, sep) in GROUPS {
+        for j in (start..end).step_by(2) {
+            let x = value[i];
+            i += 1;
+            dst[j] = HEX[(x >> 4) as usize];
+            dst[j + 1] = HEX[(x & 0x0f) as usize];
+        }
+        if sep != 0 {
+            dst[end] = sep;
         }
+    }
 
-        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 { String::from_utf8_unchecked(dst.to_vec()) }
+}
 
-            unsafe {
-                getxattr(
-                    c_path.as_ptr(),
-                    c_name.as_ptr().cast(),
-                    buf.as_mut_ptr().cast::<libc::c_void>(),
-                    bufsize,
-                )
-            }
-        }
+// See https://book.hacktricks.xyz/macos-hardening/macos-security-and-privilege-escalation/macos-security-protections/macos-tcc
+#[cfg(target_os = "macos")]
+fn display_macl(attribute: &Attribute) -> Option<String> {
+    attribute
+        .value
+        .as_ref()
+        .filter(|v| v.len() % 18 == 0)
+        .map(|v| {
+            let macls = v
+                .as_slice()
+                .chunks(18)
+                .filter(|c| c[0] != 0 || c[1] != 0)
+                .map(format_macl)
+                .collect::<Vec<String>>()
+                .join(", ");
+            format!("[{macls}]")
+        })
+}
+
+// plist::XmlWriter takes the writer instead of borrowing it.  This is a
+// wrapper around a borrowed vector that just forwards the Write trait
+// calls to the borrowed vector.
+struct BorrowedWriter<'a> {
+    pub buffer: &'a mut Vec<u8>,
+}
+
+impl<'a> io::Write for BorrowedWriter<'a> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.buffer.write(buf)
     }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.buffer.flush()
+    }
+
+    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
+        self.buffer.write_all(buf)
+    }
+}
+
+fn custom_value_display(value: &[u8]) -> Option<String> {
+    if value.starts_with(b"bplist") {
+        plist_value_display(value)
+    } else {
+        None
+    }
+}
+
+// Convert a binary plist to a XML plist.
+fn plist_value_display(value: &[u8]) -> Option<String> {
+    let reader = io::Cursor::new(value);
+    plist::Value::from_reader(reader).ok().and_then(|v| {
+        let mut buffer = Vec::new();
+        v.to_writer_xml_with_options(
+            BorrowedWriter {
+                buffer: &mut buffer,
+            },
+            &plist::XmlWriteOptions::default()
+                .indent(b' ', 0)
+                .root_element(false),
+        )
+        .ok()
+        .and_then(|()| str::from_utf8(&buffer).ok())
+        .map(|s| format!("<plist version=\"1.0\">{}</plist>", s.replace('\n', "")))
+    })
 }

+ 19 - 2
src/fs/file.rs

@@ -9,6 +9,8 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
 use std::os::windows::fs::MetadataExt;
 use std::path::{Path, PathBuf};
 #[cfg(unix)]
+use std::str;
+#[cfg(unix)]
 use std::sync::Mutex;
 use std::sync::OnceLock;
 
@@ -22,6 +24,7 @@ use crate::fs::dir::Dir;
 use crate::fs::feature::xattr;
 use crate::fs::feature::xattr::{Attribute, FileAttributes};
 use crate::fs::fields as f;
+use crate::fs::fields::SecurityContextType;
 use crate::fs::recursive_size::RecursiveSize;
 
 use super::mounts::all_mounts;
@@ -841,18 +844,32 @@ impl<'dir> File<'dir> {
     }
 
     /// This file’s security context field.
+    #[cfg(unix)]
     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,
+            Some(attr) => match &attr.value {
+                None => SecurityContextType::None,
+                Some(value) => match str::from_utf8(value) {
+                    Ok(v) => SecurityContextType::SELinux(v.trim_end_matches(char::from(0))),
+                    Err(_) => SecurityContextType::None,
+                },
+            },
+            None => SecurityContextType::None,
         };
 
         f::SecurityContext { context }
     }
+
+    #[cfg(windows)]
+    pub fn security_context(&self) -> f::SecurityContext<'_> {
+        f::SecurityContext {
+            context: SecurityContextType::None,
+        }
+    }
 }
 
 impl<'a> AsRef<File<'a>> for File<'a> {

+ 1 - 4
src/output/details.rs

@@ -448,10 +448,7 @@ impl<'a> Render<'a> {
     }
 
     fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
-        let name = TextCell::paint(
-            self.theme.ui.perms.attribute,
-            format!("{}=\"{}\"", xattr.name, xattr.value),
-        );
+        let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{xattr}"));
         Row {
             cells: None,
             name,