use crate::Result;
use crate::block::BlockDevice;
use crate::fs::squashfs::Compression;
use crate::fs::squashfs::metablock::{encode_metablock, read_metablock};
pub const XATTR_TYPE_USER: u16 = 0;
pub const XATTR_TYPE_TRUSTED: u16 = 1;
pub const XATTR_TYPE_SECURITY: u16 = 2;
const XATTR_TYPE_MASK: u16 = 0xFF;
const XATTR_FLAG_OOL: u16 = 0x0100;
#[derive(Debug, Clone)]
pub struct Xattr {
pub key: String,
pub value: Vec<u8>,
}
#[derive(Debug, Default)]
pub struct XattrReader {
loaded: bool,
kv_start: u64,
lookup: Vec<XattrId>,
}
#[derive(Debug, Clone, Copy)]
struct XattrId {
xattr_ref: u64,
count: u32,
_size: u32,
}
impl XattrReader {
pub fn new() -> Self {
Self::default()
}
pub fn ensure_loaded(
&mut self,
dev: &mut dyn BlockDevice,
xattr_table_start: u64,
compression: Compression,
) -> Result<()> {
if self.loaded {
return Ok(());
}
self.loaded = true;
if xattr_table_start == u64::MAX {
return Ok(());
}
let mut head = [0u8; 16];
dev.read_at(xattr_table_start, &mut head)?;
let kv_start = u64::from_le_bytes(head[0..8].try_into().unwrap());
let count = u32::from_le_bytes(head[8..12].try_into().unwrap());
if count == 0 {
self.kv_start = kv_start;
return Ok(());
}
let total_bytes = count as usize * 16;
let metablock_count = total_bytes.div_ceil(8192);
let mut locs = vec![0u8; metablock_count * 8];
dev.read_at(xattr_table_start + 16, &mut locs)?;
let mut entries = Vec::with_capacity(count as usize);
let mut remaining = total_bytes;
for i in 0..metablock_count {
let off = i * 8;
let mb_disk = u64::from_le_bytes(locs[off..off + 8].try_into().unwrap());
let mb = read_metablock(dev, mb_disk, compression)?;
let want = remaining.min(8192);
if mb.data.len() < want {
return Err(crate::Error::InvalidImage(format!(
"squashfs: xattr lookup metablock {i} too short"
)));
}
for j in 0..(want / 16) {
let p = j * 16;
entries.push(XattrId {
xattr_ref: u64::from_le_bytes(mb.data[p..p + 8].try_into().unwrap()),
count: u32::from_le_bytes(mb.data[p + 8..p + 12].try_into().unwrap()),
_size: u32::from_le_bytes(mb.data[p + 12..p + 16].try_into().unwrap()),
});
}
remaining -= want;
}
self.kv_start = kv_start;
self.lookup = entries;
Ok(())
}
pub fn fetch(
&self,
dev: &mut dyn BlockDevice,
idx: u32,
compression: Compression,
) -> Result<Vec<Xattr>> {
if idx == u32::MAX || idx as usize >= self.lookup.len() {
return Ok(Vec::new());
}
let entry = self.lookup[idx as usize];
let meta_block_rel = entry.xattr_ref >> 16;
let in_block_offset = (entry.xattr_ref & 0xFFFF) as usize;
let mut out = Vec::with_capacity(entry.count as usize);
let mut mb_rel = meta_block_rel;
let mut offset = in_block_offset;
for _ in 0..entry.count {
let (kv, nb, no) = read_kv_record(dev, self.kv_start, mb_rel, offset, compression)?;
mb_rel = nb;
offset = no;
out.push(kv);
}
Ok(out)
}
}
fn read_kv_record(
dev: &mut dyn BlockDevice,
kv_start: u64,
mut mb_rel: u64,
mut offset: usize,
compression: Compression,
) -> Result<(Xattr, u64, usize)> {
use crate::fs::squashfs::metablock::MetadataReader;
let mut mr = MetadataReader::new(kv_start, compression);
let (head, nb, no) = mr.read(dev, mb_rel, offset, 4)?;
mb_rel = nb;
offset = no;
let raw_type = u16::from_le_bytes(head[0..2].try_into().unwrap());
let name_size = u16::from_le_bytes(head[2..4].try_into().unwrap()) as usize;
let (name_bytes, nb, no) = mr.read(dev, mb_rel, offset, name_size)?;
mb_rel = nb;
offset = no;
let key_prefix = prefix_for_type(raw_type & XATTR_TYPE_MASK)?;
let name_str = std::str::from_utf8(&name_bytes)
.map_err(|e| crate::Error::InvalidImage(format!("squashfs: xattr name not utf-8: {e}")))?;
let key = format!("{}{}", key_prefix, name_str);
let (vh, nb, no) = mr.read(dev, mb_rel, offset, 4)?;
mb_rel = nb;
offset = no;
let v_size = u32::from_le_bytes(vh[0..4].try_into().unwrap()) as usize;
let (mut v_bytes, nb, no) = mr.read(dev, mb_rel, offset, v_size)?;
mb_rel = nb;
offset = no;
if raw_type & XATTR_FLAG_OOL != 0 && v_size == 8 {
let oref = u64::from_le_bytes(v_bytes.as_slice().try_into().unwrap());
let ref_block = oref >> 16;
let ref_offset = (oref & 0xFFFF) as usize;
let mut mr2 = MetadataReader::new(kv_start, compression);
let (vh2, nb2, no2) = mr2.read(dev, ref_block, ref_offset, 4)?;
let real_size = u32::from_le_bytes(vh2[0..4].try_into().unwrap()) as usize;
let (real_bytes, _, _) = mr2.read(dev, nb2, no2, real_size)?;
v_bytes = real_bytes;
}
Ok((
Xattr {
key,
value: v_bytes,
},
mb_rel,
offset,
))
}
fn prefix_for_type(t: u16) -> Result<&'static str> {
match t {
XATTR_TYPE_USER => Ok("user."),
XATTR_TYPE_TRUSTED => Ok("trusted."),
XATTR_TYPE_SECURITY => Ok("security."),
other => Err(crate::Error::InvalidImage(format!(
"squashfs: unknown xattr type {other}"
))),
}
}
fn type_for_prefix(key: &str) -> Option<(u16, &str)> {
if let Some(rest) = key.strip_prefix("user.") {
Some((XATTR_TYPE_USER, rest))
} else if let Some(rest) = key.strip_prefix("trusted.") {
Some((XATTR_TYPE_TRUSTED, rest))
} else if let Some(rest) = key.strip_prefix("security.") {
Some((XATTR_TYPE_SECURITY, rest))
} else {
None
}
}
pub type XattrSet = Vec<Xattr>;
pub fn encode_xattr_table(
sets: &[XattrSet],
base: u64,
compression: Compression,
) -> Result<(Vec<u8>, u64)> {
let mut kv_raw = Vec::new();
let mut per_set: Vec<(u32, u32)> = Vec::with_capacity(sets.len()); for set in sets {
let off = kv_raw.len() as u32;
per_set.push((off, set.len() as u32));
for kv in set {
let (ty, rest) = type_for_prefix(&kv.key).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"squashfs: xattr key {:?} has unknown namespace (expected user./trusted./security.)",
kv.key
))
})?;
kv_raw.extend_from_slice(&ty.to_le_bytes());
kv_raw.extend_from_slice(&(rest.len() as u16).to_le_bytes());
kv_raw.extend_from_slice(rest.as_bytes());
kv_raw.extend_from_slice(&(kv.value.len() as u32).to_le_bytes());
kv_raw.extend_from_slice(&kv.value);
}
}
let mut out = Vec::new();
let mut kv_block_offsets_abs: Vec<u64> = Vec::new();
let mut kv_block_disk_sizes: Vec<u32> = Vec::new();
{
let mut pos = 0usize;
while pos < kv_raw.len() {
let end = (pos + 8192).min(kv_raw.len());
let mb = encode_metablock(&kv_raw[pos..end], compression)?;
kv_block_offsets_abs.push(base + out.len() as u64);
kv_block_disk_sizes.push(mb.len() as u32);
out.extend_from_slice(&mb);
pos = end;
}
}
let kv_start_abs = if kv_block_offsets_abs.is_empty() {
base + out.len() as u64
} else {
kv_block_offsets_abs[0]
};
let xattr_ref_from_uncompressed = |u_off: u32| -> u64 {
let mb_idx = (u_off as usize) / 8192;
let in_off = (u_off as usize) % 8192;
let rel: u64 = kv_block_disk_sizes[..mb_idx]
.iter()
.map(|&n| n as u64)
.sum();
(rel << 16) | (in_off as u64)
};
let mut lookup_raw = Vec::with_capacity(per_set.len() * 16);
for (i, &(u_off, count)) in per_set.iter().enumerate() {
let next = if i + 1 < per_set.len() {
per_set[i + 1].0
} else {
kv_raw.len() as u32
};
let size = next - u_off;
let xref = if kv_block_offsets_abs.is_empty() {
0
} else {
xattr_ref_from_uncompressed(u_off)
};
lookup_raw.extend_from_slice(&xref.to_le_bytes());
lookup_raw.extend_from_slice(&count.to_le_bytes());
lookup_raw.extend_from_slice(&size.to_le_bytes());
}
let mut lookup_block_offsets_abs: Vec<u64> = Vec::new();
{
let mut pos = 0usize;
while pos < lookup_raw.len() {
let end = (pos + 8192).min(lookup_raw.len());
let mb = encode_metablock(&lookup_raw[pos..end], compression)?;
lookup_block_offsets_abs.push(base + out.len() as u64);
out.extend_from_slice(&mb);
pos = end;
}
}
let header_offset = out.len() as u64;
out.extend_from_slice(&kv_start_abs.to_le_bytes());
out.extend_from_slice(&(per_set.len() as u32).to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); for l in &lookup_block_offsets_abs {
out.extend_from_slice(&l.to_le_bytes());
}
Ok((out, header_offset))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn round_trip_simple_xattrs() {
let set: XattrSet = vec![
Xattr {
key: "user.color".into(),
value: b"orange".to_vec(),
},
Xattr {
key: "security.selinux".into(),
value: b"unconfined_u".to_vec(),
},
];
let base = 200u64;
let (payload, hdr_off) =
encode_xattr_table(std::slice::from_ref(&set), base, Compression::Unknown(0)).unwrap();
let mut dev = MemoryBackend::new(base + payload.len() as u64 + 64);
dev.write_at(base, &payload).unwrap();
let mut r = XattrReader::new();
r.ensure_loaded(&mut dev, base + hdr_off, Compression::Unknown(0))
.unwrap();
let read_set = r.fetch(&mut dev, 0, Compression::Unknown(0)).unwrap();
assert_eq!(read_set.len(), 2);
assert_eq!(read_set[0].key, "user.color");
assert_eq!(read_set[0].value, b"orange");
assert_eq!(read_set[1].key, "security.selinux");
assert_eq!(read_set[1].value, b"unconfined_u");
}
#[test]
fn empty_xattr_table_decodes_zero_count() {
let base = 0u64;
let (payload, hdr_off) = encode_xattr_table(&[], base, Compression::Unknown(0)).unwrap();
let mut dev = MemoryBackend::new(payload.len() as u64 + 64);
dev.write_at(0, &payload).unwrap();
let mut r = XattrReader::new();
r.ensure_loaded(&mut dev, hdr_off, Compression::Unknown(0))
.unwrap();
let empty = r.fetch(&mut dev, 0, Compression::Unknown(0)).unwrap();
assert_eq!(empty.len(), 0);
}
}