diff --git a/include/sfs.h b/include/sfs.h new file mode 100644 index 0000000..67807af --- /dev/null +++ b/include/sfs.h @@ -0,0 +1,226 @@ +#ifndef FROSTBYTE_SFS_H +#define FROSTBYTE_SFS_H + +#include "types.h" + +/* + * SFS ("simple filesystem"): a real, persistent, inode-based filesystem + * living on the ATA block device (ata.h), starting at LBA sfsBaseLba. Unlike + * the VFS tmpfs tree (vfs.c), which is pure kmalloc state that evaporates on + * reboot, SFS state (superblock, bitmaps, inode table, file data) is fully + * on disk: sfsUnmount()+sfsMount() drops every in-memory structure and + * reconstructs it by reading the disk, the same way a real filesystem driver + * would after a remount. + * + * Block size equals the sector size (512 bytes), so every filesystem block + * is exactly one ATA sector transfer. sfsBaseLba (100) and the reserved + * tail (16 sectors) keep this filesystem's LBA range clear of the raw + * sector ranges ataTest.c/disktest.c already poke directly, so the two + * layers never fight over the same bytes even though they share one disk. + */ + +#define sfsBlockSize 512 +#define sfsBaseLba 100 +#define sfsReserveEnd 16 +#define sfsMaxNodes 512 +#define sfsRootInode 1 +#define sfsNameMax 55 +#define sfsMaxSymlinkHops 8 +#define sfsSymlinkTargetMax 255 + +#define sfsTypeFree 0 +#define sfsTypeFile 1 +#define sfsTypeDir 2 +#define sfsTypeSymlink 3 +#define sfsTypeFifo 4 + +/* Formats the SFS region from scratch: fresh superblock, zeroed bitmaps and + inode table, empty root directory. Destroys any existing SFS content in + that LBA range. Returns false if no drive is present or the drive is too + small to hold the fixed-size metadata. */ +bool sfsFormat(void); + +/* Mounts the existing on-disk filesystem, auto-formatting on first use (bad + magic). Loads the inode/block bitmaps into memory; everything else (inode + table, file data, directory entries) is read from disk on demand. */ +bool sfsMount(void); + +/* Flushes bitmaps/superblock to disk and frees in-memory state. A later + sfsMount() reconstructs everything purely from what is on disk. */ +void sfsUnmount(void); + +bool sfsMounted(void); + +/* Total bytes usable for file data (excludes reserved metadata blocks). */ +uint64 sfsCapacityBytes(void); + +/* Data blocks not currently allocated to any inode (bitmap free count). + Mainly useful for tests to prove truncate/remove actually gives blocks + back rather than merely detaching them from the inode. */ +uint64 sfsFreeDataBlocks(void); + +/* path is slash-separated and relative to the SFS root ("" or "/" is the + root itself). Returns the inode number (>=1) or -1 if not found. Note: + "." and ".." are not special-cased (deliberate scope limit: paths are + always resolved from the root down, no relative-to-self hops). + + A symlink encountered anywhere along the path - including the final + component - is transparently followed to its target and re-resolved from + the root (real open()/stat() semantics), up to sfsMaxSymlinkHops hops; + beyond that (a cycle, or a chain that is simply too long) resolution fails + with -1 instead of recursing forever. Use sfsLookupNoFollow to inspect a + symlink itself rather than what it points at. */ +int32 sfsLookup(const char* path); + +/* Same resolution as sfsLookup, except a symlink at the FINAL path component + is returned as itself instead of being followed (the lstat()/readlink() + convention). Symlinks at any earlier component are still followed, since + there is no other way to keep descending through them. */ +int32 sfsLookupNoFollow(const char* path); + +int32 sfsCreateFile(const char* path); +int32 sfsCreateDir(const char* path); + +/* Creates a persistent named-pipe entry at `path`: a real, on-disk directory + entry of type sfsTypeFifo that survives sfsUnmount()+sfsMount() (and a + real reboot, unlike the VFS tmpfs-tree fifos vfsMkfifo already supports + under e.g. "/tmp") exactly the way a real filesystem's mkfifo(2) special + file does. The inode carries no data blocks -- sfsReadAt/sfsWriteAt + already refuse anything that is not sfsTypeFile/Dir/Symlink, so a fifo + inode simply has size 0 forever, matching real Unix (mkfifo persists the + NAME and TYPE of the rendezvous point, never any buffered bytes: those + only ever exist in memory while some process holds the pipe open, and + are gone the moment every process does, reboot or not). vfs.c is + responsible for the actually-live half of a fifo (attaching real + pipe.h byte-queue handles to opens of this path, see vfsOpen) -- + sfs.c itself stays entirely ignorant of pipe.h, exactly as it already is + of every other higher-layer concern. Returns the new inode number, or -1 + on the same failures as sfsCreateFile/sfsCreateDir (parent missing/not a + writable dir, name already taken, tables full). */ +int32 sfsCreateFifo(const char* path); + +/* Monotonically increasing counter, bumped exactly once every time + sfsFormat() actually wipes the on-disk filesystem (whether called + directly, e.g. every sfsTest.c test's own clean-slate setup, or + implicitly by sfsMount() the first time it sees a bad magic) -- never + bumped by a plain sfsMount()/sfsUnmount() cycle that finds a valid, + unchanged filesystem, since inode numbers keep meaning the same thing + across that kind of remount. Exists so a higher layer that caches + something keyed by inode number across calls (vfs.c's SFS fifo binding + table, see vfsOpen/vfsMkfifo/vfsUnlink in vfs.c) can tell "this cached + entry still refers to the same on-disk fifo I bound it to" apart from + "the whole filesystem underneath it was wiped and reformatted since, + inode 7 might be a completely different file/dir/fifo/nothing now" -- + without sfs.c ever having to know that cache exists. */ +uint64 sfsGeneration(void); + +/* Creates a symbolic link at `path` whose target is the literal string + `target` - never validated against anything, so a symlink may point at a + path that does not exist yet (or ever), exactly like a real filesystem. + Returns the new inode number, or -1 on failure (parent missing, name + already taken, target too long, or the inode/block tables are full). */ +int32 sfsCreateSymlink(const char* path, const char* target); + +/* Reads the target string of a symlink inode into buf, null-terminated. + Fails (returns false, buf left untouched) if inodeNum is not a symlink or + if the stored target does not fit in cap bytes including the terminator - + never silently truncates. */ +bool sfsReadlink(int32 inodeNum, char* buf, uint64 cap); + +/* Creates a second directory entry (newPath) for the same inode an existing + file or symlink resolves to (existingPath is followed through symlinks, + same as open()/stat() - the new name refers to whatever it ultimately + points at, not the symlink itself). Directories cannot be hardlinked + (matches POSIX link(2): would make the directory tree no longer a tree). + Returns the shared inode number, or -1 on failure (existingPath missing, + a directory, newPath's parent missing, or newPath already taken). */ +int32 sfsLink(const char* existingPath, const char* newPath); + +/* Number of directory entries currently pointing at this inode. 0 for an + invalid/unmounted lookup, otherwise always >=1 for anything sfsLookup can + still reach. */ +uint32 sfsLinkCountOf(int32 inodeNum); + +/* Removes a file, or a directory only if it is empty. A hardlinked file's + data and inode survive until its link count reaches zero -- only the + named directory entry is guaranteed to disappear immediately. */ +bool sfsRemove(const char* path); + +uint64 sfsSizeOf(int32 inodeNum); +uint32 sfsTypeOf(int32 inodeNum); + +/* Real wall-clock timestamps (rtc.h's rtcNow(), seconds since the Unix + epoch), stored on disk in the inode itself so they survive + sfsUnmount()+sfsMount() like everything else about the inode. mtime is + last content modification (sfsWriteAt, sfsTruncate); ctime is last inode + metadata change (mtime changes are also metadata changes, so every mtime + update bumps ctime too, plus link-count changes from sfsLink/sfsRemove + bump ctime alone, matching real inode semantics). Both are stamped at + creation, so a freshly created node has mtime == ctime. Return 0 for an + invalid/unmounted lookup, same convention as sfsSizeOf/sfsTypeOf. */ +uint64 sfsMtimeOf(int32 inodeNum); +uint64 sfsCtimeOf(int32 inodeNum); + +/* Real owner rwx permission bits (fs.h's fsModeOwner*), stored on disk in + the inode like every other field above -- survives sfsUnmount()+ + sfsMount(). Stamped at creation with a type-based default (createNode: + fsModeDefaultDir for a directory, fsModeDefaultFile for anything else) + and changed only by sfsChmod. sfsModeOf returns 0 for an invalid/ + unmounted lookup, same convention as sfsSizeOf/sfsTypeOf. sfsChmod + overwrites the whole value (no symbolic +x-style merge, matching real + chmod(2)'s own numeric-mode form) and bumps ctime like any other + metadata-only mutation (sfsLink's linkCount bump does the same) -- + returns false for an invalid/unmounted inodeNum, true otherwise. */ +uint32 sfsModeOf(int32 inodeNum); +bool sfsChmod(int32 inodeNum, uint32 mode); + +/* Real owner identity (fs.h's FbStat.uid), stored on disk like every other + inode field above -- survives sfsUnmount()+sfsMount(). Stamped once, at + creation (createNode: sched.h's currentThreadUid(), the CALLER's real + identity at the moment of the create/mkdir/symlink/mkfifo call), and + afterward only ever changed by sfsChown (below). sfsUidOf returns 0 for + an invalid/unmounted lookup, same convention as sfsModeOf. */ +uint32 sfsUidOf(int32 inodeNum); + +/* Real chown(2)'s on-disk half: overwrites the inode's owner uid outright + and bumps ctime like sfsChmod's own metadata-only mutation does. + Deliberately has NO privilege check of its own -- vfsChown (vfs.c) is the + one real choke point that enforces the actual chown(2) privilege rule + (root-only, real Unix's _POSIX_CHOWN_RESTRICTED default: not even a + file's own owner may give it away), exactly like sfsChmod leaves + ownership-vs-caller policy to vfsChmod. Returns false for an invalid/ + unmounted inodeNum, same convention as sfsChmod. */ +bool sfsChown(int32 inodeNum, uint32 newUid); + +/* Real group identity (fs.h's FbStat.gid), stored on disk like uid above -- + survives sfsUnmount()+sfsMount(). Stamped once, at creation (createNode: + sched.h's currentThreadGid()), and afterward only ever changed by + sfsChgrp (below). sfsGidOf returns 0 for an invalid/unmounted lookup, + same convention as sfsUidOf. */ +uint32 sfsGidOf(int32 inodeNum); + +/* Real chgrp(2)'s on-disk half: overwrites the inode's group gid outright + and bumps ctime, mirroring sfsChown's own shape exactly. Deliberately has + NO privilege check of its own -- vfsChgrp (vfs.c) is the real choke point + (owner may set only their own current gid, root may set any gid). Returns + false for an invalid/unmounted inodeNum, same convention as sfsChown. */ +bool sfsChgrp(int32 inodeNum, uint32 newGid); + +/* Grows or shrinks a file in place, freeing/allocating data blocks as + needed. */ +bool sfsTruncate(int32 inodeNum, uint64 newSize); + +/* Reads/writes inode-relative byte ranges. Writing past the current end of + file zero-fills the gap first (no sparse holes), then writes the payload, + growing the file and allocating blocks (direct, then single indirect, + then double indirect, then triple indirect) as needed. Returns bytes + transferred, or -1 on error (inode not a file, or the file has hit the + direct+indirect+double+triple addressing limit, roughly 1 GiB). */ +int64 sfsReadAt(int32 inodeNum, uint64 offset, void* buffer, uint64 length); +int64 sfsWriteAt(int32 inodeNum, uint64 offset, const void* buffer, uint64 length); + +/* Lists directory entries by index (0, 1, 2, ...). Returns false past the + last entry. */ +bool sfsReaddirAt(int32 dirInode, uint32 index, char* nameOut, uint32 nameCap, uint32* typeOut); + +#endif