use crate::block_io::BlockDevice;
use crate::error::{Error, Result};
use crate::inode::Inode;
pub const EXT4_XATTR_MAGIC: u32 = 0xEA02_0000;
pub const NAME_PREFIXES: &[(u8, &str)] = &[
(1, "user."),
(2, "system.posix_acl_access"),
(3, "system.posix_acl_default"),
(4, "trusted."),
(5, "lustre."),
(6, "security."),
(7, "system."),
(8, "system.richacl"),
];
pub fn prefix_for_index(idx: u8) -> Option<&'static str> {
NAME_PREFIXES
.iter()
.find(|(i, _)| *i == idx)
.map(|(_, s)| *s)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XattrEntry {
pub name: String,
pub value: Vec<u8>,
}
pub fn read_all(
dev: &dyn BlockDevice,
inode: &Inode,
inode_raw: &[u8],
inode_size: u16,
block_size: u32,
) -> Result<Vec<XattrEntry>> {
let mut out = Vec::new();
if inode_raw.len() >= 128 + 4 {
let extra_isize = u16::from_le_bytes(inode_raw[128..130].try_into().unwrap()) as usize;
let xattr_region_start = 128 + extra_isize;
if xattr_region_start + 4 <= inode_size as usize
&& xattr_region_start + 4 <= inode_raw.len()
{
let region = &inode_raw[xattr_region_start..(inode_size as usize).min(inode_raw.len())];
let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
if magic == EXT4_XATTR_MAGIC {
parse_entries(®ion[4..], region.len() - 4, &mut out)?;
}
}
}
if inode.file_acl != 0 {
let mut buf = vec![0u8; block_size as usize];
dev.read_at(inode.file_acl * block_size as u64, &mut buf)?;
let magic = u32::from_le_bytes(buf[..4].try_into().unwrap());
if magic != EXT4_XATTR_MAGIC {
return Err(Error::Corrupt("xattr block magic mismatch"));
}
parse_entries_block(&buf, &mut out)?;
}
Ok(out)
}
fn parse_entries(entries_buf: &[u8], _region_len: usize, out: &mut Vec<XattrEntry>) -> Result<()> {
let mut pos = 0;
while pos + 16 <= entries_buf.len() {
let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = entries_buf[pos] as usize;
let name_index = entries_buf[pos + 1];
let value_offs =
u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
let _value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
let value_size =
u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
let entry_size = 16 + name_len;
let entry_padded = (entry_size + 3) & !3;
if pos + 16 + name_len > entries_buf.len() {
return Err(Error::Corrupt("xattr entry name overruns region"));
}
let name_bytes = &entries_buf[pos + 16..pos + 16 + name_len];
let prefix = prefix_for_index(name_index).unwrap_or("");
let suffix =
std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
let full_name = format!("{prefix}{suffix}");
let value = if value_size > 0 {
if value_offs + value_size > entries_buf.len() {
return Err(Error::Corrupt("xattr value out of range"));
}
entries_buf[value_offs..value_offs + value_size].to_vec()
} else {
Vec::new()
};
out.push(XattrEntry {
name: full_name,
value,
});
pos += entry_padded;
}
Ok(())
}
fn parse_entries_block(block: &[u8], out: &mut Vec<XattrEntry>) -> Result<()> {
if block.len() < 32 {
return Err(Error::Corrupt("xattr block too small"));
}
let mut pos = 32; while pos + 16 <= block.len() {
let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = block[pos] as usize;
let name_index = block[pos + 1];
let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
let _value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
let entry_size = 16 + name_len;
let entry_padded = (entry_size + 3) & !3;
if pos + 16 + name_len > block.len() {
return Err(Error::Corrupt("xattr entry name overruns block"));
}
let name_bytes = &block[pos + 16..pos + 16 + name_len];
let prefix = prefix_for_index(name_index).unwrap_or("");
let suffix =
std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
let full_name = format!("{prefix}{suffix}");
let value = if value_size > 0 {
if value_offs + value_size > block.len() {
return Err(Error::Corrupt("xattr block value out of range"));
}
block[value_offs..value_offs + value_size].to_vec()
} else {
Vec::new()
};
out.push(XattrEntry {
name: full_name,
value,
});
pos += entry_padded;
}
Ok(())
}
pub fn split_qualified_name(name: &str) -> Option<(u8, &str)> {
for (idx, prefix) in NAME_PREFIXES {
if let Some(rest) = name.strip_prefix(*prefix) {
return Some((*idx, rest));
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoveOutcome {
Removed,
NotFound,
}
pub fn plan_remove_in_inode_region(region: &mut [u8], name: &str) -> Result<RemoveOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if region.len() < 4 {
return Ok(RemoveOutcome::NotFound);
}
let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
if magic != EXT4_XATTR_MAGIC {
return Ok(RemoveOutcome::NotFound);
}
let entries = decode_in_inode_entries(®ion[4..])?;
let before = entries.len();
let kept: Vec<DecodedEntry> = entries
.into_iter()
.filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
.collect();
if kept.len() == before {
return Ok(RemoveOutcome::NotFound);
}
encode_in_inode_entries(region, &kept);
Ok(RemoveOutcome::Removed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetOutcome {
Inserted,
Replaced,
}
pub fn plan_set_in_inode_region(region: &mut [u8], name: &str, value: &[u8]) -> Result<SetOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if suffix.is_empty() && !matches!(name_index, 2 | 3) {
return Err(Error::InvalidArgument("xattr name suffix is empty"));
}
if suffix.len() > 255 {
return Err(Error::NameTooLong);
}
if region.len() < 8 {
return Err(Error::NoSpaceLeftOnDevice);
}
let magic_present = {
let m = u32::from_le_bytes(region[..4].try_into().unwrap());
m == EXT4_XATTR_MAGIC
};
let mut entries = if magic_present {
decode_in_inode_entries(®ion[4..])?
} else {
Vec::new()
};
let mut outcome = SetOutcome::Inserted;
let suffix_bytes = suffix.as_bytes();
for e in entries.iter_mut() {
if e.name_index == name_index && e.name_bytes == suffix_bytes {
e.value = value.to_vec();
outcome = SetOutcome::Replaced;
break;
}
}
if matches!(outcome, SetOutcome::Inserted) {
entries.push(DecodedEntry {
name_index,
name_bytes: suffix_bytes.to_vec(),
value: value.to_vec(),
});
}
let area_len = region.len() - 4;
let needed_entries: usize = entries
.iter()
.map(|e| (16 + e.name_bytes.len() + 3) & !3)
.sum();
let needed_values: usize = entries
.iter()
.filter(|e| !e.value.is_empty())
.map(|e| (e.value.len() + 3) & !3)
.sum();
if needed_entries + 4 + needed_values > area_len {
return Err(Error::NoSpaceLeftOnDevice);
}
encode_in_inode_entries(region, &entries);
Ok(outcome)
}
#[derive(Debug, Clone)]
struct DecodedEntry {
name_index: u8,
name_bytes: Vec<u8>,
value: Vec<u8>,
}
fn decode_in_inode_entries(entries_buf: &[u8]) -> Result<Vec<DecodedEntry>> {
let mut out = Vec::new();
let mut pos = 0usize;
while pos + 16 <= entries_buf.len() {
let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = entries_buf[pos] as usize;
let name_index = entries_buf[pos + 1];
let value_offs =
u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
let value_size =
u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
if pos + 16 + name_len > entries_buf.len() {
return Err(Error::Corrupt("xattr entry name overruns region"));
}
let name_bytes = entries_buf[pos + 16..pos + 16 + name_len].to_vec();
let value = if value_size == 0 {
Vec::new()
} else {
if value_offs + value_size > entries_buf.len() {
return Err(Error::Corrupt("xattr value out of range"));
}
entries_buf[value_offs..value_offs + value_size].to_vec()
};
out.push(DecodedEntry {
name_index,
name_bytes,
value,
});
pos += (16 + name_len + 3) & !3;
}
Ok(out)
}
fn encode_in_inode_entries(region: &mut [u8], entries: &[DecodedEntry]) {
for b in region.iter_mut() {
*b = 0;
}
region[..4].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
let entries_area = &mut region[4..];
let area_len = entries_area.len();
let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
sorted.sort_by(|a, b| {
a.name_index
.cmp(&b.name_index)
.then_with(|| a.name_bytes.cmp(&b.name_bytes))
});
let mut entry_cursor: usize = 0;
let mut value_cursor: usize = area_len;
for e in &sorted {
let name_len = e.name_bytes.len();
let entry_padded = (16 + name_len + 3) & !3;
let value_offs = if e.value.is_empty() {
0
} else {
let value_padded = (e.value.len() + 3) & !3;
value_cursor -= value_padded;
entries_area[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
value_cursor
};
entries_area[entry_cursor] = name_len as u8;
entries_area[entry_cursor + 1] = e.name_index;
entries_area[entry_cursor + 2..entry_cursor + 4]
.copy_from_slice(&(value_offs as u16).to_le_bytes());
entries_area[entry_cursor + 8..entry_cursor + 12]
.copy_from_slice(&(e.value.len() as u32).to_le_bytes());
entries_area[entry_cursor + 16..entry_cursor + 16 + name_len]
.copy_from_slice(&e.name_bytes);
entry_cursor += entry_padded;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockRemoveOutcome {
Removed,
RemovedNowEmpty,
NotFound,
}
fn decode_external_block_entries(block: &[u8]) -> Result<Vec<DecodedEntry>> {
if block.len() < 32 {
return Err(Error::Corrupt("xattr block too small"));
}
let mut out = Vec::new();
let mut pos = 32usize;
while pos + 16 <= block.len() {
let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = block[pos] as usize;
let name_index = block[pos + 1];
let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
if pos + 16 + name_len > block.len() {
return Err(Error::Corrupt("xattr entry name overruns block"));
}
let name_bytes = block[pos + 16..pos + 16 + name_len].to_vec();
let value = if value_size == 0 {
Vec::new()
} else {
if value_offs + value_size > block.len() {
return Err(Error::Corrupt("xattr block value out of range"));
}
block[value_offs..value_offs + value_size].to_vec()
};
out.push(DecodedEntry {
name_index,
name_bytes,
value,
});
pos += (16 + name_len + 3) & !3;
}
Ok(out)
}
fn encode_external_block(block: &mut [u8], entries: &[DecodedEntry], refcount: u32) {
for b in block.iter_mut() {
*b = 0;
}
block[0x00..0x04].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
block[0x04..0x08].copy_from_slice(&refcount.to_le_bytes());
block[0x08..0x0C].copy_from_slice(&1u32.to_le_bytes());
let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
sorted.sort_by(|a, b| {
a.name_index
.cmp(&b.name_index)
.then_with(|| a.name_bytes.cmp(&b.name_bytes))
});
let block_len = block.len();
let mut entry_cursor: usize = 0x20;
let mut value_cursor: usize = block_len;
for e in &sorted {
let name_len = e.name_bytes.len();
let entry_padded = (16 + name_len + 3) & !3;
let value_offs = if e.value.is_empty() {
0
} else {
let value_padded = (e.value.len() + 3) & !3;
value_cursor -= value_padded;
block[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
value_cursor
};
block[entry_cursor] = name_len as u8;
block[entry_cursor + 1] = e.name_index;
block[entry_cursor + 2..entry_cursor + 4]
.copy_from_slice(&(value_offs as u16).to_le_bytes());
block[entry_cursor + 8..entry_cursor + 12]
.copy_from_slice(&(e.value.len() as u32).to_le_bytes());
block[entry_cursor + 16..entry_cursor + 16 + name_len].copy_from_slice(&e.name_bytes);
entry_cursor += entry_padded;
}
}
pub fn plan_set_in_external_block(
block: &mut [u8],
name: &str,
value: &[u8],
refcount: u32,
) -> Result<SetOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if suffix.is_empty() && !matches!(name_index, 2 | 3) {
return Err(Error::InvalidArgument("xattr name suffix is empty"));
}
if suffix.len() > 255 {
return Err(Error::NameTooLong);
}
if block.len() < 0x40 {
return Err(Error::NoSpaceLeftOnDevice);
}
let magic_present = u32::from_le_bytes(block[..4].try_into().unwrap()) == EXT4_XATTR_MAGIC;
let mut entries = if magic_present {
decode_external_block_entries(block)?
} else {
Vec::new()
};
let mut outcome = SetOutcome::Inserted;
let suffix_bytes = suffix.as_bytes();
for e in entries.iter_mut() {
if e.name_index == name_index && e.name_bytes == suffix_bytes {
e.value = value.to_vec();
outcome = SetOutcome::Replaced;
break;
}
}
if matches!(outcome, SetOutcome::Inserted) {
entries.push(DecodedEntry {
name_index,
name_bytes: suffix_bytes.to_vec(),
value: value.to_vec(),
});
}
let entries_capacity = block.len() - 0x20;
let needed_entries: usize = entries
.iter()
.map(|e| (16 + e.name_bytes.len() + 3) & !3)
.sum();
let needed_values: usize = entries
.iter()
.filter(|e| !e.value.is_empty())
.map(|e| (e.value.len() + 3) & !3)
.sum();
if needed_entries + 4 + needed_values > entries_capacity {
return Err(Error::NoSpaceLeftOnDevice);
}
encode_external_block(block, &entries, refcount);
Ok(outcome)
}
pub fn plan_remove_from_external_block(
block: &mut [u8],
name: &str,
refcount: u32,
) -> Result<BlockRemoveOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if block.len() < 0x20 {
return Ok(BlockRemoveOutcome::NotFound);
}
let magic = u32::from_le_bytes(block[..4].try_into().unwrap());
if magic != EXT4_XATTR_MAGIC {
return Ok(BlockRemoveOutcome::NotFound);
}
let entries = decode_external_block_entries(block)?;
let before = entries.len();
let kept: Vec<DecodedEntry> = entries
.into_iter()
.filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
.collect();
if kept.len() == before {
return Ok(BlockRemoveOutcome::NotFound);
}
if kept.is_empty() {
return Ok(BlockRemoveOutcome::RemovedNowEmpty);
}
encode_external_block(block, &kept, refcount);
Ok(BlockRemoveOutcome::Removed)
}
pub fn get(
dev: &dyn BlockDevice,
inode: &Inode,
inode_raw: &[u8],
inode_size: u16,
block_size: u32,
name: &str,
) -> Result<Option<Vec<u8>>> {
let all = read_all(dev, inode, inode_raw, inode_size, block_size)?;
Ok(all.into_iter().find(|e| e.name == name).map(|e| e.value))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefixes_known() {
assert_eq!(prefix_for_index(1), Some("user."));
assert_eq!(prefix_for_index(7), Some("system."));
assert_eq!(prefix_for_index(99), None);
}
#[test]
fn split_qualified_name_roundtrip() {
assert_eq!(split_qualified_name("user.color"), Some((1, "color")));
assert_eq!(
split_qualified_name("user.com.apple.FinderInfo"),
Some((1, "com.apple.FinderInfo"))
);
assert_eq!(
split_qualified_name("security.selinux"),
Some((6, "selinux"))
);
assert_eq!(split_qualified_name("unknown.foo"), None);
}
#[test]
fn remove_in_inode_roundtrips_one_of_two() {
let mut region = vec![0u8; 96];
let entries = vec![
DecodedEntry {
name_index: 1,
name_bytes: b"color".to_vec(),
value: b"red".to_vec(),
},
DecodedEntry {
name_index: 1,
name_bytes: b"mood".to_vec(),
value: b"happy".to_vec(),
},
];
encode_in_inode_entries(&mut region, &entries);
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(decoded.len(), 2);
let outcome = plan_remove_in_inode_region(&mut region, "user.color").unwrap();
assert_eq!(outcome, RemoveOutcome::Removed);
let after = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(after.len(), 1);
assert_eq!(after[0].name_bytes, b"mood");
assert_eq!(after[0].value, b"happy");
}
#[test]
fn remove_in_inode_returns_not_found_for_missing() {
let mut region = vec![0u8; 64];
let entries = vec![DecodedEntry {
name_index: 1,
name_bytes: b"color".to_vec(),
value: b"red".to_vec(),
}];
encode_in_inode_entries(&mut region, &entries);
let outcome = plan_remove_in_inode_region(&mut region, "user.mood").unwrap();
assert_eq!(outcome, RemoveOutcome::NotFound);
}
#[test]
fn remove_in_inode_unknown_prefix_is_einval() {
let mut region = vec![0u8; 64];
let err = plan_remove_in_inode_region(&mut region, "nope.name").unwrap_err();
assert!(matches!(err, Error::InvalidArgument(_)));
}
#[test]
fn remove_in_inode_missing_magic_is_not_found() {
let mut region = vec![0u8; 64];
let outcome = plan_remove_in_inode_region(&mut region, "user.x").unwrap();
assert_eq!(outcome, RemoveOutcome::NotFound);
}
#[test]
fn set_in_inode_inserts_new_into_empty_region() {
let mut region = vec![0u8; 64];
let outcome = plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
assert_eq!(outcome, SetOutcome::Inserted);
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].name_bytes, b"color");
assert_eq!(decoded[0].value, b"red");
}
#[test]
fn set_in_inode_replaces_existing_value() {
let mut region = vec![0u8; 96];
plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
let outcome = plan_set_in_inode_region(&mut region, "user.color", b"emerald").unwrap();
assert_eq!(outcome, SetOutcome::Replaced);
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].value, b"emerald");
}
#[test]
fn set_in_inode_preserves_other_entries() {
let mut region = vec![0u8; 128];
plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
plan_set_in_inode_region(&mut region, "user.mood", b"happy").unwrap();
plan_set_in_inode_region(&mut region, "user.color", b"blue").unwrap();
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
let by_name: std::collections::BTreeMap<_, _> = decoded
.into_iter()
.map(|e| (e.name_bytes.clone(), e.value))
.collect();
assert_eq!(by_name.get(b"color".as_slice()).unwrap(), b"blue");
assert_eq!(by_name.get(b"mood".as_slice()).unwrap(), b"happy");
}
#[test]
fn set_in_inode_enospc_on_overflow() {
let mut region = vec![0u8; 32];
let err =
plan_set_in_inode_region(&mut region, "user.x", b"this_is_20_bytes_xx!").unwrap_err();
assert!(matches!(err, Error::NoSpaceLeftOnDevice));
}
#[test]
fn set_in_inode_unknown_prefix_is_einval() {
let mut region = vec![0u8; 64];
let err = plan_set_in_inode_region(&mut region, "weird.key", b"v").unwrap_err();
assert!(matches!(err, Error::InvalidArgument(_)));
}
}