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