pub mod bootfs;
use bootfs::{
zbi_bootfs_dirent_t, zbi_bootfs_header_t, ZBI_BOOTFS_MAGIC, ZBI_BOOTFS_MAX_NAME_LEN,
ZBI_BOOTFS_PAGE_SIZE,
};
use byteorder::{ByteOrder, LittleEndian};
use std::ffi::CStr;
use std::mem::size_of;
use std::str::Utf8Error;
use thiserror::Error;
use zerocopy::{Ref, SplitByteSlice};
const ZBI_BOOTFS_DIRENT_SIZE: usize = size_of::<zbi_bootfs_dirent_t>();
const ZBI_BOOTFS_HEADER_SIZE: usize = size_of::<zbi_bootfs_header_t>();
fn zbi_bootfs_dirent_size(name_len: u32) -> u32 {
(ZBI_BOOTFS_DIRENT_SIZE as u32 + name_len + 3) & !3u32
}
pub fn zbi_bootfs_page_align(size: u32) -> u32 {
size.wrapping_add(ZBI_BOOTFS_PAGE_SIZE - 1) & !(ZBI_BOOTFS_PAGE_SIZE - 1)
}
pub fn zbi_bootfs_is_aligned(size: u32) -> bool {
size % ZBI_BOOTFS_PAGE_SIZE == 0
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum BootfsParserError {
#[error("Invalid magic for bootfs payload")]
BadMagic,
#[error("Directory entry {} exceeds available dirsize of {}", entry_index, dirsize)]
DirEntryTooBig { entry_index: u32, dirsize: u32 },
#[error("Failed to read payload: {}", status)]
FailedToReadPayload { status: zx::Status },
#[error("Failed to parse bootfs header")]
FailedToParseHeader,
#[error("Failed to parse directory entry")]
FailedToParseDirEntry,
#[error("Failed to read name as UTF-8: {}", cause)]
InvalidNameFormat {
#[source]
cause: Utf8Error,
},
#[error("Failed to find null terminated string for name: {}", cause)]
InvalidNameString {
#[source]
cause: std::ffi::FromBytesWithNulError,
},
#[error(
"name_len must be between 1 and {}, found {} for directory entry {}",
max_name_len,
name_len,
entry_index
)]
InvalidNameLength { name_len: u32, max_name_len: u32, entry_index: u32 },
}
#[derive(Debug)]
struct ZbiBootfsDirent<B: SplitByteSlice> {
header: Ref<B, zbi_bootfs_dirent_t>,
name_bytes: B,
}
impl<B: SplitByteSlice> ZbiBootfsDirent<B> {
pub fn parse(bytes: B) -> Result<ZbiBootfsDirent<B>, BootfsParserError> {
let (header, name_bytes) = Ref::<B, zbi_bootfs_dirent_t>::from_prefix(bytes)
.map_err(Into::into)
.map_err(|_: zerocopy::SizeError<_, _>| BootfsParserError::FailedToParseDirEntry)?;
Ok(ZbiBootfsDirent { header, name_bytes })
}
pub fn data_len(&self) -> u32 {
return self.header.data_len.get();
}
pub fn data_off(&self) -> u32 {
return self.header.data_off.get();
}
pub fn name(&self) -> Result<&str, BootfsParserError> {
match CStr::from_bytes_with_nul(&self.name_bytes[..self.header.name_len.get() as usize]) {
Ok(bytes) => {
bytes.to_str().map_err(|cause| BootfsParserError::InvalidNameFormat { cause })
}
Err(cause) => Err(BootfsParserError::InvalidNameString { cause }),
}
}
}
#[derive(Debug)]
pub struct BootfsParser {
pub(self) dirsize: u32,
pub(self) vmo: zx::Vmo,
}
impl BootfsParser {
pub fn create_from_vmo(vmo: zx::Vmo) -> Result<BootfsParser, BootfsParserError> {
let mut header_bytes = [0; ZBI_BOOTFS_HEADER_SIZE];
vmo.read(&mut header_bytes, 0)
.map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
let header = Ref::<_, zbi_bootfs_header_t>::from_bytes(&header_bytes[..])
.map_err(Into::into)
.map_err(|_: zerocopy::SizeError<_, _>| BootfsParserError::FailedToParseHeader)?;
if header.magic.get() == ZBI_BOOTFS_MAGIC {
Ok(Self { vmo, dirsize: header.dirsize.get() })
} else {
Err(BootfsParserError::BadMagic)
}
}
pub fn iter(&self) -> impl Iterator<Item = Result<BootfsEntry, BootfsParserError>> + '_ {
BootfsParserIterator::new(&self, false)
}
pub fn zero_copy_iter(
&self,
) -> impl Iterator<Item = Result<BootfsEntry, BootfsParserError>> + '_ {
BootfsParserIterator::new(&self, true)
}
}
#[derive(Debug)]
pub struct BootfsEntry {
pub name: String,
pub offset: u64,
pub size: u64,
pub payload: Option<Vec<u8>>,
}
#[derive(Debug)]
struct BootfsParserIterator<'parser> {
available_dirsize: u32,
dir_offset: u32,
entry_index: u32,
errored: bool,
zero_copy: bool,
parser: &'parser BootfsParser,
}
impl<'parser> BootfsParserIterator<'parser> {
pub fn new(parser: &'parser BootfsParser, zero_copy: bool) -> Self {
Self {
available_dirsize: parser.dirsize,
dir_offset: ZBI_BOOTFS_HEADER_SIZE as u32,
entry_index: 0,
errored: false,
zero_copy,
parser,
}
}
}
impl<'parser> Iterator for BootfsParserIterator<'parser> {
type Item = Result<BootfsEntry, BootfsParserError>;
fn next(&mut self) -> Option<Self::Item> {
if self.available_dirsize <= ZBI_BOOTFS_DIRENT_SIZE as u32 || self.errored {
return None;
}
let mut name_len_buf = [0; size_of::<u32>()];
if let Err(status) = self.parser.vmo.read(&mut name_len_buf, self.dir_offset.into()) {
self.errored = true;
return Some(Err(BootfsParserError::FailedToReadPayload { status }));
}
let name_len = LittleEndian::read_u32(&name_len_buf);
if name_len < 1 || name_len > ZBI_BOOTFS_MAX_NAME_LEN {
self.errored = true;
return Some(Err(BootfsParserError::InvalidNameLength {
entry_index: self.entry_index,
max_name_len: ZBI_BOOTFS_MAX_NAME_LEN,
name_len,
}));
}
let dirent_size = zbi_bootfs_dirent_size(name_len);
if dirent_size > self.available_dirsize {
self.errored = true;
return Some(Err(BootfsParserError::DirEntryTooBig {
dirsize: self.available_dirsize,
entry_index: self.entry_index,
}));
}
let mut dirent_buffer = vec![0; dirent_size as usize];
if let Err(status) = self.parser.vmo.read(&mut dirent_buffer, self.dir_offset.into()) {
self.errored = true;
return Some(Err(BootfsParserError::FailedToReadPayload { status }));
}
match ZbiBootfsDirent::parse(&dirent_buffer[..]) {
Ok(dirent) => {
let mut payload = None;
let offset: u64 = zbi_bootfs_page_align(dirent.data_off()).into();
let size: u64 = dirent.data_len().into();
if !self.zero_copy {
let buffer_size = usize::try_from(size).unwrap_or_else(|_| {
self.errored = true;
return 0;
});
let mut buffer = vec![0; buffer_size];
if let Err(status) = self.parser.vmo.read(&mut buffer, offset) {
self.errored = true;
return Some(Err(BootfsParserError::FailedToReadPayload { status }));
}
payload = Some(buffer);
}
self.dir_offset += dirent_buffer.len() as u32;
self.available_dirsize -= dirent_size;
self.entry_index += 1;
Some(dirent.name().map(|name| BootfsEntry {
name: name.to_owned(),
offset,
size,
payload,
}))
}
Err(err) => {
self.errored = true;
Some(Err(err))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Error;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use zx::HandleBased;
static GOLDEN_DIR: &str = "/pkg/data/golden/";
static BASIC_BOOTFS_UNCOMPRESSED_FILE: &str = "/pkg/data/basic.bootfs.uncompressed";
fn read_file_into_hashmap(dir: &str, filename: &str, map: &mut HashMap<String, Vec<u8>>) {
let mut file_buffer = Vec::new();
let path = format!("{}{}", dir, filename);
File::open(&path)
.unwrap_or_else(|e| panic!("Failed to open file {}: {:?}", &path, e))
.read_to_end(&mut file_buffer)
.unwrap_or_else(|e| panic!("Failed to read file {}: {:?}", &path, e));
map.insert(filename.to_string(), file_buffer);
}
lazy_static! {
static ref GOLDEN_FILES: HashMap<String, Vec<u8>> = {
let mut m = HashMap::new();
read_file_into_hashmap(GOLDEN_DIR, "dir/empty", &mut m);
read_file_into_hashmap(GOLDEN_DIR, "dir/lorem.txt", &mut m);
read_file_into_hashmap(GOLDEN_DIR, "dir/simple-copy.txt", &mut m);
read_file_into_hashmap(GOLDEN_DIR, "empty", &mut m);
read_file_into_hashmap(GOLDEN_DIR, "random.dat", &mut m);
read_file_into_hashmap(GOLDEN_DIR, "simple.txt", &mut m);
m
};
}
fn read_file_to_vmo(path: &str) -> Result<zx::Vmo, Error> {
let mut file_buffer = Vec::new();
File::open(path)?.read_to_end(&mut file_buffer)?;
let vmo = zx::Vmo::create(file_buffer.len() as u64)?;
vmo.write(&file_buffer, 0)?;
Ok(vmo)
}
#[test]
fn dirent_from_raw_fails_on_bad_cstring() {
const NAME_LEN: u8 = 3;
let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
dirent_buf[0] = NAME_LEN;
dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 'o' as u8;
dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 'k' as u8;
dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = 'a' as u8;
let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
.expect("Failed to create ZbiBootfsDirent from raw buffer");
match dirent.name().unwrap_err() {
BootfsParserError::InvalidNameString { cause: _cause } => (),
_ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
}
}
#[test]
fn dirent_from_raw_fails_on_non_utf8_string() {
const NAME_LEN: u8 = 3;
let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
dirent_buf[0] = NAME_LEN;
dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 0xC3;
dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 0x28;
dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = '\0' as u8;
let char_sequence = &dirent_buf[ZBI_BOOTFS_DIRENT_SIZE..dirent_buf.len()];
assert_eq!(true, String::from_utf8(char_sequence.to_vec()).is_err());
let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
.expect("Failed to create ZbiBootfsDirent from raw buffer");
match dirent.name().unwrap_err() {
BootfsParserError::InvalidNameFormat { cause: _cause } => (),
_ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
}
}
#[test]
fn create_bootfs_parser() {
let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
assert_eq!(true, BootfsParser::create_from_vmo(vmo).is_ok());
}
#[test]
fn process_basic_bootfs() {
let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to read bootfs file");
let mut files = Box::new(HashMap::new());
parser.iter().for_each(|result| {
let result = result.expect("Failed to process bootfs payload");
let BootfsEntry { name, payload, .. } = result;
files.insert(name, payload.unwrap());
});
assert_eq!(*GOLDEN_FILES, *files);
}
#[test]
fn process_bootfs_zero_copy() {
let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
let vmo_dup =
vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("Failed to duplicate vmo");
let mut files = Box::new(HashMap::new());
let parser = BootfsParser::create_from_vmo(vmo_dup).expect("Failed to read bootfs file");
parser.zero_copy_iter().for_each(|result| {
let result = result.expect("Failed to process bootfs payload");
let BootfsEntry { name, offset, size, payload } = result;
assert!(payload.is_none());
let buffer_size = usize::try_from(size).unwrap();
let mut bytes = vec![0; buffer_size];
vmo.read(&mut bytes, offset).expect("Failed to read data from the vmo");
files.insert(name, bytes);
});
assert_eq!(*GOLDEN_FILES, *files);
}
#[test]
fn process_bootfs_with_invalid_header() {
let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
let new_header = [0; ZBI_BOOTFS_HEADER_SIZE];
vmo.write(&new_header, 0).expect("Failed to wipe bootfs header");
match BootfsParser::create_from_vmo(vmo).unwrap_err() {
BootfsParserError::BadMagic => (),
_ => panic!("BootfsParser::create_from_vmo did not fail with correct error"),
}
}
#[test]
fn process_bootfs_with_invalid_direntry() {
let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
let new_header = [0; ZBI_BOOTFS_DIRENT_SIZE];
vmo.write(&new_header, ZBI_BOOTFS_HEADER_SIZE as u64).expect("Failed to wipe direntry");
let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
parser.iter().for_each(|result| match result.unwrap_err() {
BootfsParserError::InvalidNameLength { entry_index, max_name_len, name_len } => {
assert_eq!(0, entry_index);
assert_eq!(ZBI_BOOTFS_MAX_NAME_LEN, max_name_len);
assert_eq!(0, name_len);
}
_ => panic!("parser did not fail with correct error"),
});
}
#[test]
fn process_bootfs_undersized_dirsize() {
let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
let new_header = [(ZBI_BOOTFS_DIRENT_SIZE + 1) as u8, 0, 0, 0];
vmo.write(&new_header, size_of::<u32>() as u64).expect("Failed to change dirsize");
let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
parser.iter().for_each(|result| match result.unwrap_err() {
BootfsParserError::DirEntryTooBig { entry_index, dirsize } => {
assert_eq!(0, entry_index);
assert_eq!(ZBI_BOOTFS_DIRENT_SIZE + 1, dirsize as usize);
}
_ => panic!("parser did not fail with correct error"),
});
}
}