use std::collections::HashMap;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use quick_xml::events::Event;
use quick_xml::Reader;
use crate::DmgError;
const SPRS_MAGIC: u32 = 0x7370_7273; const SPARSE_HEADER_SIZE: u64 = 4096;
const SECTOR: u64 = 512;
const BAND_TABLE_OFFSET: usize = 0x40;
fn be_u32(data: &[u8], off: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = data.get(off..).and_then(|s| s.get(..4)) {
b.copy_from_slice(s);
}
u32::from_be_bytes(b)
}
pub struct SparseImageReader<R: Read + Seek> {
inner: R,
band_size: u64,
virtual_size: u64,
file_size: u64,
band_slot: HashMap<u64, u64>,
position: u64,
}
impl<R: Read + Seek> SparseImageReader<R> {
pub fn open(mut reader: R) -> Result<Self, DmgError> {
let file_size = reader.seek(SeekFrom::End(0))?;
if file_size < SPARSE_HEADER_SIZE {
return Err(DmgError::BadSparseHeader(format!(
"file too small for 4096-byte sparse header: {file_size} bytes"
)));
}
reader.seek(SeekFrom::Start(0))?;
let mut header = [0u8; SPARSE_HEADER_SIZE as usize];
reader.read_exact(&mut header)?;
let magic = be_u32(&header, 0x00);
if magic != SPRS_MAGIC {
return Err(DmgError::NotSparseImage(magic));
}
let sectors_per_band = u64::from(be_u32(&header, 0x08));
if sectors_per_band == 0 {
return Err(DmgError::BadSparseHeader(
"sectors_per_band is 0 (would divide by zero)".into(),
));
}
let band_size = sectors_per_band.saturating_mul(SECTOR);
let total_sectors = u64::from(be_u32(&header, 0x10));
let virtual_size = total_sectors.saturating_mul(SECTOR);
let phys_region = file_size.saturating_sub(SPARSE_HEADER_SIZE);
let nphys = phys_region.div_ceil(band_size);
let max_slots = (SPARSE_HEADER_SIZE as usize - BAND_TABLE_OFFSET) / 4;
if nphys > max_slots as u64 {
return Err(DmgError::BadSparseHeader(format!(
"{nphys} physical bands exceed the single 4096-byte band table (max {max_slots}); multi-node tables unsupported"
)));
}
let mut band_slot = HashMap::new();
for slot in 0..nphys {
let v = be_u32(&header, BAND_TABLE_OFFSET + slot as usize * 4);
if v != 0 {
band_slot.insert(u64::from(v) - 1, slot);
}
}
Ok(Self {
inner: reader,
band_size,
virtual_size,
file_size,
band_slot,
position: 0,
})
}
pub fn virtual_disk_size(&self) -> u64 {
self.virtual_size
}
}
impl<R: Read + Seek> Read for SparseImageReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() || self.position >= self.virtual_size {
return Ok(0);
}
let vband = self.position / self.band_size;
let off = self.position % self.band_size;
let band_remaining = self.band_size - off;
let disk_remaining = self.virtual_size - self.position;
let to_read = (buf.len() as u64).min(band_remaining).min(disk_remaining) as usize;
match self.band_slot.get(&vband) {
None => buf[..to_read].fill(0),
Some(&slot) => {
let file_off = SPARSE_HEADER_SIZE
.saturating_add(slot.saturating_mul(self.band_size))
.saturating_add(off);
if file_off >= self.file_size {
buf[..to_read].fill(0);
} else {
let avail = (self.file_size - file_off).min(to_read as u64) as usize;
self.inner.seek(SeekFrom::Start(file_off))?;
self.inner.read_exact(&mut buf[..avail])?;
if avail < to_read {
buf[avail..to_read].fill(0);
}
}
}
}
self.position += to_read as u64;
Ok(to_read)
}
}
impl<R: Read + Seek> Seek for SparseImageReader<R> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.position = seek_within(self.position, self.virtual_size, pos);
Ok(self.position)
}
}
fn seek_within(current: u64, size: u64, pos: SeekFrom) -> u64 {
match pos {
SeekFrom::Start(n) => n,
SeekFrom::End(n) => {
if n >= 0 {
size.saturating_add(n as u64)
} else {
size.saturating_sub(n.unsigned_abs())
}
}
SeekFrom::Current(n) => {
if n >= 0 {
current.saturating_add(n as u64)
} else {
current.saturating_sub(n.unsigned_abs())
}
}
}
}
const SPARSEBUNDLE_TYPE: &str = "com.apple.diskimage.sparsebundle";
pub struct SparseBundleReader {
bands_dir: PathBuf,
band_size: u64,
virtual_size: u64,
position: u64,
}
impl SparseBundleReader {
pub fn open(dir: &Path) -> Result<Self, DmgError> {
let info_path = dir.join("Info.plist");
let xml = match std::fs::read_to_string(&info_path) {
Ok(s) => s,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
return Err(DmgError::MissingInfoPlist)
}
Err(e) => return Err(DmgError::Io(e)),
};
let info = parse_info_plist(&xml)?;
match info.bundle_type.as_deref() {
Some(SPARSEBUNDLE_TYPE) => {}
other => {
return Err(DmgError::BadInfoPlist(format!(
"diskimage-bundle-type is {other:?}, expected {SPARSEBUNDLE_TYPE:?}"
)))
}
}
let band_size = info
.band_size
.ok_or_else(|| DmgError::BadInfoPlist("missing band-size key".into()))?;
if band_size == 0 {
return Err(DmgError::BadInfoPlist(
"band-size is 0 (would divide by zero)".into(),
));
}
let virtual_size = info
.size
.ok_or_else(|| DmgError::BadInfoPlist("missing size key".into()))?;
Ok(Self {
bands_dir: dir.join("bands"),
band_size,
virtual_size,
position: 0,
})
}
pub fn virtual_disk_size(&self) -> u64 {
self.virtual_size
}
}
impl Read for SparseBundleReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() || self.position >= self.virtual_size {
return Ok(0);
}
let band = self.position / self.band_size;
let off = self.position % self.band_size;
let band_remaining = self.band_size - off;
let disk_remaining = self.virtual_size - self.position;
let to_read = (buf.len() as u64).min(band_remaining).min(disk_remaining) as usize;
let path = self.bands_dir.join(format!("{band:x}"));
match std::fs::File::open(&path) {
Ok(mut f) => {
let flen = f.seek(SeekFrom::End(0))?;
if off >= flen {
buf[..to_read].fill(0);
} else {
let avail = (flen - off).min(to_read as u64) as usize;
f.seek(SeekFrom::Start(off))?;
f.read_exact(&mut buf[..avail])?;
if avail < to_read {
buf[avail..to_read].fill(0);
}
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => buf[..to_read].fill(0),
Err(e) => return Err(e),
}
self.position += to_read as u64;
Ok(to_read)
}
}
impl Seek for SparseBundleReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.position = seek_within(self.position, self.virtual_size, pos);
Ok(self.position)
}
}
struct BundleInfo {
band_size: Option<u64>,
size: Option<u64>,
bundle_type: Option<String>,
}
fn parse_info_plist(xml: &str) -> Result<BundleInfo, DmgError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut band_size = None;
let mut size = None;
let mut bundle_type = None;
let mut current_key: Option<String> = None;
let mut elem: Option<Vec<u8>> = None;
loop {
match reader.read_event() {
Ok(Event::Start(e)) => elem = Some(e.name().as_ref().to_vec()),
Ok(Event::Text(e)) => {
let text = e
.xml_content(quick_xml::XmlVersion::Implicit1_0)
.unwrap_or_default();
let t = text.trim();
if t.is_empty() {
continue;
}
match elem.as_deref() {
Some(b"key") => current_key = Some(t.to_string()),
Some(b"integer") => {
let key = current_key.as_deref();
if key == Some("band-size") || key == Some("size") {
let v: u64 = t.parse().map_err(|_| {
DmgError::BadInfoPlist(format!(
"non-integer value {t:?} for key {key:?}"
))
})?;
if key == Some("band-size") {
band_size = Some(v);
} else {
size = Some(v);
}
}
}
Some(b"string") if current_key.as_deref() == Some("diskimage-bundle-type") => {
bundle_type = Some(t.to_string());
}
_ => {}
}
}
Ok(Event::End(_)) => elem = None,
Ok(Event::Eof) => break,
Err(e) => return Err(DmgError::BadInfoPlist(e.to_string())),
_ => {}
}
}
Ok(BundleInfo {
band_size,
size,
bundle_type,
})
}
#[cfg(test)]
mod sparseimage_tests {
use super::SparseImageReader;
use crate::DmgError;
use std::io::{Cursor, Read, Seek, SeekFrom};
const HDR: usize = 4096;
fn build(spb: u32, bands: &[Option<Vec<u8>>]) -> Vec<u8> {
let band_size = spb as usize * 512;
let total_sectors = (bands.len() * spb as usize) as u32;
let mut file = vec![0u8; HDR];
file[0..4].copy_from_slice(&0x7370_7273u32.to_be_bytes()); file[4..8].copy_from_slice(&3u32.to_be_bytes()); file[8..12].copy_from_slice(&spb.to_be_bytes()); file[16..20].copy_from_slice(&total_sectors.to_be_bytes()); let mut slot = 0usize;
for (v, b) in bands.iter().enumerate() {
if let Some(content) = b {
assert_eq!(content.len(), band_size, "band must be band_size");
let o = 0x40 + slot * 4;
file[o..o + 4].copy_from_slice(&((v as u32) + 1).to_be_bytes());
file.extend_from_slice(content);
slot += 1;
}
}
file
}
fn sample() -> Vec<u8> {
let mut b0 = vec![0xAAu8; 1024];
b0[0..4].copy_from_slice(&[0x48, 0x2b, 0x00, 0x04]);
let b2 = vec![0xCCu8; 1024];
build(2, &[Some(b0), None, Some(b2)])
}
#[test]
fn bad_magic_is_not_sparse_image() {
let mut f = sample();
f[0] = 0;
assert!(matches!(
SparseImageReader::open(Cursor::new(f)),
Err(DmgError::NotSparseImage(_))
));
}
#[test]
fn zero_sectors_per_band_is_bad_header() {
let mut f = sample();
f[8..12].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(
SparseImageReader::open(Cursor::new(f)),
Err(DmgError::BadSparseHeader(_))
));
}
#[test]
fn file_too_small_is_bad_header() {
assert!(matches!(
SparseImageReader::open(Cursor::new(vec![0u8; 100])),
Err(DmgError::BadSparseHeader(_))
));
}
#[test]
fn too_many_physical_bands_is_bad_header() {
let mut f = vec![0u8; HDR + 1009 * 512];
f[0..4].copy_from_slice(&0x7370_7273u32.to_be_bytes());
f[8..12].copy_from_slice(&1u32.to_be_bytes()); f[16..20].copy_from_slice(&1009u32.to_be_bytes());
assert!(matches!(
SparseImageReader::open(Cursor::new(f)),
Err(DmgError::BadSparseHeader(_))
));
}
#[test]
fn virtual_disk_size_is_total_sectors_times_512() {
let r = SparseImageReader::open(Cursor::new(sample())).unwrap();
assert_eq!(r.virtual_disk_size(), 6 * 512);
}
#[test]
fn reads_allocated_band_magic() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
let mut buf = [0u8; 4];
r.read_exact(&mut buf).unwrap();
assert_eq!(buf, [0x48, 0x2b, 0x00, 0x04]);
}
#[test]
fn hole_band_reads_zeros() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
r.seek(SeekFrom::Start(1024)).unwrap();
let mut buf = [0xFFu8; 1024];
r.read_exact(&mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn second_allocated_band_reads_content() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
r.seek(SeekFrom::Start(2048)).unwrap();
let mut buf = [0u8; 4];
r.read_exact(&mut buf).unwrap();
assert_eq!(buf, [0xCC, 0xCC, 0xCC, 0xCC]);
}
#[test]
fn read_across_band_boundary() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
r.seek(SeekFrom::Start(1023)).unwrap();
let mut buf = [0xEEu8; 2];
r.read_exact(&mut buf).unwrap();
assert_eq!(buf, [0xAA, 0x00]);
}
#[test]
fn seek_within_band_reads_offset() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
r.seek(SeekFrom::Start(10)).unwrap();
let mut buf = [0u8; 1];
r.read_exact(&mut buf).unwrap();
assert_eq!(buf[0], 0xAA);
}
#[test]
fn seek_from_end_and_current() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
assert_eq!(r.seek(SeekFrom::End(0)).unwrap(), 6 * 512);
assert_eq!(r.seek(SeekFrom::End(-2048)).unwrap(), 1024);
assert_eq!(r.seek(SeekFrom::Current(1024)).unwrap(), 2048);
assert_eq!(r.seek(SeekFrom::Current(-2048)).unwrap(), 0);
}
#[test]
fn read_past_eof_returns_zero() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
r.seek(SeekFrom::Start(6 * 512)).unwrap();
let mut buf = [0u8; 16];
assert_eq!(r.read(&mut buf).unwrap(), 0);
}
#[test]
fn empty_buffer_reads_zero() {
let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
assert_eq!(r.read(&mut []).unwrap(), 0);
}
#[test]
fn unmapped_virtual_band_reads_zeros() {
let f = build(2, &[Some(vec![0xAAu8; 1024]), None, None]);
let mut r = SparseImageReader::open(Cursor::new(f)).unwrap();
r.seek(SeekFrom::Start(2048)).unwrap();
let mut buf = [0xFFu8; 512];
r.read_exact(&mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn truncated_physical_band_tail_reads_zeros() {
let mut f = build(2, &[Some(vec![0xAAu8; 1024])]);
f.truncate(HDR + 512);
let mut r = SparseImageReader::open(Cursor::new(f)).unwrap();
let mut buf = [0u8; 1024];
r.read_exact(&mut buf).unwrap();
assert!(buf[..512].iter().all(|&b| b == 0xAA));
assert!(buf[512..].iter().all(|&b| b == 0));
}
#[test]
fn read_starting_in_truncated_region_reads_zeros() {
let mut f = build(2, &[Some(vec![0xAAu8; 1024])]);
f.truncate(HDR + 512);
let mut r = SparseImageReader::open(Cursor::new(f)).unwrap();
r.seek(SeekFrom::Start(600)).unwrap();
let mut buf = [0xFFu8; 8];
r.read_exact(&mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}
}
#[cfg(test)]
mod sparsebundle_tests {
use super::SparseBundleReader;
use crate::DmgError;
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::TempDir;
fn bundle(plist: &str, bands: &[(u64, Vec<u8>)]) -> TempDir {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Info.plist"), plist).unwrap();
let bands_dir = dir.path().join("bands");
fs::create_dir(&bands_dir).unwrap();
for (idx, bytes) in bands {
let mut f = fs::File::create(bands_dir.join(format!("{idx:x}"))).unwrap();
f.write_all(bytes).unwrap();
}
dir
}
fn plist(band_size: Option<u64>, size: Option<u64>, bundle_type: Option<&str>) -> String {
use std::fmt::Write as _;
let mut body = String::from(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\">\n<dict>\n",
);
if let Some(t) = bundle_type {
let _ = writeln!(
body,
" <key>diskimage-bundle-type</key><string>{t}</string>"
);
}
if let Some(b) = band_size {
let _ = writeln!(body, " <key>band-size</key><integer>{b}</integer>");
}
if let Some(s) = size {
let _ = writeln!(body, " <key>size</key><integer>{s}</integer>");
}
body.push_str("</dict>\n</plist>\n");
body
}
fn valid_plist() -> String {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\">\n<dict>\n\
\t<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>\n\
\t<key>band-size</key><integer>1024</integer>\n\
\t<key>bundle-backingstore-version</key><integer>1</integer>\n\
\t<key>diskimage-bundle-type</key><string>com.apple.diskimage.sparsebundle</string>\n\
\t<key>pad</key><string> </string>\n\
\t<key>size</key><integer>3072</integer>\n\
</dict>\n</plist>\n"
.to_string()
}
fn valid_bundle() -> TempDir {
let mut b0 = vec![0xAAu8; 1024];
b0[0..4].copy_from_slice(&[0x48, 0x2b, 0x00, 0x04]);
let b2 = vec![0xCCu8; 512];
bundle(&valid_plist(), &[(0, b0), (2, b2)])
}
#[test]
fn missing_info_plist_errors() {
let dir = TempDir::new().unwrap();
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::MissingInfoPlist)
));
}
#[test]
fn malformed_plist_errors() {
let dir = bundle("<plist><dict><key>oops", &[]);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
#[test]
fn wrong_bundle_type_errors() {
let dir = bundle(
&plist(Some(1024), Some(3072), Some("com.apple.diskimage.sparse")),
&[],
);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
#[test]
fn missing_band_size_errors() {
let dir = bundle(
&plist(None, Some(3072), Some("com.apple.diskimage.sparsebundle")),
&[],
);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
#[test]
fn zero_band_size_errors() {
let dir = bundle(
&plist(
Some(0),
Some(3072),
Some("com.apple.diskimage.sparsebundle"),
),
&[],
);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
#[test]
fn missing_size_errors() {
let dir = bundle(
&plist(Some(1024), None, Some("com.apple.diskimage.sparsebundle")),
&[],
);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
#[test]
fn non_integer_band_size_errors() {
let xml = "<plist version=\"1.0\">\n<dict>\n\
<key>diskimage-bundle-type</key><string>com.apple.diskimage.sparsebundle</string>\n\
<key>band-size</key><integer>notanumber</integer>\n\
<key>size</key><integer>3072</integer>\n</dict>\n</plist>\n";
let dir = bundle(xml, &[]);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
#[test]
fn virtual_disk_size_matches_plist_size() {
let dir = valid_bundle();
let r = SparseBundleReader::open(dir.path()).unwrap();
assert_eq!(r.virtual_disk_size(), 3072);
}
#[test]
fn reads_present_band_magic() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
let mut buf = [0u8; 4];
r.read_exact(&mut buf).unwrap();
assert_eq!(buf, [0x48, 0x2b, 0x00, 0x04]);
}
#[test]
fn missing_band_file_reads_zeros() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
r.seek(SeekFrom::Start(1024)).unwrap(); let mut buf = [0xFFu8; 1024];
r.read_exact(&mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn partial_band_tail_reads_zeros() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
r.seek(SeekFrom::Start(2048)).unwrap(); let mut buf = [0u8; 1024];
r.read_exact(&mut buf).unwrap();
assert!(buf[..512].iter().all(|&b| b == 0xCC));
assert!(buf[512..].iter().all(|&b| b == 0));
}
#[test]
fn read_across_band_boundary() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
r.seek(SeekFrom::Start(1023)).unwrap();
let mut buf = [0xEEu8; 2];
r.read_exact(&mut buf).unwrap(); assert_eq!(buf, [0xAA, 0x00]);
}
#[test]
fn seek_within_band_reads_offset() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
r.seek(SeekFrom::Start(500)).unwrap();
let mut buf = [0u8; 1];
r.read_exact(&mut buf).unwrap();
assert_eq!(buf[0], 0xAA);
}
#[test]
fn seek_from_end_and_current() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
assert_eq!(r.seek(SeekFrom::End(0)).unwrap(), 3072);
assert_eq!(r.seek(SeekFrom::End(-1024)).unwrap(), 2048);
assert_eq!(r.seek(SeekFrom::Current(-2048)).unwrap(), 0);
}
#[test]
fn read_past_eof_returns_zero() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
r.seek(SeekFrom::Start(3072)).unwrap();
let mut buf = [0u8; 16];
assert_eq!(r.read(&mut buf).unwrap(), 0);
}
#[test]
fn empty_buffer_reads_zero() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
assert_eq!(r.read(&mut []).unwrap(), 0);
}
#[test]
fn read_wholly_past_short_band_reads_zeros() {
let dir = valid_bundle();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
r.seek(SeekFrom::Start(2560)).unwrap();
let mut buf = [0xFFu8; 512];
r.read_exact(&mut buf).unwrap();
assert!(buf.iter().all(|&b| b == 0));
}
#[test]
fn bands_path_that_is_a_file_errors_on_read() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Info.plist"), valid_plist()).unwrap();
fs::write(dir.path().join("bands"), b"not a directory").unwrap();
let mut r = SparseBundleReader::open(dir.path()).unwrap();
let mut buf = [0u8; 16];
assert!(r.read(&mut buf).is_err());
}
#[test]
fn info_plist_that_is_a_directory_is_io_error() {
let dir = TempDir::new().unwrap();
fs::create_dir(dir.path().join("Info.plist")).unwrap();
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::Io(_))
));
}
#[test]
fn mismatched_xml_tags_error() {
let dir = bundle("<plist version=\"1.0\">\n<dict></nope>\n</plist>\n", &[]);
assert!(matches!(
SparseBundleReader::open(dir.path()),
Err(DmgError::BadInfoPlist(_))
));
}
}