// Copyright 2023 The ChromiumOS Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //! fw_cfg device implementing QEMU's Firmware Configuration interface //! use std::collections::HashSet; use std::fs; use std::iter::repeat; use std::path::PathBuf; #[cfg(windows)] use base::error; use serde::Deserialize; use serde::Serialize; use serde_keyvalue::FromKeyValues; use thiserror::Error as ThisError; use crate::BusAccessInfo; use crate::BusDevice; use crate::DeviceId; use crate::Suspendable; pub const FW_CFG_BASE_PORT: u64 = 0x510; pub const FW_CFG_WIDTH: u64 = 0x4; // For the 16-bit selector, the 2nd highest-order bit represents whether the data port will be read // or written to. Because this has been deprecrated by Qemu, this bit is useless. The highest order // bit represents whether the selected configuration item is arch-specific. Therefore, only the // lower 14 bits are used for indexing and we mask the two highest bits off with // FW_CFG_SELECTOR_SELECT_MASK. 16384 = 2^14. pub const FW_CFG_MAX_FILE_SLOTS: usize = 16384 - FW_CFG_FILE_FIRST; const FW_CFG_FILE_FIRST: usize = 0x0020; const FW_CFG_SELECTOR_PORT_OFFSET: u64 = 0x0; const FW_CFG_DATA_PORT_OFFSET: u64 = 0x1; const FW_CFG_SELECTOR_RW_MASK: u16 = 0x2000; const FW_CFG_SELECTOR_ARCH_MASK: u16 = 0x4000; const FW_CFG_SELECTOR_SELECT_MASK: u16 = 0xbfff; const FW_CFG_SIGNATURE: [u8; 4] = [b'Q', b'E', b'M', b'U']; const FW_CFG_REVISION: [u8; 4] = [0, 0, 0, 1]; const FW_CFG_SIGNATURE_SELECTOR: u16 = 0x0000; const FW_CFG_REVISION_SELECTOR: u16 = 0x0001; const FW_CFG_FILE_DIR_SELECTOR: u16 = 0x0019; // Code that uses fw_cfg expects to read a char[56] for filenames const FW_CFG_FILENAME_SIZE: usize = 56; #[derive(ThisError, Debug)] pub enum Error { #[error("Ran out of file slots")] InsufficientFileSlots, #[error("File already exists")] FileAlreadyExists, #[error("Data blob's size too large: overflowed u32")] SizeOverflow, #[error("too many entries: oveflows u16 selector")] IndexOverflow, #[error("Filename must be less than 55 characters long")] FileNameTooLong, #[error("Unable to open file {0} for fw_cfg: {1}")] FileOpen(PathBuf, std::io::Error), #[error("fw_cfg parameters must have exactly one of string or path")] StringOrPathRequired, } pub type Result = std::result::Result; #[derive(Clone, Debug, Deserialize, Serialize, FromKeyValues, PartialEq, Eq)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct FwCfgParameters { pub name: String, pub string: Option, pub path: Option, } #[derive(PartialEq)] pub enum FwCfgItemType { GenericItem, ArchSpecificItem, FileDir, Signature, RevisionVector, } impl FwCfgItemType { fn value(&self) -> usize { match self { FwCfgItemType::ArchSpecificItem => 1, _ => 0, } } } // Contains metadata about the entries stored in fw_cfg. // FwCfgFile is exposed to the the guest // so that the guest may search for the entry // with the desired filename and obtain its 16-bit-wide select // key to write to the control register struct FwCfgFile { pub size: u32, pub select: u16, pub name: String, } // Contains the actual data. The data is represented as an // array of u8 to conviently pass // a data item byte-by-byte when read() is called // on that item #[derive(Clone)] struct FwCfgEntry { pub allow_write: bool, pub data: Vec, } // Device exposed to the rest of crosvm. Contains state information in addition to arrays of // FwCfgEntry and FwCfgFile. cur_entry keeps the index of the currently selected entry. cur_offset // keeps the byte offset within cur_entry. Storing cur_offset is neccessary because the data IO port // is only 8 bits wide, so a call to read() will only retrieve one 8 bit chunk of data at a time. // cur_offset allows for a data item larger than 8 bits to be read through multiple calls to read(), // maintaining the position of the current read and incrementing across calls to read(). pub struct FwCfgDevice { file_slots: usize, // entries[0] holds generic fw_cfg items in addition to special items (file dir, signature, and // revision vector). entries[1] holds arch-specific items. entries: [Vec; 2], files: Vec, cur_item_type: FwCfgItemType, cur_entry: u16, cur_offset: usize, file_names: HashSet, } impl FwCfgDevice { pub fn new(file_slots: usize, fw_cfg_parameters: Vec) -> Result { let mut device = FwCfgDevice { file_slots, entries: [ vec![ FwCfgEntry { allow_write: false, data: vec![] }; FW_CFG_FILE_FIRST ], Vec::new(), ], files: Vec::new(), cur_item_type: FwCfgItemType::GenericItem, cur_entry: 0, cur_offset: 0, file_names: HashSet::new(), }; for param in fw_cfg_parameters { let data = match (¶m.string, ¶m.path) { (Some(string), None) => string.as_bytes().to_vec(), (None, Some(path)) => { fs::read(path).map_err(|e| Error::FileOpen(path.clone(), e))? } _ => return Err(Error::StringOrPathRequired), }; // The file added from the command line will be a generic item. QEMU does not // give users the option to specify whether the user-specified blob is // arch-specific, so we won't either. device.add_file(¶m.name, data, FwCfgItemType::GenericItem)? } device.add_bytes(FW_CFG_SIGNATURE.to_vec(), FwCfgItemType::Signature); device.add_bytes(FW_CFG_REVISION.to_vec(), FwCfgItemType::RevisionVector); Ok(device) } /// Adds a file to the device. /// /// # Arguments /// /// - `filename`: Name of file. Must be valid a Unix-style filename /// - `data`: File data as bytes pub fn add_file( &mut self, filename: &str, data: Vec, item_type: FwCfgItemType, ) -> Result<()> { // Adds a data blob to the device under the name filename. This entails creating an // FwCfgEntry and its associated FwCfgFile and adding them to FwCfgDevice. if self.files.len() >= FW_CFG_MAX_FILE_SLOTS || self.files.len() >= self.file_slots { return Err(Error::InsufficientFileSlots); } if filename.len() > FW_CFG_FILENAME_SIZE - 1 { return Err(Error::FileNameTooLong); } // No need to worry about endianess in this function. We will deal with this in read(). We // are only using FwCfgFile internally. let index = self.entries[item_type.value()].len(); if self.file_names.contains(filename) { return Err(Error::FileAlreadyExists); } // Since the size field of an entry is stored as a u32, the largest file that can be stored // in the device is 2^32 - 1 ~ 4GB let size: u32 = data.len().try_into().map_err(|_| Error::SizeOverflow)?; let mut select: u16 = (index).try_into().map_err(|_| Error::IndexOverflow)?; if item_type == FwCfgItemType::ArchSpecificItem { select |= FW_CFG_SELECTOR_ARCH_MASK; } let new_file = FwCfgFile { size, select, name: filename.to_owned(), }; self.add_bytes(data, item_type); self.files.push(new_file); self.file_names.insert(filename.to_string()); // We need to update the file_dir entry every time we insert a new file. self.update_file_dir_entry(); Ok(()) } fn add_bytes(&mut self, data: Vec, item_type: FwCfgItemType) { // Add a FwCfgEntry to FwCfgDevice's entries array let new_entry = FwCfgEntry { allow_write: false, data, }; match item_type { FwCfgItemType::GenericItem | FwCfgItemType::ArchSpecificItem => { self.entries[item_type.value()].push(new_entry) } FwCfgItemType::FileDir => { self.entries[item_type.value()][FW_CFG_FILE_DIR_SELECTOR as usize] = new_entry } FwCfgItemType::Signature => { self.entries[item_type.value()][FW_CFG_SIGNATURE_SELECTOR as usize] = new_entry } FwCfgItemType::RevisionVector => { self.entries[item_type.value()][FW_CFG_REVISION_SELECTOR as usize] = new_entry } } } fn update_file_dir_entry(&mut self) { let mut raw_file_dir: Vec = Vec::new(); // casting to u32 should not be problematic. insert_file() assures that there can be no // more than 2^14 items in the device. let files_dir_count = self.files.len() as u32; raw_file_dir.extend_from_slice(&files_dir_count.to_be_bytes()); for file in &self.files { raw_file_dir.extend_from_slice(&file.size.to_be_bytes()); raw_file_dir.extend_from_slice(&file.select.to_be_bytes()); // The caller expects a "reserved" field to be present on each FwCfgFile. Since // we always set the field to zero, we don't bother to store it on FwCfgDevice and // return zero unconditionally. raw_file_dir.extend_from_slice(&[0, 0]); raw_file_dir.extend_from_slice(file.name.as_bytes()); // Padding for c-style char[] raw_file_dir.extend(repeat(0).take(FW_CFG_FILENAME_SIZE - file.name.as_bytes().len())); } self.add_bytes(raw_file_dir, FwCfgItemType::FileDir); } } // We implement two 8-bit registers: a Selector(Control) Register and a Data Register impl BusDevice for FwCfgDevice { fn device_id(&self) -> DeviceId { super::CrosvmDeviceId::FwCfg.into() } fn debug_label(&self) -> String { "FwCfg".to_owned() } // Read a byte from the FwCfgDevice. The byte read is based on the current state of the device. fn read(&mut self, info: BusAccessInfo, data: &mut [u8]) { if data.len() != 1 { return; } // Attemping to read anything other than the data port is a NOP if info.offset == FW_CFG_DATA_PORT_OFFSET { let entries_index = self.cur_entry as usize; // If the caller attempts to read bytes past the current entry, read returns // zero if self.cur_offset >= self.entries[self.cur_item_type.value()][entries_index] .data .len() { data[0] = 0x00; return; } data[0] = self.entries[self.cur_item_type.value()][entries_index].data[self.cur_offset]; self.cur_offset += 1; } } // Write to the FwCfgDevice. Used to set the select register. fn write(&mut self, info: BusAccessInfo, data: &[u8]) { // Attempting to write to any port other than the data port is a NOP if info.offset == FW_CFG_SELECTOR_PORT_OFFSET { if data.len() != 2 { return; } let Ok(selector) = data.try_into().map(u16::from_le_bytes) else { return; }; self.cur_offset = 0; match selector { FW_CFG_FILE_DIR_SELECTOR => { self.cur_entry = FW_CFG_FILE_DIR_SELECTOR; } FW_CFG_REVISION_SELECTOR => { self.cur_entry = FW_CFG_REVISION_SELECTOR; } FW_CFG_SIGNATURE_SELECTOR => { self.cur_entry = FW_CFG_SIGNATURE_SELECTOR; } _ => { let entries_index = selector as usize; // Checks if the 15th bit is set. The bit indicates whether the fw_cfg item // selected is archetecture specific. if (FW_CFG_SELECTOR_ARCH_MASK & selector) > 0 { self.cur_item_type = FwCfgItemType::ArchSpecificItem; } else { self.cur_item_type = FwCfgItemType::GenericItem; } // Check if the selector key is valid. if self.entries[self.cur_item_type.value()].len() <= entries_index { return; } // Checks if the 14th bit is set. The bit indicates whether the fw_cfg item // selected is going to be written to or only read via the data port. Since // writes to the data port have been deprecated as of Qemu v2.4, we don't // support them either. This code is only included for clarity. self.entries[self.cur_item_type.value()][entries_index].allow_write = (FW_CFG_SELECTOR_RW_MASK & selector) > 0; // Checks if the 15th bit is set. The bit indicates whether the fw_cfg item // selected is archetecture specific. if (FW_CFG_SELECTOR_ARCH_MASK & selector) > 0 { self.cur_item_type = FwCfgItemType::ArchSpecificItem; } else { self.cur_item_type = FwCfgItemType::GenericItem; } // Only the lower 14 bits are used for actual indexing. The 14th bit // determines whether the data item will be written to or only read // from the data port. The 15th bit determines whether the selected // configuration item is architecture specific. Therefore, we mask the 14th // and 15th bit off. self.cur_entry = selector & FW_CFG_SELECTOR_SELECT_MASK; } } } } } impl Suspendable for FwCfgDevice { fn sleep(&mut self) -> anyhow::Result<()> { Ok(()) } fn wake(&mut self) -> anyhow::Result<()> { Ok(()) } } #[cfg(test)] mod tests { use serde_keyvalue::*; use super::*; use crate::FW_CFG_BASE_PORT; const MAGIC_BYTE: u8 = 111; const MAGIC_BYTE_ALT: u8 = 222; const FILENAME: &str = "/test/device/crosvmval"; const FILENAMES: [&str; 6] = [ "test/hello.txt", "user/data/mydata.txt", "bruschetta/user/foo", "valid_unix/me/home/dir/back.txt", "/dev/null", "google/unix/sys/maple.txt", ]; fn default_params() -> Vec { Vec::new() } fn get_contents() -> [Vec; 6] { [ b"CROSVM".to_vec(), b"GOOGLE".to_vec(), b"FWCONFIG".to_vec(), b"PIZZA".to_vec(), b"CHROMEOS".to_vec(), b"42".to_vec(), ] } fn make_device( filenames: &[&str], contents: &[Vec], params: &[FwCfgParameters], file_slots: &usize, ) -> Result { let mut device = FwCfgDevice::new(*file_slots, params.to_owned())?; let count = filenames.len(); for i in 0..count { device.add_file( filenames[i], contents[i].clone(), FwCfgItemType::GenericItem, )?; } Ok(device) } fn from_fw_cfg_arg(options: &str) -> std::result::Result { from_key_values(options) } fn read_u32(device: &mut FwCfgDevice, bai: BusAccessInfo, data: &mut [u8]) -> u32 { let mut bytes: [u8; 4] = [0, 0, 0, 0]; device.read(bai, data); bytes[0] = data[0]; device.read(bai, data); bytes[1] = data[0]; device.read(bai, data); bytes[2] = data[0]; device.read(bai, data); bytes[3] = data[0]; u32::from_be_bytes(bytes) } fn read_u16(device: &mut FwCfgDevice, bai: BusAccessInfo, data: &mut [u8]) -> u16 { let mut bytes: [u8; 2] = [0, 0]; device.read(bai, data); bytes[0] = data[0]; device.read(bai, data); bytes[1] = data[0]; u16::from_be_bytes(bytes) } fn read_u8(device: &mut FwCfgDevice, bai: BusAccessInfo, data: &mut [u8]) -> u8 { let mut bytes: [u8; 1] = [0]; device.read(bai, data); bytes[0] = data[0]; u8::from_be_bytes(bytes) } fn read_char_56(device: &mut FwCfgDevice, bai: BusAccessInfo, data: &mut [u8]) -> String { let mut c: char = read_u8(device, bai, data) as char; let mut count = 1; //start at 1 b/c called read_u8 above let mut name: String = String::new(); while c != '\0' { name.push(c); c = read_u8(device, bai, data) as char; count += 1; } while count < FW_CFG_FILENAME_SIZE { read_u8(device, bai, data); count += 1; } name } fn get_entry( device: &mut FwCfgDevice, mut bai: BusAccessInfo, size: usize, selector: u16, ) -> Vec { let mut data: Vec = vec![0]; let mut blob: Vec = Vec::new(); bai.address = FW_CFG_BASE_PORT; bai.offset = FW_CFG_SELECTOR_PORT_OFFSET; let selector: [u8; 2] = selector.to_le_bytes(); device.write(bai, &selector); bai.offset = FW_CFG_DATA_PORT_OFFSET; for _i in 0..size { read_u8(device, bai, &mut data[..]); blob.push(data[0]); } blob } fn assert_read_entries(filenames: &[&str], device: &mut FwCfgDevice, bai: BusAccessInfo) { let data_len = device.entries[0][0 + FW_CFG_FILE_FIRST].data.len(); assert_eq!( get_entry(device, bai, data_len, (0 + FW_CFG_FILE_FIRST) as u16), device.entries[0][0 + FW_CFG_FILE_FIRST].data ); for i in (FW_CFG_FILE_FIRST + 1)..filenames.len() { let data_len = device.entries[0][i].data.len(); assert_eq!( get_entry(device, bai, data_len, (i + FW_CFG_FILE_FIRST) as u16), device.entries[0][i].data ); } } fn assert_read_file_dir( filenames: &[&str], contents: &[Vec], device: &mut FwCfgDevice, bai: BusAccessInfo, ) { let mut data: Vec = vec![0]; let file_count = read_u32(device, bai, &mut data[..]); assert_eq!(file_count, filenames.len() as u32); for i in 0..filenames.len() { let file_size = read_u32(device, bai, &mut data[..]); assert_eq!(file_size, contents[i].len() as u32); let file_select = read_u16(device, bai, &mut data[..]); assert_eq!(file_select - (FW_CFG_FILE_FIRST as u16), i as u16); let file_reserved = read_u16(device, bai, &mut data[..]); assert_eq!(file_reserved, 0); let file_name = read_char_56(device, bai, &mut data[..]); assert_eq!(file_name, FILENAMES[i]); } } fn setup_read( filenames: &[&str], contents: &[Vec], selector: u16, ) -> (FwCfgDevice, BusAccessInfo) { let mut device = make_device( filenames, contents, &default_params(), &(filenames.len() + 5), ) .unwrap(); let mut bai = BusAccessInfo { offset: FW_CFG_SELECTOR_PORT_OFFSET, address: FW_CFG_BASE_PORT, id: 0, }; let selector: [u8; 2] = selector.to_le_bytes(); device.write(bai, &selector); bai.offset = FW_CFG_DATA_PORT_OFFSET; (device, bai) } #[test] // Attempt to build FwCfgParams from key value pairs fn params_from_key_values() { from_fw_cfg_arg("").expect_err("parsing empty string should fail"); let params = from_fw_cfg_arg("name=foo,path=/path/to/input").unwrap(); assert_eq!( params, FwCfgParameters { name: "foo".into(), path: Some("/path/to/input".into()), string: None, } ); let params = from_fw_cfg_arg("name=bar,string=testdata").unwrap(); assert_eq!( params, FwCfgParameters { name: "bar".into(), string: Some("testdata".into()), path: None, } ); } #[test] // Try to cause underflow by using a selector less than FW_CFG_FILE_FIRST but not one of the // special selectors fn attempt_underflow_read() { let (_device, _bai) = setup_read( &FILENAMES, &get_contents(), (FW_CFG_FILE_FIRST - 0x05) as u16, ); } #[test] // Write a simple one byte file and confirm that an entry is properly created fn write_one_byte_file() { let mut fw_cfg = FwCfgDevice::new(100, default_params()).unwrap(); let data = vec![MAGIC_BYTE]; fw_cfg .add_file(FILENAME, data, FwCfgItemType::GenericItem) .expect("File insert failed"); let ind = fw_cfg.entries[0].len(); assert_eq!( ind, FW_CFG_FILE_FIRST + 1, "Insertion into fw_cfg failed: Index is wrong. expected {}, got {}, ", FW_CFG_FILE_FIRST + 1, ind ); assert_eq!( fw_cfg.entries[0][ind - 1].data, vec![MAGIC_BYTE], "Insertion failed: unexpected fw_cfg entry values" ); } #[test] // Write a simple four byte file and confirm that an entry is properly created fn write_four_byte_file() { let mut fw_cfg = FwCfgDevice::new(100, default_params()).unwrap(); let data = vec![MAGIC_BYTE, MAGIC_BYTE_ALT, MAGIC_BYTE, MAGIC_BYTE_ALT]; fw_cfg .add_file(FILENAME, data, FwCfgItemType::GenericItem) .expect("File insert failed"); let ind = fw_cfg.entries[0].len(); assert_eq!( ind, FW_CFG_FILE_FIRST + 1, "Insertion into fw_cfg failed: Index is wrong. expected {}, got {}", FW_CFG_FILE_FIRST + 1, ind ); assert_eq!( fw_cfg.entries[0][ind - 1].data, vec![MAGIC_BYTE, MAGIC_BYTE_ALT, MAGIC_BYTE, MAGIC_BYTE_ALT], "Insertion failed: unexpected fw_cfg entry values" ); } #[test] #[should_panic] // Attempt to add a file to an fw_cfg device w/ no fileslots and assert that nothing gets // inserted fn write_file_one_slot_expect_nop() { let mut fw_cfg = FwCfgDevice::new(0, default_params()).unwrap(); let data = vec![MAGIC_BYTE]; fw_cfg .add_file(FILENAME, data, FwCfgItemType::GenericItem) .expect("File insert failed"); } #[test] #[should_panic] // Attempt to add two files to an fw_cfg w/ only one fileslot and assert only first insert // succeeds. fn write_two_files_no_slots_expect_nop_on_second() { let mut fw_cfg = FwCfgDevice::new(1, default_params()).unwrap(); let data = vec![MAGIC_BYTE]; let data2 = vec![MAGIC_BYTE_ALT]; fw_cfg .add_file(FILENAME, data, FwCfgItemType::GenericItem) .expect("File insert failed"); assert_eq!( fw_cfg.entries[0].len(), 1, "Insertion into fw_cfg failed: Expected {} elements, got {}", 1, fw_cfg.entries[0].len() ); fw_cfg .add_file(FILENAME, data2, FwCfgItemType::GenericItem) .expect("File insert failed"); } #[test] // Attempt to read a FwCfgDevice's signature fn read_fw_cfg_signature() { let mut data: Vec = vec![0]; let (mut device, bai) = setup_read(&FILENAMES, &get_contents(), FW_CFG_SIGNATURE_SELECTOR); // To logically compare the revison vector to FW_CFG_REVISION byte-by-byte, we must use // to_be_bytes() since we are comparing byte arrays, not integers. let signature = read_u32(&mut device, bai, &mut data[..]).to_be_bytes(); assert_eq!(signature, FW_CFG_SIGNATURE); } #[test] // Attempt to read a FwCfgDevice's revision bit vector fn read_fw_cfg_revision() { let mut data: Vec = vec![0]; let (mut device, bai) = setup_read(&FILENAMES, &get_contents(), FW_CFG_REVISION_SELECTOR); // To logically compare the revison vector to FW_CFG_REVISION byte-by-byte, we must use // to_be_bytes() since we are comparing byte arrays, not integers. let revision = read_u32(&mut device, bai, &mut data[..]).to_be_bytes(); assert_eq!(revision, FW_CFG_REVISION); } #[test] // Attempt to read a FwCfgDevice's file directory fn read_file_dir() { let contents = get_contents(); let (mut device, bai) = setup_read(&FILENAMES, &contents, FW_CFG_FILE_DIR_SELECTOR); assert_read_file_dir(&FILENAMES, &contents, &mut device, bai); } #[test] // Attempt to read all of a FwCfgDevice's entries fn read_fw_cfg_entries() { let contents = get_contents(); let (mut device, bai) = setup_read(&FILENAMES, &contents, (0 + FW_CFG_FILE_FIRST) as u16); assert_read_entries(&FILENAMES, &mut device, bai); } #[test] // Attempt to read revision, file dir, and entries in random order to make // sure that proper state is maintained. fn read_whole_device() { let contents = get_contents(); let mut data: Vec = vec![0]; let (mut device, mut bai) = setup_read(&FILENAMES, &contents, FW_CFG_REVISION_SELECTOR); let revision = read_u32(&mut device, bai, &mut data[..]).to_be_bytes(); assert_eq!(revision, FW_CFG_REVISION); let i = FILENAMES.len() - 1; let data_len = device.entries[0][i].data.len(); assert_eq!( get_entry(&mut device, bai, data_len, (i + FW_CFG_FILE_FIRST) as u16), device.entries[0][i].data ); bai.address = FW_CFG_BASE_PORT; bai.offset = FW_CFG_SELECTOR_PORT_OFFSET; device.write(bai, &FW_CFG_FILE_DIR_SELECTOR.to_le_bytes()); bai.offset = FW_CFG_DATA_PORT_OFFSET; assert_read_file_dir(&FILENAMES, &contents, &mut device, bai); let data_len = device.entries[0][FW_CFG_FILE_FIRST + 0].data.len(); assert_eq!( get_entry(&mut device, bai, data_len, (0 + FW_CFG_FILE_FIRST) as u16), device.entries[0][FW_CFG_FILE_FIRST + 0].data ); } #[test] // Assert that the device maintains proper state after reads of random length. fn read_incorrect_bytes() { let contents = get_contents(); let mut data: Vec = vec![0]; let (mut device, mut bai) = setup_read(&FILENAMES, &contents, (0 + FW_CFG_FILE_FIRST) as u16); for _i in 1..1000 { let _random_bytes = read_u32(&mut device, bai, &mut data[..]); } bai.address = FW_CFG_BASE_PORT; device.write(bai, &FW_CFG_FILE_DIR_SELECTOR.to_le_bytes()); bai.offset = FW_CFG_DATA_PORT_OFFSET; for _i in 1..10000 { let mut data: Vec = vec![0]; let _random_bytes = read_u32(&mut device, bai, &mut data[..]); } bai.address = FW_CFG_BASE_PORT; bai.offset = FW_CFG_SELECTOR_PORT_OFFSET; device.write(bai, &FW_CFG_FILE_DIR_SELECTOR.to_le_bytes()); bai.offset = FW_CFG_DATA_PORT_OFFSET; assert_read_file_dir(&FILENAMES, &contents, &mut device, bai); let i = FILENAMES.len() - 1; bai.address = FW_CFG_BASE_PORT; bai.offset = FW_CFG_SELECTOR_PORT_OFFSET; device.write(bai, &(FW_CFG_FILE_FIRST + i).to_le_bytes()); bai.offset = FW_CFG_DATA_PORT_OFFSET; for _i in 1..1000 { let _random_bytes = read_u32(&mut device, bai, &mut data[..]); } assert_read_entries(&FILENAMES, &mut device, bai); } }