diff --git a/Cargo.lock b/Cargo.lock index 5c6aba14..1575607f 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index 1eee9d88..5e5389fc 100644 --- a/Cargo.toml +++ b/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" diff --git a/src/fs/feature/xattr.rs b/src/fs/feature/xattr.rs index 622bd8dd..adb92eac 100644 --- a/src/fs/feature/xattr.rs +++ b/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>, +} pub trait FileAttributes { fn attributes(&self) -> io::Result>; fn symlink_attributes(&self) -> io::Result>; } -#[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> { - list_attrs(&lister::Lister::new(FollowSymlinks::Yes), self) + extended_attrs::attributes(self, true) } fn symlink_attributes(&self) -> io::Result> { - 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> { 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, -} +#[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; -/// Extended attribute -#[derive(Debug, Clone)] -pub struct Attribute { - pub name: String, - pub value: String, -} + #[cfg(target_os = "macos")] + mod os { + use libc::{ + c_char, c_int, c_void, getxattr, listxattr, size_t, ssize_t, XATTR_NOFOLLOW, + XATTR_SHOWCOMPRESSION, + }; -#[cfg(any(target_os = "macos", target_os = "linux"))] -fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Result> { - 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()); + // 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> { - 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; + ) -> ssize_t { + // SAFETY: Calling C function + unsafe { getxattr(path, name, value, size, 0, get_options(follow_symlinks)) } + } } - pub struct Lister { - c_flags: c_int, + #[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; + } + + // 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 { + if follow_symlinks { + // SAFETY: Calling C function + unsafe { listxattr(path, namebuf, size) } + } else { + // SAFETY: Calling C function + unsafe { llistxattr(path, namebuf, size) } + } + } + + // 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 { + if follow_symlinks { + // SAFETY: Calling C function + unsafe { getxattr(path, name, value, size) } + } else { + // SAFETY: Calling C function + unsafe { lgetxattr(path, name, value, size) } + } + } } - impl Lister { - pub fn new(do_follow: FollowSymlinks) -> Self { - let c_flags: c_int = match do_follow { - FollowSymlinks::Yes => 0x0001, - FollowSymlinks::No => 0x0000, + // 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 { + 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() + } + + // 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 ssize_t>(f: F) -> io::Result>> { + let mut buffer: Vec = 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, }; - Self { c_flags } + 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)) + } + }; + } + } + + // 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, + namebuf: *mut c_char, + size: size_t, + ) -> ssize_t, + ) -> io::Result> { + Ok( + get_loop(|buf, size| lister(follow_symlinks, path.as_ptr(), buf.cast(), size))? + .map_or_else(Vec::new, |buffer| split_attribute_list(&buffer)), + ) + } + + // 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, + ) -> io::Result>> { + 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) + } + }) + } + + // 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> { + 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() + } + }) + } + + // 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> { + 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 { - unsafe { - std::str::from_utf8_unchecked(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 { - unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags) } - } + Ok(attrs) + } +} - 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(), - bufsize, - self.c_flags, - ) - } - } +const ATTRIBUTE_VALUE_MAX_HEX_LENGTH: usize = 16; - 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, - ) - } - } - - 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::(), - bufsize, - 0, - self.c_flags, - ) +// 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(""), + 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!("", value.len())) + } + } } } } } -#[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; +struct AttributeDisplay { + pub attribute: &'static str, + pub display: fn(&Attribute) -> Option, +} - extern "C" { - fn listxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t; +// Check for a custom display by attribute name and call the display function +fn custom_attr_display(attribute: &Attribute) -> Option { + 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); - fn llistxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t; + ATTRIBUTE_DISPLAYS + .iter() + .find(|c| c.attribute == name) + .and_then(|c| (c.display)(attribute)) +} - fn getxattr( - path: *const c_char, - name: *const c_char, - value: *mut c_void, - size: size_t, - ) -> ssize_t; +#[cfg(target_os = "macos")] +const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[ + AttributeDisplay { + attribute: "com.apple.lastuseddate", + display: display_lastuseddate, + }, + AttributeDisplay { + attribute: "com.apple.macl", + display: display_macl, + }, +]; - fn lgetxattr( - path: *const c_char, - name: *const c_char, - value: *mut c_void, - size: size_t, - ) -> ssize_t; +#[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 { + 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() + }) +} + +// 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 struct Lister { - follow_symlinks: FollowSymlinks, + unsafe { String::from_utf8_unchecked(dst.to_vec()) } +} + +// 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 { + 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::>() + .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, +} + +impl<'a> io::Write for BorrowedWriter<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buffer.write(buf) } - impl Lister { - pub fn new(follow_symlinks: FollowSymlinks) -> Lister { - Lister { follow_symlinks } - } + fn flush(&mut self) -> io::Result<()> { + self.buffer.flush() + } - 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 { - let listxattr = match self.follow_symlinks { - FollowSymlinks::Yes => listxattr, - FollowSymlinks::No => llistxattr, - }; - - unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0) } - } - - 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, - }; - - unsafe { listxattr(c_path.as_ptr(), buf.as_mut_ptr().cast(), bufsize) } - } - - 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, - }; - - unsafe { getxattr(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::(), - bufsize, - ) - } - } + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + self.buffer.write_all(buf) } } + +fn custom_value_display(value: &[u8]) -> Option { + 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 { + 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!("{}", s.replace('\n', ""))) + }) +} diff --git a/src/fs/file.rs b/src/fs/file.rs index 131e3f3b..a0bad716 100644 --- a/src/fs/file.rs +++ b/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> for File<'a> { diff --git a/src/output/details.rs b/src/output/details.rs index 11cebc2a..736933f5 100644 --- a/src/output/details.rs +++ b/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,