mod base64;
mod codec;
pub mod encrypted;
mod mish;
mod plist;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
use crate::Result;
use crate::block::BlockDevice;
pub use encrypted::{
ENCRCDSA_MAGIC, ENCRCDSA_V2_HEADER_MIN_BYTES, EncryptedDmgBackend, EncryptedDmgHeader,
probe as probe_encrypted,
};
pub use mish::{Chunk, ChunkType, Mish};
pub const KOLY_MAGIC: u32 = 0x6B6F_6C79;
pub const KOLY_SIZE: u64 = 512;
#[derive(Debug, Clone)]
pub struct KolyTrailer {
pub signature: u32,
pub version: u32,
pub header_size: u32,
pub flags: u32,
pub running_data_fork_offset: u64,
pub data_fork_offset: u64,
pub data_fork_length: u64,
pub rsrc_fork_offset: u64,
pub rsrc_fork_length: u64,
pub segment_number: u32,
pub segment_count: u32,
pub segment_id: [u8; 16],
pub data_checksum_type: u32,
pub data_checksum_size: u32,
pub data_checksum: [u8; 128],
pub xml_offset: u64,
pub xml_length: u64,
pub master_checksum_type: u32,
pub master_checksum_size: u32,
pub master_checksum: [u8; 128],
pub image_variant: u32,
pub sector_count: u64,
}
impl KolyTrailer {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < KOLY_SIZE as usize {
return Err(crate::Error::InvalidImage(
"dmg: trailer slice shorter than 512 bytes".into(),
));
}
let signature = u32::from_be_bytes(buf[0x000..0x004].try_into().unwrap());
if signature != KOLY_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"dmg: koly magic mismatch (got {signature:#010x})"
)));
}
let version = u32::from_be_bytes(buf[0x004..0x008].try_into().unwrap());
if version != 4 {
return Err(crate::Error::Unsupported(format!(
"dmg: koly version {version} not supported (only v4)"
)));
}
let mut segment_id = [0u8; 16];
segment_id.copy_from_slice(&buf[0x040..0x050]);
let mut data_checksum = [0u8; 128];
data_checksum.copy_from_slice(&buf[0x058..0x0D8]);
let mut master_checksum = [0u8; 128];
master_checksum.copy_from_slice(&buf[0x168..0x1E8]);
Ok(Self {
signature,
version,
header_size: u32::from_be_bytes(buf[0x008..0x00C].try_into().unwrap()),
flags: u32::from_be_bytes(buf[0x00C..0x010].try_into().unwrap()),
running_data_fork_offset: u64::from_be_bytes(buf[0x010..0x018].try_into().unwrap()),
data_fork_offset: u64::from_be_bytes(buf[0x018..0x020].try_into().unwrap()),
data_fork_length: u64::from_be_bytes(buf[0x020..0x028].try_into().unwrap()),
rsrc_fork_offset: u64::from_be_bytes(buf[0x028..0x030].try_into().unwrap()),
rsrc_fork_length: u64::from_be_bytes(buf[0x030..0x038].try_into().unwrap()),
segment_number: u32::from_be_bytes(buf[0x038..0x03C].try_into().unwrap()),
segment_count: u32::from_be_bytes(buf[0x03C..0x040].try_into().unwrap()),
segment_id,
data_checksum_type: u32::from_be_bytes(buf[0x050..0x054].try_into().unwrap()),
data_checksum_size: u32::from_be_bytes(buf[0x054..0x058].try_into().unwrap()),
data_checksum,
xml_offset: u64::from_be_bytes(buf[0x0D8..0x0E0].try_into().unwrap()),
xml_length: u64::from_be_bytes(buf[0x0E0..0x0E8].try_into().unwrap()),
master_checksum_type: u32::from_be_bytes(buf[0x160..0x164].try_into().unwrap()),
master_checksum_size: u32::from_be_bytes(buf[0x164..0x168].try_into().unwrap()),
master_checksum,
image_variant: u32::from_be_bytes(buf[0x1E8..0x1EC].try_into().unwrap()),
sector_count: u64::from_be_bytes(buf[0x1EC..0x1F4].try_into().unwrap()),
})
}
}
pub fn probe(path: &Path) -> Result<bool> {
let meta = match std::fs::metadata(path) {
Ok(m) => m,
Err(_) => return Ok(false),
};
if meta.len() < KOLY_SIZE {
return Ok(false);
}
let mut f = match File::open(path) {
Ok(f) => f,
Err(_) => return Ok(false),
};
let trailer_offset = meta.len() - KOLY_SIZE;
f.seek(SeekFrom::Start(trailer_offset))?;
let mut head = [0u8; 4];
if f.read_exact(&mut head).is_err() {
return Ok(false);
}
Ok(u32::from_be_bytes(head) == KOLY_MAGIC)
}
#[derive(Debug)]
pub struct DmgBackend {
file: File,
trailer: KolyTrailer,
virtual_size: u64,
cursor: u64,
chunks: Vec<Chunk>,
}
impl DmgBackend {
pub fn open(path: &Path) -> Result<Self> {
let meta = std::fs::metadata(path)?;
if meta.len() < KOLY_SIZE {
return Err(crate::Error::InvalidImage(
"dmg: file smaller than the 512-byte koly trailer".into(),
));
}
let mut file = File::open(path)?;
let trailer_offset = meta.len() - KOLY_SIZE;
file.seek(SeekFrom::Start(trailer_offset))?;
let mut buf = vec![0u8; KOLY_SIZE as usize];
file.read_exact(&mut buf)?;
let trailer = KolyTrailer::decode(&buf)?;
if trailer.segment_count > 1 {
return Err(crate::Error::Unsupported(format!(
"dmg: multi-segment images not supported (segment_count = {})",
trailer.segment_count
)));
}
let virtual_size = trailer
.sector_count
.checked_mul(512)
.ok_or_else(|| crate::Error::InvalidImage("dmg: sector_count overflows u64".into()))?;
if trailer.xml_length == 0 {
return Err(crate::Error::InvalidImage(
"dmg: koly trailer has empty XML resource fork (xml_length = 0)".into(),
));
}
const MAX_PLIST_BYTES: u64 = 128 * 1024 * 1024;
if trailer.xml_length > MAX_PLIST_BYTES {
return Err(crate::Error::InvalidImage(format!(
"dmg: resource-fork plist is implausibly large ({} bytes)",
trailer.xml_length
)));
}
file.seek(SeekFrom::Start(trailer.xml_offset))?;
let mut plist_bytes = vec![0u8; trailer.xml_length as usize];
file.read_exact(&mut plist_bytes)?;
let plist_str = std::str::from_utf8(&plist_bytes).map_err(|e| {
crate::Error::InvalidImage(format!("dmg: resource-fork plist isn't UTF-8: {e}"))
})?;
let data_entries = plist::extract_blkx_data_entries(plist_str)?;
if data_entries.is_empty() {
return Err(crate::Error::InvalidImage(
"dmg: blkx array is empty — no chunks to map".into(),
));
}
let mut chunks: Vec<Chunk> = Vec::new();
for entry in &data_entries {
let raw = base64::decode(entry)?;
let m = mish::decode_mish(&raw)?;
chunks.extend(m.chunks);
}
chunks.sort_by_key(|c| c.virtual_sector_start);
for w in chunks.windows(2) {
let prev_end = w[0].virtual_sector_start.saturating_add(w[0].sector_count);
if w[1].virtual_sector_start < prev_end {
log::warn!(
"dmg: chunk overlap detected: chunk ending at sector {} > next chunk start {}",
prev_end,
w[1].virtual_sector_start
);
}
}
Ok(Self {
file,
trailer,
virtual_size,
cursor: 0,
chunks,
})
}
pub fn trailer(&self) -> &KolyTrailer {
&self.trailer
}
pub fn chunk_count(&self) -> usize {
self.chunks.len()
}
fn read_compressed_payload(&mut self, chunk: &Chunk) -> Result<Vec<u8>> {
let abs_offset = self
.trailer
.data_fork_offset
.checked_add(chunk.compressed_offset_in_fork)
.ok_or_else(|| {
crate::Error::InvalidImage(
"dmg: chunk absolute offset overflows the data fork".into(),
)
})?;
let len = chunk.compressed_length as usize;
self.file.seek(SeekFrom::Start(abs_offset))?;
let mut buf = vec![0u8; len];
self.file.read_exact(&mut buf)?;
Ok(buf)
}
fn decode_chunk(&mut self, chunk: &Chunk) -> Result<Vec<u8>> {
let plain_len = (chunk.sector_count as usize)
.checked_mul(512)
.ok_or_else(|| {
crate::Error::InvalidImage("dmg: chunk plain length overflows usize".into())
})?;
match chunk.kind {
ChunkType::Zero | ChunkType::Ignored => Ok(vec![0u8; plain_len]),
ChunkType::Raw => {
let buf = self.read_compressed_payload(chunk)?;
if buf.len() != plain_len {
return Err(crate::Error::InvalidImage(format!(
"dmg: raw chunk has compressed_length {} but sector_count*512 = {}",
buf.len(),
plain_len
)));
}
Ok(buf)
}
ChunkType::Zlib => decode_zlib(&self.read_compressed_payload(chunk)?, plain_len),
ChunkType::Adc => {
let buf = self.read_compressed_payload(chunk)?;
codec::decode_adc_chunk(&buf, plain_len)
}
ChunkType::Bz2 => {
let buf = self.read_compressed_payload(chunk)?;
codec::decode_bzip2(&buf, plain_len)
}
ChunkType::Lzfse => {
let buf = self.read_compressed_payload(chunk)?;
codec::decode_lzfse(&buf, plain_len)
}
ChunkType::Lzma => {
let buf = self.read_compressed_payload(chunk)?;
codec::decode_lzma(&buf, plain_len)
}
ChunkType::Comment | ChunkType::Terminator => Ok(vec![0u8; plain_len]),
}
}
fn find_chunk_idx(&self, sector: u64) -> Result<usize> {
let after = self
.chunks
.partition_point(|c| c.virtual_sector_start <= sector);
if after == 0 {
return Err(crate::Error::OutOfBounds {
offset: sector * 512,
len: 0,
size: self.virtual_size,
});
}
let idx = after - 1;
let c = &self.chunks[idx];
let end = c.virtual_sector_start + c.sector_count;
if sector >= end {
return Err(crate::Error::OutOfBounds {
offset: sector * 512,
len: 0,
size: self.virtual_size,
});
}
Ok(idx)
}
}
#[cfg(feature = "gzip")]
fn decode_zlib(src: &[u8], plain_len: usize) -> Result<Vec<u8>> {
use std::io::Read;
let mut dec = flate2::read::ZlibDecoder::new(src);
let mut out = Vec::with_capacity(plain_len);
dec.read_to_end(&mut out)
.map_err(|e| crate::Error::InvalidImage(format!("dmg: zlib chunk inflate failed: {e}")))?;
if out.len() != plain_len {
return Err(crate::Error::InvalidImage(format!(
"dmg: zlib chunk inflated to {} bytes but sector_count*512 = {}",
out.len(),
plain_len
)));
}
Ok(out)
}
#[cfg(not(feature = "gzip"))]
fn decode_zlib(_src: &[u8], _plain_len: usize) -> Result<Vec<u8>> {
Err(crate::Error::Unsupported(
"dmg: zlib chunks require the `gzip` Cargo feature (the same one that gates SquashFS gzip)"
.into(),
))
}
impl BlockDevice for DmgBackend {
fn block_size(&self) -> u32 {
512
}
fn total_size(&self) -> u64 {
self.virtual_size
}
fn sync(&mut self) -> Result<()> {
Ok(())
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
let end = offset
.checked_add(buf.len() as u64)
.ok_or(crate::Error::OutOfBounds {
offset,
len: buf.len() as u64,
size: self.virtual_size,
})?;
if end > self.virtual_size {
return Err(crate::Error::OutOfBounds {
offset,
len: buf.len() as u64,
size: self.virtual_size,
});
}
if buf.is_empty() {
return Ok(());
}
let mut filled = 0usize;
let mut cursor = offset;
while filled < buf.len() {
let sector = cursor / 512;
let idx = self.find_chunk_idx(sector)?;
let chunk = self.chunks[idx];
let chunk_byte_start = chunk.virtual_sector_start * 512;
let chunk_byte_end = chunk_byte_start + chunk.sector_count * 512;
let plain = self.decode_chunk(&chunk)?;
debug_assert_eq!(plain.len() as u64, chunk.sector_count * 512);
let local_start = (cursor - chunk_byte_start) as usize;
let available = (chunk_byte_end - cursor) as usize;
let want = (buf.len() - filled).min(available);
buf[filled..filled + want].copy_from_slice(&plain[local_start..local_start + want]);
filled += want;
cursor += want as u64;
}
Ok(())
}
fn write_at(&mut self, _offset: u64, _buf: &[u8]) -> Result<()> {
Err(crate::Error::Unsupported(
"dmg: read-only container; writes are out of scope".into(),
))
}
}
impl Read for DmgBackend {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.cursor >= self.virtual_size {
return Ok(0);
}
let remaining = self.virtual_size - self.cursor;
let take = (buf.len() as u64).min(remaining) as usize;
if take == 0 {
return Ok(0);
}
self.read_at(self.cursor, &mut buf[..take])
.map_err(|e| io::Error::other(format!("{e}")))?;
self.cursor += take as u64;
Ok(take)
}
}
impl Write for DmgBackend {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::other("dmg: read-only container"))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Seek for DmgBackend {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let total = self.virtual_size;
let new = match pos {
SeekFrom::Start(o) => o,
SeekFrom::Current(d) => (self.cursor as i64).saturating_add(d).max(0) as u64,
SeekFrom::End(d) => (total as i64).saturating_add(d).max(0) as u64,
};
self.cursor = new;
Ok(new)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::dmg::mish::{Chunk, ChunkType, encode_mish_for_tests};
fn fake_trailer(
sector_count: u64,
version: u32,
data_fork_offset: u64,
data_fork_length: u64,
xml_offset: u64,
xml_length: u64,
) -> Vec<u8> {
let mut v = vec![0u8; KOLY_SIZE as usize];
v[0x000..0x004].copy_from_slice(&KOLY_MAGIC.to_be_bytes());
v[0x004..0x008].copy_from_slice(&version.to_be_bytes());
v[0x008..0x00C].copy_from_slice(&512u32.to_be_bytes());
v[0x018..0x020].copy_from_slice(&data_fork_offset.to_be_bytes());
v[0x020..0x028].copy_from_slice(&data_fork_length.to_be_bytes());
v[0x0D8..0x0E0].copy_from_slice(&xml_offset.to_be_bytes());
v[0x0E0..0x0E8].copy_from_slice(&xml_length.to_be_bytes());
v[0x1EC..0x1F4].copy_from_slice(§or_count.to_be_bytes());
v
}
fn b64_encode(bytes: &[u8]) -> String {
const ALPHA: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let (a, b, c, len) = match chunk.len() {
3 => (chunk[0], chunk[1], chunk[2], 3),
2 => (chunk[0], chunk[1], 0, 2),
1 => (chunk[0], 0, 0, 1),
_ => unreachable!(),
};
let v = ((a as u32) << 16) | ((b as u32) << 8) | (c as u32);
out.push(ALPHA[((v >> 18) & 0x3F) as usize] as char);
out.push(ALPHA[((v >> 12) & 0x3F) as usize] as char);
if len >= 2 {
out.push(ALPHA[((v >> 6) & 0x3F) as usize] as char);
} else {
out.push('=');
}
if len == 3 {
out.push(ALPHA[(v & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
fn build_test_dmg(
dir: &std::path::Path,
sector_count: u64,
chunks: &[Chunk],
data_payload: &[u8],
) -> std::path::PathBuf {
let data_offset = 0u64;
let xml_offset = data_payload.len() as u64;
let mish_buf = encode_mish_for_tests(0, sector_count, 0, chunks);
let b64 = b64_encode(&mish_buf);
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>resource-fork</key>
<dict>
<key>blkx</key>
<array>
<dict>
<key>Data</key>
<data>{b64}</data>
</dict>
</array>
</dict>
</dict>
</plist>"#
);
let xml_bytes = xml.into_bytes();
let xml_length = xml_bytes.len() as u64;
let trailer = fake_trailer(
sector_count,
4,
data_offset,
data_payload.len() as u64,
xml_offset,
xml_length,
);
let p = dir.join("img.dmg");
let mut buf = Vec::new();
buf.extend_from_slice(data_payload);
buf.extend_from_slice(&xml_bytes);
buf.extend_from_slice(&trailer);
std::fs::write(&p, &buf).unwrap();
p
}
#[test]
fn decode_recognises_valid_trailer() {
let buf = fake_trailer(2048, 4, 0, 0, 0, 0);
let t = KolyTrailer::decode(&buf).unwrap();
assert_eq!(t.signature, KOLY_MAGIC);
assert_eq!(t.version, 4);
assert_eq!(t.header_size, 512);
assert_eq!(t.sector_count, 2048);
}
#[test]
fn decode_rejects_wrong_magic() {
let mut buf = fake_trailer(0, 4, 0, 0, 0, 0);
buf[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
let err = KolyTrailer::decode(&buf).unwrap_err();
match err {
crate::Error::InvalidImage(_) => {}
_ => panic!("expected InvalidImage, got {err:?}"),
}
}
#[test]
fn decode_rejects_unknown_version() {
let buf = fake_trailer(0, 3, 0, 0, 0, 0);
let err = KolyTrailer::decode(&buf).unwrap_err();
match err {
crate::Error::Unsupported(_) => {}
_ => panic!("expected Unsupported, got {err:?}"),
}
}
#[test]
fn probe_matches_trailing_koly() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("img.dmg");
let mut content = vec![0u8; 8192];
content.extend_from_slice(&fake_trailer(16, 4, 0, 0, 0, 0));
std::fs::write(&p, &content).unwrap();
assert!(probe(&p).unwrap());
}
#[test]
fn probe_misses_when_no_trailer() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("not.dmg");
std::fs::write(&p, vec![0u8; 8192]).unwrap();
assert!(!probe(&p).unwrap());
}
#[test]
fn open_rejects_multi_segment() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("img.dmg");
let mut t = fake_trailer(0, 4, 0, 0, 0, 0);
t[0x03C..0x040].copy_from_slice(&3u32.to_be_bytes());
let mut content = vec![0u8; 8192];
content.extend_from_slice(&t);
std::fs::write(&p, &content).unwrap();
let err = DmgBackend::open(&p).unwrap_err();
match err {
crate::Error::Unsupported(_) => {}
_ => panic!("expected Unsupported, got {err:?}"),
}
}
#[test]
fn round_trip_raw_chunk() {
let dir = tempfile::tempdir().unwrap();
let mut payload = vec![0u8; 512];
for (i, b) in payload.iter_mut().enumerate() {
*b = (i & 0xFF) as u8;
}
let chunks = vec![Chunk {
kind: ChunkType::Raw,
virtual_sector_start: 0,
sector_count: 1,
compressed_offset_in_fork: 0,
compressed_length: 512,
}];
let p = build_test_dmg(dir.path(), 1, &chunks, &payload);
let mut dmg = DmgBackend::open(&p).unwrap();
assert_eq!(dmg.total_size(), 512);
assert_eq!(dmg.chunk_count(), 1);
let mut out = vec![0u8; 512];
dmg.read_at(0, &mut out).unwrap();
assert_eq!(out, payload);
let mut out2 = vec![0u8; 16];
dmg.read_at(100, &mut out2).unwrap();
assert_eq!(out2, &payload[100..116]);
}
#[test]
fn round_trip_zero_chunk() {
let dir = tempfile::tempdir().unwrap();
let chunks = vec![Chunk {
kind: ChunkType::Zero,
virtual_sector_start: 0,
sector_count: 4,
compressed_offset_in_fork: 0,
compressed_length: 0,
}];
let p = build_test_dmg(dir.path(), 4, &chunks, &[]);
let mut dmg = DmgBackend::open(&p).unwrap();
assert_eq!(dmg.total_size(), 4 * 512);
let mut out = vec![0xAAu8; 1024];
dmg.read_at(256, &mut out).unwrap();
assert!(out.iter().all(|&b| b == 0));
}
#[cfg(feature = "gzip")]
#[test]
fn round_trip_zlib_chunk() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let mut plain = vec![0u8; 1024];
for (i, b) in plain.iter_mut().enumerate() {
*b = ((i * 31 + 7) & 0xFF) as u8;
}
let mut compressed = Vec::new();
{
let mut enc =
flate2::write::ZlibEncoder::new(&mut compressed, flate2::Compression::default());
enc.write_all(&plain).unwrap();
enc.finish().unwrap();
}
let chunks = vec![Chunk {
kind: ChunkType::Zlib,
virtual_sector_start: 0,
sector_count: 2,
compressed_offset_in_fork: 0,
compressed_length: compressed.len() as u64,
}];
let p = build_test_dmg(dir.path(), 2, &chunks, &compressed);
let mut dmg = DmgBackend::open(&p).unwrap();
let mut out = vec![0u8; 1024];
dmg.read_at(0, &mut out).unwrap();
assert_eq!(out, plain);
let mut out2 = vec![0u8; 200];
dmg.read_at(700, &mut out2).unwrap();
assert_eq!(out2, &plain[700..900]);
}
#[cfg(feature = "dmg-bzip2")]
#[test]
fn round_trip_bzip2_chunk() {
let dir = tempfile::tempdir().unwrap();
let mut plain = vec![0u8; 512];
for (i, b) in plain.iter_mut().enumerate() {
*b = ((i * 7 + 3) & 0xFF) as u8;
}
let compressed: &[u8] = include_bytes!("testdata/pattern_512_i7p3.bz2");
let chunks = vec![Chunk {
kind: ChunkType::Bz2,
virtual_sector_start: 0,
sector_count: 1,
compressed_offset_in_fork: 0,
compressed_length: compressed.len() as u64,
}];
let p = build_test_dmg(dir.path(), 1, &chunks, compressed);
let mut dmg = DmgBackend::open(&p).unwrap();
let mut out = vec![0u8; 512];
dmg.read_at(0, &mut out).unwrap();
assert_eq!(out, plain);
}
#[cfg(feature = "lzma")]
#[test]
fn round_trip_lzma_chunk() {
let dir = tempfile::tempdir().unwrap();
let mut plain = vec![0u8; 1024];
for (i, b) in plain.iter_mut().enumerate() {
*b = ((i ^ (i >> 3)) & 0xFF) as u8;
}
let mut compressed = Vec::new();
{
let mut input = std::io::Cursor::new(&plain);
lzma_rs::lzma_compress(&mut input, &mut compressed).unwrap();
}
let chunks = vec![Chunk {
kind: ChunkType::Lzma,
virtual_sector_start: 0,
sector_count: 2,
compressed_offset_in_fork: 0,
compressed_length: compressed.len() as u64,
}];
let p = build_test_dmg(dir.path(), 2, &chunks, &compressed);
let mut dmg = DmgBackend::open(&p).unwrap();
let mut out = vec![0u8; 1024];
dmg.read_at(0, &mut out).unwrap();
assert_eq!(out, plain);
}
#[cfg(feature = "dmg-lzfse")]
#[test]
fn round_trip_lzfse_chunk() {
let dir = tempfile::tempdir().unwrap();
let mut plain = vec![0u8; 4096];
for (i, b) in plain.iter_mut().enumerate() {
*b = ((i * 13 + 5) & 0xFF) as u8;
}
let mut compressed = Vec::new();
lzfse_rust::encode_bytes(&plain, &mut compressed).unwrap();
let chunks = vec![Chunk {
kind: ChunkType::Lzfse,
virtual_sector_start: 0,
sector_count: 8,
compressed_offset_in_fork: 0,
compressed_length: compressed.len() as u64,
}];
let p = build_test_dmg(dir.path(), 8, &chunks, &compressed);
let mut dmg = DmgBackend::open(&p).unwrap();
let mut out = vec![0u8; 4096];
dmg.read_at(0, &mut out).unwrap();
assert_eq!(out, plain);
}
#[test]
fn round_trip_adc_chunk() {
let dir = tempfile::tempdir().unwrap();
let mut stream: Vec<u8> = Vec::new();
stream.push(0x00);
stream.push(0xAA);
let mut emitted: usize = 1;
while emitted < 512 {
let want = (512 - emitted).min(18);
let op = 0x80u8 | (((want - 3) as u8) << 2);
stream.push(op);
stream.push(0x00);
emitted += want;
}
let chunks = vec![Chunk {
kind: ChunkType::Adc,
virtual_sector_start: 0,
sector_count: 1,
compressed_offset_in_fork: 0,
compressed_length: stream.len() as u64,
}];
let p = build_test_dmg(dir.path(), 1, &chunks, &stream);
let mut dmg = DmgBackend::open(&p).unwrap();
let mut out = vec![0u8; 512];
dmg.read_at(0, &mut out).unwrap();
assert!(out.iter().all(|&b| b == 0xAA));
}
#[cfg(feature = "gzip")]
#[test]
fn round_trip_mixed_chunks() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let mut raw_payload = vec![0u8; 1024];
for (i, b) in raw_payload.iter_mut().enumerate() {
*b = (i & 0xFF) as u8;
}
let mut zlib_plain = vec![0u8; 1024];
for (i, b) in zlib_plain.iter_mut().enumerate() {
*b = ((255 - (i & 0xFF)) & 0xFF) as u8;
}
let mut zlib_payload = Vec::new();
{
let mut enc =
flate2::write::ZlibEncoder::new(&mut zlib_payload, flate2::Compression::default());
enc.write_all(&zlib_plain).unwrap();
enc.finish().unwrap();
}
let mut fork = Vec::new();
let raw_off = fork.len() as u64;
fork.extend_from_slice(&raw_payload);
let zlib_off = fork.len() as u64;
fork.extend_from_slice(&zlib_payload);
let chunks = vec![
Chunk {
kind: ChunkType::Zero,
virtual_sector_start: 0,
sector_count: 2,
compressed_offset_in_fork: 0,
compressed_length: 0,
},
Chunk {
kind: ChunkType::Raw,
virtual_sector_start: 2,
sector_count: 2,
compressed_offset_in_fork: raw_off,
compressed_length: 1024,
},
Chunk {
kind: ChunkType::Zlib,
virtual_sector_start: 4,
sector_count: 2,
compressed_offset_in_fork: zlib_off,
compressed_length: zlib_payload.len() as u64,
},
];
let p = build_test_dmg(dir.path(), 6, &chunks, &fork);
let mut dmg = DmgBackend::open(&p).unwrap();
assert_eq!(dmg.chunk_count(), 3);
assert_eq!(dmg.total_size(), 6 * 512);
let mut out = vec![0xAAu8; 6 * 512];
dmg.read_at(0, &mut out).unwrap();
assert!(out[..1024].iter().all(|&b| b == 0), "zero region");
assert_eq!(&out[1024..2048], raw_payload.as_slice(), "raw region");
assert_eq!(&out[2048..3072], zlib_plain.as_slice(), "zlib region");
let mut out2 = vec![0u8; 32];
dmg.read_at(2048 - 16, &mut out2).unwrap();
assert_eq!(&out2[..16], &raw_payload[1008..1024]);
assert_eq!(&out2[16..], &zlib_plain[..16]);
}
#[test]
fn read_at_rejects_out_of_bounds() {
let dir = tempfile::tempdir().unwrap();
let chunks = vec![Chunk {
kind: ChunkType::Zero,
virtual_sector_start: 0,
sector_count: 1,
compressed_offset_in_fork: 0,
compressed_length: 0,
}];
let p = build_test_dmg(dir.path(), 1, &chunks, &[]);
let mut dmg = DmgBackend::open(&p).unwrap();
let mut buf = [0u8; 8];
let err = dmg.read_at(512, &mut buf).unwrap_err();
match err {
crate::Error::OutOfBounds { .. } => {}
_ => panic!("expected OutOfBounds, got {err:?}"),
}
}
}