1use std::io::{self, Cursor, Read, Seek, SeekFrom};
13
14use base64::Engine;
15use flate2::read::ZlibDecoder;
16use quick_xml::events::Event;
17use quick_xml::Reader;
18use thiserror::Error;
19
20const KOLY_MAGIC: u32 = 0x6B6F6C79; const MISH_MAGIC: u32 = 0x6D697368; const KOLY_SIZE: u64 = 512;
23
24const BLK_ZERO: u32 = 0x0000_0000;
25const BLK_RAW: u32 = 0x0000_0001;
26const BLK_IGNORE: u32 = 0x0000_0002;
27const BLK_ZLIB: u32 = 0x8000_0005;
28const BLK_COMMENT: u32 = 0x7FFF_FFFE;
29const BLK_TERM: u32 = 0xFFFF_FFFF;
30
31#[derive(Debug, Error)]
33pub enum DmgError {
34 #[error("I/O error: {0}")]
35 Io(#[from] io::Error),
36 #[error("not a DMG: missing koly magic")]
37 NotADmg,
38 #[error("file too small to contain koly trailer")]
39 FileTooSmall,
40 #[error("invalid mish block: {0}")]
41 BadMish(String),
42 #[error("invalid plist XML: {0}")]
43 BadPlist(String),
44 #[error("decompression error: {0}")]
45 Compression(String),
46 #[error("unsupported compression type: {0:#010x}")]
47 NotSupported(u32),
48}
49
50#[derive(Debug, Clone)]
52struct BlkxRun {
53 entry_type: u32,
54 sector_start: u64,
55 sector_count: u64,
56 data_offset: u64,
58 data_length: u64,
59}
60
61#[derive(Debug, Clone)]
63struct Partition {
64 file_data_offset: u64,
66 sector_base: u64,
68 runs: Vec<BlkxRun>,
69}
70
71impl Partition {
72 fn total_sectors(&self) -> u64 {
74 self.runs
75 .iter()
76 .filter(|r| r.entry_type != BLK_COMMENT && r.entry_type != BLK_TERM)
77 .map(|r| r.sector_start + r.sector_count)
78 .max()
79 .unwrap_or(0)
80 }
81
82 fn contains_sector(&self, vsec: u64) -> bool {
83 if vsec < self.sector_base {
84 return false;
85 }
86 let local = vsec - self.sector_base;
87 local < self.total_sectors()
88 }
89
90 fn run_for(&self, local_sec: u64) -> Option<&BlkxRun> {
92 self.runs.iter().find(|r| {
93 r.entry_type != BLK_TERM
94 && r.entry_type != BLK_COMMENT
95 && local_sec >= r.sector_start
96 && local_sec < r.sector_start + r.sector_count
97 })
98 }
99}
100
101pub struct DmgReader<R: Read + Seek> {
103 inner: R,
104 sector_count: u64,
105 partitions: Vec<Partition>,
106 position: u64,
107}
108
109impl<R: Read + Seek> DmgReader<R> {
110 pub fn open(mut reader: R) -> Result<Self, DmgError> {
112 let file_size = reader.seek(SeekFrom::End(0))?;
114 if file_size < KOLY_SIZE {
115 return Err(DmgError::FileTooSmall);
116 }
117
118 reader.seek(SeekFrom::Start(file_size - KOLY_SIZE))?;
120 let mut koly = [0u8; 512];
121 reader.read_exact(&mut koly)?;
122
123 let magic = u32::from_be_bytes(koly[0..4].try_into().unwrap());
124 if magic != KOLY_MAGIC {
125 return Err(DmgError::NotADmg);
126 }
127
128 let xml_offset = u64::from_be_bytes(koly[216..224].try_into().unwrap());
129 let xml_length = u64::from_be_bytes(koly[224..232].try_into().unwrap());
130 let sector_count = u64::from_be_bytes(koly[492..500].try_into().unwrap());
131
132 reader.seek(SeekFrom::Start(xml_offset))?;
134 let mut xml_bytes = vec![0u8; xml_length as usize];
135 reader.read_exact(&mut xml_bytes)?;
136 let xml = std::str::from_utf8(&xml_bytes).map_err(|e| DmgError::BadPlist(e.to_string()))?;
137
138 let partitions = parse_plist(xml)?;
139
140 Ok(Self {
141 inner: reader,
142 sector_count,
143 partitions,
144 position: 0,
145 })
146 }
147
148 pub fn virtual_disk_size(&self) -> u64 {
150 self.sector_count * 512
151 }
152}
153
154impl<R: Read + Seek> Read for DmgReader<R> {
155 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
156 if buf.is_empty() {
157 return Ok(0);
158 }
159 let disk_size = self.virtual_disk_size();
160 if self.position >= disk_size {
161 return Ok(0);
162 }
163
164 let vsec = self.position / 512;
165 let sec_offset = self.position % 512;
166
167 let part = self
169 .partitions
170 .iter()
171 .find(|p| p.contains_sector(vsec))
172 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no partition"))?;
173
174 let local_sec = vsec - part.sector_base;
175 let run = part
176 .run_for(local_sec)
177 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no run"))?;
178
179 let bytes_into_run = (local_sec - run.sector_start) * 512 + sec_offset;
181 let run_total_bytes = run.sector_count * 512;
182 let available_in_run = run_total_bytes.saturating_sub(bytes_into_run);
183 let to_read = buf.len().min(available_in_run as usize);
184
185 match run.entry_type {
186 BLK_ZERO | BLK_IGNORE => {
187 buf[..to_read].fill(0);
188 }
189 BLK_RAW => {
190 let file_pos = part.file_data_offset + run.data_offset + bytes_into_run;
191 self.inner.seek(SeekFrom::Start(file_pos))?;
192 self.inner.read_exact(&mut buf[..to_read])?;
193 }
194 BLK_ZLIB => {
195 let file_pos = part.file_data_offset + run.data_offset;
197 self.inner.seek(SeekFrom::Start(file_pos))?;
198 let mut compressed = vec![0u8; run.data_length as usize];
199 self.inner.read_exact(&mut compressed)?;
200 let mut decompressed = Vec::with_capacity(run.sector_count as usize * 512);
201 ZlibDecoder::new(Cursor::new(compressed))
202 .read_to_end(&mut decompressed)
203 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
204 let start = bytes_into_run as usize;
205 let end = (start + to_read).min(decompressed.len());
206 if start >= decompressed.len() {
207 return Err(io::Error::new(
208 io::ErrorKind::UnexpectedEof,
209 "zlib underrun",
210 ));
211 }
212 buf[..end - start].copy_from_slice(&decompressed[start..end]);
213 }
214 t => {
215 return Err(io::Error::new(
216 io::ErrorKind::Unsupported,
217 format!("unsupported block type {t:#010x}"),
218 ));
219 }
220 }
221
222 self.position += to_read as u64;
223 Ok(to_read)
224 }
225}
226
227impl<R: Read + Seek> Seek for DmgReader<R> {
228 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
229 let disk_size = self.virtual_disk_size();
230 let new_pos = match pos {
231 SeekFrom::Start(n) => n,
232 SeekFrom::End(n) => {
233 if n >= 0 {
234 disk_size.saturating_add(n as u64)
235 } else {
236 disk_size.saturating_sub((-n) as u64)
237 }
238 }
239 SeekFrom::Current(n) => {
240 if n >= 0 {
241 self.position.saturating_add(n as u64)
242 } else {
243 self.position.saturating_sub((-n) as u64)
244 }
245 }
246 };
247 self.position = new_pos;
248 Ok(self.position)
249 }
250}
251
252fn parse_plist(xml: &str) -> Result<Vec<Partition>, DmgError> {
256 let mut reader = Reader::from_str(xml);
257 reader.config_mut().trim_text(true);
258
259 let mut in_blkx = false;
260 let mut in_data = false;
261 let mut last_key = String::new();
262 let mut partitions = Vec::new();
263
264 loop {
265 match reader.read_event() {
266 Ok(Event::Start(e)) => match e.name().as_ref() {
267 b"key" => {}
268 b"array" if last_key == "blkx" => {
269 in_blkx = true;
270 }
271 b"data" if in_blkx => {
272 in_data = true;
273 }
274 _ => {}
275 },
276 Ok(Event::Text(e)) => {
277 let text = e.unescape().unwrap_or_default();
278 let trimmed = text.trim();
279 if e.is_empty() || trimmed.is_empty() {
280 continue;
281 }
282 if trimmed != "blkx" && !in_blkx {
284 last_key = trimmed.to_string();
285 continue;
286 }
287 if trimmed == "blkx" {
288 last_key = "blkx".to_string();
289 continue;
290 }
291 if in_data && in_blkx {
292 let cleaned: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
294 let raw = base64::engine::general_purpose::STANDARD
295 .decode(cleaned.as_bytes())
296 .map_err(|e| DmgError::BadPlist(e.to_string()))?;
297 let partition = parse_mish(&raw)?;
298 partitions.push(partition);
299 in_data = false;
300 }
301 }
302 Ok(Event::End(e)) => {
303 if e.name().as_ref() == b"array" {
304 in_blkx = false;
305 }
306 }
307 Ok(Event::Eof) => break,
308 Err(e) => return Err(DmgError::BadPlist(e.to_string())),
309 _ => {}
310 }
311 }
312 Ok(partitions)
313}
314
315fn parse_mish(data: &[u8]) -> Result<Partition, DmgError> {
331 if data.len() < 204 {
332 return Err(DmgError::BadMish("too short".into()));
333 }
334 let magic = u32::from_be_bytes(data[0..4].try_into().unwrap());
335 if magic != MISH_MAGIC {
336 return Err(DmgError::BadMish(format!("bad magic {magic:#010x}")));
337 }
338 let sector_number = u64::from_be_bytes(data[8..16].try_into().unwrap());
339 let file_data_offset = u64::from_be_bytes(data[24..32].try_into().unwrap());
340 let block_descriptors = u32::from_be_bytes(data[200..204].try_into().unwrap()) as usize;
341
342 let runs_start = 204;
343 let run_size = 40;
344 if data.len() < runs_start + block_descriptors * run_size {
345 return Err(DmgError::BadMish("truncated run list".into()));
346 }
347
348 let mut runs = Vec::with_capacity(block_descriptors);
349 for i in 0..block_descriptors {
350 let o = runs_start + i * run_size;
351 let entry_type = u32::from_be_bytes(data[o..o + 4].try_into().unwrap());
352 let sector_start = u64::from_be_bytes(data[o + 8..o + 16].try_into().unwrap());
353 let sector_count = u64::from_be_bytes(data[o + 16..o + 24].try_into().unwrap());
354 let data_offset = u64::from_be_bytes(data[o + 24..o + 32].try_into().unwrap());
355 let data_length = u64::from_be_bytes(data[o + 32..o + 40].try_into().unwrap());
356 runs.push(BlkxRun {
357 entry_type,
358 sector_start,
359 sector_count,
360 data_offset,
361 data_length,
362 });
363 if entry_type == BLK_TERM {
364 break;
365 }
366 }
367
368 Ok(Partition {
369 file_data_offset,
370 sector_base: sector_number,
371 runs,
372 })
373}
374
375#[cfg(test)]
378mod tests {
379 use super::*;
380 use std::io::Cursor;
381
382 struct RunDef {
386 entry_type: u32,
387 sector_start: u64,
388 sector_count: u64,
389 data: Vec<u8>, }
391
392 fn make_dmg(sector_count: u64, runs: Vec<RunDef>) -> Vec<u8> {
399 let mut file: Vec<u8> = Vec::new();
400
401 let mish_data_offset = 0u64; let mut run_file_offsets: Vec<u64> = Vec::new();
404 for r in &runs {
405 run_file_offsets.push(file.len() as u64);
406 file.extend_from_slice(&r.data);
407 }
408
409 let block_descriptors = runs.len() + 1; let total_data_written: u64 = run_file_offsets.last().map_or(0, |&off| {
413 let last = &runs[runs.len() - 1];
414 off + last.data.len() as u64
415 });
416 let mut mish: Vec<u8> = Vec::new();
417 mish.extend_from_slice(&MISH_MAGIC.to_be_bytes()); mish.extend_from_slice(&1u32.to_be_bytes()); mish.extend_from_slice(&0u64.to_be_bytes()); mish.extend_from_slice(§or_count.to_be_bytes()); mish.extend_from_slice(&mish_data_offset.to_be_bytes()); mish.extend_from_slice(&0u32.to_be_bytes()); mish.extend_from_slice(&[0u8; 28]); mish.extend_from_slice(&2u32.to_be_bytes()); mish.extend_from_slice(&32u32.to_be_bytes()); mish.extend_from_slice(&[0u8; 128]); mish.extend_from_slice(&(block_descriptors as u32).to_be_bytes()); for (i, r) in runs.iter().enumerate() {
432 let data_off = run_file_offsets[i];
433 let data_len = r.data.len() as u64;
434 mish.extend_from_slice(&r.entry_type.to_be_bytes());
435 mish.extend_from_slice(&0u32.to_be_bytes()); mish.extend_from_slice(&r.sector_start.to_be_bytes());
437 mish.extend_from_slice(&r.sector_count.to_be_bytes());
438 mish.extend_from_slice(&data_off.to_be_bytes());
439 mish.extend_from_slice(&data_len.to_be_bytes());
440 }
441 mish.extend_from_slice(&BLK_TERM.to_be_bytes()); mish.extend_from_slice(&0u32.to_be_bytes()); mish.extend_from_slice(§or_count.to_be_bytes()); mish.extend_from_slice(&0u64.to_be_bytes()); mish.extend_from_slice(&total_data_written.to_be_bytes()); mish.extend_from_slice(&0u64.to_be_bytes()); let mish_b64 = base64::engine::general_purpose::STANDARD.encode(&mish);
451
452 let xml = format!(
454 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
455 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"\">\n\
456 <plist version=\"1.0\">\n\
457 <dict>\n <key>resource-fork</key>\n <dict>\n\
458 <key>blkx</key>\n<array>\n<dict>\n\
459 <key>Data</key><data>{mish_b64}</data>\n\
460 </dict>\n</array>\n </dict>\n</dict>\n</plist>\n"
461 );
462
463 let xml_offset = file.len() as u64;
464 let xml_length = xml.len() as u64;
465 file.extend_from_slice(xml.as_bytes());
466
467 let mut koly = [0u8; 512];
469 koly[0..4].copy_from_slice(&KOLY_MAGIC.to_be_bytes());
470 koly[4..8].copy_from_slice(&4u32.to_be_bytes()); koly[8..12].copy_from_slice(&512u32.to_be_bytes()); koly[216..224].copy_from_slice(&xml_offset.to_be_bytes());
473 koly[224..232].copy_from_slice(&xml_length.to_be_bytes());
474 koly[492..500].copy_from_slice(§or_count.to_be_bytes());
475 file.extend_from_slice(&koly);
476 file
477 }
478
479 fn raw_run(sector_start: u64, data: Vec<u8>) -> RunDef {
480 assert!(data.len() % 512 == 0, "raw data must be sector-aligned");
481 RunDef {
482 entry_type: BLK_RAW,
483 sector_start,
484 sector_count: data.len() as u64 / 512,
485 data,
486 }
487 }
488
489 fn zero_run(sector_start: u64, sector_count: u64) -> RunDef {
490 RunDef {
491 entry_type: BLK_ZERO,
492 sector_start,
493 sector_count,
494 data: vec![],
495 }
496 }
497
498 fn zlib_run(sector_start: u64, uncompressed: &[u8]) -> RunDef {
499 use flate2::{write::ZlibEncoder, Compression};
500 use std::io::Write;
501 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
502 enc.write_all(uncompressed).unwrap();
503 let compressed = enc.finish().unwrap();
504 RunDef {
505 entry_type: BLK_ZLIB,
506 sector_start,
507 sector_count: uncompressed.len() as u64 / 512,
508 data: compressed,
509 }
510 }
511
512 #[test]
515 fn file_too_small_returns_err() {
516 let result = DmgReader::open(Cursor::new(b"tiny"));
517 assert!(matches!(result, Err(DmgError::FileTooSmall)));
518 }
519
520 #[test]
521 fn not_a_dmg_returns_err() {
522 let result = DmgReader::open(Cursor::new(vec![0u8; 512]));
524 assert!(matches!(result, Err(DmgError::NotADmg)));
525 }
526
527 #[test]
528 fn virtual_disk_size_is_512_times_sector_count() {
529 let payload = vec![0xBBu8; 512];
530 let dmg = make_dmg(1, vec![raw_run(0, payload)]);
531 let reader = DmgReader::open(Cursor::new(dmg)).expect("open");
532 assert_eq!(reader.virtual_disk_size(), 512);
533 }
534
535 #[test]
536 fn read_raw_block_returns_correct_bytes() {
537 let payload: Vec<u8> = (0u8..=255).cycle().take(512).collect();
538 let dmg = make_dmg(1, vec![raw_run(0, payload.clone())]);
539 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
540 let mut buf = vec![0u8; 512];
541 reader.read_exact(&mut buf).expect("read_exact");
542 assert_eq!(buf, payload);
543 }
544
545 #[test]
546 fn read_zeroed_block_returns_zeros() {
547 let dmg = make_dmg(2, vec![zero_run(0, 2)]);
548 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
549 let mut buf = vec![0xFFu8; 512];
550 reader.read_exact(&mut buf).expect("read_exact");
551 assert!(buf.iter().all(|&b| b == 0), "expected all zeros");
552 }
553
554 #[test]
555 fn seek_and_read_at_offset() {
556 let mut payload = vec![0u8; 512];
557 payload[100] = 0xAB;
558 payload[101] = 0xCD;
559 let dmg = make_dmg(1, vec![raw_run(0, payload)]);
560 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
561 reader.seek(SeekFrom::Start(100)).expect("seek");
562 let mut buf = [0u8; 2];
563 reader.read_exact(&mut buf).expect("read");
564 assert_eq!(buf, [0xAB, 0xCD]);
565 }
566
567 #[test]
568 fn read_across_run_boundary() {
569 let mut sector0 = vec![0xAAu8; 512];
570 sector0[511] = 0xBB;
571 let mut sector1 = vec![0xCCu8; 512];
572 sector1[0] = 0xDD;
573 let mut payload = sector0;
574 payload.extend_from_slice(§or1);
575 let dmg = make_dmg(2, vec![raw_run(0, payload)]);
576 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
577 reader.seek(SeekFrom::Start(511)).expect("seek");
578 let mut buf = [0u8; 2];
579 reader.read_exact(&mut buf).expect("read");
580 assert_eq!(buf, [0xBB, 0xDD]);
582 }
583
584 #[test]
585 fn zlib_block_decompressed_correctly() {
586 let uncompressed: Vec<u8> = (0u8..=255).cycle().take(512).collect();
587 let dmg = make_dmg(1, vec![zlib_run(0, &uncompressed)]);
588 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
589 let mut buf = vec![0u8; 512];
590 reader.read_exact(&mut buf).expect("read_exact");
591 assert_eq!(buf, uncompressed);
592 }
593
594 #[test]
595 fn multiple_partitions_both_readable() {
596 let p0 = vec![0xAAu8; 512];
597 let p1 = vec![0xBBu8; 512];
598 let mut payload = p0.clone();
600 payload.extend_from_slice(&p1);
601 let dmg = make_dmg(2, vec![raw_run(0, payload)]);
602 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
603 let mut buf = [0u8; 512];
604 reader.read_exact(&mut buf).expect("read sector 0");
605 assert_eq!(&buf[..], &p0[..]);
606 reader.read_exact(&mut buf).expect("read sector 1");
607 assert_eq!(&buf[..], &p1[..]);
608 }
609}