use forensic_vfs::{
Allocation, DirEntry, DirStream, ExtentStream, FileId, FileSystem, FsKind, FsMeta, MacbTimes,
NodeKind, NodeStream, ResidencyKind, SectorSizes, SmallHex, StreamId, TimeResolution,
TimeSource, TimeStamp, TimeZonePolicy, VfsError, VfsResult,
};
use crate::{HfsStat, HfsVolume, ROOT_FOLDER_CNID};
const HFS_EPOCH_TO_UNIX: i128 = 2_082_844_800;
fn cnid_of(id: FileId) -> VfsResult<u32> {
match id {
FileId::Opaque(n) => u32::try_from(n).map_err(|_| VfsError::OutOfRange {
what: "hfs+ cnid",
offset: n,
len: 0,
bound: u64::from(u32::MAX),
}),
other => Err(VfsError::Unsupported {
layer: "hfs+ file-id",
scheme: format!("{other:?}"),
}),
}
}
fn hfs_time(secs: u32) -> TimeStamp {
TimeStamp {
unix_nanos: (i128::from(secs) - HFS_EPOCH_TO_UNIX) * 1_000_000_000,
source: TimeSource::InodeTable,
resolution: TimeResolution::Seconds,
}
}
pub struct HfsFs {
volume: Vec<u8>,
}
impl HfsFs {
pub fn new(volume: Vec<u8>) -> VfsResult<Self> {
let _: HfsVolume = crate::parse(&volume).ok_or_else(|| VfsError::Bootstrap {
stage: "hfs+ volume header",
detail: "no HFS+/HFSX signature at offset 1024".to_string(),
})?;
Ok(Self { volume })
}
}
impl FileSystem for HfsFs {
fn kind(&self) -> FsKind {
FsKind::HFS_PLUS
}
fn root(&self) -> FileId {
FileId::Opaque(u64::from(ROOT_FOLDER_CNID))
}
fn sector_sizes(&self) -> SectorSizes {
let block = crate::parse(&self.volume).map_or(512, |v| v.block_size);
SectorSizes {
logical: 512,
physical: 512,
cluster_or_block: block,
}
}
fn timestamp_zone(&self) -> TimeZonePolicy {
TimeZonePolicy::Utc
}
fn read_dir(&self, ino: FileId) -> VfsResult<DirStream> {
let cnid = cnid_of(ino)?;
let entries = crate::list_dir(&self.volume, cnid).ok_or_else(|| VfsError::Decode {
layer: "hfs+ catalog",
offset: 0,
detail: format!("cannot list directory CNID {cnid}"),
bytes: SmallHex::new(&[]),
})?;
let out: Vec<VfsResult<DirEntry>> = entries
.into_iter()
.map(|e| {
Ok(DirEntry {
name: e.name.into_bytes(),
id: FileId::Opaque(u64::from(e.cnid)),
kind: if e.is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
})
})
.collect();
Ok(DirStream::new(out.into_iter()))
}
fn extents(&self, _ino: FileId, _stream: StreamId) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
fn lookup(&self, parent: FileId, name: &[u8]) -> VfsResult<Option<FileId>> {
let cnid = cnid_of(parent)?;
let entries = crate::list_dir(&self.volume, cnid).ok_or_else(|| VfsError::Decode {
layer: "hfs+ catalog",
offset: 0,
detail: format!("cannot list directory CNID {cnid}"),
bytes: SmallHex::new(&[]),
})?;
for e in entries {
if e.name.as_bytes() == name {
return Ok(Some(FileId::Opaque(u64::from(e.cnid))));
}
}
Ok(None)
}
fn meta(&self, ino: FileId) -> VfsResult<FsMeta> {
let cnid = cnid_of(ino)?;
let s: HfsStat = crate::stat(&self.volume, cnid).ok_or_else(|| VfsError::Decode {
layer: "hfs+ catalog",
offset: 0,
detail: format!("no catalog record for CNID {cnid}"),
bytes: SmallHex::new(&[]),
})?;
Ok(FsMeta {
ino: u64::from(cnid),
kind: if s.is_dir {
NodeKind::Dir
} else {
NodeKind::File
},
allocated: Allocation::Allocated,
size: s.size,
nlink: 1,
uid: None,
gid: None,
mode: None,
times: MacbTimes {
born: Some(hfs_time(s.created)),
modified: Some(hfs_time(s.modified)),
changed: None,
accessed: Some(hfs_time(s.accessed)),
},
streams: Vec::new(),
residency: ResidencyKind::NonResident,
link_target: None,
})
}
fn read_at(&self, ino: FileId, stream: StreamId, off: u64, buf: &mut [u8]) -> VfsResult<usize> {
let cnid = cnid_of(ino)?;
if stream != StreamId::Default {
return Err(VfsError::Unsupported {
layer: "hfs+ stream",
scheme: format!("{stream:?}"),
});
}
let data = crate::read_file(&self.volume, cnid).ok_or_else(|| VfsError::Decode {
layer: "hfs+ file",
offset: off,
detail: format!("cannot read file CNID {cnid}"),
bytes: SmallHex::new(&[]),
})?;
let start = usize::try_from(off).unwrap_or(usize::MAX);
if start >= data.len() {
return Ok(0);
}
let n = buf.len().min(data.len() - start);
buf[..n].copy_from_slice(&data[start..start + n]);
Ok(n)
}
fn read_link(&self, _ino: FileId, _cap: usize) -> VfsResult<Vec<u8>> {
Ok(Vec::new())
}
fn deleted(&self) -> VfsResult<NodeStream> {
Ok(NodeStream::empty())
}
fn unallocated(&self) -> VfsResult<ExtentStream> {
Ok(ExtentStream::empty())
}
}