1use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
15
16use base64::Engine;
17use flate2::read::ZlibDecoder;
18use quick_xml::events::Event;
19use quick_xml::Reader;
20use thiserror::Error;
21
22const KOLY_MAGIC: u32 = 0x6B6F_6C79; const MISH_MAGIC: u32 = 0x6D69_7368; const KOLY_SIZE: u64 = 512;
25
26const BLK_ZERO: u32 = 0x0000_0000;
27const BLK_RAW: u32 = 0x0000_0001;
28const BLK_IGNORE: u32 = 0x0000_0002;
29const BLK_ADC: u32 = 0x8000_0004;
30const BLK_ZLIB: u32 = 0x8000_0005;
31const BLK_BZIP2: u32 = 0x8000_0006;
32const BLK_LZFSE: u32 = 0x8000_0007;
33const BLK_LZMA: u32 = 0x8000_0008;
34const BLK_COMMENT: u32 = 0x7FFF_FFFE;
35const BLK_TERM: u32 = 0xFFFF_FFFF;
36
37const MAX_RUN_BYTES: usize = 64 * 1024 * 1024;
42
43#[derive(Debug, Error)]
45pub enum DmgError {
46 #[error("I/O error: {0}")]
47 Io(#[from] io::Error),
48 #[error("not a DMG: missing koly magic")]
49 NotADmg,
50 #[error("file too small to contain koly trailer")]
51 FileTooSmall,
52 #[error("invalid mish block: {0}")]
53 BadMish(String),
54 #[error("invalid plist XML: {0}")]
55 BadPlist(String),
56 #[error("decompression error: {0}")]
57 Compression(String),
58 #[error("unsupported compression type: {0:#010x}")]
59 NotSupported(u32),
60}
61
62#[derive(Debug, Clone)]
64struct BlkxRun {
65 entry_type: u32,
66 sector_start: u64,
67 sector_count: u64,
68 data_offset: u64,
70 data_length: u64,
71}
72
73#[derive(Debug, Clone)]
75struct Partition {
76 file_data_offset: u64,
78 sector_base: u64,
80 runs: Vec<BlkxRun>,
81}
82
83impl Partition {
84 fn total_sectors(&self) -> u64 {
86 self.runs
87 .iter()
88 .filter(|r| r.entry_type != BLK_COMMENT && r.entry_type != BLK_TERM)
89 .map(|r| r.sector_start.saturating_add(r.sector_count))
90 .max()
91 .unwrap_or(0)
92 }
93
94 fn contains_sector(&self, vsec: u64) -> bool {
95 if vsec < self.sector_base {
96 return false;
97 }
98 let local = vsec - self.sector_base;
99 local < self.total_sectors()
100 }
101
102 fn run_for(&self, local_sec: u64) -> Option<&BlkxRun> {
104 self.runs.iter().find(|r| {
105 r.entry_type != BLK_TERM
106 && r.entry_type != BLK_COMMENT
107 && local_sec >= r.sector_start
108 && local_sec < r.sector_start.saturating_add(r.sector_count)
109 })
110 }
111}
112
113pub struct DmgReader<R: Read + Seek> {
115 inner: R,
116 sector_count: u64,
117 file_size: u64,
120 partitions: Vec<Partition>,
121 position: u64,
122}
123
124impl<R: Read + Seek> DmgReader<R> {
125 pub fn open(mut reader: R) -> Result<Self, DmgError> {
127 let file_size = reader.seek(SeekFrom::End(0))?;
129 if file_size < KOLY_SIZE {
130 return Err(DmgError::FileTooSmall);
131 }
132
133 reader.seek(SeekFrom::Start(file_size - KOLY_SIZE))?;
135 let mut koly = [0u8; 512];
136 reader.read_exact(&mut koly)?;
137
138 let magic = u32::from_be_bytes(koly[0..4].try_into().unwrap());
139 if magic != KOLY_MAGIC {
140 return Err(DmgError::NotADmg);
141 }
142
143 let xml_offset = u64::from_be_bytes(koly[216..224].try_into().unwrap());
144 let xml_length = u64::from_be_bytes(koly[224..232].try_into().unwrap());
145 let sector_count = u64::from_be_bytes(koly[492..500].try_into().unwrap());
146
147 if xml_offset
150 .checked_add(xml_length)
151 .is_none_or(|end| end > file_size)
152 {
153 return Err(DmgError::BadPlist("xml region out of file bounds".into()));
154 }
155
156 reader.seek(SeekFrom::Start(xml_offset))?;
158 let mut xml_bytes = vec![0u8; xml_length as usize];
159 reader.read_exact(&mut xml_bytes)?;
160 let xml = std::str::from_utf8(&xml_bytes).map_err(|e| DmgError::BadPlist(e.to_string()))?;
161
162 let partitions = parse_plist(xml)?;
163
164 Ok(Self {
165 inner: reader,
166 sector_count,
167 file_size,
168 partitions,
169 position: 0,
170 })
171 }
172
173 pub fn virtual_disk_size(&self) -> u64 {
175 self.sector_count.saturating_mul(512)
176 }
177}
178
179impl<R: Read + Seek> Read for DmgReader<R> {
180 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
181 if buf.is_empty() {
182 return Ok(0);
183 }
184 let disk_size = self.virtual_disk_size();
185 if self.position >= disk_size {
186 return Ok(0);
187 }
188
189 let vsec = self.position / 512;
190 let sec_offset = self.position % 512;
191
192 let part = self
194 .partitions
195 .iter()
196 .find(|p| p.contains_sector(vsec))
197 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no partition"))?;
198
199 let local_sec = vsec - part.sector_base;
200 let run = part
201 .run_for(local_sec)
202 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no run"))?;
203
204 let bytes_into_run = (local_sec - run.sector_start)
207 .saturating_mul(512)
208 .saturating_add(sec_offset);
209 let run_total_bytes = run.sector_count.saturating_mul(512);
210 let available_in_run = run_total_bytes.saturating_sub(bytes_into_run);
211 let to_read = buf.len().min(available_in_run as usize);
212
213 match run.entry_type {
214 BLK_ZERO | BLK_IGNORE => {
215 buf[..to_read].fill(0);
216 }
217 BLK_RAW => {
218 let file_pos = part
221 .file_data_offset
222 .checked_add(run.data_offset)
223 .and_then(|p| p.checked_add(bytes_into_run))
224 .filter(|&p| p.saturating_add(to_read as u64) <= self.file_size)
225 .ok_or_else(|| {
226 io::Error::new(io::ErrorKind::InvalidData, "raw block out of file bounds")
227 })?;
228 self.inner.seek(SeekFrom::Start(file_pos))?;
229 self.inner.read_exact(&mut buf[..to_read])?;
230 }
231 BLK_ADC | BLK_ZLIB | BLK_BZIP2 | BLK_LZFSE | BLK_LZMA => {
232 let file_pos = part
236 .file_data_offset
237 .checked_add(run.data_offset)
238 .ok_or_else(|| {
239 io::Error::new(io::ErrorKind::InvalidData, "block offset overflow")
240 })?;
241 let comp_ok = file_pos
242 .checked_add(run.data_length)
243 .is_some_and(|end| end <= self.file_size);
244 if !comp_ok {
245 return Err(io::Error::new(
246 io::ErrorKind::InvalidData,
247 "compressed block extends past end of file",
248 ));
249 }
250 let expected = (run.sector_count as usize).saturating_mul(512);
251 if expected > MAX_RUN_BYTES {
252 return Err(io::Error::new(
253 io::ErrorKind::InvalidData,
254 "block decompressed size exceeds cap",
255 ));
256 }
257 self.inner.seek(SeekFrom::Start(file_pos))?;
258 let mut compressed = vec![0u8; run.data_length as usize];
259 self.inner.read_exact(&mut compressed)?;
260 let decompressed = decompress(run.entry_type, &compressed, expected)
261 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
262 let start = bytes_into_run as usize;
263 if start >= decompressed.len() {
264 return Err(io::Error::new(
265 io::ErrorKind::UnexpectedEof,
266 "decompressed run underrun",
267 ));
268 }
269 let end = (start + to_read).min(decompressed.len());
270 buf[..end - start].copy_from_slice(&decompressed[start..end]);
271 }
272 t => {
273 return Err(io::Error::new(
274 io::ErrorKind::Unsupported,
275 format!("unsupported block type {t:#010x}"),
276 ));
277 }
278 }
279
280 self.position += to_read as u64;
281 Ok(to_read)
282 }
283}
284
285impl<R: Read + Seek> Seek for DmgReader<R> {
286 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
287 let disk_size = self.virtual_disk_size();
288 let new_pos = match pos {
289 SeekFrom::Start(n) => n,
290 SeekFrom::End(n) => {
291 if n >= 0 {
292 disk_size.saturating_add(n as u64)
293 } else {
294 disk_size.saturating_sub((-n) as u64)
295 }
296 }
297 SeekFrom::Current(n) => {
298 if n >= 0 {
299 self.position.saturating_add(n as u64)
300 } else {
301 self.position.saturating_sub((-n) as u64)
302 }
303 }
304 };
305 self.position = new_pos;
306 Ok(self.position)
307 }
308}
309
310fn decompress(
315 entry_type: u32,
316 compressed: &[u8],
317 expected_len: usize,
318) -> Result<Vec<u8>, DmgError> {
319 let mut out = Vec::with_capacity(expected_len);
322 let cap = expected_len as u64;
323 match entry_type {
324 BLK_ZLIB => {
325 ZlibDecoder::new(Cursor::new(compressed))
326 .take(cap)
327 .read_to_end(&mut out)
328 .map_err(|e| DmgError::Compression(e.to_string()))?;
329 }
330 BLK_BZIP2 => {
331 bzip2_rs::DecoderReader::new(Cursor::new(compressed))
332 .take(cap)
333 .read_to_end(&mut out)
334 .map_err(|e| DmgError::Compression(e.to_string()))?;
335 }
336 BLK_LZMA => {
337 let mut input = Cursor::new(compressed);
340 let mut sink = LimitWriter {
341 buf: &mut out,
342 limit: expected_len,
343 };
344 lzma_rs::xz_decompress(&mut input, &mut sink)
345 .map_err(|e| DmgError::Compression(e.to_string()))?;
346 }
347 BLK_LZFSE => {
348 let mut decoder = lzfse_rust::LzfseRingDecoder::default();
349 decoder
350 .reader_bytes(compressed)
351 .take(cap)
352 .read_to_end(&mut out)
353 .map_err(|e| DmgError::Compression(e.to_string()))?;
354 }
355 BLK_ADC => out = adc_decompress(compressed, expected_len),
356 other => return Err(DmgError::NotSupported(other)),
357 }
358 Ok(out)
359}
360
361fn adc_decompress(input: &[u8], expected_len: usize) -> Vec<u8> {
365 let mut out = Vec::with_capacity(expected_len);
366 let mut i = 0;
367 while i < input.len() && out.len() < expected_len {
370 let b = input[i];
371 i += 1;
372 if b & 0x80 != 0 {
373 let n = (b & 0x7F) as usize + 1;
375 let end = (i + n).min(input.len());
376 out.extend_from_slice(&input[i..end]);
377 i = end;
378 } else if b & 0x40 != 0 {
379 if i + 1 >= input.len() {
381 break;
382 }
383 let len = (b & 0x3F) as usize + 4;
384 let offset = ((input[i] as usize) << 8) | input[i + 1] as usize;
385 i += 2;
386 copy_back(&mut out, offset, len);
387 } else {
388 if i >= input.len() {
390 break;
391 }
392 let len = ((b >> 2) & 0x0F) as usize + 3;
393 let offset = (((b & 0x03) as usize) << 8) | input[i] as usize;
394 i += 1;
395 copy_back(&mut out, offset, len);
396 }
397 }
398 out
399}
400
401fn copy_back(out: &mut Vec<u8>, offset: usize, len: usize) {
404 for _ in 0..len {
405 if out.len() <= offset {
406 break;
407 }
408 let byte = out[out.len() - 1 - offset];
409 out.push(byte);
410 }
411}
412
413struct LimitWriter<'a> {
417 buf: &'a mut Vec<u8>,
418 limit: usize,
419}
420
421impl Write for LimitWriter<'_> {
422 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
423 if self.buf.len() + data.len() > self.limit {
424 return Err(io::Error::new(
425 io::ErrorKind::InvalidData,
426 "decompressed output exceeds cap",
427 ));
428 }
429 self.buf.extend_from_slice(data);
430 Ok(data.len())
431 }
432
433 fn flush(&mut self) -> io::Result<()> {
434 Ok(())
435 }
436}
437
438fn parse_plist(xml: &str) -> Result<Vec<Partition>, DmgError> {
442 let mut reader = Reader::from_str(xml);
443 reader.config_mut().trim_text(true);
444
445 let mut in_blkx = false;
446 let mut in_data = false;
447 let mut last_key = String::new();
448 let mut partitions = Vec::new();
449
450 loop {
451 match reader.read_event() {
452 Ok(Event::Start(e)) => match e.name().as_ref() {
453 b"array" if last_key == "blkx" => {
454 in_blkx = true;
455 }
456 b"data" if in_blkx => {
457 in_data = true;
458 }
459 _ => {}
460 },
461 Ok(Event::Text(e)) => {
462 let text = e.unescape().unwrap_or_default();
463 let trimmed = text.trim();
464 if e.is_empty() || trimmed.is_empty() {
465 continue;
466 }
467 if trimmed != "blkx" && !in_blkx {
469 last_key = trimmed.to_string();
470 continue;
471 }
472 if trimmed == "blkx" {
473 last_key = "blkx".to_string();
474 continue;
475 }
476 if in_data && in_blkx {
477 let cleaned: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
479 let raw = base64::engine::general_purpose::STANDARD
480 .decode(cleaned.as_bytes())
481 .map_err(|e| DmgError::BadPlist(e.to_string()))?;
482 let partition = parse_mish(&raw)?;
483 partitions.push(partition);
484 in_data = false;
485 }
486 }
487 Ok(Event::End(e)) => {
488 if e.name().as_ref() == b"array" {
489 in_blkx = false;
490 }
491 }
492 Ok(Event::Eof) => break,
493 Err(e) => return Err(DmgError::BadPlist(e.to_string())),
494 _ => {}
495 }
496 }
497 Ok(partitions)
498}
499
500fn parse_mish(data: &[u8]) -> Result<Partition, DmgError> {
516 if data.len() < 204 {
517 return Err(DmgError::BadMish("too short".into()));
518 }
519 let magic = u32::from_be_bytes(data[0..4].try_into().unwrap());
520 if magic != MISH_MAGIC {
521 return Err(DmgError::BadMish(format!("bad magic {magic:#010x}")));
522 }
523 let sector_number = u64::from_be_bytes(data[8..16].try_into().unwrap());
524 let file_data_offset = u64::from_be_bytes(data[24..32].try_into().unwrap());
525 let block_descriptors = u32::from_be_bytes(data[200..204].try_into().unwrap()) as usize;
526
527 let runs_start = 204;
528 let run_size = 40;
529 if data.len() < runs_start + block_descriptors * run_size {
530 return Err(DmgError::BadMish("truncated run list".into()));
531 }
532
533 let mut runs = Vec::with_capacity(block_descriptors);
534 for i in 0..block_descriptors {
535 let o = runs_start + i * run_size;
536 let entry_type = u32::from_be_bytes(data[o..o + 4].try_into().unwrap());
537 let sector_start = u64::from_be_bytes(data[o + 8..o + 16].try_into().unwrap());
538 let sector_count = u64::from_be_bytes(data[o + 16..o + 24].try_into().unwrap());
539 let data_offset = u64::from_be_bytes(data[o + 24..o + 32].try_into().unwrap());
540 let data_length = u64::from_be_bytes(data[o + 32..o + 40].try_into().unwrap());
541 runs.push(BlkxRun {
542 entry_type,
543 sector_start,
544 sector_count,
545 data_offset,
546 data_length,
547 });
548 if entry_type == BLK_TERM {
549 break;
550 }
551 }
552
553 Ok(Partition {
554 file_data_offset,
555 sector_base: sector_number,
556 runs,
557 })
558}
559
560#[cfg(test)]
563mod tests {
564 use super::*;
565 use std::io::Cursor;
566
567 struct RunDef {
571 entry_type: u32,
572 sector_start: u64,
573 sector_count: u64,
574 data: Vec<u8>, }
576
577 #[allow(clippy::needless_pass_by_value)] fn make_dmg(sector_count: u64, runs: Vec<RunDef>) -> Vec<u8> {
585 let mut file: Vec<u8> = Vec::new();
586
587 let mish_data_offset = 0u64; let mut run_file_offsets: Vec<u64> = Vec::new();
590 for r in &runs {
591 run_file_offsets.push(file.len() as u64);
592 file.extend_from_slice(&r.data);
593 }
594
595 let block_descriptors = runs.len() + 1; let total_data_written: u64 = run_file_offsets.last().map_or(0, |&off| {
599 let last = &runs[runs.len() - 1];
600 off + last.data.len() as u64
601 });
602 let mut mish: Vec<u8> = Vec::new();
603 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() {
618 let data_off = run_file_offsets[i];
619 let data_len = r.data.len() as u64;
620 mish.extend_from_slice(&r.entry_type.to_be_bytes());
621 mish.extend_from_slice(&0u32.to_be_bytes()); mish.extend_from_slice(&r.sector_start.to_be_bytes());
623 mish.extend_from_slice(&r.sector_count.to_be_bytes());
624 mish.extend_from_slice(&data_off.to_be_bytes());
625 mish.extend_from_slice(&data_len.to_be_bytes());
626 }
627 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);
637
638 let xml = format!(
640 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
641 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"\">\n\
642 <plist version=\"1.0\">\n\
643 <dict>\n <key>resource-fork</key>\n <dict>\n\
644 <key>blkx</key>\n<array>\n<dict>\n\
645 <key>Data</key><data>{mish_b64}</data>\n\
646 </dict>\n</array>\n </dict>\n</dict>\n</plist>\n"
647 );
648
649 let xml_offset = file.len() as u64;
650 let xml_length = xml.len() as u64;
651 file.extend_from_slice(xml.as_bytes());
652
653 let mut koly = [0u8; 512];
655 koly[0..4].copy_from_slice(&KOLY_MAGIC.to_be_bytes());
656 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());
659 koly[224..232].copy_from_slice(&xml_length.to_be_bytes());
660 koly[492..500].copy_from_slice(§or_count.to_be_bytes());
661 file.extend_from_slice(&koly);
662 file
663 }
664
665 fn raw_run(sector_start: u64, data: Vec<u8>) -> RunDef {
666 assert!(data.len() % 512 == 0, "raw data must be sector-aligned");
667 RunDef {
668 entry_type: BLK_RAW,
669 sector_start,
670 sector_count: data.len() as u64 / 512,
671 data,
672 }
673 }
674
675 fn zero_run(sector_start: u64, sector_count: u64) -> RunDef {
676 RunDef {
677 entry_type: BLK_ZERO,
678 sector_start,
679 sector_count,
680 data: vec![],
681 }
682 }
683
684 fn zlib_run(sector_start: u64, uncompressed: &[u8]) -> RunDef {
685 use flate2::{write::ZlibEncoder, Compression};
686 use std::io::Write;
687 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
688 enc.write_all(uncompressed).unwrap();
689 let compressed = enc.finish().unwrap();
690 RunDef {
691 entry_type: BLK_ZLIB,
692 sector_start,
693 sector_count: uncompressed.len() as u64 / 512,
694 data: compressed,
695 }
696 }
697
698 #[test]
701 fn file_too_small_returns_err() {
702 let result = DmgReader::open(Cursor::new(b"tiny"));
703 assert!(matches!(result, Err(DmgError::FileTooSmall)));
704 }
705
706 #[test]
707 fn not_a_dmg_returns_err() {
708 let result = DmgReader::open(Cursor::new(vec![0u8; 512]));
710 assert!(matches!(result, Err(DmgError::NotADmg)));
711 }
712
713 #[test]
714 fn virtual_disk_size_is_512_times_sector_count() {
715 let payload = vec![0xBBu8; 512];
716 let dmg = make_dmg(1, vec![raw_run(0, payload)]);
717 let reader = DmgReader::open(Cursor::new(dmg)).expect("open");
718 assert_eq!(reader.virtual_disk_size(), 512);
719 }
720
721 #[test]
722 fn read_raw_block_returns_correct_bytes() {
723 let payload: Vec<u8> = (0u8..=255).cycle().take(512).collect();
724 let dmg = make_dmg(1, vec![raw_run(0, payload.clone())]);
725 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
726 let mut buf = vec![0u8; 512];
727 reader.read_exact(&mut buf).expect("read_exact");
728 assert_eq!(buf, payload);
729 }
730
731 #[test]
732 fn read_zeroed_block_returns_zeros() {
733 let dmg = make_dmg(2, vec![zero_run(0, 2)]);
734 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
735 let mut buf = vec![0xFFu8; 512];
736 reader.read_exact(&mut buf).expect("read_exact");
737 assert!(buf.iter().all(|&b| b == 0), "expected all zeros");
738 }
739
740 #[test]
741 fn seek_and_read_at_offset() {
742 let mut payload = vec![0u8; 512];
743 payload[100] = 0xAB;
744 payload[101] = 0xCD;
745 let dmg = make_dmg(1, vec![raw_run(0, payload)]);
746 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
747 reader.seek(SeekFrom::Start(100)).expect("seek");
748 let mut buf = [0u8; 2];
749 reader.read_exact(&mut buf).expect("read");
750 assert_eq!(buf, [0xAB, 0xCD]);
751 }
752
753 #[test]
754 fn read_across_run_boundary() {
755 let mut sector0 = vec![0xAAu8; 512];
756 sector0[511] = 0xBB;
757 let mut sector1 = vec![0xCCu8; 512];
758 sector1[0] = 0xDD;
759 let mut payload = sector0;
760 payload.extend_from_slice(§or1);
761 let dmg = make_dmg(2, vec![raw_run(0, payload)]);
762 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
763 reader.seek(SeekFrom::Start(511)).expect("seek");
764 let mut buf = [0u8; 2];
765 reader.read_exact(&mut buf).expect("read");
766 assert_eq!(buf, [0xBB, 0xDD]);
768 }
769
770 #[test]
771 fn zlib_block_decompressed_correctly() {
772 let uncompressed: Vec<u8> = (0u8..=255).cycle().take(512).collect();
773 let dmg = make_dmg(1, vec![zlib_run(0, &uncompressed)]);
774 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
775 let mut buf = vec![0u8; 512];
776 reader.read_exact(&mut buf).expect("read_exact");
777 assert_eq!(buf, uncompressed);
778 }
779
780 #[test]
781 fn multiple_partitions_both_readable() {
782 let p0 = vec![0xAAu8; 512];
783 let p1 = vec![0xBBu8; 512];
784 let mut payload = p0.clone();
786 payload.extend_from_slice(&p1);
787 let dmg = make_dmg(2, vec![raw_run(0, payload)]);
788 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
789 let mut buf = [0u8; 512];
790 reader.read_exact(&mut buf).expect("read sector 0");
791 assert_eq!(&buf[..], &p0[..]);
792 reader.read_exact(&mut buf).expect("read sector 1");
793 assert_eq!(&buf[..], &p1[..]);
794 }
795}