disk/
composite.rs

1// Copyright 2019 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
62/// The amount of padding needed between the last partition entry and the first partition, to align
63/// the partition appropriately. The two sectors are for the MBR and the GPT header.
64const 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;
68// Keep all partitions 4k aligned for performance.
69const PARTITION_SIZE_SHIFT: u8 = 12;
70
71// From https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs.
72const 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    // Whether there have been any writes since the last fsync or fdatasync.
126    needs_flush: AtomicBool,
127}
128
129impl ComponentDiskPart {
130    fn range(&self) -> Range<u64> {
131        self.offset..(self.offset + self.length)
132    }
133}
134
135/// Represents a composite virtual disk made out of multiple component files. This is described on
136/// disk by a protocol buffer file that lists out the component file locations and their offsets
137/// and lengths on the virtual disk. The spaces covered by the component disks must be contiguous
138/// and not overlapping.
139#[derive(Debug)]
140pub struct CompositeDiskFile {
141    component_disks: Vec<ComponentDiskPart>,
142    // We keep the root composite file open so that the file lock is not dropped.
143    _disk_spec_file: File,
144}
145
146// TODO(b/271381851): implement `try_clone`. It allows virtio-blk to run multiple workers.
147impl 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
165/// The version of the composite disk format supported by this implementation.
166const COMPOSITE_DISK_VERSION: u64 = 2;
167
168/// A magic string placed at the beginning of a composite disk file to identify it.
169pub 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    /// Set up a composite disk by reading the specification from a file. The file must consist of
189    /// the CDISK_MAGIC string followed by one binary instance of the CompositeDisk protocol
190    /// buffer. Returns an error if it could not read the file or if the specification was invalid.
191    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                // Note that a read-only parts of a composite disk should NOT be marked sparse,
220                // as the action of marking them sparse is a write. This may seem a little hacky,
221                // and it is; however:
222                //    (a)  there is not a good way to pass sparseness parameters per composite disk
223                //         part (the proto does not have fields for it).
224                //    (b)  this override of sorts always matches the correct user intent.
225                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                        // TODO: Should pass `params.is_overlapped` through here. Needs testing.
231                        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, // Assigned later
239                    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
307// Implements Read and Write targeting volatile storage for composite disks.
308//
309// Note that reads and writes will return early if crossing component disk boundaries.
310// This is allowed by the read and write specifications, which only say read and write
311// have to return how many bytes were actually read or written. Use read_exact_volatile
312// or write_all_volatile to make sure all bytes are received/transmitted.
313//
314// If one of the component disks does a partial read or write, that also gets passed
315// transparently to the parent.
316impl 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        // NOTE: The fsync implementation isn't really async, so no point in adding concurrency
470        // here unless we introduce a blocking threadpool.
471        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        // NOTE: The fdatasync implementation isn't really async, so no point in adding concurrency
484        // here unless we introduce a blocking threadpool.
485        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/// Information about a partition to create.
573#[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/// The type of partition.
590#[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
605/// Write protective MBR and primary GPT table.
606fn 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 the protective MBR to the first sector.
615    write_protective_mbr(file, disk_size)?;
616
617    // Write the GPT header, and pad out to the end of the sector.
618    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    // Write partition entries, including unused ones.
629    file.write_all(partitions).map_err(Error::WriteHeader)?;
630
631    // Write zeroes to align the first partition appropriately.
632    file.write_all(&[0; PARTITION_ALIGNMENT_SIZE])
633        .map_err(Error::WriteHeader)?;
634
635    Ok(())
636}
637
638/// Write secondary GPT table.
639fn 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    // Write partition entries, including unused ones.
647    file.write_all(partitions).map_err(Error::WriteHeader)?;
648
649    // Write the GPT header, and pad out to the end of the sector.
650    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
663/// Create the `GptPartitionEntry` for the given partition.
664fn 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
678/// Create one or more `ComponentDisk` proto messages for the given partition.
679fn 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            // Fill in the gap by reusing the zero filler file, because we know it is always bigger
706            // than the alignment size. Its size is 1 << PARTITION_SIZE_SHIFT (4k).
707            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
719/// Create a new composite disk image containing the given partitions, and write it out to the given
720/// files.
721pub 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    // Write partitions to a temporary buffer so that we can calculate the CRC, and construct the
753    // ComponentDisk proto messages at the same time.
754    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    // The secondary GPT needs to be at the very end of the file, but its size (0x4200) is not
775    // aligned to the chosen partition size (0x1000). We compensate for that by writing some
776    // padding to the start of the footer file.
777    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    // Calculate CRC32 of partition entries.
790    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
826/// Create a zero filler file which can be used to fill the gaps between partition files.
827/// The filler is sized to be big enough to fill the gaps. (1 << PARTITION_SIZE_SHIFT)
828pub 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    // TODO: fsync on a RO file is legal, this test doesn't work as expected. Consider using a mock
1267    // DiskFile to detect the fsync calls.
1268    #[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            // Write to the RW part so that some fsync operation will occur.
1298            composite.write_zeroes_at(0, 20).await.unwrap();
1299
1300            // This is the test's assert. fsyncing should NOT touch a read-only disk part. On
1301            // Windows, this would be an error.
1302            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    /// Creates a composite disk image with no partitions.
1346    #[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    /// Creates a composite disk image with two partitions.
1365    #[test]
1366    #[allow(clippy::unnecessary_to_owned)] // false positives
1367    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        // The test doesn't read these, just needs to be able to open them.
1377        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                    // Needs small amount of padding.
1389                    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                    // Needs no padding.
1398                    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        // Check magic.
1412        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        // Check proto.
1417        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, // GPT_BEGINNING_SIZE,
1432                        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, // GPT_BEGINNING_SIZE + 4000,
1438                        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, // GPT_BEGINNING_SIZE + 4096,
1444                        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, // GPT_BEGINNING_SIZE + 4096 + 4096,
1450                        read_write_capability: ReadWriteCapability::READ_ONLY.into(),
1451                        ..ComponentDisk::new()
1452                    },
1453                ],
1454                length: 0xc000,
1455                ..CompositeDisk::new()
1456            }
1457        );
1458
1459        // Open the file as a composite disk and do some basic GPT header/footer validation.
1460        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            // Check the header and footer fields point to each other correctly.
1500            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 and footer should be equal if we zero the pointers and CRCs.
1511            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    /// Attempts to create a composite disk image with two partitions with the same label.
1521    #[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}