use std::collections::BTreeMap;
use super::{ExtRec, MAC_EPOCH_DELTA, ROOT_CNID, round_up_even};
use crate::block::BlockDevice;
use crate::macroman;
use crate::{Error, Result};
const ROOT_PARENT_CNID: u32 = 1;
const FIRST_USER_CNID: u32 = 16;
const NODE: usize = 512;
const DESC: usize = 14;
const ND_LEAF: u8 = 0xFF;
const ND_INDEX: u8 = 0x00;
const ND_HEADER: u8 = 0x01;
const CDR_DIR: u8 = 1;
const CDR_FILE: u8 = 2;
const CDR_DIR_THREAD: u8 = 3;
fn put_u16(v: &mut [u8], o: usize, x: u16) {
v[o..o + 2].copy_from_slice(&x.to_be_bytes());
}
fn put_u32(v: &mut [u8], o: usize, x: u32) {
v[o..o + 4].copy_from_slice(&x.to_be_bytes());
}
fn mac_time(unix: u32) -> u32 {
unix.saturating_add(MAC_EPOCH_DELTA)
}
#[derive(Debug, Clone)]
pub struct HfsFormatOpts {
pub volume_name: String,
pub block_size: Option<u32>,
}
impl Default for HfsFormatOpts {
fn default() -> Self {
Self {
volume_name: "Untitled".to_string(),
block_size: None,
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub(super) struct OwnedKey {
pub parid: u32,
pub name: Vec<u8>,
}
impl Ord for OwnedKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.parid
.cmp(&other.parid)
.then_with(|| macroman::cmp_ci(&self.name, &other.name))
}
}
impl PartialOrd for OwnedKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub(super) struct HfsWriter {
pub block_size: u32,
pub alloc_base: u64,
pub vbm_start: u64,
pub total_blocks: u16,
pub total_sectors: u64,
pub bitmap: Vec<u8>,
pub next_alloc: u16,
pub free_blocks: u16,
pub next_cnid: u32,
pub catalog: BTreeMap<OwnedKey, Vec<u8>>,
pub overflow: BTreeMap<(u8, u32, u16), ExtRec>,
pub volume_name: String,
pub create_date: u32,
cat_extents: ExtRec,
cat_size: u32,
ext_extents: ExtRec,
ext_size: u32,
}
impl HfsWriter {
pub fn format(dev: &mut dyn BlockDevice, opts: &HfsFormatOpts) -> Result<Self> {
let total_sectors = dev.total_size() / 512;
if total_sectors < 16 {
return Err(Error::InvalidArgument(
"hfs: volume too small to format".into(),
));
}
let block_size = match opts.block_size {
Some(b) => {
if b == 0 || !b.is_multiple_of(512) {
return Err(Error::InvalidArgument(
"hfs: block_size must be a non-zero multiple of 512".into(),
));
}
b
}
None => auto_block_size(total_sectors),
};
let name_bytes = macroman::encode(&opts.volume_name)?;
if name_bytes.is_empty() || name_bytes.len() > 27 {
return Err(Error::InvalidArgument(
"hfs: volume name must be 1..=27 MacRoman bytes".into(),
));
}
let geom = Geometry::compute(total_sectors, block_size)?;
let bitmap = vec![0u8; geom.total_blocks.div_ceil(8) as usize];
let create = mac_time(0);
let mut w = HfsWriter {
block_size,
alloc_base: geom.al_bl_st * 512,
vbm_start: geom.vbm_start,
total_blocks: geom.total_blocks,
total_sectors,
bitmap,
next_alloc: 0,
free_blocks: geom.total_blocks,
next_cnid: FIRST_USER_CNID,
catalog: BTreeMap::new(),
overflow: BTreeMap::new(),
volume_name: opts.volume_name.clone(),
create_date: create,
cat_extents: [(0, 0); 3],
cat_size: 0,
ext_extents: [(0, 0); 3],
ext_size: 0,
};
w.catalog.insert(
OwnedKey {
parid: ROOT_PARENT_CNID,
name: name_bytes.clone(),
},
encode_dir(ROOT_CNID, 0, create),
);
w.catalog.insert(
OwnedKey {
parid: ROOT_CNID,
name: Vec::new(),
},
encode_thread(ROOT_PARENT_CNID, &name_bytes),
);
Ok(w)
}
fn root_valence(&self) -> u16 {
self.catalog
.keys()
.filter(|k| k.parid == ROOT_CNID && !k.name.is_empty())
.count() as u16
}
fn counts(&self) -> (u32, u32, u16) {
let mut files = 0u32;
let mut dirs = 0u32;
let mut root_dirs = 0u16;
for (key, body) in &self.catalog {
match body.first().copied() {
Some(CDR_FILE) => files += 1,
Some(CDR_DIR) => {
let cnid = u32::from_be_bytes([body[6], body[7], body[8], body[9]]);
if cnid != ROOT_CNID {
dirs += 1;
}
if key.parid == ROOT_CNID {
root_dirs += 1;
}
}
_ => {}
}
}
(files, dirs, root_dirs)
}
fn bit_used(&self, b: u16) -> bool {
self.bitmap[(b / 8) as usize] & (0x80 >> (b % 8)) != 0
}
fn mark(&mut self, start: u16, count: u16, used: bool) {
for b in start..start + count {
let (byi, mask) = ((b / 8) as usize, 0x80u8 >> (b % 8));
if used {
self.bitmap[byi] |= mask;
} else {
self.bitmap[byi] &= !mask;
}
}
}
fn allocate(&mut self, n: u16) -> Result<Vec<(u16, u16)>> {
if n == 0 {
return Ok(Vec::new());
}
if self.free_blocks < n {
return Err(Error::Unsupported("hfs: volume full".into()));
}
let mut runs = Vec::new();
let mut need = n;
let mut cur = self.next_alloc.min(self.total_blocks);
while need > 0 {
while cur < self.total_blocks && self.bit_used(cur) {
cur += 1;
}
if cur >= self.total_blocks {
cur = 0;
while cur < self.total_blocks && self.bit_used(cur) {
cur += 1;
}
if cur >= self.total_blocks {
return Err(Error::Unsupported("hfs: volume full".into()));
}
}
let start = cur;
let mut len = 0u16;
while cur < self.total_blocks && !self.bit_used(cur) && len < need {
cur += 1;
len += 1;
}
self.mark(start, len, true);
self.free_blocks -= len;
need -= len;
runs.push((start, len));
}
self.next_alloc = cur;
Ok(runs)
}
fn free_run(&mut self, start: u16, count: u16) {
if count == 0 {
return;
}
self.mark(start, count, false);
self.free_blocks += count;
if start < self.next_alloc {
self.next_alloc = start;
}
}
pub fn resolve_dir(&self, path: &str) -> Result<u32> {
let mut cnid = ROOT_CNID;
for comp in path.split('/').filter(|c| !c.is_empty()) {
let name = encode_component(comp)?;
let body = self
.catalog
.get(&OwnedKey { parid: cnid, name })
.ok_or_else(|| {
Error::InvalidArgument(format!("hfs: no such directory component {comp:?}"))
})?;
if body.first() != Some(&CDR_DIR) {
return Err(Error::InvalidArgument(format!(
"hfs: path component {comp:?} is not a directory"
)));
}
cnid = u32::from_be_bytes([body[6], body[7], body[8], body[9]]);
}
Ok(cnid)
}
fn split_parent_leaf(&self, path: &str) -> Result<(u32, Vec<u8>)> {
let trimmed = path.trim_end_matches('/');
let (parent_path, leaf) = match trimmed.rsplit_once('/') {
Some((p, l)) => (p, l),
None => ("", trimmed),
};
if leaf.is_empty() {
return Err(Error::InvalidArgument(
"hfs: cannot target the root path".into(),
));
}
let parent = self.resolve_dir(parent_path)?;
let name = encode_component(leaf)?;
if name.len() > 31 {
return Err(Error::InvalidArgument(
"hfs: name exceeds 31 MacRoman bytes".into(),
));
}
Ok((parent, name))
}
fn parent_and_leaf(&self, path: &str) -> Result<(u32, Vec<u8>)> {
let (parent, name) = self.split_parent_leaf(path)?;
if self.catalog.contains_key(&OwnedKey {
parid: parent,
name: name.clone(),
}) {
return Err(Error::InvalidArgument(format!(
"hfs: {path:?} already exists"
)));
}
Ok((parent, name))
}
pub fn remove(&mut self, path: &str) -> Result<()> {
let (parent, name) = self.split_parent_leaf(path)?;
let key = OwnedKey {
parid: parent,
name,
};
let body = self
.catalog
.get(&key)
.ok_or_else(|| Error::InvalidArgument(format!("hfs: no such path {path:?}")))?;
match body.first().copied() {
Some(CDR_DIR) => {
let valence = u16::from_be_bytes([body[4], body[5]]);
if valence != 0 {
return Err(Error::InvalidArgument(format!(
"hfs: directory {path:?} is not empty"
)));
}
let cnid = u32::from_be_bytes([body[6], body[7], body[8], body[9]]);
self.catalog.remove(&key);
self.catalog.remove(&OwnedKey {
parid: cnid,
name: Vec::new(),
});
}
Some(CDR_FILE) => {
let cnid = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
let ext = super::ext_rec(body, 74);
self.catalog.remove(&key);
for (s, c) in ext {
self.free_run(s, c);
}
let keys: Vec<_> = self
.overflow
.range((0x00u8, cnid, 0u16)..=(0x00u8, cnid, u16::MAX))
.map(|(&k, _)| k)
.collect();
for k in keys {
if let Some(grp) = self.overflow.remove(&k) {
for (s, c) in grp {
self.free_run(s, c);
}
}
}
}
_ => {
return Err(Error::InvalidArgument(format!(
"hfs: {path:?} is not a removable file or directory"
)));
}
}
self.bump_valence(parent, -1)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn adopt(
block_size: u32,
alloc_base: u64,
vbm_start: u64,
total_blocks: u16,
total_sectors: u64,
bitmap: Vec<u8>,
free_blocks: u16,
next_cnid: u32,
catalog: BTreeMap<OwnedKey, Vec<u8>>,
overflow: BTreeMap<(u8, u32, u16), ExtRec>,
volume_name: String,
create_date: u32,
cat_extents: ExtRec,
cat_size: u32,
ext_extents: ExtRec,
ext_size: u32,
) -> Self {
HfsWriter {
block_size,
alloc_base,
vbm_start,
total_blocks,
total_sectors,
bitmap,
next_alloc: 0,
free_blocks,
next_cnid,
catalog,
overflow,
volume_name,
create_date,
cat_extents,
cat_size,
ext_extents,
ext_size,
}
}
fn bump_valence(&mut self, dir_cnid: u32, delta: i32) -> Result<()> {
let thread = self
.catalog
.get(&OwnedKey {
parid: dir_cnid,
name: Vec::new(),
})
.ok_or_else(|| Error::InvalidImage("hfs: missing directory thread".into()))?;
let parent = u32::from_be_bytes([thread[10], thread[11], thread[12], thread[13]]);
let name = thread[15..15 + thread[14] as usize].to_vec();
let body = self
.catalog
.get_mut(&OwnedKey {
parid: parent,
name,
})
.ok_or_else(|| Error::InvalidImage("hfs: missing directory record".into()))?;
let val = u16::from_be_bytes([body[4], body[5]]);
let nv = (val as i32 + delta).clamp(0, u16::MAX as i32) as u16;
put_u16(body, 4, nv);
Ok(())
}
pub fn insert_dir(&mut self, path: &str, mtime: u32) -> Result<u32> {
let (parent, name) = self.parent_and_leaf(path)?;
let cnid = self.next_cnid;
self.next_cnid += 1;
let t = mac_time(mtime);
self.catalog.insert(
OwnedKey {
parid: parent,
name: name.clone(),
},
encode_dir(cnid, 0, t),
);
self.catalog.insert(
OwnedKey {
parid: cnid,
name: Vec::new(),
},
encode_thread(parent, &name),
);
self.bump_valence(parent, 1)?;
Ok(cnid)
}
pub fn insert_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
src: &mut dyn std::io::Read,
len: u64,
mtime: u32,
) -> Result<u32> {
let (parent, name) = self.parent_and_leaf(path)?;
let cnid = self.next_cnid;
let (extents, total_blocks) = self.stream_data(dev, src, len, cnid)?;
self.next_cnid += 1;
let t = mac_time(mtime);
self.catalog.insert(
OwnedKey {
parid: parent,
name,
},
encode_file(cnid, len, t, &extents, total_blocks, self.block_size),
);
self.bump_valence(parent, 1)?;
Ok(cnid)
}
fn stream_data(
&mut self,
dev: &mut dyn BlockDevice,
src: &mut dyn std::io::Read,
len: u64,
cnid: u32,
) -> Result<(ExtRec, u16)> {
let bs = u64::from(self.block_size);
let total_blocks_u64 = len.div_ceil(bs);
let total_blocks = u16::try_from(total_blocks_u64).map_err(|_| {
Error::Unsupported("hfs: file too large for a 16-bit block count".into())
})?;
let runs = if total_blocks == 0 {
Vec::new()
} else {
self.allocate(total_blocks)?
};
let mut buf = vec![0u8; 64 * 1024];
let mut written = 0u64;
for &(start, count) in &runs {
let mut off = self.alloc_base + u64::from(start) * bs;
let mut run_remaining = u64::from(count) * bs;
while run_remaining > 0 && written < len {
let want = (len - written).min(run_remaining).min(buf.len() as u64) as usize;
let mut filled = 0;
while filled < want {
let n = src.read(&mut buf[filled..want])?;
if n == 0 {
return Err(Error::InvalidArgument(
"hfs: source ended before declared length".into(),
));
}
filled += n;
}
dev.write_at(off, &buf[..filled])?;
off += filled as u64;
run_remaining -= filled as u64;
written += filled as u64;
}
if run_remaining > 0 {
let zero = vec![0u8; run_remaining as usize];
dev.write_at(off, &zero)?;
}
}
let mut inline: ExtRec = [(0, 0); 3];
for (slot, run) in inline.iter_mut().zip(runs.iter()) {
*slot = *run;
}
if runs.len() > 3 {
let mut start_block: u16 = inline.iter().map(|e| e.1).sum();
let mut rest = &runs[3..];
while !rest.is_empty() {
let mut grp: ExtRec = [(0, 0); 3];
let take = rest.len().min(3);
for (slot, run) in grp.iter_mut().zip(rest.iter()) {
*slot = *run;
}
self.overflow.insert((0x00, cnid, start_block), grp);
start_block += grp.iter().map(|e| e.1).sum::<u16>();
rest = &rest[take..];
}
}
Ok((inline, total_blocks))
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
for (s, c) in std::mem::take(&mut self.cat_extents) {
self.free_run(s, c);
}
for (s, c) in std::mem::take(&mut self.ext_extents) {
self.free_run(s, c);
}
let ext_records: Vec<Vec<u8>> = self
.overflow
.iter()
.map(|(&(fork, cnid, start), ext)| encode_extent_record(fork, cnid, start, ext))
.collect();
let ext_nodes = build_btree(&ext_records, 7)?;
let cat_records: Vec<Vec<u8>> = self
.catalog
.iter()
.map(|(k, body)| encode_leaf_record(k, body))
.collect();
let cat_nodes = build_btree(&cat_records, 37)?;
let bs = u64::from(self.block_size);
let ext_blocks = ((ext_nodes.len() * NODE) as u64).div_ceil(bs) as u16;
let cat_blocks = ((cat_nodes.len() * NODE) as u64).div_ceil(bs) as u16;
let ext_extents = self.alloc_btree_file(ext_blocks)?;
let cat_extents = self.alloc_btree_file(cat_blocks)?;
self.ext_extents = ext_extents;
self.ext_size = ext_blocks as u32 * self.block_size;
self.cat_extents = cat_extents;
self.cat_size = cat_blocks as u32 * self.block_size;
write_nodes_across(dev, self.alloc_base, bs, &ext_extents, &ext_nodes)?;
write_nodes_across(dev, self.alloc_base, bs, &cat_extents, &cat_nodes)?;
dev.write_at(self.vbm_start * 512, &self.bitmap)?;
let mdb = self.encode_mdb();
dev.write_at(1024, &mdb)?;
dev.write_at((self.total_sectors - 2) * 512, &mdb)?;
dev.sync()?;
Ok(())
}
fn alloc_btree_file(&mut self, n: u16) -> Result<ExtRec> {
let runs = self.allocate(n)?;
if runs.len() > 3 {
return Err(Error::Unsupported(
"hfs: B-tree file needs more than 3 extents (volume too fragmented)".into(),
));
}
let mut ext: ExtRec = [(0, 0); 3];
for (slot, run) in ext.iter_mut().zip(runs) {
*slot = run;
}
Ok(ext)
}
fn encode_mdb(&self) -> [u8; 162] {
let mut m = [0u8; 162];
m[0..2].copy_from_slice(b"BD"); put_u32(&mut m, 2, self.create_date); put_u32(&mut m, 6, self.create_date); put_u16(&mut m, 10, 1 << 8); put_u16(&mut m, 12, self.root_valence()); put_u16(&mut m, 14, self.vbm_start as u16); put_u16(&mut m, 16, self.next_alloc); put_u16(&mut m, 18, self.total_blocks); put_u32(&mut m, 20, self.block_size); put_u32(&mut m, 24, self.block_size); put_u16(&mut m, 28, (self.alloc_base / 512) as u16); put_u32(&mut m, 30, self.next_cnid); put_u16(&mut m, 34, self.free_blocks); let (files, dirs, root_dirs) = self.counts();
put_u16(&mut m, 82, root_dirs);
put_u32(&mut m, 84, files);
put_u32(&mut m, 88, dirs);
let vn = macroman::encode(&self.volume_name).unwrap_or_default();
m[36] = vn.len() as u8;
m[37..37 + vn.len()].copy_from_slice(&vn);
put_u32(&mut m, 130, self.ext_size);
put_ext(&mut m, 134, &self.ext_extents);
put_u32(&mut m, 146, self.cat_size);
put_ext(&mut m, 150, &self.cat_extents);
m
}
}
fn encode_component(comp: &str) -> Result<Vec<u8>> {
macroman::encode(&comp.replace(':', "/"))
}
fn encode_dir(cnid: u32, valence: u16, mac_time: u32) -> Vec<u8> {
let mut d = vec![0u8; 70];
d[0] = CDR_DIR;
put_u16(&mut d, 4, valence); put_u32(&mut d, 6, cnid); put_u32(&mut d, 10, mac_time); put_u32(&mut d, 14, mac_time); d
}
fn encode_file(
cnid: u32,
size: u64,
mac_time: u32,
extents: &ExtRec,
total_blocks: u16,
block_size: u32,
) -> Vec<u8> {
let mut d = vec![0u8; 102];
d[0] = CDR_FILE;
put_u32(&mut d, 20, cnid); put_u32(&mut d, 26, size as u32); put_u32(&mut d, 30, u32::from(total_blocks) * block_size); put_u32(&mut d, 44, mac_time); put_u32(&mut d, 48, mac_time); put_ext(&mut d, 74, &[extents[0], extents[1], extents[2]]); d
}
fn encode_thread(parent: u32, name: &[u8]) -> Vec<u8> {
let n = name.len().min(31);
let mut t = vec![0u8; 46];
t[0] = CDR_DIR_THREAD;
put_u32(&mut t, 10, parent); t[14] = n as u8; t[15..15 + n].copy_from_slice(&name[..n]);
t
}
fn put_ext(v: &mut [u8], o: usize, e: &[(u16, u16); 3]) {
for (i, &(s, c)) in e.iter().enumerate() {
put_u16(v, o + i * 4, s);
put_u16(v, o + i * 4 + 2, c);
}
}
fn encode_leaf_record(key: &OwnedKey, body: &[u8]) -> Vec<u8> {
let key_len = 6 + key.name.len();
let mut r = vec![0u8; round_up_even(1 + key_len)];
r[0] = key_len as u8;
put_u32(&mut r, 2, key.parid);
r[6] = key.name.len() as u8;
r[7..7 + key.name.len()].copy_from_slice(&key.name);
r.extend_from_slice(body);
if !r.len().is_multiple_of(2) {
r.push(0);
}
r
}
fn encode_extent_record(fork: u8, cnid: u32, start: u16, ext: &ExtRec) -> Vec<u8> {
let mut r = vec![0u8; round_up_even(1 + 7)];
r[0] = 7; r[1] = fork; put_u32(&mut r, 2, cnid); put_u16(&mut r, 6, start); let mut body = vec![0u8; 12];
put_ext(&mut body, 0, ext);
r.extend_from_slice(&body);
r
}
fn build_btree(records: &[Vec<u8>], key_len_max: u16) -> Result<Vec<[u8; NODE]>> {
if records.is_empty() {
return Ok(vec![write_header_node(0, 0, 0, 0, 0, 1, key_len_max)]);
}
let leaves = pack_level(records, ND_LEAF, 1);
let leaf_count = leaves.len();
let mut levels: Vec<Vec<PackedNode>> = vec![leaves];
let mut height = 2u8;
while levels.last().unwrap().len() > 1 {
let child_level = levels.last().unwrap();
let child_start = 1 + levels[..levels.len() - 1]
.iter()
.map(|l| l.len())
.sum::<usize>() as u32;
let idx_records: Vec<Vec<u8>> = child_level
.iter()
.enumerate()
.map(|(i, n)| {
encode_index_record(&n.first_key, child_start + i as u32, key_len_max as usize)
})
.collect();
let idx_nodes = pack_level(&idx_records, ND_INDEX, height);
levels.push(idx_nodes);
height += 1;
}
let total_nodes = 1 + levels.iter().map(|l| l.len()).sum::<usize>();
let mut nodes: Vec<[u8; NODE]> = Vec::with_capacity(total_nodes);
nodes.push([0u8; NODE]);
let mut node_no = 1u32;
let mut level_ranges: Vec<(u32, u32)> = Vec::new();
for level in &levels {
let start = node_no;
for (i, pn) in level.iter().enumerate() {
let prev = if i == 0 { 0 } else { node_no - 1 };
let next = if i + 1 == level.len() { 0 } else { node_no + 1 };
nodes.push(write_node(pn, prev, next));
node_no += 1;
}
level_ranges.push((start, node_no - 1));
}
let root = node_no - 1; let depth = levels.len() as u16;
let first_leaf = level_ranges[0].0;
let last_leaf = level_ranges[0].1;
let leaf_records = records.len() as u32;
nodes[0] = write_header_node(
depth,
root,
leaf_records,
first_leaf,
last_leaf,
total_nodes as u32,
key_len_max,
);
let _ = leaf_count;
Ok(nodes)
}
struct PackedNode {
first_key: Vec<u8>,
records: Vec<Vec<u8>>,
kind: u8,
height: u8,
}
fn pack_level(records: &[Vec<u8>], kind: u8, height: u8) -> Vec<PackedNode> {
let mut out = Vec::new();
let mut cur: Vec<Vec<u8>> = Vec::new();
let mut used = DESC;
for rec in records {
let with = used + rec.len() + 2 * (cur.len() + 2);
if with > NODE && !cur.is_empty() {
out.push(finish_node(&mut cur, kind, height));
used = DESC;
}
used += rec.len();
cur.push(rec.clone());
}
if !cur.is_empty() {
out.push(finish_node(&mut cur, kind, height));
}
out
}
fn finish_node(cur: &mut Vec<Vec<u8>>, kind: u8, height: u8) -> PackedNode {
let recs = std::mem::take(cur);
let first_key = key_of(&recs[0]);
PackedNode {
first_key,
records: recs,
kind,
height,
}
}
fn key_of(rec: &[u8]) -> Vec<u8> {
let kl = rec[0] as usize;
rec[1..1 + kl].to_vec()
}
fn encode_index_record(key_content: &[u8], child: u32, key_len: usize) -> Vec<u8> {
let mut r = vec![0u8; 1 + key_len + 4];
r[0] = key_len as u8;
let n = key_content.len().min(key_len);
r[1..1 + n].copy_from_slice(&key_content[..n]);
r[1 + key_len..].copy_from_slice(&child.to_be_bytes());
r
}
fn write_node(pn: &PackedNode, blink: u32, flink: u32) -> [u8; NODE] {
let mut node = [0u8; NODE];
put_u32(&mut node, 0, flink); put_u32(&mut node, 4, blink); node[8] = pn.kind; node[9] = pn.height; put_u16(&mut node, 10, pn.records.len() as u16); let mut off = DESC;
let mut offsets = vec![DESC as u16];
for rec in &pn.records {
node[off..off + rec.len()].copy_from_slice(rec);
off += rec.len();
offsets.push(off as u16);
}
for (i, &o) in offsets.iter().enumerate() {
put_u16(&mut node, NODE - 2 * (i + 1), o);
}
node
}
#[allow(clippy::too_many_arguments)]
fn write_header_node(
depth: u16,
root: u32,
leaf_records: u32,
first_leaf: u32,
last_leaf: u32,
total_nodes: u32,
key_len_max: u16,
) -> [u8; NODE] {
let mut node = [0u8; NODE];
node[8] = ND_HEADER; put_u16(&mut node, 10, 3); let h = DESC;
put_u16(&mut node, h, depth); put_u32(&mut node, h + 2, root); put_u32(&mut node, h + 6, leaf_records); put_u32(&mut node, h + 10, first_leaf); put_u32(&mut node, h + 14, last_leaf); put_u16(&mut node, h + 18, NODE as u16); put_u16(&mut node, h + 20, key_len_max); put_u32(&mut node, h + 22, total_nodes); put_u32(&mut node, h + 26, 0); let bm = 248;
for n in 0..total_nodes as usize {
node[bm + n / 8] |= 0x80 >> (n % 8);
}
put_u16(&mut node, NODE - 2, 14);
put_u16(&mut node, NODE - 4, 120);
put_u16(&mut node, NODE - 6, 248);
put_u16(&mut node, NODE - 8, 504);
node
}
fn write_nodes_across(
dev: &mut dyn BlockDevice,
alloc_base: u64,
bs: u64,
extents: &ExtRec,
nodes: &[[u8; NODE]],
) -> Result<()> {
let mut node_i = 0usize;
for &(start, count) in extents {
if count == 0 {
continue;
}
let run_off = alloc_base + u64::from(start) * bs;
let nodes_in_run = (u64::from(count) * bs / NODE as u64) as usize;
for slot in 0..nodes_in_run {
if node_i >= nodes.len() {
return Ok(());
}
dev.write_at(run_off + (slot * NODE) as u64, &nodes[node_i])?;
node_i += 1;
}
}
Ok(())
}
struct Geometry {
vbm_start: u64,
al_bl_st: u64,
total_blocks: u16,
}
impl Geometry {
fn compute(total_sectors: u64, block_size: u32) -> Result<Self> {
let spab = u64::from(block_size) / 512; let vbm_start = 3u64; let usable_end = total_sectors - 2;
let mut vbm_sectors = 1u64;
let mut total_blocks;
loop {
let al_bl_st = vbm_start + vbm_sectors;
if al_bl_st >= usable_end {
return Err(Error::InvalidArgument("hfs: volume too small".into()));
}
total_blocks = (usable_end - al_bl_st) / spab;
if total_blocks == 0 {
return Err(Error::InvalidArgument("hfs: volume too small".into()));
}
let need = total_blocks.div_ceil(4096); if need <= vbm_sectors {
let total_blocks = u16::try_from(total_blocks).map_err(|_| {
Error::InvalidArgument("hfs: too many allocation blocks".into())
})?;
return Ok(Geometry {
vbm_start,
al_bl_st,
total_blocks,
});
}
vbm_sectors = need;
}
}
}
fn auto_block_size(total_sectors: u64) -> u32 {
let mut bs = 512u32;
while total_sectors / u64::from(bs / 512) > 60_000 {
bs += 512;
}
bs
}
#[cfg(test)]
mod tests {
use super::*;
fn key(parid: u32, name: &str) -> OwnedKey {
OwnedKey {
parid,
name: name.as_bytes().to_vec(),
}
}
#[test]
fn build_btree_is_ordered_with_an_index_level() {
let mut entries: Vec<(OwnedKey, Vec<u8>)> = (0..90)
.map(|i| (key(2, &format!("file{i:03}")), vec![0u8; 40]))
.collect();
entries.push((key(2, ""), vec![3u8; 20]));
entries.sort_by(|a, b| a.0.cmp(&b.0));
let records: Vec<Vec<u8>> = entries
.iter()
.map(|(k, b)| encode_leaf_record(k, b))
.collect();
let nodes = build_btree(&records, 37).unwrap();
assert_eq!(nodes[0][8], ND_HEADER);
let depth = u16::from_be_bytes([nodes[0][14], nodes[0][15]]);
let bth_root = u32::from_be_bytes(nodes[0][16..20].try_into().unwrap());
let bth_fnode = u32::from_be_bytes(nodes[0][24..28].try_into().unwrap());
let nnodes = u32::from_be_bytes(nodes[0][36..40].try_into().unwrap());
assert_eq!(nnodes as usize, nodes.len(), "bthNNodes != node count");
assert!(depth >= 2, "expected an index level (depth {depth})");
let root = &nodes[bth_root as usize];
assert_eq!(root[8], ND_INDEX, "root should be an index node");
let rnrecs = u16::from_be_bytes([root[10], root[11]]) as usize;
for r in 0..rnrecs {
let off = u16::from_be_bytes([root[NODE - 2 * (r + 1)], root[NODE - 2 * (r + 1) + 1]])
as usize;
assert_eq!(
root[off], 37,
"index record key must be fixed at bthKeyLen=37"
);
let child = u32::from_be_bytes(root[off + 38..off + 42].try_into().unwrap());
assert!((child as usize) < nodes.len(), "index child out of range");
}
let mut all: Vec<(u32, Vec<u8>)> = Vec::new();
let mut idx = bth_fnode;
let mut leaves = 0;
while idx != 0 {
let node = &nodes[idx as usize];
assert_eq!(node[8], ND_LEAF, "node {idx} not a leaf");
leaves += 1;
let nrecs = u16::from_be_bytes([node[10], node[11]]) as usize;
for r in 0..nrecs {
let off =
u16::from_be_bytes([node[NODE - 2 * (r + 1)], node[NODE - 2 * (r + 1) + 1]])
as usize;
let klen = node[off] as usize;
let parid = u32::from_be_bytes(node[off + 2..off + 6].try_into().unwrap());
let nlen = node[off + 6] as usize;
let name = node[off + 7..off + 7 + nlen].to_vec();
let _ = klen;
all.push((parid, name));
}
idx = u32::from_be_bytes(node[0..4].try_into().unwrap());
}
assert!(leaves > 1, "expected multiple leaf nodes, got {leaves}");
assert_eq!(all.len(), records.len(), "lost records walking the chain");
for w in all.windows(2) {
let ord = w[0].0.cmp(&w[1].0).then(macroman::cmp_ci(&w[0].1, &w[1].1));
assert_eq!(ord, std::cmp::Ordering::Less, "keys out of order: {w:?}");
}
}
}