blob: dc989ee906364f93ee4b26d317c51ed0560f6621 [file]
// Copyright (C) 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Utilities for writing tests with libstorage, e.g. creating fake block devices.
use crc32fast::Hasher;
use gbl_async::yield_now;
pub use gbl_storage::*;
use liberror::{Error, Result};
use safemath::SafeNum;
use std::{
collections::{BTreeMap, VecDeque},
mem::size_of,
};
use zerocopy::{AsBytes, FromZeroes};
/// Helper `gbl_storage::BlockIoSync` struct for TestBlockDevice.
pub struct TestBlockIo {
/// The storage block size in bytes.
pub block_size: u64,
/// The storage access alignment in bytes.
pub alignment: u64,
/// The backing storage data.
pub storage: Vec<u8>,
/// The number of successful write calls.
pub num_writes: usize,
/// The number of successful read calls.
pub num_reads: usize,
/// Injected errors.
pub errors: VecDeque<Error>,
}
impl TestBlockIo {
/// Creates a new [TestBlockIo].
pub fn new(block_size: u64, alignment: u64, data: Vec<u8>) -> Self {
Self {
block_size,
alignment,
storage: data,
num_writes: 0,
num_reads: 0,
errors: Default::default(),
}
}
fn check_alignment(&mut self, buffer: &[u8]) -> bool {
matches!(is_buffer_aligned(buffer, self.alignment), Ok(true))
&& matches!(is_aligned(buffer.len().into(), self.block_size.into()), Ok(true))
}
}
impl BlockIoAsync for TestBlockIo {
/// Returns a `BlockInfo` for the block device.
fn info(&mut self) -> BlockInfo {
BlockInfo {
block_size: self.block_size,
num_blocks: u64::try_from(self.storage.len()).unwrap() / self.block_size,
alignment: self.alignment,
}
}
async fn read_blocks(&mut self, blk_offset: u64, out: &mut [u8]) -> Result<()> {
assert!(self.check_alignment(out));
let offset = (SafeNum::from(blk_offset) * self.block_size).try_into().unwrap();
yield_now().await; // yield once to simulate IO pending.
match self.errors.pop_front() {
Some(e) => Err(e),
_ => Ok(out.clone_from_slice(&self.storage[offset..][..out.len()])),
}
}
async fn write_blocks(&mut self, blk_offset: u64, data: &mut [u8]) -> Result<()> {
assert!(self.check_alignment(data));
let offset = (SafeNum::from(blk_offset) * self.block_size).try_into().unwrap();
yield_now().await; // yield once to simulate IO pending.
match self.errors.pop_front() {
Some(e) => Err(e),
_ => Ok(self.storage[offset..][..data.len()].clone_from_slice(data)),
}
}
}
/// Simple RAM based block device used by unit tests.
pub struct TestBlockDevice {
/// The mock block device.
pub io: TestBlockIo,
/// In-memory backing store.
pub scratch: Vec<u8>,
max_gpt_entries: u64,
}
impl From<&[u8]> for TestBlockDevice {
fn from(data: &[u8]) -> Self {
TestBlockDeviceBuilder::new().set_data(data).build()
}
}
impl AsBlockDevice for TestBlockDevice {
fn with(&mut self, f: &mut dyn FnMut(&mut dyn BlockIoSync, &mut [u8], u64)) {
f(&mut self.io, &mut self.scratch[..], self.max_gpt_entries)
}
}
impl Default for TestBlockDevice {
fn default() -> Self {
TestBlockDeviceBuilder::new().build()
}
}
impl TestBlockDevice {
/// Creates an instance of `AsyncGptDevice`
pub fn as_gpt_dev(&mut self) -> AsyncGptDevice<'_, &mut TestBlockIo> {
AsyncGptDevice::<&mut TestBlockIo>::new_from_monotonic_buffer(
&mut self.io,
&mut self.scratch[..],
self.max_gpt_entries,
)
.unwrap()
}
/// Creates an instance of `AsyncBlockDevice`
pub fn as_blk_dev(&mut self) -> AsyncBlockDevice<'_, &mut TestBlockIo> {
self.as_gpt_dev().into_blk_and_gpt().0
}
}
/// A description of the backing data store for a block device or partition.
/// Can either describe explicit data the device or partition is initialized with
/// OR a size in bytes if the device or partition can be initialized in a blank state.
#[derive(Copy, Clone)]
pub enum BackingStore<'a> {
/// Exact data to use for the storage.
Data(&'a [u8]),
/// Data size to use, will be initialized as zeros.
Size(usize),
}
impl<'a> BackingStore<'a> {
fn size(&self) -> usize {
match self {
Self::Data(slice) => slice.len(),
Self::Size(size) => *size,
}
}
}
enum DiskDescription<'a> {
Disk(BackingStore<'a>),
Partitions(BTreeMap<&'static str, BackingStore<'a>>),
}
/// Builder struct for TestBlockDevice.
/// Most tests will want either:
/// 1) A blank device of a reasonable size OR
/// 2) A device with specific initial data.
/// Other customizations include block size,
/// the maximum number of GPT entries,
/// the alignment requirements,
/// and the size of the scratch buffer.
///
/// Note: setting the storage size or storage data is generally safe,
/// as long as the backing store is large enough,
/// but customizing other attributes may generate a block device
/// that cannot successfully complete any operations.
/// This may be exactly the intention, but be warned that it can be tricky
/// to customize the device and generate something that works without errors.
pub struct TestBlockDeviceBuilder<'a> {
block_size: u64,
max_gpt_entries: u64,
alignment: u64,
disk_description: DiskDescription<'a>,
scratch_size: Option<usize>,
}
impl<'a> TestBlockDeviceBuilder<'a> {
/// The default access alignment in bytes.
pub const DEFAULT_ALIGNMENT: u64 = 64;
/// The default block size in bytes.
pub const DEFAULT_BLOCK_SIZE: u64 = 512;
/// The default maximum number of GPT entries.
pub const MAX_GPT_ENTRIES: u64 = 128;
/// Creates a new TestBlockDeviceBuilder with defaults for all attributes.
pub fn new() -> Self {
Self {
block_size: Self::DEFAULT_BLOCK_SIZE,
max_gpt_entries: Self::MAX_GPT_ENTRIES,
alignment: Self::DEFAULT_ALIGNMENT,
disk_description: DiskDescription::Disk(BackingStore::Size(
(Self::DEFAULT_BLOCK_SIZE * 32) as usize,
)),
scratch_size: None,
}
}
/// Set the block size of the block device in bytes.
/// The default is `DEFAULT_BLOCK_SIZE`.
pub fn set_block_size(mut self, block_size: u64) -> Self {
self.block_size = block_size;
self
}
/// Set the maximum number of GPT entries for the GPT header.
/// The default is `MAX_GPT_ENTRIES`.
/// Note: setting too large a number of entries will make a device
/// that fails to sync its GPT.
pub fn set_max_gpt_entries(mut self, max_gpt_entries: u64) -> Self {
self.max_gpt_entries = max_gpt_entries;
self
}
/// Set the required alignment for the TestBlockDevice.
/// An alignment of `0` means there are no alignment requirements.
/// The default is `DEFAULT_ALIGNMENT`.
pub fn set_alignment(mut self, alignment: u64) -> Self {
self.alignment = alignment;
self
}
/// Set the size of TestBlockDevice in bytes.
/// When built, the TestBlockDevice will have a blank backing store of size `size`.
/// The default is `DEFAULT_BLOCK_SIZE` * 32.
///
/// Note: This option is mutually exclusive with `set_data` and `add_partition`.
/// If `set_data` or `add_partition` have been called, `set_size` overrides
/// those customizations.
pub fn set_size(mut self, size: usize) -> Self {
self.disk_description = DiskDescription::Disk(BackingStore::Size(size));
self
}
/// Sets the block device's backing data to the provided slice.
///
/// Note: This option is mutually exclusive with `set_size` and `add_partition`.
/// If `set_size` or `add_partition` have been called, `set_data` overrides
/// those customizations.
pub fn set_data(mut self, data: &'a [u8]) -> Self {
self.disk_description = DiskDescription::Disk(BackingStore::Data(data));
self
}
/// Adds a partition description.
/// Partitions can be defined either with a specific backing store
/// from a slice OR from a specific size in bytes.
/// Partition sizes are rounded up to full blocks.
/// If the same partition name is added multiple times,
/// the last definition is used.
///
/// Note: explicitly added partitions are mutually exclusive with
/// `set_size` and `set_data`.
/// If either have been called, `add_partition` overrides that customization.
pub fn add_partition(mut self, name: &'static str, backing: BackingStore<'a>) -> Self {
match self.disk_description {
DiskDescription::Disk(_) => {
let mut map = BTreeMap::new();
map.insert(name, backing);
self.disk_description = DiskDescription::Partitions(map);
}
DiskDescription::Partitions(ref mut map) => {
map.insert(name, backing);
}
};
self
}
/// Customize the size of the block device's scratch buffer.
/// The default size is a known safe minimum calculated when `build()` is called.
///
/// Note: Too small a scratch buffer will generate errors.
/// Unless a test is specifically interested in a non-default
/// scratch size, it's better to rely on the default size.
pub fn set_scratch_size(mut self, scratch_size: usize) -> Self {
self.scratch_size = Some(scratch_size);
self
}
/// Consumes the builder and generates a TestBlockDevice
/// with the desired customizations.
pub fn build(self) -> TestBlockDevice {
let storage = match self.disk_description {
DiskDescription::Disk(BackingStore::Data(slice)) => Vec::from(slice),
DiskDescription::Disk(BackingStore::Size(size)) => vec![0u8; size],
DiskDescription::Partitions(partitions) => {
partitions_to_disk_data(&partitions, self.block_size as usize)
}
};
assert_eq!(storage.len() % (self.block_size as usize), 0);
let mut io = TestBlockIo::new(self.block_size, self.alignment, storage);
let scratch_size = match self.scratch_size {
Some(s) => s,
None => {
required_scratch_size(BlockIoSync::info(&mut io), self.max_gpt_entries).unwrap()
}
};
TestBlockDevice {
io,
scratch: vec![0u8; scratch_size],
max_gpt_entries: self.max_gpt_entries,
}
}
}
fn str_to_utf16_entry_name(name: &str) -> [u16; GPT_NAME_LEN_U16] {
assert!(name.len() < GPT_NAME_LEN_U16);
let mut data = [0; GPT_NAME_LEN_U16];
let tmp: Vec<u16> = name.encode_utf16().collect();
for (d, t) in std::iter::zip(data.iter_mut(), tmp) {
*d = t;
}
data
}
fn pad_to_block_size(store: &mut Vec<u8>, block_size: usize) {
let delta = (block_size - store.len() % block_size) % block_size;
for _ in 0..delta {
store.push(0);
}
}
fn add_blocks(store: &mut Vec<u8>, data: &[u8], block_size: usize) {
store.extend(data.iter());
pad_to_block_size(store, block_size);
}
fn pad_bytes(store: &mut Vec<u8>, size: usize, block_size: usize) {
for _ in 0..size {
store.push(0);
}
pad_to_block_size(store, block_size);
}
fn partitions_to_disk_data(
partitions: &BTreeMap<&'static str, BackingStore>,
block_size: usize,
) -> Vec<u8> {
let gpt_max_entries = 128;
assert!(partitions.len() <= gpt_max_entries);
let entry_blocks =
u64::try_from(SafeNum::from(gpt_max_entries) * size_of::<GptEntry>() / block_size).unwrap();
let mut block = entry_blocks
+ 1 // Protective MBR
+ 1 // Primary GPT header
;
// Leading mbr
let mut store = vec![0; block_size];
let mut header = GptHeader {
magic: GPT_MAGIC,
current: 1,
size: size_of::<GptHeader>() as u32,
first: block,
entries: 2,
entries_count: std::cmp::min(partitions.len(), gpt_max_entries) as u32,
entries_size: size_of::<GptEntry>() as u32,
..Default::default()
};
// Define gpt entry structures
let mut entries: Vec<GptEntry> = partitions
.iter()
.enumerate()
.take(gpt_max_entries)
.map(|(idx, (k, v))| {
let last = (SafeNum::from(v.size()).round_up(block_size) / block_size + block - 1)
.try_into()
.unwrap();
let mut entry = GptEntry {
part_type: [(idx + 1) as u8; GPT_GUID_LEN],
guid: [(idx + 1) as u8; GPT_GUID_LEN],
first: block,
last,
flags: 0,
name: str_to_utf16_entry_name(k),
};
entry.guid[0] = block as u8;
block = last + 1;
entry
})
.collect();
// Patch last fields of header
header.last = block - 1;
header.backup = block + entry_blocks;
header.entries_crc = entries
.iter()
.fold(Hasher::new(), |mut h, e| {
h.update(e.as_bytes());
h
})
.finalize();
header.update_crc();
// Primary header
add_blocks(&mut store, header.as_bytes(), block_size);
// Primary entries
entries.resize(gpt_max_entries, GptEntry::new_zeroed());
for e in &entries {
store.extend(e.as_bytes());
}
pad_to_block_size(&mut store, block_size);
// Partition store
for p in partitions.values() {
match p {
BackingStore::Data(d) => add_blocks(&mut store, d, block_size),
BackingStore::Size(s) => pad_bytes(&mut store, *s, block_size),
};
}
// Backup entries
let backup_entries_block = store.len() / block_size;
for e in entries {
store.extend(e.as_bytes());
}
pad_to_block_size(&mut store, block_size);
// Tweak header to make it the backup.
header.current = header.backup;
header.backup = 1;
header.entries = backup_entries_block.try_into().unwrap();
header.update_crc();
add_blocks(&mut store, header.as_bytes(), block_size);
store
}
/// Simple RAM based multi-block device used for unit tests.
pub struct TestMultiBlockDevices(pub Vec<TestBlockDevice>);
impl TestMultiBlockDevices {
/// Creates a vector of `AsyncGptDevice`;
pub fn as_gpt_devs(&mut self) -> Vec<AsyncGptDevice<&mut TestBlockIo>> {
self.0.iter_mut().map(|v| v.as_gpt_dev()).collect::<Vec<_>>()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_builder_partitions() {
let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let mut actual: [u8; 8] = Default::default();
let mut block_dev = TestBlockDeviceBuilder::new()
.add_partition("squid", BackingStore::Data(&data))
.add_partition("clam", BackingStore::Size(28))
.build();
block_dev.sync_gpt().unwrap();
assert!(block_dev.read_gpt_partition("squid", 0, actual.as_mut_slice()).is_ok());
assert_eq!(actual, data);
assert!(block_dev.read_gpt_partition("clam", 0, actual.as_mut_slice()).is_ok());
assert_eq!(actual, [0u8; 8]);
}
}