Files
2026-09-02 12:12:43 +02:00

227 lines
12 KiB
C

#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