use crate::dir::{ftype, parse_items};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XattrEntry {
pub name: Vec<u8>,
pub value: Vec<u8>,
}
pub fn parse_xattr_items(data: &[u8]) -> Result<Vec<XattrEntry>> {
parse_items(data, "extended attribute item")?
.into_iter()
.map(|r| {
if r.ftype != ftype::XATTR {
return Err(Error::BadSuperblock(format!(
"extended attribute {:?} carries file type {} rather than \
{} — this is not an XATTR_ITEM",
String::from_utf8_lossy(r.name),
r.ftype,
ftype::XATTR
)));
}
Ok(XattrEntry {
name: r.name.to_vec(),
value: r.value.to_vec(),
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dir::{offsets, DIR_ITEM_HEADER_SIZE, XATTR_ITEM_KEY};
fn encode(name: &[u8], value: &[u8], ft: u8) -> Vec<u8> {
let mut b = vec![0u8; DIR_ITEM_HEADER_SIZE];
b[offsets::DATA_LEN..offsets::DATA_LEN + 2]
.copy_from_slice(&(value.len() as u16).to_le_bytes());
b[offsets::NAME_LEN..offsets::NAME_LEN + 2]
.copy_from_slice(&(name.len() as u16).to_le_bytes());
b[offsets::TYPE] = ft;
b.extend_from_slice(name);
b.extend_from_slice(value);
b
}
fn one(name: &[u8], value: &[u8]) -> Vec<u8> {
encode(name, value, ftype::XATTR)
}
#[test]
fn reads_a_name_and_its_value() {
let got = parse_xattr_items(&one(b"user.colour", b"blue")).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name, b"user.colour");
assert_eq!(got[0].value, b"blue");
}
#[test]
fn an_empty_value_is_still_an_attribute() {
let got = parse_xattr_items(&one(b"user.flag", b"")).unwrap();
assert_eq!(got.len(), 1);
assert!(got[0].value.is_empty());
}
#[test]
fn reads_several_attributes_packed_into_one_item() {
let mut data = one(b"user.first", b"1");
data.extend(one(b"user.second", b"22"));
data.extend(one(b"user.third", b"333"));
let got = parse_xattr_items(&data).unwrap();
assert_eq!(got.len(), 3);
assert_eq!(got[0].name, b"user.first");
assert_eq!(got[1].value, b"22");
assert_eq!(got[2].name, b"user.third");
}
#[test]
fn a_value_may_be_any_bytes_at_all() {
let value = [0u8, 1, 0xff, 0x7f, b'\n', 0];
let got = parse_xattr_items(&one(b"user.binary", &value)).unwrap();
assert_eq!(got[0].value, value);
}
#[test]
fn rejects_a_record_that_is_not_an_xattr() {
let data = encode(b"notanattr", b"", crate::dir::ftype::REG_FILE);
match parse_xattr_items(&data) {
Err(Error::BadSuperblock(m)) => assert!(m.contains("file type"), "message: {m}"),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[test]
fn rejects_a_value_running_past_the_end() {
let mut data = one(b"user.x", b"short");
data[offsets::DATA_LEN..offsets::DATA_LEN + 2].copy_from_slice(&9000u16.to_le_bytes());
assert!(matches!(
parse_xattr_items(&data),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn rejects_data_the_records_do_not_exactly_consume() {
let mut data = one(b"user.x", b"value");
data.push(0);
assert!(matches!(
parse_xattr_items(&data),
Err(Error::BadSuperblock(_))
));
}
#[test]
fn empty_data_is_an_empty_list() {
assert!(parse_xattr_items(&[]).unwrap().is_empty());
}
#[test]
fn the_key_type_is_the_one_the_format_defines() {
assert_eq!(XATTR_ITEM_KEY, 24);
}
}