blob: aa8c579af4386e28640faa450aeed7041cc78c85 [file] [log] [blame]
// Copyright 2023, The Android Open Source Project
//
// 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.
//! Provides `avb::Ops` test fixtures.
use avb::{IoError, IoResult, Ops, PublicKeyForPartitionInfo};
use std::{cmp::min, collections::HashMap, ffi::CStr};
#[cfg(feature = "uuid")]
use uuid::Uuid;
/// Represents a single fake partition.
#[derive(Default)]
pub struct FakePartition {
/// Partition contents.
pub contents: Vec<u8>,
/// Whether the partition should report as preloaded or not.
pub preloaded: bool,
/// Partition UUID.
#[cfg(feature = "uuid")]
pub uuid: Uuid,
}
/// Fake vbmeta key state.
pub struct FakeVbmetaKeyState {
/// Key trust & rollback index info.
pub info: PublicKeyForPartitionInfo,
/// If specified, indicates the specific partition this vbmeta is tied to (for
/// `validate_public_key_for_partition()`).
pub for_partition: Option<&'static str>,
}
/// Fake `Ops` test fixture.
///
/// The user is expected to set up the internal values to the desired device state - disk contents,
/// rollback indices, etc. This class then uses this state to implement the avb callback operations.
pub struct TestOps {
/// Partitions to provide to libavb callbacks.
pub partitions: HashMap<&'static str, FakePartition>,
/// Vbmeta public keys as a map of {(key, metadata): state}. Querying unknown keys will
/// return `IoError::Io`.
///
/// See `add_vbmeta_key*()` functions for simpler wrappers to inject these keys.
pub vbmeta_keys: HashMap<(Vec<u8>, Option<Vec<u8>>), FakeVbmetaKeyState>,
/// Rollback indices. Accessing unknown locations will return `IoError::Io`.
pub rollbacks: HashMap<usize, u64>,
/// Unlock state. Set an error to simulate IoError during access.
pub unlock_state: IoResult<bool>,
/// Persistent named values. Set an error to simulate `IoError` during access. Writing
/// a non-existent persistent value will create it; to simulate `NoSuchValue` instead,
/// create an entry with `Err(IoError::NoSuchValue)` as the value.
pub persistent_values: HashMap<String, IoResult<Vec<u8>>>,
}
impl TestOps {
/// Adds a partition with the given contents.
///
/// Reduces boilerplate a bit by taking in a raw array and returning a &mut so tests can
/// do something like this:
///
/// ```
/// test_ops.add_partition("foo", [1, 2, 3, 4]);
/// test_ops.add_partition("bar", [0, 0]).preloaded = true;
/// ```
pub fn add_partition<T: Into<Vec<u8>>>(
&mut self,
name: &'static str,
contents: T,
) -> &mut FakePartition {
self.partitions.insert(
name,
FakePartition {
contents: contents.into(),
..Default::default()
},
);
self.partitions.get_mut(name).unwrap()
}
/// Adds a persistent value with the given state.
///
/// Reduces boilerplate by allowing array input:
///
/// ```
/// test_ops.add_persistent_value("foo", Ok(b"contents"));
/// test_ops.add_persistent_value("bar", Err(IoError::NoSuchValue));
/// ```
pub fn add_persistent_value(&mut self, name: &str, contents: IoResult<&[u8]>) {
self.persistent_values
.insert(name.into(), contents.map(|b| b.into()));
}
/// Adds a fake vbmeta key not tied to any partition.
pub fn add_vbmeta_key(&mut self, key: Vec<u8>, metadata: Option<Vec<u8>>, trusted: bool) {
self.vbmeta_keys.insert(
(key, metadata),
FakeVbmetaKeyState {
// `rollback_index_location` doesn't matter in this case, it will be read from
// the vbmeta blob.
info: PublicKeyForPartitionInfo {
trusted,
rollback_index_location: 0,
},
for_partition: None,
},
);
}
/// Adds a fake vbmeta key tied to the given partition and rollback index location.
pub fn add_vbmeta_key_for_partition(
&mut self,
key: Vec<u8>,
metadata: Option<Vec<u8>>,
trusted: bool,
partition: &'static str,
rollback_index_location: u32,
) {
self.vbmeta_keys.insert(
(key, metadata),
FakeVbmetaKeyState {
info: PublicKeyForPartitionInfo {
trusted,
rollback_index_location,
},
for_partition: Some(partition),
},
);
}
}
impl Default for TestOps {
fn default() -> Self {
Self {
partitions: HashMap::new(),
vbmeta_keys: HashMap::new(),
rollbacks: HashMap::new(),
unlock_state: Err(IoError::Io),
persistent_values: HashMap::new(),
}
}
}
impl Ops for TestOps {
fn read_from_partition(
&mut self,
partition: &CStr,
offset: i64,
buffer: &mut [u8],
) -> IoResult<usize> {
let partition = self
.partitions
.get(partition.to_str()?)
.ok_or(IoError::NoSuchPartition)?;
// We should never be trying to read a preloaded partition from disk since we already
// have it available in memory.
assert!(!partition.preloaded);
let contents = &partition.contents;
// Negative offset means count backwards from the end.
let offset = {
if offset < 0 {
offset
.checked_add(i64::try_from(contents.len()).unwrap())
.unwrap()
} else {
offset
}
};
if offset < 0 {
return Err(IoError::RangeOutsidePartition);
}
let offset = usize::try_from(offset).unwrap();
if offset >= contents.len() {
return Err(IoError::RangeOutsidePartition);
}
// Truncating is allowed for reads past the partition end.
let end = min(offset.checked_add(buffer.len()).unwrap(), contents.len());
let bytes_read = end - offset;
buffer[..bytes_read].copy_from_slice(&contents[offset..end]);
Ok(bytes_read)
}
fn get_preloaded_partition(&mut self, partition: &CStr) -> IoResult<&[u8]> {
match self.partitions.get(partition.to_str()?) {
Some(FakePartition {
contents,
preloaded: true,
..
}) => Ok(&contents[..]),
_ => Err(IoError::NotImplemented),
}
}
fn validate_vbmeta_public_key(
&mut self,
public_key: &[u8],
public_key_metadata: Option<&[u8]>,
) -> IoResult<bool> {
self.vbmeta_keys
// The compiler can't match (&[u8], Option<&[u8]>) to keys of type
// (Vec<u8>, Option<Vec<u8>>) so we turn the &[u8] into vectors here. This is a bit
// inefficient, but it's simple which is more important for tests than efficiency.
.get(&(public_key.to_vec(), public_key_metadata.map(|m| m.to_vec())))
.ok_or(IoError::Io)
.map(|k| k.info.trusted)
}
fn read_rollback_index(&mut self, location: usize) -> IoResult<u64> {
self.rollbacks.get(&location).ok_or(IoError::Io).copied()
}
fn write_rollback_index(&mut self, location: usize, index: u64) -> IoResult<()> {
*(self.rollbacks.get_mut(&location).ok_or(IoError::Io)?) = index;
Ok(())
}
fn read_is_device_unlocked(&mut self) -> IoResult<bool> {
self.unlock_state.clone()
}
#[cfg(feature = "uuid")]
fn get_unique_guid_for_partition(&mut self, partition: &CStr) -> IoResult<Uuid> {
self.partitions
.get(partition.to_str()?)
.map(|p| p.uuid)
.ok_or(IoError::NoSuchPartition)
}
fn get_size_of_partition(&mut self, partition: &CStr) -> IoResult<u64> {
self.partitions
.get(partition.to_str()?)
.map(|p| u64::try_from(p.contents.len()).unwrap())
.ok_or(IoError::NoSuchPartition)
}
fn read_persistent_value(&mut self, name: &CStr, value: &mut [u8]) -> IoResult<usize> {
match self
.persistent_values
.get(name.to_str()?)
.ok_or(IoError::NoSuchValue)?
{
// If we were given enough space, write the value contents.
Ok(contents) if contents.len() <= value.len() => {
value[..contents.len()].clone_from_slice(contents);
Ok(contents.len())
}
// Not enough space, tell the caller how much we need.
Ok(contents) => Err(IoError::InsufficientSpace(contents.len())),
// Simulated error, return it.
Err(e) => Err(e.clone()),
}
}
fn write_persistent_value(&mut self, name: &CStr, value: &[u8]) -> IoResult<()> {
let name = name.to_str()?;
// If the test requested a simulated error on this value, return it.
if let Some(Err(e)) = self.persistent_values.get(name) {
return Err(e.clone());
}
self.persistent_values
.insert(name.to_string(), Ok(value.to_vec()));
Ok(())
}
fn erase_persistent_value(&mut self, name: &CStr) -> IoResult<()> {
let name = name.to_str()?;
// If the test requested a simulated error on this value, return it.
if let Some(Err(e)) = self.persistent_values.get(name) {
return Err(e.clone());
}
self.persistent_values.remove(name);
Ok(())
}
fn validate_public_key_for_partition(
&mut self,
partition: &CStr,
public_key: &[u8],
public_key_metadata: Option<&[u8]>,
) -> IoResult<PublicKeyForPartitionInfo> {
let key = self
.vbmeta_keys
.get(&(public_key.to_vec(), public_key_metadata.map(|m| m.to_vec())))
.ok_or(IoError::Io)?;
if let Some(for_partition) = key.for_partition {
if (for_partition == partition.to_str()?) {
// The key is registered for this partition; return its info.
return Ok(key.info);
}
}
// No match.
Err(IoError::Io)
}
}