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.
This commit is contained in:
Robert Minsk 2023-11-14 00:15:17 -08:00 committed by Christina Sørensen
parent 9df7dc69c3
commit 475ed40af9
5 changed files with 529 additions and 284 deletions

96
Cargo.lock generated
View file

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

View file

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

View file

@ -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,
}
#[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<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());
// 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;
) -> 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<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()
}
// 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,
};
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<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)),
)
}
// 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<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)
}
})
}
// 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()
}
})
}
// 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 {
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::<libc::c_void>(),
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("<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()))
}
}
}
}
}
}
#[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<String>,
}
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<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);
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<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()
})
}
// 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<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)
}
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::<libc::c_void>(),
bufsize,
)
}
}
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', "")))
})
}

View file

@ -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 files 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> {

View file

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