use crate::Result;
pub const DECMPFS_MAGIC: u32 = 0x636d_7066;
pub const UF_COMPRESSED: u8 = 0x20;
pub const HFSCOMPRESS_BLOCK_SIZE: usize = 65_536;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionType {
Uncompressed,
ZlibAttr,
ZlibResource,
LzvnAttr,
LzvnResource,
LzfseAttr,
LzfseResource,
Unknown(u32),
}
impl CompressionType {
fn from_u32(t: u32) -> Self {
match t {
1 => Self::Uncompressed,
3 => Self::ZlibAttr,
4 => Self::ZlibResource,
7 => Self::LzvnAttr,
8 => Self::LzvnResource,
11 => Self::LzfseAttr,
12 => Self::LzfseResource,
other => Self::Unknown(other),
}
}
pub fn is_resource_fork(self) -> bool {
matches!(
self,
Self::ZlibResource | Self::LzvnResource | Self::LzfseResource
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DecmpfsHeader {
pub compression_type: CompressionType,
pub uncompressed_size: u64,
}
impl DecmpfsHeader {
pub const SIZE: usize = 16;
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < Self::SIZE {
return Err(crate::Error::InvalidImage(
"hfs+: decmpfs xattr shorter than 16-byte header".into(),
));
}
let magic = u32::from_be_bytes(buf[0..4].try_into().unwrap());
if magic != DECMPFS_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs bad magic {magic:#010x} (expected {DECMPFS_MAGIC:#010x})"
)));
}
let compression_type = u32::from_le_bytes(buf[4..8].try_into().unwrap());
let uncompressed_size = u64::from_le_bytes(buf[8..16].try_into().unwrap());
Ok(Self {
compression_type: CompressionType::from_u32(compression_type),
uncompressed_size,
})
}
}
pub fn decompress_inline(
compression_type: CompressionType,
tail: &[u8],
expected_len: u64,
) -> Result<Vec<u8>> {
match compression_type {
CompressionType::Uncompressed => {
if tail.len() as u64 != expected_len {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs type 1 payload is {} bytes but header says {expected_len}",
tail.len()
)));
}
Ok(tail.to_vec())
}
CompressionType::ZlibAttr => decompress_zlib_block(tail, expected_len as usize),
CompressionType::Unknown(t) => Err(crate::Error::Unsupported(format!(
"hfs+: decmpfs unknown compression type {t}"
))),
other => Err(crate::Error::Unsupported(format!(
"hfs+: decmpfs compression type {other:?} not yet implemented"
))),
}
}
pub fn decompress_resource_fork(
compression_type: CompressionType,
resource_bytes: &[u8],
expected_len: u64,
) -> Result<Vec<u8>> {
match compression_type {
CompressionType::ZlibResource => decompress_resource_zlib(resource_bytes, expected_len),
CompressionType::LzvnResource | CompressionType::LzfseResource => {
Err(crate::Error::Unsupported(format!(
"hfs+: decmpfs compression type {compression_type:?} not yet implemented"
)))
}
other => Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs type {other:?} should not point at the resource fork"
))),
}
}
fn decompress_resource_zlib(rf: &[u8], expected_len: u64) -> Result<Vec<u8>> {
if rf.len() < 16 {
return Err(crate::Error::InvalidImage(
"hfs+: decmpfs resource fork shorter than 16-byte header".into(),
));
}
let data_offset = u32::from_be_bytes(rf[0..4].try_into().unwrap()) as usize;
let data_length = u32::from_be_bytes(rf[8..12].try_into().unwrap()) as usize;
if data_offset.saturating_add(data_length) > rf.len() {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs resource data {data_offset}..+{data_length} exceeds fork ({} bytes)",
rf.len()
)));
}
if data_length < 4 {
return Err(crate::Error::InvalidImage(
"hfs+: decmpfs resource data too short for blockCount header".into(),
));
}
let table_base = data_offset
.checked_add(4)
.ok_or_else(|| crate::Error::InvalidImage("hfs+: decmpfs data_offset overflow".into()))?;
if table_base + 4 > rf.len() {
return Err(crate::Error::InvalidImage(
"hfs+: decmpfs resource block-count past end of fork".into(),
));
}
let block_count =
u32::from_le_bytes(rf[table_base..table_base + 4].try_into().unwrap()) as usize;
if block_count == 0 {
if expected_len != 0 {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs resource has 0 blocks but header says {expected_len} bytes"
)));
}
return Ok(Vec::new());
}
let table_off = table_base + 4;
let table_end = table_off
.checked_add(block_count.checked_mul(8).ok_or_else(|| {
crate::Error::InvalidImage("hfs+: decmpfs block count overflow".into())
})?)
.ok_or_else(|| {
crate::Error::InvalidImage("hfs+: decmpfs block table end overflow".into())
})?;
if table_end > rf.len() {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs block table extends past resource fork (need {table_end}, have {})",
rf.len()
)));
}
let expected = usize::try_from(expected_len).map_err(|_| {
crate::Error::InvalidImage(format!(
"hfs+: decmpfs uncompressed size {expected_len} exceeds usize"
))
})?;
let mut out = Vec::with_capacity(expected);
for i in 0..block_count {
let entry_off = table_off + 8 * i;
let blk_off = u32::from_le_bytes(rf[entry_off..entry_off + 4].try_into().unwrap()) as usize;
let blk_len =
u32::from_le_bytes(rf[entry_off + 4..entry_off + 8].try_into().unwrap()) as usize;
let blk_start = table_base.checked_add(blk_off).ok_or_else(|| {
crate::Error::InvalidImage("hfs+: decmpfs block offset overflow".into())
})?;
let blk_end = blk_start.checked_add(blk_len).ok_or_else(|| {
crate::Error::InvalidImage("hfs+: decmpfs block length overflow".into())
})?;
if blk_end > rf.len() {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs block {i} ({blk_off}..+{blk_len}) extends past resource fork"
)));
}
let remaining = expected.saturating_sub(out.len());
let block_target = remaining.min(HFSCOMPRESS_BLOCK_SIZE);
let chunk = &rf[blk_start..blk_end];
if !chunk.is_empty() && chunk[0] == 0xFF {
let payload = &chunk[1..];
if payload.len() != block_target {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs raw block {i} is {} bytes but expected {block_target}",
payload.len()
)));
}
out.extend_from_slice(payload);
continue;
}
let decoded = decompress_zlib_block(chunk, block_target)?;
out.extend_from_slice(&decoded);
}
if out.len() as u64 != expected_len {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs decoded {} bytes but header says {expected_len}",
out.len()
)));
}
Ok(out)
}
#[cfg(feature = "gzip")]
fn decompress_zlib_block(src: &[u8], target_len: usize) -> Result<Vec<u8>> {
use std::io::Read;
let mut dec = flate2::read::ZlibDecoder::new(src);
let mut out = Vec::with_capacity(target_len);
dec.read_to_end(&mut out).map_err(|e| {
crate::Error::InvalidImage(format!("hfs+: decmpfs zlib block inflate failed: {e}"))
})?;
if out.len() != target_len {
return Err(crate::Error::InvalidImage(format!(
"hfs+: decmpfs zlib block inflated to {} bytes but expected {target_len}",
out.len()
)));
}
Ok(out)
}
#[cfg(not(feature = "gzip"))]
fn decompress_zlib_block(_src: &[u8], _target_len: usize) -> Result<Vec<u8>> {
Err(crate::Error::Unsupported(
"hfs+: decmpfs zlib decompression requires the `gzip` Cargo feature".into(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_decode_type_3() {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&DECMPFS_MAGIC.to_be_bytes());
buf[4..8].copy_from_slice(&3u32.to_le_bytes());
buf[8..16].copy_from_slice(&1234u64.to_le_bytes());
let h = DecmpfsHeader::decode(&buf).unwrap();
assert_eq!(h.compression_type, CompressionType::ZlibAttr);
assert_eq!(h.uncompressed_size, 1234);
}
#[test]
fn header_decode_rejects_bad_magic() {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&0xdeadbeefu32.to_be_bytes());
assert!(DecmpfsHeader::decode(&buf).is_err());
}
#[test]
fn header_decode_too_short() {
assert!(DecmpfsHeader::decode(&[0u8; 8]).is_err());
}
#[cfg(feature = "gzip")]
#[test]
fn inline_zlib_round_trip() {
use flate2::{Compression, write::ZlibEncoder};
use std::io::Write;
let plain = b"hello hfsplus decmpfs world!".repeat(8);
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(&plain).unwrap();
let compressed = enc.finish().unwrap();
let out =
decompress_inline(CompressionType::ZlibAttr, &compressed, plain.len() as u64).unwrap();
assert_eq!(out, plain);
}
#[test]
fn inline_uncompressed_pass_through() {
let plain = b"plain text".to_vec();
let out =
decompress_inline(CompressionType::Uncompressed, &plain, plain.len() as u64).unwrap();
assert_eq!(out, plain);
}
#[test]
fn inline_lzvn_returns_unsupported() {
let r = decompress_inline(CompressionType::LzvnAttr, &[0u8; 4], 0);
assert!(matches!(r, Err(crate::Error::Unsupported(_))));
}
#[cfg(feature = "gzip")]
#[test]
fn resource_fork_zlib_round_trip() {
use flate2::{Compression, write::ZlibEncoder};
use std::io::Write;
let mut plain = Vec::new();
plain.extend(std::iter::repeat_n(0xABu8, HFSCOMPRESS_BLOCK_SIZE));
plain.extend_from_slice(b"second block tail of hfscompression test data");
let block1 = &plain[..HFSCOMPRESS_BLOCK_SIZE];
let block2 = &plain[HFSCOMPRESS_BLOCK_SIZE..];
let compress = |data: &[u8]| -> Vec<u8> {
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(data).unwrap();
e.finish().unwrap()
};
let c1 = compress(block1);
let c2 = compress(block2);
let block_count: u32 = 2;
let table_size = 4 + 8 * block_count as usize;
let blk1_off = table_size as u32; let blk2_off = blk1_off + c1.len() as u32;
let mut rdata = Vec::new();
let inner_size: u32 = (table_size + c1.len() + c2.len()) as u32;
rdata.extend_from_slice(&inner_size.to_be_bytes());
rdata.extend_from_slice(&block_count.to_le_bytes());
rdata.extend_from_slice(&blk1_off.to_le_bytes());
rdata.extend_from_slice(&(c1.len() as u32).to_le_bytes());
rdata.extend_from_slice(&blk2_off.to_le_bytes());
rdata.extend_from_slice(&(c2.len() as u32).to_le_bytes());
rdata.extend_from_slice(&c1);
rdata.extend_from_slice(&c2);
let data_offset: u32 = 256; let data_length: u32 = rdata.len() as u32;
let mut rf = Vec::new();
rf.extend_from_slice(&data_offset.to_be_bytes());
rf.extend_from_slice(&(data_offset + data_length).to_be_bytes()); rf.extend_from_slice(&data_length.to_be_bytes());
rf.extend_from_slice(&0u32.to_be_bytes()); rf.resize(data_offset as usize, 0);
rf.extend_from_slice(&rdata);
let out = decompress_resource_fork(CompressionType::ZlibResource, &rf, plain.len() as u64)
.unwrap();
assert_eq!(out.len(), plain.len());
assert_eq!(out, plain);
}
}