use crate::Result;
pub const XFS_ATTR3_LEAF_MAGIC: u16 = 0x3bee;
pub const XFS_ATTR_LEAF_MAGIC: u16 = 0xfbee;
pub const XFS_ATTR3_LEAF_HDR_SIZE: usize = 80;
pub const XFS_ATTR_LEAF_HDR_SIZE: usize = 32;
pub const XFS_ATTR_LEAF_ENTRY_SIZE: usize = 8;
pub const XFS_ATTR_LEAF_MAPSIZE: usize = 3;
pub const XFS_ATTR3_LEAF_CRC_OFF: usize = 12;
pub const XFS_ATTR_LOCAL: u8 = 0x01;
pub const XFS_ATTR_ROOT: u8 = 0x02;
pub const XFS_ATTR_SECURE: u8 = 0x04;
pub const XFS_ATTR_INCOMPLETE: u8 = 0x80;
pub fn leaf_name_to_disk(name: &str) -> (String, u8) {
if let Some(rest) = name.strip_prefix("user.") {
(rest.to_string(), 0)
} else if let Some(rest) = name.strip_prefix("trusted.") {
(rest.to_string(), XFS_ATTR_ROOT)
} else if let Some(rest) = name.strip_prefix("security.") {
(rest.to_string(), XFS_ATTR_SECURE)
} else {
(name.to_string(), 0)
}
}
pub fn leaf_name_from_disk(suffix: &str, flags: u8) -> String {
if flags & XFS_ATTR_ROOT != 0 {
format!("trusted.{suffix}")
} else if flags & XFS_ATTR_SECURE != 0 {
format!("security.{suffix}")
} else {
format!("user.{suffix}")
}
}
pub fn dahashname(name: &[u8]) -> u32 {
super::dir::dahashname(name)
}
fn local_record_size(namelen: usize, valuelen: usize) -> usize {
let raw = 2 + 1 + namelen + valuelen;
(raw + 3) & !3
}
fn entries_size(n: usize) -> usize {
n * XFS_ATTR_LEAF_ENTRY_SIZE
}
pub fn min_leaf_block_size(attrs: &[(String, Vec<u8>)]) -> usize {
let mut bytes = XFS_ATTR3_LEAF_HDR_SIZE + entries_size(attrs.len());
for (name, value) in attrs {
let (suffix, _flags) = leaf_name_to_disk(name);
bytes += local_record_size(suffix.len(), value.len());
}
(bytes + 7) & !7
}
pub fn encode_v5_leaf(
attrs: &[(String, Vec<u8>)],
block_size: usize,
owner: u64,
uuid: &[u8; 16],
blkno: u64,
) -> Result<Vec<u8>> {
if block_size < min_leaf_block_size(attrs) {
return Err(crate::Error::InvalidArgument(format!(
"xfs: leaf xattr block size {block_size} too small for {} attrs",
attrs.len()
)));
}
if attrs.len() > u16::MAX as usize {
return Err(crate::Error::InvalidArgument(
"xfs: too many xattrs for one leaf block".into(),
));
}
let mut block = vec![0u8; block_size];
block[8..10].copy_from_slice(&XFS_ATTR3_LEAF_MAGIC.to_be_bytes());
block[16..24].copy_from_slice(&blkno.to_be_bytes());
block[32..48].copy_from_slice(uuid);
block[48..56].copy_from_slice(&owner.to_be_bytes());
let count = attrs.len();
let entries_start = XFS_ATTR3_LEAF_HDR_SIZE;
let entries_end = entries_start + entries_size(count);
let mut records: Vec<(String, u8, u32, Vec<u8>)> = Vec::with_capacity(count);
for (name, value) in attrs {
let (suffix, flags) = leaf_name_to_disk(name);
let hash = dahashname(suffix.as_bytes());
if suffix.len() > u8::MAX as usize {
return Err(crate::Error::InvalidArgument(format!(
"xfs: leaf xattr name {name:?} suffix > 255 bytes"
)));
}
if value.len() > u16::MAX as usize {
return Err(crate::Error::InvalidArgument(format!(
"xfs: leaf xattr value for {name:?} > 65535 bytes"
)));
}
records.push((suffix, flags, hash, value.clone()));
}
records.sort_by(|a, b| a.2.cmp(&b.2).then_with(|| a.0.cmp(&b.0)));
let mut next_end = block_size;
let mut nameidxs: Vec<u16> = Vec::with_capacity(count);
let mut usedbytes_total: u16 = 0;
for (suffix, _flags, _hash, value) in &records {
let rsize = local_record_size(suffix.len(), value.len());
if next_end < entries_end + rsize {
return Err(crate::Error::InvalidArgument(
"xfs: leaf xattr block too small (entries+records collide)".into(),
));
}
next_end -= rsize;
let off = next_end;
block[off..off + 2].copy_from_slice(&(value.len() as u16).to_be_bytes());
block[off + 2] = suffix.len() as u8;
let name_off = off + 3;
block[name_off..name_off + suffix.len()].copy_from_slice(suffix.as_bytes());
let val_off = name_off + suffix.len();
block[val_off..val_off + value.len()].copy_from_slice(value);
nameidxs.push(off as u16);
usedbytes_total = usedbytes_total
.checked_add(rsize as u16)
.ok_or_else(|| crate::Error::InvalidArgument("xfs: leaf usedbytes overflow".into()))?;
}
for (i, (_suffix, flags, hash, _value)) in records.iter().enumerate() {
let e_off = entries_start + i * XFS_ATTR_LEAF_ENTRY_SIZE;
block[e_off..e_off + 4].copy_from_slice(&hash.to_be_bytes());
block[e_off + 4..e_off + 6].copy_from_slice(&nameidxs[i].to_be_bytes());
block[e_off + 6] = XFS_ATTR_LOCAL | *flags;
}
block[56..58].copy_from_slice(&(count as u16).to_be_bytes());
block[58..60].copy_from_slice(&usedbytes_total.to_be_bytes());
block[60..62].copy_from_slice(&(next_end as u16).to_be_bytes());
let free_base = entries_end as u16;
let free_size = (next_end - entries_end) as u16;
block[64..66].copy_from_slice(&free_base.to_be_bytes());
block[66..68].copy_from_slice(&free_size.to_be_bytes());
stamp_v5_leaf_crc(&mut block);
Ok(block)
}
pub fn stamp_v5_leaf_crc(block: &mut [u8]) {
block[XFS_ATTR3_LEAF_CRC_OFF..XFS_ATTR3_LEAF_CRC_OFF + 4].copy_from_slice(&[0u8; 4]);
let crc = crc32c::crc32c(block);
block[XFS_ATTR3_LEAF_CRC_OFF..XFS_ATTR3_LEAF_CRC_OFF + 4].copy_from_slice(&crc.to_le_bytes());
}
pub fn decode_leaf(block: &[u8]) -> Result<std::collections::HashMap<String, Vec<u8>>> {
if block.len() < 12 {
return Err(crate::Error::InvalidImage(
"xfs: attr leaf block too small for blkinfo".into(),
));
}
let magic = u16::from_be_bytes(block[8..10].try_into().unwrap());
let (hdr_size, is_v5) = match magic {
XFS_ATTR3_LEAF_MAGIC => (XFS_ATTR3_LEAF_HDR_SIZE, true),
XFS_ATTR_LEAF_MAGIC => (XFS_ATTR_LEAF_HDR_SIZE, false),
other => {
return Err(crate::Error::InvalidImage(format!(
"xfs: bad attr-leaf magic {other:#06x}"
)));
}
};
if block.len() < hdr_size {
return Err(crate::Error::InvalidImage(
"xfs: attr leaf block shorter than its header".into(),
));
}
let (count_off, _usedbytes_off, _firstused_off) = if is_v5 {
(56usize, 58usize, 60usize)
} else {
(12usize, 14usize, 16usize)
};
let count = u16::from_be_bytes(block[count_off..count_off + 2].try_into().unwrap()) as usize;
let entries_start = hdr_size;
let entries_end = entries_start + count * XFS_ATTR_LEAF_ENTRY_SIZE;
if entries_end > block.len() {
return Err(crate::Error::InvalidImage(
"xfs: attr leaf entries[] runs past end of block".into(),
));
}
let mut out = std::collections::HashMap::with_capacity(count);
for i in 0..count {
let e_off = entries_start + i * XFS_ATTR_LEAF_ENTRY_SIZE;
let nameidx = u16::from_be_bytes(block[e_off + 4..e_off + 6].try_into().unwrap()) as usize;
let flags = block[e_off + 6];
if flags & XFS_ATTR_INCOMPLETE != 0 {
continue;
}
if flags & XFS_ATTR_LOCAL == 0 {
return Err(crate::Error::Unsupported(
"xfs: remote-value xattrs in leaf form not supported".into(),
));
}
if nameidx + 3 > block.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: attr leaf entry {i} nameidx {nameidx} past block end"
)));
}
let valuelen = u16::from_be_bytes(block[nameidx..nameidx + 2].try_into().unwrap()) as usize;
let namelen = block[nameidx + 2] as usize;
let name_start = nameidx + 3;
let name_end = name_start + namelen;
let val_end = name_end + valuelen;
if val_end > block.len() {
return Err(crate::Error::InvalidImage(format!(
"xfs: attr leaf entry {i} name/value runs past block end"
)));
}
let suffix = std::str::from_utf8(&block[name_start..name_end])
.map_err(|_| crate::Error::InvalidImage("xfs: non-UTF-8 leaf xattr name".into()))?;
let ns_flags = flags & (XFS_ATTR_ROOT | XFS_ATTR_SECURE);
let full_name = leaf_name_from_disk(suffix, ns_flags);
let value = block[name_end..val_end].to_vec();
out.insert(full_name, value);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_two_attrs() {
let attrs = vec![
("user.mime_type".to_string(), b"text/plain".to_vec()),
("trusted.foo".to_string(), b"bar".to_vec()),
];
let uuid = [0xAB; 16];
let block = encode_v5_leaf(&attrs, 4096, 128, &uuid, 8).unwrap();
let decoded = decode_leaf(&block).unwrap();
assert_eq!(decoded.len(), 2);
assert_eq!(decoded.get("user.mime_type"), Some(&b"text/plain".to_vec()));
assert_eq!(decoded.get("trusted.foo"), Some(&b"bar".to_vec()));
}
#[test]
fn round_trip_many_small_attrs() {
let mut attrs = Vec::new();
for i in 0..16 {
attrs.push((format!("user.k{i}"), format!("v{i}").into_bytes()));
}
let block = encode_v5_leaf(&attrs, 4096, 200, &[0u8; 16], 16).unwrap();
let decoded = decode_leaf(&block).unwrap();
assert_eq!(decoded.len(), 16);
for i in 0..16 {
assert_eq!(
decoded.get(&format!("user.k{i}")),
Some(&format!("v{i}").into_bytes())
);
}
}
#[test]
fn round_trip_empty_value() {
let attrs = vec![("user.flag".to_string(), Vec::new())];
let block = encode_v5_leaf(&attrs, 4096, 1, &[0; 16], 1).unwrap();
let decoded = decode_leaf(&block).unwrap();
assert_eq!(decoded.get("user.flag"), Some(&Vec::new()));
}
#[test]
fn round_trip_all_three_namespaces() {
let attrs = vec![
("user.a".to_string(), b"u".to_vec()),
("trusted.b".to_string(), b"t".to_vec()),
("security.c".to_string(), b"s".to_vec()),
];
let block = encode_v5_leaf(&attrs, 4096, 1, &[0; 16], 1).unwrap();
let decoded = decode_leaf(&block).unwrap();
assert_eq!(decoded.get("user.a"), Some(&b"u".to_vec()));
assert_eq!(decoded.get("trusted.b"), Some(&b"t".to_vec()));
assert_eq!(decoded.get("security.c"), Some(&b"s".to_vec()));
}
#[test]
fn reject_oversize_block() {
let attrs = vec![("user.k".to_string(), vec![0u8; 8000])];
assert!(encode_v5_leaf(&attrs, 256, 1, &[0; 16], 1).is_err());
}
#[test]
fn rejects_bad_magic() {
let mut block = vec![0u8; 4096];
block[8..10].copy_from_slice(&0xdeadu16.to_be_bytes());
let r = decode_leaf(&block);
assert!(matches!(r, Err(crate::Error::InvalidImage(_))));
}
#[test]
fn skips_incomplete_entries() {
let attrs = vec![
("user.good".to_string(), b"g".to_vec()),
("user.bad".to_string(), b"b".to_vec()),
];
let mut block = encode_v5_leaf(&attrs, 4096, 1, &[0; 16], 1).unwrap();
let count = u16::from_be_bytes(block[56..58].try_into().unwrap()) as usize;
for i in 0..count {
let e_off = XFS_ATTR3_LEAF_HDR_SIZE + i * XFS_ATTR_LEAF_ENTRY_SIZE;
let nameidx =
u16::from_be_bytes(block[e_off + 4..e_off + 6].try_into().unwrap()) as usize;
let namelen = block[nameidx + 2] as usize;
let name = &block[nameidx + 3..nameidx + 3 + namelen];
if name == b"bad" {
block[e_off + 6] |= XFS_ATTR_INCOMPLETE;
}
}
let decoded = decode_leaf(&block).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded.get("user.good"), Some(&b"g".to_vec()));
assert!(!decoded.contains_key("user.bad"));
}
#[test]
fn hashval_ascending_in_entries() {
let attrs = vec![
("user.zzz".to_string(), b"1".to_vec()),
("user.aaa".to_string(), b"2".to_vec()),
("user.mmm".to_string(), b"3".to_vec()),
];
let block = encode_v5_leaf(&attrs, 4096, 1, &[0; 16], 1).unwrap();
let count = u16::from_be_bytes(block[56..58].try_into().unwrap()) as usize;
let mut prev = 0u32;
for i in 0..count {
let e_off = XFS_ATTR3_LEAF_HDR_SIZE + i * XFS_ATTR_LEAF_ENTRY_SIZE;
let h = u32::from_be_bytes(block[e_off..e_off + 4].try_into().unwrap());
assert!(h >= prev, "hashvals must be non-decreasing in entries[]");
prev = h;
}
}
}