//! VFS + ramfs, used directly from the kernel (no user mode involved). #![no_std] #![no_main] #![feature(custom_test_frameworks)] #![test_runner(rustbelt::test_runner)] #![reexport_test_harness_main = "test_main"] extern crate alloc; use alloc::{string::String, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rustbelt::errno::Errno; use rustbelt::fs::file::{File, InodeFile, KIND_FILE, SEEK_CUR, SEEK_END, SEEK_SET, Stat}; use rustbelt::fs::{self, InodeKind, ramfs::RamFs}; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rustbelt::init(); rustbelt::init_memory(boot_info); fs::init(); test_main(); rustbelt::hlt_loop(); } #[test_case] fn read_boot_files() { let file = InodeFile::open(fs::lookup("/hello.txt").unwrap()).unwrap(); let mut buf = [0u8; 64]; let n = file.read(&mut buf).unwrap(); assert!(buf[..n].starts_with(b"Hello from the ramfs!")); assert_eq!(file.read(&mut buf), Ok(0)); // EOF assert_eq!(fs::lookup("/etc/motd").unwrap().kind(), InodeKind::File); assert_eq!(fs::lookup("/etc").unwrap().kind(), InodeKind::Directory); assert_eq!(fs::lookup("//etc/./motd").unwrap().kind(), InodeKind::File); } #[test_case] fn lookup_errors() { assert_eq!(fs::lookup("/nope").err(), Some(Errno::ENOENT)); assert_eq!(fs::lookup("/hello.txt/x").err(), Some(Errno::ENOTDIR)); assert_eq!(fs::lookup("relative").err(), Some(Errno::EINVAL)); // Directories can be opened (for getdents) but not read like files. let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap(); assert_eq!(dir.read(&mut [0u8; 8]), Err(Errno::EISDIR)); } #[test_case] fn mounts_are_separate_file_systems() { // /tmp is its own ramfs: files created there don't show up in "/"'s tmp dir. fs::create("/tmp/a.txt", InodeKind::File).unwrap(); assert!(fs::lookup("/tmp/a.txt").is_ok()); let root_tmp_dir = fs::lookup("/").unwrap().lookup("tmp").unwrap(); assert_eq!(root_tmp_dir.list().unwrap().len(), 0); assert_eq!(fs::create("/tmp/a.txt", InodeKind::File).err(), Some(Errno::EEXIST)); // Mount a third one; the longest matching mount point wins. fs::create("/tmp/deeper", InodeKind::Directory).unwrap(); fs::mount("/tmp/deeper", RamFs::new()).unwrap(); assert_eq!(fs::lookup("/tmp/deeper").unwrap().list().unwrap().len(), 0); assert_eq!(fs::mount("/tmp/deeper", RamFs::new()).err(), Some(Errno::EEXIST)); assert_eq!(fs::mounts().len(), 3); } #[test_case] fn write_grow_and_list() { let inode = fs::create("/tmp/grow.bin", InodeKind::File).unwrap(); inode.write_at(4, b"abc").unwrap(); // gap is filled with zeros let mut buf = vec![0xffu8; 10]; assert_eq!(inode.read_at(0, &mut buf), Ok(7)); assert_eq!(&buf[..7], b"\0\0\0\0abc"); let names: Vec = fs::lookup("/").unwrap().list().unwrap().into_iter().map(|e| e.name).collect(); assert_eq!(names, vec!["etc", "hello.txt", "mnt", "tmp"]); } #[test_case] fn paths_are_normalized() { assert_eq!(fs::normalize("/", "a/b").unwrap(), "/a/b"); assert_eq!(fs::normalize("/home/tyler", "../etc//./motd").unwrap(), "/home/etc/motd"); assert_eq!(fs::normalize("/x", "/../../y/").unwrap(), "/y"); // ".." of "/" is "/" assert_eq!(fs::normalize("/x", "..").unwrap(), "/"); assert_eq!(fs::normalize("/", "").err(), Some(Errno::ENOENT)); // ".." works across mount points: /tmp/.. is the root ramfs again. assert_eq!(fs::lookup("/tmp/../etc/motd").unwrap().kind(), InodeKind::File); } #[test_case] fn remove_files_and_directories() { fs::create("/tmp/rm", InodeKind::Directory).unwrap(); fs::create("/tmp/rm/f", InodeKind::File).unwrap(); assert_eq!(fs::remove("/tmp/rm", true), Err(Errno::ENOTEMPTY)); assert_eq!(fs::remove("/tmp/rm", false), Err(Errno::EISDIR)); // unlink a dir assert_eq!(fs::remove("/tmp/rm/f", true), Err(Errno::ENOTDIR)); // rmdir a file fs::remove("/tmp/rm/f", false).unwrap(); fs::remove("/tmp/rm", true).unwrap(); assert_eq!(fs::lookup("/tmp/rm").err(), Some(Errno::ENOENT)); assert_eq!(fs::remove("/tmp", true), Err(Errno::EBUSY)); // mount point } #[test_case] fn seek_stat_and_truncate() { let inode = fs::create("/tmp/seek.txt", InodeKind::File).unwrap(); let file = InodeFile::open(inode.clone()).unwrap(); file.write(b"0123456789").unwrap(); assert_eq!(file.seek(-3, SEEK_END), Ok(7)); let mut buf = [0u8; 8]; assert_eq!(file.read(&mut buf), Ok(3)); assert_eq!(&buf[..3], b"789"); assert_eq!(file.seek(2, SEEK_SET), Ok(2)); assert_eq!(file.seek(1, SEEK_CUR), Ok(3)); assert_eq!(file.seek(-4, SEEK_CUR), Err(Errno::EINVAL)); // before the start assert_eq!(file.stat(), Ok(Stat { kind: KIND_FILE, size: 10 })); inode.truncate().unwrap(); assert_eq!(inode.size(), 0); } #[test_case] fn getdents_returns_linux_dirent64_records() { let dir = InodeFile::open(fs::lookup("/etc").unwrap()).unwrap(); let mut buf = [0u8; 64]; let n = dir.getdents(&mut buf).unwrap(); // One entry, "motd": 19-byte header + "motd\0" = 24 bytes (already 8-aligned). assert_eq!(n, 24); assert_eq!(u16::from_le_bytes([buf[16], buf[17]]), 24); // d_reclen assert_eq!(buf[18], 8); // DT_REG assert_eq!(&buf[19..24], b"motd\0"); assert_eq!(dir.getdents(&mut buf), Ok(0)); // end of directory assert_eq!(dir.seek(0, SEEK_SET), Ok(0)); // rewind assert_eq!(dir.getdents(&mut [0u8; 10]), Err(Errno::EINVAL)); // too small } #[test_case] fn pipes_pass_bytes_and_report_eof() { let (reader, writer) = fs::pipe::new(); assert_eq!(writer.write(b"through the pipe"), Ok(16)); let mut buf = [0u8; 7]; assert_eq!(reader.read(&mut buf), Ok(7)); assert_eq!(&buf, b"through"); assert_eq!(reader.seek(0, SEEK_SET), Err(Errno::ESPIPE)); drop(writer); // last writer gone: remaining data, then EOF (no blocking) let mut rest = [0u8; 32]; assert_eq!(reader.read(&mut rest), Ok(9)); assert_eq!(reader.read(&mut rest), Ok(0)); let (reader, writer) = fs::pipe::new(); drop(reader); assert_eq!(writer.write(b"x"), Err(Errno::EPIPE)); } #[panic_handler] fn panic(info: &PanicInfo) -> ! { rustbelt::test_panic_handler(info) }