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::volume_header::ForkData;
pub const ROOT_PARENT_ID: u32 = 1;
pub const ROOT_FOLDER_ID: u32 = 2;
pub const REC_FOLDER: i16 = 0x0001;
pub const REC_FILE: i16 = 0x0002;
pub const REC_FOLDER_THREAD: i16 = 0x0003;
pub const REC_FILE_THREAD: i16 = 0x0004;
pub mod mode {
pub const S_IFMT: u16 = 0o170000;
pub const S_IFIFO: u16 = 0o010000;
pub const S_IFCHR: u16 = 0o020000;
pub const S_IFDIR: u16 = 0o040000;
pub const S_IFBLK: u16 = 0o060000;
pub const S_IFREG: u16 = 0o100000;
pub const S_IFLNK: u16 = 0o120000;
pub const S_IFSOCK: u16 = 0o140000;
}
#[derive(Debug, Clone, Default)]
pub struct UniStr {
pub code_units: Vec<u16>,
}
impl UniStr {
pub fn decode(buf: &[u8]) -> Result<(Self, usize)> {
if buf.len() < 2 {
return Err(crate::Error::InvalidImage(
"hfs+: short HFSUniStr255 length".into(),
));
}
let n = u16::from_be_bytes([buf[0], buf[1]]) as usize;
if n > 255 {
return Err(crate::Error::InvalidImage(format!(
"hfs+: HFSUniStr255 length {n} exceeds 255"
)));
}
let bytes_needed = 2 + 2 * n;
if buf.len() < bytes_needed {
return Err(crate::Error::InvalidImage(
"hfs+: HFSUniStr255 truncated".into(),
));
}
let mut code_units = Vec::with_capacity(n);
for i in 0..n {
let off = 2 + 2 * i;
code_units.push(u16::from_be_bytes([buf[off], buf[off + 1]]));
}
Ok((Self { code_units }, bytes_needed))
}
pub fn from_str_lossy(s: &str) -> Self {
Self {
code_units: s.encode_utf16().collect(),
}
}
pub fn to_string_lossy(&self) -> String {
String::from_utf16_lossy(&self.code_units)
}
}
fn fold_case(c: u16) -> u16 {
match c {
0x41..=0x5A => c + 0x20,
0xC0..=0xD6 => c + 0x20,
0xD8..=0xDE => c + 0x20,
_ => c,
}
}
pub fn compare_unistr(a: &UniStr, b: &UniStr, case_sensitive: bool) -> Ordering {
let n = a.code_units.len().min(b.code_units.len());
for i in 0..n {
let (ai, bi) = if case_sensitive {
(a.code_units[i], b.code_units[i])
} else {
(fold_case(a.code_units[i]), fold_case(b.code_units[i]))
};
match ai.cmp(&bi) {
Ordering::Equal => continue,
o => return o,
}
}
a.code_units.len().cmp(&b.code_units.len())
}
#[derive(Debug, Clone)]
pub struct CatalogKey {
pub parent_id: u32,
pub name: UniStr,
pub encoded_len: usize,
}
impl CatalogKey {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < 6 {
return Err(crate::Error::InvalidImage("hfs+: short catalog key".into()));
}
let key_length = u16::from_be_bytes([buf[0], buf[1]]) as usize;
let parent_id = u32::from_be_bytes(buf[2..6].try_into().unwrap());
let (name, name_len) = UniStr::decode(&buf[6..])?;
let used = 2 + key_length;
if 4 + name_len != key_length {
return Err(crate::Error::InvalidImage(format!(
"hfs+: catalog key_length {key_length} disagrees with parsed name \
(parent + {name_len} name bytes)"
)));
}
let encoded_len = if used % 2 == 0 { used } else { used + 1 };
Ok(Self {
parent_id,
name,
encoded_len,
})
}
pub fn compare(&self, other: &CatalogKey, case_sensitive: bool) -> Ordering {
match self.parent_id.cmp(&other.parent_id) {
Ordering::Equal => compare_unistr(&self.name, &other.name, case_sensitive),
o => o,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BsdInfo {
pub owner_id: u32,
pub group_id: u32,
pub admin_flags: u8,
pub owner_flags: u8,
pub file_mode: u16,
pub special: u32,
}
impl BsdInfo {
fn decode(buf: &[u8]) -> Self {
Self {
owner_id: u32::from_be_bytes(buf[0..4].try_into().unwrap()),
group_id: u32::from_be_bytes(buf[4..8].try_into().unwrap()),
admin_flags: buf[8],
owner_flags: buf[9],
file_mode: u16::from_be_bytes(buf[10..12].try_into().unwrap()),
special: u32::from_be_bytes(buf[12..16].try_into().unwrap()),
}
}
}
#[derive(Debug, Clone)]
pub struct CatalogFolder {
pub folder_id: u32,
pub valence: u32,
pub bsd: BsdInfo,
}
#[derive(Debug, Clone)]
pub struct CatalogFile {
pub file_id: u32,
pub bsd: BsdInfo,
pub file_type: [u8; 4],
pub creator: [u8; 4],
pub data_fork: ForkData,
pub resource_fork: ForkData,
}
impl CatalogFile {
pub fn is_hard_link(&self) -> bool {
&self.file_type == b"hlnk" && &self.creator == b"hfs+"
}
pub fn is_symlink(&self) -> bool {
&self.file_type == b"slnk" && &self.creator == b"rhap"
}
}
#[derive(Debug, Clone)]
pub struct CatalogThread {
pub parent_id: u32,
pub name: UniStr,
}
#[derive(Debug, Clone)]
pub enum CatalogRecord {
Folder(CatalogFolder),
File(CatalogFile),
Thread(CatalogThread),
}
impl CatalogRecord {
pub fn decode(body: &[u8]) -> Result<Self> {
if body.len() < 2 {
return Err(crate::Error::InvalidImage(
"hfs+: short catalog record body".into(),
));
}
let rec_type = i16::from_be_bytes([body[0], body[1]]);
match rec_type {
REC_FOLDER => Self::decode_folder(body),
REC_FILE => Self::decode_file(body),
REC_FOLDER_THREAD | REC_FILE_THREAD => Self::decode_thread(body),
other => Err(crate::Error::InvalidImage(format!(
"hfs+: unknown catalog record type {other:#06x}"
))),
}
}
fn decode_folder(body: &[u8]) -> Result<Self> {
if body.len() < 88 {
return Err(crate::Error::InvalidImage(
"hfs+: short HFSPlusCatalogFolder".into(),
));
}
let valence = u32::from_be_bytes(body[4..8].try_into().unwrap());
let folder_id = u32::from_be_bytes(body[8..12].try_into().unwrap());
let bsd = BsdInfo::decode(&body[32..48]);
Ok(Self::Folder(CatalogFolder {
folder_id,
valence,
bsd,
}))
}
fn decode_file(body: &[u8]) -> Result<Self> {
if body.len() < 248 {
return Err(crate::Error::InvalidImage(
"hfs+: short HFSPlusCatalogFile".into(),
));
}
let file_id = u32::from_be_bytes(body[8..12].try_into().unwrap());
let bsd = BsdInfo::decode(&body[32..48]);
let mut file_type = [0u8; 4];
let mut creator = [0u8; 4];
file_type.copy_from_slice(&body[48..52]);
creator.copy_from_slice(&body[52..56]);
let mut fbuf = [0u8; 80];
fbuf.copy_from_slice(&body[88..168]);
let data_fork = ForkData::decode(&fbuf);
fbuf.copy_from_slice(&body[168..248]);
let resource_fork = ForkData::decode(&fbuf);
Ok(Self::File(CatalogFile {
file_id,
bsd,
file_type,
creator,
data_fork,
resource_fork,
}))
}
fn decode_thread(body: &[u8]) -> Result<Self> {
if body.len() < 8 {
return Err(crate::Error::InvalidImage(
"hfs+: short catalog thread record".into(),
));
}
let parent_id = u32::from_be_bytes(body[4..8].try_into().unwrap());
let (name, _) = UniStr::decode(&body[8..])?;
Ok(Self::Thread(CatalogThread { parent_id, name }))
}
}
pub struct Catalog {
pub fork: ForkReader,
pub header: BTreeHeader,
pub case_sensitive: bool,
}
impl Catalog {
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+: catalog 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,
wanted: &CatalogKey,
) -> Result<Option<CatalogRecord>> {
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 = CatalogKey::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+: catalog record key overruns its slot".into(),
));
}
let body = &rec[body_start..];
return Ok(Some(CatalogRecord::decode(body)?));
}
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 = CatalogKey::decode(rec)?;
let pointer_off = align2(key.encoded_len);
if pointer_off + 4 > rec.len() {
return Err(crate::Error::InvalidImage(
"hfs+: 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 catalog traversal",
desc.kind
)));
}
}
}
}
fn align2(n: usize) -> usize {
n + (n & 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unistr_decode_round_trip() {
let mut buf = Vec::new();
buf.extend_from_slice(&4u16.to_be_bytes());
for c in "ABcd".encode_utf16() {
buf.extend_from_slice(&c.to_be_bytes());
}
let (s, n) = UniStr::decode(&buf).unwrap();
assert_eq!(n, 10);
assert_eq!(s.code_units, vec![0x41, 0x42, 0x63, 0x64]);
assert_eq!(s.to_string_lossy(), "ABcd");
}
#[test]
fn unistr_decode_truncated_errors() {
let mut buf = Vec::new();
buf.extend_from_slice(&3u16.to_be_bytes());
buf.extend_from_slice(&0x41u16.to_be_bytes());
assert!(UniStr::decode(&buf).is_err());
}
#[test]
fn compare_unistr_case_insensitive_default() {
let a = UniStr::from_str_lossy("Hello");
let b = UniStr::from_str_lossy("hello");
assert_eq!(compare_unistr(&a, &b, false), Ordering::Equal);
assert_eq!(compare_unistr(&a, &b, true), Ordering::Less);
}
#[test]
fn catalog_key_compare_orders_by_parent_then_name() {
let k1 = CatalogKey {
parent_id: 1,
name: UniStr::from_str_lossy("z"),
encoded_len: 0,
};
let k2 = CatalogKey {
parent_id: 2,
name: UniStr::from_str_lossy("a"),
encoded_len: 0,
};
assert_eq!(k1.compare(&k2, false), Ordering::Less);
let k3 = CatalogKey {
parent_id: 2,
name: UniStr::from_str_lossy("b"),
encoded_len: 0,
};
assert_eq!(k2.compare(&k3, false), Ordering::Less);
}
#[test]
fn catalog_key_decode_round_trip() {
let name_bytes_len = 2 + 2 * 2; let key_payload_len = 4 + name_bytes_len; let mut buf = Vec::new();
buf.extend_from_slice(&(key_payload_len as u16).to_be_bytes());
buf.extend_from_slice(&42u32.to_be_bytes());
buf.extend_from_slice(&2u16.to_be_bytes());
for c in "hi".encode_utf16() {
buf.extend_from_slice(&c.to_be_bytes());
}
let key = CatalogKey::decode(&buf).unwrap();
assert_eq!(key.parent_id, 42);
assert_eq!(key.name.to_string_lossy(), "hi");
assert_eq!(key.encoded_len, 12);
}
}