Skip to main content

dmg/
lib.rs

1//! Pure-Rust forensic Apple Disk Image (DMG/UDIF) reader.
2//!
3//! A DMG file uses the UDIF (Universal Disk Image Format) container:
4//! - 512-byte **koly** trailer at the very end of the file (all big-endian)
5//! - XML plist at `xml_offset` containing partition block tables (`blkx` array)
6//! - Each blkx `Data` field is a base64-encoded **mish** block describing
7//!   how virtual sectors map to data in the file
8//!
9//! Supported block types: zero (0x00), raw (0x01), ignore (0x02), zlib (0x80000005).
10//! bzip2 (0x80000006) and LZFSE (0x80000007) return `NotSupported`.
11
12use 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; // b"koly"
21const MISH_MAGIC: u32 = 0x6D697368; // b"mish"
22const 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/// Errors returned by `DmgReader`.
32#[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/// One BLKXRun entry from a mish block.
51#[derive(Debug, Clone)]
52struct BlkxRun {
53    entry_type: u32,
54    sector_start: u64,
55    sector_count: u64,
56    /// Byte offset relative to the partition's `data_offset`.
57    data_offset: u64,
58    data_length: u64,
59}
60
61/// One partition (mish block) within the DMG.
62#[derive(Debug, Clone)]
63struct Partition {
64    /// Absolute byte offset in the file for this partition's data.
65    file_data_offset: u64,
66    /// First virtual sector of this partition.
67    sector_base: u64,
68    runs: Vec<BlkxRun>,
69}
70
71impl Partition {
72    /// True if this partition contains the given virtual sector.
73    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    /// Find the run covering local sector `local_sec` (relative to sector_base).
91    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
101/// Read-only Apple DMG (UDIF) reader implementing `Read + Seek`.
102pub 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    /// Open a DMG file, parsing the koly trailer and XML plist.
111    pub fn open(mut reader: R) -> Result<Self, DmgError> {
112        // Confirm the file is large enough to hold the koly trailer.
113        let file_size = reader.seek(SeekFrom::End(0))?;
114        if file_size < KOLY_SIZE {
115            return Err(DmgError::FileTooSmall);
116        }
117
118        // Read the 512-byte koly trailer.
119        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        // Read the XML plist.
133        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    /// Total virtual disk size in bytes (`sector_count × 512`).
149    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        // Find the partition and run covering this sector.
168        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        // Byte offset within this run (relative to the run's first sector).
180        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                // Decompress the entire run, then slice.
196                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
252// ── XML plist parser ──────────────────────────────────────────────────────────
253
254/// Parse the XML plist and extract all mish (blkx) partitions.
255fn 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                // Check if this text is for a <key> element
283                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                    // base64-encoded mish block
293                    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
315/// Parse a raw mish block into a `Partition`.
316///
317/// Real mish layout (all big-endian):
318///   0-3:    magic "mish"
319///   4-7:    version
320///   8-15:   firstSectorNumber
321///   16-23:  sectorCount
322///   24-31:  dataStart (byte offset into data fork)
323///   32-35:  decompressBufferRequested
324///   36-63:  reserved (28 bytes)
325///   64-67:  checksum.type
326///   68-71:  checksum.size (= 32 u32 words)
327///   72-199: checksum.data (128 bytes)
328///   200-203: blockDescriptorCount
329///   204+:   BLKXRun entries (40 bytes each)
330fn 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// ── Tests ─────────────────────────────────────────────────────────────────────
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use std::io::Cursor;
381
382    // ── Synthetic DMG builder ─────────────────────────────────────────────────
383
384    /// One run entry for the test DMG builder.
385    struct RunDef {
386        entry_type: u32,
387        sector_start: u64,
388        sector_count: u64,
389        data: Vec<u8>, // raw or pre-compressed bytes; empty for zero/ignore
390    }
391
392    /// Build a minimal synthetic DMG in memory.
393    ///
394    /// Layout:
395    ///   [data bytes for all raw/compressed runs]
396    ///   [xml plist]
397    ///   [512-byte koly trailer]
398    fn make_dmg(sector_count: u64, runs: Vec<RunDef>) -> Vec<u8> {
399        let mut file: Vec<u8> = Vec::new();
400
401        // Phase 1: write all run data and track offsets.
402        let mish_data_offset = 0u64; // data fork starts at byte 0
403        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        // Phase 2: build the mish block (binary, big-endian).
410        // Header is 204 bytes before the first run entry (see parse_mish layout comment).
411        let block_descriptors = runs.len() + 1; // +1 for BLK_TERM terminator
412        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()); // 0-3
418        mish.extend_from_slice(&1u32.to_be_bytes()); // 4-7:  version
419        mish.extend_from_slice(&0u64.to_be_bytes()); // 8-15: sector_number
420        mish.extend_from_slice(&sector_count.to_be_bytes()); // 16-23: sector_count
421        mish.extend_from_slice(&mish_data_offset.to_be_bytes()); // 24-31: data_offset
422        mish.extend_from_slice(&0u32.to_be_bytes()); // 32-35: buffers_needed
423        mish.extend_from_slice(&[0u8; 28]); // 36-63: reserved
424                                            // Checksum at offset 64 (136 bytes: type + size + data[32 u32s])
425        mish.extend_from_slice(&2u32.to_be_bytes()); // 64-67: checksum.type (CRC32)
426        mish.extend_from_slice(&32u32.to_be_bytes()); // 68-71: checksum.size
427        mish.extend_from_slice(&[0u8; 128]); // 72-199: checksum.data (zeros)
428        mish.extend_from_slice(&(block_descriptors as u32).to_be_bytes()); // 200-203: count
429
430        // Runs at offset 204 (40 bytes each: type + reserved + sec_start + sec_count + d_off + d_len)
431        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()); // reserved
436            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        // Terminator run (BLK_TERM, 40 bytes)
442        mish.extend_from_slice(&BLK_TERM.to_be_bytes()); // type
443        mish.extend_from_slice(&0u32.to_be_bytes()); // reserved
444        mish.extend_from_slice(&sector_count.to_be_bytes()); // sector_start = end
445        mish.extend_from_slice(&0u64.to_be_bytes()); // sector_count = 0
446        mish.extend_from_slice(&total_data_written.to_be_bytes()); // data_offset
447        mish.extend_from_slice(&0u64.to_be_bytes()); // data_length = 0
448
449        // Phase 3: base64-encode the mish block.
450        let mish_b64 = base64::engine::general_purpose::STANDARD.encode(&mish);
451
452        // Phase 4: build the XML plist.
453        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        // Phase 5: build the 512-byte koly trailer.
468        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()); // version
471        koly[8..12].copy_from_slice(&512u32.to_be_bytes()); // header_size
472        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(&sector_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    // ── Tests ─────────────────────────────────────────────────────────────────
513
514    #[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        // 512 bytes of zeros — no koly magic
523        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(&sector1);
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        // byte 511 = sector0[511] = 0xBB; byte 512 = sector1[0] = 0xDD
581        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        // Two separate runs at sector 0 and sector 1
599        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}