RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / tests / vfs.rs
6369 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! 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);
    rustbelt::init_smp(); // all 4 CPUs run threads
    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(), 4); // "/", /dev (devfs), /tmp, /tmp/deeper
}

#[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<String> = fs::lookup("/").unwrap().list().unwrap().into_iter().map(|e| e.name).collect();
    assert_eq!(names, vec!["dev", "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, mode: 0 }));
    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)
}