fat: FAT12, FAT16 and FAT32 with long names, read and write
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
This commit is contained in:
parent
d90a72e6d3
commit
edb560de66
15 changed files with 2580 additions and 50 deletions
|
|
@ -20,6 +20,7 @@
|
|||
#include "pciuser.h"
|
||||
#include "keymap.h" /* KB_*LAYOUT — the keyboard layout catalogue */
|
||||
#include "netuser.h"
|
||||
#include "fat.h"
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Linux-ABI-compatible struct stat (x86_64 "struct stat" from
|
||||
|
|
@ -173,23 +174,25 @@ at_nameiparent(int dirfd, char *path, char *name)
|
|||
|
||||
static struct inode*
|
||||
createat_x(int dirfd, char *path, short type, short major, short minor,
|
||||
int *rofs)
|
||||
int *err)
|
||||
{
|
||||
struct inode *ip, *dp;
|
||||
char name[DIRSIZ];
|
||||
uint off;
|
||||
if (!(dp = at_nameiparent(dirfd, path, name))) return 0;
|
||||
ilock(dp);
|
||||
// Nothing below this point tolerates a refusal: ialloc and dirlink both
|
||||
// panic on failure, because on a writable filesystem they cannot fail for
|
||||
// any reason the caller could have avoided.
|
||||
//
|
||||
// The flag lets the caller report EROFS. Returning 0 alone made a refused
|
||||
// `err` lets the caller report why. Returning 0 alone made a refused
|
||||
// write say "No such file or directory", which is what a shell prints for
|
||||
// every create failure and tells the user nothing about why.
|
||||
if (fs_readonly(dp->dev)) {
|
||||
iunlockput(dp);
|
||||
if (rofs) *rofs = 1;
|
||||
if (err) *err = EROFS;
|
||||
return 0;
|
||||
}
|
||||
// A FAT volume has nowhere to put a device node, a symlink or a socket.
|
||||
if (dp->ops->refuse_types & (1u << type)) {
|
||||
iunlockput(dp);
|
||||
if (err) *err = EPERM;
|
||||
return 0;
|
||||
}
|
||||
if ((ip = dirlookup(dp, name, &off)) != 0) {
|
||||
|
|
@ -208,7 +211,15 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
|
|||
iunlockput(ip);
|
||||
return 0;
|
||||
}
|
||||
if (!(ip = ialloc(dp->dev, type))) panic("create: ialloc");
|
||||
// v1 and SchFS cannot fail either of these for a reason the caller could
|
||||
// have avoided. FAT can: the volume fills up, the fixed root directory of
|
||||
// FAT16 holds 512 names and no more, and a name may hold characters FAT
|
||||
// does not allow. So a failure is undone and reported, not a panic.
|
||||
if (!(ip = ialloc(dp->dev, type))) {
|
||||
iunlockput(dp);
|
||||
if (err) *err = ENOSPC;
|
||||
return 0;
|
||||
}
|
||||
ilock(ip);
|
||||
ip->major = major; ip->minor = minor; ip->nlink = 1;
|
||||
ip->showmode = 1;
|
||||
|
|
@ -218,17 +229,20 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
|
|||
iupdate(dp);
|
||||
build_dir_tree(ip, dp->inum);
|
||||
}
|
||||
if (dirlink(dp, name, ip->inum) < 0) panic("create: dirlink");
|
||||
int r;
|
||||
if ((r = dirlink(dp, name, ip->inum)) < 0) {
|
||||
if (type == T_DIR) { dp->nlink--; iupdate(dp); }
|
||||
iunlockput(dp);
|
||||
ip->nlink = 0;
|
||||
iupdate(ip);
|
||||
iunlockput(ip); // the last reference: ifree gives it back
|
||||
if (err) *err = r == -EINVAL ? EINVAL : ENOSPC;
|
||||
return 0;
|
||||
}
|
||||
iunlockput(dp);
|
||||
return ip;
|
||||
}
|
||||
|
||||
static struct inode*
|
||||
createat(int dirfd, char *path, short type, short major, short minor)
|
||||
{
|
||||
return createat_x(dirfd, path, type, major, minor, 0);
|
||||
}
|
||||
|
||||
static struct inode*
|
||||
create(char *path, short type, short major, short minor)
|
||||
{
|
||||
|
|
@ -508,14 +522,14 @@ addr_t sys_open(void)
|
|||
struct inode *ex = namei(path);
|
||||
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
||||
}
|
||||
int rofs = 0;
|
||||
int cerr = 0;
|
||||
ip = createat_x(AT_FDCWD, path,
|
||||
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &rofs);
|
||||
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr);
|
||||
// A refusal because the filesystem is read-only is not "no such file".
|
||||
// A shell prints the same sentence for every create failure, so
|
||||
// returning ENOENT here made writing to a snapshot look like a missing
|
||||
// directory.
|
||||
if (!ip) { end_op(); return rofs ? -EROFS : -ENOENT; }
|
||||
if (!ip) { end_op(); return cerr ? -cerr : -ENOENT; }
|
||||
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
|
||||
itrunc(ip);
|
||||
} else {
|
||||
|
|
@ -562,7 +576,11 @@ addr_t sys_openat(void)
|
|||
struct inode *ex = at_namei(dirfd, path);
|
||||
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
||||
}
|
||||
if (!(ip = createat(dirfd, path, T_FILE, 0, 0))) { end_op(); return -ENOENT; }
|
||||
int cerr = 0;
|
||||
if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr))) {
|
||||
end_op();
|
||||
return cerr ? -cerr : -ENOENT;
|
||||
}
|
||||
} else {
|
||||
if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; }
|
||||
ilock(ip);
|
||||
|
|
@ -748,7 +766,11 @@ addr_t sys_mkdir(void)
|
|||
end_op();
|
||||
return -EEXIST;
|
||||
}
|
||||
if (!(ip = create(path, T_DIR, 0, 0))) { end_op(); return -ENOENT; }
|
||||
int cerr = 0;
|
||||
if (!(ip = createat_x(AT_FDCWD, path, T_DIR, 0, 0, &cerr))) {
|
||||
end_op();
|
||||
return cerr ? -cerr : -ENOENT;
|
||||
}
|
||||
iunlockput(ip); end_op();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -799,6 +821,7 @@ addr_t sys_link(void)
|
|||
if (!(dp = nameiparent(new, name))) { err = ENOENT; goto bad_link; }
|
||||
ilock(dp);
|
||||
if (dp->dev != ip->dev) { iunlockput(dp); err = EXDEV; goto bad_link; }
|
||||
if (dp->ops->no_hardlinks) { iunlockput(dp); err = EPERM; goto bad_link; }
|
||||
if (dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); err = EEXIST; goto bad_link; }
|
||||
iunlockput(dp); iput(ip); end_op();
|
||||
return 0;
|
||||
|
|
@ -1065,9 +1088,10 @@ do_symlink(int newdirfd, char *target, char *linkpath)
|
|||
end_op();
|
||||
return -EEXIST;
|
||||
}
|
||||
if (!(ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0))) {
|
||||
int cerr = 0;
|
||||
if (!(ip = createat_x(newdirfd, linkpath, T_SYMLINK, 0, 0, &cerr))) {
|
||||
end_op();
|
||||
return -ENOENT;
|
||||
return cerr ? -cerr : -ENOENT;
|
||||
}
|
||||
len = strlen(target);
|
||||
if (writei(ip, 0, (uint64)target, 0, len) != len) {
|
||||
|
|
@ -1538,6 +1562,7 @@ addr_t sys_rmdir(void)
|
|||
addr_t sys_sync(void)
|
||||
{
|
||||
schfs_ksync_all();
|
||||
fat_ksync_all(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1550,6 +1575,10 @@ addr_t sys_fsync(void)
|
|||
// this file's last write is in whichever group is open.
|
||||
if (f->type == FD_INODE && f->ip && SCHFS_IS_VOL_DEV(f->ip->dev))
|
||||
return schfs_ksync(f->ip->dev) == 0 ? 0 : -EIO;
|
||||
// FAT has written everything by the time a write returns; what is left is
|
||||
// the drive's own cache.
|
||||
if (f->type == FD_INODE && f->ip && FAT_IS_DEV(f->ip->dev))
|
||||
fat_ksync_all(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1593,7 +1622,59 @@ addr_t sys_pwrite(void)
|
|||
// utime / utimes / utimensat — timestamps not tracked; stubs.
|
||||
addr_t sys_utime(void) { return 0; }
|
||||
addr_t sys_utimes(void) { return 0; }
|
||||
addr_t sys_utimensat(void) { return 0; }
|
||||
// utimensat / futimens.
|
||||
//
|
||||
// This was a stub returning 0, which was worse than not having it: toybox's
|
||||
// touch calls utimensat first and creates the file only when that fails with
|
||||
// ENOENT, so `touch newfile` succeeded and made nothing, on every filesystem.
|
||||
#define UTIME_NOW ((1L << 30) - 1)
|
||||
#define UTIME_OMIT ((1L << 30) - 2)
|
||||
addr_t sys_utimensat(void)
|
||||
{
|
||||
int dirfd = 0, flags = 0;
|
||||
addr_t upath = 0, utimes = 0;
|
||||
struct inode *ip;
|
||||
struct timespec ts[2];
|
||||
|
||||
argint(0, &dirfd);
|
||||
argaddr(1, &upath);
|
||||
argaddr(2, &utimes);
|
||||
argint(3, &flags);
|
||||
if (utimes && copyin(myproc()->pgdir, utimes, ts, sizeof(ts)) < 0)
|
||||
return -EFAULT;
|
||||
|
||||
if (upath == 0) { // futimens(fd, times)
|
||||
struct file *f;
|
||||
if (argfd(0, 0, &f) < 0 || f->type != FD_INODE || !f->ip)
|
||||
return -EBADF;
|
||||
ip = idup(f->ip);
|
||||
} else {
|
||||
char *path;
|
||||
if (argstr(1, &path) < 0) return -EFAULT;
|
||||
begin_op();
|
||||
ip = at_namei(dirfd, path);
|
||||
end_op();
|
||||
if (!ip) return -ENOENT;
|
||||
}
|
||||
|
||||
uint64 now = 0, us = 0;
|
||||
wallclock(&now, &us);
|
||||
begin_op();
|
||||
ilock(ip);
|
||||
if (fs_readonly(ip->dev)) { iunlockput(ip); end_op(); return -EROFS; }
|
||||
for (int i = 0; i < 2; i++) {
|
||||
long sec = utimes ? ts[i].tv_sec : (long)now;
|
||||
if (utimes && ts[i].tv_nsec == UTIME_OMIT) continue;
|
||||
if (utimes && ts[i].tv_nsec == UTIME_NOW) sec = (long)now;
|
||||
if (i == 0) ip->atime = (uint)sec;
|
||||
else ip->mtime = (uint)sec;
|
||||
}
|
||||
ip->ctime = (uint)now;
|
||||
iupdate(ip);
|
||||
iunlockput(ip);
|
||||
end_op();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// sendfile: not implemented.
|
||||
addr_t sys_sendfile(void) { return -ENOSYS; }
|
||||
|
|
@ -1637,6 +1718,18 @@ static void fill_statfs(struct statfs *sf, uint dev) {
|
|||
return;
|
||||
}
|
||||
|
||||
uint64 csize, clusters, freec;
|
||||
if (fat_kstatfs(dev, &csize, &clusters, &freec) == 0) {
|
||||
sf->f_type = 0x4d44; // MSDOS_SUPER_MAGIC, as Linux says
|
||||
sf->f_bsize = (int64)csize;
|
||||
sf->f_frsize = (int64)csize;
|
||||
sf->f_blocks = (int64)clusters;
|
||||
sf->f_bfree = (int64)freec;
|
||||
sf->f_bavail = (int64)freec;
|
||||
sf->f_namelen = 255;
|
||||
return;
|
||||
}
|
||||
|
||||
// v1, /proc, or anything else: the old shape, but at least with a block
|
||||
// size that matches the one the buffer cache actually uses.
|
||||
sf->f_bsize = BSIZE;
|
||||
|
|
@ -1697,7 +1790,11 @@ addr_t sys_mkdirat(void)
|
|||
end_op();
|
||||
return -EEXIST;
|
||||
}
|
||||
if (!(ip = createat(dirfd, path, T_DIR, 0, 0))) { end_op(); return -ENOENT; }
|
||||
int cerr = 0;
|
||||
if (!(ip = createat_x(dirfd, path, T_DIR, 0, 0, &cerr))) {
|
||||
end_op();
|
||||
return cerr ? -cerr : -ENOENT;
|
||||
}
|
||||
iunlockput(ip); end_op();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1714,8 +1811,10 @@ addr_t sys_mknodat(void)
|
|||
int major = (dev >> 8) & 0xff;
|
||||
int minor = dev & 0xff;
|
||||
begin_op();
|
||||
struct inode *ip = createat(dirfd, path, T_DEV, (short)major, (short)minor);
|
||||
if (!ip) { end_op(); return -1; }
|
||||
int cerr = 0;
|
||||
struct inode *ip = createat_x(dirfd, path, T_DEV, (short)major,
|
||||
(short)minor, &cerr);
|
||||
if (!ip) { end_op(); return cerr ? -cerr : -1; }
|
||||
iunlockput(ip); end_op();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1820,7 +1919,8 @@ addr_t sys_linkat(void)
|
|||
ip->nlink++; iupdate(ip); iunlock(ip);
|
||||
if (!(dp = at_nameiparent(newdirfd, new, name))) goto bad_linkat;
|
||||
ilock(dp);
|
||||
if (dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0) {
|
||||
if (dp->dev != ip->dev || dp->ops->no_hardlinks ||
|
||||
dirlink(dp, name, ip->inum) < 0) {
|
||||
iunlockput(dp); goto bad_linkat;
|
||||
}
|
||||
iunlockput(dp); iput(ip); end_op();
|
||||
|
|
@ -1913,6 +2013,55 @@ addr_t sys_fstatat(void)
|
|||
// decides where in the namespace the result appears.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// The block device a path names: a node with major 8, whose minor is the
|
||||
// device number. Returns it, or a negative errno.
|
||||
static int
|
||||
source_bdev(char *source)
|
||||
{
|
||||
begin_op();
|
||||
struct inode *sp = namei(source);
|
||||
if (!sp) { end_op(); return -ENOENT; }
|
||||
ilock(sp);
|
||||
int r = (sp->type == T_DEV && sp->major == 8) ? sp->minor : -ENOTBLK;
|
||||
iunlockput(sp);
|
||||
end_op();
|
||||
return r;
|
||||
}
|
||||
|
||||
// `mount -t vfat /dev/usb0p1 /mnt`. MS_RDONLY (flag 1, argument 3) mounts it
|
||||
// read-only; nothing else in the flags or the data means anything to FAT yet.
|
||||
static int
|
||||
mount_fat(char *source, char *target)
|
||||
{
|
||||
int flags = 0;
|
||||
argint(3, &flags);
|
||||
if (!source || !source[0])
|
||||
return -EINVAL;
|
||||
int bdev = source_bdev(source);
|
||||
if (bdev < 0)
|
||||
return bdev;
|
||||
|
||||
begin_op();
|
||||
struct inode *dp = namei(target);
|
||||
if (!dp) { end_op(); return -ENOENT; }
|
||||
ilock(dp);
|
||||
if (dp->type != T_DIR) { iunlockput(dp); end_op(); return -ENOTDIR; }
|
||||
uint dev = dp->dev;
|
||||
uint64 inum = dp->inum;
|
||||
iunlockput(dp);
|
||||
end_op();
|
||||
|
||||
int fsdev = fat_kmount((uint)bdev, flags & 1);
|
||||
if (fsdev < 0)
|
||||
return fsdev;
|
||||
if (mount_add(dev, inum, (uint)fsdev, FAT_ROOT_INO, target, source,
|
||||
"vfat") != 0) {
|
||||
fat_kumount((uint)fsdev);
|
||||
return -EBUSY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
addr_t sys_mount(void)
|
||||
{
|
||||
char *source, *target, *fstype;
|
||||
|
|
@ -1934,8 +2083,15 @@ addr_t sys_mount(void)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Only SchFS. Checking the name still matters: a caller that asked for
|
||||
// something else should be told no rather than quietly handed this.
|
||||
// FAT, under the three names Linux knows it by. vfat is what everything
|
||||
// means today; msdos is the same driver without long names on Linux, and
|
||||
// here simply the same driver.
|
||||
if (strncmp(fstype, "vfat", 5) == 0 || strncmp(fstype, "msdos", 6) == 0 ||
|
||||
strncmp(fstype, "fat", 4) == 0)
|
||||
return mount_fat(source, target);
|
||||
|
||||
// Otherwise SchFS. Checking the name still matters: a caller that asked
|
||||
// for something else should be told no rather than quietly handed this.
|
||||
if (strncmp(fstype, "schfs", 6) != 0)
|
||||
return -ENODEV;
|
||||
|
||||
|
|
@ -2065,6 +2221,13 @@ addr_t sys_umount2(void)
|
|||
iput(dp);
|
||||
end_op();
|
||||
|
||||
// Something still open or a working directory inside it -- the root
|
||||
// included, which nothing holds once the namei above has let go: the
|
||||
// volume cannot be let go of, since its inodes would outlive it. Linux
|
||||
// says EBUSY here, and `umount` prints "target is busy".
|
||||
if (FAT_IS_DEV(dev) && ibusy(dev, 0))
|
||||
return -EBUSY;
|
||||
|
||||
if (mount_del_by_root(dev, inum) != 0)
|
||||
return -EINVAL;
|
||||
|
||||
|
|
@ -2079,6 +2242,8 @@ addr_t sys_umount2(void)
|
|||
// to fall back to -- and mount_del_by_root has already failed for it,
|
||||
// since nothing mounts the root over anything.
|
||||
schfs_kumount(dev);
|
||||
else if (FAT_IS_DEV(dev))
|
||||
fat_kumount(dev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "uaccess.h"
|
||||
#include "errno.h"
|
||||
#include "acpi.h"
|
||||
#include "fat.h"
|
||||
|
||||
addr_t sys_fork(void) { return fork(); }
|
||||
addr_t sys_wait(void) { return wait(); }
|
||||
|
|
@ -153,6 +154,9 @@ sys_uptime(void)
|
|||
addr_t sys_reboot(void)
|
||||
{
|
||||
cprintf("System is rebooting...\n");
|
||||
// A stick left mounted should come back readable, and marked clean.
|
||||
schfs_ksync_all();
|
||||
fat_ksync_all(1);
|
||||
// The FADT's reset register first: it is the one the board's designer
|
||||
// wired up. Then the PCI reset port and the keyboard controller, which
|
||||
// are what nearly every PC has had since there were PCs.
|
||||
|
|
@ -170,6 +174,7 @@ addr_t sys_shutdown(void)
|
|||
// kept for a machine whose AML did not load; a real board answers none
|
||||
// of them.
|
||||
schfs_ksync_all();
|
||||
fat_ksync_all(1);
|
||||
acpi_poweroff();
|
||||
outw(0x604, 0x2000);
|
||||
outw(0xB004, 0x2000);
|
||||
|
|
|
|||
Loading…
Reference in a new issue