use super::*;
use sha2::{Digest, Sha256};
use std::ffi::{CString, c_char, c_void};
use std::fs::File;
use std::os::fd::AsRawFd;
unsafe extern "C" {
fn flistxattr(fd: i32, buffer: *mut c_char, size: usize, options: i32) -> isize;
fn fgetxattr(
fd: i32,
name: *const c_char,
value: *mut c_void,
size: usize,
position: u32,
options: i32,
) -> isize;
}
const MAX_NAMES: usize = 65536;
const MAX_BYTES: usize = 4 * 1024 * 1024;
pub(in crate::files) fn digest(file: &File) -> Result<(String, usize), FileError> {
digest_filtered(file, false).map(|(digest, bytes, _)| (digest, bytes))
}
pub(in crate::files) fn trash_digest(file: &File) -> Result<(String, usize, bool), FileError> {
digest_filtered(file, true)
}
fn digest_filtered(file: &File, allow_macl: bool) -> Result<(String, usize, bool), FileError> {
let names = names(file)?;
let mut values: Vec<_> = names.split(|b| *b == 0).filter(|s| !s.is_empty()).collect();
values.sort_unstable();
let mut hash = Sha256::new();
let mut total = 0;
let mut excluded_macl = false;
let mut excluded_bytes = 0;
for name in values {
let c_name = CString::new(name).map_err(|_| invalid())?;
let value = read_value(file, &c_name, MAX_BYTES - total)?;
let size = value.len();
total += size;
if allow_macl && name == b"com.apple.macl" {
excluded_bytes = size;
excluded_macl = true;
continue;
}
hash.update((name.len() as u64).to_be_bytes());
hash.update(name);
hash.update((size as u64).to_be_bytes());
hash.update(value);
}
if names != self::names(file)? {
return Err(FileError::new(
FileErrorCode::Changed,
"attribute names changed while reading",
));
}
Ok((
format!("{:x}", hash.finalize()),
total - excluded_bytes,
excluded_macl,
))
}
fn read_value(file: &File, name: &CString, maximum: usize) -> Result<Vec<u8>, FileError> {
let size = bounded(
unsafe {
fgetxattr(
file.as_raw_fd(),
name.as_ptr(),
std::ptr::null_mut(),
0,
0,
0,
)
},
maximum,
)?;
let mut value = vec![0u8; size];
let actual = unsafe {
fgetxattr(
file.as_raw_fd(),
name.as_ptr(),
value.as_mut_ptr().cast(),
size,
0,
0,
)
};
if bounded(actual, size)? != size {
return Err(FileError::new(
FileErrorCode::Changed,
"attribute changed while reading",
));
}
Ok(value)
}
fn names(file: &File) -> Result<Vec<u8>, FileError> {
let size = bounded(
unsafe { flistxattr(file.as_raw_fd(), std::ptr::null_mut(), 0, 0) },
MAX_NAMES,
)?;
let mut names = vec![0u8; size];
let actual = unsafe { flistxattr(file.as_raw_fd(), names.as_mut_ptr().cast(), size, 0) };
if bounded(actual, size)? != size || (size > 0 && names.last() != Some(&0)) {
return Err(invalid());
}
Ok(names)
}
fn bounded(value: isize, maximum: usize) -> Result<usize, FileError> {
if value < 0 {
return Err(std::io::Error::last_os_error().into());
}
let size = value as usize;
if size > maximum {
return Err(FileError::new(
FileErrorCode::ScanLimit,
"copy xattrs exceed 64 KiB names or 4 MiB aggregate value budget",
));
}
Ok(size)
}
fn invalid() -> FileError {
FileError::new(
FileErrorCode::Changed,
"invalid or changed attribute name list",
)
}