1use std::cmp::max;
6use std::cmp::min;
7use std::collections::HashSet;
8use std::convert::TryInto;
9use std::fs::File;
10use std::fs::OpenOptions;
11use std::io;
12use std::io::ErrorKind;
13use std::io::Read;
14use std::io::Seek;
15use std::io::SeekFrom;
16use std::io::Write;
17use std::ops::Range;
18use std::path::Path;
19use std::path::PathBuf;
20use std::sync::atomic::AtomicBool;
21use std::sync::atomic::Ordering;
22use std::sync::Arc;
23
24use async_trait::async_trait;
25use base::AsRawDescriptors;
26use base::FileAllocate;
27use base::FileReadWriteAtVolatile;
28use base::FileSetLen;
29use base::RawDescriptor;
30use base::VolatileSlice;
31use crc32fast::Hasher;
32use cros_async::BackingMemory;
33use cros_async::Executor;
34use cros_async::MemRegionIter;
35use protobuf::Message;
36use protos::cdisk_spec;
37use protos::cdisk_spec::ComponentDisk;
38use protos::cdisk_spec::CompositeDisk;
39use protos::cdisk_spec::ReadWriteCapability;
40use remain::sorted;
41use thiserror::Error;
42use uuid::Uuid;
43
44use crate::gpt;
45use crate::gpt::write_gpt_header;
46use crate::gpt::write_protective_mbr;
47use crate::gpt::GptPartitionEntry;
48use crate::gpt::GPT_BEGINNING_SIZE;
49use crate::gpt::GPT_END_SIZE;
50use crate::gpt::GPT_HEADER_SIZE;
51use crate::gpt::GPT_NUM_PARTITIONS;
52use crate::gpt::GPT_PARTITION_ENTRY_SIZE;
53use crate::gpt::SECTOR_SIZE;
54use crate::open_disk_file;
55use crate::AsyncDisk;
56use crate::DiskFile;
57use crate::DiskFileParams;
58use crate::DiskGetLen;
59use crate::ImageType;
60use crate::ToAsyncDisk;
61
62const PARTITION_ALIGNMENT_SIZE: usize = GPT_BEGINNING_SIZE as usize
65 - 2 * SECTOR_SIZE as usize
66 - GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize;
67const HEADER_PADDING_LENGTH: usize = SECTOR_SIZE as usize - GPT_HEADER_SIZE as usize;
68const PARTITION_SIZE_SHIFT: u8 = 12;
70
71const LINUX_FILESYSTEM_GUID: Uuid = Uuid::from_u128(0x0FC63DAF_8483_4772_8E79_3D69D8477DE4);
73const EFI_SYSTEM_PARTITION_GUID: Uuid = Uuid::from_u128(0xC12A7328_F81F_11D2_BA4B_00A0C93EC93B);
74
75#[sorted]
76#[derive(Error, Debug)]
77pub enum Error {
78 #[error("failed to use underlying disk: \"{0}\"")]
79 DiskError(Box<crate::Error>),
80 #[error("duplicate GPT partition label \"{0}\"")]
81 DuplicatePartitionLabel(String),
82 #[error("failed to write GPT header: \"{0}\"")]
83 GptError(gpt::Error),
84 #[error("invalid magic header for composite disk format")]
85 InvalidMagicHeader,
86 #[error("invalid partition path {0:?}")]
87 InvalidPath(PathBuf),
88 #[error("failed to parse specification proto: \"{0}\"")]
89 InvalidProto(protobuf::Error),
90 #[error("invalid specification: \"{0}\"")]
91 InvalidSpecification(String),
92 #[error("no image files for partition {0:?}")]
93 NoImageFiles(PartitionInfo),
94 #[error("failed to open component file \"{1}\": \"{0}\"")]
95 OpenFile(io::Error, String),
96 #[error("failed to read specification: \"{0}\"")]
97 ReadSpecificationError(io::Error),
98 #[error("Read-write partition {0:?} size is not a multiple of {multiple}.", multiple = 1 << PARTITION_SIZE_SHIFT)]
99 UnalignedReadWrite(PartitionInfo),
100 #[error("unknown version {0} in specification")]
101 UnknownVersion(u64),
102 #[error("unsupported component disk type \"{0:?}\"")]
103 UnsupportedComponent(ImageType),
104 #[error("failed to write composite disk header: \"{0}\"")]
105 WriteHeader(io::Error),
106 #[error("failed to write specification proto: \"{0}\"")]
107 WriteProto(protobuf::Error),
108 #[error("failed to write zero filler: \"{0}\"")]
109 WriteZeroFiller(io::Error),
110}
111
112impl From<gpt::Error> for Error {
113 fn from(e: gpt::Error) -> Self {
114 Self::GptError(e)
115 }
116}
117
118pub type Result<T> = std::result::Result<T, Error>;
119
120#[derive(Debug)]
121struct ComponentDiskPart {
122 file: Box<dyn DiskFile>,
123 offset: u64,
124 length: u64,
125 needs_flush: AtomicBool,
127}
128
129impl ComponentDiskPart {
130 fn range(&self) -> Range<u64> {
131 self.offset..(self.offset + self.length)
132 }
133}
134
135#[derive(Debug)]
140pub struct CompositeDiskFile {
141 component_disks: Vec<ComponentDiskPart>,
142 _disk_spec_file: File,
144}
145
146impl DiskFile for CompositeDiskFile {}
148
149fn ranges_overlap(a: &Range<u64>, b: &Range<u64>) -> bool {
150 range_intersection(a, b).is_some()
151}
152
153fn range_intersection(a: &Range<u64>, b: &Range<u64>) -> Option<Range<u64>> {
154 let r = Range {
155 start: max(a.start, b.start),
156 end: min(a.end, b.end),
157 };
158 if r.is_empty() {
159 None
160 } else {
161 Some(r)
162 }
163}
164
165const COMPOSITE_DISK_VERSION: u64 = 2;
167
168pub const CDISK_MAGIC: &str = "composite_disk\x1d";
170
171impl CompositeDiskFile {
172 fn new(mut disks: Vec<ComponentDiskPart>, disk_spec_file: File) -> Result<CompositeDiskFile> {
173 disks.sort_by(|d1, d2| d1.offset.cmp(&d2.offset));
174 for s in disks.windows(2) {
175 if s[0].offset == s[1].offset {
176 return Err(Error::InvalidSpecification(format!(
177 "Two disks at offset {}",
178 s[0].offset
179 )));
180 }
181 }
182 Ok(CompositeDiskFile {
183 component_disks: disks,
184 _disk_spec_file: disk_spec_file,
185 })
186 }
187
188 pub fn from_file(mut file: File, params: DiskFileParams) -> Result<CompositeDiskFile> {
192 file.seek(SeekFrom::Start(0))
193 .map_err(Error::ReadSpecificationError)?;
194 let mut magic_space = [0u8; CDISK_MAGIC.len()];
195 file.read_exact(&mut magic_space[..])
196 .map_err(Error::ReadSpecificationError)?;
197 if magic_space != CDISK_MAGIC.as_bytes() {
198 return Err(Error::InvalidMagicHeader);
199 }
200 let proto: cdisk_spec::CompositeDisk =
201 Message::parse_from_reader(&mut file).map_err(Error::InvalidProto)?;
202 if proto.version > COMPOSITE_DISK_VERSION {
203 return Err(Error::UnknownVersion(proto.version));
204 }
205 let mut disks: Vec<ComponentDiskPart> = proto
206 .component_disks
207 .iter()
208 .map(|disk| {
209 let writable = !params.is_read_only
210 && disk.read_write_capability
211 == cdisk_spec::ReadWriteCapability::READ_WRITE.into();
212 let component_path = PathBuf::from(&disk.file_path);
213 let path = if component_path.is_relative() || proto.version > 1 {
214 params.path.parent().unwrap().join(component_path)
215 } else {
216 component_path
217 };
218
219 Ok(ComponentDiskPart {
226 file: open_disk_file(DiskFileParams {
227 path: path.to_owned(),
228 is_read_only: !writable,
229 is_sparse_file: params.is_sparse_file && writable,
230 is_overlapped: false,
232 is_direct: params.is_direct,
233 lock: params.lock,
234 depth: params.depth + 1,
235 })
236 .map_err(|e| Error::DiskError(Box::new(e)))?,
237 offset: disk.offset,
238 length: 0, needs_flush: AtomicBool::new(false),
240 })
241 })
242 .collect::<Result<Vec<ComponentDiskPart>>>()?;
243 disks.sort_by(|d1, d2| d1.offset.cmp(&d2.offset));
244 for i in 0..(disks.len() - 1) {
245 let length = disks[i + 1].offset - disks[i].offset;
246 if length == 0 {
247 let text = format!("Two disks at offset {}", disks[i].offset);
248 return Err(Error::InvalidSpecification(text));
249 }
250 if let Some(disk) = disks.get_mut(i) {
251 disk.length = length;
252 } else {
253 let text = format!("Unable to set disk length {length}");
254 return Err(Error::InvalidSpecification(text));
255 }
256 }
257 if let Some(last_disk) = disks.last_mut() {
258 if proto.length <= last_disk.offset {
259 let text = format!(
260 "Full size of disk doesn't match last offset. {} <= {}",
261 proto.length, last_disk.offset
262 );
263 return Err(Error::InvalidSpecification(text));
264 }
265 last_disk.length = proto.length - last_disk.offset;
266 } else {
267 let text = format!("Unable to set last disk length to end at {}", proto.length);
268 return Err(Error::InvalidSpecification(text));
269 }
270
271 CompositeDiskFile::new(disks, file)
272 }
273
274 fn length(&self) -> u64 {
275 if let Some(disk) = self.component_disks.last() {
276 disk.offset + disk.length
277 } else {
278 0
279 }
280 }
281
282 fn disk_at_offset(&self, offset: u64) -> io::Result<&ComponentDiskPart> {
283 self.component_disks
284 .iter()
285 .find(|disk| disk.range().contains(&offset))
286 .ok_or_else(|| {
287 io::Error::new(
288 ErrorKind::InvalidData,
289 format!("no disk at offset {offset}"),
290 )
291 })
292 }
293}
294
295impl DiskGetLen for CompositeDiskFile {
296 fn get_len(&self) -> io::Result<u64> {
297 Ok(self.length())
298 }
299}
300
301impl FileSetLen for CompositeDiskFile {
302 fn set_len(&self, _len: u64) -> io::Result<()> {
303 Err(io::Error::other("unsupported operation"))
304 }
305}
306
307impl FileReadWriteAtVolatile for CompositeDiskFile {
317 fn read_at_volatile(&self, slice: VolatileSlice, offset: u64) -> io::Result<usize> {
318 let cursor_location = offset;
319 let disk = self.disk_at_offset(cursor_location)?;
320 let subslice = if cursor_location + slice.size() as u64 > disk.offset + disk.length {
321 let new_size = disk.offset + disk.length - cursor_location;
322 slice
323 .sub_slice(0, new_size as usize)
324 .map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string()))?
325 } else {
326 slice
327 };
328 disk.file
329 .read_at_volatile(subslice, cursor_location - disk.offset)
330 }
331 fn write_at_volatile(&self, slice: VolatileSlice, offset: u64) -> io::Result<usize> {
332 let cursor_location = offset;
333 let disk = self.disk_at_offset(cursor_location)?;
334 let subslice = if cursor_location + slice.size() as u64 > disk.offset + disk.length {
335 let new_size = disk.offset + disk.length - cursor_location;
336 slice
337 .sub_slice(0, new_size as usize)
338 .map_err(|e| io::Error::new(ErrorKind::InvalidData, e.to_string()))?
339 } else {
340 slice
341 };
342
343 let bytes = disk
344 .file
345 .write_at_volatile(subslice, cursor_location - disk.offset)?;
346 disk.needs_flush.store(true, Ordering::SeqCst);
347 Ok(bytes)
348 }
349}
350
351impl AsRawDescriptors for CompositeDiskFile {
352 fn as_raw_descriptors(&self) -> Vec<RawDescriptor> {
353 self.component_disks
354 .iter()
355 .flat_map(|d| d.file.as_raw_descriptors())
356 .collect()
357 }
358}
359
360struct AsyncComponentDiskPart {
361 file: Box<dyn AsyncDisk>,
362 offset: u64,
363 length: u64,
364 needs_flush: AtomicBool,
365}
366
367pub struct AsyncCompositeDiskFile {
368 component_disks: Vec<AsyncComponentDiskPart>,
369}
370
371impl DiskGetLen for AsyncCompositeDiskFile {
372 fn get_len(&self) -> io::Result<u64> {
373 Ok(self.length())
374 }
375}
376
377impl FileSetLen for AsyncCompositeDiskFile {
378 fn set_len(&self, _len: u64) -> io::Result<()> {
379 Err(io::Error::other("unsupported operation"))
380 }
381}
382
383impl FileAllocate for AsyncCompositeDiskFile {
384 fn allocate(&self, offset: u64, length: u64) -> io::Result<()> {
385 let range = offset..(offset + length);
386 let disks = self
387 .component_disks
388 .iter()
389 .filter(|disk| ranges_overlap(&disk.range(), &range));
390 for disk in disks {
391 if let Some(intersection) = range_intersection(&range, &disk.range()) {
392 disk.file.allocate(
393 intersection.start - disk.offset,
394 intersection.end - intersection.start,
395 )?;
396 disk.needs_flush.store(true, Ordering::SeqCst);
397 }
398 }
399 Ok(())
400 }
401}
402
403impl ToAsyncDisk for CompositeDiskFile {
404 fn to_async_disk(self: Box<Self>, ex: &Executor) -> crate::Result<Box<dyn AsyncDisk>> {
405 Ok(Box::new(AsyncCompositeDiskFile {
406 component_disks: self
407 .component_disks
408 .into_iter()
409 .map(|disk| -> crate::Result<_> {
410 Ok(AsyncComponentDiskPart {
411 file: disk.file.to_async_disk(ex)?,
412 offset: disk.offset,
413 length: disk.length,
414 needs_flush: disk.needs_flush,
415 })
416 })
417 .collect::<crate::Result<Vec<_>>>()?,
418 }))
419 }
420}
421
422impl AsyncComponentDiskPart {
423 fn range(&self) -> Range<u64> {
424 self.offset..(self.offset + self.length)
425 }
426
427 fn set_needs_flush(&self) {
428 self.needs_flush.store(true, Ordering::SeqCst);
429 }
430}
431
432impl AsyncCompositeDiskFile {
433 fn length(&self) -> u64 {
434 if let Some(disk) = self.component_disks.last() {
435 disk.offset + disk.length
436 } else {
437 0
438 }
439 }
440
441 fn disk_at_offset(&self, offset: u64) -> io::Result<&AsyncComponentDiskPart> {
442 self.component_disks
443 .iter()
444 .find(|disk| disk.range().contains(&offset))
445 .ok_or_else(|| {
446 io::Error::new(
447 ErrorKind::InvalidData,
448 format!("no disk at offset {offset}"),
449 )
450 })
451 }
452
453 fn disks_in_range<'a>(&'a self, range: &Range<u64>) -> Vec<&'a AsyncComponentDiskPart> {
454 self.component_disks
455 .iter()
456 .filter(|disk| ranges_overlap(&disk.range(), range))
457 .collect()
458 }
459}
460
461#[async_trait(?Send)]
462impl AsyncDisk for AsyncCompositeDiskFile {
463 async fn flush(&self) -> crate::Result<()> {
464 futures::future::try_join_all(self.component_disks.iter().map(|c| c.file.flush())).await?;
465 Ok(())
466 }
467
468 async fn fsync(&self) -> crate::Result<()> {
469 for disk in self.component_disks.iter() {
472 if disk.needs_flush.fetch_and(false, Ordering::SeqCst) {
473 if let Err(e) = disk.file.fsync().await {
474 disk.set_needs_flush();
475 return Err(e);
476 }
477 }
478 }
479 Ok(())
480 }
481
482 async fn fdatasync(&self) -> crate::Result<()> {
483 for disk in self.component_disks.iter() {
486 if disk.needs_flush.fetch_and(false, Ordering::SeqCst) {
487 if let Err(e) = disk.file.fdatasync().await {
488 disk.set_needs_flush();
489 return Err(e);
490 }
491 }
492 }
493 Ok(())
494 }
495
496 async fn read_to_mem<'a>(
497 &'a self,
498 file_offset: u64,
499 mem: Arc<dyn BackingMemory + Send + Sync>,
500 mem_offsets: MemRegionIter<'a>,
501 ) -> crate::Result<usize> {
502 let disk = self
503 .disk_at_offset(file_offset)
504 .map_err(crate::Error::ReadingData)?;
505 let remaining_disk = disk.offset + disk.length - file_offset;
506 disk.file
507 .read_to_mem(
508 file_offset - disk.offset,
509 mem,
510 mem_offsets.take_bytes(remaining_disk.try_into().unwrap()),
511 )
512 .await
513 }
514
515 async fn write_from_mem<'a>(
516 &'a self,
517 file_offset: u64,
518 mem: Arc<dyn BackingMemory + Send + Sync>,
519 mem_offsets: MemRegionIter<'a>,
520 ) -> crate::Result<usize> {
521 let disk = self
522 .disk_at_offset(file_offset)
523 .map_err(crate::Error::ReadingData)?;
524 let remaining_disk = disk.offset + disk.length - file_offset;
525 let n = disk
526 .file
527 .write_from_mem(
528 file_offset - disk.offset,
529 mem,
530 mem_offsets.take_bytes(remaining_disk.try_into().unwrap()),
531 )
532 .await?;
533 disk.set_needs_flush();
534 Ok(n)
535 }
536
537 async fn punch_hole(&self, file_offset: u64, length: u64) -> crate::Result<()> {
538 let range = file_offset..(file_offset + length);
539 let disks = self.disks_in_range(&range);
540 for disk in disks {
541 if let Some(intersection) = range_intersection(&range, &disk.range()) {
542 disk.file
543 .punch_hole(
544 intersection.start - disk.offset,
545 intersection.end - intersection.start,
546 )
547 .await?;
548 disk.set_needs_flush();
549 }
550 }
551 Ok(())
552 }
553
554 async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> crate::Result<()> {
555 let range = file_offset..(file_offset + length);
556 let disks = self.disks_in_range(&range);
557 for disk in disks {
558 if let Some(intersection) = range_intersection(&range, &disk.range()) {
559 disk.file
560 .write_zeroes_at(
561 intersection.start - disk.offset,
562 intersection.end - intersection.start,
563 )
564 .await?;
565 disk.set_needs_flush();
566 }
567 }
568 Ok(())
569 }
570}
571
572#[derive(Clone, Debug, Eq, PartialEq)]
574pub struct PartitionInfo {
575 pub label: String,
576 pub path: PathBuf,
577 pub partition_type: ImagePartitionType,
578 pub writable: bool,
579 pub size: u64,
580 pub part_guid: Option<Uuid>,
581}
582
583impl PartitionInfo {
584 fn aligned_size(&self) -> u64 {
585 self.size.next_multiple_of(1 << PARTITION_SIZE_SHIFT)
586 }
587}
588
589#[derive(Copy, Clone, Debug, Eq, PartialEq)]
591pub enum ImagePartitionType {
592 LinuxFilesystem,
593 EfiSystemPartition,
594}
595
596impl ImagePartitionType {
597 fn guid(self) -> Uuid {
598 match self {
599 Self::LinuxFilesystem => LINUX_FILESYSTEM_GUID,
600 Self::EfiSystemPartition => EFI_SYSTEM_PARTITION_GUID,
601 }
602 }
603}
604
605fn write_beginning(
607 file: &mut impl Write,
608 disk_guid: Uuid,
609 partitions: &[u8],
610 partition_entries_crc32: u32,
611 secondary_table_offset: u64,
612 disk_size: u64,
613) -> Result<()> {
614 write_protective_mbr(file, disk_size)?;
616
617 write_gpt_header(
619 file,
620 disk_guid,
621 partition_entries_crc32,
622 secondary_table_offset,
623 false,
624 )?;
625 file.write_all(&[0; HEADER_PADDING_LENGTH])
626 .map_err(Error::WriteHeader)?;
627
628 file.write_all(partitions).map_err(Error::WriteHeader)?;
630
631 file.write_all(&[0; PARTITION_ALIGNMENT_SIZE])
633 .map_err(Error::WriteHeader)?;
634
635 Ok(())
636}
637
638fn write_end(
640 file: &mut impl Write,
641 disk_guid: Uuid,
642 partitions: &[u8],
643 partition_entries_crc32: u32,
644 secondary_table_offset: u64,
645) -> Result<()> {
646 file.write_all(partitions).map_err(Error::WriteHeader)?;
648
649 write_gpt_header(
651 file,
652 disk_guid,
653 partition_entries_crc32,
654 secondary_table_offset,
655 true,
656 )?;
657 file.write_all(&[0; HEADER_PADDING_LENGTH])
658 .map_err(Error::WriteHeader)?;
659
660 Ok(())
661}
662
663fn create_gpt_entry(partition: &PartitionInfo, offset: u64) -> GptPartitionEntry {
665 let mut partition_name: Vec<u16> = partition.label.encode_utf16().collect();
666 partition_name.resize(36, 0);
667
668 GptPartitionEntry {
669 partition_type_guid: partition.partition_type.guid(),
670 unique_partition_guid: partition.part_guid.unwrap_or(Uuid::new_v4()),
671 first_lba: offset / SECTOR_SIZE,
672 last_lba: (offset + partition.aligned_size()) / SECTOR_SIZE - 1,
673 attributes: 0,
674 partition_name: partition_name.try_into().unwrap(),
675 }
676}
677
678fn create_component_disks(
680 partition: &PartitionInfo,
681 offset: u64,
682 zero_filler_path: &str,
683) -> Result<Vec<ComponentDisk>> {
684 let aligned_size = partition.aligned_size();
685
686 let mut component_disks = vec![ComponentDisk {
687 offset,
688 file_path: partition
689 .path
690 .to_str()
691 .ok_or_else(|| Error::InvalidPath(partition.path.to_owned()))?
692 .to_string(),
693 read_write_capability: if partition.writable {
694 ReadWriteCapability::READ_WRITE.into()
695 } else {
696 ReadWriteCapability::READ_ONLY.into()
697 },
698 ..ComponentDisk::new()
699 }];
700
701 if partition.size != aligned_size {
702 if partition.writable {
703 return Err(Error::UnalignedReadWrite(partition.to_owned()));
704 } else {
705 component_disks.push(ComponentDisk {
708 offset: offset + partition.size,
709 file_path: zero_filler_path.to_owned(),
710 read_write_capability: ReadWriteCapability::READ_ONLY.into(),
711 ..ComponentDisk::new()
712 });
713 }
714 }
715
716 Ok(component_disks)
717}
718
719pub fn create_composite_disk(
722 partitions: &[PartitionInfo],
723 zero_filler_path: &Path,
724 header_path: &Path,
725 header_file: &mut impl Write,
726 footer_path: &Path,
727 footer_file: &mut impl Write,
728 output_composite: &mut File,
729) -> Result<()> {
730 let zero_filler_path = zero_filler_path
731 .to_str()
732 .ok_or_else(|| Error::InvalidPath(zero_filler_path.to_owned()))?
733 .to_string();
734 let header_path = header_path
735 .to_str()
736 .ok_or_else(|| Error::InvalidPath(header_path.to_owned()))?
737 .to_string();
738 let footer_path = footer_path
739 .to_str()
740 .ok_or_else(|| Error::InvalidPath(footer_path.to_owned()))?
741 .to_string();
742
743 let mut composite_proto = CompositeDisk::new();
744 composite_proto.version = COMPOSITE_DISK_VERSION;
745 composite_proto.component_disks.push(ComponentDisk {
746 file_path: header_path,
747 offset: 0,
748 read_write_capability: ReadWriteCapability::READ_ONLY.into(),
749 ..ComponentDisk::new()
750 });
751
752 let mut partitions_buffer =
755 [0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
756 let mut writer: &mut [u8] = &mut partitions_buffer;
757 let mut next_disk_offset = GPT_BEGINNING_SIZE;
758 let mut labels = HashSet::with_capacity(partitions.len());
759 for partition in partitions {
760 let gpt_entry = create_gpt_entry(partition, next_disk_offset);
761 if !labels.insert(gpt_entry.partition_name) {
762 return Err(Error::DuplicatePartitionLabel(partition.label.clone()));
763 }
764 gpt_entry.write_bytes(&mut writer)?;
765
766 for component_disk in
767 create_component_disks(partition, next_disk_offset, &zero_filler_path)?
768 {
769 composite_proto.component_disks.push(component_disk);
770 }
771
772 next_disk_offset += partition.aligned_size();
773 }
774 const FOOTER_PADDING: u64 =
778 GPT_END_SIZE.next_multiple_of(1 << PARTITION_SIZE_SHIFT) - GPT_END_SIZE;
779 let footer_file_offset = next_disk_offset;
780 let secondary_table_offset = footer_file_offset + FOOTER_PADDING;
781 let disk_size = secondary_table_offset + GPT_END_SIZE;
782 composite_proto.component_disks.push(ComponentDisk {
783 file_path: footer_path,
784 offset: footer_file_offset,
785 read_write_capability: ReadWriteCapability::READ_ONLY.into(),
786 ..ComponentDisk::new()
787 });
788
789 let mut hasher = Hasher::new();
791 hasher.update(&partitions_buffer);
792 let partition_entries_crc32 = hasher.finalize();
793
794 let disk_guid = Uuid::new_v4();
795 write_beginning(
796 header_file,
797 disk_guid,
798 &partitions_buffer,
799 partition_entries_crc32,
800 secondary_table_offset,
801 disk_size,
802 )?;
803
804 footer_file
805 .write_all(&[0; FOOTER_PADDING as usize])
806 .map_err(Error::WriteHeader)?;
807 write_end(
808 footer_file,
809 disk_guid,
810 &partitions_buffer,
811 partition_entries_crc32,
812 secondary_table_offset,
813 )?;
814
815 composite_proto.length = disk_size;
816 output_composite
817 .write_all(CDISK_MAGIC.as_bytes())
818 .map_err(Error::WriteHeader)?;
819 composite_proto
820 .write_to_writer(output_composite)
821 .map_err(Error::WriteProto)?;
822
823 Ok(())
824}
825
826pub fn create_zero_filler<P: AsRef<Path>>(zero_filler_path: P) -> Result<()> {
829 let f = OpenOptions::new()
830 .create(true)
831 .read(true)
832 .write(true)
833 .truncate(true)
834 .open(zero_filler_path.as_ref())
835 .map_err(Error::WriteZeroFiller)?;
836 f.set_len(1 << PARTITION_SIZE_SHIFT)
837 .map_err(Error::WriteZeroFiller)
838}
839
840#[cfg(test)]
841mod tests {
842 use std::fs::OpenOptions;
843 use std::io::Write;
844 use std::matches;
845
846 use base::AsRawDescriptor;
847 use tempfile::tempfile;
848
849 use super::*;
850
851 fn new_from_components(disks: Vec<ComponentDiskPart>) -> Result<CompositeDiskFile> {
852 CompositeDiskFile::new(disks, tempfile().unwrap())
853 }
854
855 #[test]
856 fn block_duplicate_offset_disks() {
857 let file1 = tempfile().unwrap();
858 let file2 = tempfile().unwrap();
859 let disk_part1 = ComponentDiskPart {
860 file: Box::new(file1),
861 offset: 0,
862 length: 100,
863 needs_flush: AtomicBool::new(false),
864 };
865 let disk_part2 = ComponentDiskPart {
866 file: Box::new(file2),
867 offset: 0,
868 length: 100,
869 needs_flush: AtomicBool::new(false),
870 };
871 assert!(new_from_components(vec![disk_part1, disk_part2]).is_err());
872 }
873
874 #[test]
875 fn get_len() {
876 let file1 = tempfile().unwrap();
877 let file2 = tempfile().unwrap();
878 let disk_part1 = ComponentDiskPart {
879 file: Box::new(file1),
880 offset: 0,
881 length: 100,
882 needs_flush: AtomicBool::new(false),
883 };
884 let disk_part2 = ComponentDiskPart {
885 file: Box::new(file2),
886 offset: 100,
887 length: 100,
888 needs_flush: AtomicBool::new(false),
889 };
890 let composite = new_from_components(vec![disk_part1, disk_part2]).unwrap();
891 let len = composite.get_len().unwrap();
892 assert_eq!(len, 200);
893 }
894
895 #[test]
896 fn async_get_len() {
897 let file1 = tempfile().unwrap();
898 let file2 = tempfile().unwrap();
899 let disk_part1 = ComponentDiskPart {
900 file: Box::new(file1),
901 offset: 0,
902 length: 100,
903 needs_flush: AtomicBool::new(false),
904 };
905 let disk_part2 = ComponentDiskPart {
906 file: Box::new(file2),
907 offset: 100,
908 length: 100,
909 needs_flush: AtomicBool::new(false),
910 };
911 let composite = new_from_components(vec![disk_part1, disk_part2]).unwrap();
912
913 let ex = Executor::new().unwrap();
914 let composite = Box::new(composite).to_async_disk(&ex).unwrap();
915 let len = composite.get_len().unwrap();
916 assert_eq!(len, 200);
917 }
918
919 #[test]
920 fn single_file_passthrough() {
921 let file = tempfile().unwrap();
922 let disk_part = ComponentDiskPart {
923 file: Box::new(file),
924 offset: 0,
925 length: 100,
926 needs_flush: AtomicBool::new(false),
927 };
928 let composite = new_from_components(vec![disk_part]).unwrap();
929 let mut input_memory = [55u8; 5];
930 let input_volatile_memory = VolatileSlice::new(&mut input_memory[..]);
931 composite
932 .write_all_at_volatile(input_volatile_memory, 0)
933 .unwrap();
934 let mut output_memory = [0u8; 5];
935 let output_volatile_memory = VolatileSlice::new(&mut output_memory[..]);
936 composite
937 .read_exact_at_volatile(output_volatile_memory, 0)
938 .unwrap();
939 assert_eq!(input_memory, output_memory);
940 }
941
942 #[test]
943 fn async_single_file_passthrough() {
944 let file = tempfile().unwrap();
945 let disk_part = ComponentDiskPart {
946 file: Box::new(file),
947 offset: 0,
948 length: 100,
949 needs_flush: AtomicBool::new(false),
950 };
951 let composite = new_from_components(vec![disk_part]).unwrap();
952 let ex = Executor::new().unwrap();
953 ex.run_until(async {
954 let composite = Box::new(composite).to_async_disk(&ex).unwrap();
955 let expected = [55u8; 5];
956 assert_eq!(
957 composite.write_double_buffered(0, &expected).await.unwrap(),
958 5
959 );
960 let mut buf = [0u8; 5];
961 assert_eq!(
962 composite
963 .read_double_buffered(0, &mut buf[..])
964 .await
965 .unwrap(),
966 5
967 );
968 assert_eq!(buf, expected);
969 })
970 .unwrap();
971 }
972
973 #[test]
974 fn triple_file_descriptors() {
975 let file1 = tempfile().unwrap();
976 let file2 = tempfile().unwrap();
977 let file3 = tempfile().unwrap();
978 let mut in_descriptors = vec![
979 file1.as_raw_descriptor(),
980 file2.as_raw_descriptor(),
981 file3.as_raw_descriptor(),
982 ];
983 in_descriptors.sort_unstable();
984 let disk_part1 = ComponentDiskPart {
985 file: Box::new(file1),
986 offset: 0,
987 length: 100,
988 needs_flush: AtomicBool::new(false),
989 };
990 let disk_part2 = ComponentDiskPart {
991 file: Box::new(file2),
992 offset: 100,
993 length: 100,
994 needs_flush: AtomicBool::new(false),
995 };
996 let disk_part3 = ComponentDiskPart {
997 file: Box::new(file3),
998 offset: 200,
999 length: 100,
1000 needs_flush: AtomicBool::new(false),
1001 };
1002 let composite = new_from_components(vec![disk_part1, disk_part2, disk_part3]).unwrap();
1003 let mut out_descriptors = composite.as_raw_descriptors();
1004 out_descriptors.sort_unstable();
1005 assert_eq!(in_descriptors, out_descriptors);
1006 }
1007
1008 #[test]
1009 fn triple_file_passthrough() {
1010 let file1 = tempfile().unwrap();
1011 let file2 = tempfile().unwrap();
1012 let file3 = tempfile().unwrap();
1013 let disk_part1 = ComponentDiskPart {
1014 file: Box::new(file1),
1015 offset: 0,
1016 length: 100,
1017 needs_flush: AtomicBool::new(false),
1018 };
1019 let disk_part2 = ComponentDiskPart {
1020 file: Box::new(file2),
1021 offset: 100,
1022 length: 100,
1023 needs_flush: AtomicBool::new(false),
1024 };
1025 let disk_part3 = ComponentDiskPart {
1026 file: Box::new(file3),
1027 offset: 200,
1028 length: 100,
1029 needs_flush: AtomicBool::new(false),
1030 };
1031 let composite = new_from_components(vec![disk_part1, disk_part2, disk_part3]).unwrap();
1032 let mut input_memory = [55u8; 200];
1033 let input_volatile_memory = VolatileSlice::new(&mut input_memory[..]);
1034 composite
1035 .write_all_at_volatile(input_volatile_memory, 50)
1036 .unwrap();
1037 let mut output_memory = [0u8; 200];
1038 let output_volatile_memory = VolatileSlice::new(&mut output_memory[..]);
1039 composite
1040 .read_exact_at_volatile(output_volatile_memory, 50)
1041 .unwrap();
1042 assert!(input_memory.iter().eq(output_memory.iter()));
1043 }
1044
1045 #[test]
1046 fn async_triple_file_passthrough() {
1047 let file1 = tempfile().unwrap();
1048 let file2 = tempfile().unwrap();
1049 let file3 = tempfile().unwrap();
1050 let disk_part1 = ComponentDiskPart {
1051 file: Box::new(file1),
1052 offset: 0,
1053 length: 100,
1054 needs_flush: AtomicBool::new(false),
1055 };
1056 let disk_part2 = ComponentDiskPart {
1057 file: Box::new(file2),
1058 offset: 100,
1059 length: 100,
1060 needs_flush: AtomicBool::new(false),
1061 };
1062 let disk_part3 = ComponentDiskPart {
1063 file: Box::new(file3),
1064 offset: 200,
1065 length: 100,
1066 needs_flush: AtomicBool::new(false),
1067 };
1068 let composite = new_from_components(vec![disk_part1, disk_part2, disk_part3]).unwrap();
1069 let ex = Executor::new().unwrap();
1070 ex.run_until(async {
1071 let composite = Box::new(composite).to_async_disk(&ex).unwrap();
1072
1073 let expected = [55u8; 200];
1074 assert_eq!(
1075 composite.write_double_buffered(0, &expected).await.unwrap(),
1076 100
1077 );
1078 assert_eq!(
1079 composite
1080 .write_double_buffered(100, &expected[100..])
1081 .await
1082 .unwrap(),
1083 100
1084 );
1085
1086 let mut buf = [0u8; 200];
1087 assert_eq!(
1088 composite
1089 .read_double_buffered(0, &mut buf[..])
1090 .await
1091 .unwrap(),
1092 100
1093 );
1094 assert_eq!(
1095 composite
1096 .read_double_buffered(100, &mut buf[100..])
1097 .await
1098 .unwrap(),
1099 100
1100 );
1101 assert_eq!(buf, expected);
1102 })
1103 .unwrap();
1104 }
1105
1106 #[test]
1107 fn async_triple_file_punch_hole() {
1108 let file1 = tempfile().unwrap();
1109 let file2 = tempfile().unwrap();
1110 let file3 = tempfile().unwrap();
1111 let disk_part1 = ComponentDiskPart {
1112 file: Box::new(file1),
1113 offset: 0,
1114 length: 100,
1115 needs_flush: AtomicBool::new(false),
1116 };
1117 let disk_part2 = ComponentDiskPart {
1118 file: Box::new(file2),
1119 offset: 100,
1120 length: 100,
1121 needs_flush: AtomicBool::new(false),
1122 };
1123 let disk_part3 = ComponentDiskPart {
1124 file: Box::new(file3),
1125 offset: 200,
1126 length: 100,
1127 needs_flush: AtomicBool::new(false),
1128 };
1129 let composite = new_from_components(vec![disk_part1, disk_part2, disk_part3]).unwrap();
1130 let ex = Executor::new().unwrap();
1131 ex.run_until(async {
1132 let composite = Box::new(composite).to_async_disk(&ex).unwrap();
1133
1134 let input = [55u8; 300];
1135 assert_eq!(
1136 composite.write_double_buffered(0, &input).await.unwrap(),
1137 100
1138 );
1139 assert_eq!(
1140 composite
1141 .write_double_buffered(100, &input[100..])
1142 .await
1143 .unwrap(),
1144 100
1145 );
1146 assert_eq!(
1147 composite
1148 .write_double_buffered(200, &input[200..])
1149 .await
1150 .unwrap(),
1151 100
1152 );
1153
1154 composite.punch_hole(50, 200).await.unwrap();
1155
1156 let mut buf = [0u8; 300];
1157 assert_eq!(
1158 composite
1159 .read_double_buffered(0, &mut buf[..])
1160 .await
1161 .unwrap(),
1162 100
1163 );
1164 assert_eq!(
1165 composite
1166 .read_double_buffered(100, &mut buf[100..])
1167 .await
1168 .unwrap(),
1169 100
1170 );
1171 assert_eq!(
1172 composite
1173 .read_double_buffered(200, &mut buf[200..])
1174 .await
1175 .unwrap(),
1176 100
1177 );
1178
1179 let mut expected = input;
1180 expected[50..250].iter_mut().for_each(|x| *x = 0);
1181 assert_eq!(buf, expected);
1182 })
1183 .unwrap();
1184 }
1185
1186 #[test]
1187 fn async_triple_file_write_zeroes() {
1188 let file1 = tempfile().unwrap();
1189 let file2 = tempfile().unwrap();
1190 let file3 = tempfile().unwrap();
1191 let disk_part1 = ComponentDiskPart {
1192 file: Box::new(file1),
1193 offset: 0,
1194 length: 100,
1195 needs_flush: AtomicBool::new(false),
1196 };
1197 let disk_part2 = ComponentDiskPart {
1198 file: Box::new(file2),
1199 offset: 100,
1200 length: 100,
1201 needs_flush: AtomicBool::new(false),
1202 };
1203 let disk_part3 = ComponentDiskPart {
1204 file: Box::new(file3),
1205 offset: 200,
1206 length: 100,
1207 needs_flush: AtomicBool::new(false),
1208 };
1209 let composite = new_from_components(vec![disk_part1, disk_part2, disk_part3]).unwrap();
1210 let ex = Executor::new().unwrap();
1211 ex.run_until(async {
1212 let composite = Box::new(composite).to_async_disk(&ex).unwrap();
1213
1214 let input = [55u8; 300];
1215 assert_eq!(
1216 composite.write_double_buffered(0, &input).await.unwrap(),
1217 100
1218 );
1219 assert_eq!(
1220 composite
1221 .write_double_buffered(100, &input[100..])
1222 .await
1223 .unwrap(),
1224 100
1225 );
1226 assert_eq!(
1227 composite
1228 .write_double_buffered(200, &input[200..])
1229 .await
1230 .unwrap(),
1231 100
1232 );
1233
1234 composite.write_zeroes_at(50, 200).await.unwrap();
1235
1236 let mut buf = [0u8; 300];
1237 assert_eq!(
1238 composite
1239 .read_double_buffered(0, &mut buf[..])
1240 .await
1241 .unwrap(),
1242 100
1243 );
1244 assert_eq!(
1245 composite
1246 .read_double_buffered(100, &mut buf[100..])
1247 .await
1248 .unwrap(),
1249 100
1250 );
1251 assert_eq!(
1252 composite
1253 .read_double_buffered(200, &mut buf[200..])
1254 .await
1255 .unwrap(),
1256 100
1257 );
1258
1259 let mut expected = input;
1260 expected[50..250].iter_mut().for_each(|x| *x = 0);
1261 assert_eq!(buf, expected);
1262 })
1263 .unwrap();
1264 }
1265
1266 #[test]
1269 fn async_fsync_skips_unchanged_parts() {
1270 let mut rw_file = tempfile().unwrap();
1271 rw_file.write_all(&[0u8; 100]).unwrap();
1272 rw_file.seek(SeekFrom::Start(0)).unwrap();
1273 let mut ro_disk_image = tempfile::NamedTempFile::new().unwrap();
1274 ro_disk_image.write_all(&[0u8; 100]).unwrap();
1275 let ro_file = OpenOptions::new()
1276 .read(true)
1277 .open(ro_disk_image.path())
1278 .unwrap();
1279
1280 let rw_part = ComponentDiskPart {
1281 file: Box::new(rw_file),
1282 offset: 0,
1283 length: 100,
1284 needs_flush: AtomicBool::new(false),
1285 };
1286 let ro_part = ComponentDiskPart {
1287 file: Box::new(ro_file),
1288 offset: 100,
1289 length: 100,
1290 needs_flush: AtomicBool::new(false),
1291 };
1292 let composite = new_from_components(vec![rw_part, ro_part]).unwrap();
1293 let ex = Executor::new().unwrap();
1294 ex.run_until(async {
1295 let composite = Box::new(composite).to_async_disk(&ex).unwrap();
1296
1297 composite.write_zeroes_at(0, 20).await.unwrap();
1299
1300 composite.fsync().await.expect(
1303 "Failed to fsync composite disk. \
1304 This can happen if the disk writable state is wrong.",
1305 );
1306 })
1307 .unwrap();
1308 }
1309
1310 #[test]
1311 fn beginning_size() {
1312 let mut buffer = vec![];
1313 let partitions = [0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
1314 let disk_size = 1000 * SECTOR_SIZE;
1315 write_beginning(
1316 &mut buffer,
1317 Uuid::from_u128(0x12345678_1234_5678_abcd_12345678abcd),
1318 &partitions,
1319 42,
1320 disk_size - GPT_END_SIZE,
1321 disk_size,
1322 )
1323 .unwrap();
1324
1325 assert_eq!(buffer.len(), GPT_BEGINNING_SIZE as usize);
1326 }
1327
1328 #[test]
1329 fn end_size() {
1330 let mut buffer = vec![];
1331 let partitions = [0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
1332 let disk_size = 1000 * SECTOR_SIZE;
1333 write_end(
1334 &mut buffer,
1335 Uuid::from_u128(0x12345678_1234_5678_abcd_12345678abcd),
1336 &partitions,
1337 42,
1338 disk_size - GPT_END_SIZE,
1339 )
1340 .unwrap();
1341
1342 assert_eq!(buffer.len(), GPT_END_SIZE as usize);
1343 }
1344
1345 #[test]
1347 fn create_composite_disk_empty() {
1348 let mut header_image = tempfile().unwrap();
1349 let mut footer_image = tempfile().unwrap();
1350 let mut composite_image = tempfile().unwrap();
1351
1352 create_composite_disk(
1353 &[],
1354 Path::new("/zero_filler.img"),
1355 Path::new("/header_path.img"),
1356 &mut header_image,
1357 Path::new("/footer_path.img"),
1358 &mut footer_image,
1359 &mut composite_image,
1360 )
1361 .unwrap();
1362 }
1363
1364 #[test]
1366 #[allow(clippy::unnecessary_to_owned)] fn create_composite_disk_success() {
1368 fn tmpfile(prefix: &str) -> tempfile::NamedTempFile {
1369 tempfile::Builder::new().prefix(prefix).tempfile().unwrap()
1370 }
1371
1372 let mut header_image = tmpfile("header");
1373 let mut footer_image = tmpfile("footer");
1374 let mut composite_image = tmpfile("composite");
1375
1376 let partition1 = tmpfile("partition1");
1378 let partition2 = tmpfile("partition1");
1379 let zero_filler = tmpfile("zero");
1380
1381 create_composite_disk(
1382 &[
1383 PartitionInfo {
1384 label: "partition1".to_string(),
1385 path: partition1.path().to_path_buf(),
1386 partition_type: ImagePartitionType::LinuxFilesystem,
1387 writable: false,
1388 size: 4000,
1390 part_guid: None,
1391 },
1392 PartitionInfo {
1393 label: "partition2".to_string(),
1394 path: partition2.path().to_path_buf(),
1395 partition_type: ImagePartitionType::LinuxFilesystem,
1396 writable: true,
1397 size: 4096,
1399 part_guid: Some(Uuid::from_u128(0x4049C8DC_6C2B_C740_A95A_BDAA629D4378)),
1400 },
1401 ],
1402 zero_filler.path(),
1403 &header_image.path().to_path_buf(),
1404 header_image.as_file_mut(),
1405 &footer_image.path().to_path_buf(),
1406 footer_image.as_file_mut(),
1407 composite_image.as_file_mut(),
1408 )
1409 .unwrap();
1410
1411 composite_image.rewind().unwrap();
1413 let mut magic_space = [0u8; CDISK_MAGIC.len()];
1414 composite_image.read_exact(&mut magic_space[..]).unwrap();
1415 assert_eq!(magic_space, CDISK_MAGIC.as_bytes());
1416 let proto = CompositeDisk::parse_from_reader(&mut composite_image).unwrap();
1418 assert_eq!(
1419 proto,
1420 CompositeDisk {
1421 version: 2,
1422 component_disks: vec![
1423 ComponentDisk {
1424 file_path: header_image.path().to_str().unwrap().to_string(),
1425 offset: 0,
1426 read_write_capability: ReadWriteCapability::READ_ONLY.into(),
1427 ..ComponentDisk::new()
1428 },
1429 ComponentDisk {
1430 file_path: partition1.path().to_str().unwrap().to_string(),
1431 offset: 0x5000, read_write_capability: ReadWriteCapability::READ_ONLY.into(),
1433 ..ComponentDisk::new()
1434 },
1435 ComponentDisk {
1436 file_path: zero_filler.path().to_str().unwrap().to_string(),
1437 offset: 0x5fa0, read_write_capability: ReadWriteCapability::READ_ONLY.into(),
1439 ..ComponentDisk::new()
1440 },
1441 ComponentDisk {
1442 file_path: partition2.path().to_str().unwrap().to_string(),
1443 offset: 0x6000, read_write_capability: ReadWriteCapability::READ_WRITE.into(),
1445 ..ComponentDisk::new()
1446 },
1447 ComponentDisk {
1448 file_path: footer_image.path().to_str().unwrap().to_string(),
1449 offset: 0x7000, read_write_capability: ReadWriteCapability::READ_ONLY.into(),
1451 ..ComponentDisk::new()
1452 },
1453 ],
1454 length: 0xc000,
1455 ..CompositeDisk::new()
1456 }
1457 );
1458
1459 let ex = Executor::new().unwrap();
1461 ex.run_until(async {
1462 let disk = Box::new(
1463 CompositeDiskFile::from_file(
1464 composite_image.into_file(),
1465 DiskFileParams {
1466 path: "/foo".into(),
1467 is_read_only: true,
1468 is_sparse_file: false,
1469 is_overlapped: false,
1470 is_direct: false,
1471 lock: false,
1472 depth: 0,
1473 },
1474 )
1475 .unwrap(),
1476 )
1477 .to_async_disk(&ex)
1478 .unwrap();
1479
1480 let header_offset = SECTOR_SIZE;
1481 let footer_offset = disk.get_len().unwrap() - SECTOR_SIZE;
1482
1483 let mut header_bytes = [0u8; SECTOR_SIZE as usize];
1484 assert_eq!(
1485 disk.read_double_buffered(header_offset, &mut header_bytes[..])
1486 .await
1487 .unwrap(),
1488 SECTOR_SIZE as usize
1489 );
1490
1491 let mut footer_bytes = [0u8; SECTOR_SIZE as usize];
1492 assert_eq!(
1493 disk.read_double_buffered(footer_offset, &mut footer_bytes[..])
1494 .await
1495 .unwrap(),
1496 SECTOR_SIZE as usize
1497 );
1498
1499 let header_current_lba = u64::from_le_bytes(header_bytes[24..32].try_into().unwrap());
1501 assert_eq!(header_current_lba * SECTOR_SIZE, header_offset);
1502 let header_backup_lba = u64::from_le_bytes(header_bytes[32..40].try_into().unwrap());
1503 assert_eq!(header_backup_lba * SECTOR_SIZE, footer_offset);
1504
1505 let footer_current_lba = u64::from_le_bytes(footer_bytes[24..32].try_into().unwrap());
1506 assert_eq!(footer_current_lba * SECTOR_SIZE, footer_offset);
1507 let footer_backup_lba = u64::from_le_bytes(footer_bytes[32..40].try_into().unwrap());
1508 assert_eq!(footer_backup_lba * SECTOR_SIZE, header_offset);
1509
1510 header_bytes[16..20].fill(0);
1512 header_bytes[24..40].fill(0);
1513 footer_bytes[16..20].fill(0);
1514 footer_bytes[24..40].fill(0);
1515 assert_eq!(header_bytes, footer_bytes);
1516 })
1517 .unwrap();
1518 }
1519
1520 #[test]
1522 fn create_composite_disk_duplicate_label() {
1523 let mut header_image = tempfile().unwrap();
1524 let mut footer_image = tempfile().unwrap();
1525 let mut composite_image = tempfile().unwrap();
1526
1527 let result = create_composite_disk(
1528 &[
1529 PartitionInfo {
1530 label: "label".to_string(),
1531 path: "/partition1.img".to_string().into(),
1532 partition_type: ImagePartitionType::LinuxFilesystem,
1533 writable: false,
1534 size: 0,
1535 part_guid: None,
1536 },
1537 PartitionInfo {
1538 label: "label".to_string(),
1539 path: "/partition2.img".to_string().into(),
1540 partition_type: ImagePartitionType::LinuxFilesystem,
1541 writable: true,
1542 size: 0,
1543 part_guid: None,
1544 },
1545 ],
1546 Path::new("/zero_filler.img"),
1547 Path::new("/header_path.img"),
1548 &mut header_image,
1549 Path::new("/footer_path.img"),
1550 &mut footer_image,
1551 &mut composite_image,
1552 );
1553 assert!(matches!(result, Err(Error::DuplicatePartitionLabel(label)) if label == "label"));
1554 }
1555}