use std::cmp::Ordering;
use crate::Result;
use crate::block::BlockDevice;
use super::btree::{
BTreeHeader, ForkReader, HEADER_REC_SIZE, KIND_INDEX, KIND_LEAF, NODE_DESCRIPTOR_SIZE,
NodeDescriptor, read_node, record_bytes, record_offsets,
};
use super::catalog::{UniStr, compare_unistr};
use super::volume_header::ForkData;
pub const REC_INLINE_DATA: u32 = 0x10;
pub const REC_FORK_DATA: u32 = 0x20;
pub const REC_EXTENTS: u32 = 0x30;
#[derive(Debug, Clone)]
pub struct AttrKey {
pub file_id: u32,
pub start_block: u32,
pub name: UniStr,
pub encoded_len: usize,
}
impl AttrKey {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < 14 {
return Err(crate::Error::InvalidImage(
"hfs+: short attribute key".into(),
));
}
let key_length = u16::from_be_bytes([buf[0], buf[1]]) as usize;
let file_id = u32::from_be_bytes(buf[4..8].try_into().unwrap());
let start_block = u32::from_be_bytes(buf[8..12].try_into().unwrap());
let name_len = u16::from_be_bytes([buf[12], buf[13]]) as usize;
if name_len > 127 {
return Err(crate::Error::InvalidImage(format!(
"hfs+: attribute name length {name_len} exceeds 127"
)));
}
let payload_len = 14 + 2 * name_len;
if 12 + 2 * name_len != key_length {
return Err(crate::Error::InvalidImage(format!(
"hfs+: attribute key_length {key_length} disagrees with parsed name \
({name_len} UTF-16 code units)"
)));
}
if buf.len() < payload_len {
return Err(crate::Error::InvalidImage(
"hfs+: attribute key truncated".into(),
));
}
let mut code_units = Vec::with_capacity(name_len);
for i in 0..name_len {
let off = 14 + 2 * i;
code_units.push(u16::from_be_bytes([buf[off], buf[off + 1]]));
}
let used = payload_len;
let encoded_len = if used % 2 == 0 { used } else { used + 1 };
Ok(Self {
file_id,
start_block,
name: UniStr { code_units },
encoded_len,
})
}
pub fn compare(&self, other: &AttrKey, case_sensitive: bool) -> Ordering {
match self.file_id.cmp(&other.file_id) {
Ordering::Equal => match compare_unistr(&self.name, &other.name, case_sensitive) {
Ordering::Equal => self.start_block.cmp(&other.start_block),
o => o,
},
o => o,
}
}
}
#[derive(Debug, Clone)]
pub enum AttrRecord {
Inline {
data: Vec<u8>,
},
Fork {
fork: ForkData,
},
}
impl AttrRecord {
pub fn decode(body: &[u8]) -> Result<Self> {
if body.len() < 4 {
return Err(crate::Error::InvalidImage(
"hfs+: short attribute record body".into(),
));
}
let rec_type = u32::from_be_bytes(body[0..4].try_into().unwrap());
match rec_type {
REC_INLINE_DATA => {
if body.len() < 12 {
return Err(crate::Error::InvalidImage(
"hfs+: short HFSPlusAttrInlineData".into(),
));
}
let attr_size = u32::from_be_bytes(body[8..12].try_into().unwrap()) as usize;
if body.len() < 12 + attr_size {
return Err(crate::Error::InvalidImage(format!(
"hfs+: HFSPlusAttrInlineData attrSize {attr_size} exceeds record body ({} bytes)",
body.len() - 12
)));
}
Ok(Self::Inline {
data: body[12..12 + attr_size].to_vec(),
})
}
REC_FORK_DATA => {
if body.len() < 8 + 80 {
return Err(crate::Error::InvalidImage(
"hfs+: short HFSPlusAttrForkData".into(),
));
}
let mut tmp = [0u8; 80];
tmp.copy_from_slice(&body[8..88]);
Ok(Self::Fork {
fork: ForkData::decode(&tmp),
})
}
REC_EXTENTS => Err(crate::Error::Unsupported(
"hfs+: attribute overflow-extents record (0x30) not supported".into(),
)),
other => Err(crate::Error::InvalidImage(format!(
"hfs+: unknown attribute record type {other:#010x}"
))),
}
}
}
pub struct Attributes {
pub fork: ForkReader,
pub header: BTreeHeader,
pub case_sensitive: bool,
}
impl Attributes {
pub fn open(dev: &mut dyn BlockDevice, fork: ForkReader, case_sensitive: bool) -> Result<Self> {
let mut bootstrap = vec![0u8; 512];
fork.read(dev, 0, &mut bootstrap)?;
let probe_desc = NodeDescriptor::decode(&bootstrap)?;
if probe_desc.kind != super::btree::KIND_HEADER {
return Err(crate::Error::InvalidImage(format!(
"hfs+: attributes node 0 has kind {} (expected header)",
probe_desc.kind
)));
}
let hdr_buf = &bootstrap[NODE_DESCRIPTOR_SIZE..NODE_DESCRIPTOR_SIZE + HEADER_REC_SIZE];
let header = BTreeHeader::decode(hdr_buf)?;
Ok(Self {
fork,
header,
case_sensitive,
})
}
pub fn lookup(
&self,
dev: &mut dyn BlockDevice,
file_id: u32,
name: &str,
) -> Result<Option<AttrRecord>> {
let wanted = AttrKey {
file_id,
start_block: 0,
name: UniStr::from_str_lossy(name),
encoded_len: 0,
};
let node_size = u32::from(self.header.node_size);
let mut node_idx = self.header.root_node;
if node_idx == 0 {
return Ok(None);
}
loop {
let node = read_node(dev, &self.fork, node_idx, node_size)?;
let desc = NodeDescriptor::decode(&node)?;
let offs = record_offsets(&node, desc.num_records)?;
if desc.kind == KIND_LEAF {
for i in 0..desc.num_records as usize {
let rec = record_bytes(&node, &offs, i);
let key = AttrKey::decode(rec)?;
match key.compare(&wanted, self.case_sensitive) {
Ordering::Equal => {
let body_start = align2(key.encoded_len);
if body_start > rec.len() {
return Err(crate::Error::InvalidImage(
"hfs+: attribute key overruns record".into(),
));
}
return Ok(Some(AttrRecord::decode(&rec[body_start..])?));
}
Ordering::Greater => return Ok(None),
Ordering::Less => continue,
}
}
return Ok(None);
} else if desc.kind == KIND_INDEX {
let mut child: Option<u32> = None;
for i in 0..desc.num_records as usize {
let rec = record_bytes(&node, &offs, i);
let key = AttrKey::decode(rec)?;
let pointer_off = align2(key.encoded_len);
if pointer_off + 4 > rec.len() {
return Err(crate::Error::InvalidImage(
"hfs+: attributes index record missing child pointer".into(),
));
}
let next =
u32::from_be_bytes(rec[pointer_off..pointer_off + 4].try_into().unwrap());
match key.compare(&wanted, self.case_sensitive) {
Ordering::Less | Ordering::Equal => child = Some(next),
Ordering::Greater => break,
}
}
node_idx = match child {
Some(c) => c,
None => return Ok(None),
};
} else {
return Err(crate::Error::InvalidImage(format!(
"hfs+: unexpected B-tree node kind {} in attributes traversal",
desc.kind
)));
}
}
}
}
fn align2(n: usize) -> usize {
n + (n & 1)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_inline_record(name: &str, value: &[u8]) -> Vec<u8> {
let name_units: Vec<u16> = name.encode_utf16().collect();
let key_payload_len = 12 + 2 * name_units.len();
let mut out = Vec::new();
out.extend_from_slice(&(key_payload_len as u16).to_be_bytes());
out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&42u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&(name_units.len() as u16).to_be_bytes());
for u in &name_units {
out.extend_from_slice(&u.to_be_bytes());
}
out.extend_from_slice(&REC_INLINE_DATA.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&(value.len() as u32).to_be_bytes()); out.extend_from_slice(value);
out
}
#[test]
fn attr_key_round_trip() {
let buf = make_inline_record("com.apple.decmpfs", &[0u8; 32]);
let key = AttrKey::decode(&buf).unwrap();
assert_eq!(key.file_id, 42);
assert_eq!(key.start_block, 0);
assert_eq!(key.name.to_string_lossy(), "com.apple.decmpfs");
assert_eq!(key.encoded_len, 48);
}
#[test]
fn attr_inline_record_decode() {
let payload = b"hello world".to_vec();
let buf = make_inline_record("foo", &payload);
let key = AttrKey::decode(&buf).unwrap();
let body = &buf[align2(key.encoded_len)..];
match AttrRecord::decode(body).unwrap() {
AttrRecord::Inline { data } => assert_eq!(data, payload),
other => panic!("expected Inline, got {other:?}"),
}
}
#[test]
fn attr_key_compare_orders_by_file_id_then_name() {
let a = AttrKey {
file_id: 1,
start_block: 0,
name: UniStr::from_str_lossy("zzz"),
encoded_len: 0,
};
let b = AttrKey {
file_id: 2,
start_block: 0,
name: UniStr::from_str_lossy("aaa"),
encoded_len: 0,
};
assert_eq!(a.compare(&b, false), Ordering::Less);
let c = AttrKey {
file_id: 2,
start_block: 0,
name: UniStr::from_str_lossy("bbb"),
encoded_len: 0,
};
assert_eq!(b.compare(&c, false), Ordering::Less);
}
}