//! 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 ). 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; /// Write bytes; returns how many were written. fn write(&self, buf: &[u8]) -> Result; /// Move the offset; returns the new offset. Consoles and pipes can't. fn seek(&self, _offset: i64, _whence: u64) -> Result { Err(Errno::ESPIPE) } fn stat(&self) -> Result; /// 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 { 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 { 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> { 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 { Err(Errno::ENOTTY) } /// pread/pwrite: at an explicit offset, leaving the file offset alone. fn read_at(&self, _buf: &mut [u8], _offset: u64) -> Result { Err(Errno::ESPIPE) } fn write_at(&self, _buf: &[u8], _offset: u64) -> Result { 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> { 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, offset: Mutex, /// 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>>, } 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, path: String) -> Result { Self::open_flags(inode, path, O_RDWR) } pub fn open_append(inode: Arc, path: String) -> Result { 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, path: String, flags: u64) -> Result { 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 { let mut offset = self.offset.lock(); let n = self.inode.read_at(*offset, buf)?; *offset += n; Ok(n) } fn write(&self, buf: &[u8]) -> Result { 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 { 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 { Ok(Stat::of(&*self.inode)) } fn path(&self) -> Option { Some(self.path.clone()) } fn read_at(&self, buf: &mut [u8], offset: u64) -> Result { 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 { match self.inode.kind() { InodeKind::Directory => Err(Errno::EISDIR), _ => self.inode.write_at(offset as usize, buf), } } fn inode(&self) -> Option> { (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> { Some(self.inode.clone()) } fn getdents(&self, buf: &mut [u8]) -> Result { // 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) } }