• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2019 The Chromium OS Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use std::cmp::min;
6 use std::fmt::{self, Debug, Display};
7 use std::fs::File;
8 use std::io::{self, Read, Seek, SeekFrom, Write};
9 use std::sync::Arc;
10 
11 use async_trait::async_trait;
12 use base::{
13     AsRawDescriptors, FileAllocate, FileReadWriteAtVolatile, FileSetLen, FileSync, PunchHole,
14     SeekHole, WriteZeroesAt,
15 };
16 use cros_async::Executor;
17 use libc::EINVAL;
18 use remain::sorted;
19 use vm_memory::GuestMemory;
20 
21 mod qcow;
22 pub use qcow::{QcowFile, QCOW_MAGIC};
23 
24 #[cfg(feature = "composite-disk")]
25 mod composite;
26 #[cfg(feature = "composite-disk")]
27 use composite::{CompositeDiskFile, CDISK_MAGIC, CDISK_MAGIC_LEN};
28 
29 mod android_sparse;
30 use android_sparse::{AndroidSparse, SPARSE_HEADER_MAGIC};
31 
32 #[sorted]
33 #[derive(Debug)]
34 pub enum Error {
35     BlockDeviceNew(base::Error),
36     ConversionNotSupported,
37     CreateAndroidSparseDisk(android_sparse::Error),
38     #[cfg(feature = "composite-disk")]
39     CreateCompositeDisk(composite::Error),
40     CreateSingleFileDisk(cros_async::AsyncError),
41     Fallocate(cros_async::AsyncError),
42     Fsync(cros_async::AsyncError),
43     QcowError(qcow::Error),
44     ReadingData(io::Error),
45     ReadingHeader(io::Error),
46     ReadToMem(cros_async::AsyncError),
47     SeekingFile(io::Error),
48     SettingFileSize(io::Error),
49     UnknownType,
50     WriteFromMem(cros_async::AsyncError),
51     WriteFromVec(cros_async::AsyncError),
52     WritingData(io::Error),
53 }
54 
55 pub type Result<T> = std::result::Result<T, Error>;
56 
57 /// A trait for getting the length of a disk image or raw block device.
58 pub trait DiskGetLen {
59     /// Get the current length of the disk in bytes.
get_len(&self) -> io::Result<u64>60     fn get_len(&self) -> io::Result<u64>;
61 }
62 
63 impl DiskGetLen for File {
get_len(&self) -> io::Result<u64>64     fn get_len(&self) -> io::Result<u64> {
65         let mut s = self;
66         let orig_seek = s.seek(SeekFrom::Current(0))?;
67         let end = s.seek(SeekFrom::End(0))? as u64;
68         s.seek(SeekFrom::Start(orig_seek))?;
69         Ok(end)
70     }
71 }
72 
73 /// The prerequisites necessary to support a block device.
74 #[rustfmt::skip] // rustfmt won't wrap the long list of trait bounds.
75 pub trait DiskFile:
76     FileSetLen
77     + DiskGetLen
78     + FileSync
79     + FileReadWriteAtVolatile
80     + PunchHole
81     + WriteZeroesAt
82     + FileAllocate
83     + Send
84     + AsRawDescriptors
85     + Debug
86 {
87 }
88 impl<
89         D: FileSetLen
90             + DiskGetLen
91             + FileSync
92             + PunchHole
93             + FileReadWriteAtVolatile
94             + WriteZeroesAt
95             + FileAllocate
96             + Send
97             + AsRawDescriptors
98             + Debug,
99     > DiskFile for D
100 {
101 }
102 
103 /// A `DiskFile` that can be converted for asychronous access.
104 pub trait ToAsyncDisk: DiskFile {
105     /// Convert a boxed self in to a box-wrapped implementaiton of AsyncDisk.
106     /// Used to convert a standard disk image to an async disk image. This conversion and the
107     /// inverse are needed so that the `Send` DiskImage can be given to the block thread where it is
108     /// converted to a non-`Send` AsyncDisk. The AsyncDisk can then be converted back and returned
109     /// to the main device thread if the block device is destroyed or reset.
to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>110     fn to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>;
111 }
112 
113 impl ToAsyncDisk for File {
to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>114     fn to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>> {
115         Ok(Box::new(SingleFileDisk::new(*self, ex)?))
116     }
117 }
118 
119 impl Display for Error {
120     #[remain::check]
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result121     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122         use self::Error::*;
123 
124         #[sorted]
125         match self {
126             BlockDeviceNew(e) => write!(f, "failed to create block device: {}", e),
127             ConversionNotSupported => write!(f, "requested file conversion not supported"),
128             CreateAndroidSparseDisk(e) => write!(f, "failure in android sparse disk: {}", e),
129             #[cfg(feature = "composite-disk")]
130             CreateCompositeDisk(e) => write!(f, "failure in composite disk: {}", e),
131             CreateSingleFileDisk(e) => write!(f, "failure creating single file disk: {}", e),
132             Fallocate(e) => write!(f, "failure with fallocate: {}", e),
133             Fsync(e) => write!(f, "failure with fsync: {}", e),
134             QcowError(e) => write!(f, "failure in qcow: {}", e),
135             ReadingData(e) => write!(f, "failed to read data: {}", e),
136             ReadingHeader(e) => write!(f, "failed to read header: {}", e),
137             ReadToMem(e) => write!(f, "failed to read to memory: {}", e),
138             SeekingFile(e) => write!(f, "failed to seek file: {}", e),
139             SettingFileSize(e) => write!(f, "failed to set file size: {}", e),
140             UnknownType => write!(f, "unknown disk type"),
141             WriteFromMem(e) => write!(f, "failed to write from memory: {}", e),
142             WriteFromVec(e) => write!(f, "failed to write from vec: {}", e),
143             WritingData(e) => write!(f, "failed to write data: {}", e),
144         }
145     }
146 }
147 
148 /// The variants of image files on the host that can be used as virtual disks.
149 #[derive(Debug, PartialEq, Eq)]
150 pub enum ImageType {
151     Raw,
152     Qcow2,
153     CompositeDisk,
154     AndroidSparse,
155 }
156 
convert_copy<R, W>(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> Result<()> where R: Read + Seek, W: Write + Seek,157 fn convert_copy<R, W>(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> Result<()>
158 where
159     R: Read + Seek,
160     W: Write + Seek,
161 {
162     const CHUNK_SIZE: usize = 65536;
163     let mut buf = [0; CHUNK_SIZE];
164     let mut read_count = 0;
165     reader
166         .seek(SeekFrom::Start(offset))
167         .map_err(Error::SeekingFile)?;
168     writer
169         .seek(SeekFrom::Start(offset))
170         .map_err(Error::SeekingFile)?;
171     loop {
172         let this_count = min(CHUNK_SIZE as u64, size - read_count) as usize;
173         let nread = reader
174             .read(&mut buf[..this_count])
175             .map_err(Error::ReadingData)?;
176         writer.write(&buf[..nread]).map_err(Error::WritingData)?;
177         read_count += nread as u64;
178         if nread == 0 || read_count == size {
179             break;
180         }
181     }
182 
183     Ok(())
184 }
185 
convert_reader_writer<R, W>(reader: &mut R, writer: &mut W, size: u64) -> Result<()> where R: Read + Seek + SeekHole, W: Write + Seek,186 fn convert_reader_writer<R, W>(reader: &mut R, writer: &mut W, size: u64) -> Result<()>
187 where
188     R: Read + Seek + SeekHole,
189     W: Write + Seek,
190 {
191     let mut offset = 0;
192     while offset < size {
193         // Find the next range of data.
194         let next_data = match reader.seek_data(offset).map_err(Error::SeekingFile)? {
195             Some(o) => o,
196             None => {
197                 // No more data in the file.
198                 break;
199             }
200         };
201         let next_hole = match reader.seek_hole(next_data).map_err(Error::SeekingFile)? {
202             Some(o) => o,
203             None => {
204                 // This should not happen - there should always be at least one hole
205                 // after any data.
206                 return Err(Error::SeekingFile(io::Error::from_raw_os_error(EINVAL)));
207             }
208         };
209         let count = next_hole - next_data;
210         convert_copy(reader, writer, next_data, count)?;
211         offset = next_hole;
212     }
213 
214     Ok(())
215 }
216 
convert_reader<R>(reader: &mut R, dst_file: File, dst_type: ImageType) -> Result<()> where R: Read + Seek + SeekHole,217 fn convert_reader<R>(reader: &mut R, dst_file: File, dst_type: ImageType) -> Result<()>
218 where
219     R: Read + Seek + SeekHole,
220 {
221     let src_size = reader.seek(SeekFrom::End(0)).map_err(Error::SeekingFile)?;
222     reader
223         .seek(SeekFrom::Start(0))
224         .map_err(Error::SeekingFile)?;
225 
226     // Ensure the destination file is empty before writing to it.
227     dst_file.set_len(0).map_err(Error::SettingFileSize)?;
228 
229     match dst_type {
230         ImageType::Qcow2 => {
231             let mut dst_writer = QcowFile::new(dst_file, src_size).map_err(Error::QcowError)?;
232             convert_reader_writer(reader, &mut dst_writer, src_size)
233         }
234         ImageType::Raw => {
235             let mut dst_writer = dst_file;
236             // Set the length of the destination file to convert it into a sparse file
237             // of the desired size.
238             dst_writer
239                 .set_len(src_size)
240                 .map_err(Error::SettingFileSize)?;
241             convert_reader_writer(reader, &mut dst_writer, src_size)
242         }
243         _ => Err(Error::ConversionNotSupported),
244     }
245 }
246 
247 /// Copy the contents of a disk image in `src_file` into `dst_file`.
248 /// The type of `src_file` is automatically detected, and the output file type is
249 /// determined by `dst_type`.
convert(src_file: File, dst_file: File, dst_type: ImageType) -> Result<()>250 pub fn convert(src_file: File, dst_file: File, dst_type: ImageType) -> Result<()> {
251     let src_type = detect_image_type(&src_file)?;
252     match src_type {
253         ImageType::Qcow2 => {
254             let mut src_reader = QcowFile::from(src_file).map_err(Error::QcowError)?;
255             convert_reader(&mut src_reader, dst_file, dst_type)
256         }
257         ImageType::Raw => {
258             // src_file is a raw file.
259             let mut src_reader = src_file;
260             convert_reader(&mut src_reader, dst_file, dst_type)
261         }
262         // TODO(schuffelen): Implement Read + Write + SeekHole for CompositeDiskFile
263         _ => Err(Error::ConversionNotSupported),
264     }
265 }
266 
267 /// Detect the type of an image file by checking for a valid header of the supported formats.
detect_image_type(file: &File) -> Result<ImageType>268 pub fn detect_image_type(file: &File) -> Result<ImageType> {
269     let mut f = file;
270     let disk_size = f.get_len().map_err(Error::SeekingFile)?;
271     let orig_seek = f.seek(SeekFrom::Current(0)).map_err(Error::SeekingFile)?;
272     f.seek(SeekFrom::Start(0)).map_err(Error::SeekingFile)?;
273 
274     // Try to read the disk in a nicely-aligned block size unless the whole file is smaller.
275     const MAGIC_BLOCK_SIZE: usize = 4096;
276     let mut magic = [0u8; MAGIC_BLOCK_SIZE];
277     let magic_read_len = if disk_size > MAGIC_BLOCK_SIZE as u64 {
278         MAGIC_BLOCK_SIZE
279     } else {
280         // This cast is safe since we know disk_size is less than MAGIC_BLOCK_SIZE (4096) and
281         // therefore is representable in usize.
282         disk_size as usize
283     };
284 
285     f.read_exact(&mut magic[0..magic_read_len])
286         .map_err(Error::ReadingHeader)?;
287     f.seek(SeekFrom::Start(orig_seek))
288         .map_err(Error::SeekingFile)?;
289 
290     #[cfg(feature = "composite-disk")]
291     if let Some(cdisk_magic) = magic.get(0..CDISK_MAGIC_LEN) {
292         if cdisk_magic == CDISK_MAGIC.as_bytes() {
293             return Ok(ImageType::CompositeDisk);
294         }
295     }
296 
297     if let Some(magic4) = magic.get(0..4) {
298         if magic4 == QCOW_MAGIC.to_be_bytes() {
299             return Ok(ImageType::Qcow2);
300         } else if magic4 == SPARSE_HEADER_MAGIC.to_le_bytes() {
301             return Ok(ImageType::AndroidSparse);
302         }
303     }
304 
305     Ok(ImageType::Raw)
306 }
307 
308 /// Check if the image file type can be used for async disk access.
async_ok(raw_image: &File) -> Result<bool>309 pub fn async_ok(raw_image: &File) -> Result<bool> {
310     let image_type = detect_image_type(raw_image)?;
311     Ok(match image_type {
312         ImageType::Raw => true,
313         ImageType::Qcow2 | ImageType::AndroidSparse | ImageType::CompositeDisk => false,
314     })
315 }
316 
317 /// Inspect the image file type and create an appropriate disk file to match it.
create_async_disk_file(raw_image: File) -> Result<Box<dyn ToAsyncDisk>>318 pub fn create_async_disk_file(raw_image: File) -> Result<Box<dyn ToAsyncDisk>> {
319     let image_type = detect_image_type(&raw_image)?;
320     Ok(match image_type {
321         ImageType::Raw => Box::new(raw_image) as Box<dyn ToAsyncDisk>,
322         ImageType::Qcow2 | ImageType::AndroidSparse | ImageType::CompositeDisk => {
323             return Err(Error::UnknownType)
324         }
325     })
326 }
327 
328 /// Inspect the image file type and create an appropriate disk file to match it.
create_disk_file(raw_image: File) -> Result<Box<dyn DiskFile>>329 pub fn create_disk_file(raw_image: File) -> Result<Box<dyn DiskFile>> {
330     let image_type = detect_image_type(&raw_image)?;
331     Ok(match image_type {
332         ImageType::Raw => Box::new(raw_image) as Box<dyn DiskFile>,
333         ImageType::Qcow2 => {
334             Box::new(QcowFile::from(raw_image).map_err(Error::QcowError)?) as Box<dyn DiskFile>
335         }
336         #[cfg(feature = "composite-disk")]
337         ImageType::CompositeDisk => {
338             // Valid composite disk header present
339             Box::new(CompositeDiskFile::from_file(raw_image).map_err(Error::CreateCompositeDisk)?)
340                 as Box<dyn DiskFile>
341         }
342         #[cfg(not(feature = "composite-disk"))]
343         ImageType::CompositeDisk => return Err(Error::UnknownType),
344         ImageType::AndroidSparse => {
345             Box::new(AndroidSparse::from_file(raw_image).map_err(Error::CreateAndroidSparseDisk)?)
346                 as Box<dyn DiskFile>
347         }
348     })
349 }
350 
351 /// An asynchronously accessible disk.
352 #[async_trait(?Send)]
353 pub trait AsyncDisk: DiskGetLen + FileSetLen + FileAllocate {
354     /// Returns the inner file consuming self.
into_inner(self: Box<Self>) -> Box<dyn ToAsyncDisk>355     fn into_inner(self: Box<Self>) -> Box<dyn ToAsyncDisk>;
356 
357     /// Asynchronously fsyncs any completed operations to the disk.
fsync(&self) -> Result<()>358     async fn fsync(&self) -> Result<()>;
359 
360     /// Reads from the file at 'file_offset' in to memory `mem` at `mem_offsets`.
361     /// `mem_offsets` is similar to an iovec except relative to the start of `mem`.
read_to_mem<'a>( &self, file_offset: u64, mem: Arc<GuestMemory>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>362     async fn read_to_mem<'a>(
363         &self,
364         file_offset: u64,
365         mem: Arc<GuestMemory>,
366         mem_offsets: &'a [cros_async::MemRegion],
367     ) -> Result<usize>;
368 
369     /// Writes to the file at 'file_offset' from memory `mem` at `mem_offsets`.
write_from_mem<'a>( &self, file_offset: u64, mem: Arc<GuestMemory>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>370     async fn write_from_mem<'a>(
371         &self,
372         file_offset: u64,
373         mem: Arc<GuestMemory>,
374         mem_offsets: &'a [cros_async::MemRegion],
375     ) -> Result<usize>;
376 
377     /// Replaces a range of bytes with a hole.
punch_hole(&self, file_offset: u64, length: u64) -> Result<()>378     async fn punch_hole(&self, file_offset: u64, length: u64) -> Result<()>;
379 
380     /// Writes up to `length` bytes of zeroes to the stream, returning how many bytes were written.
write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>381     async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>;
382 }
383 
384 use cros_async::IoSourceExt;
385 
386 /// A disk backed by a single file that implements `AsyncDisk` for access.
387 pub struct SingleFileDisk {
388     inner: Box<dyn IoSourceExt<File>>,
389 }
390 
391 impl SingleFileDisk {
new(disk: File, ex: &Executor) -> Result<Self>392     pub fn new(disk: File, ex: &Executor) -> Result<Self> {
393         ex.async_from(disk)
394             .map_err(Error::CreateSingleFileDisk)
395             .map(|inner| SingleFileDisk { inner })
396     }
397 }
398 
399 impl DiskGetLen for SingleFileDisk {
get_len(&self) -> io::Result<u64>400     fn get_len(&self) -> io::Result<u64> {
401         self.inner.as_source().get_len()
402     }
403 }
404 
405 impl FileSetLen for SingleFileDisk {
set_len(&self, len: u64) -> io::Result<()>406     fn set_len(&self, len: u64) -> io::Result<()> {
407         self.inner.as_source().set_len(len)
408     }
409 }
410 
411 impl FileAllocate for SingleFileDisk {
allocate(&mut self, offset: u64, len: u64) -> io::Result<()>412     fn allocate(&mut self, offset: u64, len: u64) -> io::Result<()> {
413         self.inner.as_source_mut().allocate(offset, len)
414     }
415 }
416 
417 #[async_trait(?Send)]
418 impl AsyncDisk for SingleFileDisk {
into_inner(self: Box<Self>) -> Box<dyn ToAsyncDisk>419     fn into_inner(self: Box<Self>) -> Box<dyn ToAsyncDisk> {
420         Box::new(self.inner.into_source())
421     }
422 
fsync(&self) -> Result<()>423     async fn fsync(&self) -> Result<()> {
424         self.inner.fsync().await.map_err(Error::Fsync)
425     }
426 
read_to_mem<'a>( &self, file_offset: u64, mem: Arc<GuestMemory>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>427     async fn read_to_mem<'a>(
428         &self,
429         file_offset: u64,
430         mem: Arc<GuestMemory>,
431         mem_offsets: &'a [cros_async::MemRegion],
432     ) -> Result<usize> {
433         self.inner
434             .read_to_mem(file_offset, mem, mem_offsets)
435             .await
436             .map_err(Error::ReadToMem)
437     }
438 
write_from_mem<'a>( &self, file_offset: u64, mem: Arc<GuestMemory>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>439     async fn write_from_mem<'a>(
440         &self,
441         file_offset: u64,
442         mem: Arc<GuestMemory>,
443         mem_offsets: &'a [cros_async::MemRegion],
444     ) -> Result<usize> {
445         self.inner
446             .write_from_mem(file_offset, mem, mem_offsets)
447             .await
448             .map_err(Error::WriteFromMem)
449     }
450 
punch_hole(&self, file_offset: u64, length: u64) -> Result<()>451     async fn punch_hole(&self, file_offset: u64, length: u64) -> Result<()> {
452         self.inner
453             .fallocate(
454                 file_offset,
455                 length,
456                 (libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE) as u32,
457             )
458             .await
459             .map_err(Error::Fallocate)
460     }
461 
write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>462     async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()> {
463         if self
464             .inner
465             .fallocate(
466                 file_offset,
467                 length,
468                 (libc::FALLOC_FL_ZERO_RANGE | libc::FALLOC_FL_KEEP_SIZE) as u32,
469             )
470             .await
471             .is_ok()
472         {
473             return Ok(());
474         }
475 
476         // Fall back to writing zeros if fallocate doesn't work.
477         let buf_size = min(length, 0x10000);
478         let mut nwritten = 0;
479         while nwritten < length {
480             let remaining = length - nwritten;
481             let write_size = min(remaining, buf_size) as usize;
482             let buf = vec![0u8; write_size];
483             nwritten += self
484                 .inner
485                 .write_from_vec(file_offset + nwritten as u64, buf)
486                 .await
487                 .map(|(n, _)| n as u64)
488                 .map_err(Error::WriteFromVec)?;
489         }
490         Ok(())
491     }
492 }
493 
494 #[cfg(test)]
495 mod tests {
496     use super::*;
497 
498     use std::fs::{File, OpenOptions};
499 
500     use cros_async::{Executor, MemRegion};
501     use vm_memory::{GuestAddress, GuestMemory};
502 
503     #[test]
read_async()504     fn read_async() {
505         async fn read_zeros_async(ex: &Executor) {
506             let guest_mem = Arc::new(GuestMemory::new(&[(GuestAddress(0), 4096)]).unwrap());
507             let f = File::open("/dev/zero").unwrap();
508             let async_file = SingleFileDisk::new(f, ex).unwrap();
509             let result = async_file
510                 .read_to_mem(
511                     0,
512                     Arc::clone(&guest_mem),
513                     &[MemRegion { offset: 0, len: 48 }],
514                 )
515                 .await;
516             assert_eq!(48, result.unwrap());
517         }
518 
519         let ex = Executor::new().unwrap();
520         ex.run_until(read_zeros_async(&ex)).unwrap();
521     }
522 
523     #[test]
write_async()524     fn write_async() {
525         async fn write_zeros_async(ex: &Executor) {
526             let guest_mem = Arc::new(GuestMemory::new(&[(GuestAddress(0), 4096)]).unwrap());
527             let f = OpenOptions::new().write(true).open("/dev/null").unwrap();
528             let async_file = SingleFileDisk::new(f, ex).unwrap();
529             let result = async_file
530                 .write_from_mem(
531                     0,
532                     Arc::clone(&guest_mem),
533                     &[MemRegion { offset: 0, len: 48 }],
534                 )
535                 .await;
536             assert_eq!(48, result.unwrap());
537         }
538 
539         let ex = Executor::new().unwrap();
540         ex.run_until(write_zeros_async(&ex)).unwrap();
541     }
542 
543     #[test]
detect_image_type_raw()544     fn detect_image_type_raw() {
545         let mut t = tempfile::tempfile().unwrap();
546         // Fill the first block of the file with "random" data.
547         let buf = "ABCD".as_bytes().repeat(1024);
548         t.write_all(&buf).unwrap();
549         let image_type = detect_image_type(&t).expect("failed to detect image type");
550         assert_eq!(image_type, ImageType::Raw);
551     }
552 
553     #[test]
detect_image_type_qcow2()554     fn detect_image_type_qcow2() {
555         let mut t = tempfile::tempfile().unwrap();
556         // Write the qcow2 magic signature. The rest of the header is not filled in, so if
557         // detect_image_type is ever updated to validate more of the header, this test would need
558         // to be updated.
559         let buf: &[u8] = &[0x51, 0x46, 0x49, 0xfb];
560         t.write_all(&buf).unwrap();
561         let image_type = detect_image_type(&t).expect("failed to detect image type");
562         assert_eq!(image_type, ImageType::Qcow2);
563     }
564 
565     #[test]
detect_image_type_android_sparse()566     fn detect_image_type_android_sparse() {
567         let mut t = tempfile::tempfile().unwrap();
568         // Write the Android sparse magic signature. The rest of the header is not filled in, so if
569         // detect_image_type is ever updated to validate more of the header, this test would need
570         // to be updated.
571         let buf: &[u8] = &[0x3a, 0xff, 0x26, 0xed];
572         t.write_all(&buf).unwrap();
573         let image_type = detect_image_type(&t).expect("failed to detect image type");
574         assert_eq!(image_type, ImageType::AndroidSparse);
575     }
576 
577     #[test]
578     #[cfg(feature = "composite-disk")]
detect_image_type_composite()579     fn detect_image_type_composite() {
580         let mut t = tempfile::tempfile().unwrap();
581         // Write the composite disk magic signature. The rest of the header is not filled in, so if
582         // detect_image_type is ever updated to validate more of the header, this test would need
583         // to be updated.
584         let buf = "composite_disk\x1d".as_bytes();
585         t.write_all(&buf).unwrap();
586         let image_type = detect_image_type(&t).expect("failed to detect image type");
587         assert_eq!(image_type, ImageType::CompositeDisk);
588     }
589 
590     #[test]
detect_image_type_small_file()591     fn detect_image_type_small_file() {
592         let mut t = tempfile::tempfile().unwrap();
593         // Write a file smaller than the four-byte qcow2/sparse magic to ensure the small file logic
594         // works correctly and handles it as a raw file.
595         let buf: &[u8] = &[0xAA, 0xBB];
596         t.write_all(&buf).unwrap();
597         let image_type = detect_image_type(&t).expect("failed to detect image type");
598         assert_eq!(image_type, ImageType::Raw);
599     }
600 }
601