1use std::collections::HashMap;
10use std::io::{self, Read, Seek, SeekFrom};
11use std::path::{Path, PathBuf};
12
13use quick_xml::events::Event;
14use quick_xml::Reader;
15
16use crate::DmgError;
17
18const SPRS_MAGIC: u32 = 0x7370_7273; const SPARSE_HEADER_SIZE: u64 = 4096;
20const SECTOR: u64 = 512;
21const BAND_TABLE_OFFSET: usize = 0x40;
23
24fn be_u32(data: &[u8], off: usize) -> u32 {
27 let mut b = [0u8; 4];
28 if let Some(s) = data.get(off..).and_then(|s| s.get(..4)) {
31 b.copy_from_slice(s);
32 }
33 u32::from_be_bytes(b)
34}
35
36pub struct SparseImageReader<R: Read + Seek> {
45 inner: R,
46 band_size: u64,
47 virtual_size: u64,
48 file_size: u64,
50 band_slot: HashMap<u64, u64>,
52 position: u64,
53}
54
55impl<R: Read + Seek> SparseImageReader<R> {
56 pub fn open(mut reader: R) -> Result<Self, DmgError> {
58 let file_size = reader.seek(SeekFrom::End(0))?;
59 if file_size < SPARSE_HEADER_SIZE {
60 return Err(DmgError::BadSparseHeader(format!(
61 "file too small for 4096-byte sparse header: {file_size} bytes"
62 )));
63 }
64 reader.seek(SeekFrom::Start(0))?;
65 let mut header = [0u8; SPARSE_HEADER_SIZE as usize];
66 reader.read_exact(&mut header)?;
67
68 let magic = be_u32(&header, 0x00);
69 if magic != SPRS_MAGIC {
70 return Err(DmgError::NotSparseImage(magic));
71 }
72
73 let sectors_per_band = u64::from(be_u32(&header, 0x08));
74 if sectors_per_band == 0 {
75 return Err(DmgError::BadSparseHeader(
76 "sectors_per_band is 0 (would divide by zero)".into(),
77 ));
78 }
79 let band_size = sectors_per_band.saturating_mul(SECTOR);
80 let total_sectors = u64::from(be_u32(&header, 0x10));
81 let virtual_size = total_sectors.saturating_mul(SECTOR);
82
83 let phys_region = file_size.saturating_sub(SPARSE_HEADER_SIZE);
86 let nphys = phys_region.div_ceil(band_size);
87
88 let max_slots = (SPARSE_HEADER_SIZE as usize - BAND_TABLE_OFFSET) / 4;
93 if nphys > max_slots as u64 {
94 return Err(DmgError::BadSparseHeader(format!(
95 "{nphys} physical bands exceed the single 4096-byte band table (max {max_slots}); multi-node tables unsupported"
96 )));
97 }
98
99 let mut band_slot = HashMap::new();
101 for slot in 0..nphys {
102 let v = be_u32(&header, BAND_TABLE_OFFSET + slot as usize * 4);
103 if v != 0 {
104 band_slot.insert(u64::from(v) - 1, slot);
105 }
106 }
107
108 Ok(Self {
109 inner: reader,
110 band_size,
111 virtual_size,
112 file_size,
113 band_slot,
114 position: 0,
115 })
116 }
117
118 pub fn virtual_disk_size(&self) -> u64 {
120 self.virtual_size
121 }
122}
123
124impl<R: Read + Seek> Read for SparseImageReader<R> {
125 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
126 if buf.is_empty() || self.position >= self.virtual_size {
127 return Ok(0);
128 }
129 let vband = self.position / self.band_size;
130 let off = self.position % self.band_size;
131 let band_remaining = self.band_size - off;
132 let disk_remaining = self.virtual_size - self.position;
133 let to_read = (buf.len() as u64).min(band_remaining).min(disk_remaining) as usize;
134
135 match self.band_slot.get(&vband) {
136 None => buf[..to_read].fill(0),
138 Some(&slot) => {
139 let file_off = SPARSE_HEADER_SIZE
142 .saturating_add(slot.saturating_mul(self.band_size))
143 .saturating_add(off);
144 if file_off >= self.file_size {
145 buf[..to_read].fill(0);
146 } else {
147 let avail = (self.file_size - file_off).min(to_read as u64) as usize;
148 self.inner.seek(SeekFrom::Start(file_off))?;
149 self.inner.read_exact(&mut buf[..avail])?;
150 if avail < to_read {
151 buf[avail..to_read].fill(0);
152 }
153 }
154 }
155 }
156
157 self.position += to_read as u64;
158 Ok(to_read)
159 }
160}
161
162impl<R: Read + Seek> Seek for SparseImageReader<R> {
163 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
164 self.position = seek_within(self.position, self.virtual_size, pos);
165 Ok(self.position)
166 }
167}
168
169fn seek_within(current: u64, size: u64, pos: SeekFrom) -> u64 {
171 match pos {
172 SeekFrom::Start(n) => n,
173 SeekFrom::End(n) => {
174 if n >= 0 {
175 size.saturating_add(n as u64)
176 } else {
177 size.saturating_sub(n.unsigned_abs())
178 }
179 }
180 SeekFrom::Current(n) => {
181 if n >= 0 {
182 current.saturating_add(n as u64)
183 } else {
184 current.saturating_sub(n.unsigned_abs())
185 }
186 }
187 }
188}
189
190const SPARSEBUNDLE_TYPE: &str = "com.apple.diskimage.sparsebundle";
191
192pub struct SparseBundleReader {
198 bands_dir: PathBuf,
199 band_size: u64,
200 virtual_size: u64,
201 position: u64,
202}
203
204impl SparseBundleReader {
205 pub fn open(dir: &Path) -> Result<Self, DmgError> {
207 let info_path = dir.join("Info.plist");
208 let xml = match std::fs::read_to_string(&info_path) {
209 Ok(s) => s,
210 Err(e) if e.kind() == io::ErrorKind::NotFound => {
211 return Err(DmgError::MissingInfoPlist)
212 }
213 Err(e) => return Err(DmgError::Io(e)),
214 };
215
216 let info = parse_info_plist(&xml)?;
217
218 match info.bundle_type.as_deref() {
219 Some(SPARSEBUNDLE_TYPE) => {}
220 other => {
221 return Err(DmgError::BadInfoPlist(format!(
222 "diskimage-bundle-type is {other:?}, expected {SPARSEBUNDLE_TYPE:?}"
223 )))
224 }
225 }
226
227 let band_size = info
228 .band_size
229 .ok_or_else(|| DmgError::BadInfoPlist("missing band-size key".into()))?;
230 if band_size == 0 {
231 return Err(DmgError::BadInfoPlist(
232 "band-size is 0 (would divide by zero)".into(),
233 ));
234 }
235 let virtual_size = info
236 .size
237 .ok_or_else(|| DmgError::BadInfoPlist("missing size key".into()))?;
238
239 Ok(Self {
240 bands_dir: dir.join("bands"),
241 band_size,
242 virtual_size,
243 position: 0,
244 })
245 }
246
247 pub fn virtual_disk_size(&self) -> u64 {
249 self.virtual_size
250 }
251}
252
253impl Read for SparseBundleReader {
254 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
255 if buf.is_empty() || self.position >= self.virtual_size {
256 return Ok(0);
257 }
258 let band = self.position / self.band_size;
259 let off = self.position % self.band_size;
260 let band_remaining = self.band_size - off;
261 let disk_remaining = self.virtual_size - self.position;
262 let to_read = (buf.len() as u64).min(band_remaining).min(disk_remaining) as usize;
263
264 let path = self.bands_dir.join(format!("{band:x}"));
265 match std::fs::File::open(&path) {
266 Ok(mut f) => {
267 let flen = f.seek(SeekFrom::End(0))?;
268 if off >= flen {
269 buf[..to_read].fill(0);
270 } else {
271 let avail = (flen - off).min(to_read as u64) as usize;
272 f.seek(SeekFrom::Start(off))?;
273 f.read_exact(&mut buf[..avail])?;
274 if avail < to_read {
275 buf[avail..to_read].fill(0);
276 }
277 }
278 }
279 Err(e) if e.kind() == io::ErrorKind::NotFound => buf[..to_read].fill(0),
280 Err(e) => return Err(e),
281 }
282
283 self.position += to_read as u64;
284 Ok(to_read)
285 }
286}
287
288impl Seek for SparseBundleReader {
289 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
290 self.position = seek_within(self.position, self.virtual_size, pos);
291 Ok(self.position)
292 }
293}
294
295struct BundleInfo {
297 band_size: Option<u64>,
298 size: Option<u64>,
299 bundle_type: Option<String>,
300}
301
302fn parse_info_plist(xml: &str) -> Result<BundleInfo, DmgError> {
305 let mut reader = Reader::from_str(xml);
306 reader.config_mut().trim_text(true);
307
308 let mut band_size = None;
309 let mut size = None;
310 let mut bundle_type = None;
311 let mut current_key: Option<String> = None;
312 let mut elem: Option<Vec<u8>> = None;
313
314 loop {
315 match reader.read_event() {
316 Ok(Event::Start(e)) => elem = Some(e.name().as_ref().to_vec()),
317 Ok(Event::Text(e)) => {
318 let text = e
319 .xml_content(quick_xml::XmlVersion::Implicit1_0)
320 .unwrap_or_default();
321 let t = text.trim();
322 if t.is_empty() {
323 continue;
324 }
325 match elem.as_deref() {
326 Some(b"key") => current_key = Some(t.to_string()),
327 Some(b"integer") => {
328 let key = current_key.as_deref();
329 if key == Some("band-size") || key == Some("size") {
330 let v: u64 = t.parse().map_err(|_| {
331 DmgError::BadInfoPlist(format!(
332 "non-integer value {t:?} for key {key:?}"
333 ))
334 })?;
335 if key == Some("band-size") {
336 band_size = Some(v);
337 } else {
338 size = Some(v);
339 }
340 }
341 }
342 Some(b"string") if current_key.as_deref() == Some("diskimage-bundle-type") => {
343 bundle_type = Some(t.to_string());
344 }
345 _ => {}
346 }
347 }
348 Ok(Event::End(_)) => elem = None,
349 Ok(Event::Eof) => break,
350 Err(e) => return Err(DmgError::BadInfoPlist(e.to_string())),
351 _ => {}
352 }
353 }
354
355 Ok(BundleInfo {
356 band_size,
357 size,
358 bundle_type,
359 })
360}
361
362#[cfg(test)]
363mod sparseimage_tests {
364 use super::SparseImageReader;
365 use crate::DmgError;
366 use std::io::{Cursor, Read, Seek, SeekFrom};
367
368 const HDR: usize = 4096;
369
370 fn build(spb: u32, bands: &[Option<Vec<u8>>]) -> Vec<u8> {
379 let band_size = spb as usize * 512;
380 let total_sectors = (bands.len() * spb as usize) as u32;
381 let mut file = vec![0u8; HDR];
382 file[0..4].copy_from_slice(&0x7370_7273u32.to_be_bytes()); file[4..8].copy_from_slice(&3u32.to_be_bytes()); file[8..12].copy_from_slice(&spb.to_be_bytes()); file[16..20].copy_from_slice(&total_sectors.to_be_bytes()); let mut slot = 0usize;
387 for (v, b) in bands.iter().enumerate() {
388 if let Some(content) = b {
389 assert_eq!(content.len(), band_size, "band must be band_size");
390 let o = 0x40 + slot * 4;
391 file[o..o + 4].copy_from_slice(&((v as u32) + 1).to_be_bytes());
392 file.extend_from_slice(content);
393 slot += 1;
394 }
395 }
396 file
397 }
398
399 fn sample() -> Vec<u8> {
402 let mut b0 = vec![0xAAu8; 1024];
403 b0[0..4].copy_from_slice(&[0x48, 0x2b, 0x00, 0x04]);
404 let b2 = vec![0xCCu8; 1024];
405 build(2, &[Some(b0), None, Some(b2)])
406 }
407
408 #[test]
409 fn bad_magic_is_not_sparse_image() {
410 let mut f = sample();
411 f[0] = 0;
412 assert!(matches!(
413 SparseImageReader::open(Cursor::new(f)),
414 Err(DmgError::NotSparseImage(_))
415 ));
416 }
417
418 #[test]
419 fn zero_sectors_per_band_is_bad_header() {
420 let mut f = sample();
421 f[8..12].copy_from_slice(&0u32.to_be_bytes());
422 assert!(matches!(
423 SparseImageReader::open(Cursor::new(f)),
424 Err(DmgError::BadSparseHeader(_))
425 ));
426 }
427
428 #[test]
429 fn file_too_small_is_bad_header() {
430 assert!(matches!(
431 SparseImageReader::open(Cursor::new(vec![0u8; 100])),
432 Err(DmgError::BadSparseHeader(_))
433 ));
434 }
435
436 #[test]
437 fn too_many_physical_bands_is_bad_header() {
438 let mut f = vec![0u8; HDR + 1009 * 512];
441 f[0..4].copy_from_slice(&0x7370_7273u32.to_be_bytes());
442 f[8..12].copy_from_slice(&1u32.to_be_bytes()); f[16..20].copy_from_slice(&1009u32.to_be_bytes());
444 assert!(matches!(
445 SparseImageReader::open(Cursor::new(f)),
446 Err(DmgError::BadSparseHeader(_))
447 ));
448 }
449
450 #[test]
451 fn virtual_disk_size_is_total_sectors_times_512() {
452 let r = SparseImageReader::open(Cursor::new(sample())).unwrap();
453 assert_eq!(r.virtual_disk_size(), 6 * 512);
454 }
455
456 #[test]
457 fn reads_allocated_band_magic() {
458 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
459 let mut buf = [0u8; 4];
460 r.read_exact(&mut buf).unwrap();
461 assert_eq!(buf, [0x48, 0x2b, 0x00, 0x04]);
462 }
463
464 #[test]
465 fn hole_band_reads_zeros() {
466 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
467 r.seek(SeekFrom::Start(1024)).unwrap();
468 let mut buf = [0xFFu8; 1024];
469 r.read_exact(&mut buf).unwrap();
470 assert!(buf.iter().all(|&b| b == 0));
471 }
472
473 #[test]
474 fn second_allocated_band_reads_content() {
475 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
476 r.seek(SeekFrom::Start(2048)).unwrap();
477 let mut buf = [0u8; 4];
478 r.read_exact(&mut buf).unwrap();
479 assert_eq!(buf, [0xCC, 0xCC, 0xCC, 0xCC]);
480 }
481
482 #[test]
483 fn read_across_band_boundary() {
484 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
485 r.seek(SeekFrom::Start(1023)).unwrap();
486 let mut buf = [0xEEu8; 2];
487 r.read_exact(&mut buf).unwrap();
488 assert_eq!(buf, [0xAA, 0x00]);
489 }
490
491 #[test]
492 fn seek_within_band_reads_offset() {
493 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
494 r.seek(SeekFrom::Start(10)).unwrap();
495 let mut buf = [0u8; 1];
496 r.read_exact(&mut buf).unwrap();
497 assert_eq!(buf[0], 0xAA);
498 }
499
500 #[test]
501 fn seek_from_end_and_current() {
502 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
503 assert_eq!(r.seek(SeekFrom::End(0)).unwrap(), 6 * 512);
504 assert_eq!(r.seek(SeekFrom::End(-2048)).unwrap(), 1024);
505 assert_eq!(r.seek(SeekFrom::Current(1024)).unwrap(), 2048);
506 assert_eq!(r.seek(SeekFrom::Current(-2048)).unwrap(), 0);
507 }
508
509 #[test]
510 fn read_past_eof_returns_zero() {
511 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
512 r.seek(SeekFrom::Start(6 * 512)).unwrap();
513 let mut buf = [0u8; 16];
514 assert_eq!(r.read(&mut buf).unwrap(), 0);
515 }
516
517 #[test]
518 fn empty_buffer_reads_zero() {
519 let mut r = SparseImageReader::open(Cursor::new(sample())).unwrap();
520 assert_eq!(r.read(&mut []).unwrap(), 0);
521 }
522
523 #[test]
524 fn unmapped_virtual_band_reads_zeros() {
525 let f = build(2, &[Some(vec![0xAAu8; 1024]), None, None]);
527 let mut r = SparseImageReader::open(Cursor::new(f)).unwrap();
528 r.seek(SeekFrom::Start(2048)).unwrap();
529 let mut buf = [0xFFu8; 512];
530 r.read_exact(&mut buf).unwrap();
531 assert!(buf.iter().all(|&b| b == 0));
532 }
533
534 #[test]
535 fn truncated_physical_band_tail_reads_zeros() {
536 let mut f = build(2, &[Some(vec![0xAAu8; 1024])]);
539 f.truncate(HDR + 512);
540 let mut r = SparseImageReader::open(Cursor::new(f)).unwrap();
541 let mut buf = [0u8; 1024];
542 r.read_exact(&mut buf).unwrap();
543 assert!(buf[..512].iter().all(|&b| b == 0xAA));
544 assert!(buf[512..].iter().all(|&b| b == 0));
545 }
546
547 #[test]
548 fn read_starting_in_truncated_region_reads_zeros() {
549 let mut f = build(2, &[Some(vec![0xAAu8; 1024])]);
552 f.truncate(HDR + 512);
553 let mut r = SparseImageReader::open(Cursor::new(f)).unwrap();
554 r.seek(SeekFrom::Start(600)).unwrap();
555 let mut buf = [0xFFu8; 8];
556 r.read_exact(&mut buf).unwrap();
557 assert!(buf.iter().all(|&b| b == 0));
558 }
559}
560
561#[cfg(test)]
562mod sparsebundle_tests {
563 use super::SparseBundleReader;
564 use crate::DmgError;
565 use std::fs;
566 use std::io::{Read, Seek, SeekFrom, Write};
567 use tempfile::TempDir;
568
569 fn bundle(plist: &str, bands: &[(u64, Vec<u8>)]) -> TempDir {
572 let dir = TempDir::new().unwrap();
573 fs::write(dir.path().join("Info.plist"), plist).unwrap();
574 let bands_dir = dir.path().join("bands");
575 fs::create_dir(&bands_dir).unwrap();
576 for (idx, bytes) in bands {
577 let mut f = fs::File::create(bands_dir.join(format!("{idx:x}"))).unwrap();
578 f.write_all(bytes).unwrap();
579 }
580 dir
581 }
582
583 fn plist(band_size: Option<u64>, size: Option<u64>, bundle_type: Option<&str>) -> String {
584 use std::fmt::Write as _;
585 let mut body = String::from(
586 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\">\n<dict>\n",
587 );
588 if let Some(t) = bundle_type {
589 let _ = writeln!(
590 body,
591 " <key>diskimage-bundle-type</key><string>{t}</string>"
592 );
593 }
594 if let Some(b) = band_size {
595 let _ = writeln!(body, " <key>band-size</key><integer>{b}</integer>");
596 }
597 if let Some(s) = size {
598 let _ = writeln!(body, " <key>size</key><integer>{s}</integer>");
599 }
600 body.push_str("</dict>\n</plist>\n");
601 body
602 }
603
604 fn valid_plist() -> String {
608 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\">\n<dict>\n\
609 \t<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>\n\
610 \t<key>band-size</key><integer>1024</integer>\n\
611 \t<key>bundle-backingstore-version</key><integer>1</integer>\n\
612 \t<key>diskimage-bundle-type</key><string>com.apple.diskimage.sparsebundle</string>\n\
613 \t<key>pad</key><string> </string>\n\
614 \t<key>size</key><integer>3072</integer>\n\
615 </dict>\n</plist>\n"
616 .to_string()
617 }
618
619 fn valid_bundle() -> TempDir {
622 let mut b0 = vec![0xAAu8; 1024];
623 b0[0..4].copy_from_slice(&[0x48, 0x2b, 0x00, 0x04]);
624 let b2 = vec![0xCCu8; 512];
625 bundle(&valid_plist(), &[(0, b0), (2, b2)])
626 }
627
628 #[test]
629 fn missing_info_plist_errors() {
630 let dir = TempDir::new().unwrap();
631 assert!(matches!(
632 SparseBundleReader::open(dir.path()),
633 Err(DmgError::MissingInfoPlist)
634 ));
635 }
636
637 #[test]
638 fn malformed_plist_errors() {
639 let dir = bundle("<plist><dict><key>oops", &[]);
640 assert!(matches!(
641 SparseBundleReader::open(dir.path()),
642 Err(DmgError::BadInfoPlist(_))
643 ));
644 }
645
646 #[test]
647 fn wrong_bundle_type_errors() {
648 let dir = bundle(
649 &plist(Some(1024), Some(3072), Some("com.apple.diskimage.sparse")),
650 &[],
651 );
652 assert!(matches!(
653 SparseBundleReader::open(dir.path()),
654 Err(DmgError::BadInfoPlist(_))
655 ));
656 }
657
658 #[test]
659 fn missing_band_size_errors() {
660 let dir = bundle(
661 &plist(None, Some(3072), Some("com.apple.diskimage.sparsebundle")),
662 &[],
663 );
664 assert!(matches!(
665 SparseBundleReader::open(dir.path()),
666 Err(DmgError::BadInfoPlist(_))
667 ));
668 }
669
670 #[test]
671 fn zero_band_size_errors() {
672 let dir = bundle(
673 &plist(
674 Some(0),
675 Some(3072),
676 Some("com.apple.diskimage.sparsebundle"),
677 ),
678 &[],
679 );
680 assert!(matches!(
681 SparseBundleReader::open(dir.path()),
682 Err(DmgError::BadInfoPlist(_))
683 ));
684 }
685
686 #[test]
687 fn missing_size_errors() {
688 let dir = bundle(
689 &plist(Some(1024), None, Some("com.apple.diskimage.sparsebundle")),
690 &[],
691 );
692 assert!(matches!(
693 SparseBundleReader::open(dir.path()),
694 Err(DmgError::BadInfoPlist(_))
695 ));
696 }
697
698 #[test]
699 fn non_integer_band_size_errors() {
700 let xml = "<plist version=\"1.0\">\n<dict>\n\
701 <key>diskimage-bundle-type</key><string>com.apple.diskimage.sparsebundle</string>\n\
702 <key>band-size</key><integer>notanumber</integer>\n\
703 <key>size</key><integer>3072</integer>\n</dict>\n</plist>\n";
704 let dir = bundle(xml, &[]);
705 assert!(matches!(
706 SparseBundleReader::open(dir.path()),
707 Err(DmgError::BadInfoPlist(_))
708 ));
709 }
710
711 #[test]
712 fn virtual_disk_size_matches_plist_size() {
713 let dir = valid_bundle();
714 let r = SparseBundleReader::open(dir.path()).unwrap();
715 assert_eq!(r.virtual_disk_size(), 3072);
716 }
717
718 #[test]
719 fn reads_present_band_magic() {
720 let dir = valid_bundle();
721 let mut r = SparseBundleReader::open(dir.path()).unwrap();
722 let mut buf = [0u8; 4];
723 r.read_exact(&mut buf).unwrap();
724 assert_eq!(buf, [0x48, 0x2b, 0x00, 0x04]);
725 }
726
727 #[test]
728 fn missing_band_file_reads_zeros() {
729 let dir = valid_bundle();
730 let mut r = SparseBundleReader::open(dir.path()).unwrap();
731 r.seek(SeekFrom::Start(1024)).unwrap(); let mut buf = [0xFFu8; 1024];
733 r.read_exact(&mut buf).unwrap();
734 assert!(buf.iter().all(|&b| b == 0));
735 }
736
737 #[test]
738 fn partial_band_tail_reads_zeros() {
739 let dir = valid_bundle();
740 let mut r = SparseBundleReader::open(dir.path()).unwrap();
741 r.seek(SeekFrom::Start(2048)).unwrap(); let mut buf = [0u8; 1024];
743 r.read_exact(&mut buf).unwrap();
744 assert!(buf[..512].iter().all(|&b| b == 0xCC));
745 assert!(buf[512..].iter().all(|&b| b == 0));
746 }
747
748 #[test]
749 fn read_across_band_boundary() {
750 let dir = valid_bundle();
751 let mut r = SparseBundleReader::open(dir.path()).unwrap();
752 r.seek(SeekFrom::Start(1023)).unwrap();
753 let mut buf = [0xEEu8; 2];
754 r.read_exact(&mut buf).unwrap(); assert_eq!(buf, [0xAA, 0x00]);
756 }
757
758 #[test]
759 fn seek_within_band_reads_offset() {
760 let dir = valid_bundle();
761 let mut r = SparseBundleReader::open(dir.path()).unwrap();
762 r.seek(SeekFrom::Start(500)).unwrap();
763 let mut buf = [0u8; 1];
764 r.read_exact(&mut buf).unwrap();
765 assert_eq!(buf[0], 0xAA);
766 }
767
768 #[test]
769 fn seek_from_end_and_current() {
770 let dir = valid_bundle();
771 let mut r = SparseBundleReader::open(dir.path()).unwrap();
772 assert_eq!(r.seek(SeekFrom::End(0)).unwrap(), 3072);
773 assert_eq!(r.seek(SeekFrom::End(-1024)).unwrap(), 2048);
774 assert_eq!(r.seek(SeekFrom::Current(-2048)).unwrap(), 0);
775 }
776
777 #[test]
778 fn read_past_eof_returns_zero() {
779 let dir = valid_bundle();
780 let mut r = SparseBundleReader::open(dir.path()).unwrap();
781 r.seek(SeekFrom::Start(3072)).unwrap();
782 let mut buf = [0u8; 16];
783 assert_eq!(r.read(&mut buf).unwrap(), 0);
784 }
785
786 #[test]
787 fn empty_buffer_reads_zero() {
788 let dir = valid_bundle();
789 let mut r = SparseBundleReader::open(dir.path()).unwrap();
790 assert_eq!(r.read(&mut []).unwrap(), 0);
791 }
792
793 #[test]
794 fn read_wholly_past_short_band_reads_zeros() {
795 let dir = valid_bundle();
798 let mut r = SparseBundleReader::open(dir.path()).unwrap();
799 r.seek(SeekFrom::Start(2560)).unwrap();
800 let mut buf = [0xFFu8; 512];
801 r.read_exact(&mut buf).unwrap();
802 assert!(buf.iter().all(|&b| b == 0));
803 }
804
805 #[test]
806 fn bands_path_that_is_a_file_errors_on_read() {
807 let dir = TempDir::new().unwrap();
810 fs::write(dir.path().join("Info.plist"), valid_plist()).unwrap();
811 fs::write(dir.path().join("bands"), b"not a directory").unwrap();
812 let mut r = SparseBundleReader::open(dir.path()).unwrap();
813 let mut buf = [0u8; 16];
814 assert!(r.read(&mut buf).is_err());
815 }
816
817 #[test]
818 fn info_plist_that_is_a_directory_is_io_error() {
819 let dir = TempDir::new().unwrap();
822 fs::create_dir(dir.path().join("Info.plist")).unwrap();
823 assert!(matches!(
824 SparseBundleReader::open(dir.path()),
825 Err(DmgError::Io(_))
826 ));
827 }
828
829 #[test]
830 fn mismatched_xml_tags_error() {
831 let dir = bundle("<plist version=\"1.0\">\n<dict></nope>\n</plist>\n", &[]);
833 assert!(matches!(
834 SparseBundleReader::open(dir.path()),
835 Err(DmgError::BadInfoPlist(_))
836 ));
837 }
838}