RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / fs / mod.rs
6256 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
156
157
158
159
160
161
162
//! VFS (virtual file system): the layer every concrete file system plugs into.
//!
//! Modelled (very loosely) on Linux:
//!
//! * [`Inode`] -- one file or directory *inside some file system*. Knows how to
//!   read/write its bytes at an offset and (for directories) look up / create
//!   children. A FAT32, ext2, exFAT or btrfs driver implements this trait.
//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on disk 0");
//!   gives you its root inode.
//! * the **mount table** -- which file system is attached at which path ("/",
//!   "/tmp", ...). `lookup("/tmp/x")` finds the longest matching mount point
//!   and walks the rest of the path from that file system's root.
//! * [`file::File`] -- an *open* file (what a file descriptor points to): an
//!   inode plus a current offset, or a device like the console. The per-process
//!   fd table holds `Arc<dyn File>`s, so syscalls don't care what's behind it.
//!
//! Today there is one implementation, [`ramfs`] (everything in RAM). Later: a
//! block device layer (virtio-blk / ATA PIO), then FAT32, ext2, exFAT and
//! btrfs (read-only first) as further `FileSystem`s.

pub mod file;
pub mod ramfs;

use crate::errno::Errno;
use alloc::{string::String, sync::Arc, vec::Vec};
use spin::Mutex;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InodeKind {
    File,
    Directory,
}

/// A file or directory inside a file system.
///
/// Default method bodies return the error a "wrong kind" of inode would give
/// (e.g. `lookup` on a file -> ENOTDIR), so implementations only override
/// what makes sense for them.
pub trait Inode: Send + Sync {
    fn kind(&self) -> InodeKind;
    /// Size in bytes (files) or number of entries (directories).
    fn size(&self) -> usize;
    /// Read bytes starting at `offset`; returns how many were read (0 = EOF).
    fn read_at(&self, _offset: usize, _buf: &mut [u8]) -> Result<usize, Errno> {
        Err(Errno::EISDIR)
    }
    /// Write bytes at `offset` (growing the file if needed).
    fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result<usize, Errno> {
        Err(Errno::EISDIR)
    }
    /// Find a child of this directory by name.
    fn lookup(&self, _name: &str) -> Result<Arc<dyn Inode>, Errno> {
        Err(Errno::ENOTDIR)
    }
    /// Create a child of this directory.
    fn create(&self, _name: &str, _kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {
        Err(Errno::ENOTDIR)
    }
    /// Names of all children of this directory.
    fn list(&self) -> Result<Vec<String>, Errno> {
        Err(Errno::ENOTDIR)
    }
}

/// A mounted file system.
pub trait FileSystem: Send + Sync {
    /// Type name, like the `-t` argument of `mount` ("ramfs", "fat32", ...).
    fn name(&self) -> &'static str;
    fn root(&self) -> Arc<dyn Inode>;
}

struct Mount {
    path: String, // normalized, e.g. "/" or "/tmp"
    fs: Arc<dyn FileSystem>,
}

/// The mount table. Lookups take the lock only briefly (never while doing
/// I/O), so it's fine as a spinlock. Not used from interrupt handlers.
static MOUNTS: Mutex<Vec<Mount>> = Mutex::new(Vec::new());

/// Attach `fs` at `path` (must be absolute, like "/" or "/tmp").
pub fn mount(path: &str, fs: Arc<dyn FileSystem>) -> Result<(), Errno> {
    if !path.starts_with('/') {
        return Err(Errno::EINVAL);
    }
    let path = String::from(if path.len() > 1 { path.trim_end_matches('/') } else { path });
    let mut mounts = MOUNTS.lock();
    if mounts.iter().any(|m| m.path == path) {
        return Err(Errno::EEXIST);
    }
    mounts.push(Mount { path, fs });
    Ok(())
}

/// (mount path, fs type) of every mount, for printing.
pub fn mounts() -> Vec<(String, &'static str)> {
    MOUNTS.lock().iter().map(|m| (m.path.clone(), m.fs.name())).collect()
}

/// Does mount point `mount` contain `path`? ("/tmp" contains "/tmp/x" but
/// not "/tmpfoo"; "/" contains everything.)
fn mount_contains(mount: &str, path: &str) -> bool {
    mount == "/"
        || path == mount
        || (path.starts_with(mount) && path.as_bytes().get(mount.len()) == Some(&b'/'))
}

/// Resolve an absolute path to an inode.
pub fn lookup(path: &str) -> Result<Arc<dyn Inode>, Errno> {
    if !path.starts_with('/') {
        return Err(Errno::EINVAL); // no current directory yet
    }
    if path.len() > 255 {
        return Err(Errno::ENAMETOOLONG);
    }
    // Longest mount point that contains the path wins.
    let (root, rest) = {
        let mounts = MOUNTS.lock();
        let m = mounts
            .iter()
            .filter(|m| mount_contains(&m.path, path))
            .max_by_key(|m| m.path.len())
            .ok_or(Errno::ENOENT)?;
        let rest = if m.path == "/" { path } else { &path[m.path.len()..] };
        (m.fs.root(), rest)
    };
    // Walk the remaining components ("a/b/c") from that file system's root.
    let mut inode = root;
    for name in rest.split('/').filter(|c| !c.is_empty() && *c != ".") {
        if name == ".." {
            return Err(Errno::EINVAL); // not supported yet
        }
        inode = inode.lookup(name)?;
    }
    Ok(inode)
}

/// Create a file at `path` (its parent directory must exist).
pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {
    let path = path.trim_end_matches('/');
    let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?;
    if name.is_empty() {
        return Err(Errno::EINVAL);
    }
    let parent = lookup(if parent.is_empty() { "/" } else { parent })?;
    parent.create(name, kind)
}

/// Boot-time file system setup: a ramfs as "/", with a few files, and a
/// second, empty ramfs mounted at "/tmp" (to exercise the mount table).
pub fn init() {
    let root = ramfs::RamFs::new();
    let dir = root.root();
    let hello = dir.create("hello.txt", InodeKind::File).unwrap();
    hello.write_at(0, b"Hello from the ramfs! This is /hello.txt.\n").unwrap();
    let etc = dir.create("etc", InodeKind::Directory).unwrap();
    let motd = etc.create("motd", InodeKind::File).unwrap();
    motd.write_at(0, b"Welcome to Tyler's kernel. Next up: FAT32, ext2, exFAT, btrfs.\n").unwrap();
    dir.create("tmp", InodeKind::Directory).unwrap(); // mount point
    mount("/", root).unwrap();
    mount("/tmp", ramfs::RamFs::new()).unwrap();
}