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