1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
18
19mod sparse;
20
21pub use sparse::{SparseBundleReader, SparseImageReader};
22
23use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
24
25use base64::Engine;
26use flate2::read::ZlibDecoder;
27use quick_xml::events::Event;
28use quick_xml::Reader;
29use safe_read::{be_u32, be_u64};
30use thiserror::Error;
31
32const KOLY_MAGIC: u32 = 0x6B6F_6C79; const MISH_MAGIC: u32 = 0x6D69_7368; const KOLY_SIZE: u64 = 512;
35
36const RUNS_START: usize = 204;
38const RUN_SIZE: usize = 40;
39
40const BLK_ZERO: u32 = 0x0000_0000;
41const BLK_RAW: u32 = 0x0000_0001;
42const BLK_IGNORE: u32 = 0x0000_0002;
43const BLK_ADC: u32 = 0x8000_0004;
44const BLK_ZLIB: u32 = 0x8000_0005;
45const BLK_BZIP2: u32 = 0x8000_0006;
46const BLK_LZFSE: u32 = 0x8000_0007;
47const BLK_LZMA: u32 = 0x8000_0008;
48const BLK_COMMENT: u32 = 0x7FFF_FFFE;
49const BLK_TERM: u32 = 0xFFFF_FFFF;
50
51const MAX_RUN_BYTES: usize = 64 * 1024 * 1024;
56
57#[derive(Debug, Error)]
59pub enum DmgError {
60 #[error("I/O error: {0}")]
61 Io(#[from] io::Error),
62 #[error("not a DMG: missing koly magic")]
63 NotADmg,
64 #[error("file too small to contain koly trailer")]
65 FileTooSmall,
66 #[error("invalid mish block: {0}")]
67 BadMish(String),
68 #[error("invalid plist XML: {0}")]
69 BadPlist(String),
70 #[error("decompression error: {0}")]
71 Compression(String),
72 #[error("unsupported compression type: {0:#010x}")]
73 NotSupported(u32),
74 #[error("not a sparse image: bad sprs magic {0:#010x}")]
75 NotSparseImage(u32),
76 #[error("invalid sparse image header: {0}")]
77 BadSparseHeader(String),
78 #[error("sparsebundle Info.plist not found")]
79 MissingInfoPlist,
80 #[error("invalid sparsebundle Info.plist: {0}")]
81 BadInfoPlist(String),
82}
83
84#[derive(Debug, Clone)]
86struct BlkxRun {
87 entry_type: u32,
88 sector_start: u64,
89 sector_count: u64,
90 data_offset: u64,
92 data_length: u64,
93}
94
95#[derive(Debug, Clone)]
97struct Partition {
98 file_data_offset: u64,
100 sector_base: u64,
102 runs: Vec<BlkxRun>,
103}
104
105impl Partition {
106 fn total_sectors(&self) -> u64 {
108 self.runs
109 .iter()
110 .filter(|r| r.entry_type != BLK_COMMENT && r.entry_type != BLK_TERM)
111 .map(|r| r.sector_start.saturating_add(r.sector_count))
112 .max()
113 .unwrap_or(0)
114 }
115
116 fn contains_sector(&self, vsec: u64) -> bool {
117 if vsec < self.sector_base {
118 return false;
119 }
120 let local = vsec - self.sector_base;
121 local < self.total_sectors()
122 }
123
124 fn run_for(&self, local_sec: u64) -> Option<&BlkxRun> {
126 self.runs.iter().find(|r| {
127 r.entry_type != BLK_TERM
128 && r.entry_type != BLK_COMMENT
129 && local_sec >= r.sector_start
130 && local_sec < r.sector_start.saturating_add(r.sector_count)
131 })
132 }
133}
134
135pub struct DmgReader<R: Read + Seek> {
137 inner: R,
138 sector_count: u64,
139 file_size: u64,
142 partitions: Vec<Partition>,
143 position: u64,
144}
145
146impl<R: Read + Seek> DmgReader<R> {
147 pub fn open(mut reader: R) -> Result<Self, DmgError> {
149 let file_size = reader.seek(SeekFrom::End(0))?;
151 if file_size < KOLY_SIZE {
152 return Err(DmgError::FileTooSmall);
153 }
154
155 reader.seek(SeekFrom::Start(file_size - KOLY_SIZE))?;
157 let mut koly = [0u8; 512];
158 reader.read_exact(&mut koly)?;
159
160 let magic = be_u32(&koly, 0);
161 if magic != KOLY_MAGIC {
162 return Err(DmgError::NotADmg);
163 }
164
165 let xml_offset = be_u64(&koly, 216);
166 let xml_length = be_u64(&koly, 224);
167 let sector_count = be_u64(&koly, 492);
168
169 if xml_offset
172 .checked_add(xml_length)
173 .is_none_or(|end| end > file_size)
174 {
175 return Err(DmgError::BadPlist("xml region out of file bounds".into()));
176 }
177
178 reader.seek(SeekFrom::Start(xml_offset))?;
180 let mut xml_bytes = vec![0u8; xml_length as usize];
181 reader.read_exact(&mut xml_bytes)?;
182 let xml = std::str::from_utf8(&xml_bytes).map_err(|e| DmgError::BadPlist(e.to_string()))?;
183
184 let partitions = parse_plist(xml)?;
185
186 Ok(Self {
187 inner: reader,
188 sector_count,
189 file_size,
190 partitions,
191 position: 0,
192 })
193 }
194
195 pub fn virtual_disk_size(&self) -> u64 {
197 self.sector_count.saturating_mul(512)
198 }
199}
200
201impl<R: Read + Seek> Read for DmgReader<R> {
202 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
203 if buf.is_empty() {
204 return Ok(0);
205 }
206 let disk_size = self.virtual_disk_size();
207 if self.position >= disk_size {
208 return Ok(0);
209 }
210
211 let vsec = self.position / 512;
212 let sec_offset = self.position % 512;
213
214 let part = self
216 .partitions
217 .iter()
218 .find(|p| p.contains_sector(vsec))
219 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no partition"))?;
220
221 let local_sec = vsec - part.sector_base;
222 let run = part
223 .run_for(local_sec)
224 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no run"))?;
225
226 let bytes_into_run = (local_sec - run.sector_start)
229 .saturating_mul(512)
230 .saturating_add(sec_offset);
231 let run_total_bytes = run.sector_count.saturating_mul(512);
232 let available_in_run = run_total_bytes.saturating_sub(bytes_into_run);
233 let to_read = buf.len().min(available_in_run as usize);
234
235 match run.entry_type {
236 BLK_ZERO | BLK_IGNORE => {
237 buf[..to_read].fill(0);
238 }
239 BLK_RAW => {
240 let file_pos = part
243 .file_data_offset
244 .checked_add(run.data_offset)
245 .and_then(|p| p.checked_add(bytes_into_run))
246 .filter(|&p| p.saturating_add(to_read as u64) <= self.file_size)
247 .ok_or_else(|| {
248 io::Error::new(io::ErrorKind::InvalidData, "raw block out of file bounds")
249 })?;
250 self.inner.seek(SeekFrom::Start(file_pos))?;
251 self.inner.read_exact(&mut buf[..to_read])?;
252 }
253 BLK_ADC | BLK_ZLIB | BLK_BZIP2 | BLK_LZFSE | BLK_LZMA => {
254 let file_pos = part
258 .file_data_offset
259 .checked_add(run.data_offset)
260 .ok_or_else(|| {
261 io::Error::new(io::ErrorKind::InvalidData, "block offset overflow")
262 })?;
263 let comp_ok = file_pos
264 .checked_add(run.data_length)
265 .is_some_and(|end| end <= self.file_size);
266 if !comp_ok {
267 return Err(io::Error::new(
268 io::ErrorKind::InvalidData,
269 "compressed block extends past end of file",
270 ));
271 }
272 let expected = (run.sector_count as usize).saturating_mul(512);
273 if expected > MAX_RUN_BYTES {
274 return Err(io::Error::new(
275 io::ErrorKind::InvalidData,
276 "block decompressed size exceeds cap",
277 ));
278 }
279 self.inner.seek(SeekFrom::Start(file_pos))?;
280 let mut compressed = vec![0u8; run.data_length as usize];
281 self.inner.read_exact(&mut compressed)?;
282 let decompressed = decompress(run.entry_type, &compressed, expected)
283 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
284 let start = bytes_into_run as usize;
285 if start >= decompressed.len() {
286 return Err(io::Error::new(
287 io::ErrorKind::UnexpectedEof,
288 "decompressed run underrun",
289 ));
290 }
291 let end = (start + to_read).min(decompressed.len());
292 buf[..end - start].copy_from_slice(&decompressed[start..end]);
293 }
294 t => {
295 return Err(io::Error::new(
296 io::ErrorKind::Unsupported,
297 format!("unsupported block type {t:#010x}"),
298 ));
299 }
300 }
301
302 self.position += to_read as u64;
303 Ok(to_read)
304 }
305}
306
307impl<R: Read + Seek> Seek for DmgReader<R> {
308 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
309 let disk_size = self.virtual_disk_size();
310 let new_pos = match pos {
311 SeekFrom::Start(n) => n,
312 SeekFrom::End(n) => {
313 if n >= 0 {
314 disk_size.saturating_add(n as u64)
315 } else {
316 disk_size.saturating_sub((-n) as u64)
317 }
318 }
319 SeekFrom::Current(n) => {
320 if n >= 0 {
321 self.position.saturating_add(n as u64)
322 } else {
323 self.position.saturating_sub((-n) as u64)
324 }
325 }
326 };
327 self.position = new_pos;
328 Ok(self.position)
329 }
330}
331
332fn decompress(
337 entry_type: u32,
338 compressed: &[u8],
339 expected_len: usize,
340) -> Result<Vec<u8>, DmgError> {
341 let mut out = Vec::with_capacity(expected_len);
344 let cap = expected_len as u64;
345 match entry_type {
346 BLK_ZLIB => {
347 ZlibDecoder::new(Cursor::new(compressed))
348 .take(cap)
349 .read_to_end(&mut out)
350 .map_err(|e| DmgError::Compression(e.to_string()))?;
351 }
352 BLK_BZIP2 => {
353 bzip2_rs::DecoderReader::new(Cursor::new(compressed))
354 .take(cap)
355 .read_to_end(&mut out)
356 .map_err(|e| DmgError::Compression(e.to_string()))?;
357 }
358 BLK_LZMA => {
359 let mut input = Cursor::new(compressed);
362 let mut sink = LimitWriter {
363 buf: &mut out,
364 limit: expected_len,
365 };
366 lzma_rs::xz_decompress(&mut input, &mut sink)
367 .map_err(|e| DmgError::Compression(e.to_string()))?;
368 }
369 BLK_LZFSE => {
370 let mut decoder = lzfse_rust::LzfseRingDecoder::default();
371 decoder
372 .reader_bytes(compressed)
373 .take(cap)
374 .read_to_end(&mut out)
375 .map_err(|e| DmgError::Compression(e.to_string()))?;
376 }
377 BLK_ADC => out = adc_decompress(compressed, expected_len),
378 other => return Err(DmgError::NotSupported(other)),
379 }
380 Ok(out)
381}
382
383fn adc_decompress(input: &[u8], expected_len: usize) -> Vec<u8> {
387 let mut out = Vec::with_capacity(expected_len);
388 let mut i = 0;
389 while i < input.len() && out.len() < expected_len {
392 let b = input[i];
393 i += 1;
394 if b & 0x80 != 0 {
395 let n = (b & 0x7F) as usize + 1;
397 let end = (i + n).min(input.len());
398 out.extend_from_slice(&input[i..end]);
399 i = end;
400 } else if b & 0x40 != 0 {
401 if i + 1 >= input.len() {
403 break;
404 }
405 let len = (b & 0x3F) as usize + 4;
406 let offset = ((input[i] as usize) << 8) | input[i + 1] as usize;
407 i += 2;
408 copy_back(&mut out, offset, len);
409 } else {
410 if i >= input.len() {
412 break;
413 }
414 let len = ((b >> 2) & 0x0F) as usize + 3;
415 let offset = (((b & 0x03) as usize) << 8) | input[i] as usize;
416 i += 1;
417 copy_back(&mut out, offset, len);
418 }
419 }
420 out
421}
422
423fn copy_back(out: &mut Vec<u8>, offset: usize, len: usize) {
426 for _ in 0..len {
427 if out.len() <= offset {
428 break;
429 }
430 let byte = out[out.len() - 1 - offset];
431 out.push(byte);
432 }
433}
434
435struct LimitWriter<'a> {
439 buf: &'a mut Vec<u8>,
440 limit: usize,
441}
442
443impl Write for LimitWriter<'_> {
444 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
445 if self.buf.len() + data.len() > self.limit {
446 return Err(io::Error::new(
447 io::ErrorKind::InvalidData,
448 "decompressed output exceeds cap",
449 ));
450 }
451 self.buf.extend_from_slice(data);
452 Ok(data.len())
453 }
454
455 fn flush(&mut self) -> io::Result<()> {
456 Ok(())
457 }
458}
459
460fn parse_plist(xml: &str) -> Result<Vec<Partition>, DmgError> {
464 let mut reader = Reader::from_str(xml);
465 reader.config_mut().trim_text(true);
466
467 let mut in_blkx = false;
468 let mut in_data = false;
469 let mut last_key = String::new();
470 let mut partitions = Vec::new();
471
472 loop {
473 match reader.read_event() {
474 Ok(Event::Start(e)) => match e.name().as_ref() {
475 b"array" if last_key == "blkx" => {
476 in_blkx = true;
477 }
478 b"data" if in_blkx => {
479 in_data = true;
480 }
481 _ => {}
482 },
483 Ok(Event::Text(e)) => {
484 let text = e
485 .xml_content(quick_xml::XmlVersion::Implicit1_0)
486 .unwrap_or_default();
487 let trimmed = text.trim();
488 if e.is_empty() || trimmed.is_empty() {
489 continue;
490 }
491 if trimmed != "blkx" && !in_blkx {
493 last_key = trimmed.to_string();
494 continue;
495 }
496 if trimmed == "blkx" {
497 last_key = "blkx".to_string();
498 continue;
499 }
500 if in_data && in_blkx {
501 let cleaned: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
503 let raw = base64::engine::general_purpose::STANDARD
504 .decode(cleaned.as_bytes())
505 .map_err(|e| DmgError::BadPlist(e.to_string()))?;
506 let partition = parse_mish(&raw)?;
507 partitions.push(partition);
508 in_data = false;
509 }
510 }
511 Ok(Event::End(e)) => {
512 if e.name().as_ref() == b"array" {
513 in_blkx = false;
514 }
515 }
516 Ok(Event::Eof) => break,
517 Err(e) => return Err(DmgError::BadPlist(e.to_string())),
518 _ => {}
519 }
520 }
521 Ok(partitions)
522}
523
524fn parse_mish(data: &[u8]) -> Result<Partition, DmgError> {
540 if data.len() < 204 {
541 return Err(DmgError::BadMish("too short".into()));
542 }
543 let magic = be_u32(data, 0);
544 if magic != MISH_MAGIC {
545 return Err(DmgError::BadMish(format!("bad magic {magic:#010x}")));
546 }
547 let sector_number = be_u64(data, 8);
548 let file_data_offset = be_u64(data, 24);
549 let block_descriptors = be_u32(data, 200) as usize;
550
551 let runs_end = block_descriptors
556 .checked_mul(RUN_SIZE)
557 .and_then(|n| n.checked_add(RUNS_START))
558 .ok_or_else(|| DmgError::BadMish("run list size overflows usize".into()))?;
559 if data.len() < runs_end {
560 return Err(DmgError::BadMish("truncated run list".into()));
561 }
562
563 let mut runs = Vec::with_capacity(block_descriptors);
567 for i in 0..block_descriptors {
568 let o = RUNS_START + i * RUN_SIZE;
569 let entry_type = be_u32(data, o);
570 let sector_start = be_u64(data, o + 8);
571 let sector_count = be_u64(data, o + 16);
572 let data_offset = be_u64(data, o + 24);
573 let data_length = be_u64(data, o + 32);
574 runs.push(BlkxRun {
575 entry_type,
576 sector_start,
577 sector_count,
578 data_offset,
579 data_length,
580 });
581 if entry_type == BLK_TERM {
582 break;
583 }
584 }
585
586 Ok(Partition {
587 file_data_offset,
588 sector_base: sector_number,
589 runs,
590 })
591}
592
593#[cfg(feature = "vfs")]
596mod vfs;
597#[cfg(feature = "vfs")]
598pub use vfs::DmgSource;
599
600#[cfg(test)]
603mod tests {
604 use super::*;
605 use std::io::Cursor;
606
607 struct RunDef {
611 entry_type: u32,
612 sector_start: u64,
613 sector_count: u64,
614 data: Vec<u8>, }
616
617 #[allow(clippy::needless_pass_by_value)] fn make_dmg(sector_count: u64, runs: Vec<RunDef>) -> Vec<u8> {
625 let mut file: Vec<u8> = Vec::new();
626
627 let mish_data_offset = 0u64; let mut run_file_offsets: Vec<u64> = Vec::new();
630 for r in &runs {
631 run_file_offsets.push(file.len() as u64);
632 file.extend_from_slice(&r.data);
633 }
634
635 let block_descriptors = runs.len() + 1; let total_data_written: u64 = run_file_offsets.last().map_or(0, |&off| {
639 let last = &runs[runs.len() - 1];
640 off + last.data.len() as u64
641 });
642 let mut mish: Vec<u8> = Vec::new();
643 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() {
658 let data_off = run_file_offsets[i];
659 let data_len = r.data.len() as u64;
660 mish.extend_from_slice(&r.entry_type.to_be_bytes());
661 mish.extend_from_slice(&0u32.to_be_bytes()); mish.extend_from_slice(&r.sector_start.to_be_bytes());
663 mish.extend_from_slice(&r.sector_count.to_be_bytes());
664 mish.extend_from_slice(&data_off.to_be_bytes());
665 mish.extend_from_slice(&data_len.to_be_bytes());
666 }
667 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);
677
678 let xml = format!(
680 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
681 <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"\">\n\
682 <plist version=\"1.0\">\n\
683 <dict>\n <key>resource-fork</key>\n <dict>\n\
684 <key>blkx</key>\n<array>\n<dict>\n\
685 <key>Data</key><data>{mish_b64}</data>\n\
686 </dict>\n</array>\n </dict>\n</dict>\n</plist>\n"
687 );
688
689 let xml_offset = file.len() as u64;
690 let xml_length = xml.len() as u64;
691 file.extend_from_slice(xml.as_bytes());
692
693 let mut koly = [0u8; 512];
695 koly[0..4].copy_from_slice(&KOLY_MAGIC.to_be_bytes());
696 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());
699 koly[224..232].copy_from_slice(&xml_length.to_be_bytes());
700 koly[492..500].copy_from_slice(§or_count.to_be_bytes());
701 file.extend_from_slice(&koly);
702 file
703 }
704
705 fn raw_run(sector_start: u64, data: Vec<u8>) -> RunDef {
706 assert!(data.len() % 512 == 0, "raw data must be sector-aligned");
707 RunDef {
708 entry_type: BLK_RAW,
709 sector_start,
710 sector_count: data.len() as u64 / 512,
711 data,
712 }
713 }
714
715 fn zero_run(sector_start: u64, sector_count: u64) -> RunDef {
716 RunDef {
717 entry_type: BLK_ZERO,
718 sector_start,
719 sector_count,
720 data: vec![],
721 }
722 }
723
724 fn zlib_run(sector_start: u64, uncompressed: &[u8]) -> RunDef {
725 use flate2::{write::ZlibEncoder, Compression};
726 use std::io::Write;
727 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
728 enc.write_all(uncompressed).unwrap();
729 let compressed = enc.finish().unwrap();
730 RunDef {
731 entry_type: BLK_ZLIB,
732 sector_start,
733 sector_count: uncompressed.len() as u64 / 512,
734 data: compressed,
735 }
736 }
737
738 #[test]
741 fn file_too_small_returns_err() {
742 let result = DmgReader::open(Cursor::new(b"tiny"));
743 assert!(matches!(result, Err(DmgError::FileTooSmall)));
744 }
745
746 #[test]
747 fn not_a_dmg_returns_err() {
748 let result = DmgReader::open(Cursor::new(vec![0u8; 512]));
750 assert!(matches!(result, Err(DmgError::NotADmg)));
751 }
752
753 #[test]
754 fn virtual_disk_size_is_512_times_sector_count() {
755 let payload = vec![0xBBu8; 512];
756 let dmg = make_dmg(1, vec![raw_run(0, payload)]);
757 let reader = DmgReader::open(Cursor::new(dmg)).expect("open");
758 assert_eq!(reader.virtual_disk_size(), 512);
759 }
760
761 #[test]
762 fn read_raw_block_returns_correct_bytes() {
763 let payload: Vec<u8> = (0u8..=255).cycle().take(512).collect();
764 let dmg = make_dmg(1, vec![raw_run(0, payload.clone())]);
765 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
766 let mut buf = vec![0u8; 512];
767 reader.read_exact(&mut buf).expect("read_exact");
768 assert_eq!(buf, payload);
769 }
770
771 #[test]
772 fn read_zeroed_block_returns_zeros() {
773 let dmg = make_dmg(2, vec![zero_run(0, 2)]);
774 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
775 let mut buf = vec![0xFFu8; 512];
776 reader.read_exact(&mut buf).expect("read_exact");
777 assert!(buf.iter().all(|&b| b == 0), "expected all zeros");
778 }
779
780 #[test]
781 fn seek_and_read_at_offset() {
782 let mut payload = vec![0u8; 512];
783 payload[100] = 0xAB;
784 payload[101] = 0xCD;
785 let dmg = make_dmg(1, vec![raw_run(0, payload)]);
786 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
787 reader.seek(SeekFrom::Start(100)).expect("seek");
788 let mut buf = [0u8; 2];
789 reader.read_exact(&mut buf).expect("read");
790 assert_eq!(buf, [0xAB, 0xCD]);
791 }
792
793 #[test]
794 fn read_across_run_boundary() {
795 let mut sector0 = vec![0xAAu8; 512];
796 sector0[511] = 0xBB;
797 let mut sector1 = vec![0xCCu8; 512];
798 sector1[0] = 0xDD;
799 let mut payload = sector0;
800 payload.extend_from_slice(§or1);
801 let dmg = make_dmg(2, vec![raw_run(0, payload)]);
802 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
803 reader.seek(SeekFrom::Start(511)).expect("seek");
804 let mut buf = [0u8; 2];
805 reader.read_exact(&mut buf).expect("read");
806 assert_eq!(buf, [0xBB, 0xDD]);
808 }
809
810 #[test]
811 fn zlib_block_decompressed_correctly() {
812 let uncompressed: Vec<u8> = (0u8..=255).cycle().take(512).collect();
813 let dmg = make_dmg(1, vec![zlib_run(0, &uncompressed)]);
814 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
815 let mut buf = vec![0u8; 512];
816 reader.read_exact(&mut buf).expect("read_exact");
817 assert_eq!(buf, uncompressed);
818 }
819
820 #[test]
821 fn multiple_partitions_both_readable() {
822 let p0 = vec![0xAAu8; 512];
823 let p1 = vec![0xBBu8; 512];
824 let mut payload = p0.clone();
826 payload.extend_from_slice(&p1);
827 let dmg = make_dmg(2, vec![raw_run(0, payload)]);
828 let mut reader = DmgReader::open(Cursor::new(dmg)).expect("open");
829 let mut buf = [0u8; 512];
830 reader.read_exact(&mut buf).expect("read sector 0");
831 assert_eq!(&buf[..], &p0[..]);
832 reader.read_exact(&mut buf).expect("read sector 1");
833 assert_eq!(&buf[..], &p1[..]);
834 }
835}