| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 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, |
| }, |
| byteorder::{ByteOrder, LittleEndian}, |
| fuchsia_zircon as zx, |
| std::{ffi::CStr, mem::size_of, str::Utf8Error}, |
| thiserror::Error, |
| zerocopy::{ByteSlice, Ref}, |
| }; |
| |
| const ZBI_BOOTFS_DIRENT_SIZE: usize = size_of::<zbi_bootfs_dirent_t>(); |
| const ZBI_BOOTFS_HEADER_SIZE: usize = size_of::<zbi_bootfs_header_t>(); |
| |
| // Each directory entry has a variable size of [16,268] bytes that |
| // must be a multiple of 4 bytes. |
| fn zbi_bootfs_dirent_size(name_len: u32) -> u32 { |
| (ZBI_BOOTFS_DIRENT_SIZE as u32 + name_len + 3) & !3u32 |
| } |
| |
| fn zbi_bootfs_page_align(size: u32) -> u32 { |
| size.wrapping_add(ZBI_BOOTFS_PAGE_SIZE - 1) & !(ZBI_BOOTFS_PAGE_SIZE - 1) |
| } |
| |
| #[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: ByteSlice> { |
| header: Ref<B, zbi_bootfs_dirent_t>, |
| name_bytes: B, |
| } |
| impl<B: ByteSlice> ZbiBootfsDirent<B> { |
| pub fn parse(bytes: B) -> Result<ZbiBootfsDirent<B>, BootfsParserError> { |
| let (header, name_bytes) = Ref::<B, zbi_bootfs_dirent_t>::new_unaligned_from_prefix(bytes) |
| .ok_or(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> { |
| // Name is stored as a array reference to a block of characters. |
| // Characters must be UTF-8 encoded. |
| // Valid names are terminated with NUL. |
| // We should fail if either of the above conditions are not met. |
| 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 }), |
| } |
| } |
| } |
| |
| /// Parser for bootfs-formatted structures. |
| #[derive(Debug)] |
| pub struct BootfsParser { |
| // Expose fields for BootfsParserIterator access. |
| pub(self) dirsize: u32, |
| pub(self) vmo: zx::Vmo, |
| } |
| impl BootfsParser { |
| /// Creates a BootfsParser from an existing VMO. |
| /// |
| /// If `vmo` contains invalid header data, BootfsParserError is returned. |
| 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>::new_unaligned(&header_bytes[..]) |
| .ok_or(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, |
| |
| // Not filled when doing a zero copy parse of bootfs. |
| 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; |
| } |
| |
| // Read the name_len field only. |
| 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, |
| })); |
| } |
| |
| // Now that we know how long the name is, read the whole entry. |
| 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) => { |
| // We have a directory entry now, so retrieve the payload. |
| 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::*, |
| anyhow::Error, |
| fuchsia_zircon::HandleBased, |
| lazy_static::lazy_static, |
| std::{collections::HashMap, fs::File, io::prelude::*}, |
| }; |
| |
| 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, "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; |
| // This should be NUL...but it's not for this test. |
| 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]; |
| |
| // This is an invalid UTF-8 sequence. |
| 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; |
| |
| // Assert that it actually IS an invalid UTF-8 string. |
| 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; |
| |
| // The zero_copy iterator doesn't make a copy of the files, but using the offset and |
| // size we can read it from the vmo. |
| 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]; |
| |
| // Wipe the header of a known good bootfs payload. |
| 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]; |
| |
| // Wipe the first direntry of a known good bootfs payload. |
| // The first direntry starts immediately after the header. |
| 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]; |
| |
| // Change dirsize to ZBI_BOOTFS_DIRENT_SIZE+1. |
| // It is the second u32 value in the zbi_bootfs_header_t struct. |
| 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"), |
| }); |
| } |
| } |