The filesystem every other system can read: a USB stick, an SD card, the EFI system partition. It is here first for the hardware report, which has to leave a machine on something that can be read on another one. mount -t vfat /dev/usb0p1 /mnt (msdos and fat work too; -o ro) Written from Microsoft's specification, and matched to Linux vfat where the specification leaves a choice: 8.3 names with the NT lowercase bits, long-name entries otherwise, Linux's alias rule (REPORTS, then BIGNUM~1), case-insensitive lookup, UTC times, the boot-sector dirty bit, every FAT copy and FSInfo kept up to date. It uses its own sector cache over the new blkdev_xfer, since a partition need not be a whole number of 4 KB blocks, and every operation that changes something reaches the disk before it returns. Inode numbers come from the entry position, or the first cluster for a directory, plus a small table for entries that moved. Around it: - /dev lists every disk and partition the block layer found (major 8, minor = block device), and raw reads and writes on them go by sector. - The ESP and MBR FAT partitions are registered, typed so the root probe passes over them. MBR entries with a bad status byte are skipped, so a superfloppy's boot code is not read as a partition table. - statfs, sync, fsync, umount (EBUSY while in use), shutdown, reboot and the power button all know about FAT. - create undoes and reports a failed ialloc or dirlink (ENOSPC, EINVAL, EPERM) instead of panicking; fs_ops gains refuse_types and no_hardlinks. - filewrite returns a short write instead of panicking on it. - utimensat is implemented. As a stub returning 0 it made toybox touch believe the file existed, so `touch newfile` created nothing on any filesystem. Tested in QEMU against mkfs.vfat images (FAT12 floppy, FAT16 on MBR, FAT32 superfloppy and GPT ESP) by writing, renaming, deleting, filling to ENOSPC and truncating there, then checking with fsck.fat -n and mtools on the host: all clean, contents match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
342 lines
9.4 KiB
C
342 lines
9.4 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "stat.h"
|
|
#include "param.h"
|
|
#include "fs.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "file.h"
|
|
#include "fsstat.h"
|
|
#include "posix.h"
|
|
#include "errno.h"
|
|
|
|
extern struct devsw devsw[];
|
|
|
|
struct {
|
|
struct spinlock lock;
|
|
struct file file[NFILE];
|
|
} ftable;
|
|
|
|
void
|
|
fileinit(void)
|
|
{
|
|
initlock(&ftable.lock, "ftable");
|
|
}
|
|
|
|
struct file*
|
|
filealloc(void)
|
|
{
|
|
struct file *f;
|
|
|
|
acquire(&ftable.lock);
|
|
for(f = ftable.file; f < ftable.file + NFILE; f++){
|
|
if(f->ref == 0){
|
|
f->ref = 1;
|
|
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1
|
|
// Cleared here rather than left to the caller: every creator sets
|
|
// readable/writable/off, but only open() sets nonblock, so a struct file
|
|
// recycled from a non-blocking device would hand its flag to the next
|
|
// pipe or socket that happened to land in this slot.
|
|
f->nonblock = 0;
|
|
release(&ftable.lock);
|
|
return f;
|
|
}
|
|
}
|
|
release(&ftable.lock);
|
|
return 0;
|
|
}
|
|
|
|
struct file*
|
|
filedup(struct file *f)
|
|
{
|
|
acquire(&ftable.lock);
|
|
if(f->ref < 1)
|
|
panic("filedup");
|
|
f->ref++;
|
|
release(&ftable.lock);
|
|
return f;
|
|
}
|
|
|
|
void
|
|
fileclose(struct file *f)
|
|
{
|
|
struct file ff;
|
|
|
|
acquire(&ftable.lock);
|
|
if(f->ref < 1)
|
|
panic("fileclose");
|
|
if(--f->ref > 0){
|
|
release(&ftable.lock);
|
|
return;
|
|
}
|
|
ff = *f;
|
|
f->ref = 0;
|
|
f->type = FD_NONE;
|
|
FSSTAT_DROP(fsstat.file_used); // 1 -> 0
|
|
release(&ftable.lock);
|
|
|
|
if(ff.type == FD_MEMFD)
|
|
shm_detach(ff.memfd_seg); // the last mapper frees the pages
|
|
else if(ff.type == FD_PIPE)
|
|
pipeclose(ff.pipe, ff.writable);
|
|
else if(ff.type == FD_SOCKET)
|
|
sockclose(ff.sock);
|
|
else if(ff.type == FD_INODE){
|
|
// Give a device a chance to put the hardware back before the inode goes.
|
|
// Done here rather than in exit() because a program that closes the
|
|
// framebuffer without exiting has finished with it just as thoroughly.
|
|
if(ff.ip && ff.ip->type == T_DEV && ff.ip->major >= 0 &&
|
|
ff.ip->major < NDEV && devsw[ff.ip->major].close)
|
|
devsw[ff.ip->major].close(ff.ip);
|
|
|
|
begin_op();
|
|
iput(ff.ip);
|
|
end_op();
|
|
}
|
|
}
|
|
|
|
int
|
|
filestat(struct file *f, struct stat *st)
|
|
{
|
|
// A memfd has no inode, so the size is the only field with an answer. It is
|
|
// the field that matters: mmap length and ftruncate round-trips are checked
|
|
// against it, and a caller that fstat()s to find out how big the buffer is
|
|
// gets the truth.
|
|
if(f->type == FD_MEMFD){
|
|
memset(st, 0, sizeof(*st));
|
|
st->type = T_FILE;
|
|
st->size = memfd_size(f->memfd_seg);
|
|
st->nlink = 1;
|
|
return 0;
|
|
}
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
stati(f->ip, st);
|
|
iunlock(f->ip);
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
fileread(struct file *f, char *addr, int n)
|
|
{
|
|
int r;
|
|
|
|
if(f->readable == 0)
|
|
return -1;
|
|
|
|
// A memfd is a file: read() and write() move bytes in and out of the shared
|
|
// pages, and the descriptor carries its own offset like any other. mmap is
|
|
// what it is usually for, but a descriptor that refuses read() is a surprise
|
|
// nobody needs.
|
|
if(f->type == FD_MEMFD){
|
|
r = memfd_rw(f->memfd_seg, 1, (uint64)addr, f->off, n, 0);
|
|
if(r > 0)
|
|
f->off += r;
|
|
return r;
|
|
}
|
|
|
|
// O_NONBLOCK on a pipe or a socket. Reported here rather than inside
|
|
// piperead/sockread so that the blocking implementations stay unchanged:
|
|
// this is a check of the same condition their sleep loops wait on, made one
|
|
// level up. Writes still block -- fcntl documents that.
|
|
if(f->type == FD_PIPE){
|
|
// POLLHUP counts as readable here, and leaving it out was a bug: a pipe
|
|
// whose writers have all gone is not "nothing yet", it is end of file, and
|
|
// the answer to a read is 0. Told EAGAIN instead, a caller that polls and
|
|
// reads spins forever waiting for an end that has already happened.
|
|
// sockreadable() has always included the equivalent condition.
|
|
if(f->nonblock &&
|
|
!(pipe_poll(f->pipe, f->readable, f->writable) & (POLLIN | POLLHUP)))
|
|
return -EAGAIN;
|
|
return piperead(f->pipe, addr, n);
|
|
}
|
|
|
|
if(f->type == FD_SOCKET){
|
|
if(f->nonblock && !sockreadable(f->sock))
|
|
return -EAGAIN;
|
|
return sockread(f->sock, addr, n);
|
|
}
|
|
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
if(f->ip->type == 3){
|
|
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].read){
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
|
|
// O_NONBLOCK. Without this a game that opens an input device
|
|
// non-blocking and drains it once per frame stops dead on the first
|
|
// frame -- it blocks in read() waiting for a key, and the result looks
|
|
// exactly like a hung renderer rather than a blocking read.
|
|
if(f->nonblock && devsw[f->ip->major].ready &&
|
|
!devsw[f->ip->major].ready(f->ip)){
|
|
iunlock(f->ip);
|
|
return -EAGAIN;
|
|
}
|
|
|
|
r = devsw[f->ip->major].read(f->ip, f->off, addr, n);
|
|
if(r > 0)
|
|
f->off += r;
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
if((f->ip->rwmode & 2) == 0){
|
|
cprintf("SchOS: permission denied (read)\n");
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
|
|
// The "recycle bin" check that used to sit here is gone.
|
|
//
|
|
// It made a file that still existed, still had a name and still had blocks
|
|
// refuse every read with a printf -- a state no POSIX program is prepared
|
|
// for. It was reachable only through `delete`, a NO_LINUX syscall from the
|
|
// third-party filesystem this file derives from, which the decision to keep
|
|
// only Linux numbering had already orphaned.
|
|
//
|
|
// The on-disk `showmode` field stays for now: removing it changes the inode
|
|
// layout, and this whole file is being replaced by SchFS. It is simply
|
|
// never consulted.
|
|
|
|
if((r = readi(f->ip, 0, (uint64)addr, f->off, n)) > 0)
|
|
f->off += r;
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
panic("fileread");
|
|
}
|
|
|
|
int
|
|
filewrite(struct file *f, char *addr, int n)
|
|
{
|
|
int r;
|
|
|
|
if(f->writable == 0)
|
|
return -1;
|
|
|
|
if(f->type == FD_MEMFD){
|
|
r = memfd_rw(f->memfd_seg, 1, (uint64)addr, f->off, n, 1);
|
|
if(r > 0)
|
|
f->off += r;
|
|
return r;
|
|
}
|
|
|
|
if(f->type == FD_PIPE)
|
|
return pipewrite(f->pipe, addr, n);
|
|
|
|
if(f->type == FD_SOCKET)
|
|
return sockwrite(f->sock, addr, n);
|
|
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
|
|
if(f->ip->type == 3){
|
|
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].write){
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
r = devsw[f->ip->major].write(f->ip, f->off, addr, n);
|
|
if(r > 0)
|
|
f->off += r;
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
if((f->ip->rwmode & 1) == 0){
|
|
cprintf("SchOS: permission denied (write)\n");
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
|
|
// Recycle-bin check removed here too; see the note in fileread().
|
|
iunlock(f->ip);
|
|
|
|
// How much may go in one call is the filesystem's business. v1 is bounded
|
|
// by its log; SchFS is not bounded at all and would rather have the lot,
|
|
// because it makes one transaction out of whatever it is given.
|
|
uint max = f->ip->ops->max_write;
|
|
int i = 0;
|
|
while(i < n){
|
|
int n1 = n - i;
|
|
if(max && (uint)n1 > max)
|
|
n1 = (int)max;
|
|
|
|
begin_op();
|
|
ilock(f->ip);
|
|
if ((r = writei(f->ip, 1, (uint64)(addr + i), f->off, n1)) > 0)
|
|
f->off += r;
|
|
|
|
iunlock(f->ip);
|
|
end_op();
|
|
|
|
if(r < 0)
|
|
break;
|
|
i += r;
|
|
// Short means full. v1 and SchFS never wrote short, which is why this
|
|
// was a panic; FAT does, when the volume or its root directory fills.
|
|
// write(2) reports what went in, and the next call gets the error.
|
|
if(r != n1)
|
|
break;
|
|
}
|
|
if(i > 0)
|
|
return i;
|
|
return r < 0 ? r : -1;
|
|
}
|
|
panic("filewrite");
|
|
}
|
|
|
|
// poll(2) readiness for one open file. Returns a POLL* bitmask restricted
|
|
// to the requested events (plus error/hangup conditions, which POSIX says
|
|
// are always reported). Pipes and the console tty have real state; other
|
|
// inodes (regular files, block-ish devices) are always ready, per POSIX.
|
|
int
|
|
file_poll(struct file *f, int events)
|
|
{
|
|
if(f->type == FD_PIPE)
|
|
return pipe_poll(f->pipe, f->readable, f->writable) &
|
|
(events | POLLERR | POLLHUP);
|
|
|
|
if(f->type == FD_SOCKET){
|
|
int rev = 0;
|
|
if(sockreadable(f->sock))
|
|
rev |= POLLIN;
|
|
// A stream socket is writable whenever it has a peer. The buffer can
|
|
// still be full, in which case the write blocks -- same latitude poll()
|
|
// has always had, and the same answer a pipe gives.
|
|
if(sock_hup(f->sock))
|
|
rev |= POLLHUP;
|
|
else
|
|
rev |= POLLOUT;
|
|
return rev & (events | POLLERR | POLLHUP);
|
|
}
|
|
|
|
if(f->type == FD_INODE){
|
|
// An input device is only readable when an event is waiting. Reporting it
|
|
// always-ready -- which is what the generic answer below does -- turns a
|
|
// poll loop into a busy loop that blocks in read().
|
|
if(f->ip && f->ip->type == T_DEV && f->ip->major == INPUT_MAJOR){
|
|
int rev = 0;
|
|
if((events & POLLIN) && f->readable && input_ready(f->ip->minor))
|
|
rev |= POLLIN;
|
|
if(events & POLLOUT)
|
|
rev |= POLLOUT;
|
|
return rev;
|
|
}
|
|
|
|
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
|
|
int rev = 0;
|
|
if((events & POLLIN) && f->readable && console_input_ready(f->ip ? f->ip->minor : 1))
|
|
rev |= POLLIN;
|
|
if((events & POLLOUT) && f->writable)
|
|
rev |= POLLOUT;
|
|
return rev;
|
|
}
|
|
return events & (POLLIN | POLLOUT);
|
|
}
|
|
return POLLNVAL;
|
|
}
|