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);