diff options
Diffstat (limited to 'disk/src/composite.rs')
-rw-r--r-- | disk/src/composite.rs | 566 |
1 files changed, 566 insertions, 0 deletions
diff --git a/disk/src/composite.rs b/disk/src/composite.rs new file mode 100644 index 0000000..5d6e5a2 --- /dev/null +++ b/disk/src/composite.rs @@ -0,0 +1,566 @@ +// Copyright 2019 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::cmp::{max, min}; +use std::convert::TryFrom; +use std::fmt::{self, Display}; +use std::fs::{File, OpenOptions}; +use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; +use std::ops::Range; +use std::os::unix::io::RawFd; + +use crate::{create_disk_file, DiskFile, ImageType}; +use data_model::VolatileSlice; +use protos::cdisk_spec; +use remain::sorted; +use sys_util::{AsRawFds, FileReadWriteVolatile, FileSetLen, FileSync, PunchHole, WriteZeroes}; + +#[sorted] +#[derive(Debug)] +pub enum Error { + DiskError(Box<crate::Error>), + InvalidMagicHeader, + InvalidProto(protobuf::ProtobufError), + InvalidSpecification(String), + OpenFile(io::Error), + ReadSpecificationError(io::Error), + UnknownVersion(u64), + UnsupportedComponent(ImageType), +} + +impl Display for Error { + #[remain::check] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Error::*; + + #[sorted] + match self { + DiskError(e) => write!(f, "failed to use underlying disk: \"{}\"", e), + InvalidMagicHeader => write!(f, "invalid magic header for composite disk format"), + InvalidProto(e) => write!(f, "failed to parse specification proto: \"{}\"", e), + InvalidSpecification(s) => write!(f, "invalid specification: \"{}\"", s), + OpenFile(e) => write!(f, "failed to open component file: \"{}\"", e), + ReadSpecificationError(e) => write!(f, "failed to read specification: \"{}\"", e), + UnknownVersion(v) => write!(f, "unknown version {} in specification", v), + UnsupportedComponent(c) => write!(f, "unsupported component disk type \"{:?}\"", c), + } + } +} + +pub type Result<T> = std::result::Result<T, Error>; + +struct ComponentDiskPart { + file: Box<dyn DiskFile>, + offset: u64, + length: u64, +} + +impl ComponentDiskPart { + fn range(&self) -> Range<u64> { + self.offset..(self.offset + self.length) + } +} + +/// Represents a composite virtual disk made out of multiple component files. This is described on +/// disk by a protocol buffer file that lists out the component file locations and their offsets +/// and lengths on the virtual disk. The spaces covered by the component disks must be contiguous +/// and not overlapping. +pub struct CompositeDiskFile { + component_disks: Vec<ComponentDiskPart>, + cursor_location: u64, +} + +fn ranges_overlap(a: &Range<u64>, b: &Range<u64>) -> bool { + // essentially !range_intersection(a, b).is_empty(), but that's experimental + let intersection = range_intersection(a, b); + intersection.start < intersection.end +} + +fn range_intersection(a: &Range<u64>, b: &Range<u64>) -> Range<u64> { + Range { + start: max(a.start, b.start), + end: min(a.end, b.end), + } +} + +/// A magic string placed at the beginning of a composite disk file to identify it. +pub static CDISK_MAGIC: &str = "composite_disk\x1d"; +/// The length of the CDISK_MAGIC string. Created explicitly as a static constant so that it is +/// possible to create a character array of the same length. +pub const CDISK_MAGIC_LEN: usize = 15; + +impl CompositeDiskFile { + fn new(mut disks: Vec<ComponentDiskPart>) -> Result<CompositeDiskFile> { + disks.sort_by(|d1, d2| d1.offset.cmp(&d2.offset)); + let contiguous_err = disks + .windows(2) + .map(|s| { + if s[0].offset == s[1].offset { + let text = format!("Two disks at offset {}", s[0].offset); + Err(Error::InvalidSpecification(text)) + } else { + Ok(()) + } + }) + .find(|r| r.is_err()); + if let Some(Err(e)) = contiguous_err { + return Err(e); + } + Ok(CompositeDiskFile { + component_disks: disks, + cursor_location: 0, + }) + } + + /// Set up a composite disk by reading the specification from a file. The file must consist of + /// the CDISK_MAGIC string followed by one binary instance of the CompositeDisk protocol + /// buffer. Returns an error if it could not read the file or if the specification was invalid. + pub fn from_file(mut file: File) -> Result<CompositeDiskFile> { + file.seek(SeekFrom::Start(0)) + .map_err(Error::ReadSpecificationError)?; + let mut magic_space = [0u8; CDISK_MAGIC_LEN]; + file.read_exact(&mut magic_space[..]) + .map_err(Error::ReadSpecificationError)?; + if magic_space != CDISK_MAGIC.as_bytes() { + return Err(Error::InvalidMagicHeader); + } + let proto: cdisk_spec::CompositeDisk = + protobuf::parse_from_reader(&mut file).map_err(Error::InvalidProto)?; + if proto.get_version() != 1 { + return Err(Error::UnknownVersion(proto.get_version())); + } + let mut open_options = OpenOptions::new(); + open_options.read(true); + let mut disks: Vec<ComponentDiskPart> = proto + .get_component_disks() + .into_iter() + .map(|disk| { + open_options.write( + disk.get_read_write_capability() == cdisk_spec::ReadWriteCapability::READ_WRITE, + ); + let file = open_options + .open(disk.get_file_path()) + .map_err(Error::OpenFile)?; + Ok(ComponentDiskPart { + file: create_disk_file(file).map_err(|e| Error::DiskError(Box::new(e)))?, + offset: disk.get_offset(), + length: 0, // Assigned later + }) + }) + .collect::<Result<Vec<ComponentDiskPart>>>()?; + disks.sort_by(|d1, d2| d1.offset.cmp(&d2.offset)); + for i in 0..(disks.len() - 1) { + let length = disks[i + 1].offset - disks[i].offset; + if length == 0 { + let text = format!("Two disks at offset {}", disks[i].offset); + return Err(Error::InvalidSpecification(text)); + } + if let Some(disk) = disks.get_mut(i) { + disk.length = length; + } else { + let text = format!("Unable to set disk length {}", length); + return Err(Error::InvalidSpecification(text)); + } + } + let num_disks = disks.len(); + if let Some(last_disk) = disks.get_mut(num_disks - 1) { + if proto.get_length() <= last_disk.offset { + let text = format!( + "Full size of disk doesn't match last offset. {} <= {}", + proto.get_length(), + last_disk.offset + ); + return Err(Error::InvalidSpecification(text)); + } + last_disk.length = proto.get_length() - last_disk.offset; + } else { + let text = format!( + "Unable to set last disk length to end at {}", + proto.get_length() + ); + return Err(Error::InvalidSpecification(text)); + } + + CompositeDiskFile::new(disks) + } + + fn length(&self) -> u64 { + if let Some(disk) = self.component_disks.last() { + disk.offset + disk.length + } else { + 0 + } + } + + fn disk_at_offset<'a>(&'a mut self, offset: u64) -> io::Result<&'a mut ComponentDiskPart> { + self.component_disks + .iter_mut() + .find(|disk| disk.range().contains(&offset)) + .ok_or(io::Error::new( + ErrorKind::InvalidData, + format!("no disk at offset {}", offset), + )) + } + + fn disks_in_range<'a>(&'a mut self, range: &Range<u64>) -> Vec<&'a mut ComponentDiskPart> { + self.component_disks + .iter_mut() + .filter(|disk| ranges_overlap(&disk.range(), range)) + .collect() + } +} + +impl FileSetLen for CompositeDiskFile { + fn set_len(&self, _len: u64) -> io::Result<()> { + Err(io::Error::new(ErrorKind::Other, "unsupported operation")) + } +} + +impl FileSync for CompositeDiskFile { + fn fsync(&mut self) -> io::Result<()> { + for disk in self.component_disks.iter_mut() { + disk.file.fsync()?; + } + Ok(()) + } +} + +// Implements Read and Write targeting volatile storage for composite disks. +// +// Note that reads and writes will return early if crossing component disk boundaries. +// This is allowed by the read and write specifications, which only say read and write +// have to return how many bytes were actually read or written. Use read_exact_volatile +// or write_all_volatile to make sure all bytes are received/transmitted. +// +// If one of the component disks does a partial read or write, that also gets passed +// transparently to the parent. +impl FileReadWriteVolatile for CompositeDiskFile { + fn read_volatile(&mut self, slice: VolatileSlice) -> io::Result<usize> { + let cursor_location = self.cursor_location; + let disk = self.disk_at_offset(cursor_location)?; + disk.file + .seek(SeekFrom::Start(cursor_location - disk.offset))?; + let subslice = if cursor_location + slice.size() > disk.offset + disk.length { + let new_size = disk.offset + disk.length - cursor_location; + slice + .sub_slice(0, new_size) + .map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("{:?}", e)))? + } else { + slice + }; + let result = disk.file.read_volatile(subslice); + if let Ok(size) = result { + self.cursor_location += size as u64; + } + result + } + fn write_volatile(&mut self, slice: VolatileSlice) -> io::Result<usize> { + let cursor_location = self.cursor_location; + let disk = self.disk_at_offset(cursor_location)?; + disk.file + .seek(SeekFrom::Start(cursor_location - disk.offset))?; + let subslice = if cursor_location + slice.size() > disk.offset + disk.length { + let new_size = disk.offset + disk.length - cursor_location; + slice + .sub_slice(0, new_size) + .map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("{:?}", e)))? + } else { + slice + }; + let result = disk.file.write_volatile(subslice); + if let Ok(size) = result { + self.cursor_location += size as u64; + } + result + } +} + +impl PunchHole for CompositeDiskFile { + fn punch_hole(&mut self, offset: u64, length: u64) -> io::Result<()> { + let range = offset..(offset + length); + let disks = self.disks_in_range(&range); + for disk in disks { + let intersection = range_intersection(&range, &disk.range()); + if intersection.start >= intersection.end { + continue; + } + let result = disk.file.punch_hole( + intersection.start - disk.offset, + intersection.end - intersection.start, + ); + if result.is_err() { + return result; + } + } + Ok(()) + } +} + +impl Seek for CompositeDiskFile { + fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { + let cursor_location = match pos { + SeekFrom::Start(offset) => Ok(offset), + SeekFrom::End(offset) => u64::try_from(self.length() as i64 + offset), + SeekFrom::Current(offset) => u64::try_from(self.cursor_location as i64 + offset), + } + .map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?; + self.cursor_location = cursor_location; + Ok(cursor_location) + } +} + +impl WriteZeroes for CompositeDiskFile { + fn write_zeroes(&mut self, length: usize) -> io::Result<usize> { + let cursor_location = self.cursor_location; + let disk = self.disk_at_offset(cursor_location)?; + disk.file + .seek(SeekFrom::Start(cursor_location - disk.offset))?; + let new_length = if cursor_location + length as u64 > disk.offset + disk.length { + (disk.offset + disk.length - cursor_location) as usize + } else { + length + }; + let result = disk.file.write_zeroes(new_length); + if let Ok(size) = result { + self.cursor_location += size as u64; + } + result + } +} + +impl AsRawFds for CompositeDiskFile { + fn as_raw_fds(&self) -> Vec<RawFd> { + self.component_disks + .iter() + .map(|d| d.file.as_raw_fds()) + .flatten() + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use data_model::VolatileMemory; + use std::os::unix::io::AsRawFd; + use sys_util::SharedMemory; + + #[test] + fn block_duplicate_offset_disks() { + let file1: File = SharedMemory::new(None).unwrap().into(); + let file2: File = SharedMemory::new(None).unwrap().into(); + let disk_part1 = ComponentDiskPart { + file: Box::new(file1), + offset: 0, + length: 100, + }; + let disk_part2 = ComponentDiskPart { + file: Box::new(file2), + offset: 0, + length: 100, + }; + assert!(CompositeDiskFile::new(vec![disk_part1, disk_part2]).is_err()); + } + + #[test] + fn seek_to_end() { + let file1: File = SharedMemory::new(None).unwrap().into(); + let file2: File = SharedMemory::new(None).unwrap().into(); + let disk_part1 = ComponentDiskPart { + file: Box::new(file1), + offset: 0, + length: 100, + }; + let disk_part2 = ComponentDiskPart { + file: Box::new(file2), + offset: 100, + length: 100, + }; + let mut composite = CompositeDiskFile::new(vec![disk_part1, disk_part2]).unwrap(); + let location = composite.seek(SeekFrom::End(0)).unwrap(); + assert_eq!(location, 200); + } + + #[test] + fn single_file_passthrough() { + let file: File = SharedMemory::new(None).unwrap().into(); + let disk_part = ComponentDiskPart { + file: Box::new(file), + offset: 0, + length: 100, + }; + let mut composite = CompositeDiskFile::new(vec![disk_part]).unwrap(); + let mut input_memory = [55u8; 5]; + let input_volatile_memory = &mut input_memory[..]; + composite + .write_all_volatile(input_volatile_memory.get_slice(0, 5).unwrap()) + .unwrap(); + composite.seek(SeekFrom::Start(0)).unwrap(); + let mut output_memory = [0u8; 5]; + let output_volatile_memory = &mut output_memory[..]; + composite + .read_exact_volatile(output_volatile_memory.get_slice(0, 5).unwrap()) + .unwrap(); + assert_eq!(input_memory, output_memory); + } + + #[test] + fn triple_file_fds() { + let file1: File = SharedMemory::new(None).unwrap().into(); + let file2: File = SharedMemory::new(None).unwrap().into(); + let file3: File = SharedMemory::new(None).unwrap().into(); + let mut in_fds = vec![file1.as_raw_fd(), file2.as_raw_fd(), file3.as_raw_fd()]; + in_fds.sort(); + let disk_part1 = ComponentDiskPart { + file: Box::new(file1), + offset: 0, + length: 100, + }; + let disk_part2 = ComponentDiskPart { + file: Box::new(file2), + offset: 100, + length: 100, + }; + let disk_part3 = ComponentDiskPart { + file: Box::new(file3), + offset: 200, + length: 100, + }; + let composite = CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap(); + let mut out_fds = composite.as_raw_fds(); + out_fds.sort(); + assert_eq!(in_fds, out_fds); + } + + #[test] + fn triple_file_passthrough() { + let file1: File = SharedMemory::new(None).unwrap().into(); + let file2: File = SharedMemory::new(None).unwrap().into(); + let file3: File = SharedMemory::new(None).unwrap().into(); + let disk_part1 = ComponentDiskPart { + file: Box::new(file1), + offset: 0, + length: 100, + }; + let disk_part2 = ComponentDiskPart { + file: Box::new(file2), + offset: 100, + length: 100, + }; + let disk_part3 = ComponentDiskPart { + file: Box::new(file3), + offset: 200, + length: 100, + }; + let mut composite = + CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap(); + composite.seek(SeekFrom::Start(50)).unwrap(); + let mut input_memory = [55u8; 200]; + let input_volatile_memory = &mut input_memory[..]; + composite + .write_all_volatile(input_volatile_memory.get_slice(0, 200).unwrap()) + .unwrap(); + composite.seek(SeekFrom::Start(50)).unwrap(); + let mut output_memory = [0u8; 200]; + let output_volatile_memory = &mut output_memory[..]; + composite + .read_exact_volatile(output_volatile_memory.get_slice(0, 200).unwrap()) + .unwrap(); + assert!(input_memory.into_iter().eq(output_memory.into_iter())); + } + + #[test] + fn triple_file_punch_hole() { + let file1: File = SharedMemory::new(None).unwrap().into(); + let file2: File = SharedMemory::new(None).unwrap().into(); + let file3: File = SharedMemory::new(None).unwrap().into(); + let disk_part1 = ComponentDiskPart { + file: Box::new(file1), + offset: 0, + length: 100, + }; + let disk_part2 = ComponentDiskPart { + file: Box::new(file2), + offset: 100, + length: 100, + }; + let disk_part3 = ComponentDiskPart { + file: Box::new(file3), + offset: 200, + length: 100, + }; + let mut composite = + CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap(); + composite.seek(SeekFrom::Start(0)).unwrap(); + let mut input_memory = [55u8; 300]; + let input_volatile_memory = &mut input_memory[..]; + composite + .write_all_volatile(input_volatile_memory.get_slice(0, 300).unwrap()) + .unwrap(); + composite.punch_hole(50, 200).unwrap(); + composite.seek(SeekFrom::Start(0)).unwrap(); + let mut output_memory = [0u8; 300]; + let output_volatile_memory = &mut output_memory[..]; + composite + .read_exact_volatile(output_volatile_memory.get_slice(0, 300).unwrap()) + .unwrap(); + + for i in 50..250 { + input_memory[i] = 0; + } + assert!(input_memory.into_iter().eq(output_memory.into_iter())); + } + + #[test] + fn triple_file_write_zeroes() { + let file1: File = SharedMemory::new(None).unwrap().into(); + let file2: File = SharedMemory::new(None).unwrap().into(); + let file3: File = SharedMemory::new(None).unwrap().into(); + let disk_part1 = ComponentDiskPart { + file: Box::new(file1), + offset: 0, + length: 100, + }; + let disk_part2 = ComponentDiskPart { + file: Box::new(file2), + offset: 100, + length: 100, + }; + let disk_part3 = ComponentDiskPart { + file: Box::new(file3), + offset: 200, + length: 100, + }; + let mut composite = + CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap(); + composite.seek(SeekFrom::Start(0)).unwrap(); + let mut input_memory = [55u8; 300]; + let input_volatile_memory = &mut input_memory[..]; + composite + .write_all_volatile(input_volatile_memory.get_slice(0, 300).unwrap()) + .unwrap(); + composite.seek(SeekFrom::Start(50)).unwrap(); + let mut zeroes_written = 0; + while zeroes_written < 200 { + zeroes_written += composite.write_zeroes(200 - zeroes_written).unwrap(); + } + composite.seek(SeekFrom::Start(0)).unwrap(); + let mut output_memory = [0u8; 300]; + let output_volatile_memory = &mut output_memory[..]; + composite + .read_exact_volatile(output_volatile_memory.get_slice(0, 300).unwrap()) + .unwrap(); + + for i in 50..250 { + input_memory[i] = 0; + } + for i in 0..300 { + println!( + "input[{0}] = {1}, output[{0}] = {2}", + i, input_memory[i], output_memory[i] + ); + } + assert!(input_memory.into_iter().eq(output_memory.into_iter())); + } +} |