use std::cell::Cell;
use rsext4::{
bmalloc::{AbsoluteBN, BGIndex},
error::{Ext4Error, Ext4Result},
*,
};
struct ErrorMockDevice {
data: Vec<u8>,
block_size: u32,
fail_on_open: bool,
fail_on_close: bool,
fail_on_read: bool,
fail_on_write: bool,
fail_on_specific_block: Option<AbsoluteBN>,
fail_after_bytes: Option<usize>,
bytes_written: usize,
now: Cell<i64>,
}
impl ErrorMockDevice {
fn new(size: usize) -> Self {
Self {
data: vec![0; size],
block_size: rsext4::BLOCK_SIZE as u32,
fail_on_open: false,
fail_on_close: false,
fail_on_read: false,
fail_on_write: false,
fail_on_specific_block: None,
fail_after_bytes: None,
bytes_written: 0,
now: Cell::new(1_700_000_000),
}
}
}
impl BlockDevice for ErrorMockDevice {
fn read(&mut self, buffer: &mut [u8], block_id: AbsoluteBN, _count: u32) -> Ext4Result<()> {
if self.fail_on_read {
return Err(Ext4Error::io());
}
if let Some(fail_block) = self.fail_on_specific_block {
if block_id == fail_block {
return Err(Ext4Error::corrupted());
}
}
let start = block_id.as_usize()? * self.block_size as usize;
let end = start + buffer.len();
if end > self.data.len() {
return Err(Ext4Error::block_out_of_range(
block_id.to_u32()?,
(self.data.len() / self.block_size as usize) as u64,
));
}
buffer.copy_from_slice(&self.data[start..end]);
Ok(())
}
fn write(&mut self, buffer: &[u8], block_id: AbsoluteBN, _count: u32) -> Ext4Result<()> {
if self.fail_on_write {
return Err(Ext4Error::io());
}
if let Some(fail_block) = self.fail_on_specific_block {
if block_id == fail_block {
return Err(Ext4Error::corrupted());
}
}
if let Some(limit) = self.fail_after_bytes {
self.bytes_written += buffer.len();
if self.bytes_written > limit {
return Err(Ext4Error::no_space());
}
}
let start = block_id.as_usize()? * self.block_size as usize;
let end = start + buffer.len();
if end > self.data.len() {
return Err(Ext4Error::block_out_of_range(
block_id.to_u32()?,
(self.data.len() / self.block_size as usize) as u64,
));
}
self.data[start..end].copy_from_slice(buffer);
Ok(())
}
fn open(&mut self) -> Ext4Result<()> {
if self.fail_on_open {
return Err(Ext4Error::badf());
}
Ok(())
}
fn close(&mut self) -> Ext4Result<()> {
if self.fail_on_close {
return Err(Ext4Error::badf());
}
Ok(())
}
fn total_blocks(&self) -> u64 {
(self.data.len() / self.block_size as usize) as u64
}
fn block_size(&self) -> u32 {
self.block_size
}
fn current_time(&self) -> Ext4Result<Ext4Timestamp> {
let sec = self.now.get();
self.now.set(sec + 1);
Ok(Ext4Timestamp::new(sec, 0))
}
}
#[cfg(test)]
mod error_handling_tests {
use super::*;
#[test]
fn test_block_device_errors() {
let device = ErrorMockDevice::new(100 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
mkdir(&mut jbd2_dev, &mut fs, "/error_test").expect("mkdir failed");
let test_data = b"Test data for error scenarios";
mkfile(
&mut jbd2_dev,
&mut fs,
"/error_test/test.txt",
Some(test_data),
None,
)
.expect("mkfile failed");
let data =
read_file(&mut jbd2_dev, &mut fs, "/error_test/test.txt").expect("read_file failed");
assert_eq!(data, test_data.to_vec());
let _ = umount(fs, &mut jbd2_dev);
}
#[test]
fn test_filesystem_boundaries() {
let small_device = ErrorMockDevice::new(20 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, small_device, true);
let result = mkfs(&mut jbd2_dev);
println!("mkfs on small device result: {:?}", result);
let normal_device = ErrorMockDevice::new(50 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, normal_device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
mkdir(&mut jbd2_dev, &mut fs, "/boundary").expect("mkdir failed");
mkfile(&mut jbd2_dev, &mut fs, "/boundary/empty.txt", None, None).expect("mkfile failed");
let long_name = "a".repeat(rsext4::DIRNAME_LEN);
let _ = mkfile(
&mut jbd2_dev,
&mut fs,
&format!("/boundary/{}.txt", long_name),
Some(b"test"),
None,
);
let too_long_name = "a".repeat(rsext4::DIRNAME_LEN + 1);
let result = mkfile(
&mut jbd2_dev,
&mut fs,
&format!("/boundary/{}.txt", too_long_name),
Some(b"test"),
None,
);
println!("mkfile with long filename result: {:?}", result);
umount(fs, &mut jbd2_dev).expect("umount failed");
}
#[test]
fn test_invalid_paths() {
let device = ErrorMockDevice::new(100 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
let result = mkfile(&mut jbd2_dev, &mut fs, "", Some(b"test"), None);
println!("mkfile with empty path result: {:?}", result);
let result = mkfile(&mut jbd2_dev, &mut fs, "/", Some(b"test"), None);
println!("mkfile with root path result: {:?}", result);
let _ = mkdir(&mut jbd2_dev, &mut fs, "//invalid//path//");
let _ = mkdir(&mut jbd2_dev, &mut fs, "/path/with\0null");
umount(fs, &mut jbd2_dev).expect("umount failed");
}
#[test]
fn test_concurrent_operation_errors() {
let device = ErrorMockDevice::new(100 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
mkdir(&mut jbd2_dev, &mut fs, "/concurrent").expect("mkdir failed");
mkfile(
&mut jbd2_dev,
&mut fs,
"/concurrent/base.txt",
Some(b"base content"),
None,
)
.expect("mkfile failed");
let file_path = "/concurrent/delete_test.txt";
mkfile(
&mut jbd2_dev,
&mut fs,
file_path,
Some(b"to be deleted"),
None,
)
.expect("mkfile failed");
delete_file(&mut fs, &mut jbd2_dev, file_path).expect("delete failed");
let result = read_file(&mut jbd2_dev, &mut fs, file_path).expect_err("deleted file");
assert_eq!(result.code, Errno::ENOENT);
mkfile(
&mut jbd2_dev,
&mut fs,
file_path,
Some(b"new content"),
None,
)
.expect("mkfile failed");
let data = read_file(&mut jbd2_dev, &mut fs, file_path).expect("read_file failed");
assert_eq!(data, b"new content".to_vec());
umount(fs, &mut jbd2_dev).expect("umount failed");
}
#[test]
fn test_resource_exhaustion() {
let device = ErrorMockDevice::new(50 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
mkdir(&mut jbd2_dev, &mut fs, "/exhaustion").expect("mkdir failed");
let mut file_count = 0;
let file_size = 1024 * 1024; let large_data = vec![b'X'; file_size];
loop {
let filename = format!("/exhaustion/file{}.dat", file_count);
let result = mkfile(&mut jbd2_dev, &mut fs, &filename, Some(&large_data), None);
match result {
Ok(_) => file_count += 1,
Err(_) => break,
}
if file_count > 40 {
break;
}
}
assert!(file_count > 0);
let last_filename = format!("/exhaustion/file{}.dat", file_count - 1);
let data = read_file(&mut jbd2_dev, &mut fs, &last_filename).expect("read_file failed");
assert_eq!(data, large_data);
let _ = umount(fs, &mut jbd2_dev);
}
#[test]
fn test_partial_last_group_padding_is_not_allocated() {
let blocks_per_group = 8 * rsext4::BLOCK_SIZE as u64;
let total_blocks = blocks_per_group + 128;
let device = ErrorMockDevice::new(total_blocks as usize * rsext4::BLOCK_SIZE);
let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let fs = mount(&mut jbd2_dev).expect("mount failed");
let last_group = BGIndex::new(fs.group_count - 1);
let last_desc = fs
.get_group_desc(last_group)
.expect("last group descriptor should exist");
assert!(last_desc.free_blocks_count() < 128);
jbd2_dev
.read_block(AbsoluteBN::new(last_desc.block_bitmap()))
.expect("read last block bitmap");
let bitmap = jbd2_dev.buffer();
for bit in 128..fs.superblock.s_blocks_per_group as usize {
assert!(
bitmap[bit / 8] & (1 << (bit % 8)) != 0,
"partial-group padding bit {bit} should be marked allocated"
);
}
let _ = umount(fs, &mut jbd2_dev);
}
#[test]
fn test_inconsistent_state_handling() {
let device = ErrorMockDevice::new(100 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
mkdir(&mut jbd2_dev, &mut fs, "/state_test").expect("mkdir failed");
mkfile(
&mut jbd2_dev,
&mut fs,
"/state_test/consistent.txt",
Some(b"original data"),
None,
)
.expect("mkfile failed");
let mut file =
open(&mut jbd2_dev, &mut fs, "/state_test/consistent.txt", true).expect("open failed");
write_at(&mut jbd2_dev, &mut fs, &mut file, b"partial").expect("write_at failed");
drop(file);
drop(fs);
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
let data = read_file(&mut jbd2_dev, &mut fs, "/state_test/consistent.txt");
println!("File data after remount: {:?}", data);
umount(fs, &mut jbd2_dev).expect("umount failed");
}
#[test]
fn test_permission_handling() {
let device = ErrorMockDevice::new(100 * 1024 * 1024); let mut jbd2_dev = Jbd2Dev::initial_jbd2dev(0, device, true);
mkfs(&mut jbd2_dev).expect("mkfs failed");
let mut fs = mount(&mut jbd2_dev).expect("mount failed");
mkdir(&mut jbd2_dev, &mut fs, "/permission").expect("mkdir failed");
mkfile(
&mut jbd2_dev,
&mut fs,
"/permission/test.txt",
Some(b"permission test"),
None,
)
.expect("mkfile failed");
let data =
read_file(&mut jbd2_dev, &mut fs, "/permission/test.txt").expect("read_file failed");
assert_eq!(data, b"permission test".to_vec());
write_file(
&mut jbd2_dev,
&mut fs,
"/permission/test.txt",
0,
b"modified",
)
.expect("write_file failed");
let data =
read_file(&mut jbd2_dev, &mut fs, "/permission/test.txt").expect("read_file failed");
assert_eq!(
&data[..b"modified".len()],
b"modified",
"The updated prefix should be written correctly",
);
umount(fs, &mut jbd2_dev).expect("umount failed");
}
}