RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / fs / mod.rs
10547 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! 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,
//!   list and remove children. ramfs and FAT32 implement it today; ext2, exFAT
//!   and btrfs will later.
//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on vda1");
//!   gives you its root inode.
//! * the **mount table** -- which file system is attached at which path ("/",
//!   "/tmp", "/mnt/disk0"). `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 the console, or a pipe. The per-process
//!   fd table holds `Arc<dyn File>`s, so syscalls don't care what's behind it.
//!
//! Paths: every path is first turned into a clean absolute path by
//! [`normalize`] (relative paths are joined to the process's current
//! directory; ".", ".." and "//" are resolved by plain string processing).
//! That makes ".." work across mount points for free -- real kernels track
//! parent dentries instead, which also handles symlinks (we have none).

pub mod fat;
pub mod file;
pub mod pipe;
pub mod ramfs;

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

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

/// One entry of a directory listing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {
    pub name: String,
    pub kind: InodeKind,
}

/// 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)
    }
    /// Cut the file to length 0 (for `open(..., O_TRUNC)`, i.e. shell `>`).
    fn truncate(&self) -> Result<(), 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)
    }
    /// All children of this directory (without "." and "..").
    fn list(&self) -> Result<Vec<DirEntry>, Errno> {
        Err(Errno::ENOTDIR)
    }
    /// Delete a child (a file, or an *empty* directory).
    fn remove(&self, _name: &str) -> Result<(), 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` (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 = normalize("/", 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'/'))
}

/// Turn `path` into a clean absolute path. Relative paths start at `cwd`
/// (which must be absolute). "." is dropped, ".." removes the previous
/// component (".." of "/" is "/", like Unix).
///   normalize("/home", "../etc//motd") == "/etc/motd"
pub fn normalize(cwd: &str, path: &str) -> Result<String, Errno> {
    if path.is_empty() {
        return Err(Errno::ENOENT); // like Linux: "" is not a path
    }
    let mut parts: Vec<&str> = Vec::new();
    let joined = if path.starts_with('/') { String::from(path) } else { format!("{}/{}", cwd, path) };
    for component in joined.split('/') {
        match component {
            "" | "." => {}
            ".." => {
                parts.pop();
            }
            name if name.len() > 255 => return Err(Errno::ENAMETOOLONG),
            name => parts.push(name),
        }
    }
    let result = format!("/{}", parts.join("/"));
    if result.len() > 4096 {
        return Err(Errno::ENAMETOOLONG);
    }
    Ok(result)
}

/// 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); // callers resolve relative paths first
    }
    let path = normalize("/", path)?;
    // 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 = String::from(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()) {
        inode = inode.lookup(name)?;
    }
    Ok(inode)
}

/// Split a normalized path into (parent directory inode, last component).
fn parent_of(path: &str) -> Result<(Arc<dyn Inode>, String), Errno> {
    let path = normalize("/", path)?;
    let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?;
    if name.is_empty() {
        return Err(Errno::EEXIST); // "/" itself
    }
    Ok((lookup(if parent.is_empty() { "/" } else { parent })?, String::from(name)))
}

/// Create a file or directory at absolute `path` (parent must exist).
pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {
    let (parent, name) = parent_of(path)?;
    parent.create(&name, kind)
}

/// Delete the file (`want_dir` false, like `unlink`) or empty directory
/// (`want_dir` true, like `rmdir`) at absolute `path`.
pub fn remove(path: &str, want_dir: bool) -> Result<(), Errno> {
    let normalized = normalize("/", path)?;
    if MOUNTS.lock().iter().any(|m| m.path == normalized) {
        return Err(Errno::EBUSY); // can't delete a mount point
    }
    let inode = lookup(&normalized)?;
    match (inode.kind(), want_dir) {
        (InodeKind::Directory, false) => return Err(Errno::EISDIR),
        (InodeKind::File, true) => return Err(Errno::ENOTDIR),
        _ => {}
    }
    let (parent, name) = parent_of(&normalized)?;
    parent.remove(&name)
}

/// Read a whole file into memory (used to load programs).
pub fn read_all(path: &str) -> Result<Vec<u8>, Errno> {
    let inode = lookup(path)?;
    if inode.kind() == InodeKind::Directory {
        return Err(Errno::EISDIR);
    }
    let mut data = alloc::vec![0u8; inode.size()];
    let mut done = 0;
    while done < data.len() {
        match inode.read_at(done, &mut data[done..])? {
            0 => break,
            n => done += n,
        }
    }
    data.truncate(done);
    Ok(data)
}

/// 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: ext2, exFAT, btrfs.\n").unwrap();
    dir.create("tmp", InodeKind::Directory).unwrap(); // mount point
    dir.create("mnt", InodeKind::Directory).unwrap(); // disks go to /mnt/diskN
    mount("/", root).unwrap();
    mount("/tmp", ramfs::RamFs::new()).unwrap();
}

/// Try to mount every registered partition (and unpartitioned disk) that
/// holds a FAT file system at /mnt/disk0, /mnt/disk1, ... Returns log lines.
/// (Later: also probe for ext2, exFAT, btrfs superblocks here.)
pub fn mount_disks() -> Vec<String> {
    let mut log = Vec::new();
    let devices = crate::block::devices();
    let mut n = 0;
    for (name, dev) in &devices {
        // Skip a whole disk if it has partitions ("vda" when "vda1" exists).
        let has_partitions = devices.iter().any(|(other, _)| other.len() > name.len() && other.starts_with(name.as_str()));
        if has_partitions {
            continue;
        }
        match fat::FatFs::mount(dev.clone()) {
            Ok(fs) => {
                let path = format!("/mnt/disk{}", n);
                let _ = create(&path, InodeKind::Directory); // mount point (may exist)
                let label = fs.label();
                match mount(&path, fs) {
                    Ok(()) => log.push(format!("{}: FAT32 volume \"{}\" mounted at {}", name, label, path)),
                    Err(e) => log.push(format!("{}: mount at {} failed: {:?}", name, path, e)),
                }
                n += 1;
            }
            Err(e) => log.push(format!("{}: not a FAT file system ({:?})", name, e)),
        }
    }
    log
}