Skip to content

Commit

Permalink
Expand file system tests
Browse files Browse the repository at this point in the history
Add code in xtask to build a disk image with a FAT partition, then run
file system tests against that disk (when the test runner is built with
the qemu feature). A disk image is used instead of QEMU's VVFAT option
so that everything is fully controlled (can test exact FS info values)
and isolated from the host.

This partially addresses
#57. There are some more
partition and file system tests that can be added here.
  • Loading branch information
nicholasbishop authored and GabrielMajeri committed Mar 17, 2022
1 parent ccc5404 commit 87df393
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 44 deletions.
180 changes: 180 additions & 0 deletions uefi-test-runner/src/proto/media/known_disk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use alloc::string::ToString;
use uefi::prelude::*;
use uefi::proto::media::file::{
Directory, File, FileAttribute, FileInfo, FileMode, FileSystemInfo,
};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::table::boot::{OpenProtocolAttributes, OpenProtocolParams};
use uefi::table::runtime::{Daylight, Time};
use uefi::CString16;

/// Test directory entry iteration.
fn test_existing_dir(directory: &mut Directory) {
info!("Testing existing directory");

let input_dir_path = CString16::try_from("test_dir").unwrap();
let mut dir = directory
.open(&input_dir_path, FileMode::Read, FileAttribute::empty())
.expect("failed to open directory")
.into_directory()
.expect("not a directory");

// Collect and validate the directory entries.
let mut entry_names = vec![];
let mut buf = vec![0; 200];
loop {
let entry = dir.read_entry(&mut buf).expect("failed to read directory");
if let Some(entry) = entry {
entry_names.push(entry.file_name().to_string());
} else {
break;
}
}
assert_eq!(entry_names, [".", "..", "test_input.txt"]);
}

/// Test that deleting a file opened in read-only mode fails with a
/// warning. This is mostly just an excuse to verify that warnings are
/// properly converted to errors.
fn test_delete_warning(directory: &mut Directory) {
let input_file_path = CString16::try_from("test_dir\\test_input.txt").unwrap();
let file = directory
.open(&input_file_path, FileMode::Read, FileAttribute::empty())
.expect("failed to open file")
.into_regular_file()
.expect("not a regular file");

assert_eq!(
file.delete().unwrap_err().status(),
Status::WARN_DELETE_FAILURE
);
}

/// Test operations on an existing file.
fn test_existing_file(directory: &mut Directory) {
info!("Testing existing file");

// Open an existing file.
let input_file_path = CString16::try_from("test_dir\\test_input.txt").unwrap();
let mut file = directory
.open(
&input_file_path,
FileMode::ReadWrite,
FileAttribute::empty(),
)
.expect("failed to open file")
.into_regular_file()
.expect("not a regular file");

// Read the file.
let mut buffer = vec![0; 128];
let size = file.read(&mut buffer).expect("failed to read file");
let buffer = &buffer[..size];
info!("Successfully read {}", input_file_path);
assert_eq!(buffer, b"test input data");

// Check file metadata.
let mut info_buffer = vec![0; 128];
let info = file.get_info::<FileInfo>(&mut info_buffer).unwrap();
assert_eq!(info.file_size(), 15);
assert_eq!(info.physical_size(), 512);
assert_eq!(
*info.create_time(),
Time::new(2000, 1, 24, 0, 0, 0, 0, 2047, Daylight::empty())
);
assert_eq!(
*info.last_access_time(),
Time::new(2001, 2, 25, 0, 0, 0, 0, 2047, Daylight::empty())
);
assert_eq!(
*info.modification_time(),
Time::new(2002, 3, 26, 0, 0, 0, 0, 2047, Daylight::empty())
);
assert_eq!(info.attribute(), FileAttribute::empty());
assert_eq!(
info.file_name(),
CString16::try_from("test_input.txt").unwrap()
);

// Delete the file.
file.delete().unwrap();

// Verify the file is gone.
assert!(directory
.open(&input_file_path, FileMode::Read, FileAttribute::empty())
.is_err());
}

/// Test file creation.
fn test_create_file(directory: &mut Directory) {
info!("Testing file creation");

// Create a new file.
let mut file = directory
.open(
&CString16::try_from("new_test_file.txt").unwrap(),
FileMode::CreateReadWrite,
FileAttribute::empty(),
)
.expect("failed to create file")
.into_regular_file()
.expect("not a regular file");
file.write(b"test output data").unwrap();
}

/// Run various tests on a special test disk. The disk is created by
/// xtask/src/disk.rs.
pub fn test_known_disk(image: Handle, bt: &BootServices) {
// This test is only valid when running in the specially-prepared
// qemu with the test disk.
if !cfg!(feature = "qemu") {
return;
}

let handles = bt
.find_handles::<SimpleFileSystem>()
.expect("Failed to get handles for `SimpleFileSystem` protocol");
assert_eq!(handles.len(), 2);

let mut found_test_disk = false;
for handle in handles {
let sfs = bt
.open_protocol::<SimpleFileSystem>(
OpenProtocolParams {
handle,
agent: image,
controller: None,
},
OpenProtocolAttributes::Exclusive,
)
.expect("Failed to get simple file system");
let sfs = unsafe { &mut *sfs.interface.get() };
let mut directory = sfs.open_volume().unwrap();

let mut fs_info_buf = vec![0; 128];
let fs_info = directory
.get_info::<FileSystemInfo>(&mut fs_info_buf)
.unwrap();

if fs_info.volume_label().to_string() == "MbrTestDisk" {
info!("Checking MbrTestDisk");
found_test_disk = true;
} else {
continue;
}

assert!(!fs_info.read_only());
assert_eq!(fs_info.volume_size(), 512 * 1192);
assert_eq!(fs_info.free_space(), 512 * 1190);
assert_eq!(fs_info.block_size(), 512);

test_existing_dir(&mut directory);
test_delete_warning(&mut directory);
test_existing_file(&mut directory);
test_create_file(&mut directory);
}

if !found_test_disk {
panic!("MbrTestDisk not found");
}
}
49 changes: 5 additions & 44 deletions uefi-test-runner/src/proto/media/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
mod known_disk;

use uefi::prelude::*;
use uefi::proto::media::file::{
Directory, File, FileAttribute, FileMode, FileSystemInfo, FileSystemVolumeLabel, FileType,
};
use uefi::proto::media::file::{Directory, File, FileSystemInfo, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::partition::PartitionInfo;
use uefi::table::boot::{OpenProtocolAttributes, OpenProtocolParams};
use uefi::CString16;

/// Test `FileSystemInfo` and `FileSystemVolumeLabel`.
fn test_file_system_info(directory: &mut Directory) {
Expand All @@ -23,45 +22,6 @@ fn test_file_system_info(directory: &mut Directory) {

// Both types should provide the same volume label.
assert_eq!(fs_info.volume_label(), fs_vol.volume_label());

// If running under qemu we can verify the exact name.
if cfg!(feature = "qemu") {
assert_eq!(
fs_info.volume_label(),
CString16::try_from("QEMU VVFAT").unwrap()
);
}
}

/// Open and read a test file in the boot directory.
fn test_open_and_read(directory: &mut Directory) {
let test_input_path = CString16::try_from("EFI\\BOOT\\test_input.txt").unwrap();
match directory.open(&test_input_path, FileMode::Read, FileAttribute::empty()) {
Ok(file) => {
let file = file.into_type().unwrap();
if let FileType::Regular(mut file) = file {
let mut buffer = vec![0; 128];
let size = file
.read(&mut buffer)
.unwrap_or_else(|_| panic!("failed to read {}", test_input_path));
let buffer = &buffer[..size];
info!("Successfully read {}", test_input_path);
assert_eq!(buffer, b"test input data");
} else {
panic!("{} is not a regular file", test_input_path);
}
}
Err(err) => {
let msg = format!("Failed to open {}: {:?}", test_input_path, err);
// The file might reasonably not be present when running on real
// hardware, so only panic on failure under qemu.
if cfg!(feature = "qemu") {
panic!("{}", msg);
} else {
warn!("{}", msg);
}
}
}
}

pub fn test(image: Handle, bt: &BootServices) {
Expand Down Expand Up @@ -93,7 +53,6 @@ pub fn test(image: Handle, bt: &BootServices) {
directory.reset_entry_readout().unwrap();

test_file_system_info(&mut directory);
test_open_and_read(&mut directory);
} else {
warn!("`SimpleFileSystem` protocol is not available");
}
Expand Down Expand Up @@ -123,4 +82,6 @@ pub fn test(image: Handle, bt: &BootServices) {
info!("Unknown partition");
}
}

known_disk::test_known_disk(image, bt);
}
3 changes: 3 additions & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ publish = false
[dependencies]
anyhow = "1.0.51"
clap = { version = "3.0.13", features = ["derive"] }
# The latest fatfs release (0.3.5) is old, use git instead to pick up some fixes.
fatfs = { git = "https://github.com/rafalh/rust-fatfs.git", rev = "87fc1ed5074a32b4e0344fcdde77359ef9e75432" }
fs-err = "2.6.0"
mbrman = "0.4.2"
nix = "0.23.1"
regex = "1.5.4"
serde_json = "1.0.73"
Expand Down
117 changes: 117 additions & 0 deletions xtask/src/disk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use anyhow::Result;
use fatfs::{Date, DateTime, FileSystem, FormatVolumeOptions, FsOptions, StdIoWrapper, Time};
use mbrman::{MBRPartitionEntry, CHS, MBR};
use std::io::{Cursor, Read, Write};
use std::ops::Range;
use std::path::Path;

const SECTOR_SIZE: usize = 512;

fn get_partition_byte_range(mbr: &MBR) -> Range<usize> {
let partition_start_byte = mbr[1].starting_lba as usize * SECTOR_SIZE;
let partition_num_bytes = mbr[1].sectors as usize * SECTOR_SIZE;
partition_start_byte..partition_start_byte + partition_num_bytes
}

pub fn create_mbr_test_disk(path: &Path) -> Result<()> {
let num_sectors = 1234;

let partition_byte_range;
let mut disk = vec![0; num_sectors * SECTOR_SIZE];
{
let mut cur = std::io::Cursor::new(&mut disk);

let mut mbr = MBR::new_from(&mut cur, SECTOR_SIZE as u32, [0xff; 4])?;
mbr[1] = MBRPartitionEntry {
boot: false,
first_chs: CHS::empty(),
sys: 0x06,
last_chs: CHS::empty(),
starting_lba: 1,
sectors: mbr.disk_size - 1,
};

partition_byte_range = get_partition_byte_range(&mbr);

mbr.write_into(&mut cur)?;
}

init_fat_test_partition(&mut disk, partition_byte_range)?;

fs_err::write(path, &disk)?;

Ok(())
}

fn init_fat_test_partition(disk: &mut [u8], partition_byte_range: Range<usize>) -> Result<()> {
{
let mut cursor = StdIoWrapper::from(Cursor::new(&mut disk[partition_byte_range.clone()]));
fatfs::format_volume(
&mut cursor,
FormatVolumeOptions::new().volume_label(*b"MbrTestDisk"),
)?;
}

let cursor = Cursor::new(&mut disk[partition_byte_range]);
let fs = FileSystem::new(cursor, FsOptions::new().update_accessed_date(false))?;

assert_eq!(
fs.read_volume_label_from_root_dir().unwrap(),
Some("MbrTestDisk".to_string())
);

let root_dir = fs.root_dir();

let dir = root_dir.create_dir("test_dir")?;

let mut file = dir.create_file("test_input.txt")?;
file.write_all(b"test input data")?;

// The datetime-setting functions have been deprecated, but are
// useful here to force an exact date that can be checked in the
// test.
#[allow(deprecated)]
{
let time = Time::new(0, 0, 0, 0);
file.set_created(DateTime::new(Date::new(2000, 1, 24), time));
file.set_accessed(Date::new(2001, 2, 25));
file.set_modified(DateTime::new(Date::new(2002, 3, 26), time));
}

let stats = fs.stats()?;
// Assert these specific numbers here since they are checked by the
// test-runner too.
assert_eq!(stats.total_clusters(), 1192);
assert_eq!(stats.free_clusters(), 1190);

Ok(())
}

pub fn check_mbr_test_disk(path: &Path) -> Result<()> {
println!("Verifying test disk has been correctly modified");
let mut disk = fs_err::read(path)?;

let partition_byte_range;
{
let mut cursor = Cursor::new(&disk);
let mbr = MBR::read_from(&mut cursor, SECTOR_SIZE as u32)?;
partition_byte_range = get_partition_byte_range(&mbr);
}

let cursor = Cursor::new(&mut disk[partition_byte_range]);
let fs = FileSystem::new(cursor, FsOptions::new().update_accessed_date(false))?;
let root_dir = fs.root_dir();

// Check that the new file was created.
let mut file = root_dir.open_file("new_test_file.txt")?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
assert_eq!(bytes, b"test output data");

// Check that the original input file was deleted.
let dir = root_dir.open_dir("test_dir")?;
let children: Vec<_> = dir.iter().map(|e| e.unwrap().file_name()).collect();
assert_eq!(children, [".", ".."]);

Ok(())
}
1 change: 1 addition & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod arch;
mod cargo;
mod disk;
mod opt;
mod qemu;
mod util;
Expand Down
Loading

0 comments on commit 87df393

Please sign in to comment.