RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / fs / file.rs
16725 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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! Open files: what a file descriptor refers to.
//!
//! `File` is the interface the read/write/lseek/fstat/getdents syscalls use.
//! It's separate from `Inode` because the same inode can be open several
//! times with different offsets, and because some files aren't in any file
//! system at all (the console, pipes). Linux calls this `struct file` +
//! `file_operations`.

use super::{Inode, InodeKind};
use crate::errno::Errno;
use alloc::{string::String, sync::Arc};
use spin::Mutex;
use core::sync::atomic::{AtomicBool, Ordering};

/// What a `File` or inode tells the kernel about itself. Our own small
/// layout; the syscalls turn it into Linux's `struct stat` (below), which
/// is what user programs get.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Stat {
    pub kind: u64, // one of the KIND_* constants
    pub size: u64, // bytes (files), entries (directories)
    pub mode: u64, // permission bits like 0o644 (0 = the file system has none)
}

pub const KIND_FILE: u64 = 1;
pub const KIND_DIR: u64 = 2;
pub const KIND_CONSOLE: u64 = 3;
pub const KIND_PIPE: u64 = 4;
pub const KIND_SYMLINK: u64 = 5;
pub const KIND_SOCKET: u64 = 6;
pub const KIND_DEVICE: u64 = 7; // /dev/null, /dev/fb0, ...

impl Stat {
    pub fn of(inode: &dyn Inode) -> Stat {
        let kind = match inode.kind() {
            InodeKind::File => KIND_FILE,
            InodeKind::Directory => KIND_DIR,
            InodeKind::Symlink => KIND_SYMLINK,
            InodeKind::Device => KIND_DEVICE,
        };
        Stat { kind, size: inode.size() as u64, mode: inode.mode() as u64 }
    }
}

/// Linux's `struct stat` on x86_64 (144 bytes), what stat/fstat/lstat/
/// newfstatat write to user memory since stage 15.1: a C library reads
/// these exact offsets. We fill in what we know and make up the rest the
/// way a simple Linux file system would.
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct LinuxStat {
    pub st_dev: u64,     // which file system (we say 1 for all)
    pub st_ino: u64,     // file number: we hash the path (see below)
    pub st_nlink: u64,   // hard links: 1 (directories 2)
    pub st_mode: u32,    // S_IF* type bits | permission bits
    pub st_uid: u32,     // owner: everything belongs to root (0)
    pub st_gid: u32,
    _pad0: u32,
    pub st_rdev: u64,    // device number for device files (0 here)
    pub st_size: i64,    // bytes
    pub st_blksize: i64, // good I/O size: 4096
    pub st_blocks: i64,  // 512-byte blocks used
    pub st_atime: [i64; 2], // last access (seconds, nanoseconds); 0 if the file system keeps no times
    pub st_mtime: [i64; 2], // last change of the contents
    pub st_ctime: [i64; 2], // last change of the inode (chmod, utimensat, writes)
    _unused: [i64; 3],
}

/// File type bits of `st_mode` (octal, as in <sys/stat.h>).
pub const S_IFIFO: u32 = 0o010000;
pub const S_IFCHR: u32 = 0o020000;
pub const S_IFDIR: u32 = 0o040000;
pub const S_IFREG: u32 = 0o100000;
pub const S_IFLNK: u32 = 0o120000;
pub const S_IFSOCK: u32 = 0o140000;

impl LinuxStat {
    /// `ino` should tell files apart: callers pass a hash of the path (or of
    /// the open file's address for pipes and sockets). Programs use
    /// (st_dev, st_ino) to spot "same file" (`cp a a`), nothing more.
    pub fn new(stat: &Stat, ino: u64) -> LinuxStat {
        let (ty, default_perm) = match stat.kind {
            KIND_DIR => (S_IFDIR, 0o755),
            KIND_SYMLINK => (S_IFLNK, 0o777),
            KIND_CONSOLE | KIND_DEVICE => (S_IFCHR, 0o666),
            KIND_PIPE => (S_IFIFO, 0o600),
            KIND_SOCKET => (S_IFSOCK, 0o777),
            _ => (S_IFREG, 0o644),
        };
        let perm = if stat.mode == 0 { default_perm } else { stat.mode as u32 & 0o7777 };
        LinuxStat {
            st_dev: 1,
            st_ino: ino.max(1), // 0 means "no inode" to some programs
            st_nlink: if stat.kind == KIND_DIR { 2 } else { 1 },
            st_mode: ty | perm,
            st_size: stat.size as i64,
            st_blksize: 4096,
            st_blocks: stat.size.div_ceil(512) as i64,
            ..Default::default()
        }
    }

    /// Fill in the times (`Inode::times`, stage 16), if there are any.
    pub fn with_times(mut self, times: Option<[[i64; 2]; 3]>) -> LinuxStat {
        if let Some([a, m, c]) = times {
            self.st_atime = a;
            self.st_mtime = m;
            self.st_ctime = c;
        }
        self
    }
}

/// `struct stat` for an inode reached by `path` (stage 17): the real
/// inode number, link count and owner where the file system has them
/// (ramfs, ext2, btrfs), the path hash and made-up values where it doesn't
/// (FAT, exFAT, /dev), and which mounted file system it's on (st_dev).
pub fn linux_stat(inode: &dyn Inode, path: &str) -> LinuxStat {
    let mut st = LinuxStat::new(&Stat::of(inode), inode.ino().unwrap_or_else(|| ino_hash(path.as_bytes()))).with_times(inode.times());
    if let Some(n) = inode.nlink() {
        st.st_nlink = n.max(1);
    }
    (st.st_uid, st.st_gid) = inode.owner();
    st.st_dev = super::dev_of(path);
    st
}

/// The inode number getdents reports for entry `name` of the directory at
/// `dir`: the file system's own, or the same path hash `linux_stat` uses.
fn entry_ino(dir: &str, entry: &super::DirEntry) -> u64 {
    if entry.ino != 0 {
        return entry.ino;
    }
    let path = if dir == "/" { alloc::format!("/{}", entry.name) } else { alloc::format!("{}/{}", dir, entry.name) };
    ino_hash(path.as_bytes()).max(1)
}

/// A 64-bit FNV-1a hash: our stand-in for inode numbers (from the path).
pub fn ino_hash(bytes: &[u8]) -> u64 {
    bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h, &b| (h ^ b as u64).wrapping_mul(0x100_0000_01b3))
}

/// `whence` values for `seek`, as in Linux's lseek.
pub const SEEK_SET: u64 = 0;
pub const SEEK_CUR: u64 = 1;
pub const SEEK_END: u64 = 2;

pub trait File: Send + Sync {
    /// Read up to `buf.len()` bytes; 0 means end of file.
    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno>;
    /// Write bytes; returns how many were written.
    fn write(&self, buf: &[u8]) -> Result<usize, Errno>;
    /// Move the offset; returns the new offset. Consoles and pipes can't.
    fn seek(&self, _offset: i64, _whence: u64) -> Result<u64, Errno> {
        Err(Errno::ESPIPE)
    }
    fn stat(&self) -> Result<Stat, Errno>;
    /// The absolute path this was opened by, if it was (files and
    /// directories): for openat & co. relative to a directory fd, fchdir,
    /// and fstat's inode number.
    fn path(&self) -> Option<String> {
        None
    }
    /// Fill `buf` with Linux `dirent64` records for the next directory
    /// entries; returns bytes used, 0 at the end.
    /// Socket syscalls (connect, accept, ...) need the socket behind an fd;
    /// everything that isn't a socket says "no" (ENOTSOCK).
    fn as_socket(&self) -> Option<&crate::net::socket::Socket> {
        None
    }
    fn getdents(&self, _buf: &mut [u8]) -> Result<usize, Errno> {
        Err(Errno::ENOTDIR)
    }
    /// The framebuffer ioctls need /dev/fb0 behind the fd.
    fn as_framebuffer(&self) -> Option<&crate::dev::Framebuffer> {
        None
    }
    /// mmap of a file needs the regular file's inode behind the fd.
    fn inode(&self) -> Option<Arc<dyn Inode>> {
        None
    }

    // --- stage 15: what event loops and a C library need -----------------

    /// Readiness right now, as poll(2) bits (crate::poll::POLLIN...): can a
    /// read / a write go ahead without blocking? Files that never block
    /// (regular files, /dev/null) are always ready.
    fn poll(&self) -> u16 {
        crate::poll::POLLIN | crate::poll::POLLOUT
    }
    /// O_NONBLOCK (fcntl F_SETFL, ioctl FIONBIO): instead of blocking,
    /// fail with EAGAIN. It belongs to the open file, so dup'ed and
    /// inherited fds share it. Only files that can block keep it.
    fn set_nonblocking(&self, _on: bool) {}
    fn nonblocking(&self) -> bool {
        false
    }
    /// Device-specific ioctls (ttys: termios, window size...).
    fn ioctl(&self, _cmd: u64, _arg: u64) -> Result<u64, Errno> {
        Err(Errno::ENOTTY)
    }
    /// pread/pwrite: at an explicit offset, leaving the file offset alone.
    fn read_at(&self, _buf: &mut [u8], _offset: u64) -> Result<usize, Errno> {
        Err(Errno::ESPIPE)
    }
    fn write_at(&self, _buf: &[u8], _offset: u64) -> Result<usize, Errno> {
        Err(Errno::ESPIPE)
    }
    /// epoll_ctl / epoll_wait need the epoll instance behind the fd.
    fn as_epoll(&self) -> Option<&crate::poll::Epoll> {
        None
    }
    /// fcntl(F_GETFL): the access mode it was opened with (O_RDONLY 0,
    /// O_WRONLY 1, O_RDWR 2) | O_APPEND. (O_NONBLOCK comes from
    /// `nonblocking`.) Files that don't know say O_RDWR.
    fn status_flags(&self) -> u64 {
        2
    }
    /// fcntl(F_SETFL, O_APPEND on/off). Only files opened by path have it.
    fn set_append(&self, _on: bool) {}
    /// The inode's times, for fstat (stage 16).
    fn times(&self) -> Option<[[i64; 2]; 3]> {
        None
    }
    /// The inode behind an fd opened by path, file *or* directory (fchmod,
    /// futimens; `inode` above is regular files only, for mmap).
    fn vfs_inode(&self) -> Option<Arc<dyn Inode>> {
        None
    }
    /// The terminal behind the fd, if it is one (tty.rs).
    fn as_tty(&self) -> Option<&crate::tty::TtyFile> {
        None
    }
}

/// A file or directory opened through the VFS: inode + current position.
/// For directories the "position" is the index of the next entry to list.
pub struct InodeFile {
    inode: Arc<dyn Inode>,
    offset: Mutex<usize>,
    /// O_APPEND: every write goes to the current end of the file (`>>`).
    /// fcntl(F_SETFL) can switch it, so it's an atomic.
    append: AtomicBool,
    /// O_NONBLOCK: files on a disk never block, so it changes nothing, but
    /// F_GETFL reports what F_SETFL (or open) set, as on Linux.
    nonblock: AtomicBool,
    /// O_RDONLY / O_WRONLY / O_RDWR as passed to open (stage 16). Reads
    /// and writes don't check it yet; F_GETFL, ftruncate and record locks do.
    accmode: u64,
    /// The absolute path it was opened by (see `File::path`).
    path: String,
    /// Directories (stage 17): the listing, taken at the first getdents
    /// and kept until a rewind, so deleting entries while reading the
    /// directory (`rm -r`) doesn't make us skip any.
    listing: Mutex<Option<alloc::vec::Vec<super::DirEntry>>>,
}

impl Drop for InodeFile {
    /// The last fd for this open file is gone: drop its flock (stage 17).
    fn drop(&mut self) {
        super::lock::flock_release(self as *const InodeFile as usize);
    }
}

/// open() flag bits InodeFile looks at (Linux's values).
pub const O_ACCMODE: u64 = 3;
pub const O_RDONLY: u64 = 0;
pub const O_WRONLY: u64 = 1;
pub const O_RDWR: u64 = 2;
pub const O_APPEND: u64 = 0o2000;
pub const O_NONBLOCK: u64 = 0o4000;

impl InodeFile {
    /// Opened read/write (the kernel's own callers, tests).
    pub fn open(inode: Arc<dyn Inode>, path: String) -> Result<Self, Errno> {
        Self::open_flags(inode, path, O_RDWR)
    }

    pub fn open_append(inode: Arc<dyn Inode>, path: String) -> Result<Self, Errno> {
        Self::open_flags(inode, path, O_RDWR | O_APPEND)
    }

    /// With open()'s flags: the access mode and O_APPEND count.
    pub fn open_flags(inode: Arc<dyn Inode>, path: String, flags: u64) -> Result<Self, Errno> {
        let accmode = match flags & O_ACCMODE {
            3 => O_RDWR, // Linux's odd "3": treat as read/write
            m => m,
        };
        Ok(InodeFile {
            inode,
            offset: Mutex::new(0),
            append: AtomicBool::new(flags & O_APPEND != 0),
            nonblock: AtomicBool::new(flags & O_NONBLOCK != 0),
            accmode,
            path,
            listing: Mutex::new(None),
        })
    }
}

impl File for InodeFile {
    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {
        let mut offset = self.offset.lock();
        let n = self.inode.read_at(*offset, buf)?;
        *offset += n;
        Ok(n)
    }

    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {
        let mut offset = self.offset.lock();
        if self.append.load(Ordering::Relaxed) {
            *offset = self.inode.size();
        }
        let n = self.inode.write_at(*offset, buf)?;
        *offset += n;
        Ok(n)
    }

    fn seek(&self, offset: i64, whence: u64) -> Result<u64, Errno> {
        if self.inode.kind() == InodeKind::Directory {
            // The offset is an entry index (what getdents puts in d_off),
            // so seekdir/telldir work. Back to 0 (rewinddir) re-reads it.
            if whence != SEEK_SET || offset < 0 {
                return if whence == SEEK_CUR && offset == 0 { Ok(*self.offset.lock() as u64) } else { Err(Errno::EINVAL) };
            }
            let mut listing = self.listing.lock();
            if offset == 0 {
                *listing = None;
            }
            *self.offset.lock() = offset as usize;
            return Ok(offset as u64);
        }
        let mut current = self.offset.lock();
        let base = match whence {
            SEEK_SET => 0,
            SEEK_CUR => *current as i64,
            SEEK_END => self.inode.size() as i64,
            _ => return Err(Errno::EINVAL),
        };
        let new = base.checked_add(offset).filter(|&n| n >= 0).ok_or(Errno::EINVAL)?;
        *current = new as usize;
        Ok(new as u64)
    }

    fn stat(&self) -> Result<Stat, Errno> {
        Ok(Stat::of(&*self.inode))
    }

    fn path(&self) -> Option<String> {
        Some(self.path.clone())
    }

    fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<usize, Errno> {
        match self.inode.kind() {
            InodeKind::Directory => Err(Errno::EISDIR),
            _ => self.inode.read_at(offset as usize, buf),
        }
    }

    fn write_at(&self, buf: &[u8], offset: u64) -> Result<usize, Errno> {
        match self.inode.kind() {
            InodeKind::Directory => Err(Errno::EISDIR),
            _ => self.inode.write_at(offset as usize, buf),
        }
    }

    fn inode(&self) -> Option<Arc<dyn Inode>> {
        (self.inode.kind() == InodeKind::File).then(|| self.inode.clone())
    }

    fn status_flags(&self) -> u64 {
        self.accmode | if self.append.load(Ordering::Relaxed) { O_APPEND } else { 0 }
    }

    fn set_append(&self, on: bool) {
        self.append.store(on, Ordering::Relaxed);
    }

    fn set_nonblocking(&self, on: bool) {
        self.nonblock.store(on, Ordering::Relaxed);
    }

    fn nonblocking(&self) -> bool {
        self.nonblock.load(Ordering::Relaxed)
    }

    fn times(&self) -> Option<[[i64; 2]; 3]> {
        self.inode.times()
    }

    fn vfs_inode(&self) -> Option<Arc<dyn Inode>> {
        Some(self.inode.clone())
    }

    fn getdents(&self, buf: &mut [u8]) -> Result<usize, Errno> {
        // List the directory once (stage 17; before, each call re-listed
        // it and skipped by index, which skipped entries when files were
        // deleted in between) and hand it out from that copy.
        let mut listing = self.listing.lock();
        if listing.is_none() {
            *listing = Some(self.inode.list()?);
        }
        let entries = listing.as_ref().unwrap();
        let mut index = self.offset.lock();
        let mut used = 0;
        for entry in entries.iter().skip(*index) {
            // struct linux_dirent64 { u64 ino; i64 off; u16 reclen; u8 type; char name[]; }
            // padded to a multiple of 8 bytes.
            let reclen = (19 + entry.name.len() + 1).next_multiple_of(8);
            if used + reclen > buf.len() {
                if used == 0 {
                    return Err(Errno::EINVAL); // buffer too small for even one entry
                }
                break;
            }
            let rec = &mut buf[used..used + reclen];
            rec.fill(0);
            rec[0..8].copy_from_slice(&entry_ino(&self.path, entry).to_le_bytes()); // the inode number stat gives
            rec[8..16].copy_from_slice(&(*index as i64 + 1).to_le_bytes()); // offset of next
            rec[16..18].copy_from_slice(&(reclen as u16).to_le_bytes());
            rec[18] = match entry.kind {
                InodeKind::Directory => 4, // DT_DIR
                InodeKind::File => 8,      // DT_REG
                InodeKind::Symlink => 10,  // DT_LNK
                InodeKind::Device => 2,    // DT_CHR (a "character device")
            };
            rec[19..19 + entry.name.len()].copy_from_slice(entry.name.as_bytes());
            used += reclen;
            *index += 1;
        }
        Ok(used)
    }
}