From edb560de6676f76bcf6956c72e58ded7313fca35 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Fri, 23 Oct 2026 09:33:35 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- Makefile | 5 +- docs/architecture/fat.md | 105 ++ docs/architecture/filesystem.md | 34 +- include/blkdev.h | 19 +- include/fat.h | 30 + include/file.h | 15 + kernel/acpi/aml.c | 2 + kernel/dev/blkdev.c | 64 +- kernel/dev/disk.c | 57 + kernel/fs/devfs.c | 27 + kernel/fs/fat/fat.c | 1996 +++++++++++++++++++++++++++++++ kernel/fs/file.c | 11 +- kernel/fs/fs.c | 37 +- kernel/sys/sys_fs.c | 223 +++- kernel/sys/sys_proc.c | 5 + 15 files changed, 2580 insertions(+), 50 deletions(-) create mode 100644 docs/architecture/fat.md create mode 100644 include/fat.h create mode 100644 kernel/fs/fat/fat.c diff --git a/Makefile b/Makefile index f509c03..780cc8c 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,9 @@ KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \ KERN_FS += $(addprefix $(BUILD)/kernel/fs/schfs/, \ btree.o space.o vol.o obj.o objw.o attr.o kmount.o vfs.o) +# FAT12/16/32 with long names, for the disks every other system can read. +KERN_FS += $(BUILD)/kernel/fs/fat/fat.o + # Sockets. Unix domain only for now; an IP stack becomes more files here # rather than a reshaping of what is already written. KERN_NET = $(addprefix $(BUILD)/kernel/net/, \ @@ -926,7 +929,7 @@ $(BUILD)/kernel/drivers/%.o: kernel/drivers/%.c $(CC) $(CFLAGS) -I$(dir $<) -c $< -o $@ $(BUILD)/kernel/fs/%.o: kernel/fs/%.c - @mkdir -p $(BUILD)/kernel/fs + @mkdir -p $(dir $@) $(CC) $(CFLAGS) -c $< -o $@ $(BUILD)/kernel/fs/schfs/%.o: kernel/fs/schfs/%.c diff --git a/docs/architecture/fat.md b/docs/architecture/fat.md new file mode 100644 index 0000000..5d91ce3 --- /dev/null +++ b/docs/architecture/fat.md @@ -0,0 +1,105 @@ +# FAT + +FAT12, FAT16 and FAT32 with VFAT long names, read and write. +`kernel/fs/fat/fat.c`, behind the same `fs_ops` vtable as SchFS. + +It exists for one reason: it is the filesystem every other system can read. A +USB stick written here opens on Linux, Windows and macOS, and so does the EFI +system partition. The first user is the hardware report, which has to leave a +machine that may have no network on something you can carry to another one. + +``` +mount -t vfat /dev/usb0p1 /mnt # msdos and fat are accepted as well +mount -t vfat -o ro /dev/sata0p1 /boot/efi +umount /mnt +``` + +The disks and partitions the block layer found are in `/dev` under the names +it gave them (`usb0`, `usb0p1`, `sata0p2`, `nvme0p1`), generated like the rest +of devtmpfs: a stick appears when it is plugged in and goes when it is pulled. + +--- + +## Written from the specification, matched to Linux + +The on-disk rules come from Microsoft's FAT specification (1.03). Where the +specification leaves a choice, the choice is Linux's vfat, so that a volume +written here and one written by Linux look alike: + +- **The FAT type is decided by the cluster count and nothing else**: under 4085 + is FAT12, under 65525 FAT16, otherwise FAT32. The "FAT32" label in the boot + sector is a comment. +- **Short names.** A name that fits 8.3 in one case (`readme.txt`, `README.TXT`) + is stored as a short entry alone, with the lowercase bits in byte 12 that + Windows NT and Linux use. A name that does not (`ReadMe.txt`, `Reports`, + `big numbers.log`) gets long-name entries plus an alias: its uppercase self + when that loses nothing (`REPORTS`), otherwise `BIGNUM~1.LOG`, counting up + until it is unique. +- **Lookups ignore case** (ASCII and Latin-1), and a name with a long entry + still answers to its alias, so `PROGRA~1` works. +- **Times are stored as UTC.** FAT has no time zone, and that is what Linux + assumes when it is not told one. +- **The dirty bit.** Set in the boot sector while a volume is mounted + read-write, cleared on unmount, sync-on-shutdown and reboot. A volume found + with it set is reported (`fsck.fat` reads the same bit). +- **Every FAT copy is written**, unless the volume has mirroring turned off, in + which case only the active one is. FSInfo's free count and next-free hint are + read and kept up to date. +- **Refused, with Linux's errno**: device nodes, symlinks and sockets (`EPERM`, + through `fs_ops.refuse_types`), hard links (`EPERM`, `fs_ops.no_hardlinks`), + and names containing `" * / : < > ? \ |` (`EINVAL`). + +Checked against `mkfs.vfat`-made FAT12, FAT16 (MBR) and FAT32 (superfloppy and +GPT ESP) volumes: written here, then `fsck.fat -n` and mtools on the host. + +## Inodes, on a filesystem that has none + +A FAT file *is* its directory entry: name, first cluster, size and times are all +in those 32 bytes. The inode number says where to find it: + +| object | number | +|--------|--------| +| the root | `1` | +| a directory | `2 << 40` \| its first cluster | +| a file | `1 << 40` \| byte position of its short entry / 32 | +| a file created here, or one whose number is taken | `3 << 40` \| a counter | + +A directory is numbered by its cluster because `..` must be able to find it, +and `..` holds the parent's cluster and nothing about where the parent's entry +is. A file is numbered by its entry, as Linux does it. + +The VFS renames by linking the new name to the same number and then unlinking +the old one, so a file's entry can move while its number stays. A small +per-volume table records every number whose entry is not where the number says. +It is consulted first in both directions, and anything nobody holds can be +dropped from it, since the next lookup by name finds the entry afresh. + +## I/O + +Through a cache of its own (64 lines of 4 KB per volume), not the buffer cache. +FAT addresses sectors, a partition need not be a whole number of 4 KB blocks, +and the buffer cache's last block of one would read and write into the next +partition. `blkdev_xfer` takes sectors and refuses a transfer past the end +rather than panicking. + +**Every operation that changes something writes its dirty lines back before it +returns.** A stick pulled out between two commands holds both of them. `sync` +and `fsync` also flush the drive's own cache. Unmount, shutdown, reboot and the +power button sync and mark the volume clean. + +A new cluster is zeroed in the cache before use. FAT has no holes, and a +directory ends at the first entry whose first byte is zero. + +## What it does not do + +- **Names longer than 29 bytes.** FAT allows 255 UTF-16 units, but the VFS's + `DIRSIZ` caps a path component, so a longer name is cut short on the way in. + Lifting that is a VFS change. +- **Permissions and owners.** Every file shows as `0755 root:root`, like Linux + vfat with its defaults. The read-only attribute is kept but not enforced. +- **exFAT.** Large SD cards (SDXC, over 32 GB) ship with it. It is a different + filesystem, not a FAT variant. +- **Code pages.** Short names are read as CP437 and written as ASCII; anything + else goes in a long name. +- **Directories don't move**, which is the VFS's rule for every filesystem. +- **No `fsck`.** A volume found dirty is reported, not repaired. diff --git a/docs/architecture/filesystem.md b/docs/architecture/filesystem.md index b714ecf..029ae82 100644 --- a/docs/architecture/filesystem.md +++ b/docs/architecture/filesystem.md @@ -30,11 +30,28 @@ struct fs_ops { struct inode *(*ialloc)(uint dev, short type); int (*getdents)(struct inode *dp, char *out, int cap, uint64 *off); + + uint max_write; // largest write in one call; 0 for no limit + uint refuse_types; // 1 << T_* the filesystem cannot store: EPERM + int no_hardlinks; // link() fails with EPERM }; ``` -Two implementations exist: `schfs_ops` (`kernel/fs/schfs/vfs.c`) and `procfs_ops` -(`kernel/fs/procfs.c`). +The implementations: `schfs_ops` (`kernel/fs/schfs/vfs.c`), `fat_ops` +(`kernel/fs/fat/fat.c`, see [fat.md](fat.md)), `procfs_ops` +(`kernel/fs/procfs.c`) and `devfs_ops` (`kernel/fs/devfs.c`). + +**Creation can fail, and is undone when it does.** `ialloc` and `dirlink` used +to panic on failure, because on v1 and SchFS they cannot fail for a reason the +caller could have avoided. FAT can: the volume fills, the fixed root directory +of FAT16 holds 512 names, a name holds a character FAT does not allow. So +`createat_x()` gives the object back and reports `ENOSPC`, `EINVAL`, `EPERM` or +`EROFS`, and `open`, `mkdir`, `mknod` and `symlink` return that errno instead of +`ENOENT`. + +**A short write is a full disk.** `filewrite()` panicked on one, which held as +long as no filesystem ever wrote short. Now `write(2)` returns what went in, and +the next call gets the error. `poff` is deliberately **an opaque cookie** rather than a byte offset — meaningful only to the same filesystem's `dirunlink`. A directory does not have @@ -102,6 +119,10 @@ during path resolution. `/proc` is mounted by init: mount("proc", "/proc", "proc", 0, 0); ``` +The filesystem types are `proc`, `devtmpfs`, `schfs` and `vfat` (also accepted +as `msdos` and `fat`). Unmounting a FAT volume while anything on it is open, or +is some process's working directory, fails with `EBUSY`. + ## procfs `kernel/fs/procfs.c`. A real filesystem behind the same vtable, not a set of @@ -165,7 +186,14 @@ A node exists because the kernel has the device, or it does not exist. `console`, `null`, `keyboard`, `zero`, `full`, `random`, `urandom`, `tty`, `stdin`, `stdout`, `stderr`, `fb0`, `mouse`, `hda`, `hdb`, and `input/event0` -and `input/event1` in one subdirectory. Minors under major 1 follow Linux's +and `input/event1` in one subdirectory. + +Every disk and partition the block layer registered is there too, under the +name it was given (`sata0`, `sata0p2`, `nvme0p1`, `usb0p1`): major 8, minor its +block device number, listed only while it is present. Reads and writes go +through `blkdev_xfer` in whole sectors, end at the end of the device, and do +not pass through the buffer cache. A partition need not be a whole number of +4 KB blocks, and the buffer cache's last block of one would run into the next. Minors under major 1 follow Linux's mem-major layout where it costs nothing. The raw disks are created whether or not a drive is present: opening one with diff --git a/include/blkdev.h b/include/blkdev.h index 16537ec..2cd2a1d 100644 --- a/include/blkdev.h +++ b/include/blkdev.h @@ -51,6 +51,23 @@ int blkdev_flush(uint dev); // disk they are on. Returns the device number of the i'th, or -1 past the end. int blkdev_nth(int i); const char *blkdev_name(uint dev); -int blkdev_is_schfs(uint dev); // a partition typed SCHFS_GPT_TYPE +// 1 for a partition typed SCHFS_GPT_TYPE, 0 for anything that might be SchFS, +// and -1 for one whose type says it cannot be -- an EFI system partition, an +// MBR FAT type. The root probe takes 1 and then 0, so -1 is never tried: a +// FAT partition is mounted as FAT (kernel/fs/fat), not offered as a root. +int blkdev_is_schfs(uint dev); int blkdev_info(char *buf, int cap); // /proc/partitions + +// Sectors straight to and from a filesystem's own buffer, for one that does +// not go through the buffer cache. kernel/fs/fat is: FAT addresses 512-byte +// sectors, a partition need not be a whole number of 4 KB blocks, and a +// cache block hanging over the end of one is a write into the next. +// +// `lba` counts the device's own sectors from the start of the partition, and +// the transfer is at most BSIZE bytes. Returns 0, or -1 for a transfer out of +// range or one the device refused -- it does not panic, because a USB stick +// can leave in the middle of one. +int blkdev_xfer(uint dev, uint64 lba, uint32 count, void *buf, int write); +// Size in sectors and sector size; 0 if there is no such device. +uint64 blkdev_sectors(uint dev, uint32 *secsize); diff --git a/include/fat.h b/include/fat.h new file mode 100644 index 0000000..c72214e --- /dev/null +++ b/include/fat.h @@ -0,0 +1,30 @@ +#pragma once + +// FAT12, FAT16 and FAT32, with VFAT long names: the filesystem every other +// operating system can read, which is the whole reason to have it. A USB +// stick, an EFI system partition, an SD card. See kernel/fs/fat/fat.c. +// +// Mounted the way Linux mounts it -- `mount -t vfat /dev/usb0p1 /mnt` -- and +// "msdos" and "fat" are accepted as the same thing. + +// Filesystem device numbers for mounted FAT volumes. Like SCHFS_DEV, runtime +// identities for the inode cache that nothing on disk records. +#define FAT_VOL_SLOTS 4 +#define FAT_DEV 104 +#define FAT_IS_DEV(d) ((d) >= FAT_DEV && (d) < FAT_DEV + FAT_VOL_SLOTS) + +// The root directory's inode number on every FAT volume. +#define FAT_ROOT_INO 1 + +// Mount the FAT volume on block device `bdev`. Returns its filesystem device +// number, or a negative errno: -ENODEV for no FAT there, -EBUSY for no free +// slot or already mounted. +int fat_kmount(uint bdev, int readonly); +// Flush it, mark it cleanly unmounted, and let it go. +int fat_kumount(uint dev); +int fat_readonly(uint dev); +// Write out everything dirty on every mounted volume. `final` also marks each +// one clean, for a shutdown that will not get to unmount them. +void fat_ksync_all(int final); +// Cluster size, clusters and free clusters, for statfs. -1 if not FAT. +int fat_kstatfs(uint dev, uint64 *bsize, uint64 *total, uint64 *freec); diff --git a/include/file.h b/include/file.h index 49357cf..d2e25c9 100644 --- a/include/file.h +++ b/include/file.h @@ -82,6 +82,14 @@ struct fs_ops { // handed, so chopping a 1 MB write into 6 KB pieces bought 170 commits and // the flushes that go with them for nothing. uint max_write; + + // Object types this filesystem has no way to store, as a mask of 1 << T_*. + // Creating one fails with EPERM, which is what Linux answers for mknod or a + // symlink on vfat. 0 for "anything". + uint refuse_types; + // One name per file: link() fails with EPERM. Rename still works -- it is + // a second name for as long as it takes to remove the first. + int no_hardlinks; }; // In-memory inode. @@ -161,6 +169,13 @@ struct inode { // The synthetic /dev. See kernel/fs/devfs.c. #define DEVFS_DEV 81 +// Mounted FAT volumes, FAT_DEV onward. See include/fat.h. + +// 1 if some inode on dev with this number is referenced. In fs.c. +int iheld(uint dev, uint64 inum); +// 1 if any inode on dev but `except` is referenced (0: no exception). In fs.c. +int ibusy(uint dev, uint64 except); + // table mapping major device number to // device functions struct devsw { diff --git a/kernel/acpi/aml.c b/kernel/acpi/aml.c index 5306a53..e0c5b94 100644 --- a/kernel/acpi/aml.c +++ b/kernel/acpi/aml.c @@ -24,6 +24,7 @@ #include "pci.h" #include "irq.h" #include "acpi.h" +#include "fat.h" #include #include @@ -87,6 +88,7 @@ power_off_work(uacpi_handle ctx) (void)ctx; cprintf("acpi: power button pressed; syncing and powering off\n"); schfs_ksync_all(); + fat_ksync_all(1); acpi_poweroff(); cprintf("acpi: power-off failed; it is safe to switch off now\n"); } diff --git a/kernel/dev/blkdev.c b/kernel/dev/blkdev.c index 990e100..cb40129 100644 --- a/kernel/dev/blkdev.c +++ b/kernel/dev/blkdev.c @@ -21,7 +21,7 @@ struct blkdev { uint64 nsectors; uint32 secsize; int parent; // the disk's slot for a partition, -1 for a disk - int schfs; // GPT says this is a SchFS volume + int schfs; // 1: typed SchFS; -1: typed FAT; 0: anything else }; static struct blkdev devs[NBLKDEV]; @@ -134,8 +134,8 @@ readsec(struct blkdev *d, uint64 lba, uchar *buf) // it says. Only the primary copy is read -- a disk whose primary is damaged // gets reported as having no partitions rather than half of some. // -// The EFI system partition is skipped. It is FAT, nothing here can mount it, -// and registering it would only give the root probe one more thing to try. +// The EFI system partition is registered like any other, as FAT: the root +// probe passes over it (see blkdev_is_schfs), and kernel/fs/fat mounts it. static int scan_gpt(int disk, uchar *sec) { @@ -159,19 +159,19 @@ scan_gpt(int disk, uchar *sec) if (i % per == 0 && readsec(d, elba + i / per, sec) < 0) break; uchar *e = sec + (i % per) * esize; - if (guid_zero(e) || guid_is(e, ESP_TYPE)) + if (guid_zero(e)) continue; uint64 first = le64(e + 32), last = le64(e + 40); if (first == 0 || last < first || last >= d->nsectors) continue; char name[16]; partname(name, d->name, i + 1); - int schfs = guid_is(e, SCHFS_GPT_TYPE); + int schfs = guid_is(e, SCHFS_GPT_TYPE) ? 1 : guid_is(e, ESP_TYPE) ? -1 : 0; if (add(name, d->ops, d->priv, first, last - first + 1, d->secsize, disk, schfs) >= 0) { kinfo("blkdev: %s: %d MB%s\n", name, (int)(((last - first + 1) * d->secsize) >> 20), - schfs ? ", SchFS" : ""); + schfs > 0 ? ", SchFS" : schfs < 0 ? ", EFI system" : ""); found++; } } @@ -180,6 +180,22 @@ scan_gpt(int disk, uchar *sec) // The PC partition table, for disks that have one instead. Primary entries // only; a protective entry (0xEE) means the disk is GPT and was read above. +// +// A FAT volume with no partition table -- a "superfloppy", which is how a +// good many USB sticks leave the factory -- also ends in 55 AA, and its boot +// code would read as four nonsense entries. Linux tells them apart by the +// status byte, which in a real entry is 0x00 or 0x80 and in boot code is +// whatever the code happens to be. +static int +fat_type(uint32 type) +{ + switch (type) { + case 0x01: case 0x04: case 0x06: case 0x0B: case 0x0C: case 0x0E: case 0xEF: + return 1; + } + return 0; +} + static void scan_mbr(int disk, uchar *sec) { @@ -190,14 +206,15 @@ scan_mbr(int disk, uchar *sec) for (int i = 0; i < 4; i++) { uchar *e = sec + 446 + i * 16; uint32 type = e[4], first = le32(e + 8), n = le32(e + 12); - // Empty, protective, extended (which would need its own chain read), and - // FAT/ESP types: none of them can be SchFS. - if (type == 0 || type == 0xEE || type == 0x05 || type == 0x0F || - type == 0xEF || n == 0 || first + (uint64)n > d->nsectors) + // Empty, protective, and extended (which would need its own chain read). + if ((e[0] != 0x00 && e[0] != 0x80) || + type == 0 || type == 0xEE || type == 0x05 || type == 0x0F || + n == 0 || first + (uint64)n > d->nsectors) continue; char name[16]; partname(name, d->name, i + 1); - if (add(name, d->ops, d->priv, first, n, d->secsize, disk, 0) >= 0) + if (add(name, d->ops, d->priv, first, n, d->secsize, disk, + fat_type(type) ? -1 : 0) >= 0) kinfo("blkdev: %s: %d MB, MBR type %x\n", name, (int)(((uint64)n * d->secsize) >> 20), type); } @@ -280,6 +297,28 @@ blkdev_rw(struct buf *b) b->flags &= ~B_DIRTY; } +int +blkdev_xfer(uint dev, uint64 lba, uint32 count, void *buf, int write) +{ + struct blkdev *d = lookup(dev); + + if (!d || count == 0 || (uint64)count * d->secsize > BSIZE || + lba + count > d->nsectors || lba + count < lba) + return -1; + return d->ops->rw(d->priv, d->start + lba, count, buf, write) < 0 ? -1 : 0; +} + +uint64 +blkdev_sectors(uint dev, uint32 *secsize) +{ + struct blkdev *d = lookup(dev); + if (!d) + return 0; + if (secsize) + *secsize = d->secsize; + return d->nsectors; +} + int blkdev_flush(uint dev) { @@ -348,7 +387,8 @@ blkdev_info(char *buf, int cap) off = put(buf, cap, off, " "); off = putu(buf, cap, off, (d->nsectors * d->secsize) >> 20); off = put(buf, cap, off, d->parent < 0 ? " disk" : - d->schfs ? " schfs" : " part"); + d->schfs > 0 ? " schfs" : + d->schfs < 0 ? " fat" : " part"); off = put(buf, cap, off, "\n"); } return off; diff --git a/kernel/dev/disk.c b/kernel/dev/disk.c index dd6ead7..b87c168 100644 --- a/kernel/dev/disk.c +++ b/kernel/dev/disk.c @@ -30,6 +30,7 @@ #include "buf.h" #include "file.h" #include "errno.h" +#include "blkdev.h" // The byte offset is 64-bit, all the way from struct file's off through the // devsw signature to bread's block number. It used to narrow to a 32-bit uint @@ -38,11 +39,65 @@ // not a refusal at 4 GB but silent wraparound past it. Nothing here may narrow // it again: an NVMe namespace or any modern disk is far beyond that. +// A disk or partition that registered with the block layer (minor 4 onward): +// a SATA or NVMe disk, a USB stick, any partition on them. Moved through +// blkdev_xfer in whole sectors rather than through the buffer cache, because a +// partition need not be a whole number of BSIZE blocks and the buffer cache's +// last block of one would run into the next. A read at the end returns 0, as +// a file's does, and a write there fails with ENOSPC. +static int +blk_rw(uint dev, uint64 off, char *p, int n, int write) +{ + uint32 ss = 0; + uint64 nsec = blkdev_sectors(dev, &ss); + if (nsec == 0) + return -ENXIO; + if (n <= 0) + return 0; + uint64 size = nsec * ss; + if (off >= size) + return write ? -ENOSPC : 0; + if ((uint64)n > size - off) + n = (int)(size - off); + + char *tmp = kalloc(); + if (!tmp) + return -ENOMEM; + int done = 0; + while (done < n) { + uint64 pos = off + (uint64)done; + uint64 lba = pos / ss; + uint in = (uint)(pos % ss); + uint32 cnt = (uint32)(BSIZE / ss); + if (lba + cnt > nsec) + cnt = (uint32)(nsec - lba); + int want = (int)(cnt * ss) - (int)in; + if (want > n - done) + want = n - done; + // Whole sectors are written as they are; a partial one is read first. + uint32 rcnt = (in + want + ss - 1) / ss; + if (!write || in || want % ss) + if (blkdev_xfer(dev, lba, rcnt, tmp, 0) < 0) + break; + if (write) { + memmove(tmp + in, p + done, want); + if (blkdev_xfer(dev, lba, rcnt, tmp, 1) < 0) + break; + } else + memmove(p + done, tmp + in, want); + done += want; + } + kfree(tmp); + return done ? done : -EIO; +} + int diskread(struct inode *ip, uint64 off, char *dst, int n) { int dev = ip->minor; + if (dev >= BLKDEV_FIRST) + return blk_rw((uint)dev, off, dst, n, 0); if (!ide_disk_present(dev)) return -ENXIO; if (n <= 0) @@ -71,6 +126,8 @@ diskwrite(struct inode *ip, uint64 off, char *src, int n) { int dev = ip->minor; + if (dev >= BLKDEV_FIRST) + return blk_rw((uint)dev, off, src, n, 1); if (!ide_disk_present(dev)) return -ENXIO; if (n <= 0) diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index 5300758..2a80aec 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -37,12 +37,18 @@ #include "fs.h" #include "file.h" #include "errno.h" +#include "blkdev.h" // Inode numbering. Two directories and a flat table of nodes, so the number is // either a directory id or an index into devnodes[] offset past them. #define DEVFS_ROOT 1ULL #define DEVFS_INPUT 2ULL /* the /dev/input directory */ #define DEVFS_FILE0 16ULL /* devnodes[0] is inode 16 */ +// Disks and partitions the block layer found: /dev/sata0, /dev/sata0p1, +// /dev/usb0p1, named as it named them. Major 8 like hda, minor the block +// device number, and inode DEVFS_BLK0 plus that number. Not in devnodes[], +// because they come and go: a stick plugged in appears, pulled out vanishes. +#define DEVFS_BLK0 4096ULL // Which directory a node lives in. Only two exist and a third would want a real // tree rather than a flag, which is the point at which this file should grow @@ -140,6 +146,12 @@ devfs_iload(struct inode *ip) ip->nlink = 1; ip->major = devnodes[i].major; ip->minor = devnodes[i].minor; + } else if (ip->inum >= DEVFS_BLK0 && + blkdev_present((uint)(ip->inum - DEVFS_BLK0))) { + ip->type = T_DEV; + ip->nlink = 1; + ip->major = 8; + ip->minor = (short)(ip->inum - DEVFS_BLK0); } else { ip->type = 0; /* no such node */ ip->nlink = 0; @@ -185,6 +197,11 @@ devfs_dirlookup(struct inode *dp, char *name, uint *poff) if (devnodes[i].dir == want && streq(name, devnodes[i].name)) return iget(DEVFS_DEV, DEVFS_FILE0 + (uint64)i); + if (want == D_ROOT) + for (int k = 0, b; (b = blkdev_nth(k)) >= 0; k++) + if (streq(name, blkdev_name((uint)b))) + return iget(DEVFS_DEV, DEVFS_BLK0 + (uint64)b); + return 0; } @@ -270,6 +287,16 @@ devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff) base++; } + if (want == D_ROOT) + for (int k = 0, b; (b = blkdev_nth(k)) >= 0; k++) { + if (n > base) { base++; continue; } + if (emit(out, cap, &used, DEVFS_BLK0 + (uint64)b, 6 /* DT_BLK */, + blkdev_name((uint)b)) < 0) + goto out; + n++; + base++; + } + out: *poff = n; return used; diff --git a/kernel/fs/fat/fat.c b/kernel/fs/fat/fat.c new file mode 100644 index 0000000..345d9fc --- /dev/null +++ b/kernel/fs/fat/fat.c @@ -0,0 +1,1996 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// FAT12, FAT16 and FAT32, with VFAT long file names. See include/fat.h. +// +// Written from Microsoft's FAT specification (the "FAT32 File System +// Specification", 1.03) and matching Linux's vfat where the specification +// leaves a choice: which names get a long-name entry, how the short alias is +// made, the lowercase bits in byte 12, the "not cleanly unmounted" bit. A +// stick written here should look, to Linux, Windows and macOS, like one any of +// them wrote. +// +// --------------------------------------------------------------------------- +// Inodes, on a filesystem that has none +// +// A FAT file is its directory entry: the name, the first cluster, the size and +// the times are all in those 32 bytes, and there is nothing else to point at. +// Linux numbers a FAT inode by the position of that entry on the volume, and +// so does this, with two refinements the VFS here needs: +// +// files TAG_FILE | (byte position of the short entry / 32) +// directories TAG_DIR | first cluster +// the root FAT_ROOT_INO +// +// A directory is numbered by its cluster because ".." has to find it: a +// directory's ".." entry holds its parent's first cluster and nothing about +// where the parent's own entry is. +// +// A file's entry moves when it is renamed, and the VFS renames by linking the +// new name to the same inode number and then unlinking the old one. So a file +// whose entry is not where its number says is recorded in a per-volume table, +// and so is a file created here, which has a number (TAG_NEW) before it has an +// entry. The table is consulted first, in both directions; a number not in it +// means what it says. +// +// --------------------------------------------------------------------------- +// I/O +// +// Through a cache of its own, not the buffer cache. FAT addresses sectors, a +// partition is not obliged to be a whole number of 4 KB blocks, and the buffer +// cache's last block on such a partition would be a write into the next one. +// Every operation that changes anything writes its dirty lines back before it +// returns, so a stick pulled out between two commands holds both of them. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "stat.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "proc.h" +#include "fs.h" +#include "file.h" +#include "errno.h" +#include "blkdev.h" +#include "fat.h" + +#define LINE 4096 +#define NLINE 64 // 256 KB of cache per volume +#define NMAP 1024 + +#define TAG_FILE (1ULL << 40) +#define TAG_DIR (2ULL << 40) +#define TAG_NEW (3ULL << 40) +#define TAG_MASK (0xFFULL << 40) + +#define A_RO 0x01 +#define A_HIDDEN 0x02 +#define A_SYSTEM 0x04 +#define A_VOLUME 0x08 +#define A_DIR 0x10 +#define A_ARCHIVE 0x20 +#define A_LFN 0x0F + +#define NT_LOWER_BASE 0x08 +#define NT_LOWER_EXT 0x10 + +#define FREE_UNKNOWN 0xFFFFFFFFu + +struct line { + uint64 idx; // which LINE-sized piece of the volume + int valid, dirty; + uint use; + uchar *data; +}; + +struct imap { + uint64 inum; + uint64 pos; // 0: no entry -- not yet linked, or unlinked +}; + +#define MAXNAME 255 + +// One name in a directory: its long-name entries, if any, and the short entry +// they belong to. +struct found { + uint32 first_i, sfn_i; // entry indexes: where the group starts, ends + uint64 sfn_pos; + uchar e[32]; + uint16 name[MAXNAME + 1]; + int nlen; +}; + +struct fvol { + int used, ro, err; + uint bdev; + uint32 secsize; + uint64 bytes; + + int fat; // 12, 16 or 32 + uint32 clsize, nfats; + uint64 fat_off, fat_bytes; // the first FAT this writes, and one's size + uint64 root_off; // FAT12/16: the fixed root directory + uint32 root_ents; + uint32 root_clus; // FAT32 + uint64 data_off; // cluster 2 + uint32 nclus; // data clusters; valid ones are 2..nclus+1 + uint64 fsinfo_off; // 0 when there is none + uint32 free_count, next_free; + int info_dirty; + uint64 state_off; // the boot sector's "dirty" byte, or 0 + + struct sleeplock lk; + struct line line[NLINE]; + uint clock; + struct imap map[NMAP]; + int nmap; + uint64 nextnew; + + // Scratch for the operation that holds `lk`. Here rather than on the stack, + // which is 8 KB and already deep by the time a path lookup gets this far: + // a name is 255 UTF-16 units, and a lookup wants three of them at once. + struct found scan, hit; + uint16 lfn[20 * 13]; + uint16 nm[MAXNAME + 1]; + char u8[MAXNAME * 3 + 1]; +}; + +static struct fvol vols[FAT_VOL_SLOTS]; +static struct spinlock slotlk; +static int slotinit; + +static struct fvol * +vol_of(uint dev) +{ + if (!FAT_IS_DEV(dev)) + return 0; + struct fvol *v = &vols[dev - FAT_DEV]; + return v->used ? v : 0; +} + +static uint16 le16(const uchar *p) { return p[0] | p[1] << 8; } +static uint32 le32(const uchar *p) +{ return p[0] | p[1] << 8 | p[2] << 16 | (uint32)p[3] << 24; } +static void put16(uchar *p, uint32 v) { p[0] = v; p[1] = v >> 8; } +static void put32(uchar *p, uint32 v) +{ p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; } + +// --- the cache --------------------------------------------------------------- + +static int +line_io(struct fvol *v, struct line *l, int write) +{ + uint64 off = l->idx * LINE; + uint64 n = v->bytes - off; + if (n > LINE) + n = LINE; + return blkdev_xfer(v->bdev, off / v->secsize, (uint32)(n / v->secsize), + l->data, write); +} + +static int +line_out(struct fvol *v, struct line *l) +{ + if (!l->dirty) + return 0; + l->dirty = 0; + if (line_io(v, l, 1) < 0) { + if (!v->err) + cprintf("fat: %s: write failed; the volume may be damaged\n", + blkdev_name(v->bdev)); + v->err = 1; + return -1; + } + return 0; +} + +// The line holding byte LINE*idx. `whole` when the caller is about to +// overwrite all of it, which saves reading what is about to be replaced. +static struct line * +line_get(struct fvol *v, uint64 idx, int whole) +{ + struct line *l, *victim = 0; + + for (l = v->line; l < v->line + NLINE; l++) + if (l->valid && l->idx == idx) { + l->use = ++v->clock; + return l; + } + for (l = v->line; l < v->line + NLINE; l++) { + if (!l->valid) { victim = l; break; } + if (!victim || l->use < victim->use) + victim = l; + } + l = victim; + if (l->valid) + line_out(v, l); + if (!l->data && (l->data = (uchar *)kalloc()) == 0) + return 0; + l->valid = 0; + l->idx = idx; + if (!whole && line_io(v, l, 0) < 0) { + if (!v->err) + cprintf("fat: %s: read failed\n", blkdev_name(v->bdev)); + v->err = 1; + return 0; + } + l->valid = 1; + l->dirty = 0; + l->use = ++v->clock; + return l; +} + +// Move n bytes between the volume at `off` and `addr`, which is a kernel +// address or, with `user`, one in the current process. +static int +vio_x(struct fvol *v, uint64 off, uint64 addr, uint64 n, int write, int user) +{ + if (off + n > v->bytes || off + n < off) + return -1; + while (n) { + uint64 idx = off / LINE, o = off % LINE, k = LINE - o; + if (k > n) + k = n; + struct line *l = line_get(v, idx, write && o == 0 && k == LINE); + if (!l) + return -1; + if (write) { + if (user) { + if (copyin(myproc()->pgdir, (addr_t)addr, l->data + o, k) < 0) + return -1; + } else + memmove(l->data + o, (void *)addr, k); + l->dirty = 1; + } else { + if (user) { + if (copyout(myproc()->pgdir, (addr_t)addr, l->data + o, k) < 0) + return -1; + } else + memmove((void *)addr, l->data + o, k); + } + addr += k; + off += k; + n -= k; + } + return 0; +} + +static int +vio(struct fvol *v, uint64 off, void *buf, uint64 n, int write) +{ + return vio_x(v, off, (uint64)buf, n, write, 0); +} + +static void +write_fsinfo(struct fvol *v) +{ + uchar b[8]; + if (!v->fsinfo_off || !v->info_dirty) + return; + put32(b, v->free_count); + put32(b + 4, v->next_free); + vio(v, v->fsinfo_off + 488, b, 8, 1); + v->info_dirty = 0; +} + +// Everything dirty, out. `device` also asks the drive to commit its own +// cache, which is a sync's job rather than every write's. +static int +flush(struct fvol *v, int device) +{ + int r = 0; + if (v->ro) + return 0; + for (struct line *l = v->line; l < v->line + NLINE; l++) + if (l->valid && line_out(v, l) < 0) + r = -1; + if (device) + blkdev_flush(v->bdev); + return r; +} + +// --- the allocation table ---------------------------------------------------- + +static int +valid_clus(struct fvol *v, uint32 c) +{ + return c >= 2 && c < v->nclus + 2; +} + +static uint32 +eoc(struct fvol *v) +{ + return v->fat == 32 ? 0x0FFFFFFF : v->fat == 16 ? 0xFFFF : 0xFFF; +} + +static uint32 +fat_get(struct fvol *v, uint32 c) +{ + uchar b[4] = { 0, 0, 0, 0 }; + + if (v->fat == 32) { + if (vio(v, v->fat_off + (uint64)c * 4, b, 4, 0) < 0) + return 0x0FFFFFFF; + return le32(b) & 0x0FFFFFFF; + } + if (v->fat == 16) { + if (vio(v, v->fat_off + (uint64)c * 2, b, 2, 0) < 0) + return 0xFFFF; + return le16(b); + } + if (vio(v, v->fat_off + c + c / 2, b, 2, 0) < 0) + return 0xFFF; + return (c & 1) ? le16(b) >> 4 : le16(b) & 0xFFF; +} + +// The next cluster in a chain, or 0 at its end -- whether that end is an +// end-of-chain mark, a bad-cluster mark or a value pointing nowhere. +static uint32 +next_clus(struct fvol *v, uint32 c) +{ + uint32 n = fat_get(v, c); + return valid_clus(v, n) ? n : 0; +} + +// Every copy of the table, unless the volume says only one is live. +static void +fat_set(struct fvol *v, uint32 c, uint32 val) +{ + uchar b[4]; + + for (uint32 i = 0; i < v->nfats; i++) { + uint64 base = v->fat_off + i * v->fat_bytes; + if (v->fat == 32) { + uint64 o = base + (uint64)c * 4; + vio(v, o, b, 4, 0); + put32(b, (le32(b) & 0xF0000000) | (val & 0x0FFFFFFF)); + vio(v, o, b, 4, 1); + } else if (v->fat == 16) { + put16(b, val); + vio(v, base + (uint64)c * 2, b, 2, 1); + } else { + uint64 o = base + c + c / 2; + vio(v, o, b, 2, 0); + uint32 x = le16(b); + x = (c & 1) ? (x & 0x000F) | ((val & 0xFFF) << 4) + : (x & 0xF000) | (val & 0xFFF); + put16(b, x); + vio(v, o, b, 2, 1); + } + } +} + +static uint64 +clus_off(struct fvol *v, uint32 c) +{ + return v->data_off + (uint64)(c - 2) * v->clsize; +} + +static void +zero_clus(struct fvol *v, uint32 c) +{ + uint64 off = clus_off(v, c), end = off + v->clsize; + while (off < end) { + uint64 idx = off / LINE, o = off % LINE, k = LINE - o; + if (k > end - off) + k = end - off; + struct line *l = line_get(v, idx, o == 0 && k == LINE); + if (!l) + return; + memset(l->data + o, 0, k); + l->dirty = 1; + off += k; + } +} + +// A free cluster, zeroed, marked end-of-chain and linked after `prev` if +// there is one. 0 when the volume is full. +// +// Zeroed because FAT has no holes: a file extended past its end reads back as +// whatever the new cluster held, and a directory's end is the first entry +// whose first byte is zero. The zeroing happens in the cache, so a cluster +// that is about to be written in full costs no extra I/O. +static uint32 +clus_alloc(struct fvol *v, uint32 prev) +{ + uint32 start = valid_clus(v, v->next_free) ? v->next_free : 2; + + if (v->free_count == 0) + return 0; + for (uint32 i = 0; i < v->nclus; i++) { + uint32 c = 2 + (start - 2 + i) % v->nclus; + if (fat_get(v, c) != 0) + continue; + fat_set(v, c, eoc(v)); + if (prev) + fat_set(v, prev, c); + zero_clus(v, c); + v->next_free = c + 1; + if (v->free_count != FREE_UNKNOWN) + v->free_count--; + v->info_dirty = 1; + return c; + } + v->free_count = 0; + return 0; +} + +static void +free_chain(struct fvol *v, uint32 c) +{ + // Bounded by the size of the volume: a chain that loops is damage, and + // following it would never end. + for (uint32 n = 0; valid_clus(v, c) && n <= v->nclus; n++) { + uint32 next = fat_get(v, c); + fat_set(v, c, 0); + if (v->free_count != FREE_UNKNOWN) + v->free_count++; + v->info_dirty = 1; + c = valid_clus(v, next) ? next : 0; + } +} + +static uint32 +count_free(struct fvol *v) +{ + uint32 n = 0; + for (uint32 c = 2; c < v->nclus + 2; c++) + if (fat_get(v, c) == 0) + n++; + return n; +} + +// --- directories ------------------------------------------------------------- + +// A walk over one directory's entries by index. `first` is its first +// cluster, or 0 for the fixed root directory of FAT12 and FAT16. Remembers +// where it is, so stepping forward is not a walk from the start each time. +struct dwalk { + struct fvol *v; + uint32 first, clus, cidx; +}; + +static void +dwalk_init(struct dwalk *d, struct fvol *v, uint32 first) +{ + d->v = v; + d->first = first; + d->clus = 0; + d->cidx = 0; +} + +// The volume position of entry i, or 0 past the end of the directory. +static uint64 +ent_pos(struct dwalk *d, uint32 i) +{ + struct fvol *v = d->v; + + if (d->first == 0) + return i < v->root_ents ? v->root_off + (uint64)i * 32 : 0; + + uint32 per = v->clsize / 32, want = i / per; + if (d->clus == 0 || want < d->cidx) { + d->clus = d->first; + d->cidx = 0; + } + while (d->cidx < want) { + uint32 n = next_clus(v, d->clus); + if (n == 0) + return 0; + d->clus = n; + d->cidx++; + } + return clus_off(v, d->clus) + (uint64)(i % per) * 32; +} + +// The cluster a directory inode's entries start in. +static uint32 +dir_first(struct fvol *v, struct inode *dp) +{ + if (dp->inum == FAT_ROOT_INO) + return v->fat == 32 ? v->root_clus : 0; + return (uint32)(dp->inum & ~TAG_MASK); +} + +static uint32 +ent_clus(struct fvol *v, const uchar *e) +{ + uint32 c = le16(e + 26); + if (v->fat == 32) + c |= (uint32)le16(e + 20) << 16; + return c; +} + +static void +set_clus(uchar *e, uint32 c) +{ + put16(e + 26, c & 0xFFFF); + put16(e + 20, c >> 16); +} + +// --- names ------------------------------------------------------------------- + +// Code page 437, 0x80-0xFF, which is what a short name's high bytes mean +// unless a volume says otherwise -- and none say. +static const uint16 cp437[128] = { + 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7, + 0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5, + 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9, + 0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192, + 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA, + 0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB, + 0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556, + 0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510, + 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F, + 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567, + 0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B, + 0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580, + 0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x00B5,0x03C4, + 0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229, + 0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248, + 0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0, +}; + +// UTF-8 to UTF-16. Returns the length in code units, or -1 for a name that +// is not valid UTF-8 or will not fit. +static int +utf8_to_16(const char *s, int n, uint16 *out) +{ + int len = 0; + + for (int i = 0; i < n;) { + uint32 c = (uchar)s[i]; + int extra = c < 0x80 ? 0 : (c & 0xE0) == 0xC0 ? 1 : (c & 0xF0) == 0xE0 ? 2 + : (c & 0xF8) == 0xF0 ? 3 : -1; + if (extra < 0 || i + extra >= n + (extra ? 0 : 1)) + return -1; + if (extra) + c &= 0x3F >> extra; + for (int k = 1; k <= extra; k++) { + uchar b = s[i + k]; + if ((b & 0xC0) != 0x80) + return -1; + c = (c << 6) | (b & 0x3F); + } + i += 1 + extra; + if (c >= 0x10000) { + if (len + 2 > MAXNAME) + return -1; + c -= 0x10000; + out[len++] = 0xD800 | (c >> 10); + out[len++] = 0xDC00 | (c & 0x3FF); + } else { + if (len + 1 > MAXNAME) + return -1; + out[len++] = c; + } + } + return len; +} + +// UTF-16 to UTF-8, NUL-terminated. Returns the byte length. +static int +utf16_to_8(const uint16 *s, int n, char *out, int cap) +{ + int len = 0; + + for (int i = 0; i < n; i++) { + uint32 c = s[i]; + if (c >= 0xD800 && c < 0xDC00 && i + 1 < n && + s[i + 1] >= 0xDC00 && s[i + 1] < 0xE000) { + c = 0x10000 + ((c - 0xD800) << 10) + (s[i + 1] - 0xDC00); + i++; + } + char t[4]; + int k; + if (c < 0x80) { t[0] = c; k = 1; } + else if (c < 0x800) { t[0] = 0xC0 | c >> 6; t[1] = 0x80 | (c & 0x3F); k = 2; } + else if (c < 0x10000){ t[0] = 0xE0 | c >> 12; t[1] = 0x80 | ((c >> 6) & 0x3F); + t[2] = 0x80 | (c & 0x3F); k = 3; } + else { t[0] = 0xF0 | c >> 18; t[1] = 0x80 | ((c >> 12) & 0x3F); + t[2] = 0x80 | ((c >> 6) & 0x3F); t[3] = 0x80 | (c & 0x3F); k = 4; } + if (len + k >= cap) + break; + for (int j = 0; j < k; j++) + out[len++] = t[j]; + } + out[len] = 0; + return len; +} + +// FAT names compare without regard to case. ASCII and Latin-1 are folded; +// beyond that a name has to match as written, which is what Linux does too +// without a full Unicode case table. +static uint16 +fold(uint16 c) +{ + if (c >= 'a' && c <= 'z') + return c - 32; + if (c >= 0xE0 && c <= 0xFE && c != 0xF7) + return c - 32; + return c; +} + +static int +name_eq(const uint16 *a, int an, const uint16 *b, int bn) +{ + if (an != bn) + return 0; + for (int i = 0; i < an; i++) + if (fold(a[i]) != fold(b[i])) + return 0; + return 1; +} + +// The short name as it is shown: "README.TXT", or "readme.txt" when byte 12 +// says the parts were lowercase -- which is how Windows NT and Linux store an +// all-lowercase 8.3 name without a long-name entry. +static int +sfn_name(const uchar *e, uint16 *out) +{ + int n = 0, b = 8, x = 3; + + while (b > 0 && e[b - 1] == ' ') + b--; + while (x > 0 && e[8 + x - 1] == ' ') + x--; + for (int i = 0; i < b; i++) { + uchar c = (i == 0 && e[0] == 0x05) ? 0xE5 : e[i]; + if ((e[12] & NT_LOWER_BASE) && c >= 'A' && c <= 'Z') + c += 32; + out[n++] = c < 0x80 ? c : cp437[c - 0x80]; + } + if (x) { + out[n++] = '.'; + for (int i = 0; i < x; i++) { + uchar c = e[8 + i]; + if ((e[12] & NT_LOWER_EXT) && c >= 'A' && c <= 'Z') + c += 32; + out[n++] = c < 0x80 ? c : cp437[c - 0x80]; + } + } + return n; +} + +static uchar +sfn_sum(const uchar *e) +{ + uchar s = 0; + for (int i = 0; i < 11; i++) + s = ((s & 1) << 7) + (s >> 1) + e[i]; + return s; +} + +// A name the VFS hands over, as UTF-16, with the trailing dots Windows and +// Linux both drop. -1 for one FAT cannot hold. +static int +vfs_name(const char *name, uint16 *out) +{ + int n = 0; + while (n < DIRSIZ && name[n]) + n++; + int len = utf8_to_16(name, n, out); + if (len < 0) + return -1; + if (!(len == 1 && out[0] == '.') && !(len == 2 && out[0] == '.' && out[1] == '.')) + while (len > 0 && out[len - 1] == '.') + len--; + return len > 0 ? len : -1; +} + +static int +lfn_char_ok(uint16 c) +{ + if (c < 0x20) + return 0; + switch (c) { + case '"': case '*': case '/': case ':': case '<': case '>': case '?': + case '\\': case '|': + return 0; + } + return 1; +} + +static int +sfn_char_ok(uint16 c) +{ + if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) + return 1; + switch (c) { + case '!': case '#': case '$': case '%': case '&': case '\'': case '(': + case ')': case '-': case '@': case '^': case '_': case '`': case '{': + case '}': case '~': + return 1; + } + return 0; +} + +// Does the name fit 8.3 as it stands? If it does, fill in the eleven bytes +// and byte 12's lowercase bits, and no long-name entry is needed. Mixed case +// within a part ("Readme") does not fit: only a long name can keep it. +static int +fits_83(const uint16 *s, int n, uchar *sfn, uchar *nt) +{ + int dot = -1, lower[2] = { 0, 0 }, upper[2] = { 0, 0 }; + + for (int i = 0; i < n; i++) + if (s[i] == '.') { + if (dot >= 0) + return 0; + dot = i; + } + int blen = dot < 0 ? n : dot, xlen = dot < 0 ? 0 : n - dot - 1; + if (blen < 1 || blen > 8 || xlen > 3 || (dot >= 0 && xlen == 0)) + return 0; + + memset(sfn, ' ', 11); + for (int i = 0; i < n; i++) { + if (i == dot) + continue; + int part = dot >= 0 && i > dot; + uint16 c = s[i]; + if (c >= 'a' && c <= 'z') { lower[part] = 1; c -= 32; } + else if (c >= 'A' && c <= 'Z') upper[part] = 1; + if (!sfn_char_ok(c)) + return 0; + sfn[part ? 8 + (i - dot - 1) : i] = c; + } + if ((lower[0] && upper[0]) || (lower[1] && upper[1])) + return 0; + if (sfn[0] == 0xE5) + sfn[0] = 0x05; + *nt = (lower[0] ? NT_LOWER_BASE : 0) | (lower[1] ? NT_LOWER_EXT : 0); + return 1; +} + +// --- scanning a directory ---------------------------------------------------- + +typedef int (*scan_fn)(struct fvol *, struct found *, void *); + +// Call fn for every name from entry `from` on, stopping when it returns +// non-zero (and returning that). `*end` is where the directory ends. +static int +dir_scan(struct fvol *v, uint32 first, uint32 from, scan_fn fn, void *arg, + uint32 *end) +{ + struct dwalk d; + struct found *fp = &v->scan; + uint16 *lfn = v->lfn; + int have = 0, expect = 0, total = 0; + uchar sum = 0; + uint32 start = 0, i; + + dwalk_init(&d, v, first); + for (i = from;; i++) { + uint64 pos = ent_pos(&d, i); + uchar e[32]; + if (pos == 0 || vio(v, pos, e, 32, 0) < 0) + break; + if (e[0] == 0x00) + break; // nothing in use from here on + if (e[0] == 0xE5) { + have = 0; + continue; + } + if ((e[11] & 0x3F) == A_LFN) { + int ord = e[0] & 0x3F; + if (e[0] & 0x40) { + if (ord == 0 || ord > 20) { have = 0; continue; } + have = 1; + total = expect = ord; + sum = e[13]; + start = i; + } + if (!have || ord != expect || e[13] != sum) { + have = 0; + continue; + } + uint16 *p = lfn + (ord - 1) * 13; + for (int k = 0; k < 5; k++) p[k] = le16(e + 1 + 2 * k); + for (int k = 0; k < 6; k++) p[5 + k] = le16(e + 14 + 2 * k); + for (int k = 0; k < 2; k++) p[11 + k] = le16(e + 28 + 2 * k); + expect--; + continue; + } + if (e[11] & A_VOLUME) { + have = 0; + continue; + } + + memmove(fp->e, e, 32); + fp->sfn_i = i; + fp->sfn_pos = pos; + if (have && expect == 0 && sfn_sum(e) == sum) { + int n = 0; + while (n < total * 13 && lfn[n] != 0x0000 && n < MAXNAME) + n++; + memmove(fp->name, lfn, n * sizeof(uint16)); + fp->nlen = n; + fp->first_i = start; + } else { + fp->nlen = sfn_name(e, fp->name); + fp->first_i = i; + } + have = 0; + int r = fn(v, fp, arg); + if (r) + return r; + } + if (end) + *end = i; + return 0; +} + +struct want { + const uint16 *name; + int nlen; + struct found *out; +}; + +static int +match_name(struct fvol *v, struct found *f, void *arg) +{ + struct want *w = arg; + uint16 alt[16]; + (void)v; + if (name_eq(f->name, f->nlen, w->name, w->nlen)) { + *w->out = *f; + return 1; + } + // A name with a long entry still answers to its short alias, as it does + // everywhere else -- "PROGRA~1" is how a good many scripts reach things. + int an = sfn_name(f->e, alt); + if (name_eq(alt, an, w->name, w->nlen)) { + *w->out = *f; + return 1; + } + return 0; +} + +static int +find(struct fvol *v, uint32 first, const uint16 *name, int nlen, + struct found *out) +{ + struct want w = { name, nlen, out }; + return dir_scan(v, first, 0, match_name, &w, 0) == 1; +} + +struct sfnq { const uchar *sfn; }; + +static int +match_sfn(struct fvol *v, struct found *f, void *arg) +{ + (void)v; + return memcmp(f->e, ((struct sfnq *)arg)->sfn, 11) == 0; +} + +static int +sfn_taken(struct fvol *v, uint32 first, const uchar *sfn) +{ + struct sfnq q = { sfn }; + return dir_scan(v, first, 0, match_sfn, &q, 0) == 1; +} + +// The short alias for a name that needs a long one, the way Linux makes it. +// A name that only needed one for its case ("Reports", "ReadMe2.Txt") gets +// its uppercase self, "REPORTS", if no other entry has it. Otherwise: the name +// uppercased, without spaces or dots, with anything a short name cannot hold +// turned into '_', cut to six characters and "~1" added -- or "~2", and on, +// until it is unique in the directory. +static int +make_alias(struct fvol *v, uint32 first, const uint16 *s, int n, uchar *sfn) +{ + uchar base[8], ext[3]; + int blen = 0, xlen = 0, dot = -1, lossy = 0; + + for (int i = n - 1; i > 0; i--) + if (s[i] == '.') { dot = i; break; } + if (dot < 0 && s[0] == '.') + lossy = 1; // ".profile": a leading dot is not an extension + for (int i = 0; i < (dot < 0 ? n : dot); i++) { + uint16 c = s[i]; + if (c == ' ' || c == '.') { lossy = 1; continue; } + if (blen == 8) { lossy = 1; break; } + if (c >= 'a' && c <= 'z') c -= 32; + if (!sfn_char_ok(c)) { lossy = 1; c = '_'; } + base[blen++] = c; + } + if (dot >= 0) + for (int i = dot + 1; i < n; i++) { + uint16 c = s[i]; + if (c == ' ' || c == '.') { lossy = 1; continue; } + if (xlen == 3) { lossy = 1; break; } + if (c >= 'a' && c <= 'z') c -= 32; + if (!sfn_char_ok(c)) { lossy = 1; c = '_'; } + ext[xlen++] = c; + } + if (blen == 0) { + base[blen++] = '_'; + lossy = 1; + } + + if (!lossy) { + memset(sfn, ' ', 11); + memmove(sfn, base, blen); + memmove(sfn + 8, ext, xlen); + if (sfn[0] == 0xE5) + sfn[0] = 0x05; + if (!sfn_taken(v, first, sfn)) + return 0; + } + + for (uint32 num = 1; num < 1000000; num++) { + char digits[8]; + int nd = 0; + for (uint32 t = num; t; t /= 10) + digits[nd++] = '0' + t % 10; + int keep = 8 - 1 - nd; + if (keep > blen) + keep = blen; + memset(sfn, ' ', 11); + memmove(sfn, base, keep); + sfn[keep] = '~'; + for (int k = 0; k < nd; k++) + sfn[keep + 1 + k] = digits[nd - 1 - k]; + memmove(sfn + 8, ext, xlen); + if (!sfn_taken(v, first, sfn)) + return 0; + } + return -1; +} + +// Add a cluster to the end of a directory. The fixed root of FAT12/16 cannot +// grow, and a full one is full. +static int +dir_grow(struct fvol *v, uint32 first) +{ + if (first == 0) + return -1; + uint32 c = first, n; + for (uint32 k = 0; (n = next_clus(v, c)) != 0 && k <= v->nclus; k++) + c = n; + return clus_alloc(v, c) ? 0 : -1; +} + +// Write a name into a directory: its long-name entries, then `e`, the short +// entry, whose first eleven bytes and byte 12 this fills in. Returns the +// short entry's volume position, or 0. +static uint64 +dir_add(struct fvol *v, uint32 first, const uint16 *name, int nlen, uchar *e) +{ + uchar nt = 0; + int need_lfn = !fits_83(name, nlen, e, &nt); + + if (need_lfn) { + if (make_alias(v, first, name, nlen, e) < 0) + return 0; + nt = 0; + } + e[12] = nt; + + int nl = need_lfn ? (nlen + 12) / 13 : 0, count = nl + 1; + + // The first run of `count` free entries, growing the directory as needed. + struct dwalk d; + uint32 run = 0, i; + dwalk_init(&d, v, first); + for (i = 0;; i++) { + uint64 pos = ent_pos(&d, i); + if (pos == 0) { + if (dir_grow(v, first) < 0) + return 0; + pos = ent_pos(&d, i); + if (pos == 0) + return 0; + } + uchar b; + if (vio(v, pos, &b, 1, 0) < 0) + return 0; + if (b == 0x00 || b == 0xE5) { + if (++run == (uint32)count) + break; + } else + run = 0; + } + uint32 at = i + 1 - count; + + uchar sum = sfn_sum(e); + for (int k = 0; k < nl; k++) { + int ord = nl - k; + uchar l[32]; + memset(l, 0, 32); + l[0] = ord | (k == 0 ? 0x40 : 0); + l[11] = A_LFN; + l[13] = sum; + for (int j = 0; j < 13; j++) { + int ci = (ord - 1) * 13 + j; + uint16 c = ci < nlen ? name[ci] : ci == nlen ? 0x0000 : 0xFFFF; + int o = j < 5 ? 1 + 2 * j : j < 11 ? 14 + 2 * (j - 5) : 28 + 2 * (j - 11); + put16(l + o, c); + } + if (vio(v, ent_pos(&d, at + k), l, 32, 1) < 0) + return 0; + } + uint64 pos = ent_pos(&d, at + nl); + if (vio(v, pos, e, 32, 1) < 0) + return 0; + return pos; +} + +static void +dir_del(struct fvol *v, uint32 first, struct found *f) +{ + struct dwalk d; + uchar gone = 0xE5; + dwalk_init(&d, v, first); + for (uint32 i = f->first_i; i <= f->sfn_i; i++) { + uint64 pos = ent_pos(&d, i); + if (pos) + vio(v, pos, &gone, 1, 1); + } +} + +// --- times ------------------------------------------------------------------- + +static int64 +days_from_civil(int64 y, unsigned m, unsigned d) +{ + y -= m <= 2; + int64 era = (y >= 0 ? y : y - 399) / 400; + unsigned yoe = (unsigned)(y - era * 400); + unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + (int64)doe - 719468; +} + +static void +civil_from_days(int64 z, int *y, int *m, int *d) +{ + z += 719468; + int64 era = (z >= 0 ? z : z - 146096) / 146097; + unsigned doe = (unsigned)(z - era * 146097); + unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + unsigned mp = (5 * doy + 2) / 153; + *d = doy - (153 * mp + 2) / 5 + 1; + *m = mp < 10 ? mp + 3 : mp - 9; + *y = (int)(yoe + era * 400) + (*m <= 2); +} + +// Times are stored as UTC. FAT has no time zone; Linux's default for one it +// is not told about is the same. +static void +to_fat(uint64 t, uint16 *date, uint16 *time) +{ + int y, m, d; + uint64 s = t % 86400; + civil_from_days((int64)(t / 86400), &y, &m, &d); + if (y < 1980) { y = 1980; m = 1; d = 1; s = 0; } + if (y > 2107) { y = 2107; m = 12; d = 31; s = 86399; } + *date = (uint16)(((y - 1980) << 9) | (m << 5) | d); + *time = (uint16)(((s / 3600) << 11) | (((s / 60) % 60) << 5) | ((s % 60) / 2)); +} + +static uint +from_fat(uint16 date, uint16 time) +{ + unsigned m = (date >> 5) & 15, d = date & 31; + if (date == 0) + return 0; + if (m < 1) m = 1; + if (d < 1) d = 1; + int64 days = days_from_civil(1980 + (date >> 9), m, d); + return (uint)(days * 86400 + (time >> 11) * 3600 + ((time >> 5) & 63) * 60 + + (time & 31) * 2); +} + +static uint +now(void) +{ + uint64 s = 0, us = 0; + wallclock(&s, &us); + return (uint)s; +} + +// --- inode numbers ----------------------------------------------------------- + +static uint64 +pos_of(struct fvol *v, uint64 inum) +{ + for (int i = 0; i < v->nmap; i++) + if (v->map[i].inum == inum) + return v->map[i].pos; + if ((inum & TAG_MASK) == TAG_FILE) + return (inum & ~TAG_MASK) * 32; + return 0; +} + +static void +map_del(struct fvol *v, uint64 inum) +{ + for (int i = 0; i < v->nmap; i++) + if (v->map[i].inum == inum) { + v->map[i] = v->map[--v->nmap]; + return; + } +} + +static void +map_set(struct fvol *v, uint dev, uint64 inum, uint64 pos) +{ + if ((inum & TAG_MASK) == TAG_FILE && pos == (inum & ~TAG_MASK) * 32) { + map_del(v, inum); + return; + } + for (int i = 0; i < v->nmap; i++) + if (v->map[i].inum == inum) { + v->map[i].pos = pos; + return; + } + if (v->nmap == NMAP) { + // Full. Anything nobody holds can go: an inode with no references is + // reloaded from scratch by the next iget, and finds its entry by name. + for (int i = 0; i < v->nmap;) + if (!iheld(dev, v->map[i].inum)) + v->map[i] = v->map[--v->nmap]; + else + i++; + if (v->nmap == NMAP) { + cprintf("fat: inode table full; a renamed file may be seen twice\n"); + v->nmap--; + } + } + v->map[v->nmap].inum = inum; + v->map[v->nmap].pos = pos; + v->nmap++; +} + +// The number for the entry at `pos`. Normally its position -- but if that +// number is already someone else's, because a file renamed away from here or +// unlinked while open still answers to it, the entry here gets a fresh one. +static uint64 +inum_at(struct fvol *v, uint dev, uint64 pos) +{ + uint64 natural = TAG_FILE | (pos / 32); + int taken = 0; + for (int i = 0; i < v->nmap; i++) { + if (v->map[i].pos == pos && (v->map[i].inum & TAG_MASK) != TAG_DIR) + return v->map[i].inum; + if (v->map[i].inum == natural) + taken = 1; + } + if (!taken) + return natural; + uint64 fresh = TAG_NEW | ++v->nextnew; + map_set(v, dev, fresh, pos); + return fresh; +} + +// The inode number for the name `f`. +static uint64 +inum_of(struct fvol *v, uint dev, struct found *f) +{ + if (f->e[11] & A_DIR) { + uint32 c = ent_clus(v, f->e); + if (c == 0 || (v->fat == 32 && c == v->root_clus)) + return FAT_ROOT_INO; + return TAG_DIR | c; + } + return inum_at(v, dev, f->sfn_pos); +} + +// --- fs_ops ------------------------------------------------------------------ + +static void +fat_iload(struct inode *ip) +{ + struct fvol *v = vol_of(ip->dev); + uchar e[32]; + + ip->major = ip->minor = 0; + ip->rwmode = RW; + ip->supermode = 0; + ip->showmode = 1; + ip->atime = ip->mtime = ip->ctime = 0; + ip->size = 0; + memset(ip->addrs, 0, sizeof(ip->addrs)); + ip->type = T_FILE; + ip->nlink = 0; + if (!v) { + ip->valid = 1; + return; + } + + acquiresleep(&v->lk); + uint64 pos = pos_of(v, ip->inum); + int have = pos && vio(v, pos, e, 32, 0) == 0 && e[0] != 0xE5 && e[0] != 0; + if (ip->inum == FAT_ROOT_INO || (ip->inum & TAG_MASK) == TAG_DIR) { + ip->type = T_DIR; + ip->nlink = 2; + ip->addrs[0] = ip->inum == FAT_ROOT_INO ? 0 : (uint)(ip->inum & ~TAG_MASK); + } else if (have && !(e[11] & A_DIR)) { + ip->nlink = 1; + ip->size = le32(e + 28); + ip->addrs[0] = ent_clus(v, e); + ip->addrs[1] = e[11]; + } + if (have) { + ip->mtime = from_fat(le16(e + 24), le16(e + 22)); + ip->ctime = from_fat(le16(e + 16), le16(e + 14)); + ip->atime = from_fat(le16(e + 18), 0); + if (ip->ctime == 0) + ip->ctime = ip->mtime; + if (ip->atime == 0) + ip->atime = ip->mtime; + } + releasesleep(&v->lk); + ip->valid = 1; +} + +// The directory entry, brought up to date. Caller holds the volume. +static void +store(struct fvol *v, struct inode *ip) +{ + uchar e[32]; + uint64 pos; + + if (v->ro || ip->type == T_DIR || (pos = pos_of(v, ip->inum)) == 0) + return; + if (vio(v, pos, e, 32, 0) < 0 || e[0] == 0xE5 || e[0] == 0) + return; + uint16 d, t; + set_clus(e, ip->addrs[0]); + put32(e + 28, (uint32)ip->size); + if (ip->mtime) { + to_fat(ip->mtime, &d, &t); + put16(e + 24, d); + put16(e + 22, t); + } + if (ip->atime) { + to_fat(ip->atime, &d, &t); + put16(e + 18, d); + } + vio(v, pos, e, 32, 1); +} + +static void +fat_istore(struct inode *ip) +{ + struct fvol *v = vol_of(ip->dev); + if (!v || v->ro) + return; + acquiresleep(&v->lk); + store(v, ip); + flush(v, 0); + releasesleep(&v->lk); +} + +static void +fat_ifree(struct inode *ip) +{ + struct fvol *v = vol_of(ip->dev); + if (!v) + return; + acquiresleep(&v->lk); + if (!v->ro) { + free_chain(v, ip->addrs[0]); + write_fsinfo(v); + flush(v, 0); + } + map_del(v, ip->inum); + releasesleep(&v->lk); +} + +// The cluster holding byte idx*clsize of a file, allocating up to it when +// `grow`. The last one found is kept in addrs[2..3], so reading or writing a +// file front to back walks its chain once rather than once per call. +static uint32 +file_clus(struct fvol *v, struct inode *ip, uint32 idx, int grow) +{ + uint32 c, at; + + if (ip->addrs[0] == 0) { + if (!grow || (ip->addrs[0] = clus_alloc(v, 0)) == 0) + return 0; + ip->addrs[2] = 0; + } + if (ip->addrs[2] && ip->addrs[2] - 1 <= idx) { + at = ip->addrs[2] - 1; + c = ip->addrs[3]; + } else { + at = 0; + c = ip->addrs[0]; + } + while (at < idx) { + uint32 n = next_clus(v, c); + if (n == 0) { + if (!grow || (n = clus_alloc(v, c)) == 0) + return 0; + } + c = n; + at++; + } + ip->addrs[2] = idx + 1; + ip->addrs[3] = c; + return c; +} + +static int +fat_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n) +{ + struct fvol *v = vol_of(ip->dev); + + if (ip->type == T_DIR) { + // build_dir_tree() reads a new directory's first two entries in the v1 + // layout before overwriting them. FAT writes its own "." and ".." in + // dirlink, so there is nothing to show it. + if (user_dst) + return -EISDIR; + memset((void *)dst, 0, n); + return n; + } + if (!v) + return -EIO; + + acquiresleep(&v->lk); + if (off >= ip->size) { + releasesleep(&v->lk); + return 0; + } + if (off + n > ip->size) + n = (uint)(ip->size - off); + + uint done = 0; + while (done < n) { + uint64 o = off + done; + uint32 c = file_clus(v, ip, (uint32)(o / v->clsize), 0); + if (c == 0) + break; + uint64 in = o % v->clsize, k = v->clsize - in; + if (k > n - done) + k = n - done; + if (vio_x(v, clus_off(v, c) + in, dst + done, k, 0, user_dst) < 0) + break; + done += (uint)k; + } + releasesleep(&v->lk); + return done ? (int)done : (v->err ? -EIO : 0); +} + +static char zeros[512]; + +// Write n bytes at off; caller holds the volume. Returns what was written. +static uint +put_data(struct fvol *v, struct inode *ip, int user, uint64 src, uint64 off, + uint n, int zero) +{ + uint done = 0; + while (done < n) { + uint64 o = off + done; + uint32 c = file_clus(v, ip, (uint32)(o / v->clsize), 1); + if (c == 0) + break; + uint64 in = o % v->clsize, k = v->clsize - in; + if (k > n - done) + k = n - done; + if (zero && k > sizeof(zeros)) + k = sizeof(zeros); + if (vio_x(v, clus_off(v, c) + in, zero ? (uint64)zeros : src + done, k, 1, + zero ? 0 : user) < 0) + break; + done += (uint)k; + } + return done; +} + +static int +fat_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n) +{ + struct fvol *v = vol_of(ip->dev); + + if (ip->type == T_DIR) + return n; // build_dir_tree(); see fat_read + if (!v) + return -EIO; + if (v->ro) + return -EROFS; + if (off >= 0xFFFFFFFFULL) + return -EFBIG; + if (off + n > 0xFFFFFFFFULL) + n = (uint)(0xFFFFFFFFULL - off); + + acquiresleep(&v->lk); + // No holes in FAT: a write past the end fills the gap with zeros first. + if (off > ip->size) { + uint gap = (uint)(off - ip->size); + if (put_data(v, ip, 0, 0, ip->size, gap, 1) < gap) { + store(v, ip); + flush(v, 0); + releasesleep(&v->lk); + return -ENOSPC; + } + ip->size = off; + } + uint done = put_data(v, ip, user_src, src, off, n, 0); + if (off + done > ip->size) + ip->size = off + done; + ip->mtime = now(); + store(v, ip); + write_fsinfo(v); + int r = flush(v, 0); + releasesleep(&v->lk); + if (done == 0 && n) + return v->free_count == 0 ? -ENOSPC : -EIO; + return r < 0 && done == 0 ? -EIO : (int)done; +} + +static void +fat_truncate(struct inode *ip, uint64 newsize) +{ + struct fvol *v = vol_of(ip->dev); + + if (!v || v->ro || ip->type == T_DIR) + return; + acquiresleep(&v->lk); + if (newsize < ip->size) { + uint32 keep = (uint32)((newsize + v->clsize - 1) / v->clsize); + if (keep == 0) { + free_chain(v, ip->addrs[0]); + ip->addrs[0] = 0; + } else { + uint32 last = file_clus(v, ip, keep - 1, 0); + if (last) { + uint32 rest = next_clus(v, last); + fat_set(v, last, eoc(v)); + free_chain(v, rest); + } + } + ip->addrs[2] = 0; + ip->size = newsize; + } else if (newsize > ip->size && newsize < 0xFFFFFFFFULL) { + uint gap = (uint)(newsize - ip->size); + ip->size += put_data(v, ip, 0, 0, ip->size, gap, 1); + } + ip->mtime = now(); + store(v, ip); + write_fsinfo(v); + flush(v, 0); + releasesleep(&v->lk); +} + +static struct inode * +fat_dirlookup(struct inode *dp, char *name, uint *poff) +{ + struct fvol *v = vol_of(dp->dev); + int n; + + if (poff) + *poff = 0; + if (!v) + return 0; + + acquiresleep(&v->lk); + uint16 *nm = v->nm; + struct found *fp = &v->hit; + if ((n = vfs_name(name, nm)) < 0) { + releasesleep(&v->lk); + return 0; + } + if (n == 1 && nm[0] == '.') { + releasesleep(&v->lk); + return iget(dp->dev, dp->inum); + } + uint32 first = dir_first(v, dp); + uint64 inum = 0; + + if (n == 2 && nm[0] == '.' && nm[1] == '.') { + if (dp->inum == FAT_ROOT_INO) { + inum = FAT_ROOT_INO; + } else { + // Entry 1 of every directory but the root is "..", and holds the + // parent's first cluster -- 0 when the parent is the root. + struct dwalk d; + uchar e[32]; + dwalk_init(&d, v, first); + uint64 pos = ent_pos(&d, 1); + if (pos && vio(v, pos, e, 32, 0) == 0 && e[0] == '.' && e[1] == '.') { + uint32 c = ent_clus(v, e); + inum = (c == 0 || (v->fat == 32 && c == v->root_clus)) + ? FAT_ROOT_INO : TAG_DIR | c; + } + } + } else if (find(v, first, nm, n, fp) && + !(fp->nlen == 1 && fp->name[0] == '.') && + !(fp->nlen == 2 && fp->name[0] == '.' && fp->name[1] == '.')) { + inum = inum_of(v, dp->dev, fp); + if ((inum & TAG_MASK) == TAG_DIR) + map_set(v, dp->dev, inum, fp->sfn_pos); // for its times + if (poff) + *poff = fp->sfn_i; + } + releasesleep(&v->lk); + return inum ? iget(dp->dev, inum) : 0; +} + +static int +fat_dirlink(struct inode *dp, char *name, uint64 inum) +{ + struct fvol *v = vol_of(dp->dev); + uchar e[32]; + int n; + + if (!v) + return -EIO; + if (v->ro) + return -EROFS; + + acquiresleep(&v->lk); + uint16 *nm = v->nm; + struct found *fp = &v->hit; + if ((n = vfs_name(name, nm)) < 0) { + releasesleep(&v->lk); + return -EINVAL; + } + if ((n == 1 && nm[0] == '.') || (n == 2 && nm[0] == '.' && nm[1] == '.')) { + releasesleep(&v->lk); + return 0; // written with the directory; see below + } + for (int i = 0; i < n; i++) + if (!lfn_char_ok(nm[i])) { + releasesleep(&v->lk); + return -EINVAL; + } + uint32 first = dir_first(v, dp); + if (find(v, first, nm, n, fp)) { + releasesleep(&v->lk); + return -EEXIST; + } + + uint t = now(); + uint16 d, tm; + to_fat(t, &d, &tm); + memset(e, 0, 32); + put16(e + 14, tm); put16(e + 16, d); put16(e + 18, d); + put16(e + 22, tm); put16(e + 24, d); + + uint64 old = (inum & TAG_MASK) == TAG_DIR ? 0 : pos_of(v, inum); + if ((inum & TAG_MASK) == TAG_DIR) { + // A new directory: its cluster came from ialloc, zeroed. Its own "." and + // ".." go in first; ".." names the parent's cluster, and 0 for the root + // even on FAT32, which is what the specification asks for. + uint32 c = (uint32)(inum & ~TAG_MASK); + uint32 pc = dp->inum == FAT_ROOT_INO ? 0 : first; + uchar dot[32]; + memmove(dot, e, 32); + memset(dot, ' ', 11); + dot[0] = '.'; + dot[11] = A_DIR; + set_clus(dot, c); + vio(v, clus_off(v, c), dot, 32, 1); + dot[1] = '.'; + set_clus(dot, pc); + vio(v, clus_off(v, c) + 32, dot, 32, 1); + e[11] = A_DIR; + set_clus(e, c); + } else if (old && vio(v, old, e, 32, 0) == 0 && e[0] != 0xE5 && e[0] != 0) { + // A second name for a file that has one: rename's first half. The entry + // is copied whole -- cluster, size, attributes, times -- and the old one + // goes when rename unlinks it. + } else { + e[11] = A_ARCHIVE; + } + + uint64 pos = dir_add(v, first, nm, n, e); + if (pos) + map_set(v, dp->dev, inum, pos); + write_fsinfo(v); + flush(v, 0); + releasesleep(&v->lk); + return pos ? 0 : -ENOSPC; +} + +static int +fat_dirunlink(struct inode *dp, char *name, uint off) +{ + struct fvol *v = vol_of(dp->dev); + int n; + + (void)off; + if (!v) + return -EIO; + if (v->ro) + return -EROFS; + + acquiresleep(&v->lk); + uint16 *nm = v->nm; + struct found *fp = &v->hit; + if ((n = vfs_name(name, nm)) < 0) { + releasesleep(&v->lk); + return -1; + } + uint32 first = dir_first(v, dp); + if (!find(v, first, nm, n, fp)) { + releasesleep(&v->lk); + return -1; + } + uint64 inum = inum_of(v, dp->dev, fp); + dir_del(v, first, fp); + // The inode loses its entry -- unless its number now lives somewhere + // else, which is the old name of a file being renamed. + if ((inum & TAG_MASK) == TAG_DIR) + map_del(v, inum); + else if (pos_of(v, inum) == fp->sfn_pos) + map_set(v, dp->dev, inum, 0); + flush(v, 0); + releasesleep(&v->lk); + return 0; +} + +static int +not_dot(struct fvol *v, struct found *f, void *arg) +{ + (void)v; (void)arg; + return !((f->nlen == 1 && f->name[0] == '.') || + (f->nlen == 2 && f->name[0] == '.' && f->name[1] == '.')); +} + +static int +fat_dirempty(struct inode *dp) +{ + struct fvol *v = vol_of(dp->dev); + if (!v) + return 1; + acquiresleep(&v->lk); + int any = dir_scan(v, dir_first(v, dp), 0, not_dot, 0, 0); + releasesleep(&v->lk); + return !any; +} + +static struct inode * +fat_ialloc(uint dev, short type) +{ + struct fvol *v = vol_of(dev); + uint64 inum; + + if (!v || v->ro) + return 0; + acquiresleep(&v->lk); + if (type == T_DIR) { + uint32 c = clus_alloc(v, 0); + write_fsinfo(v); + flush(v, 0); + releasesleep(&v->lk); + if (c == 0) + return 0; + inum = TAG_DIR | c; + } else { + inum = TAG_NEW | ++v->nextnew; + map_set(v, dev, inum, 0); + releasesleep(&v->lk); + } + return iget(dev, inum); +} + +// --- getdents64 -------------------------------------------------------------- +// +// The cookie is 0 for ".", 1 for "..", and 2 + the index of the next entry to +// look at after that -- always the start of a name, never the middle of one. + +struct gd { + uint dev; + char *out; + int cap, written; + uint32 next; +}; + +static int +gd_put(struct gd *g, uint64 ino, int dtype, const char *name, int nlen, + uint64 cookie) +{ + int reclen = (19 + nlen + 1 + 7) & ~7; + if (g->written + reclen > g->cap) + return -1; + char *k = g->out + g->written; + *(uint64 *)k = ino; + *(uint64 *)(k + 8) = cookie; + *(uint16 *)(k + 16) = (uint16)reclen; + *(uint8 *)(k + 18) = dtype; + memmove(k + 19, name, nlen); + memset(k + 19 + nlen, 0, reclen - 19 - nlen); + g->written += reclen; + return 0; +} + +static int +gd_entry(struct fvol *v, struct found *f, void *arg) +{ + struct gd *g = arg; + char *name = v->u8; + + if (!not_dot(v, f, 0)) { + g->next = f->sfn_i + 1; + return 0; + } + int n = utf16_to_8(f->name, f->nlen, name, sizeof(v->u8)); + int dir = (f->e[11] & A_DIR) != 0; + if (gd_put(g, inum_of(v, g->dev, f), dir ? 4 : 8, name, n, 2 + f->sfn_i + 1) < 0) + return 1; + g->next = f->sfn_i + 1; + return 0; +} + +static int +fat_getdents(struct inode *dp, char *out, int cap, uint64 *off) +{ + struct fvol *v = vol_of(dp->dev); + struct gd g = { dp->dev, out, cap, 0, 0 }; + + if (!v) + return -EIO; + if (*off == 0) { + if (gd_put(&g, dp->inum, 4, ".", 1, 1) < 0) + return -EINVAL; + *off = 1; + } + if (*off == 1) { + if (gd_put(&g, dp->inum, 4, "..", 2, 2) < 0) + return g.written ? g.written : -EINVAL; + *off = 2; + } + + acquiresleep(&v->lk); + uint32 end = 0; + g.next = (uint32)(*off - 2); + int stopped = dir_scan(v, dir_first(v, dp), g.next, gd_entry, &g, &end); + releasesleep(&v->lk); + + if (stopped && g.written == 0) + return -EINVAL; // one name bigger than the whole buffer + *off = 2 + (stopped ? g.next : end); + return g.written; +} + +const struct fs_ops fat_ops = { + .name = "vfat", + .iload = fat_iload, + .istore = fat_istore, + .ifree = fat_ifree, + .read = fat_read, + .write = fat_write, + .truncate = fat_truncate, + .dirlookup = fat_dirlookup, + .dirlink = fat_dirlink, + .dirunlink = fat_dirunlink, + .dirempty = fat_dirempty, + .ialloc = fat_ialloc, + .getdents = fat_getdents, + // Files and directories, and one name each. A device node, a symlink or a + // socket has nowhere to go in a FAT entry, and neither has a second name + // for the same clusters -- a hard link here would be what fsck calls a + // cross-linked file. + .refuse_types = (1 << T_DEV) | (1 << T_SYMLINK) | (1 << T_SOCK), + .no_hardlinks = 1, +}; + +// --- mounting ---------------------------------------------------------------- + +static void +release_lines(struct fvol *v) +{ + for (struct line *l = v->line; l < v->line + NLINE; l++) { + if (l->data) + kfree((char *)l->data); + l->data = 0; + l->valid = l->dirty = 0; + } +} + +// The boot sector's "volume dirty" bit, which Linux sets for as long as it +// has a volume mounted read-write and clears when it lets go, and which +// Windows and fsck.fat read as "this was not cleanly unmounted". +static void +mark(struct fvol *v, int dirty) +{ + uchar b; + if (!v->state_off || v->ro || vio(v, v->state_off, &b, 1, 0) < 0) + return; + b = dirty ? (b | 1) : (b & ~1); + vio(v, v->state_off, &b, 1, 1); +} + +static int +probe(struct fvol *v) +{ + uchar bs[512]; + + if (vio(v, 0, bs, 512, 0) < 0) + return -1; + if (bs[510] != 0x55 || bs[511] != 0xAA || (bs[0] != 0xEB && bs[0] != 0xE9)) + return -1; + + uint32 bps = le16(bs + 11), spc = bs[13], rsvd = le16(bs + 14); + uint32 nfats = bs[16], rootents = le16(bs + 17); + uint32 tot = le16(bs + 19) ? le16(bs + 19) : le32(bs + 32); + uint32 fatsz = le16(bs + 22) ? le16(bs + 22) : le32(bs + 36); + + if ((bps != 512 && bps != 1024 && bps != 2048 && bps != 4096) || + bps < v->secsize || spc == 0 || (spc & (spc - 1)) || rsvd == 0 || + nfats == 0 || nfats > 4 || fatsz == 0 || tot == 0 || + (uint64)bps * spc > 64 * 1024) + return -1; + + uint32 rootsecs = (rootents * 32 + bps - 1) / bps; + uint64 meta = rsvd + (uint64)nfats * fatsz + rootsecs; + if (meta >= tot) + return -1; + uint32 nclus = (uint32)((tot - meta) / spc); + + // The type is decided by the cluster count and by nothing else -- not the + // label in the boot sector, which is a comment. This is the specification's + // own rule, and it is what makes a FAT16 volume that says "FAT32" FAT16. + v->fat = nclus < 4085 ? 12 : nclus < 65525 ? 16 : 32; + if (v->fat == 32 && le16(bs + 22) != 0) + return -1; + if (v->fat != 32 && rootents == 0) + return -1; + + v->clsize = bps * spc; + v->nclus = nclus; + v->fat_bytes = (uint64)fatsz * bps; + v->fat_off = (uint64)rsvd * bps; + v->nfats = nfats; + v->root_off = v->fat_off + nfats * v->fat_bytes; + v->root_ents = v->fat == 32 ? 0 : rootents; + v->data_off = v->root_off + (uint64)rootsecs * bps; + v->free_count = FREE_UNKNOWN; + v->next_free = 2; + v->fsinfo_off = 0; + v->state_off = 0; + + // A table bigger than the volume, or one too small for its clusters, is + // not a table to write into. + uint64 need = v->fat == 32 ? (uint64)(nclus + 2) * 4 : + v->fat == 16 ? (uint64)(nclus + 2) * 2 : ((uint64)(nclus + 2) * 3 + 1) / 2; + if (need > v->fat_bytes || v->data_off + (uint64)nclus * v->clsize > v->bytes) + return -1; + + if (v->fat == 32) { + uint32 ext = le16(bs + 40); + v->root_clus = le32(bs + 44); + if (!valid_clus(v, v->root_clus)) + return -1; + // Mirroring off: only the active copy is live, and only it is written. + if (ext & 0x80) { + v->fat_off += (ext & 0x0F) * v->fat_bytes; + v->nfats = 1; + } + uint32 fsi = le16(bs + 48); + uchar fi[512]; + if (fsi && fsi < rsvd && vio(v, (uint64)fsi * bps, fi, 512, 0) == 0 && + le32(fi) == 0x41615252 && le32(fi + 484) == 0x61417272) { + v->fsinfo_off = (uint64)fsi * bps; + if (le32(fi + 488) <= nclus) + v->free_count = le32(fi + 488); + if (valid_clus(v, le32(fi + 492))) + v->next_free = le32(fi + 492); + } + if (bs[66] == 0x29) + v->state_off = 65; + } else if (bs[38] == 0x29) { + v->state_off = 37; + } + return 0; +} + +int +fat_kmount(uint bdev, int readonly) +{ + uint32 secsize = 0; + uint64 nsec = blkdev_sectors(bdev, &secsize); + int slot = -1; + + if (nsec == 0) + return -ENODEV; + if (!slotinit) { + initlock(&slotlk, "fatslots"); + slotinit = 1; + } + acquire(&slotlk); + for (int i = 0; i < FAT_VOL_SLOTS; i++) { + if (vols[i].used && vols[i].bdev == bdev) { + release(&slotlk); + return -EBUSY; + } + if (!vols[i].used && slot < 0) + slot = i; + } + if (slot < 0) { + release(&slotlk); + return -EBUSY; + } + struct fvol *v = &vols[slot]; + memset(v, 0, sizeof(*v)); + v->used = 2; // claimed, not yet usable + release(&slotlk); + + initsleeplock(&v->lk, "fat"); + v->bdev = bdev; + v->secsize = secsize; + v->bytes = nsec * secsize; + v->ro = readonly; + if (probe(v) < 0) { + release_lines(v); + v->used = 0; + return -ENODEV; + } + + uchar st = 0; + if (v->state_off && vio(v, v->state_off, &st, 1, 0) == 0 && (st & 1)) + cprintf("fat: %s was not cleanly unmounted; some data may be damaged. " + "Run fsck.fat on it.\n", blkdev_name(bdev)); + mark(v, 1); + flush(v, 1); + v->used = 1; + kinfo("fat: %s: FAT%d, %d KB clusters, %d MB%s\n", blkdev_name(bdev), + v->fat, (int)(v->clsize / 1024), + (int)(((uint64)v->nclus * v->clsize) >> 20), v->ro ? ", read-only" : ""); + return FAT_DEV + slot; +} + +int +fat_kumount(uint dev) +{ + struct fvol *v = vol_of(dev); + if (!v) + return -EINVAL; + acquiresleep(&v->lk); + write_fsinfo(v); + mark(v, 0); + flush(v, 1); + release_lines(v); + v->nmap = 0; + releasesleep(&v->lk); + v->used = 0; + return 0; +} + +int +fat_readonly(uint dev) +{ + struct fvol *v = vol_of(dev); + return v ? v->ro : 0; +} + +void +fat_ksync_all(int final) +{ + for (int i = 0; i < FAT_VOL_SLOTS; i++) { + struct fvol *v = vol_of(FAT_DEV + i); + if (!v || v->ro) + continue; + acquiresleep(&v->lk); + write_fsinfo(v); + if (final) + mark(v, 0); + flush(v, 1); + releasesleep(&v->lk); + } +} + +int +fat_kstatfs(uint dev, uint64 *bsize, uint64 *total, uint64 *freec) +{ + struct fvol *v = vol_of(dev); + if (!v) + return -1; + acquiresleep(&v->lk); + if (v->free_count == FREE_UNKNOWN) { + v->free_count = count_free(v); + v->info_dirty = 1; + } + *bsize = v->clsize; + *total = v->nclus; + *freec = v->free_count; + releasesleep(&v->lk); + return 0; +} diff --git a/kernel/fs/file.c b/kernel/fs/file.c index b60b3ec..12f4937 100644 --- a/kernel/fs/file.c +++ b/kernel/fs/file.c @@ -276,11 +276,16 @@ filewrite(struct file *f, char *addr, int n) if(r < 0) break; - if(r != n1) - panic("short filewrite"); 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; } - return i == n ? n : -1; + if(i > 0) + return i; + return r < 0 ? r : -1; } panic("filewrite"); } diff --git a/kernel/fs/fs.c b/kernel/fs/fs.c index 8a749a4..702e32a 100644 --- a/kernel/fs/fs.c +++ b/kernel/fs/fs.c @@ -43,6 +43,7 @@ #include "buf.h" #include "fsstat.h" #include "file.h" +#include "fat.h" extern uint uptime(void); @@ -277,6 +278,7 @@ static const struct fs_ops v1_ops = { // Defined in fs/schfs/vfs.c, which is only linked when SchFS is built in. extern const struct fs_ops schfs_ops; +extern const struct fs_ops fat_ops; // Defined in fs/procfs.c -- the synthetic /proc. extern const struct fs_ops procfs_ops; extern const struct fs_ops devfs_ops; @@ -290,7 +292,7 @@ extern const struct fs_ops devfs_ops; int fs_readonly(uint dev) { - return SCHFS_IS_SNAP_DEV(dev); + return SCHFS_IS_SNAP_DEV(dev) || (FAT_IS_DEV(dev) && fat_readonly(dev)); } static const struct fs_ops * @@ -302,6 +304,7 @@ ops_for(uint dev) if (SCHFS_IS_ANY_DEV(dev)) return &schfs_ops; if (dev == PROCFS_DEV) return &procfs_ops; if (dev == DEVFS_DEV) return &devfs_ops; + if (FAT_IS_DEV(dev)) return &fat_ops; return &v1_ops; } @@ -468,6 +471,38 @@ iget(uint dev, uint64 inum) return ip; } +// Is (dev, inum) in the inode cache with a reference? For a filesystem that +// keeps its own table of inode numbers and wants to know which it can drop. +int +iheld(uint dev, uint64 inum) +{ + int held = 0; + acquire(&itable.lock); + for (struct inode *ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++) + if (ip->ref > 0 && ip->dev == dev && ip->inum == inum) { + held = 1; + break; + } + release(&itable.lock); + return held; +} + +// Is anything on dev held, other than `except` (a mounted root, which the +// mount itself holds)? For umount. +int +ibusy(uint dev, uint64 except) +{ + int busy = 0; + acquire(&itable.lock); + for (struct inode *ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++) + if (ip->ref > 0 && ip->dev == dev && ip->inum != except) { + busy = 1; + break; + } + release(&itable.lock); + return busy; +} + struct inode * idup(struct inode *ip) { diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 6e3f0ca..0d0770b 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -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; } diff --git a/kernel/sys/sys_proc.c b/kernel/sys/sys_proc.c index c65df6d..592e88e 100644 --- a/kernel/sys/sys_proc.c +++ b/kernel/sys/sys_proc.c @@ -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);