use crate::macroman;
mod writer;
pub use writer::HfsFormatOpts;
use std::collections::HashMap;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
use crate::block::BlockDevice;
use crate::fs::{DirEntry, EntryKind, FileAttrs, Filesystem, MutationCapability};
use crate::{Error, Result};
const MDB_OFFSET: u64 = 1024;
const NODE_SIZE: usize = 512;
const ROOT_CNID: u32 = 2;
const MAC_EPOCH_DELTA: u32 = 2_082_844_800;
const MAX_TREE_BYTES: u64 = 64 * 1024 * 1024;
#[inline]
fn be16(b: &[u8], o: usize) -> u16 {
u16::from_be_bytes([b[o], b[o + 1]])
}
#[inline]
fn be32(b: &[u8], o: usize) -> u32 {
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
#[inline]
fn round_up_even(x: usize) -> usize {
(x + 1) & !1
}
type ExtRec = [(u16, u16); 3];
fn ext_rec(b: &[u8], o: usize) -> ExtRec {
[
(be16(b, o), be16(b, o + 2)),
(be16(b, o + 4), be16(b, o + 6)),
(be16(b, o + 8), be16(b, o + 10)),
]
}
struct Node {
name: String,
cnid: u32,
is_dir: bool,
size: u64,
mtime: u32,
inline: ExtRec,
rsrc_size: u64,
rsrc_inline: ExtRec,
}
pub struct Hfs {
block_size: u32,
alloc_base: u64,
pub volume_name: String,
children: HashMap<u32, Vec<Node>>,
overflow: HashMap<(u8, u32, u16), ExtRec>,
writer: Option<writer::HfsWriter>,
}
impl Hfs {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
let dev_len = dev.total_size();
if dev_len < MDB_OFFSET + 512 {
return Err(Error::InvalidImage(
"hfs: device too small for an MDB".into(),
));
}
let mut m = [0u8; 512];
dev.read_at(MDB_OFFSET, &mut m)?;
if &m[0..2] != b"BD" {
return Err(Error::InvalidImage(
"hfs: bad MDB signature (expected BD)".into(),
));
}
if be16(&m, 0x7C) == 0x482B {
return Err(Error::Unsupported(
"hfs: HFS-wrapped HFS+ volume is not supported (embedded H+)".into(),
));
}
let block_size = be32(&m, 20);
let al_bl_st = be16(&m, 28) as u64;
if block_size == 0 || !block_size.is_multiple_of(512) {
return Err(Error::InvalidImage(format!(
"hfs: bad allocation block size {block_size}"
)));
}
let alloc_base = al_bl_st * 512;
let vn_len = (m[36] as usize).min(27);
let volume_name = macroman::decode(&m[37..37 + vn_len]);
let xt_size = be32(&m, 130) as u64;
let xt_ext = ext_rec(&m, 134);
let ct_size = be32(&m, 146) as u64;
let ct_ext = ext_rec(&m, 150);
for (sz, what) in [(xt_size, "extents"), (ct_size, "catalog")] {
if sz > MAX_TREE_BYTES || sz > dev_len {
return Err(Error::InvalidImage(format!(
"hfs: {what} file size {sz} is implausible"
)));
}
}
let mut hfs = Hfs {
block_size,
alloc_base,
volume_name,
children: HashMap::new(),
overflow: HashMap::new(),
writer: None,
};
let xt_bytes = hfs.read_fork(dev, &xt_ext, xt_size, dev_len)?;
hfs.build_overflow(&xt_bytes)?;
let cat_ext = hfs.full_extents(4, 0, &ct_ext, ct_size);
let cat_bytes = hfs.read_fork(dev, &cat_ext, ct_size, dev_len)?;
hfs.build_catalog(&cat_bytes)?;
Ok(hfs)
}
fn fork_ranges(
&self,
extents: &[(u16, u16)],
logical: u64,
dev_len: u64,
) -> Result<Vec<(u64, u64)>> {
let mut out = Vec::new();
let mut acc = 0u64;
for &(sb, bc) in extents {
if bc == 0 || acc >= logical {
continue;
}
let off = self
.alloc_base
.checked_add(sb as u64 * self.block_size as u64)
.ok_or_else(|| Error::InvalidImage("hfs: extent offset overflow".into()))?;
let mut len = bc as u64 * self.block_size as u64;
if acc + len > logical {
len = logical - acc;
}
if off.checked_add(len).is_none_or(|e| e > dev_len) {
return Err(Error::InvalidImage("hfs: extent past end of device".into()));
}
out.push((off, len));
acc += len;
}
Ok(out)
}
fn read_fork(
&self,
dev: &mut dyn BlockDevice,
extents: &[(u16, u16)],
logical: u64,
dev_len: u64,
) -> Result<Vec<u8>> {
let ranges = self.fork_ranges(extents, logical, dev_len)?;
let mut out = vec![0u8; logical as usize];
let mut pos = 0usize;
for (off, len) in ranges {
let len = len as usize;
dev.read_at(off, &mut out[pos..pos + len])?;
pos += len;
}
Ok(out)
}
fn full_extents(
&self,
cnid: u32,
fork_type: u8,
inline: &ExtRec,
logical: u64,
) -> Vec<(u16, u16)> {
let mut exts: Vec<(u16, u16)> = inline.iter().copied().filter(|&(_, c)| c != 0).collect();
let mut covered: u32 = exts.iter().map(|&(_, c)| c as u32).sum();
let need = logical.div_ceil(self.block_size.max(1) as u64) as u32;
let mut guard = 0u32;
while covered < need {
guard += 1;
if guard > 8192 {
break;
}
let Some(rec) = self.overflow.get(&(fork_type, cnid, covered as u16)) else {
break;
};
let mut progressed = false;
for &(s, c) in rec {
if c != 0 {
exts.push((s, c));
covered += c as u32;
progressed = true;
}
}
if !progressed {
break;
}
}
exts
}
fn walk_leaves<F>(buf: &[u8], mut f: F) -> Result<()>
where
F: FnMut(&[u8], &[u8]) -> Result<()>,
{
if buf.len() < NODE_SIZE {
return Ok(());
}
let first_leaf = be32(buf, 24);
let n_nodes = be32(buf, 36);
let total_nodes = (buf.len() / NODE_SIZE) as u32;
let mut node_idx = first_leaf;
let mut steps = n_nodes.min(total_nodes).max(1) as u64 + 1;
while node_idx != 0 {
if steps == 0 {
return Err(Error::InvalidImage("hfs: B-tree leaf chain cycle".into()));
}
steps -= 1;
if node_idx >= total_nodes {
return Err(Error::InvalidImage(
"hfs: leaf node index out of range".into(),
));
}
let start = node_idx as usize * NODE_SIZE;
let node = &buf[start..start + NODE_SIZE];
let flink = be32(node, 0);
let ntype = node[8];
let nrecs = be16(node, 10) as usize;
if ntype != 0xFF {
break; }
for i in 0..nrecs {
let lo = NODE_SIZE - 2 * (i + 1);
let o = be16(node, lo) as usize;
let o2 = be16(node, lo - 2) as usize;
if o < 14 || o2 > NODE_SIZE || o >= o2 {
continue;
}
let rec = &node[o..o2];
let key_len = rec[0] as usize;
let key_total = round_up_even(1 + key_len);
if 1 + key_len > rec.len() || key_total > rec.len() {
continue;
}
f(&rec[1..1 + key_len], &rec[key_total..])?;
}
node_idx = flink;
}
Ok(())
}
fn build_overflow(&mut self, buf: &[u8]) -> Result<()> {
let mut map = HashMap::new();
Self::walk_leaves(buf, |key, data| {
if key.len() < 7 || data.len() < 12 {
return Ok(());
}
let fork_type = key[0];
let cnid = be32(key, 1);
let start_abn = be16(key, 5);
map.insert((fork_type, cnid, start_abn), ext_rec(data, 0));
Ok(())
})?;
self.overflow = map;
Ok(())
}
fn build_catalog(&mut self, buf: &[u8]) -> Result<()> {
let mut children: HashMap<u32, Vec<Node>> = HashMap::new();
Self::walk_leaves(buf, |key, data| {
if key.len() < 6 || data.is_empty() {
return Ok(());
}
let parent = be32(key, 1);
let name_len = (key[5] as usize).min(31);
if 6 + name_len > key.len() {
return Ok(());
}
let name = macroman::decode(&key[6..6 + name_len]).replace('/', ":");
match data[0] {
1 if data.len() >= 18 => {
children.entry(parent).or_default().push(Node {
name,
cnid: be32(data, 6),
is_dir: true,
size: 0,
mtime: be32(data, 14),
inline: [(0, 0); 3],
rsrc_size: 0,
rsrc_inline: [(0, 0); 3],
});
}
2 if data.len() >= 98 => {
children.entry(parent).or_default().push(Node {
name,
cnid: be32(data, 20),
is_dir: false,
size: be32(data, 26) as u64,
mtime: be32(data, 48),
inline: ext_rec(data, 74),
rsrc_size: be32(data, 36) as u64,
rsrc_inline: ext_rec(data, 86),
});
}
_ => {} }
Ok(())
})?;
self.children = children;
Ok(())
}
fn resolve(&self, path: &str) -> Option<Resolved<'_>> {
let mut cnid = ROOT_CNID;
let comps: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
if comps.is_empty() {
return Some(Resolved::Dir(ROOT_CNID));
}
for (i, comp) in comps.iter().enumerate() {
let kids = self.children.get(&cnid)?;
let node = kids
.iter()
.find(|n| macroman::eq_ignore_case(&n.name, comp))?;
let last = i + 1 == comps.len();
if node.is_dir {
cnid = node.cnid;
if last {
return Some(Resolved::Dir(node.cnid));
}
} else if last {
return Some(Resolved::File(node));
} else {
return None; }
}
Some(Resolved::Dir(cnid))
}
pub fn list_path(&self, path: &str) -> Result<Vec<DirEntry>> {
let cnid = match self.resolve(path) {
Some(Resolved::Dir(c)) => c,
Some(Resolved::File(_)) => {
return Err(Error::InvalidArgument(format!(
"hfs: {path:?} is not a directory"
)));
}
None => {
return Err(Error::InvalidArgument(format!(
"hfs: no such path {path:?}"
)));
}
};
let mut out = Vec::new();
if let Some(kids) = self.children.get(&cnid) {
for n in kids {
out.push(DirEntry {
name: n.name.clone(),
inode: n.cnid,
kind: if n.is_dir {
EntryKind::Dir
} else {
EntryKind::Regular
},
size: n.size,
});
}
}
Ok(out)
}
pub fn open_file_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<HfsFileReader<'a>> {
let (cnid, size, inline) = match self.resolve(path) {
Some(Resolved::File(n)) => (n.cnid, n.size, n.inline),
Some(Resolved::Dir(_)) => {
return Err(Error::InvalidArgument(format!(
"hfs: {path:?} is a directory"
)));
}
None => {
return Err(Error::InvalidArgument(format!(
"hfs: no such file {path:?}"
)));
}
};
let dev_len = dev.total_size();
let extents = self.full_extents(cnid, 0, &inline, size);
let ranges = self.fork_ranges(&extents, size, dev_len)?;
Ok(HfsFileReader {
dev,
ranges,
total: size,
pos: 0,
})
}
pub fn open_resource_fork_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<HfsFileReader<'a>> {
let (cnid, size, inline) = match self.resolve(path) {
Some(Resolved::File(n)) => (n.cnid, n.rsrc_size, n.rsrc_inline),
Some(Resolved::Dir(_)) => {
return Err(Error::InvalidArgument(format!(
"hfs: {path:?} is a directory"
)));
}
None => {
return Err(Error::InvalidArgument(format!(
"hfs: no such file {path:?}"
)));
}
};
if size == 0 {
return Err(Error::InvalidArgument(format!(
"hfs: {path:?} has no resource fork"
)));
}
let dev_len = dev.total_size();
let extents = self.full_extents(cnid, 0xFF, &inline, size);
let ranges = self.fork_ranges(&extents, size, dev_len)?;
Ok(HfsFileReader {
dev,
ranges,
total: size,
pos: 0,
})
}
pub fn resource_fork_size(&self, path: &str) -> Option<u64> {
match self.resolve(path) {
Some(Resolved::File(n)) if n.rsrc_size > 0 => Some(n.rsrc_size),
_ => None,
}
}
pub fn format(dev: &mut dyn BlockDevice, opts: &writer::HfsFormatOpts) -> Result<Self> {
let w = writer::HfsWriter::format(dev, opts)?;
Ok(Self::from_writer(w))
}
pub fn open_writable(dev: &mut dyn BlockDevice) -> Result<Self> {
let ro = Hfs::open(dev)?;
let dev_len = dev.total_size();
let total_sectors = dev_len / 512;
let mut m = [0u8; 512];
dev.read_at(MDB_OFFSET, &mut m)?;
let block_size = be32(&m, 20);
let vbm_start = be16(&m, 14) as u64;
let total_blocks = be16(&m, 18);
let free_blocks = be16(&m, 34);
let next_cnid = be32(&m, 30).max(16);
let cat_ext = ext_rec(&m, 150);
let cat_size = be32(&m, 146);
let xt_ext = ext_rec(&m, 134);
let xt_size = be32(&m, 130);
let mut bitmap = vec![0u8; (total_blocks as usize).div_ceil(8)];
dev.read_at(vbm_start * 512, &mut bitmap)?;
let cat_extents = ro.full_extents(4, 0, &cat_ext, cat_size as u64);
let cat_bytes = ro.read_fork(dev, &cat_extents, cat_size as u64, dev_len)?;
let mut catalog = std::collections::BTreeMap::new();
Self::walk_leaves(&cat_bytes, |key, data| {
if key.len() < 6 || data.is_empty() {
return Ok(());
}
let parid = be32(key, 1);
let name_len = (key[5] as usize).min(31);
if 6 + name_len > key.len() {
return Ok(());
}
catalog.insert(
writer::OwnedKey {
parid,
name: key[6..6 + name_len].to_vec(),
},
data.to_vec(),
);
Ok(())
})?;
let overflow: std::collections::BTreeMap<_, _> =
ro.overflow.iter().map(|(&k, &v)| (k, v)).collect();
let w = writer::HfsWriter::adopt(
block_size,
ro.alloc_base,
vbm_start,
total_blocks,
total_sectors,
bitmap,
free_blocks,
next_cnid,
catalog,
overflow,
ro.volume_name.clone(),
be32(&m, 2),
cat_ext,
cat_size,
xt_ext,
xt_size,
);
Ok(Self::from_writer(w))
}
fn from_writer(w: writer::HfsWriter) -> Self {
let mut hfs = Hfs {
block_size: w.block_size,
alloc_base: w.alloc_base,
volume_name: w.volume_name.clone(),
children: HashMap::new(),
overflow: HashMap::new(),
writer: Some(w),
};
hfs.rebuild_index();
hfs
}
fn rebuild_index(&mut self) {
let Some(w) = self.writer.as_ref() else {
return;
};
let mut children: HashMap<u32, Vec<Node>> = HashMap::new();
for (key, body) in &w.catalog {
if body.is_empty() {
continue;
}
let name = macroman::decode(&key.name).replace('/', ":");
match body[0] {
1 if body.len() >= 18 => {
children.entry(key.parid).or_default().push(Node {
name,
cnid: be32(body, 6),
is_dir: true,
size: 0,
mtime: be32(body, 14),
inline: [(0, 0); 3],
rsrc_size: 0,
rsrc_inline: [(0, 0); 3],
});
}
2 if body.len() >= 98 => {
children.entry(key.parid).or_default().push(Node {
name,
cnid: be32(body, 20),
is_dir: false,
size: be32(body, 26) as u64,
mtime: be32(body, 48),
inline: ext_rec(body, 74),
rsrc_size: be32(body, 36) as u64,
rsrc_inline: ext_rec(body, 86),
});
}
_ => {}
}
}
self.children = children;
self.overflow = w.overflow.iter().map(|(&k, &v)| (k, v)).collect();
}
}
enum Resolved<'a> {
Dir(u32),
File(&'a Node),
}
pub struct HfsFileReader<'a> {
dev: &'a mut dyn BlockDevice,
ranges: Vec<(u64, u64)>,
total: u64,
pos: u64,
}
impl HfsFileReader<'_> {
fn locate(&self, pos: u64) -> Option<(u64, u64)> {
let mut walked = 0u64;
for &(off, len) in &self.ranges {
if pos < walked + len {
let within = pos - walked;
return Some((off + within, len - within));
}
walked += len;
}
None
}
}
impl Read for HfsFileReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.pos >= self.total || buf.is_empty() {
return Ok(0);
}
let (off, avail) = match self.locate(self.pos) {
Some(v) => v,
None => return Ok(0),
};
let want = (buf.len() as u64).min(avail).min(self.total - self.pos) as usize;
self.dev
.read_at(off, &mut buf[..want])
.map_err(io::Error::other)?;
self.pos += want as u64;
Ok(want)
}
}
impl Seek for HfsFileReader<'_> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let new = match pos {
SeekFrom::Start(n) => n as i128,
SeekFrom::End(d) => self.total as i128 + d as i128,
SeekFrom::Current(d) => self.pos as i128 + d as i128,
};
if new < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"hfs: seek before start",
));
}
self.pos = new as u64;
Ok(self.pos)
}
}
impl crate::fs::FileReadHandle for HfsFileReader<'_> {
fn len(&self) -> u64 {
self.total
}
}
impl crate::fs::FilesystemFactory for Hfs {
type FormatOpts = writer::HfsFormatOpts;
fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
Self::format(dev, opts)
}
fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
Self::open(dev)
}
}
impl Filesystem for Hfs {
fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
src: crate::fs::FileSource,
meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
let w = self.writer.as_mut().ok_or(Error::Immutable {
kind: "hfs",
op: "add",
})?;
let len = src.len()?;
let (mut reader, _) = src.open()?;
w.insert_file(dev, s, &mut reader, len, meta.mtime)?;
self.rebuild_index();
Ok(())
}
fn create_dir(
&mut self,
_dev: &mut dyn BlockDevice,
path: &Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
let w = self.writer.as_mut().ok_or(Error::Immutable {
kind: "hfs",
op: "mkdir",
})?;
w.insert_dir(s, meta.mtime)?;
self.rebuild_index();
Ok(())
}
fn create_symlink(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &Path,
_target: &Path,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(Error::Unsupported(
"hfs: classic HFS has no symbolic links".into(),
))
}
fn create_device(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &Path,
_kind: crate::fs::DeviceKind,
_major: u32,
_minor: u32,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(Error::Immutable {
kind: "hfs",
op: "mknod",
})
}
fn remove(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
let w = self.writer.as_mut().ok_or(Error::Immutable {
kind: "hfs",
op: "rm",
})?;
w.remove(s)?;
self.rebuild_index();
Ok(())
}
fn list(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<DirEntry>> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
self.list_path(s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
) -> Result<Box<dyn Read + 'a>> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
Ok(Box::new(self.open_file_reader(dev, s)?))
}
fn open_file_ro<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
Ok(Box::new(self.open_file_reader(dev, s)?))
}
fn getattr(&mut self, _dev: &mut dyn BlockDevice, path: &Path) -> Result<FileAttrs> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
let (kind, size, mtime, inode) = match self.resolve(s) {
Some(Resolved::Dir(cnid)) => (EntryKind::Dir, 0u64, 0u32, cnid),
Some(Resolved::File(n)) => (EntryKind::Regular, n.size, n.mtime, n.cnid),
None => return Err(Error::InvalidArgument(format!("hfs: no such path {s:?}"))),
};
let mtime = mtime.saturating_sub(MAC_EPOCH_DELTA);
Ok(FileAttrs {
kind,
mode: if kind == EntryKind::Dir { 0o755 } else { 0o644 },
uid: 0,
gid: 0,
size,
blocks: size.div_ceil(512),
nlink: if kind == EntryKind::Dir { 2 } else { 1 },
atime: mtime,
mtime,
ctime: mtime,
rdev: 0,
inode,
})
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
if let Some(w) = self.writer.as_mut() {
w.flush(dev)?;
self.rebuild_index();
}
Ok(())
}
fn list_xattrs(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
) -> Result<Vec<crate::fs::XattrPair>> {
let s = path
.to_str()
.ok_or_else(|| Error::InvalidArgument("hfs: non-UTF-8 path".into()))?;
if self.resource_fork_size(s).is_none() {
return Ok(Vec::new());
}
const CAP: u64 = 16 * 1024 * 1024;
let r = self.open_resource_fork_reader(dev, s)?;
let mut buf = Vec::new();
r.take(CAP).read_to_end(&mut buf)?;
Ok(vec![crate::fs::XattrPair {
name: "com.apple.ResourceFork".into(),
value: buf,
}])
}
fn mutation_capability(&self) -> MutationCapability {
if self.writer.is_some() {
MutationCapability::Mutable
} else {
MutationCapability::Immutable
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
const HELLO: &[u8] = b"Hello from classic HFS!\n";
const DEEP: &[u8] = b"Deep file in sub.\n";
const RSRC: &[u8] = b"resource fork payload bytes\n";
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 put_ext(v: &mut [u8], o: usize, e: &[(u16, u16)]) {
for (i, &(s, c)) in e.iter().enumerate() {
put_u16(v, o + i * 4, s);
put_u16(v, o + i * 4 + 2, c);
}
}
fn file_record(cnid: u32, size: u32, start_block: u16) -> Vec<u8> {
let mut d = vec![0u8; 102];
d[0] = 2; d[4..8].copy_from_slice(b"TEXT"); d[8..12].copy_from_slice(b"ttxt"); d[20..24].copy_from_slice(&cnid.to_be_bytes()); d[26..30].copy_from_slice(&size.to_be_bytes()); d[30..34].copy_from_slice(&512u32.to_be_bytes()); put_ext(&mut d, 74, &[(start_block, 1), (0, 0), (0, 0)]); d
}
fn file_record_with_rsrc(
cnid: u32,
size: u32,
start_block: u16,
rsrc_size: u32,
rsrc_block: u16,
) -> Vec<u8> {
let mut d = file_record(cnid, size, start_block);
d[36..40].copy_from_slice(&rsrc_size.to_be_bytes()); d[40..44].copy_from_slice(&512u32.to_be_bytes()); put_ext(&mut d, 86, &[(rsrc_block, 1), (0, 0), (0, 0)]); d
}
fn dir_record(cnid: u32) -> Vec<u8> {
let mut d = vec![0u8; 70];
d[0] = 1; d[6..10].copy_from_slice(&cnid.to_be_bytes()); d
}
fn leaf_record(parent: u32, name: &[u8], data: &[u8]) -> Vec<u8> {
let key_len = 6 + name.len();
let mut r = vec![0u8; 1 + key_len];
r[0] = key_len as u8;
r[2..6].copy_from_slice(&parent.to_be_bytes());
r[6] = name.len() as u8;
r[7..7 + name.len()].copy_from_slice(name);
r.resize(round_up_even(1 + key_len), 0); r.extend_from_slice(data);
r
}
fn build_volume() -> Vec<u8> {
let block = 512usize;
let al_bl_st = 4u16;
let num_alloc_blocks = 6usize;
let mut v = vec![0u8; (al_bl_st as usize + num_alloc_blocks + 1) * block];
let mdb = 1024;
v[mdb..mdb + 2].copy_from_slice(b"BD");
put_u32(&mut v, mdb + 20, block as u32); put_u16(&mut v, mdb + 28, al_bl_st); v[mdb + 36] = 7;
v[mdb + 37..mdb + 44].copy_from_slice(b"TestVol"); put_u32(&mut v, mdb + 130, block as u32); put_ext(&mut v, mdb + 134, &[(2, 1), (0, 0), (0, 0)]); put_u32(&mut v, mdb + 146, (2 * block) as u32); put_ext(&mut v, mdb + 150, &[(0, 2), (0, 0), (0, 0)]);
let xt = (al_bl_st as usize + 2) * block;
v[xt + 8] = 1; put_u16(&mut v, xt + 32, 512); put_u32(&mut v, xt + 36, 1);
let cat = al_bl_st as usize * block;
v[cat + 8] = 1; put_u32(&mut v, cat + 24, 1); put_u16(&mut v, cat + 32, 512); put_u32(&mut v, cat + 36, 2); let leaf = cat + 512;
v[leaf + 8] = 0xFF; v[leaf + 9] = 1; let recs = [
leaf_record(
2,
b"hello.txt",
&file_record_with_rsrc(17, HELLO.len() as u32, 3, RSRC.len() as u32, 5),
),
leaf_record(2, b"sub", &dir_record(16)),
leaf_record(16, b"deep.txt", &file_record(18, DEEP.len() as u32, 4)),
leaf_record(2, b"A/B", &file_record(19, HELLO.len() as u32, 3)),
];
let mut pos = 14usize;
let mut offs = vec![14u16];
for r in &recs {
v[leaf + pos..leaf + pos + r.len()].copy_from_slice(r);
pos += r.len();
offs.push(pos as u16);
}
put_u16(&mut v, leaf + 10, recs.len() as u16); for (i, &o) in offs.iter().enumerate() {
put_u16(&mut v, leaf + 512 - 2 * (i + 1), o);
}
let hd = (al_bl_st as usize + 3) * block;
v[hd..hd + HELLO.len()].copy_from_slice(HELLO);
let dd = (al_bl_st as usize + 4) * block;
v[dd..dd + DEEP.len()].copy_from_slice(DEEP);
let rd = (al_bl_st as usize + 5) * block;
v[rd..rd + RSRC.len()].copy_from_slice(RSRC);
v
}
fn dev_from(bytes: &[u8]) -> MemoryBackend {
let mut dev = MemoryBackend::new(bytes.len() as u64);
dev.write_at(0, bytes).unwrap();
dev
}
fn read_all(fs: &mut Hfs, dev: &mut dyn BlockDevice, path: &str) -> Vec<u8> {
let mut r = fs.read_file(dev, Path::new(path)).unwrap();
let mut out = Vec::new();
std::io::Read::read_to_end(&mut r, &mut out).unwrap();
out
}
#[test]
fn writer_round_trip() {
let mut dev = MemoryBackend::new(4 * 1024 * 1024);
let opts = super::writer::HfsFormatOpts {
volume_name: "WriteTest".into(),
block_size: None,
};
let mut w = super::writer::HfsWriter::format(&mut dev, &opts).unwrap();
w.insert_dir("/sub", 0).unwrap();
let hello = b"hello from the HFS writer\n";
w.insert_file(
&mut dev,
"/hello.txt",
&mut &hello[..],
hello.len() as u64,
0,
)
.unwrap();
w.insert_file(&mut dev, "/sub/deep.txt", &mut &b"deep\n"[..], 5, 0)
.unwrap();
w.flush(&mut dev).unwrap();
let mut fs = Hfs::open(&mut dev).unwrap();
assert_eq!(fs.volume_name, "WriteTest");
let root: Vec<_> = fs
.list(&mut dev, Path::new("/"))
.unwrap()
.into_iter()
.map(|e| (e.name, e.kind))
.collect();
assert!(
root.contains(&("hello.txt".into(), EntryKind::Regular)),
"root = {root:?}"
);
assert!(
root.contains(&("sub".into(), EntryKind::Dir)),
"root = {root:?}"
);
let sub: Vec<_> = fs
.list(&mut dev, Path::new("/sub"))
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(sub, vec!["deep.txt"]);
assert_eq!(read_all(&mut fs, &mut dev, "/hello.txt"), hello);
assert_eq!(read_all(&mut fs, &mut dev, "/sub/deep.txt"), b"deep\n");
}
#[test]
fn in_place_mutation_round_trip() {
use crate::fs::{FileMeta, FileSource, Filesystem};
let mut dev = MemoryBackend::new(4 * 1024 * 1024);
let opts = super::writer::HfsFormatOpts {
volume_name: "InPlace".into(),
block_size: None,
};
{
let mut w = super::writer::HfsWriter::format(&mut dev, &opts).unwrap();
w.insert_dir("/keep", 0).unwrap();
w.insert_file(&mut dev, "/old.txt", &mut &b"old\n"[..], 4, 0)
.unwrap();
w.insert_file(&mut dev, "/keep/inner.txt", &mut &b"inner\n"[..], 6, 0)
.unwrap();
w.flush(&mut dev).unwrap();
}
let meta = FileMeta {
mode: 0o644,
uid: 0,
gid: 0,
mtime: 0,
atime: 0,
ctime: 0,
};
let mut fs = Hfs::open_writable(&mut dev).unwrap();
let data = b"brand new content\n".to_vec();
fs.create_file(
&mut dev,
Path::new("/new.txt"),
FileSource::Reader {
reader: Box::new(std::io::Cursor::new(data.clone())),
len: data.len() as u64,
},
meta,
)
.unwrap();
fs.create_dir(&mut dev, Path::new("/newdir"), meta).unwrap();
fs.remove(&mut dev, Path::new("/old.txt")).unwrap();
fs.flush(&mut dev).unwrap();
let mut ro = Hfs::open(&mut dev).unwrap();
let root: Vec<String> = ro
.list(&mut dev, Path::new("/"))
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(root.contains(&"new.txt".to_string()), "{root:?}");
assert!(root.contains(&"newdir".to_string()), "{root:?}");
assert!(root.contains(&"keep".to_string()), "{root:?}");
assert!(
!root.contains(&"old.txt".to_string()),
"old.txt should be gone: {root:?}"
);
assert_eq!(read_all(&mut ro, &mut dev, "/new.txt"), data);
assert_eq!(read_all(&mut ro, &mut dev, "/keep/inner.txt"), b"inner\n");
}
#[test]
fn resource_fork_round_trip() {
let vol = build_volume();
let mut dev = dev_from(&vol);
let mut fs = Hfs::open(&mut dev).unwrap();
let mut r = fs
.open_resource_fork_reader(&mut dev, "/hello.txt")
.unwrap();
let mut got = Vec::new();
std::io::Read::read_to_end(&mut r, &mut got).unwrap();
assert_eq!(got, RSRC);
assert_eq!(read_all(&mut fs, &mut dev, "/hello.txt"), HELLO);
let xattrs = fs.list_xattrs(&mut dev, Path::new("/hello.txt")).unwrap();
assert_eq!(xattrs.len(), 1);
assert_eq!(xattrs[0].name, "com.apple.ResourceFork");
assert_eq!(xattrs[0].value, RSRC);
assert!(
fs.open_resource_fork_reader(&mut dev, "/sub/deep.txt")
.is_err()
);
assert!(
fs.list_xattrs(&mut dev, Path::new("/sub/deep.txt"))
.unwrap()
.is_empty()
);
}
#[test]
fn synthetic_volume_round_trip() {
let vol = build_volume();
let mut dev = dev_from(&vol);
let mut fs = Hfs::open(&mut dev).unwrap();
assert_eq!(fs.volume_name, "TestVol");
let root: Vec<_> = fs
.list(&mut dev, Path::new("/"))
.unwrap()
.into_iter()
.map(|e| (e.name, e.kind))
.collect();
assert!(root.contains(&("hello.txt".into(), EntryKind::Regular)));
assert!(root.contains(&("sub".into(), EntryKind::Dir)));
assert!(
root.contains(&("A:B".into(), EntryKind::Regular)),
"expected canonical 'A:B' in root: {root:?}"
);
let sub: Vec<_> = fs
.list(&mut dev, Path::new("/sub"))
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(sub, vec!["deep.txt"]);
assert_eq!(read_all(&mut fs, &mut dev, "/hello.txt"), HELLO);
assert_eq!(read_all(&mut fs, &mut dev, "/sub/deep.txt"), DEEP);
assert_eq!(read_all(&mut fs, &mut dev, "/A:B"), HELLO);
}
#[test]
fn diskcopy_wrapped_hfs_via_anyfs() {
let vol = build_volume();
let mut img = vec![0u8; 0x54];
img[0x40..0x44].copy_from_slice(&(vol.len() as u32).to_be_bytes()); img[0x50] = 3; img[0x52] = 0x01; img.extend_from_slice(&vol);
let mem = dev_from(&img);
let mut dc = crate::block::DiskCopy42Backend::new(Box::new(mem)).unwrap();
let mut fs = crate::inspect::AnyFs::open(&mut dc).unwrap();
assert_eq!(fs.kind_string(), "hfs");
let names: Vec<_> = fs
.list(&mut dc, "/")
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert!(names.iter().any(|n| n == "hello.txt"));
let mut out = Vec::new();
fs.copy_file_to(&mut dc, "/sub/deep.txt", &mut out).unwrap();
assert_eq!(out, DEEP);
}
}