pub mod btree;
pub mod catalog;
pub mod extents;
pub mod volume_header;
pub mod writer;
pub use writer::FormatOpts;
use std::io::Read;
use crate::Result;
use crate::block::BlockDevice;
use btree::ForkReader;
use catalog::{Catalog, CatalogFile, CatalogKey, CatalogRecord, ROOT_FOLDER_ID, UniStr, mode};
use extents::ExtentsOverflow;
use volume_header::{ForkData, VolumeHeader, read_volume_header};
const SYMLINK_MAX_BYTES: u64 = 4096;
pub struct HfsPlus {
volume_header: VolumeHeader,
catalog: Catalog,
overflow: Option<ExtentsOverflow>,
private_dir_cnid: std::cell::Cell<Option<u32>>,
private_dir_resolved: std::cell::Cell<bool>,
volume_name: String,
writer: Option<writer::Writer>,
}
impl HfsPlus {
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
let vh = read_volume_header(dev)?;
let case_sensitive = vh.is_hfsx();
let cat_fork = ForkReader::from_inline(&vh.catalog_file, vh.block_size, "catalog")?;
let catalog = Catalog::open(dev, cat_fork, case_sensitive)?;
let overflow = if vh.extents_file.total_blocks > 0 {
let ext_fork =
ForkReader::from_inline(&vh.extents_file, vh.block_size, "extents-overflow")?;
Some(ExtentsOverflow::open(dev, ext_fork)?)
} else {
None
};
let volume_name = lookup_thread_name(dev, &catalog, ROOT_FOLDER_ID)?
.unwrap_or_else(|| "Untitled".to_string());
Ok(Self {
volume_header: vh,
catalog,
overflow,
private_dir_cnid: std::cell::Cell::new(None),
private_dir_resolved: std::cell::Cell::new(false),
volume_name,
writer: None,
})
}
pub fn format(dev: &mut dyn BlockDevice, opts: &writer::FormatOpts) -> Result<Self> {
let (vh, w) = writer::format(dev, opts)?;
let case_sensitive = vh.is_hfsx();
let mut w = w;
let mut vh_mut = vh.clone();
writer::flush(&mut w, &mut vh_mut, dev)?;
w.flushed = false;
let cat_fork = ForkReader::from_inline(&vh_mut.catalog_file, vh_mut.block_size, "catalog")?;
let catalog = Catalog::open(dev, cat_fork, case_sensitive)?;
let overflow = if vh_mut.extents_file.total_blocks > 0 {
let ext_fork = ForkReader::from_inline(
&vh_mut.extents_file,
vh_mut.block_size,
"extents-overflow",
)?;
Some(ExtentsOverflow::open(dev, ext_fork)?)
} else {
None
};
let volume_name = w.volume_name.clone();
Ok(Self {
volume_header: vh_mut,
catalog,
overflow,
private_dir_cnid: std::cell::Cell::new(None),
private_dir_resolved: std::cell::Cell::new(false),
volume_name,
writer: Some(w),
})
}
pub fn create_dir(
&mut self,
_dev: &mut dyn BlockDevice,
path: &str,
mode: u16,
uid: u32,
gid: u32,
) -> Result<u32> {
let (parent_id, name) = self.resolve_create_target(path)?;
let w = self
.writer
.as_mut()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: volume is read-only".into()))?;
let cnid = w.next_cnid;
w.next_cnid = w
.next_cnid
.checked_add(1)
.ok_or_else(|| crate::Error::Unsupported("hfs+: CNID space exhausted".into()))?;
writer::insert_folder(w, parent_id, &name, cnid, mode, uid, gid)?;
Ok(cnid)
}
#[allow(clippy::too_many_arguments)]
pub fn create_file<R: std::io::Read>(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
src: &mut R,
len: u64,
mode: u16,
uid: u32,
gid: u32,
) -> Result<u32> {
let (parent_id, name) = self.resolve_create_target(path)?;
let w = self
.writer
.as_mut()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: volume is read-only".into()))?;
let cnid = w.next_cnid;
w.next_cnid = w
.next_cnid
.checked_add(1)
.ok_or_else(|| crate::Error::Unsupported("hfs+: CNID space exhausted".into()))?;
let fork = writer::stream_data_to_blocks(w, dev, src, len, cnid)?;
writer::insert_file(
w,
parent_id,
&name,
cnid,
mode,
uid,
gid,
*b"\0\0\0\0",
*b"\0\0\0\0",
&fork,
false,
)?;
Ok(cnid)
}
pub fn create_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &str,
target: &str,
mode: u16,
uid: u32,
gid: u32,
) -> Result<u32> {
let (parent_id, name) = self.resolve_create_target(path)?;
let w = self
.writer
.as_mut()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: volume is read-only".into()))?;
let cnid = w.next_cnid;
w.next_cnid = w
.next_cnid
.checked_add(1)
.ok_or_else(|| crate::Error::Unsupported("hfs+: CNID space exhausted".into()))?;
let fork = writer::write_inline_data(w, dev, target.as_bytes())?;
writer::insert_file(
w, parent_id, &name, cnid, mode, uid, gid, *b"slnk", *b"rhap", &fork, true,
)?;
Ok(cnid)
}
pub fn create_hardlink(
&mut self,
_dev: &mut dyn BlockDevice,
src_path: &str,
dst_path: &str,
) -> Result<u32> {
let (src_parent, src_name) = self.resolve_create_target(src_path)?;
let (dst_parent, dst_name) = self.resolve_create_target(dst_path)?;
let w = self
.writer
.as_mut()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: volume is read-only".into()))?;
writer::promote_to_hardlink(w, src_parent, &src_name, dst_parent, &dst_name)
}
pub fn remove(&mut self, _dev: &mut dyn BlockDevice, path: &str) -> Result<()> {
let (parent_id, name) = self.resolve_create_target(path)?;
let w = self
.writer
.as_mut()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: volume is read-only".into()))?;
writer::remove_entry(w, parent_id, &name)
}
#[doc(hidden)]
#[cfg(test)]
pub fn test_writer(&self) -> Option<&writer::Writer> {
self.writer.as_ref()
}
pub fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
let Some(w) = self.writer.as_mut() else {
return Ok(());
};
writer::flush(w, &mut self.volume_header, dev)?;
let case_sensitive = self.volume_header.is_hfsx();
let cat_fork = ForkReader::from_inline(
&self.volume_header.catalog_file,
self.volume_header.block_size,
"catalog",
)?;
self.catalog = Catalog::open(dev, cat_fork, case_sensitive)?;
w.flushed = false;
Ok(())
}
fn resolve_create_target(&self, path: &str) -> Result<(u32, UniStr)> {
let parts = split_path(path);
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(
"hfs+: cannot create at the root path".into(),
));
}
let (last, prefix) = parts.split_last().unwrap();
let mut cnid = ROOT_FOLDER_ID;
let w = self
.writer
.as_ref()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: volume is read-only".into()))?;
for part in prefix {
let name = UniStr::from_str_lossy(part);
let (_, child_cnid, rec_type) = w.lookup(cnid, &name).ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"hfs+: parent component {part:?} does not exist"
))
})?;
if rec_type != catalog::REC_FOLDER {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: component {part:?} is not a directory"
)));
}
cnid = child_cnid;
}
Ok((cnid, UniStr::from_str_lossy(last)))
}
pub fn total_bytes(&self) -> u64 {
u64::from(self.volume_header.total_blocks) * u64::from(self.volume_header.block_size)
}
pub fn block_size(&self) -> u32 {
self.volume_header.block_size
}
pub fn volume_name(&self) -> &str {
&self.volume_name
}
pub fn list_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let cnid = self.resolve_dir(dev, path)?;
self.list_cnid(dev, cnid)
}
pub fn open_file_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<HfsPlusFileReader<'a>> {
let rec = self.lookup_path(dev, path)?;
let file = match rec {
CatalogRecord::File(f) => f,
CatalogRecord::Folder(_) => {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {path:?} is a directory, not a file"
)));
}
CatalogRecord::Thread(_) => {
return Err(crate::Error::InvalidImage(format!(
"hfs+: path {path:?} resolved to a thread record"
)));
}
};
let file = self.resolve_hard_link(dev, file)?;
let mode_bits = file.bsd.file_mode & mode::S_IFMT;
if file.is_symlink() || mode_bits == mode::S_IFLNK {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {path:?} is a symlink; use read_symlink_target_path"
)));
}
if mode_bits != 0 && mode_bits != mode::S_IFREG {
return Err(crate::Error::Unsupported(format!(
"hfs+: {path:?} is not a regular file (mode {:#06o})",
file.bsd.file_mode
)));
}
let fork = self.open_data_fork(dev, &file)?;
Ok(HfsPlusFileReader {
dev,
fork,
remaining: file.data_fork.logical_size,
position: 0,
})
}
pub fn read_symlink_target_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<String> {
let rec = self.lookup_path(dev, path)?;
let file = match rec {
CatalogRecord::File(f) => f,
_ => {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {path:?} is not a file (cannot be a symlink)"
)));
}
};
let file = self.resolve_hard_link(dev, file)?;
self.read_symlink_target_inner(dev, &file, path)
}
pub fn read_symlink_target(&self, dev: &mut dyn BlockDevice, cnid: u32) -> Result<String> {
let file = self.lookup_file_by_cnid(dev, cnid)?;
let file = self.resolve_hard_link(dev, file)?;
self.read_symlink_target_inner(dev, &file, &format!("CNID {cnid}"))
}
pub fn file_size(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u64> {
let rec = self.lookup_path(dev, path)?;
let file = match rec {
CatalogRecord::File(f) => f,
_ => {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {path:?} is not a file"
)));
}
};
let file = self.resolve_hard_link(dev, file)?;
Ok(file.data_fork.logical_size)
}
fn resolve_dir(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u32> {
let parts = split_path(path);
let mut cnid = ROOT_FOLDER_ID;
for part in parts {
let rec = self.lookup_component(dev, cnid, part)?;
match rec {
CatalogRecord::Folder(f) => {
cnid = f.folder_id;
}
CatalogRecord::File(_) => {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {part:?} is not a directory (in {path:?})"
)));
}
CatalogRecord::Thread(_) => {
return Err(crate::Error::InvalidImage(format!(
"hfs+: lookup of {part:?} returned a thread record"
)));
}
}
}
Ok(cnid)
}
fn lookup_path(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<CatalogRecord> {
let parts = split_path(path);
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(
"hfs+: cannot resolve root \"/\" as a leaf entry".into(),
));
}
let (last, prefix) = parts.split_last().unwrap();
let mut cnid = ROOT_FOLDER_ID;
for part in prefix {
let rec = self.lookup_component(dev, cnid, part)?;
match rec {
CatalogRecord::Folder(f) => cnid = f.folder_id,
_ => {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {part:?} is not a directory (in {path:?})"
)));
}
}
}
self.lookup_component(dev, cnid, last)
}
fn lookup_component(
&self,
dev: &mut dyn BlockDevice,
parent_id: u32,
name: &str,
) -> Result<CatalogRecord> {
let key = CatalogKey {
parent_id,
name: UniStr::from_str_lossy(name),
encoded_len: 0,
};
self.catalog.lookup(dev, &key)?.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"hfs+: no such entry {name:?} under CNID {parent_id}"
))
})
}
fn lookup_file_by_cnid(&self, dev: &mut dyn BlockDevice, cnid: u32) -> Result<CatalogFile> {
let thread_key = CatalogKey {
parent_id: cnid,
name: UniStr::default(),
encoded_len: 0,
};
let (parent_id, name) = match self.catalog.lookup(dev, &thread_key)? {
Some(CatalogRecord::Thread(t)) => (t.parent_id, t.name),
Some(_) => {
return Err(crate::Error::InvalidImage(format!(
"hfs+: CNID {cnid} thread key did not return a thread record"
)));
}
None => {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: no thread record for CNID {cnid}"
)));
}
};
let child_key = CatalogKey {
parent_id,
name,
encoded_len: 0,
};
match self.catalog.lookup(dev, &child_key)? {
Some(CatalogRecord::File(f)) => Ok(f),
Some(CatalogRecord::Folder(_)) => Err(crate::Error::InvalidArgument(format!(
"hfs+: CNID {cnid} names a folder, not a file"
))),
Some(CatalogRecord::Thread(_)) => Err(crate::Error::InvalidImage(format!(
"hfs+: CNID {cnid} reverse-lookup returned a thread record"
))),
None => Err(crate::Error::InvalidArgument(format!(
"hfs+: no catalog file for CNID {cnid}"
))),
}
}
fn resolve_hard_link(
&self,
dev: &mut dyn BlockDevice,
file: CatalogFile,
) -> Result<CatalogFile> {
if !file.is_hard_link() {
return Ok(file);
}
let private_dir = match self.private_data_dir(dev)? {
Some(cnid) => cnid,
None => {
return Err(crate::Error::InvalidImage(
"hfs+: hard-link record but no '\\0\\0\\0\\0HFS+ Private Data' \
directory found"
.into(),
));
}
};
let inode_id = file.bsd.special;
let name = format!("iNode{inode_id}");
let rec = self.lookup_component(dev, private_dir, &name)?;
match rec {
CatalogRecord::File(f) => {
if f.is_hard_link() {
return Err(crate::Error::InvalidImage(format!(
"hfs+: iNode{inode_id} is itself a hard-link indirection"
)));
}
Ok(f)
}
_ => Err(crate::Error::InvalidImage(format!(
"hfs+: iNode{inode_id} is not a regular file record"
))),
}
}
fn private_data_dir(&self, dev: &mut dyn BlockDevice) -> Result<Option<u32>> {
if self.private_dir_resolved.get() {
return Ok(self.private_dir_cnid.get());
}
let mut code_units: Vec<u16> = vec![0, 0, 0, 0];
code_units.extend("HFS+ Private Data".encode_utf16());
let key = CatalogKey {
parent_id: ROOT_FOLDER_ID,
name: UniStr { code_units },
encoded_len: 0,
};
let cnid = match self.catalog.lookup(dev, &key)? {
Some(CatalogRecord::Folder(f)) => Some(f.folder_id),
_ => None,
};
self.private_dir_cnid.set(cnid);
self.private_dir_resolved.set(true);
Ok(cnid)
}
fn open_data_fork(&self, dev: &mut dyn BlockDevice, file: &CatalogFile) -> Result<ForkReader> {
self.open_fork(
dev,
&file.data_fork,
file.file_id,
extents::FORK_DATA,
"data",
)
}
fn open_fork(
&self,
dev: &mut dyn BlockDevice,
fork: &ForkData,
file_id: u32,
fork_type: u8,
what: &str,
) -> Result<ForkReader> {
if u64::from(fork.total_blocks) <= fork.inline_blocks() {
return ForkReader::from_inline(fork, self.volume_header.block_size, what);
}
let overflow = self.overflow.as_ref().ok_or_else(|| {
crate::Error::InvalidImage(
"hfs+: fork needs overflow extents but volume has no \
extents-overflow file"
.into(),
)
})?;
let first_overflow_block = u32::try_from(fork.inline_blocks()).map_err(|_| {
crate::Error::InvalidImage("hfs+: inline fork block count overflows u32".into())
})?;
let extra = overflow.find_extents(dev, file_id, fork_type, first_overflow_block)?;
ForkReader::from_inline_plus_overflow(fork, &extra, self.volume_header.block_size, what)
}
fn read_symlink_target_inner(
&self,
dev: &mut dyn BlockDevice,
file: &CatalogFile,
descriptor: &str,
) -> Result<String> {
let mode_bits = file.bsd.file_mode & mode::S_IFMT;
let by_mode = mode_bits == mode::S_IFLNK;
let by_finder = file.is_symlink();
if !by_mode && !by_finder {
return Err(crate::Error::InvalidArgument(format!(
"hfs+: {descriptor} is not a symlink (mode {:#06o}, \
FileInfo type {:?}, creator {:?})",
file.bsd.file_mode,
bytes_to_osstr(&file.file_type),
bytes_to_osstr(&file.creator),
)));
}
let len = file.data_fork.logical_size;
if len == 0 {
return Ok(String::new());
}
if len > SYMLINK_MAX_BYTES {
return Err(crate::Error::InvalidImage(format!(
"hfs+: {descriptor} symlink target too large ({len} bytes, \
max {SYMLINK_MAX_BYTES})"
)));
}
let fork = self.open_data_fork(dev, file)?;
let mut buf = vec![0u8; len as usize];
fork.read(dev, 0, &mut buf)?;
if buf.last() == Some(&0) {
buf.pop();
}
String::from_utf8(buf).map_err(|e| {
crate::Error::InvalidImage(format!(
"hfs+: {descriptor} symlink target is not valid UTF-8: {e}"
))
})
}
fn list_cnid(&self, dev: &mut dyn BlockDevice, cnid: u32) -> Result<Vec<crate::fs::DirEntry>> {
use crate::fs::{DirEntry as FsDirEntry, EntryKind};
let mut out = Vec::new();
let node_size = u32::from(self.catalog.header.node_size);
let mut node_idx = self.catalog.header.first_leaf_node;
while node_idx != 0 {
let node = btree::read_node(dev, &self.catalog.fork, node_idx, node_size)?;
let desc = btree::NodeDescriptor::decode(&node)?;
if desc.kind != btree::KIND_LEAF {
return Err(crate::Error::InvalidImage(format!(
"hfs+: leaf chain node {node_idx} has non-leaf kind {}",
desc.kind
)));
}
let offs = btree::record_offsets(&node, desc.num_records)?;
let mut passed_parent = false;
for i in 0..desc.num_records as usize {
let rec = btree::record_bytes(&node, &offs, i);
let key = CatalogKey::decode(rec)?;
use std::cmp::Ordering as O;
match key.parent_id.cmp(&cnid) {
O::Less => continue,
O::Greater => {
passed_parent = true;
break;
}
O::Equal => {}
}
let body_start = align2(key.encoded_len);
if body_start > rec.len() {
return Err(crate::Error::InvalidImage(
"hfs+: catalog key overruns record".into(),
));
}
let body = &rec[body_start..];
let record = CatalogRecord::decode(body)?;
let (kind, child_id) = match &record {
CatalogRecord::Folder(f) => (EntryKind::Dir, f.folder_id),
CatalogRecord::File(f) => {
if f.is_hard_link() {
let resolved_kind = self
.resolve_hard_link(dev, f.clone())
.map(|r| file_kind(&r))
.unwrap_or(EntryKind::Unknown);
(resolved_kind, f.file_id)
} else if f.is_symlink() {
(EntryKind::Symlink, f.file_id)
} else {
(file_kind(f), f.file_id)
}
}
CatalogRecord::Thread(_) => continue,
};
let size = match &record {
CatalogRecord::File(f) if matches!(kind, EntryKind::Regular) => {
f.data_fork.logical_size
}
_ => 0,
};
out.push(FsDirEntry {
name: key.name.to_string_lossy(),
inode: child_id,
kind,
size,
});
}
if passed_parent {
break;
}
node_idx = desc.f_link;
}
Ok(out)
}
}
pub struct HfsPlusFileReader<'a> {
dev: &'a mut dyn BlockDevice,
fork: ForkReader,
remaining: u64,
position: u64,
}
impl<'a> Read for HfsPlusFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.remaining == 0 || buf.is_empty() {
return Ok(0);
}
let want = (buf.len() as u64).min(self.remaining) as usize;
self.fork
.read(self.dev, self.position, &mut buf[..want])
.map_err(std::io::Error::other)?;
self.position += want as u64;
self.remaining -= want as u64;
Ok(want)
}
}
pub fn probe(dev: &mut dyn BlockDevice) -> Result<bool> {
if dev.total_size() < 1024 + 2 {
return Ok(false);
}
let mut sig = [0u8; 2];
dev.read_at(1024, &mut sig)?;
Ok(&sig == b"H+" || &sig == b"HX")
}
fn file_kind(f: &CatalogFile) -> crate::fs::EntryKind {
use crate::fs::EntryKind;
match f.bsd.file_mode & mode::S_IFMT {
mode::S_IFREG => EntryKind::Regular,
mode::S_IFDIR => EntryKind::Dir, mode::S_IFLNK => EntryKind::Symlink,
mode::S_IFCHR => EntryKind::Char,
mode::S_IFBLK => EntryKind::Block,
mode::S_IFIFO => EntryKind::Fifo,
mode::S_IFSOCK => EntryKind::Socket,
0 => EntryKind::Regular,
_ => EntryKind::Unknown,
}
}
fn lookup_thread_name(
dev: &mut dyn BlockDevice,
catalog: &Catalog,
cnid: u32,
) -> Result<Option<String>> {
let key = CatalogKey {
parent_id: cnid,
name: UniStr::default(),
encoded_len: 0,
};
match catalog.lookup(dev, &key)? {
Some(CatalogRecord::Thread(t)) => Ok(Some(t.name.to_string_lossy())),
_ => Ok(None),
}
}
fn bytes_to_osstr(b: &[u8; 4]) -> String {
if b.iter().all(|&c| (0x20..=0x7E).contains(&c)) {
format!("'{}'", String::from_utf8_lossy(b))
} else {
format!("0x{:02x}{:02x}{:02x}{:02x}", b[0], b[1], b[2], b[3])
}
}
fn align2(n: usize) -> usize {
n + (n & 1)
}
fn split_path(path: &str) -> Vec<&str> {
path.split('/')
.filter(|p| !p.is_empty() && *p != ".")
.collect()
}
impl crate::fs::FilesystemFactory for HfsPlus {
type FormatOpts = writer::FormatOpts;
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 crate::fs::Filesystem for HfsPlus {
fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
src: crate::fs::FileSource,
meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: non-UTF-8 path".into()))?;
let len = src.len()?;
let (mut reader, _) = src.open()?;
let mode = meta.mode;
self.create_file(dev, s, &mut reader, len, mode, meta.uid, meta.gid)
.map(|_| ())
}
fn create_dir(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: non-UTF-8 path".into()))?;
let mode = meta.mode;
self.create_dir(dev, s, mode, meta.uid, meta.gid)
.map(|_| ())
}
fn create_symlink(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
target: &std::path::Path,
meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: non-UTF-8 path".into()))?;
let t = target.to_str().ok_or_else(|| {
crate::Error::InvalidArgument("hfs+: non-UTF-8 symlink target".into())
})?;
let mode = meta.mode;
self.create_symlink(dev, s, t, mode, meta.uid, meta.gid)
.map(|_| ())
}
fn create_device(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &std::path::Path,
_kind: crate::fs::DeviceKind,
_major: u32,
_minor: u32,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"hfs+: device / FIFO / socket nodes are not yet implemented".into(),
))
}
fn remove(&mut self, dev: &mut dyn BlockDevice, path: &std::path::Path) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: non-UTF-8 path".into()))?;
self.remove(dev, s)
}
fn list(
&mut self,
dev: &mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Vec<crate::fs::DirEntry>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: non-UTF-8 path".into()))?;
self.list_path(dev, s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &std::path::Path,
) -> Result<Box<dyn std::io::Read + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("hfs+: non-UTF-8 path".into()))?;
let r = self.open_file_reader(dev, s)?;
Ok(Box::new(r))
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
Self::flush(self, dev)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn probe_recognises_h_plus_signature() {
let mut dev = MemoryBackend::new(8192);
dev.write_at(1024, b"H+").unwrap();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn probe_recognises_hx_signature() {
let mut dev = MemoryBackend::new(8192);
dev.write_at(1024, b"HX").unwrap();
assert!(probe(&mut dev).unwrap());
}
#[test]
fn probe_rejects_unknown_signature() {
let mut dev = MemoryBackend::new(8192);
dev.write_at(1024, b"NO").unwrap();
assert!(!probe(&mut dev).unwrap());
}
#[test]
fn open_fails_on_garbage() {
let mut dev = MemoryBackend::new(8192);
assert!(HfsPlus::open(&mut dev).is_err());
}
}