#![cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use fs_ext4::block_io::BlockDevice;
use fs_ext4::error::Result;
use fs_ext4::file_io;
use fs_ext4::fs::Filesystem;
use std::sync::{Arc, Mutex};
use wasm_bindgen_test::wasm_bindgen_test;
const SIZE: u64 = 16 * 1024 * 1024;
const BLOCK_SIZE: u32 = 4096;
const UUID: [u8; 16] = [
0x6a, 0x1e, 0x77, 0x02, 0x3c, 0x9d, 0x4b, 0x51, 0x8e, 0x20, 0x11, 0xf4, 0x5a, 0x09, 0xc3, 0x7d,
];
struct MemDev {
bytes: Mutex<Vec<u8>>,
}
impl MemDev {
fn new(size: u64) -> Arc<Self> {
Arc::new(Self {
bytes: Mutex::new(vec![0u8; size as usize]),
})
}
}
impl BlockDevice for MemDev {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
let b = self.bytes.lock().unwrap();
let start = offset as usize;
buf.copy_from_slice(&b[start..start + buf.len()]);
Ok(())
}
fn size_bytes(&self) -> u64 {
self.bytes.lock().unwrap().len() as u64
}
fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
let mut b = self.bytes.lock().unwrap();
let start = offset as usize;
b[start..start + buf.len()].copy_from_slice(buf);
Ok(())
}
fn flush(&self) -> Result<()> {
Ok(())
}
fn is_writable(&self) -> bool {
true
}
}
fn formatted() -> Arc<MemDev> {
let dev = MemDev::new(SIZE);
fs_ext4::mkfs::format_filesystem(dev.as_ref(), Some("wasm"), Some(UUID), SIZE, BLOCK_SIZE)
.expect("format_filesystem with a caller-supplied UUID");
dev
}
fn mount(dev: &Arc<MemDev>) -> Filesystem {
let dev: Arc<dyn BlockDevice> = dev.clone();
Filesystem::mount(dev).expect("mount")
}
fn ino_of(fs: &Filesystem, path: &str) -> u32 {
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
fs_ext4::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, path).expect("lookup")
}
#[wasm_bindgen_test]
fn a_formatted_volume_mounts_with_only_lost_found_in_the_root() {
let dev = formatted();
let fs = mount(&dev);
assert_eq!(fs.sb.volume_name, "wasm");
assert_eq!(fs.sb.uuid, UUID);
assert_eq!(fs.sb.block_size(), BLOCK_SIZE);
assert!(fs.sb.is_clean(), "a fresh volume is marked clean");
let (root, _) = fs.read_inode_verified(2).expect("root inode verifies");
assert!(root.is_dir(), "the root is a directory");
assert_eq!(root.links_count, 3, "`.`, `..` and lost+found's `..`");
assert_eq!(ino_of(&fs, "/lost+found"), 11, "lost+found is inode 11");
fs.finish().expect("unmount");
}
#[wasm_bindgen_test]
fn a_file_created_and_written_reads_back_after_unmount() {
let dev = formatted();
let data: Vec<u8> = (0..10_000u32).map(|i| (i % 251) as u8 + 1).collect();
{
let fs = mount(&dev);
let ino = fs.apply_create("/hello.bin", 0o644).expect("create");
let (inode, _) = fs.read_inode_verified(ino).expect("new inode verifies");
assert!(inode.mtime > 0, "the browser clock stamped the new inode");
let size = fs
.apply_pwrite("/hello.bin", 0, &data)
.expect("write the file");
assert_eq!(size, data.len() as u64);
fs.finish().expect("unmount");
}
let fs = mount(&dev);
let (inode, _) = fs
.read_inode_verified(ino_of(&fs, "/hello.bin"))
.expect("the file is there after a remount");
assert_eq!(inode.size, data.len() as u64);
let got = file_io::read_all(&fs, &inode).expect("read the file back");
assert!(got == data, "the file does not read back as written");
fs.finish().expect("unmount");
}
#[wasm_bindgen_test]
fn format_without_a_uuid_draws_a_random_v4_uuid() {
let uuid_of_a_fresh_volume = || {
let dev = MemDev::new(SIZE);
fs_ext4::mkfs::format_filesystem(dev.as_ref(), None, None, SIZE, BLOCK_SIZE)
.expect("format_filesystem with no UUID");
let fs = mount(&dev);
let uuid = fs.sb.uuid;
fs.finish().expect("unmount");
uuid
};
let a = uuid_of_a_fresh_volume();
let b = uuid_of_a_fresh_volume();
for uuid in [a, b] {
assert_eq!(uuid[6] >> 4, 4, "version nibble of {uuid:02x?}");
assert_eq!(uuid[8] >> 6, 0b10, "variant bits of {uuid:02x?}");
}
assert_ne!(a, b, "two formats drew the same UUID");
}
#[wasm_bindgen_test]
fn an_audit_of_a_written_volume_is_clean() {
let dev = formatted();
{
let fs = mount(&dev);
fs.apply_mkdir("/dir", 0o755).expect("mkdir");
for i in 0..20 {
let path = format!("/dir/f{i}");
fs.apply_create(&path, 0o644).expect("create");
fs.apply_pwrite(&path, 0, &[i as u8; 3000]).expect("write");
}
fs.finish().expect("unmount");
}
let fs = mount(&dev);
let report = fs_ext4::fsck::audit(&fs, u32::MAX, u32::MAX).expect("audit");
assert!(report.is_clean(), "anomalies: {:?}", report.anomalies);
let mut inode_ticks = 0u32;
let report = fs_ext4::fsck::audit_with_callbacks(
&fs,
u32::MAX,
u32::MAX,
|phase, _, _| {
if phase == fs_ext4::fsck::FsckPhase::Inodes {
inode_ticks += 1;
}
},
|a| panic!("finding on a clean volume: {a:?}"),
)
.expect("audit_with_callbacks");
assert_eq!(report.anomalies_count, 0);
assert!(
(1..=3).contains(&inode_ticks),
"{inode_ticks} Inodes progress callbacks for 23 inodes"
);
fs.finish().expect("unmount");
}
struct Sparse;
impl BlockDevice for Sparse {
fn read_at(&self, _offset: u64, buf: &mut [u8]) -> Result<()> {
buf.fill(0);
Ok(())
}
fn size_bytes(&self) -> u64 {
1 << 40
}
fn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()> {
panic!("nothing writes here")
}
fn flush(&self) -> Result<()> {
Ok(())
}
fn is_writable(&self) -> bool {
false
}
}
#[wasm_bindgen_test]
fn a_group_count_past_usize_is_refused_not_truncated() {
let mut raw = vec![0u8; 1024];
raw[0x00..0x04].copy_from_slice(&16u32.to_le_bytes()); raw[0x04..0x08].copy_from_slice(&1u32.to_le_bytes()); raw[0x150..0x154].copy_from_slice(&1u32.to_le_bytes()); raw[0x14..0x18].copy_from_slice(&1u32.to_le_bytes()); raw[0x20..0x24].copy_from_slice(&1u32.to_le_bytes()); raw[0x28..0x2C].copy_from_slice(&1u32.to_le_bytes()); raw[0x38..0x3A].copy_from_slice(&0xEF53u16.to_le_bytes()); raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); let sb = fs_ext4::superblock::Superblock::parse(raw).expect("the superblock parses");
assert_eq!(
sb.block_group_count(),
1 << 32,
"the fixture's own geometry"
);
let csum = fs_ext4::checksum::Checksummer::from_superblock(&sb);
let got = fs_ext4::bgd::read_all(&Sparse, &sb, &csum);
assert!(
got.is_err(),
"2^32 groups read as a table of {} descriptors",
got.map(|t| t.len()).unwrap_or_default()
);
}