use crate::constants::*;
use crate::error::FormatError;
const XATTR_PREFIXES: &[(u8, &str)] = &[
(1, "user."),
(2, "system.posix_acl_access"),
(3, "system.posix_acl_default"),
(4, "trusted."),
(6, "security."),
(7, "system."),
(8, "system.richacl"),
];
#[inline]
fn align_up(n: usize, align: usize) -> usize {
(n + align - 1) & !(align - 1)
}
#[derive(Debug, Clone)]
pub struct ExtendedAttribute {
pub name: String,
pub index: u8,
pub value: Vec<u8>,
}
impl ExtendedAttribute {
pub fn new(full_name: &str, value: Vec<u8>) -> Self {
let (index, suffix) = Self::compress_name(full_name);
Self {
name: suffix,
index,
value,
}
}
pub fn compress_name(name: &str) -> (u8, String) {
let mut best_index = 0u8;
let mut best_prefix_len = 0usize;
for &(idx, prefix) in XATTR_PREFIXES {
if name.starts_with(prefix) && prefix.len() > best_prefix_len {
best_index = idx;
best_prefix_len = prefix.len();
}
}
let suffix = &name[best_prefix_len..];
(best_index, suffix.to_string())
}
pub fn decompress_name(index: u8, suffix: &str) -> String {
for &(idx, prefix) in XATTR_PREFIXES {
if idx == index {
return format!("{}{}", prefix, suffix);
}
}
suffix.to_string()
}
pub fn entry_size(&self) -> u32 {
align_up(self.name.len() + 16, 4) as u32
}
pub fn value_size(&self) -> u32 {
align_up(self.value.len(), 4) as u32
}
pub fn total_size(&self) -> u32 {
self.entry_size() + self.value_size()
}
pub fn hash(&self) -> u32 {
let mut h = 0u32;
for &b in self.name.as_bytes() {
h = (h << NAME_HASH_SHIFT) ^ (h >> (8 * 4 - NAME_HASH_SHIFT)) ^ (b as u32);
}
let value = &self.value;
let full_words = value.len() / 4;
for i in 0..full_words {
let off = i * 4;
let word = u32::from_le_bytes([
value[off],
value[off + 1],
value[off + 2],
value[off + 3],
]);
h = (h << VALUE_HASH_SHIFT) ^ (h >> (8 * 4 - VALUE_HASH_SHIFT)) ^ word;
}
let tail = value.len() % 4;
if tail > 0 {
let off = full_words * 4;
let mut bytes = [0u8; 4];
bytes[..tail].copy_from_slice(&value[off..]);
let word = u32::from_le_bytes(bytes);
h = (h << VALUE_HASH_SHIFT) ^ (h >> (8 * 4 - VALUE_HASH_SHIFT)) ^ word;
}
h
}
}
const NAME_HASH_SHIFT: u32 = 5;
const VALUE_HASH_SHIFT: u32 = 16;
pub struct XattrState {
inode_capacity: u32,
block_capacity: u32,
inline_attrs: Vec<ExtendedAttribute>,
block_attrs: Vec<ExtendedAttribute>,
used_inline: u32,
used_block: u32,
inode_number: u32,
}
impl XattrState {
pub fn new(inode: u32, inode_capacity: u32, block_capacity: u32) -> Self {
Self {
inode_capacity,
block_capacity,
inline_attrs: Vec::new(),
block_attrs: Vec::new(),
used_inline: XATTR_INODE_HEADER_SIZE,
used_block: XATTR_BLOCK_HEADER_SIZE,
inode_number: inode,
}
}
pub fn add(&mut self, attr: ExtendedAttribute) -> Result<(), FormatError> {
let total = attr.total_size();
if self.used_inline + total + 4 <= self.inode_capacity {
self.used_inline += total;
self.inline_attrs.push(attr);
return Ok(());
}
if self.used_block + total + 4 <= self.block_capacity {
self.used_block += total;
self.block_attrs.push(attr);
return Ok(());
}
Err(FormatError::XattrInsufficientSpace(self.inode_number))
}
pub fn has_inline(&self) -> bool {
!self.inline_attrs.is_empty()
}
pub fn has_block(&self) -> bool {
!self.block_attrs.is_empty()
}
pub fn write_inline(&self) -> Result<Vec<u8>, FormatError> {
let capacity = self.inode_capacity as usize;
let mut buf = vec![0u8; capacity];
buf[0..4].copy_from_slice(&XATTR_HEADER_MAGIC.to_le_bytes());
let mut entry_offset = XATTR_INODE_HEADER_SIZE as usize;
let mut value_end = capacity;
for attr in &self.inline_attrs {
let val_size_aligned = align_up(attr.value.len(), 4);
value_end -= val_size_aligned;
let rel_value_offset = value_end - XATTR_INODE_HEADER_SIZE as usize;
if entry_offset + 16 + attr.name.len() > buf.len() {
return Err(FormatError::MalformedXattrBuffer);
}
write_xattr_entry(
&mut buf[entry_offset..],
&attr.name,
attr.index,
rel_value_offset as u16,
attr.value.len() as u32,
0, );
entry_offset += align_up(16 + attr.name.len(), 4);
buf[value_end..value_end + attr.value.len()].copy_from_slice(&attr.value);
}
Ok(buf)
}
pub fn write_block(&self) -> Result<Vec<u8>, FormatError> {
let capacity = self.block_capacity as usize;
let mut buf = vec![0u8; capacity];
buf[0..4].copy_from_slice(&XATTR_HEADER_MAGIC.to_le_bytes());
buf[4..8].copy_from_slice(&1u32.to_le_bytes());
buf[8..12].copy_from_slice(&1u32.to_le_bytes());
let mut sorted: Vec<&ExtendedAttribute> = self.block_attrs.iter().collect();
sorted.sort_by(|a, b| {
a.index.cmp(&b.index)
.then_with(|| a.name.len().cmp(&b.name.len()))
.then_with(|| a.name.cmp(&b.name))
});
let mut entry_offset = XATTR_BLOCK_HEADER_SIZE as usize;
let mut value_end = capacity;
for attr in &sorted {
let val_size_aligned = align_up(attr.value.len(), 4);
value_end -= val_size_aligned;
let rel_value_offset = value_end;
if entry_offset + 16 + attr.name.len() > buf.len() {
return Err(FormatError::MalformedXattrBuffer);
}
write_xattr_entry(
&mut buf[entry_offset..],
&attr.name,
attr.index,
rel_value_offset as u16,
attr.value.len() as u32,
attr.hash(),
);
entry_offset += align_up(16 + attr.name.len(), 4);
buf[value_end..value_end + attr.value.len()].copy_from_slice(&attr.value);
}
Ok(buf)
}
}
fn write_xattr_entry(
buf: &mut [u8],
name: &str,
name_index: u8,
value_offset: u16,
value_size: u32,
hash: u32,
) {
let name_bytes = name.as_bytes();
buf[0] = name_bytes.len() as u8;
buf[1] = name_index;
buf[2..4].copy_from_slice(&value_offset.to_le_bytes());
buf[4..8].copy_from_slice(&0u32.to_le_bytes());
buf[8..12].copy_from_slice(&value_size.to_le_bytes());
buf[12..16].copy_from_slice(&hash.to_le_bytes());
buf[16..16 + name_bytes.len()].copy_from_slice(name_bytes);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compress_name_user_prefix() {
let (idx, suffix) = ExtendedAttribute::compress_name("user.mime_type");
assert_eq!(idx, 1);
assert_eq!(suffix, "mime_type");
}
#[test]
fn test_compress_name_security_prefix() {
let (idx, suffix) = ExtendedAttribute::compress_name("security.selinux");
assert_eq!(idx, 6);
assert_eq!(suffix, "selinux");
}
#[test]
fn test_compress_name_system_posix_acl() {
let (idx, suffix) = ExtendedAttribute::compress_name("system.posix_acl_access");
assert_eq!(idx, 2);
assert_eq!(suffix, "");
}
#[test]
fn test_compress_name_system_generic() {
let (idx, suffix) = ExtendedAttribute::compress_name("system.something");
assert_eq!(idx, 7);
assert_eq!(suffix, "something");
}
#[test]
fn test_compress_name_no_match() {
let (idx, suffix) = ExtendedAttribute::compress_name("unknown.attr");
assert_eq!(idx, 0);
assert_eq!(suffix, "unknown.attr");
}
#[test]
fn test_decompress_name() {
assert_eq!(
ExtendedAttribute::decompress_name(1, "mime_type"),
"user.mime_type"
);
assert_eq!(
ExtendedAttribute::decompress_name(6, "selinux"),
"security.selinux"
);
assert_eq!(
ExtendedAttribute::decompress_name(2, ""),
"system.posix_acl_access"
);
assert_eq!(
ExtendedAttribute::decompress_name(99, "foo"),
"foo"
);
}
#[test]
fn test_entry_and_value_sizes() {
let attr = ExtendedAttribute::new("user.x", vec![0u8; 10]);
assert_eq!(attr.entry_size(), 20);
assert_eq!(attr.value_size(), 12);
assert_eq!(attr.total_size(), 32);
}
#[test]
fn test_hash_deterministic() {
let attr = ExtendedAttribute::new("user.test", b"hello".to_vec());
let h1 = attr.hash();
let h2 = attr.hash();
assert_eq!(h1, h2);
assert_ne!(h1, 0);
}
#[test]
fn test_xattr_state_inline() {
let mut state = XattrState::new(11, INODE_EXTRA_SIZE, 4096);
let attr = ExtendedAttribute::new("user.x", vec![1, 2, 3]);
state.add(attr).unwrap();
assert!(state.has_inline());
assert!(!state.has_block());
let buf = state.write_inline().unwrap();
assert_eq!(buf.len(), INODE_EXTRA_SIZE as usize);
let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
assert_eq!(magic, XATTR_HEADER_MAGIC);
}
#[test]
fn test_xattr_state_overflow_to_block() {
let mut state = XattrState::new(11, XATTR_INODE_HEADER_SIZE, 4096);
let attr = ExtendedAttribute::new("user.large", vec![0u8; 100]);
state.add(attr).unwrap();
assert!(!state.has_inline());
assert!(state.has_block());
let buf = state.write_block().unwrap();
assert_eq!(buf.len(), 4096);
let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
assert_eq!(magic, XATTR_HEADER_MAGIC);
let refcount = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
assert_eq!(refcount, 1);
}
#[test]
fn test_xattr_state_insufficient_space() {
let mut state = XattrState::new(11, XATTR_INODE_HEADER_SIZE, XATTR_BLOCK_HEADER_SIZE);
let attr = ExtendedAttribute::new("user.big", vec![0u8; 100]);
let result = state.add(attr);
assert!(result.is_err());
}
#[test]
fn test_compress_decompress_roundtrip() {
let names = [
"user.custom_key",
"security.selinux",
"trusted.overlay.opaque",
"system.posix_acl_access",
"system.posix_acl_default",
"system.richacl",
"system.other",
];
for full_name in names {
let (idx, suffix) = ExtendedAttribute::compress_name(full_name);
let reconstructed = ExtendedAttribute::decompress_name(idx, &suffix);
assert_eq!(
reconstructed, full_name,
"roundtrip failed for {full_name}"
);
}
}
#[test]
fn test_hash_different_values() {
let a = ExtendedAttribute::new("user.test", b"value_a".to_vec());
let b = ExtendedAttribute::new("user.test", b"value_b".to_vec());
assert_ne!(a.hash(), b.hash());
}
#[test]
fn test_hash_different_names() {
let a = ExtendedAttribute::new("user.alpha", b"same".to_vec());
let b = ExtendedAttribute::new("user.beta", b"same".to_vec());
assert_ne!(a.hash(), b.hash());
}
#[test]
fn test_hash_empty_value() {
let attr = ExtendedAttribute::new("user.empty", Vec::new());
assert_ne!(attr.hash(), 0);
}
#[test]
fn test_hash_value_with_trailing_bytes() {
let attr = ExtendedAttribute::new("user.tail", vec![1, 2, 3, 4, 5]);
let h = attr.hash();
assert_ne!(h, 0);
assert_eq!(h, attr.hash());
}
#[test]
fn test_entry_size_alignment() {
let attr = ExtendedAttribute::new("user.abcd", vec![0]);
assert_eq!(attr.entry_size(), 20);
let attr = ExtendedAttribute::new("user.abcde", vec![0]);
assert_eq!(attr.entry_size(), 24);
let attr = ExtendedAttribute::new("system.posix_acl_access", vec![0]);
assert_eq!(attr.entry_size(), 16);
}
#[test]
fn test_value_size_alignment() {
let attr = ExtendedAttribute::new("user.x", Vec::new());
assert_eq!(attr.value_size(), 0);
let attr = ExtendedAttribute::new("user.x", vec![42]);
assert_eq!(attr.value_size(), 4);
let attr = ExtendedAttribute::new("user.x", vec![0; 4]);
assert_eq!(attr.value_size(), 4);
let attr = ExtendedAttribute::new("user.x", vec![0; 5]);
assert_eq!(attr.value_size(), 8);
}
#[test]
fn test_xattr_state_multiple_inline() {
let mut state = XattrState::new(11, INODE_EXTRA_SIZE, 4096);
state
.add(ExtendedAttribute::new("user.a", vec![1]))
.unwrap();
state
.add(ExtendedAttribute::new("user.b", vec![2]))
.unwrap();
state
.add(ExtendedAttribute::new("user.c", vec![3]))
.unwrap();
assert!(state.has_inline());
assert!(!state.has_block());
let buf = state.write_inline().unwrap();
let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
assert_eq!(magic, XATTR_HEADER_MAGIC);
assert!(buf[4..].iter().any(|&b| b != 0));
}
#[test]
fn test_xattr_state_mixed_inline_and_block() {
let inline_cap = 32;
let mut state = XattrState::new(11, inline_cap, 4096);
state
.add(ExtendedAttribute::new("user.a", vec![1]))
.unwrap();
state
.add(ExtendedAttribute::new("user.b", vec![2]))
.unwrap();
assert!(state.has_inline());
assert!(state.has_block());
}
#[test]
fn test_write_block_sorted_output() {
let mut state = XattrState::new(11, XATTR_INODE_HEADER_SIZE, 4096);
state
.add(ExtendedAttribute::new("user.zzz", vec![3]))
.unwrap();
state
.add(ExtendedAttribute::new("security.aaa", vec![1]))
.unwrap();
state
.add(ExtendedAttribute::new("user.aaa", vec![2]))
.unwrap();
let buf = state.write_block().unwrap();
let first_entry_index = buf[32 + 1]; let second_entry_index = buf[32 + 1 + align_up(16 + 3, 4) as usize]; assert!(
first_entry_index <= second_entry_index,
"entries should be sorted by name_index: {} vs {}",
first_entry_index,
second_entry_index,
);
}
fn align_up(n: usize, align: usize) -> usize {
(n + align - 1) & !(align - 1)
}
}