mirror of
https://github.com/eza-community/eza
synced 2024-10-14 20:03:50 +00:00
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:
parent
9df7dc69c3
commit
475ed40af9
96
Cargo.lock
generated
96
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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', "")))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue