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