//! ext2 through the VFS. Cargo.toml's test-args attach three ext2 images //! that build.rs makes (all snapshot=on, so writes vanish after the test): //! //! vdb target/ext2.img /mnt/disk1 1 KiB blocks, revision 1 //! vde target/ext2-4k.img /mnt/disk4 4 KiB blocks //! vdf target/ext2-rev0.img /mnt/disk5 revision 0 (no file types in //! directory entries) //! (vdc, /mnt/disk2, is the exFAT disk: see tests/exfat.rs; vdd, /mnt/disk3, //! and vdh-vdk are btrfs: see tests/btrfs.rs.) //! //! scripts/ext2-check.sh runs this kernel on *writable* copies and then //! checks them with the host's e2fsck. #![no_std] #![no_main] #![feature(custom_test_frameworks)] #![test_runner(rustbelt::test_runner)] #![reexport_test_harness_main = "test_main"] extern crate alloc; use alloc::{format, string::String, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rustbelt::block; use rustbelt::errno::Errno; use rustbelt::fs::{self, InodeKind}; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rustbelt::init(); rustbelt::init_memory(boot_info); fs::init(); rustbelt::init_storage(); test_main(); rustbelt::hlt_loop(); } const DISK: &str = "/mnt/disk1"; /// Read `len` bytes at `offset` of a file. fn read(path: &str, offset: usize, len: usize) -> Vec { let mut buf = vec![0u8; len]; let n = fs::lookup(path).unwrap().read_at(offset, &mut buf).unwrap(); buf.truncate(n); buf } /// (free blocks, free inodes), straight from the superblock on the disk /// (byte 1024 = sector 2). The driver has no cache, so this is current. fn free_counts(device: &str) -> (u32, u32) { let mut sb = [0u8; 1024]; block::get(device).unwrap().read_sectors(2, &mut sb).unwrap(); let get = |at: usize| u32::from_le_bytes(sb[at..at + 4].try_into().unwrap()); (get(12), get(16)) } fn names(path: &str) -> Vec { let mut names: Vec = fs::lookup(path).unwrap().list().unwrap().into_iter().map(|e| e.name).collect(); names.sort(); names } // --- reading ----------------------------------------------------------------- #[test_case] fn ext2_volumes_are_mounted() { let mounts = fs::mounts(); for path in ["/mnt/disk1", "/mnt/disk4", "/mnt/disk5"] { assert!(mounts.iter().any(|(p, kind)| p == path && *kind == "ext2"), "{} is not ext2", path); } // (Tests run in name order and share the disk, so others may have // added files by now: check what build.rs put there.) let found = names(DISK); for name in ["big.txt", "docs", "hello.txt", "lost+found", "readme.txt", "sparse.bin"] { assert!(found.iter().any(|n| n == name), "{} missing", name); } } #[test_case] fn read_small_file_and_permissions() { let hello = fs::lookup("/mnt/disk1/hello.txt").unwrap(); assert_eq!(hello.kind(), InodeKind::File); assert_eq!(hello.size(), 142); assert_eq!(hello.mode(), 0o644); assert!(read("/mnt/disk1/hello.txt", 0, 200).starts_with(b"Hello from the ext2 disk!")); assert_eq!(fs::lookup(DISK).unwrap().mode(), 0o755); assert_eq!(fs::lookup("/mnt/disk1/lost+found").unwrap().mode(), 0o700); // ext2 names are case-sensitive (unlike FAT). assert_eq!(fs::lookup("/mnt/disk1/HELLO.TXT").err(), Some(Errno::ENOENT)); assert_eq!(fs::lookup("/mnt/disk1/hello.txt/x").err(), Some(Errno::ENOTDIR)); assert_eq!(read("/mnt/disk1/docs/deep/er/nested.txt", 0, 100), b"You found the deeply nested file.\n"); } #[test_case] fn big_file_through_indirect_blocks() { // big.txt: 16 000 lines of 19 bytes. With 1 KiB blocks the first 12 KiB // are direct blocks, up to 268 KiB single indirect, the rest double. let path = "/mnt/disk1/big.txt"; assert_eq!(fs::lookup(path).unwrap().size(), 304_000); for line in [0usize, 100, 700, 5_000, 14_500, 15_999] { let expected = format!("big.txt line {:05}\n", line); assert_eq!(read(path, line * 19, 19), expected.as_bytes(), "line {}", line); } // One read spanning many blocks, across the single -> double boundary. let whole = fs::read_all(path).unwrap(); assert_eq!(whole.len(), 304_000); assert!(whole.chunks(19).enumerate().all(|(i, l)| l == format!("big.txt line {:05}\n", i).as_bytes())); } #[test_case] fn sparse_file_and_triple_indirect() { // sparse.bin is 66 MiB but only three 1 KiB chunks hold data: at 0 // (direct), 40 MiB (double indirect) and 65 MiB (triple indirect). let path = "/mnt/disk1/sparse.bin"; assert_eq!(fs::lookup(path).unwrap().size(), 66 << 20); for at in [0usize, 40 << 20, 65 << 20] { let expected = format!("sparse.bin data at offset {}\n", at); assert_eq!(read(path, at, expected.len()), expected.as_bytes()); } // Holes read as zeros. assert!(read(path, 10 << 20, 4096).iter().all(|&b| b == 0)); assert!(read(path, (66 << 20) - 100, 4096).iter().all(|&b| b == 0)); // last block, cut at EOF assert_eq!(read(path, (66 << 20) - 100, 4096).len(), 100); } #[test_case] fn fast_and_slow_symlinks() { // Fast: the 9-byte target sits inside the inode's block pointers. assert_eq!(fs::readlink("/mnt/disk1/hello-link").unwrap(), "hello.txt"); assert_eq!(fs::lookup_nofollow("/mnt/disk1/hello-link").unwrap().kind(), InodeKind::Symlink); let followed = fs::lookup("/mnt/disk1/hello-link").unwrap(); assert_eq!((followed.kind(), followed.size()), (InodeKind::File, 142)); // Slow: a 74-byte target lives in a data block. let target = fs::readlink("/mnt/disk1/docs/long-link").unwrap(); assert_eq!(target.len(), 74); assert!(target.ends_with("/deep/er/nested.txt")); assert_eq!(read("/mnt/disk1/docs/long-link", 0, 100), b"You found the deeply nested file.\n"); assert_eq!(fs::readlink("/mnt/disk1/hello.txt").err(), Some(Errno::EINVAL)); // Removing a symlink removes the link, not the file. fs::remove("/mnt/disk1/hello-link", false).unwrap(); assert_eq!(fs::lookup_nofollow("/mnt/disk1/hello-link").err(), Some(Errno::ENOENT)); assert_eq!(fs::lookup("/mnt/disk1/hello.txt").unwrap().size(), 142); } #[test_case] fn four_k_blocks_and_revision_0() { for disk in ["/mnt/disk4", "/mnt/disk5"] { assert_eq!(names(disk), vec!["big.txt", "docs", "hello-link", "hello.txt", "lost+found"], "{}", disk); let big = format!("{}/big.txt", disk); assert_eq!(read(&big, 15_999 * 19, 19), b"big.txt line 15999\n"); assert_eq!(read(&format!("{}/hello-link", disk), 0, 9), b"Hello fro"); // Revision 0 has no file type in directory entries: the driver // reads each inode to tell directories from files. let kinds: Vec = fs::lookup(disk).unwrap().list().unwrap().into_iter().map(|e| e.kind).collect(); assert!(kinds.contains(&InodeKind::Symlink) && kinds.contains(&InodeKind::Directory)); } } // --- writing ----------------------------------------------------------------- /// Create, fill, check and delete a file on `disk`, inside a new directory. /// Afterwards every free count is back where it started. fn write_round_trip(disk: &str, device: &str) { let before = free_counts(device); let dir = format!("{}/scratch", disk); fs::create(&dir, InodeKind::Directory).unwrap(); assert_eq!(fs::lookup(&dir).unwrap().mode(), 0o755); let path = format!("{}/data.bin", dir); let inode = fs::create(&path, InodeKind::File).unwrap(); assert_eq!(fs::create(&path, InodeKind::File).err(), Some(Errno::EEXIST)); // 300 000 bytes: with 1 KiB blocks that needs a double-indirect block. let data: Vec = (0..300_000u32).map(|i| (i * 31 % 251) as u8).collect(); assert_eq!(inode.write_at(0, &data), Ok(data.len())); assert_eq!(fs::read_all(&path).unwrap(), data); // Append, overwrite across a block boundary. assert_eq!(inode.write_at(300_000, b"tail"), Ok(4)); inode.write_at(1022, b"XYZW").unwrap(); assert_eq!(read(&path, 1020, 8), [data[1020], data[1021], b'X', b'Y', b'Z', b'W', data[1026], data[1027]]); assert_eq!(read(&path, 299_998, 10), [data[299_998], data[299_999], b't', b'a', b'i', b'l']); // A write far past the end leaves a hole (zeros) and lands in the // triple-indirect range on 1 KiB disks. inode.write_at(70 << 20, b"far away").unwrap(); assert_eq!(inode.size(), (70 << 20) + 8); assert!(read(&path, 50 << 20, 1024).iter().all(|&b| b == 0)); assert_eq!(read(&path, 70 << 20, 100), b"far away"); let used = free_counts(device); assert!(used.0 < before.0 && used.1 == before.1 - 2); // Truncate frees every data block (and the indirect blocks). inode.truncate().unwrap(); assert_eq!(inode.size(), 0); assert_eq!(read(&path, 0, 10), b""); inode.write_at(0, b"short again\n").unwrap(); // A second file and a subdirectory, then rmdir/unlink rules. fs::create(&format!("{}/sub", dir), InodeKind::Directory).unwrap(); assert_eq!(fs::remove(&dir, true), Err(Errno::ENOTEMPTY)); assert_eq!(fs::remove(&format!("{}/sub", dir), false), Err(Errno::EISDIR)); assert_eq!(fs::remove(&path, true), Err(Errno::ENOTDIR)); assert_eq!(names(&dir), vec!["data.bin", "sub"]); fs::remove(&format!("{}/sub", dir), true).unwrap(); fs::remove(&path, false).unwrap(); fs::remove(&dir, true).unwrap(); assert_eq!(fs::lookup(&dir).err(), Some(Errno::ENOENT)); assert_eq!(free_counts(device), before); } #[test_case] fn write_1k_blocks() { write_round_trip("/mnt/disk1", "vdb"); } #[test_case] fn write_4k_blocks() { write_round_trip("/mnt/disk4", "vde"); } #[test_case] fn write_revision_0() { write_round_trip("/mnt/disk5", "vdf"); } #[test_case] fn many_files_grow_a_directory() { // 200 entries do not fit in one 1 KiB directory block: the directory // grows, and every name is still found afterwards. let before = free_counts("vdb"); fs::create("/mnt/disk1/many", InodeKind::Directory).unwrap(); for i in 0..200 { let f = fs::create(&format!("/mnt/disk1/many/file-number-{:03}.txt", i), InodeKind::File).unwrap(); f.write_at(0, format!("{}\n", i).as_bytes()).unwrap(); } assert!(fs::lookup("/mnt/disk1/many").unwrap().size() == 200); assert_eq!(read("/mnt/disk1/many/file-number-123.txt", 0, 10), b"123\n"); for i in 0..200 { fs::remove(&format!("/mnt/disk1/many/file-number-{:03}.txt", i), false).unwrap(); } fs::remove("/mnt/disk1/many", true).unwrap(); assert_eq!(free_counts("vdb"), before); } #[test_case] fn copy_from_fat_to_ext2() { // What `cp B:/notes/todo.txt C:/` does. let todo = fs::read_all("/mnt/disk0/notes/todo.txt").unwrap(); let copy = fs::create("/mnt/disk1/todo.txt", InodeKind::File).unwrap(); copy.write_at(0, &todo).unwrap(); assert_eq!(fs::read_all("/mnt/disk1/todo.txt").unwrap(), todo); assert_eq!(copy.mode(), 0o644); } #[panic_handler] fn panic(info: &PanicInfo) -> ! { rustbelt::test_panic_handler(info) }