blob: 25a62191354b5e24401bcc7f04de1ecbcbf96f92 [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.
use core::cell::RefCell;
use crc32fast::Hasher;
pub use gbl_storage::{
alignment_scratch_size, is_aligned, is_buffer_aligned, required_scratch_size, AsBlockDevice,
AsMultiBlockDevices, BlockDeviceEx, BlockInfo, BlockIo, BlockIoError, GptEntry, GptHeader,
IoStatus, NonBlockingBlockIo, GPT_MAGIC, GPT_NAME_LEN_U16,
};
use safemath::SafeNum;
use std::collections::BTreeMap;
use zerocopy::AsBytes;
// Declares a per-thread global instance of timestamp. The timestamp is used to control the
// execution of non-blocking IO.
thread_local! {
static TIMESTAMP: RefCell<u64> = RefCell::new(Default::default());
/// Number of `TimestampPauser` in effect.
static NUM_TIMESTAMP_PAUSER: RefCell<u64> = RefCell::new(Default::default());
}
/// Increases the value of timestamp.
pub fn advance_timestamp() {
TIMESTAMP.with(|ts| (*ts.borrow_mut()) += 1);
}
/// Queries the current value of timestamp.
pub fn query_timestamp() -> u64 {
NUM_TIMESTAMP_PAUSER.with(|v| (*v.borrow() == 0).then(|| advance_timestamp()));
TIMESTAMP.with(|ts| *ts.borrow())
}
/// When a `TimestampPauser` is in scope, timestamp will not be increased by query.
pub struct TimestampPauser {}
impl Drop for TimestampPauser {
fn drop(&mut self) {
NUM_TIMESTAMP_PAUSER.with(|v| *v.borrow_mut() -= 1);
}
}
impl TimestampPauser {
/// Creates a new instance to pause the timestamp.
pub fn new() -> Self {
NUM_TIMESTAMP_PAUSER.with(|v| *v.borrow_mut() += 1);
Self {}
}
/// Consumes the pauser, causing it to go out of scope. When all pausers go out of scope,
/// timestamp resumes.
pub fn resume(self) {}
}
/// `NonBlockingIoState` tracks the non-blocking IO state.
enum NonBlockingIoState {
// (timestamp when initiated, blk offset, buffer, is read)
Pending(u64, u64, &'static mut [u8], bool),
Ready(IoStatus),
}
/// Helper `gbl_storage::BlockIo` 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,
/// Pending non-blocking IO
io: Option<NonBlockingIoState>,
}
impl 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, io: None }
}
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 BlockIo for TestBlockIo {
fn info(&mut self) -> BlockInfo {
NonBlockingBlockIo::info(self)
}
fn read_blocks(&mut self, blk_offset: u64, out: &mut [u8]) -> Result<(), BlockIoError> {
// `BlockIo` is implemented for `&mut dyn NonBlockingBlockIo`
BlockIo::read_blocks(&mut (self as &mut dyn NonBlockingBlockIo), blk_offset, out)
}
fn write_blocks(&mut self, blk_offset: u64, data: &mut [u8]) -> Result<(), BlockIoError> {
BlockIo::write_blocks(&mut (self as &mut dyn NonBlockingBlockIo), blk_offset, data)
}
}
// SAFETY:
// * When `TestBlockIo::io` is `Some(NonBlockingIoState(Pending(_, _, buffer, _)))`,
// `check_status()` always returns `IoStatus::Pending`. `check_status()` returns other `IoStatus`
// values if and only if `TestBlockIo::io` is not `Some(NonBlockingIoState(Pending())`, in which
// case the buffer is not tracked anymore and thus will not be retained again.
// * `Self::check_status()` does not dereference the input pointer.
// * `TestBlockIo::io` is set to `Some(NonBlockingIoState(Pending(_, _, buffer, _)))` and retains
// the buffer only on success (returning Ok(())).
unsafe impl NonBlockingBlockIo 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,
}
}
unsafe fn write_blocks(
&mut self,
blk_offset: u64,
buffer: *mut [u8],
) -> core::result::Result<(), BlockIoError> {
match self.io {
Some(_) => Err(BlockIoError::MediaBusy),
_ => {
self.num_writes += 1;
// SAFETY: By safety requirement, trait implementation can retain the buffer until
// it no longer returns `IoStatus::Pending` in `Self::check_status()`.
let buffer = unsafe { &mut *buffer };
assert!(self.check_alignment(buffer));
self.io =
Some(NonBlockingIoState::Pending(query_timestamp(), blk_offset, buffer, false));
Ok(())
}
}
}
unsafe fn read_blocks(
&mut self,
blk_offset: u64,
buffer: *mut [u8],
) -> core::result::Result<(), BlockIoError> {
match self.io {
Some(_) => Err(BlockIoError::MediaBusy),
_ => {
self.num_reads += 1;
// SAFETY: By safety requirement, trait implementation can retain the buffer until
// it no longer returns `IoStatus::Pending` in `Self::check_status()`.
let buffer = unsafe { &mut *buffer };
assert!(self.check_alignment(buffer));
self.io =
Some(NonBlockingIoState::Pending(query_timestamp(), blk_offset, buffer, true));
Ok(())
}
}
}
fn check_status(&mut self, buf: *mut [u8]) -> IoStatus {
match self.io.as_mut() {
Some(NonBlockingIoState::Pending(ts, blk_offset, ref mut buffer, is_read))
if std::ptr::eq(*buffer as *const [u8], buf as _) =>
{
// Executes the IO if current timestamp is newer.
if query_timestamp() > *ts {
let offset = (SafeNum::from(*blk_offset) * self.block_size).try_into().unwrap();
match is_read {
true => buffer.clone_from_slice(&self.storage[offset..][..buffer.len()]),
_ => self.storage[offset..][..buffer.len()].clone_from_slice(buffer),
}
self.io = Some(NonBlockingIoState::Ready(IoStatus::Completed));
}
IoStatus::Pending
}
Some(NonBlockingIoState::Ready(v)) => {
let res = *v;
self.io.take();
res
}
_ => IoStatus::NotFound,
}
}
fn abort(&mut self) -> core::result::Result<(), BlockIoError> {
match self.io {
Some(NonBlockingIoState::Pending(_, _, _, _)) => {
self.io = Some(NonBlockingIoState::Ready(IoStatus::Aborted));
}
_ => {}
}
Ok(())
}
}
/// Simple RAM based block device used by unit tests.
pub struct TestBlockDevice {
/// The BlockIo helper struct.
pub io: TestBlockIo,
/// In-memory backing store.
pub scratch: Vec<u8>,
max_gpt_entries: u64,
}
impl TestBlockDevice {
pub fn as_block_device_ex(&mut self) -> BlockDeviceEx {
BlockDeviceEx::new((&mut self.io as &mut dyn NonBlockingBlockIo).into())
}
}
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 BlockIo, &mut [u8], u64)) {
f(&mut self.io, &mut self.scratch[..], self.max_gpt_entries)
}
}
impl Default for TestBlockDevice {
fn default() -> Self {
TestBlockDeviceBuilder::new().build()
}
}
/// 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> {
Data(&'a [u8]),
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!(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(&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 = ((SafeNum::from(partitions.len()) * std::mem::size_of::<GptEntry>())
.round_up(block_size)
/ block_size)
.try_into()
.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: std::mem::size_of::<GptHeader>() as u32,
first: block,
entries: 2,
entries_count: std::cmp::min(partitions.len(), gpt_max_entries) as u32,
entries_size: std::mem::size_of::<GptEntry>() as u32,
..Default::default()
};
// Define gpt entry structures
let entries: Vec<GptEntry> = partitions
.iter()
.take(gpt_max_entries)
.map(|(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: Default::default(),
guid: Default::default(),
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
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 AsMultiBlockDevices for TestMultiBlockDevices {
fn for_each(
&mut self,
f: &mut dyn FnMut(&mut dyn AsBlockDevice, u64),
) -> core::result::Result<(), Option<&'static str>> {
let _ = self
.0
.iter_mut()
.enumerate()
.for_each(|(idx, ele)| f(ele, u64::try_from(idx).unwrap()));
Ok(())
}
}
#[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();
assert!(block_dev.sync_gpt().is_ok());
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]);
}
}