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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -331,6 +337,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "deranged"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.4" version = "1.0.4"
@ -389,6 +404,7 @@ dependencies = [
"palette", "palette",
"percent-encoding", "percent-encoding",
"phf", "phf",
"plist",
"proc-mounts", "proc-mounts",
"scoped_threadpool", "scoped_threadpool",
"terminal_size", "terminal_size",
@ -463,9 +479,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.0" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@ -525,9 +541,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.0.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -608,6 +624,15 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.11" version = "0.4.11"
@ -817,6 +842,19 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 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]] [[package]]
name = "plotters" name = "plotters"
version = "0.3.5" version = "0.3.5"
@ -845,6 +883,12 @@ dependencies = [
"plotters-backend", "plotters-backend",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.66" version = "1.0.66"
@ -863,6 +907,15 @@ dependencies = [
"partition-identity", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.33"
@ -972,6 +1025,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1136,6 +1195,35 @@ dependencies = [
"syn", "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]] [[package]]
name = "timeago" name = "timeago"
version = "0.4.2" 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" once_cell = "1.18.0"
percent-encoding = "2.3.0" percent-encoding = "2.3.0"
phf = { version = "0.11.2", features = ["macros"] } phf = { version = "0.11.2", features = ["macros"] }
plist = { version = "1.6.0", default-features = false }
scoped_threadpool = "0.1" scoped_threadpool = "0.1"
uutils_term_grid = "0.3" uutils_term_grid = "0.3"
terminal_size = "0.3.0" 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 #![allow(trivial_casts)] // for ARM
#[cfg(any(target_os = "macos", target_os = "linux"))] use std::fmt::{Display, Formatter};
use std::cmp::Ordering;
#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::ffi::CString;
use std::io; use std::io;
use std::path::Path; 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 { pub trait FileAttributes {
fn attributes(&self) -> io::Result<Vec<Attribute>>; fn attributes(&self) -> io::Result<Vec<Attribute>>;
fn symlink_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 { impl FileAttributes for Path {
fn attributes(&self) -> io::Result<Vec<Attribute>> { 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>> { 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 { impl FileAttributes for Path {
fn attributes(&self) -> io::Result<Vec<Attribute>> { fn attributes(&self) -> io::Result<Vec<Attribute>> {
Ok(Vec::new()) 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", target_os = "netbsd"))]
#[cfg(any(target_os = "macos", target_os = "linux"))] mod extended_attrs {
#[derive(Copy, Clone)] use super::Attribute;
pub enum FollowSymlinks { use libc::{c_char, c_void, size_t, ssize_t, ENODATA, ERANGE};
Yes, use std::ffi::{CStr, CString, OsStr, OsString};
No, use std::io;
} use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::ptr::null_mut;
/// Extended attribute #[cfg(target_os = "macos")]
#[derive(Debug, Clone)] mod os {
pub struct Attribute { use libc::{
pub name: String, c_char, c_int, c_void, getxattr, listxattr, size_t, ssize_t, XATTR_NOFOLLOW,
pub value: String, XATTR_SHOWCOMPRESSION,
} };
#[cfg(any(target_os = "macos", target_os = "linux"))] // Options to use for MacOS versions of getxattr and listxattr
fn get_secattr(lister: &lister::Lister, c_path: &std::ffi::CString) -> io::Result<Vec<Attribute>> { fn get_options(follow_symlinks: bool) -> c_int {
const SELINUX_XATTR_NAME: &str = "security.selinux"; if follow_symlinks {
const ENODATA: i32 = 61; XATTR_SHOWCOMPRESSION
} else {
let c_attr_name = XATTR_NOFOLLOW | XATTR_SHOWCOMPRESSION
CString::new(SELINUX_XATTR_NAME).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let size = lister.getxattr_first(c_path, &c_attr_name);
let size = match size.cmp(&0) {
Ordering::Less => {
let e = io::Error::last_os_error();
if e.kind() == io::ErrorKind::Other && e.raw_os_error() == Some(ENODATA) {
return Ok(Vec::new());
} }
return Err(e);
}
Ordering::Equal => return Err(io::Error::from(io::ErrorKind::InvalidData)),
Ordering::Greater => size as usize,
};
let 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 = // Wrapper around listxattr that handles symbolic links
CString::new(attr_name).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; pub(super) fn list_xattr(
let size = lister.getxattr_first(&c_path, &c_attr_name); follow_symlinks: bool,
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(
path: *const c_char, path: *const c_char,
namebuf: *mut c_char, namebuf: *mut c_char,
size: size_t, 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, path: *const c_char,
name: *const c_char, name: *const c_char,
value: *mut c_void, value: *mut c_void,
size: size_t, size: size_t,
position: u32, ) -> ssize_t {
options: c_int, // SAFETY: Calling C function
) -> ssize_t; unsafe { getxattr(path, name, value, size, 0, get_options(follow_symlinks)) }
}
} }
pub struct Lister { #[cfg(any(target_os = "linux", target_os = "netbsd"))]
c_flags: c_int, 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 { // Split attribute name list. Each attribute name is null terminated in the
pub fn new(do_follow: FollowSymlinks) -> Self { // list.
let c_flags: c_int = match do_follow { #[cfg(any(target_os = "macos", target_os = "linux", target_os = "netbsd"))]
FollowSymlinks::Yes => 0x0001, fn split_attribute_list(buffer: &[u8]) -> Vec<OsString> {
FollowSymlinks::No => 0x0000, 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 { let mut attrs = Vec::with_capacity(attr_names.len());
unsafe { for attr_name in attr_names {
std::str::from_utf8_unchecked(input) if let Some(name) = attr_name.to_str() {
.trim_end_matches('\0') let attr_name =
.into() 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 { Ok(attrs)
unsafe { listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags) } }
} }
pub fn listxattr_second( const ATTRIBUTE_VALUE_MAX_HEX_LENGTH: usize = 16;
&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,
)
}
}
pub fn getxattr_first(&self, c_path: &CString, c_name: &CString) -> ssize_t { // Display for an attribute. Attribute values that have a custom display are
unsafe { // enclosed in curley brackets.
getxattr( impl Display for Attribute {
c_path.as_ptr(), fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
c_name.as_ptr().cast(), f.write_fmt(format_args!("{}: ", self.name))?;
ptr::null_mut(), if let Some(value) = custom_attr_display(self) {
0, f.write_fmt(format_args!("<{value}>"))
0, } else {
self.c_flags, match &self.value {
) None => f.write_str("<empty>"),
} Some(value) => {
} if let Some(val) = custom_value_display(value) {
f.write_fmt(format_args!("<{val}>"))
pub fn getxattr_second( } else if let Ok(v) = str::from_utf8(value) {
&self, f.write_fmt(format_args!("{:?}", v.trim_end_matches(char::from(0))))
c_path: &CString, } else if value.len() <= ATTRIBUTE_VALUE_MAX_HEX_LENGTH {
c_name: &CString, f.write_fmt(format_args!("{value:02x?}"))
buf: &mut [u8], } else {
bufsize: size_t, f.write_fmt(format_args!("<length {}>", value.len()))
) -> 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,
)
} }
} }
} }
} }
#[cfg(target_os = "linux")] struct AttributeDisplay {
mod lister { pub attribute: &'static str,
use super::FollowSymlinks; pub display: fn(&Attribute) -> Option<String>,
use libc::{c_char, c_void, size_t, ssize_t}; }
use std::ffi::CString;
use std::ptr;
extern "C" { // Check for a custom display by attribute name and call the display function
fn listxattr(path: *const c_char, list: *mut c_char, size: size_t) -> ssize_t; 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( #[cfg(target_os = "macos")]
path: *const c_char, const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[
name: *const c_char, AttributeDisplay {
value: *mut c_void, attribute: "com.apple.lastuseddate",
size: size_t, display: display_lastuseddate,
) -> ssize_t; },
AttributeDisplay {
attribute: "com.apple.macl",
display: display_macl,
},
];
fn lgetxattr( #[cfg(not(target_os = "macos"))]
path: *const c_char, const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[];
name: *const c_char,
value: *mut c_void, // com.apple.lastuseddate is two 64-bit values representing the seconds and nano seconds
size: size_t, // from January 1, 1970
) -> ssize_t; #[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 { unsafe { String::from_utf8_unchecked(dst.to_vec()) }
follow_symlinks: FollowSymlinks, }
// 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 { fn flush(&mut self) -> io::Result<()> {
pub fn new(follow_symlinks: FollowSymlinks) -> Lister { self.buffer.flush()
Lister { follow_symlinks } }
}
pub fn translate_attribute_data(&self, input: &[u8]) -> String { fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
String::from_utf8_lossy(input).trim_end_matches('\0').into() self.buffer.write_all(buf)
}
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 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::os::windows::fs::MetadataExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[cfg(unix)] #[cfg(unix)]
use std::str;
#[cfg(unix)]
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::OnceLock; use std::sync::OnceLock;
@ -22,6 +24,7 @@ use crate::fs::dir::Dir;
use crate::fs::feature::xattr; use crate::fs::feature::xattr;
use crate::fs::feature::xattr::{Attribute, FileAttributes}; use crate::fs::feature::xattr::{Attribute, FileAttributes};
use crate::fs::fields as f; use crate::fs::fields as f;
use crate::fs::fields::SecurityContextType;
use crate::fs::recursive_size::RecursiveSize; use crate::fs::recursive_size::RecursiveSize;
use super::mounts::all_mounts; use super::mounts::all_mounts;
@ -841,18 +844,32 @@ impl<'dir> File<'dir> {
} }
/// This files security context field. /// This files security context field.
#[cfg(unix)]
pub fn security_context(&self) -> f::SecurityContext<'_> { pub fn security_context(&self) -> f::SecurityContext<'_> {
let context = match self let context = match self
.extended_attributes() .extended_attributes()
.iter() .iter()
.find(|a| a.name == "security.selinux") .find(|a| a.name == "security.selinux")
{ {
Some(attr) => f::SecurityContextType::SELinux(&attr.value), Some(attr) => match &attr.value {
None => f::SecurityContextType::None, 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 } 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> { 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 { fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
let name = TextCell::paint( let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{xattr}"));
self.theme.ui.perms.attribute,
format!("{}=\"{}\"", xattr.name, xattr.value),
);
Row { Row {
cells: None, cells: None,
name, name,